포스트

OpenTelemetry + Jaeger로 분산 추적 — 시세 파이프라인 지연 측정

분산 시스템에서 “느리다”를 어떻게 찾는가

monticker의 시세 파이프라인은 여러 컴포넌트를 거친다.

1
Go Gateway → Kafka → Worker → Redis → API → WebSocket → Client

“응답이 느리다”는 신고를 받았을 때, 어디가 병목인지 찾으려면 각 단계의 시간을 측정해야 한다. 로그를 수동으로 분석하는 방법은 느리고 오류가 많다.

분산 추적(Distributed Tracing) 은 하나의 요청이 여러 서비스를 거치는 동안 전체 경로와 각 구간의 시간을 자동으로 기록한다.


의존성 추가

1
2
3
4
5
6
7
8
9
10
// build.gradle.kts
dependencies {
    // OpenTelemetry SDK
    implementation("io.opentelemetry:opentelemetry-sdk:1.38.0")
    implementation("io.opentelemetry:opentelemetry-exporter-otlp:1.38.0")

    // Spring Boot Micrometer 연동
    implementation("io.micrometer:micrometer-tracing-bridge-otel:1.3.0")
    implementation("io.opentelemetry.instrumentation:opentelemetry-spring-boot-starter:2.5.0")
}
1
2
3
4
5
6
7
8
9
# application.yml
management:
  tracing:
    enabled: true
    sampling:
      probability: 1.0     # 개발 환경: 모든 요청 추적. 운영: 0.1 (10%)
  otlp:
    tracing:
      endpoint: http://jaeger:4318/v1/traces

자동 계측 (Auto-instrumentation)

Spring Boot의 opentelemetry-spring-boot-starter를 추가하면 추가 코드 없이 자동으로 계측된다.

  • HTTP 요청: 모든 @RestController 메서드에 span 자동 생성
  • JDBC: JdbcTemplate 쿼리에 span 자동 생성 (SQL, 실행 시간)
  • Spring Kafka: @KafkaListener 메서드에 span 자동 생성

수동 계측 — 시세 파이프라인 지연 추적

자동 계측이 안 되는 부분은 수동으로 span을 추가한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@Service
class LatencyTracker(
    private val tracer: Tracer,
    private val meterRegistry: MeterRegistry,
) {
    fun recordTickGenerated(stockId: Long, generatedAt: Instant) {
        val latencyMs = Duration.between(generatedAt, Instant.now()).toMillis()

        // Micrometer 타이머 기록 (Prometheus/Grafana용)
        meterRegistry.timer("tick.pipeline.latency",
            "stage", "kafka_consume",
            "market", getMarket(stockId)
        ).record(latencyMs, TimeUnit.MILLISECONDS)
    }

    fun <T> tracedBlock(spanName: String, block: () -> T): T {
        val span = tracer.spanBuilder(spanName).startSpan()
        return try {
            span.makeCurrent().use { block() }
        } catch (e: Exception) {
            span.recordException(e)
            span.setStatus(StatusCode.ERROR)
            throw e
        } finally {
            span.end()
        }
    }
}

시세 처리 파이프라인에 span을 추가한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@KafkaListener(topics = ["market.ticks"])
fun onTick(record: ConsumerRecord<String, String>) {
    val tick = objectMapper.readValue(record.value(), GeneratedTick::class.java)
    latencyTracker.recordTickGenerated(tick.stockId, tick.generatedAt)

    latencyTracker.tracedBlock("redis.write") {
        redisTickWriter.write(tick)
    }

    latencyTracker.tracedBlock("candle.aggregate") {
        candleAggregator.onTick(tick)
    }

    latencyTracker.tracedBlock("event.detect") {
        eventDetector.detect(tick)
    }
}

Jaeger UI에서 보는 것

Jaeger에 저장된 trace를 UI에서 확인한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
[요청 추적: POST /api/matching/orders]
  │
  ├── HTTP /api/matching/orders [12ms]
  │     ├── RiskChecker.preCheck [2ms]
  │     │     ├── JDBC: SELECT realized_pnl... [0.8ms]
  │     │     └── JDBC: SELECT COUNT(*)... [0.5ms]
  │     │
  │     ├── OrderBook.submit [1ms]
  │     │
  │     └── LedgerService.recordFill [3ms]
  │           └── JDBC: INSERT INTO ledger_events [2ms]

[시세 파이프라인 추적: market.ticks → Redis]
  │
  ├── Kafka consume [0.5ms]
  ├── redis.write [0.8ms]
  ├── candle.aggregate [1.2ms]
  └── event.detect [2.1ms]
  │
  Total pipeline latency: 4.6ms

지연 API 엔드포인트

시세 파이프라인의 실시간 지연을 조회하는 API를 제공한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@RestController
@RequestMapping("/api/latency")
class LatencyController(private val latencyTracker: LatencyTracker) {

