콘텐츠로 이동

느린 요청의 원인을 어떤 신호로 좁힐까

대시보드를 많이 만드는 대신, 사용자 증상을 지표로 찾고 한 요청의 경로와 구체적인 사건까지 내려가는 조사 흐름을 설계합니다.

예상 읽기 시간 8분 · 핵심 질문: p99가 올랐을 때 어디서 왜 느려졌는지 설명할 수 있는가

배포 뒤 주문 API의 p99가 200ms에서 1.2초로 올랐다고 하자. CPU 평균은 그대로고 오류 로그도 많지 않다. 어느 지역과 경로에서 시작됐는지, 한 요청이 어느 서비스에서 기다렸는지, 그 순간 어떤 잠금이나 코드가 시간을 썼는지 답하려면 서로 다른 신호가 필요하다.

관측 가능성(observability)은 시스템이 내보낸 신호로 미리 정하지 않은 내부 질문에도 답할 수 있는 성질이다. 지표·로그·추적 도구를 설치한 상태 자체가 아니다. 질문을 넓은 범위에서 구체적인 원인으로 좁힐 수 있어야 한다.

flowchart TB
  S["사용자 증상<br/>주문 p99와 오류율 증가"] --> M["지표<br/>언제·어느 경로·얼마나 넓게"]
  M --> T["분산 추적<br/>한 요청이 느려진 서비스와 구간"]
  T --> L["구조화 로그<br/>그 구간에서 일어난 구체적인 사건"]
  L --> P["프로파일<br/>CPU·할당·잠금을 쓴 코드 위치"]
  P --> D["원인 수정 뒤 같은 SLI로 확인"]

먼저 사용자 증상과 자원 포화를 나눈다

RED는 Rate, Errors, Duration의 머리글자다. 요청률, 오류 수, 소요 시간으로 사용자가 받은 서비스를 본다. “주문 생성 경로의 p99가 특정 배포 뒤 올랐다”처럼 조사 범위를 정하는 출발점이다.

USE는 Utilization, Saturation, Errors의 머리글자다. CPU·디스크·네트워크·스레드 풀·연결 풀 같은 자원의 사용률, 기다리는 작업의 포화와 오류를 본다. CPU 사용률이 60%여도 데이터베이스 연결 풀이 모두 사용 중이라 요청이 기다릴 수 있다. 사용률만이 아니라 대기열 길이와 자원을 얻는 시간을 함께 보는 이유다.

RED만 보면 사용자가 느리다는 사실은 알아도 원인을 모른다. USE만 보면 자원이 바쁜 사실은 알아도 어떤 사용자 요청이 영향을 받았는지 모른다. RED로 증상 범위를 찾고 같은 시간의 USE 신호로 원인 후보를 좁힌다.

SLI와 SLO는 “빠르게”를 검증 가능한 약속으로 바꾼다

SLI(Service Level Indicator, 서비스 수준 지표)는 사용자가 받은 결과를 실제로 측정한 값이다. 예를 들어 “유효한 주문 요청 가운데 300ms 안에 정상 완료한 비율”처럼 대상, 성공 조건과 측정 경계를 포함한다.

SLO(Service Level Objective, 서비스 수준 목표)는 일정 기간 동안 SLI가 도달해야 할 목표다. “28일 동안 유효 주문 요청의 99.9%가 300ms 안에 성공한다”라고 적으면 어느 요청이 분모에 들어가는지, 시간 초과 뒤 늦은 성공은 실패인지 결정해야 한다. 외부 책임과 보상까지 포함한 계약은 SLA(Service Level Agreement)로 구분한다.

SLO가 99.9%라면 0.1%의 실패 여유가 오류 예산(error budget)이다. 실제 오류율이 허용 오류율의 몇 배인지를 소진 속도로 본다. 짧은 구간과 긴 구간에서 모두 빠르게 예산을 소모할 때 경보를 울리면 순간적인 작은 흔들림보다 지속적인 사용자 영향을 잘 잡을 수 있다.

내부 CPU나 복제 지연은 중요하지만 그 자체가 사용자 SLI는 아니다. SLI 악화와 연결될 때 원인 신호가 된다.

