develop

Micrometer Meter 관련 성능 이슈

레이니블루 2023. 10. 28. 22:56

Micrometer를 통한 metrics을 관리하고 있는데, Profiling 시 가장 많은 CPU를 소모하고 있어서 분석을 해 보았습니다.

meterRegistry.find("testMetricName").timer();

문제되는 코드인데, meterRegistry에서 단순 조회를 한다고 생각했는데 MeterRegistry.find()의 내부 구현은 다음과 같습니다.

Class MicrometerRegistry {
    public Search find(String name) {
       return Search.in(this).name(name);
    }
}

class Search {
    public Timer timer() {
        return findOne(Timer.class);
    }
    
    private <M extends Meter> M findOne(Class<M> clazz) {
        return meterStream().filter(clazz::isInstance).findAny().map(clazz::cast).orElse(null);
    }
    
    private Stream<Meter> meterStream() {
        Stream<Meter> meterStream = registry.getMeters().stream().filter(m -> nameMatches.test(m.getId().getName()));
        if (!tags.isEmpty() || !requiredTagKeys.isEmpty() || !tagMatches.isEmpty()) {
            meterStream = meterStream.filter(m -> isTagsMatched(m.getId()));
        }
        return meterStream;
    }
}

여기서부터 좀 고민됩니다. find()에서 Search.in()으로 Search 객체를 생성하는데, Timer를 얻어 올 때에 findOnce()의 구현 이 stream을 생성해서 iteration 하게 됩니다.

 

MicrometerRegistry의 구현은 다음과 같습니다.

class MeterRegistry {
    private final Map<Id, Meter> meterMap = new ConcurrentHashMap<>();
    
    public List<Meter> getMeters() {
        return Collections.unmodifiableList(new ArrayList<>(meterMap.values()));
    }

    /**
     * Iterate over each meter in the registry.
     * @param consumer Consumer of each meter during iteration.
     */
    public void forEachMeter(Consumer<? super Meter> consumer) {
        meterMap.values().forEach(consumer);
    }
}

객체 생성 관점에서 Search -> meterMap.values() -> ArrayList -> unmodifiableList -> stream 의 주요 객체가 생성되고 있어서 메모리 overhead가 상당하네요.

 

 

이 부분은 다음과 같이 수정했습니다. 일반적으로 사용할 수는 없고 제 서비스 특화된 구현이기 때문에 어디까지나 참고사항입니다.

Timer findTimer(List<Tag> tags) {
    AtomicReference<Timer> timer = new AtomicReference<>(null);
    meterRegistry.forEachMeter(it -> {
        if ("testMetricsName".equals(it.getId().getName()) && it instanceof Timer && isTagsMatched(it.getId(), tags)) {
            timer.set((Timer) it);
        }
    });
    return timer.get();
}

private boolean isTagsMatched(Meter.Id id, List<Tag> tags) {
    HashSet<Tag> set = new HashSet<>();
    id.getTagsAsIterable()
            .forEach(set::add);
    return set.containsAll(tags);
}

P.S. 위의 findTimer()에는 버그가 있습니다.