    @GetMapping
    fun getLatency(): LatencyStats {
        return LatencyStats(
            kafkaToRedis   = latencyTracker.getPercentile("tick.pipeline.latency", 0.99),
            p50LatencyMs   = latencyTracker.getPercentile("tick.pipeline.latency", 0.50),
            p95LatencyMs   = latencyTracker.getPercentile("tick.pipeline.latency", 0.95),
            p99LatencyMs   = latencyTracker.getPercentile("tick.pipeline.latency", 0.99),
            sampleCount    = latencyTracker.getSampleCount("tick.pipeline.latency"),
            measuredAt     = Instant.now(),
        )
    }
}

응답 예시:

1
2
3
4
5
6
{
  "p50LatencyMs": 2.3,
  "p95LatencyMs": 8.1,
  "p99LatencyMs": 15.4,
  "sampleCount": 12420
}

Docker Compose Jaeger 설정

1
2
3
4
5
6
7
jaeger:
  image: jaegertracing/all-in-one:1.57
  ports:
    - "16686:16686"    # Jaeger UI
    - "4318:4318"      # OTLP HTTP
  environment:
    COLLECTOR_OTLP_ENABLED: "true"

브라우저에서 http://localhost:16686에 접속하면 Jaeger UI를 볼 수 있다.


Micrometer와 OpenTelemetry의 관계

Spring Boot에서 두 가지가 공존한다.

  • Micrometer: 메트릭 수집 (카운터, 타이머, 게이지). Prometheus, Grafana에 쿼리
  • OpenTelemetry: 분산 추적. Jaeger에서 시각화

Micrometer Tracing Bridge를 사용하면 Micrometer API로 작성한 코드가 자동으로 OpenTelemetry span을 생성한다.

1
2
3
// Micrometer Timer API
val timer = meterRegistry.timer("api.request", "endpoint", "/api/orders")
timer.record { processOrder() }  // 자동으로 OTel span도 생성

정리

  • opentelemetry-spring-boot-starter로 HTTP/JDBC/Kafka를 코드 변경 없이 자동 계측한다.
  • 직접 작성한 블록에는 tracer.spanBuilder()로 수동 span을 추가한다.
  • Micrometer Timer로 p50/p95/p99 지연 퍼센타일을 측정해 /api/latency로 노출한다.
  • sampling.probability: 1.0은 개발 환경용. 운영에서는 0.1 이하로 줄여야 한다.

마지막 편에서는 KIS·Yahoo Finance 같은 외부 API 장애 시 자동으로 폴백하는 Circuit Breaker를 다룬다.

  1. 1 가격이 아니라 이벤트를 팔자 — monticker 설계 철학
  2. 2 모듈식 모놀리스를 선택한 이유 — MSA의 유혹을 거부하기
  3. 3 TimescaleDB를 시계열 DB로 고른 이유 — Hypertable과 연속 집계
  4. 4 Go goroutine으로 202개 종목 동시 수집하기 — Market Gateway 설계
  5. 5 Kafka로 시세 파이프라인 분리하기 — 토픽 설계와 at-least-once
  6. 6 Netty로 수만 연결에 시세 브로드캐스트하기 — NioEventLoopGroup 리액터 패턴
  7. 7 EMA 기반 이상 탐지 — 가격 급등과 거래량 서지 실시간 감지
  8. 8 TreeMap으로 CLOB 호가창 구현하기 — 가격/시간 우선 매칭과 슬리피지
  9. 9 주문 전 동기 리스크 게이트 설계 — VaR, 집중도, 일일손실 5가지 규칙
  10. 10 잔고를 저장하지 말고 재구성하라 — 이벤트 소싱 원장 설계
  11. 11 감정 태그 × 수익률 — 투자 습관을 데이터로 기록하기
  12. 12 룰 엔진: RSI·MACD 조건식을 JSON DSL로 — Quant Lab 설계
  13. 13 백테스트 엔진: look-ahead 없는 시뮬레이션 — Sharpe·MDD·PF 계산
  14. 14 전략 지문(SHA-256)으로 룰셋 보호하기 — 서버 사이드 실행과 역공학 방어
  15. 15 Markowitz 최적화를 솔버 없이 구현하기 — 프로젝션 경사하강법
  16. 16 Kelly Criterion: 수학적 파산 방지 베팅 비율 — Half Kelly와 백테스트 연동
  17. 17 ZigZag + 패턴 템플릿 매칭으로 차트 패턴 감지 — 헤드앤숄더·이중바닥
  18. 18 ADX로 시장 국면 분류하기 — BULL·BEAR·SIDEWAYS·HIGH_VOL
  19. 19 손익통산으로 세금 줄이기 — Tax-Loss Harvesting 시뮬레이션
  20. 20 MockK로 JdbcTemplate 목킹하기 — 311개 테스트 작성 경험
  21. 21 OpenTelemetry + Jaeger로 분산 추적 — 시세 파이프라인 지연 측정
  22. 22 Circuit Breaker로 외부 API 장애 격리 — Resilience4j + KIS·Yahoo 폴백 체인
이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.

댓글

아직 댓글이 없습니다