Tempo Ingester Flush Mechanism & OOM

1. 정의 (Definition)

Tempo Ingester가 메모리에 데이터를 쌓아두는 방식(Head Block)과 이를 디스크(WAL/Backend)로 내리는 조건(Flush)에 대한 메커니즘입니다. 특히 **Active Trace(진행 중인 트레이스)**가 Flush 조건(max_block_bytes)을 무시하고 메모리를 계속 점유할 수 있는 구조적 이유를 설명합니다.

2. 핵심 메커니즘 (How it Works)

Ingester는 들어오는 Span 데이터를 Head Block이라는 메모리 공간에 저장합니다. 이 Head Block은 다음 조건 중 하나를 만족할 때 Cut (완료 처리)되고 디스크(WAL)로 Flush 됩니다.

Flush Trigger 조건

  1. max_block_bytes (Size Limit): 블록에 쌓인 “완료된(Complete) Trace들의 총합” + 기타 메타데이터 크기가 설정값(예: 64MB)을 초과할 때.
  2. max_block_duration (Time Limit): 블록이 생성된 지 일정 시간(예: 30분)이 지났을 때.
  3. Shutdown: Ingester가 종료될 때.

”Active Trace”의 함정 (The Pitfall)

가장 중요한 점은 “아직 Span이 계속 들어오고 있는(Active) Trace”는 완료된 것으로 간주되지 않아 Block Size 계산에서 제외되거나, Block이 Cut되더라도 다음 Block으로 이월(Carry-over)될 수 있다는 점입니다.

Why? Trace ID가 같은 Span들은 하나의 Trace 객체로 묶여야 하므로, Trace가 끝났다는 신호(Timeout 등)가 오기 전까지는 메모리에서 해제할 수 없습니다.

3. 시각화 (Visualization)

sequenceDiagram
    participant Client
    participant Ingester(Mem)
    participant WAL(Disk)

    Note over Ingester(Mem): Head Block 생성 (Size: 0)

    Client->>Ingester(Mem): Trace A (10MB) 전송
    Note over Ingester(Mem): Trace A: Active (Mem: 10MB)

    Client->>Ingester(Mem): Trace B (5MB) 전송
    Note over Ingester(Mem): Trace B: Active (Mem: 15MB)
    
    Client->>Ingester(Mem): Trace B 완료 (End)
    Note over Ingester(Mem): Trace B: Complete (Flush 대상)

    Client->>Ingester(Mem): Trace A (100MB) 추가 전송 (누적)
    Note over Ingester(Mem): Trace A: Active (Mem: 115MB)
    
    Note over Ingester(Mem): max_block_bytes(64MB) 초과 감지!
    
    Ingester(Mem)->>WAL(Disk): Flush 시도 (Block Cut)
    
    Note right of Ingester(Mem): Trace B는 Flush됨.<br/>하지만 Trace A는 아직 Active 상태라<br/>메모리에 남아있거나 다음 블록으로 넘어감.
    
    Note over Ingester(Mem): **결국 메모리는 해제되지 않음 (OOM 위험)**

4. OOM 발생 원인 (Root Cause of OOM)

CashtalkAPI 사례처럼, 클라이언트가 하나의 Trace ID로 작은 Span을 끊임없이(수만 번) 보내면 다음과 같은 일이 벌어집니다.

  1. Ingester는 해당 Trace를 “진행 중”으로 판단.
  2. max_block_bytes (64MB)가 차서 Flush를 시도함.
  3. 다른 완료된 Trace들은 Flush 되고 메모리에서 비워짐.
  4. 하지만 100MB가 넘는 거대 Active Trace는 메모리에 그대로 남음.
  5. 이 상태에서 또 다른 거대 Trace가 들어오거나, 메모리 사용량이 Limit(16GB)을 치면 OOM Killed.

5. 결론 및 대응

Tempo의 max_block_bytes 설정은 **“여러 개의 작은 Trace들이 쌓이는 것”**을 방어하는 기제이지, **“하나의 거대한 Trace”**를 자르는 가위가 아닙니다.

따라서 이를 막으려면:

  1. Ingestion Rate Limit: 데이터 유입 속도 자체를 제한 (ingestion_rate_limit_bytes).
  2. External Proxy: NGINX 등에서 HTTP Body Size를 원천 차단.
  3. Client Fix: 클라이언트가 불필요하게 큰 Trace를 만들지 않도록 수정.

References