콘텐츠로 이동

로그 시스템 아키텍처

상태

이 문서는 Continuum Router의 유계(bounded) 비동기 프로세스 로그 파이프라인이 사용하는 아키텍처를 정의합니다. 이 구현은 재시작이 필요한 logging.queue_capacity(기본값 8,192개 레코드)와 logging.queue_capacity_bytes(기본값 16 MiB), 그리고 레코드당 고정 64 KiB 상한과 1초의 정상 종료(graceful-shutdown) 예산을 사용합니다.

운영자는 continuum_process_log_dropped_total, continuum_process_log_write_errors_total, continuum_process_log_truncated_total과 continuum_process_log_queue_depth, continuum_process_log_queue_bytes 게이지를 통해 손실과 저하 상태를 관찰할 수 있습니다.

기본적으로 로그는 stdout이나 stderr로 전달되어 컨테이너 런타임, systemd/journald, 또는 다른 감독 프로세스가 영속성과 로테이션을 담당합니다. 선택적인 logging.file 블록을 설정하면 라우터가 직접 관리하는 파일 싱크를 사용하며 동작 방식은 다음과 같습니다.

  • 로테이션 및 권한: 완결된 레코드 사이에서 rotation.max_bytes 지점마다 로테이션하고 rotation.max_files개의 일치하는 파일을 보존하고(rotation.max_age_days를 설정하면 그보다 오래된 파일은 보존하지 않음), 기존 파일 이름을 덮어쓰지 않고 모드 0600으로 Unix 파일을 생성합니다.
  • 파일명 템플릿: {date}, {time}, {pid}와 필수 플레이스홀더인 {sequence}를 사용할 수 있습니다.
  • 운영 제약: 프로세스가 활성 파일 디스크립터를 열어 둔 채 유지하므로 동일 경로에 대해 외부 이름 변경 기반 logrotate 규칙을 함께 사용해서는 안 됩니다. 또한 보존 처리는 패턴과 일치하는 파일 전체에 대해 디렉터리 단위로 수행되므로, 동시에 실행 중인 다른 Router 프로세스와 로그 디렉터리를 공유하면 다른 프로세스의 세그먼트를 삭제할 수 있습니다.

파일 싱크는 ANSI 색상 코드를 비활성화한 일반 텍스트로 기록하며, stdout과 stderr는 tracing-subscriber의 기본 동작(NO_COLOR가 설정되지 않는 한 색상 사용)을 유지합니다.

로테이션과 보존은 최선 노력(best-effort) 방식으로 동작합니다. 디스크 공간 부족, 디렉터리 삭제, 시퀀스 소진 등으로 새 세그먼트를 생성할 수 없거나 정리를 위해 디렉터리 목록을 조회할 수 없는 경우, 싱크는 문제를 stderr에 보고하고 기존 세그먼트에 계속 기록한 뒤 rotation.max_bytes만큼 추가 출력된 시점에 로테이션을 재시도합니다. 열려 있는 세그먼트에 대한 쓰기 자체가 실패했을 때만 파이프라인이 실패 처리됩니다.

라이터는 지속적으로 바쁜 상태에서도 최소 초당 한 번 플러시하며 로테이션과 정상 종료 시에도 플러시합니다. 레코드마다 명시적인 fsync/sync_all을 호출하지 않아 로깅이 트랜잭션 I/O로 변하지 않으며 페이지 캐시 쓰기 반영(writeback)은 운영체제가 제어합니다. 따라서 머신이나 커널 크래시 시 가장 최근의 파일 기반 로그가 유실될 수 있습니다. 더 높은 로그 내구성이 필요한 배포 환경에서는 요청 처리 경로에 fsync 지연을 추가하는 대신 전용 로그 수집기와 함께 stdout을 사용해야 합니다.

3회 시행에 걸친 최초의 동기/비동기 비교와 초기 회귀 예산은 프로세스 로그 파이프라인 벤치마크 (2026-09-19)에 기록되어 있습니다.

