index

Timer

Timer는 애플리케이션의 작업이나 요청에 걸리는 시간을 측정하고 그 작업의 수행 빈도(호출 횟수)와 함께 분포 정보를 수집하는 메트릭 타입이다

평균, 최소, 최대, 백분위(percentile) 값 등을 파악할 수 있어 성능 분석과 병목 지점을 확인하는 데 유용하다

시간 측정

Timer는 작업(요청)이 시작되어 끝날 때까지의 경과 시간을 측정한다

모니터링 시스템 별 또는 도출하고자 하는 값의 종류에 따라 필요한 시간 단위가 다를 수 있기 때문에 micrometer는 특정 시간 단위를 제안하지 않는다

따라서 시간 측정 시 필요에 따른 시간 단위를 지정해야 한다

호출 횟수(count)와 총 소요 시간(total time)

Timer는 작업이 몇 번 실행되었는지와 모든 작업의 총 소요 시간을 동시에 기록한다

이 두 값으로 평균 응답 시간(total time % count)을 계산할 수 있다

분포 정보

Timer는 추가적으로 히스토그램이나 백분위(percentile)같은 통계치를 기록할 수 있다

이를 통해 작업의 지연 분포를 시각화하고 작업이 얼마나 빠르게 처리되는지, 지연이 발생하는지 파악할 수 있다

registry.timer, Timer.builder

아래와 같이 MeterRegistry의 timer 메서드 또는 Timer.builder를 통해 Timer 인스턴스를 생성할 수 있다

registry.timer("timer.sample", "type", "account")
timer = Timer.builder("timer.record")
                .description("time taken for a example task")
                .register(registry);

timer.record(), Timer.Sample

Timer 인스턴스를 활용해서 값을 측정하는 방법은 record 인스턴스 메서드 활용하는 방법과 Timer.Sample을 이용하는 방법이 있다

record 메서드는 Runnable 또는 Supplier<T>를 파라미터로 받으며, record 메서드의 호출 횟수와 코드를 실행하는 데 걸린 시간, 가장 오래 걸린 작업의 시간을 측정한다

public class RecordTimer {

    private final Timer timer;

    public RecordTimer(MeterRegistry registry) {
        timer = Timer.builder("timer.record")
                .description("time taken for a example task")
                .register(registry);
    }

    // timer.record(Runnable) 사용
    public void executeRunnableTask() {
        timer.record(() -> {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        });
    }

    // timer.record(Supplier<T>) 사용
    public String executeSupplierTask() {
        return timer.record(() -> {
            try {
                Thread.sleep(200);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }

            return "completed";
        });
    }

}

Timer.Sample을 이용하면 작업의 시작과 종료 시점을 명시적으로 제어하여 값을 측정할 수 있다

public class SampleTimer {

    private final MeterRegistry registry;
    private final Timer timer;
    private Timer.Sample sample;

    // Timer.Sample을 사용하면 작업의 시작과 종료 시점을 명시적으로 제어하여 측정할 수 있다
    public SampleTimer(MeterRegistry registry) {
        this.registry = registry;

        timer = Timer.builder("timer.sample")
                .description("time taken for a example task")
                .register(registry);
    }

    // sample의 시작 시간은 registry의 Clock을 기반으로 기록한다
    public void startTimer() {
        sample = Timer.start(registry);
    }

    public void doSomething() {
        try {
            Thread.sleep(300);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }

    public void stopTimer() {
        sample.stop(timer);
    }
}

@Timed

@Timed 어노테이션은 메서드의 실행 시간을 자동으로 측정하여 Timer 메트릭을 기록하는 데 사용된다

메서드 실행 전후로 자동으로 타이머를 시작하고 정지하여 실행 시간을 측정한다

기록된 데이터는 MeterRegistry에 누적되어 외부 모니터링 시스템에게 전달될 수 있으며 메서드 호출 빈도, 총 실행 시간, 평균 실행 시간, 백분위 등 다양한 성능 지표를 모니터링할 수 있다

public class TimedAOP {

    @Timed(value = "timer.timed", description = "time taken for a example task")
    public void executeTask() {
        try {
            Thread.sleep(300);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }
}

longTask 속성을 활성화하면 Timer 대신 Long Task Timer로 처리한다

public class TimedAOP {