지표는 넓은 범위와 변화 시점을 찾는다

카운터는 요청·오류처럼 누적되는 사건 수를, 게이지는 현재 연결 수처럼 한 시점의 상태를 나타낸다. 히스토그램은 지연 값을 여러 구간에 세어 분포를 남긴다. Prometheus는 이런 지표를 레이블 차원으로 나눠 수집할 수 있다.

경로, 상태 코드, 배포 버전과 지역처럼 값의 종류가 제한된 레이블은 비교에 유용하다. 사용자 ID나 요청 ID를 지표 레이블에 넣으면 요청마다 새 시계열이 생길 수 있다. 개별 식별자는 로그나 추적에 두고 지표는 제한된 범주의 집계에 집중한다.

p95와 p99는 느린 일부를 보여 주지만 표본과 구간이 필요하다

p99는 관측값 99%가 그 값 이하였다는 경계다. 평균이 정상이어도 일부 사용자가 연결 풀 대기나 잠금 충돌로 매우 느린 상황을 드러낸다. 하지만 트래픽이 적은 구간의 p99는 요청 몇 건에 크게 흔들리므로 표본 수를 함께 본다.

인스턴스별 p99를 평균 내면 전체 서비스 p99가 되지 않는다. 백분위수는 일반적으로 평균으로 합칠 수 없다. 병합 가능한 히스토그램 구간을 합친 뒤 전체 분포에서 다시 계산해야 한다. 기대하는 SLO 경계 주변을 충분히 구분하도록 히스토그램 구간도 설계한다.

분산 추적은 한 요청에서 시간이 사라진 구간을 찾는다

분산 추적은 한 요청이 여러 서비스를 지난 인과 경로를 스팬(span)으로 표현한다. 스팬에는 시작과 끝, 상태, 제한된 속성과 사건이 들어간다. 추적 식별자가 HTTP·메시지 경계를 넘어 전달돼야 주문 서비스의 50ms와 결제 서비스의 700ms를 한 요청으로 연결할 수 있다.

OpenTelemetry는 지표·로그·추적을 만들고 전달하기 위한 중립적인 API, SDK, 의미 규약과 수집기를 제공한다. 자동 계측은 HTTP와 데이터베이스 호출을 빠르게 연결하지만, 업무 대기열에서 기다린 이유나 캐시를 우회한 판단까지 알지는 못한다. 중요한 업무 경계에는 이름과 속성이 안정된 수동 스팬을 보완한다.

표본 추출로 일부 추적만 보관한다면 오류와 느린 요청을 우선 남기는 후행 표본 추출을 고려할 수 있다. 대신 완료될 때까지 추적을 잠시 버퍼에 보관하는 메모리와 판단 지연이 필요하다.

로그는 한 구간에서 실제로 일어난 사건을 남긴다

자유로운 문자열만 쓰기보다 시각, 심각도, 서비스와 버전, 경로 양식, 요청·추적 식별자, 오류 종류를 구조화된 필드로 남긴다. “DB error”보다 어떤 동작이 어떤 제한 시간과 오류 분류로 실패했는지 기록해야 추적과 연결할 수 있다.

MDC(Mapped Diagnostic Context)는 현재 실행 맥락의 요청 식별자 같은 값을 로그에 붙인다. 비동기 실행기와 리액티브 흐름에서는 스레드가 바뀌므로 자동으로 전달된다고 가정하면 안 된다. 가상 스레드에서도 큰 객체를 스레드 로컬에 넣는 비용을 확인한다.

토큰, 비밀번호, 개인정보와 AI 프롬프트 원문은 수집 뒤 지우는 것이 아니라 애플리케이션 경계에서 기록하지 않도록 막는다. 관측 저장소는 운영자가 넓게 조회할 수 있으므로 새로운 민감 정보 시스템이 된다.

Grafana와 Loki는 신호를 탐색하는 도구다