우선순위는 다음 순서와 같습니다.

  1. 지속적인 과부하 상황에서도 진단 로그를 버리는 한이 있더라도 라우터 프로세스를 중단 없이 유지하고 서비스 용량을 보존합니다.
  2. 로그 싱크가 느리거나, 사용 불가능하거나, 쓰기 오류를 반복적으로 발생시킬 때에도 요청 가용성과 프로세스 안정성을 보존합니다.
  3. 기존 로그의 진단 가치와 보안 속성을 보존합니다.
  4. 요청 경로에서 로그 싱크 지연을 제거합니다.
  5. 측정 결과 로깅이 병목임이 확인된 경우에만 처리량을 개선합니다.

서비스 생존이 최우선 규칙입니다. 압박 상황에서 프로세스 로그는 버릴 수 있지만 요청 경로의 진행, 유계 메모리, 헬스 리포팅, 정상적인 성능 저하(graceful degradation)는 포기할 수 없습니다. 어떤 로깅 실패도 패닉, 중단(abort), 메모리 고갈을 일으키거나 Tokio 워커를 무기한 점유해서는 안 됩니다.

교체 가능한 로거 경계

src/core/tracing/logger.rs는 구독자(subscriber) 구성과 로그 전달 사이의 구현 경계입니다. LogPipelineFactory는 파이프라인을 생성하고 EventWriterFactory는 포맷된 이벤트마다 하나의 레코드 라이터를 제공하며 LogPipelineGuard는 구현별 종료 처리를 담당합니다. 구독자는 이 인터페이스들만 참조하며 유계 비동기 라이터는 하드코딩된 의존성이 아닌 기본 AsyncProcessLoggerFactory 구현체입니다.

동기식, 원격, 테스트용, 또는 플랫폼 네이티브 파이프라인 같은 대체 구현은 포맷팅 레이어나 OTLP 구성을 변경하지 않고 init_process_subscriber_with_factory를 통해 설치할 수 있습니다. 선택은 프로세스 시작 시점에 이루어집니다. 콜사이트 관심(callsite interest)과 라이프사이클 상태가 프로세스 전역이기 때문에 전역으로 설치된 tracing 구독자를 런타임에 교체하는 것은 지원하지 않습니다.

기본 비동기 구현은 구체적인 enum 패스트 패스를 유지하므로 이벤트를 생성하면 EventBuffer를 직접 반환하고 트레이트 객체(trait-object) 할당을 추가하지 않습니다. 이벤트별 박싱(boxing) 비용은 외부에서 제공된 동적 라이터 팩토리를 사용할 때만 발생합니다. LoggerGuard::status와 종료 결과는 구현 중립적인 선택적 LoggerStatus를 노출하며 커스텀 가드는 비교 가능한 카운터가 없을 때 None을 반환할 수 있습니다.

LoggerWriter는 Send + Sync이며 여러 생산자 스레드에 걸쳐 복제될 수 있습니다. 각 호출은 Send인 독립적인 LoggerEventWriter를 생성합니다. 큐 변경(mutation)은 짧은 push/pop 임계 구역에서만 직렬화되며 단일 목적지 라이터가 수락된 큐 순서를 보존합니다. 따라서 생산자별 순서는 보존되지만 생산자 사이의 인터리빙은 스케줄러에 따라 결정됩니다.

큐는 짧은 Mutex<VecDeque> push/pop 구간을 사용하며 처음에는 최대 1,024개의 슬롯을 할당합니다. 라이터는 목적지 I/O 도중 이 뮤텍스를 유지하지 않으며, 큐는 설정된 레코드 수와 페이로드 바이트 한도 내에서 유계 상태를 유지합니다.

범위

이 설계는 tracing을 통해 발생하고 tracing-subscriber로 포맷되는 사람이 읽을 수 있는 프로세스 로그를 다룹니다. HTTP 서버 모드, stdio 모드, 일회성 명령, 그리고 선택적 OTLP 스팬 익스포터와의 공존을 포괄합니다.

다음 항목들은 프로세스 로그 전달 계약에서 제외됩니다.

  • 청구 및 사용량 레코드
  • 내구성 있게 보존되어야 하는 감사(audit) 레코드
  • Prometheus 메트릭
  • OTLP 스팬
  • CLI, ACP, MCP 명령이 stdout에 기록하는 애플리케이션 프로토콜 출력