    @Timed(value = "timer.timed.long", longTask = true, description = "long running task execution time")
    public void executeLongTask() {
        try {
            Thread.sleep(500);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }
}

@MeterTag

@Counted 어노테이션과 마찬가지로 @Timed 어노테이션을 사용할 때 메서드 파라미터에 @MeterTag를 적용하여 파라미터 값을 태그에 추가할 수 있다

public class TimedAOP {

    @Timed(value = "timer.timed.metertag")
    public void executeTask(@MeterTag(key = "name") String name) {
    }
}

FunctionTimer

FunctionTimer (function-tracking timer)는 애플리케이션 내에서 발생한 이벤트를 기록하기 위해 매번 record()를 호출하는 대신, 이미 누적된 값(총 실행 횟수와 총 실행 시간)을 직접 제공하는 객체의 상태를 기반으로 타이머 값을 계산한다

값을 제공하는 객체는 아래의 두 개의 메서드를 활용하여 평균 실행 시간 및 초당 이벤트 발생률을 계산한다

상태 객체의 필수 구성 메서드

용도

특징

다만 FunctionTimer는 참조한 객체의 상태를 기반으로 값을 계산하므로 해당 객체가 GC 대상이 되지 않도록 주의해야 한다

public class FunctionTimers {

    private final OperationStats stats;

    /*
        FunctionTimer는 빌더를 이용하여 생성할 수 있다
        첫 번째 인자: 메트릭 이름
        두 번째 인자: 통계 값 제공 객체
        세 번째 인자: count function
        네 번째 인자: total time function
        다섯 번째 인자: 시간 단위
     */
    public FunctionTimers(MeterRegistry registry) {
        stats = new OperationStats();

        FunctionTimer.builder("timer.function", stats, OperationStats::getCount, OperationStats::getTotalTime, TimeUnit.NANOSECONDS)
                .description("a function-tracking timer that measures operation durations")
                .register(registry);
    }

    public void executeTask() {
        stats.record(10);
    }

    public void executeOther() {
        stats.record(100);
    }

    // 상태 객체는 내부적으로 작업 횟수(count)와 작업에 걸린 총 시간(totalTimeInNanos)을 업데이트한다
    private static class OperationStats {
        private long count;
        private long totalTimeInNanos;

        public void record(long durationInNanos) {
            count++;
            totalTimeInNanos += durationInNanos;
        }

        public long getCount() {
            return count;
        }

        public double getTotalTime() {
            return totalTimeInNanos;
        }
    }
}

pause detection

마이크로미터의 Timer는 애플리케이션의 응답 시간이나 작업 수행 시간 등 지연(latency)을 측정하는 데 사용되는데, 실제 시스템에서는 jvm이나 운영체제의 일시적인 정지(gc, 스케줄러 지연 등)로 인해 실제 응답 시간이 더 길어졌음에도 불구하고 측정된 지연 값이 낮게 나타날 수 있다

이를 coordinated omission 문제라고 하는데, 마이크로미터는 이러한 문제를 보정하기 위해 pause detection 기능을 제공한다

이 기능은 LaytencyUtils 패키지를 사용하여 시스템 정지로 인해 발생하는 추가 지연을 보정하여 백분위, slo(서비스 수준 목표) 계산 등 분포 통계에 반영된다

마이크로미터에서 제공하는 두 개의 pause detector 구현체 중 필요에 따라 선택하여 기능을 사용할 수 있다

clock-drift based detector

마이크로미터 1.0.10/1.1.4/1.2.0 이전 버전에 기본적으로 사용되는 구현체로 시스템의 시계(clock)와 실제 경과 시간 사이의 차이(drift)를 감지하여 시스템 정지로 인한 지연을 보장한다

또한 다음 두 가지 옵션을 설정할 수 있다

공식 문서에서는 두 값 모두 100ms 정도로 설정하면 긴 pause 이벤트를 감지하면서도 cpu 사용량은 최소화할 수 있다고 한다

no-op detector

마이크로미터 1.0.10/1.14/1.2.0 이후 버전에서 기본적으로 사용되는 구현체로 pause detection 기능을 적용하지 않는다

configuration

MeterRegistry의 config에서 아래와 같이 pause detector 구현체를 지정할 수 있다

// clock-drift detector 사용
registry.config().pauseDetector(new ClockDriftPauseDetector(sleepInterval, pauseThreshold));

// no-op detector 사용
registry.config().pauseDetector(new NoPauseDetector());