홈으로 돌아가기

HTTP 지연 진단: Nginx에서 커널까지

이 기사는 HTTP 요청 처리 파이프라인의 지연 진단에 대한 체계적 접근을 설명합니다. Nginx 로그, Server-Timing 헤더, Linux 커널 TCP_INFO 메트릭을 사용해 로드 밸런서 측, 애플리케이션, 네트워크 경로의 문제 위치를 파악하는 방법을 다룹니다.

5분 만에 HTTP 파이프라인의 지연 원인 찾기
Advertisement 728x90

HTTP 요청 지연 진단: 로드 밸런서부터 Linux 커널까지

프로덕션 환경에서 응답 시간이 증가하거나 갑작스러운 5xx 오류가 발생하면 즉각적이고 정확한 문제 위치 파악이 필요합니다. 서비스를 무작정 확인하는 대신 전체 요청 처리 파이프라인을 네트워크와 애플리케이션 영역으로 나누는 체계적인 접근 방식을 사용하는 것이 더 효과적입니다. 표준 Nginx 로그, Server-Timing 헤더, 그리고 저수준 TCP_INFO 메트릭스를 활용해 병목 지점을 빠르게 찾아내는 방법을 자세히 살펴보겠습니다.

출발점: 로드 밸런서 로그 분석

일반적인 웹 앱 아키텍처는 여러 계층으로 구성됩니다: 사용자, CDN, 로드 밸런서(SLB), 애플리케이션, 연결 풀, 데이터베이스 등. 각 계층에서 지연이 발생할 수 있지만, 진단의 출발점은 항상 로드 밸런서여야 합니다. 이는 파이프라인의 중심에 위치해 검색 방향을 즉시 결정할 수 있기 때문입니다.

Nginx의 경우 핵심 변수는 $request_time$upstream_response_time입니다. 첫 번째는 클라이언트로부터 첫 바이트를 수신한 후 응답의 마지막 바이트를 전송할 때까지의 전체 요청 처리 시간을 기록합니다. 두 번째는 업스트림 서버로부터 응답을 기다리는 시간만 측정합니다. 아직 이러한 메트릭을 수집하지 않았다면 로깅 형식을 확장하세요:

Google AdInline article slot
log_format timing '$remote_addr - $request_uri '
                  'status=$status '
                  'rt=$request_time '
                  'uct=$upstream_connect_time '
                  'urt=$upstream_response_time';

access_log /var/log/nginx/access.log timing;

데이터 해석은 간단한 비교에 기반합니다. $upstream_response_time$request_time과 비슷하다면 병목은 로드 밸런서 오른쪽에 있습니다: 앱 코드, 연결 풀, 또는 데이터베이스 쪽입니다. $upstream_response_time은 정상인데 $request_time이 비정상적으로 높다면 문제는 왼쪽에 있습니다: 네트워크 인프라, CDN 라우팅, 또는 클라이언트 측입니다. 정확한 측정을 위해 add_header를 통해 응답 헤더에 $request_time을 삽입하는 것은 권장되지 않습니다. Nginx가 응답 본문 전송을 완료하기 전에 헤더를 형성하기 때문입니다. 신뢰할 수 있는 데이터는 연결 종료 후 access 로그에서만 확인할 수 있습니다.

애플리케이션 측 문제 정확히 파악하기

로드 밸런서가 업스트림 지연을 가리킬 때 앱 내부 동작을 세밀하게 들여다봐야 합니다. W3C Server-Timing 표준을 사용하면 개별 요청 처리 단계의 정확한 타이밍을 클라이언트나 모니터링 시스템으로 전송할 수 있습니다. 헤더는 다음과 같습니다: Server-Timing: app;dur=120, db;dur=95, pool-wait;dur=18. 무거운 APM 에이전트를 배포하지 않고도 투명성을 제공합니다.

앱 스택에 헤더를 통합하는 데는 최소한의 변경만 필요합니다. 인기 언어별 구현 예시:

Google AdInline article slot

Go (net/http):

start := time.Now()
rows, err := db.QueryContext(ctx, query)
dbDur := time.Since(start)

w.Header().Set("Server-Timing",
    fmt.Sprintf("db;dur=%.2f", float64(dbDur.Microseconds())/1000))

Python (Django middleware):

class ServerTimingMiddleware:
    def __init__(self, get_response):
        self.get_response = get_response

    def __call__(self, request):
        start = time.monotonic()
        response = self.get_response(request)
        dur = (time.monotonic() - start) * 1000
        response["Server-Timing"] = f"app;dur={dur:.2f}"
        return response

Node.js (Express):

Google AdInline article slot
app.use((req, res, next) => {
  const start = process.hrtime.bigint();
  res.on('finish', () => {
    const ms = Number(process.hrtime.bigint() - start) / 1e6;
    // already sent, but for logging:
    console.log(`Server-Timing: app;dur=${ms.toFixed(2)}`);
  });
  // for header — time up to response:
  const origEnd = res.end;
  res.end = function(...args) {
    const ms = Number(process.hrtime.bigint() - start) / 1e6;
    res.setHeader('Server-Timing', `app;dur=${ms.toFixed(2)}`);
    origEnd.apply(this, args);
  };
  next();
});