청구, 쿼터, 감사의 정확성은 프로세스 로그 한 줄의 전달 성공 여부에 의존해서는 안 됩니다. 이러한 레코드는 각자 확인 가능하고(acknowledged) 정합성을 맞출 수 있는(reconcilable) 별도의 영속화 경로를 사용합니다.

부트스트랩 및 OTLP 독립성

포맷팅 레이어에서 std::io::stdout이나 std::io::stderr을 직접 라이터로 사용하면 목적지 I/O가 이벤트를 발생시키는 태스크가 폴링되는 동안 일어나므로, 느린 컨테이너 로그 드라이버, 파이프, 터미널, 저널이 요청을 처리 중인 Tokio 워커까지 지연시킬 수 있습니다.

선택적인 OTLP 익스포터는 자체적인 배치(batching) 설계를 사용하지만 이를 활성화한다고 해서 포맷팅 레이어가 비동기가 되지는 않습니다. 두 출력 경로는 서로 독립적입니다.

구독자 초기화는 설정 로딩 자체가 로그를 발생시키기 때문에 설정 로딩보다 먼저 일어납니다. 따라서 큐 설정을 일반 설정에서 그대로 읽어와 첫 로그 줄에 적용할 수는 없습니다. 로그 파이프라인에 영향을 주는 스키마 필드는 나중에 적용하는 대신 부트스트랩 순서를 명시적으로 해결해야 합니다.

요구 속성

가용성과 유계 리소스

  • 막힌 로그 목적지가 HTTP 요청 핸들러나 Tokio 런타임 워커를 블록해서는 안 됩니다.
  • 인메모리 큐는 고정된 상한을 가져야 합니다.
  • 레코드 수와 보유 페이로드 바이트 모두 유계여야 합니다. 가변 크기 레코드의 개수만 제한하는 것은 메모리 한도가 아닙니다.
  • 큐 포화 상태가 무제한 스필(spill) 버퍼를 할당해서는 안 됩니다.
  • 요청 경로 블로킹이나 프로세스 OOM보다는 진단 로그 손실이 낫습니다.
  • 워커 실패가 요청 경로를 패닉시켜서는 안 됩니다.
  • 반복되는 목적지 오류는 로깅 서브시스템 내부에 국한되어야 하며 무제한 CPU를 소비하거나 재귀적으로 로그를 발생시키는 재시도 루프를 유발해서는 안 됩니다.
  • 큐가 포화된 이후에도 생산자의 작업은 O(1)을 유지해야 하며 큐 용량을 기다려서는 안 됩니다.
  • 로깅 과부하가 라이브니스나 레디니스 처리를 실패한 라이터에 의존하게 만들어서는 안 됩니다.

성능 저하 정책

프로세스 로그 경로는 트래픽 서빙에 대해 명시적으로 fail-open입니다.

  1. 큐에 여유가 있으면 포맷된 레코드를 큐에 넣습니다.
  2. 큐가 가득 차면 새 레코드를 버리고 드롭 카운터를 증가시킵니다.
  3. 라이터가 연결이 끊겼거나 영구적으로 실패하고 있으면 비용이 큰 이벤트별 복구 시도를 중단하고 서빙을 계속합니다.
  4. 요청 태스크에서 stdout이나 stderr로 동기적으로 폴백하지 않습니다. 그런 폴백은 싱크가 비정상인 순간에 원래의 실패 모드를 재현하게 됩니다.
  5. 로그 라이터에 의존하지 않는 메트릭과 프로세스 상태를 통해 저하된 상태를 노출합니다.

이 정책은 진단용 프로세스 로그에만 적용됩니다. fail-closed 비즈니스 계약을 가진 청구, 사용량, 감사 작업은 로깅이 아니라 자체 영속화 경로와 승인(admission) 경로를 통해 그 계약을 강제해야 합니다.

관측 가능한 손실

  • 큐가 가득 차서 거부된 모든 줄은 단조 증가하는 드롭 카운터를 증가시켜야 합니다.
  • 이 카운터는 동일한 포화 상태의 로그 큐를 통해 또 다른 이벤트를 보내지 않고도 읽을 수 있어야 합니다.
  • metrics 기능이 활성화되어 있으면 이 개수는 낮은 카디널리티의 Prometheus 메트릭으로 노출되어야 합니다.
  • 종료 진단은 최선 노력 방식으로 손실을 보고하되 기준 상태는 메트릭 값입니다. 이 보고를 로깅하려는 시도가 종료를 무기한 지연시켜서는 안 됩니다.