Grafana는 지표·로그·추적 같은 여러 데이터 원본을 대시보드와 탐색 화면에 연결한다. Loki는 값의 종류가 제한된 레이블로 로그 묶음을 찾고 본문을 저장하는 구조를 사용한다. 로그 레이블에도 사용자 ID나 가공하지 않은 URL을 넣지 않는 원칙이 적용된다.

대시보드는 원인을 보여 주는 완성된 답이 아니다. 사용자 SLI, 배포 표시와 주요 의존성 신호를 같은 시간축에 두어 다음 질문을 빠르게 만드는 도구다. 페이지마다 수십 개 그래프를 붙이기보다 “사용자 영향이 있는가, 어느 경로인가, 어떤 자원이 포화됐는가” 순서로 배치한다.

카디널리티는 관측 시스템의 용량을 결정한다

카디널리티는 레이블 조합으로 만들어지는 서로 다른 시계열 수다. method 5개, route 100개, status 10개만 조합해도 최대 5천 개가 된다. 여기에 사용자 ID 수백만 개를 곱하면 수집기와 저장소가 감당하기 어렵다.

서비스별로 허용할 레이블과 값의 범위를 정하고, 새 계측이 만드는 시계열 수를 배포 전에 확인한다. 상세 식별자는 필요할 때 지표의 예시 추적이나 로그로 이동한다. 관측 신호도 무제한으로 만들 수 있는 데이터가 아니다.

프로파일은 코드가 사용한 자원의 분포를 보여 준다

플레임 그래프는 표본으로 수집한 호출 스택을 합쳐, 가로 폭으로 CPU에서 관찰된 비중을 보여 준다. 폭이 넓다는 것은 표본에 자주 나타났다는 뜻이지 한 번의 호출이 그만큼 오래 걸렸다는 뜻은 아니다.

CPU가 높으면 CPU 프로파일, 메모리가 빠르게 늘면 할당 프로파일, 스레드가 기다리면 잠금이나 벽시계 프로파일을 고른다. 지속 프로파일은 배포 전후 회귀를 찾기 좋지만 표본 비용과 심볼, 민감한 함수·인자 정보의 취급을 정해야 한다.

eBPF는 코드 밖의 커널·네트워크 빈칸을 채운다

eBPF(extended Berkeley Packet Filter)는 안전성 검사를 거친 프로그램을 리눅스 커널의 지정 지점에서 실행해 시스템 호출, 네트워크와 스케줄링 사건을 관찰하게 한다. 애플리케이션 코드를 바꾸기 어려운 서비스에서도 연결과 요청 경로의 일부를 볼 수 있다.

하지만 암호화된 본문의 업무 의미, 프로세스 내부의 사용자별 대기열, 어떤 규칙으로 캐시를 우회했는지는 알 수 없다. eBPF는 애플리케이션 계측을 대체하지 않고 커널과 네트워크의 빈칸을 보완한다.

관측 수집 경로도 실패하는 운영 시스템이다

애플리케이션과 에이전트가 만든 신호는 SDK와 수집기를 거쳐 저장소로 간다. 이 경로에도 배치, 큐, 재시도와 삭제가 있다. 관측 저장소가 느릴 때 애플리케이션 요청 스레드를 막지 않도록 비동기 전송과 로컬 버퍼 상한, 버릴 우선순위를 정한다.

운영의 목표는 모든 것을 영구 보관하는 것이 아니다. 넓은 지표는 오래 두고, 상세 로그와 추적은 필요한 기간만 보존하며, 오류와 느린 요청은 더 높은 비율로 남길 수 있다. 관측 비용·신호 손실률·수집기 포화도 자체도 지표로 감시한다.

좋은 관측 구조는 “대시보드에 그래프가 있다”가 아니라 다음 문장을 빠르게 만들 수 있다.

배포 버전 42 이후 서울 지역 주문 요청의 p99가 상승했다. 느린 추적의 대부분은 결제 서비스가 아니라 주문 서비스의 데이터베이스 연결 대기에서 발생했고, 같은 시간 연결 풀 포화와 잠금 대기가 증가했다. 오류 예산 소진 속도가 경보 기준을 넘었으므로 배포를 되돌리고 해당 질의와 트랜잭션 경계를 확인한다.

참고 자료