Nginx 자체 메트릭을 Server-Timing으로 전달하려면 정적 파일이나 오류에 빈 값을 보내지 않도록 map 지시어를 사용하세요:

Nginx (upstream 타이밍을 Server-Timing으로):

      # In http {} block of nginx.conf:
      map $upstream_header_time $server_timing_upstream {
      "-"     "";
       default "ngx-upstream;dur=$upstream_header_time;desc=\"NgxUpstream\"";
      }

      # In location {} block of your server:
      add_header Server-Timing-Ngx "ngx-total;dur=$request_time;desc=\"NgxTotal\"" always;
      add_header Server-Timing-Ngx $server_timing_upstream always;

Server-Timing-Ngx 접두사와 $upstream_header_time 변수 사용에 주목하세요. 브라우저는 밀리초 단위를 기대하지만 Nginx는 초 단위를 반환합니다. 또한 $upstream_header_time은 업스트림으로부터 헤더를 수신한 순간을 포착해 응답 생성 시간을 정확히 반영합니다. 특정 버퍼링 조건에서 0이 될 수 있는 $upstream_response_time과 다릅니다.

이 데이터를 분석하면 인시던트를 빠르게 분류할 수 있습니다:

  • 지배적인 db;dur은 비효율적인 SQL 쿼리, 누락된 인덱스, 또는 테이블 잠금을 가리킵니다.
  • 높은 pool-wait;dur은 PgBouncer 같은 풀러의 연결 한도 소진을 나타냅니다.
  • db;dur이 작지만 app;dur이 크면 CPU 중심 작업, 차단 I/O, 또는 런타임 메모리 누수를 의미합니다.
  • 타이밍이 정상인데 로드 밸런서에서 502/504 오류가 발생하면 앱 프로세스 충돌이나 오케스트레이터 타임아웃을 뜻합니다.

네트워크 영역: HTTP 단계와 커널 메트릭

업스트림이 빠르게 응답하는데 전체 지연이 증가한다면 문제는 네트워크 경로에 있습니다. 모든 HTTP 요청은 6개의 순차적 단계를 거칩니다: DNS, Connect(TCP 핸드셰이크), TLS 핸드셰이크, Send, Wait(TTFB), Receive. 특정 단계의 이상은 검색 범위를 즉시 좁혀줍니다. DNS가 길면 리졸버 문제나 복잡한 CNAME 체인을 의심하세요. Connect와 TLS 값이 높으면 CDN 엣지 노드까지의 네트워크 지연, TLS 세션 재개 부족, 또는 구버전 프로토콜을 확인하세요. Connect와 TLS가 최소인데 Wait가 비정상적으로 높으면 WAF, CDN 워커, 또는 오리진 페치 수준에서 지연이 발생합니다.

깊이 있는 네트워크 진단을 위해 표준 HTTP 분석으로는 부족합니다. getsockopt(TCP_INFO)를 통한 Linux 커널 메트릭이 필요합니다. 커널은 모든 TCP 연결을 추적하며 RTT, 재전송 횟수, 혼잡 윈도우(cwnd) 크기, RTO 등의 정확한 데이터를 제공합니다. 이는 HTTP 타이밍이 한 TCP 세그먼트에 응답이 맞을 때 네트워크 지연과 서버 처리 시간을 분리할 수 없기 때문에 중요합니다. Wait 단계에서 커널 RTT를 빼면 진짜 요청 처리 시간을 얻을 수 있습니다. 재전송과 좁아진 cwnd는 패킷 손실과 혼잡 제어 알고리즘으로 인한 수백 밀리초 지연을 직접 가리킵니다. 앱 헤더와 저수준 네트워크 메트릭을 결합하면 타사 벤더에 의존하지 않고 완전한 관찰성을 확보할 수 있습니다.

주요 요약

  • Nginx에서 $request_time$upstream_response_time을 비교하면 네트워크와 앱 책임을 즉시 분리할 수 있습니다.
  • Server-Timing 헤더는 무거운 APM 시스템 없이 내부 지연(DB, 풀, 런타임)에 대한 세밀한 가시성을 제공합니다.
  • HTTP 요청 단계를 분석하면 DNS, TLS 핸드셰이크, CDN 라우팅 문제를 정확히 식별합니다.
  • TCP_INFO 커널 메트릭은 앱 수준에서 감지 불가능한 네트워크 손실과 지연에 대한 객관적 뷰를 줍니다.
  • 체계적인 진단 접근은 MTTR을 줄이고 인시던트 중 무작정 서비스 재시작을 없앱니다.

— Editorial Team

Advertisement 728x90

다음 읽기