출력 호환성

  • HTTP 모드는 계속해서 프로세스 로그를 stdout으로 보냅니다.
  • Stdio, ACP, MCP, 일회성 명령 모드는 프로토콜 출력을 stdout에, 진단 정보를 stderr에 유지합니다.
  • 기존의 필드 이스케이프, 리댁션(redaction), 필터링, 포맷팅 동작은 별도의 변경이 명시적으로 수정하지 않는 한 그대로 보존됩니다.
  • OTLP 재로드 가능 레이어와 그 프라이버시 경계는 포맷팅 라이터와 독립적으로 계속 동작합니다.

라이프사이클 안전성

  • 라이터 가드는 프로세스 종료까지 유지되어야 합니다. 시작 도중 이를 drop해도 비동기 전달이 멈춰서는 안 됩니다.
  • std::process::exit는 소멸자를 실행하지 않으므로 main이 들고 있는 가드는 시작 코드 내부에서 호출되는 exit를 감지할 수 없습니다. 따라서 설치된 파이프라인은 core::tracing::subscriber가 소유하며 설치 이후의 모든 exit는 subscriber::exit_process를 거쳐 동일한 유한 예산 내에서 큐를 비운 뒤 종료합니다. 이를 거치지 않으면 시작 실패 원인을 담은 레코드가 큐에 남은 채 프로세스가 종료됩니다.
  • 정상 종료(graceful shutdown)에는 유한한 플러시 예산이 주어집니다.
  • 예산이 만료되면 종료가 진행되며 불완전한 플러시를 기록합니다.
  • 갑작스러운 종료는 전달을 보장할 수 없으며 보장할 수 있는 것처럼 문서화해서는 안 됩니다.
  • 라이브러리 임베더는 여전히 자신만의 구독자를 설치하고 관리할 책임이 있습니다.

파이프라인 아키텍처

tracing event on request/background task
                 |
                 v
       existing filters and formatter
                 |
                 v
       bounded non-blocking enqueue
          | accepted       | full/disconnected
          v                v
    fixed-capacity queue   atomic dropped counter
          |
          v
   dedicated writer thread
          |
          v
 stdout or stderr destination

이 구현은 src/core/tracing/process_log.rs의 유계 라이터를 사용합니다. tracing-appender 프로토타입은 워커 가드를 drop하면 타임아웃 없이 라이터 스레드에 join하므로 하위 Write 호출이 멈춰 있을 때 종료가 무한정 블록될 수 있어 채택하지 않았습니다. 라우터 전용 라이터는 유한한 플러시 예산을 직접 소유하고 카운터를 직접 노출합니다.

기본 큐 정책은 포화 상태에서 손실을 허용합니다. 무손실 유계 모드는 생산자를 블록하여 가용성 요구사항을 위반하고, 무제한 모드는 싱크 장애가 길어질 때 프로세스 OOM으로 이어질 수 있습니다.

라이터는 두 계층에서 한도를 강제합니다. EventBuffer::write는 이벤트 하나를 포맷하는 동안 최대 64 KiB만 유지하므로 크기가 큰 필드가 큐에 들어가기 전에 무제한 임시 버퍼를 할당하지 않습니다. 그다음 큐는 설정된 레코드 한도와 누적 페이로드 예산을 모두 만족할 때만 레코드를 수락하며, 누적 페이로드 예산은 레코드 수 추정치가 아니라 push와 pop 시점에 증분 방식으로 추적됩니다.

모든 일반 심각도 수준은 순서를 보존하는 동일한 큐를 공유합니다. 경고와 오류를 위한 별도 우선순위 큐는 큐 간 순서 모호성을 유발하면서도 전달 내구성을 보장하지 못하므로 두지 않습니다.

설정과 부트스트랩 결정

logging.queue_capacity, logging.queue_capacity_bytes, logging.file은 재시작이 필요한 설정입니다. 이 값들은 구독자가 설치되기 전에 설정 파일을 미리 확인하는 단계(server.workers로 Tokio 런타임 크기를 정하는 패스)에서 한 번 읽힙니다.

본 설정 로드는 그 이후에 실행되며 사전 조회(peek)가 구성한 파이프라인을 통해 로그를 남기므로, 초기 시작 로그와 이후 로그가 서로 다른 라이터를 쓰는 분리 부트스트랩이 발생하지 않습니다. 사전 조회에서 파일을 읽거나 파싱할 수 없으면 파이프라인은 컴파일된 기본값으로 시작하고 본 로드 단계에서 실제 오류를 보고합니다.

로더는 이 필드들을 나머지 설정과 함께 다음과 같이 검증합니다.

  • logging.queue_capacity: 1개에서 1,000,000개 레코드.
  • logging.queue_capacity_bytes: 최대 크기 레코드 하나(64 KiB)에서 1 GiB.
  • logging.file 파일명 템플릿: {sequence}를 반드시 포함해야 하며, 경로 구분자나 허용된 네 가지({date}, {time}, {pid}, {sequence}) 외의 플레이스홀더를 포함할 수 없음.

범위를 벗어난 큐 한도는 시작 시 파이프라인 assert로 패닉하지 않도록 설정 로드 시점에 거부됩니다. 종료 예산은 고정값으로 유지됩니다.

일회성 명령(config, metadata, auth, mcp-serve, --migrate-config-file)은 항상 컴파일된 기본값으로 stderr에 로그를 남기며 파일 싱크를 열지 않습니다. 이 명령들은 실행 중인 서버 설정이 아닌 다른 설정 파일을 검사할 수 있으므로 부수 효과로 서버의 로그 디렉터리를 로테이션하거나 정리하지 않습니다.

환경 변수만으로 동작하는 별도 부트스트랩 설정은 검증과 문서화 경로가 이원화되므로 제공하지 않습니다.

테스트 전략

테스트는 결정적(deterministic) 정확성 테스트와 반복 가능한 성능 실험으로 나뉩니다. 성능 수치만으로는 안전성을 입증할 수 없습니다.

1. 결정적 라이터 테스트

실제 터미널이나 컨테이너 런타임이 아니라 제어 가능한 Write 구현을 사용합니다. 다음 세 가지 모드를 지원해야 합니다.

  • fast: 바이트를 즉시 소비합니다.
  • throttled: 초당 지정된 바이트 수만큼만 허용합니다.
  • stalled: 테스트가 게이트를 해제할 때까지 블록합니다.

테스트는 다음을 입증해야 합니다.

  1. 싱크가 멈춰 있는 동안에도 이벤트 발생은 짧고 유한한 데드라인 내에 반환됩니다.
  2. 큐 용량보다 많은 이벤트가 들어와도 상주 큐 저장소는 설정된 한도를 넘지 않습니다.
  3. 포화 상태는 거부된 개수만큼 드롭 카운트를 증가시킵니다.
  4. 싱크가 재개된 뒤에는 수락된 레코드가 FIFO 순서로 배출됩니다.
  5. 연결이 끊기거나 실패하는 라이터는 이벤트를 발생시키는 태스크를 패닉시키지 않습니다.
  6. 복제된 일반 라이터 핸들을 drop해도 워커가 종료되지 않습니다.
  7. 소유 가드를 drop하면 문서화된 유한 플러시 동작이 수행됩니다.

채널이나 게이트로 동일한 속성을 결정적으로 입증할 수 있는 곳에서는 타이밍에만 의존하는 검증을 피합니다. 데드락 검사에는 타임아웃을 두어 회귀 발생 시 테스트 스위트가 멈추지 않고 실패하도록 합니다.

2. 구독자 통합 테스트

제어된 라이터와 함께 실제 포맷팅 레이어를 설치하고 다음을 검증합니다.

  • HTTP 모드는 stdout을 선택하고 stdio 모드는 stderr을 선택합니다.
  • 포맷된 레코드는 레벨, 타겟, 메시지, 구조화된 필드를 유지합니다.
  • 개행 이스케이프와 비밀 정보 리댁션(secret-redaction) 동작이 그대로 유지됩니다.
  • otel 구성과 비-otel 구독자 구성 모두 포맷된 출력을 큐에 넣습니다.
  • 일반 로그 이벤트는 계속 OTLP 익스포트에서 제외됩니다.
  • 초기화는 프로세스 전체 라이프사이클 동안 워커 가드를 유지합니다.

tracing 이벤트를 캡처하는 테스트는 tests/tracing_capture_support가 요구하는 대로, 캡처용 구독자를 생성하기 전에 저장소의 permissive global default를 계속 설치해야 합니다.

3. 프로세스 및 종료 테스트

테스트 프로세스가 제어하는 파이프에 stdout이나 stderr를 연결한 상태로 실제 바이너리를 실행합니다.

  • 커널 버퍼가 가득 찰 때까지 파이프를 소비하지 않은 다음, /health와 대표적인 프록시 라우트가 여전히 응답하는지 검증합니다.
  • 소비를 재개하고 프로세스가 계속 정상 상태인지 검증합니다.
  • 쓰기 가능한 싱크 상태에서 SIGTERM을 보내고 수락된 레코드가 플러시되는지 검증합니다.
  • 멈춰 있는 싱크 상태에서 SIGTERM을 보내고 유한한 종료 데드라인 내에 종료가 일어나는지 검증합니다.
  • stdout의 MCP/ACP 프로토콜 메시지가 진단 정보와 섞이지 않는지 검증합니다.

이 테스트들은 단위 테스트가 볼 수 없는 배선(wiring), 특히 가드 소유권과 목적지 선택을 보호합니다.

4. 엔드투엔드 부하 실험

공급자 지연이나 속도 제한이 라우터 동작을 가리지 않도록 로컬 모의(mock) LLM 백엔드를 사용합니다. 유휴 상태인 호스트에서 릴리스 빌드를 실행하고 정확한 커밋, CPU 개수, Tokio 워커 개수, 파일 디스크립터 한도, 명령, 페이로드, 동시성을 기록합니다.

다음 두 워크로드를 모두 수행합니다.

  • non-streaming: 요청 오버헤드를 강조하는 작고 고정된 응답
  • streaming: 고정된 청크 주기를 갖는 고정 길이 SSE 응답으로, 장수명 LLM 연결을 대표합니다.

각 워크로드에 대해 다음 로그 목적지들을 실행합니다.

케이스 목적지 목적
A /dev/null 또는 빠른 리더 포맷팅과 enqueue 오버헤드를 측정합니다
B 스로틀된(throttled) 파이프 느린 수집기나 컨테이너 로그 드라이버를 모사합니다
C 멈춘(stalled) 파이프 격리와 유계 성능 저하를 검증합니다

동일한 매트릭스를 변경 전후에 여러 제공 부하 수준(포화 이하 및 포화 이상 포함)에서 실행합니다. 지연으로 인한 협조적 누락(coordinated omission)이 느린 시스템을 정상으로 보이게 만들지 않도록 가능하면 개방 루프(open-loop) 부하 생성기를 사용합니다.

최소한 다음을 수집합니다.

  • 달성된 초당 요청 수
  • p50, p95, p99 지연 시간
  • 스트리밍의 첫 바이트까지 시간과 스트림 완료 시간
  • 오류 및 타임아웃 횟수
  • 프로세스 CPU와 상주 메모리
  • 관측 가능한 경우 Tokio 워커 포화도
  • 열린 파일 디스크립터와 TCP 연결 상태
  • 발생, 수락, 기록, 드롭된 로그 개수
  • 정상 종료 소요 시간

샘플을 기록하기 전에 바이너리와 커넥션 풀을 워밍업합니다. 유리한 실행 한 번만 골라 보고하는 대신 중앙값과 분산을 보고할 수 있을 만큼 각 케이스를 충분히 반복합니다.

수락 게이트

변경 사항은 다음 게이트를 모두 통과할 때에만 수락 가능합니다.

정확성 게이트

  • 기존의 로그 내용, 보안, OTLP, 프로토콜 분리 테스트가 통과합니다.
  • 빠른 싱크 상태에서 수락된 모든 테스트 레코드가 순서대로 기록됩니다.
  • 결정적 테스트에서 드롭된 줄 집계가 의도적으로 유발한 포화 상태와 정확히 일치합니다.

안정성 게이트

  • 멈춰 있는 싱크가 로깅 때문에 대표적인 HTTP 요청이 테스트 데드라인을 놓치게 만들지 않습니다.
  • 지속적인 이벤트 폭주 동안에도 큐 메모리는 유계 상태를 유지합니다.
  • 싱크 실패가 라우터를 패닉시키거나 종료시키지 않습니다.
  • 지속적으로 가득 찬 큐가 CPU 폭주, 할당량 증가, 재귀적인 내부 오류 폭주를 일으키지 않습니다.
  • 싱크가 멈춰 있고 로그 큐가 계속 포화 상태여도 헬스 엔드포인트는 계속 응답합니다.
  • 싱크가 계속 멈춰 있어도 종료는 문서화된 한도 내에 완료됩니다.

성능 게이트

  • 빠른 싱크 상태에서 비동기 구현은 정상적인 벤치마크 변동 범위를 벗어나는 실질적인 회귀를 일으키지 않습니다.
  • 스로틀되거나 멈춰 있는 싱크 상태에서, 요청 처리량과 테일 지연 시간은 동기식 버전보다 로그 싱크가 없는 기준선(baseline)에 실질적으로 더 가깝게 유지됩니다.
  • 개선을 주장할 때는 마이크로벤치마크만으로 추론하지 않고 기록된 부하 테스트 산출물로 뒷받침해야 합니다.

기준선을 측정하기 전에는 고정된 퍼센트를 명시하지 않습니다. 최초의 벤치마크 실행이 분산과 검토 가능한 회귀 예산을 확립하며 이 예산은 구현 변경이 머지되기 전에 기록되어야 합니다.

구현 순서

  1. 제어된 라이터 테스트 픽스처를 추가하고 동기식 기준선을 기록합니다.
  2. 재현 가능한 엔드투엔드 부하 스크립트를 추가하고 기준선 명령과 결과를 저장합니다.
  3. 유계 논블로킹 라이터와 프로세스 라이프타임 가드를 도입합니다.
  4. 포화된 경로를 통해 로깅하지 않고도 드롭된 줄 집계를 노출합니다.
  5. 라이프사이클, stdio 분리, OTLP 공존 테스트를 추가합니다.
  6. 동일한 부하 매트릭스를 반복하고 결과를 비교합니다.
  7. 통상적인 저장소 점검을 실행한 뒤 머지 전에 통합된 결과에 대해 make ci-local을 실행합니다.

위험과 완화 방안

위험 완화 방안
버스트 도중 진단 정보 손실 유계 드롭 카운터, 메트릭, 측정된 버스트로부터 선택한 용량
로그가 감사 원장이라는 오해 청구/감사 영속화를 프로세스 로그와 명시적으로 분리
가드가 너무 일찍 drop됨 프로세스 수준 소유권과 초기화 이후까지 유지되는 통합 테스트
무응답 싱크에서 종료가 멈춤 유한한 플러시 예산과 만료 후 강제 진행
큐가 과도한 로깅을 숨김 드롭과 이벤트 양을 추적하고 핫 패스 이벤트는 별도로 줄이거나 샘플링
OTLP 동작이 실수로 변경됨 otel 기능 유무 양쪽에서 구독자 구성을 테스트
시작 로그가 다른 시맨틱을 사용함 최초의 설정 로딩 로그부터 하나의 구독자만 사용
메트릭 보고가 재귀적으로 로그를 발생시킴 원자적/오류 카운터를 직접 읽고 동일한 큐를 통해 포화 상태를 보고하지 않음

비목표

  • SIGKILL, 커널 실패, 호스트 손실 전반에 걸친 프로세스 로그 전달 보장.
  • 로깅을 청구나 사용량의 기준 데이터(source of truth)로 사용하는 것.
  • 첫 구현에서 로그 필드 이름이나 로그 형식을 변경하는 것.
  • 무관한 아웃바운드 커넥션 풀이나 HTTP/2 성능 문제를 해결하는 것.
  • 엔드투엔드 측정 없이 라우터 전반의 성능 향상을 주장하는 것.