메트릭과 분산 추적을 활용한 게임 서버 모니터링

모니터링의 목적

서버 모니터링은 시스템이 정상적으로 동작하는지 확인하는 것을 넘어, 장애의 원인을 파악하고 성능 저하를 사전에 감지하기 위한 핵심 운영 기술이다.

실시간 게임 서버와 멀티플레이어 플랫폼에서는 단순히 서버가 실행 중인지 확인하는 것만으로는 충분하지 않다. 서버 프로세스가 살아 있더라도 이벤트 루프가 지연되거나, 데이터베이스 응답이 느려지거나, 특정 API에서 오류가 급증할 수 있기 때문이다.

효과적인 모니터링은 다음과 같은 관측 데이터를 함께 활용한다.

  • Metrics: 시스템의 상태와 성능을 수치로 측정한다.
  • Logs: 특정 시점에 발생한 이벤트와 오류의 상세 내용을 기록한다.
  • Distributed Tracing: 요청이 여러 서비스와 시스템을 거치는 경로를 추적한다.

이러한 데이터를 함께 활용하면 장애를 빠르게 감지하고, 문제가 발생한 위치를 좁히며, 실제 원인까지 추적할 수 있다.

Metrics

Metrics는 시스템의 상태를 시간에 따라 측정한 수치 데이터다. 서버의 성능과 안정성을 정량적으로 평가하고, 임계치를 기반으로 경고를 발생시키는 데 사용한다.

주요 메트릭

시스템 메트릭

  • CPU 사용률
  • 메모리 사용량
  • 디스크 사용량과 I/O
  • 네트워크 송수신량

시스템 메트릭은 서버 자원이 충분한지 판단하는 데 사용한다. CPU 사용률이 지속적으로 높다면 연산량이 많은 작업이나 비효율적인 로직을 의심할 수 있다. 메모리 사용량이 계속 증가한다면 메모리 누수나 캐시의 무제한 증가 가능성을 확인해야 한다.

애플리케이션 메트릭

  • 요청 처리량
  • 응답 시간
  • 오류율
  • 활성 연결 수
  • 데이터베이스 연결 풀 사용량
  • 작업 큐 대기량

API 서버에서는 요청 처리량과 응답 시간, 오류율이 중요하다. 실시간 게임 서버에서는 여기에 WebSocket 연결 수, 활성 세션 수, 매칭 대기열 길이, 룸별 인원, 메시지 처리 지연 등을 추가로 측정할 수 있다.

지연 시간 분포

평균 응답 시간만으로는 사용자 경험을 정확하게 평가하기 어렵다. 대부분의 요청은 빠르게 처리되더라도 일부 요청이 심각하게 지연될 수 있기 때문이다.

  • p50: 요청의 절반이 이 시간 이내에 처리된다.
  • p95: 요청의 대부분이 이 시간 이내에 처리된다.
  • p99: 요청의 거의 전부가 이 시간 이내에 처리되며, 느린 요청의 영향을 파악하는 데 유용하다.

예를 들어 평균 응답 시간이 양호하더라도 p99가 급격히 상승한다면 특정 요청에서 데이터베이스 지연, 락 경합 또는 외부 서비스 응답 지연이 발생하고 있을 가능성이 있다.

메트릭 설계 시 주의사항

메트릭에는 측정 대상과 단위를 명확하게 정의해야 한다. 요청 수는 초당 요청 수인 RPS로, 지연 시간은 밀리초 단위로, 메모리는 바이트 단위로 기록하는 식이다.

또한 메트릭의 레이블을 무분별하게 늘리지 않아야 한다. 사용자 ID, 세션 ID, 요청 ID처럼 값의 종류가 매우 많은 데이터를 레이블로 사용하면 시계열 수가 급격히 증가해 모니터링 시스템에 부담을 줄 수 있다.

고유한 요청 정보는 로그나 트레이스에 기록하고, 메트릭에는 서비스 이름, HTTP 메서드, 상태 코드 그룹처럼 제한된 범위의 레이블을 사용하는 것이 적절하다.

Prometheus

Prometheus는 시계열 메트릭을 수집하고 저장하며, 쿼리를 통해 데이터를 분석할 수 있는 모니터링 시스템이다. 서버와 애플리케이션의 상태를 수집하고, 성능 지표를 조회하거나 장애 경고를 구성하는 데 활용한다.

수집 구조

Prometheus는 기본적으로 모니터링 대상이 제공하는 메트릭 엔드포인트를 주기적으로 조회하는 Pull 방식을 사용한다.

일반적인 수집 과정은 다음과 같다.

  • 애플리케이션이 메트릭을 노출한다.
  • Prometheus가 설정된 주기에 따라 메트릭 엔드포인트를 조회한다.
  • 수집한 값을 시계열 데이터로 저장한다.
  • PromQL로 메트릭을 조회하고 분석한다.
  • Alertmanager 등과 연동해 조건에 따른 경고를 전달한다.

Node.js 애플리케이션에서는 prom-client 라이브러리를 활용해 메트릭을 노출할 수 있다.

import express from "express";
import client from "prom-client";

const app = express();

const register = new client.Registry();

client.collectDefaultMetrics({
  register,
});

const httpRequests = new client.Counter({
  name: "http_requests_total",
  help: "Total number of HTTP requests",
  labelNames: ["method", "route", "status"],
});

const httpDuration = new client.Histogram({
  name: "http_request_duration_seconds",
  help: "HTTP request duration in seconds",
  labelNames: ["method", "route"],
  buckets: [0.01, 0.05, 0.1, 0.25, 0.5, 1, 2.5],
});

register.registerMetric(httpRequests);
register.registerMetric(httpDuration);

app.get("/health", (req, res) => {
  res.json({ status: "ok" });
});

app.get("/metrics", async (req, res) => {
  res.set("Content-Type", register.contentType);
  res.end(await register.metrics());
});

app.listen(3000);

이 코드는 기본적인 메트릭 수집 구조를 보여준다. 실제 서비스에서는 요청 처리 미들웨어에서 카운터와 히스토그램을 기록해야 한다. 예시의 메트릭 객체를 생성하는 것만으로 모든 HTTP 요청이 자동으로 집계되지는 않는다.

또한 실제 서비스에서는 route 레이블에 원본 URL을 그대로 사용하지 않아야 한다. /users/123과 /users/456을 각각 별도 레이블로 기록하면 시계열이 과도하게 증가할 수 있으므로 /users/:id와 같은 라우트 템플릿을 사용한다.

PromQL을 활용한 메트릭 분석

Prometheus는 PromQL이라는 쿼리 언어를 제공한다.

초당 요청 수

sum(rate(http_requests_total[5m]))

최근 구간의 증가율을 계산해 전체 요청 처리량을 확인한다.

HTTP 오류율

sum(rate(http_requests_total{status=~"5.."}[5m]))
/
sum(rate(http_requests_total[5m]))

서버 오류에 해당하는 요청의 비율을 계산한다. 요청이 없는 상황에서는 분모가 영이 될 수 있으므로 실제 대시보드와 경고 규칙에서는 해당 상황도 고려해야 한다.

요청 지연 시간의 p95

histogram_quantile(
  0.95,
  sum by (le) (
    rate(http_request_duration_seconds_bucket[5m])
  )
)

히스토그램 버킷을 이용해 최근 요청의 p95 지연 시간을 추정한다.

메트릭을 수집하는 것만으로는 운영 체계가 완성되지 않는다. 서비스의 정상 범위를 정의하고, 장애를 감지할 경고 규칙과 상황을 파악할 대시보드를 함께 설계해야 한다.

Distributed Tracing

Distributed Tracing은 하나의 요청이 여러 서비스와 시스템을 통과하는 전체 경로를 추적하는 기술이다.

단일 서버에서는 로그만으로도 요청의 흐름을 파악하기 비교적 쉽다. 그러나 게임 플랫폼이 API 서버, 인증 서버, 매칭 서버, 데이터베이스, 메시지 브로커 등으로 분리되면 요청 하나가 여러 구성 요소를 거칠 수 있다.

이때 전체 요청의 흐름을 연결하지 않으면 어느 구간에서 지연이나 오류가 발생했는지 파악하기 어렵다.

Trace와 Span

분산 추적의 핵심 개념은 Trace와 Span이다.

  • Trace: 하나의 요청이 처리되는 전체 과정을 나타낸다.
  • Span: 요청 처리 과정에서 수행된 개별 작업을 나타낸다.
  • Parent Span: 다른 작업을 호출한 상위 작업이다.
  • Trace ID: 동일한 요청 흐름에 속한 작업을 연결하는 식별자다.
  • Span ID: 개별 작업을 식별하는 값이다.

예를 들어 플레이어가 매칭을 요청했다고 가정하자.

매칭 요청
 ├─ API 인증
 ├─ 매칭 서버 호출
 │   ├─ Redis 대기열 조회
 │   └─ 매칭 결과 계산
 ├─ 결과 저장
 └─ 알림 이벤트 발행

각 작업을 Span으로 기록하면 전체 요청에 소요된 시간뿐 아니라 개별 작업의 처리 시간도 확인할 수 있다.

매칭 요청 전체에 긴 시간이 걸렸더라도 실제 원인은 Redis 조회, 데이터베이스 쓰기, 외부 서비스 호출 중 특정 구간에 있을 수 있다. 분산 추적은 이런 병목을 구분하는 데 유용하다.

분산 추적의 운영상 주의점

분산 추적은 모든 작업을 기록하는 것만으로 충분하지 않다. 요청량이 많으면 저장 공간과 수집 비용이 증가하므로 샘플링 정책을 적용할 수 있다.

또한 추적 데이터를 전파하려면 서비스 간 호출에서 Trace Context를 전달해야 한다. 비동기 작업이나 메시지 큐를 사용하는 경우에도 메시지에 추적 컨텍스트를 포함해야 작업 간 연결이 유지된다.

민감한 사용자 정보나 인증 토큰은 Span 속성에 기록하지 않아야 한다. 추적 데이터 역시 운영 로그와 마찬가지로 접근 권한과 보존 정책을 적용해야 한다.

OpenTelemetry

OpenTelemetry는 애플리케이션에서 관측 데이터를 생성하고, 수집하며, 다른 모니터링 시스템으로 전달할 수 있도록 지원하는 오픈소스 표준과 도구 모음이다.

Metrics, Traces, Logs를 생성하고 전달하는 공통 방식을 제공하므로 특정 관측 플랫폼에 대한 의존성을 줄이는 데 도움이 된다.

OpenTelemetry 자체가 모든 관측 데이터를 저장하고 시각화하는 완성형 모니터링 백엔드는 아니다. 데이터 계측과 전송을 담당하는 구성 요소를 제공하며, 실제 저장과 조회에는 Prometheus, Grafana, Tempo, Jaeger 등의 시스템을 조합할 수 있다.

주요 구성 요소

API와 SDK

애플리케이션 코드에서 추적이나 메트릭을 생성하는 데 사용하는 인터페이스와 구현체다.

Instrumentation

HTTP 요청, 데이터베이스 호출 등 주요 작업을 자동 또는 수동으로 계측한다. 자동 계측을 사용하면 기존 코드의 변경을 줄이면서 요청 흐름을 수집할 수 있다.

OTLP

OpenTelemetry Protocol의 약자로, 관측 데이터를 전송하기 위한 표준 프로토콜이다.

OpenTelemetry Collector

애플리케이션이나 다른 수집기에서 데이터를 받아 처리하고, 필요한 백엔드로 전달하는 독립적인 구성 요소다.

Collector는 데이터를 수신한 뒤 필터링, 속성 변경, 배치 처리, 샘플링 등의 작업을 수행할 수 있다. 이를 활용하면 애플리케이션이 특정 관측 플랫폼에 직접 종속되는 구조를 줄일 수 있다.

Node.js에서 분산 추적 적용하기

Node.js 애플리케이션에서는 OpenTelemetry의 Node.js SDK와 자동 계측 패키지를 이용해 HTTP 요청과 외부 호출을 추적할 수 있다.

개념적인 초기화 예시는 다음과 같다.

import { NodeSDK } from "@opentelemetry/sdk-node";
import {
  getNodeAutoInstrumentations,
} from "@opentelemetry/auto-instrumentations-node";
import {
  OTLPTraceExporter,
} from "@opentelemetry/exporter-trace-otlp-http";

const sdk = new NodeSDK({
  traceExporter: new OTLPTraceExporter({
    url: "http://localhost:4318/v1/traces",
  }),
  instrumentations: [
    getNodeAutoInstrumentations(),
  ],
});

sdk.start();

이 예시는 추적 데이터를 OTLP HTTP 엔드포인트로 전달하는 기본적인 구성을 보여준다. 실제 환경에서는 Collector 또는 호환되는 수집 엔드포인트를 구성하고, 필요한 패키지를 설치해야 한다.

자동 계측이 모든 사용자 정의 작업을 자동으로 추적하는 것은 아니다. 게임 로직, 매칭 계산, 보상 정산처럼 업무 의미가 중요한 작업은 수동 Span을 추가해 추적하는 것이 좋다.

또한 계측 초기화는 가능한 한 애플리케이션의 다른 모듈보다 먼저 실행해야 한다. 초기화 순서가 잘못되면 일부 라이브러리의 자동 계측이 적용되지 않을 수 있다.

오류 추적

오류 추적은 애플리케이션에서 발생한 예외와 실패를 수집하고, 발생 조건과 원인을 분석하는 과정이다.

단순히 오류 메시지를 기록하는 것만으로는 충분하지 않다. 같은 오류가 어느 API에서 발생했는지, 어떤 요청과 연관되어 있는지, 특정 배포 이후 증가했는지 등의 정보가 필요하다.

오류 추적에 필요한 정보

  • 오류 종류와 메시지
  • 스택 트레이스
  • 발생한 서비스와 환경
  • 요청 경로와 작업 종류
  • Trace ID와 Span ID
  • 애플리케이션 버전
  • 발생 시각과 오류 빈도

오류를 기록할 때는 동일한 오류가 반복해서 발생하는지 확인할 수 있도록 오류 유형과 원인을 기준으로 그룹화하는 것이 좋다.

예를 들어 데이터베이스 연결 실패가 특정 서비스 인스턴스에서만 발생한다면 해당 인스턴스의 네트워크, 연결 풀, 리소스 상태를 집중적으로 확인할 수 있다.

오류와 Trace 연결하기

오류 추적 시스템과 분산 추적 시스템을 연결하면 오류가 발생한 요청의 전체 경로를 확인할 수 있다.

요청 수신
  ↓
인증 처리
  ↓
매칭 서비스 호출
  ↓
Redis 조회
  ↓
오류 발생
  ↓
오류 이벤트에 Trace ID 연결

이 구조에서는 오류 이벤트를 확인한 뒤 해당 Trace로 이동해 어떤 작업이 실패했는지, 그 직전에 어떤 호출이 있었는지 살펴볼 수 있다.

다만 오류가 발생했다는 사실과 사용자가 실제로 피해를 입었다는 사실은 구분해야 한다. 예상된 입력 오류, 취소된 요청, 재시도로 복구된 일시적 실패까지 모두 동일한 심각도로 취급하면 경고 피로가 발생한다.

따라서 오류를 심각도와 영향 범위에 따라 분류하고, 실제 사용자 경험이나 데이터 정합성에 영향을 미치는 오류에 우선순위를 부여해야 한다.

이벤트 루프 지연 모니터링

Node.js는 기본적으로 이벤트 루프를 통해 JavaScript 콜백과 비동기 작업을 처리한다. 이벤트 루프가 장시간 점유되면 다른 요청의 콜백 실행이 늦어지고, API 응답 시간과 WebSocket 메시지 처리 지연이 증가할 수 있다.

특히 실시간 게임 서버에서는 이벤트 루프 지연이 여러 플레이어에게 동시에 영향을 줄 수 있다.

이벤트 루프 지연의 원인

CPU 집약적 작업

복잡한 경로 탐색, 대규모 정렬, 무거운 JSON 직렬화처럼 메인 스레드에서 오랫동안 실행되는 작업은 이벤트 루프를 차단할 수 있다.

동기식 I/O

동기식 파일 접근이나 기타 블로킹 작업은 작업이 완료될 때까지 이벤트 루프의 진행을 막을 수 있다.

과도한 객체 처리와 가비지 컬렉션

대량의 객체 생성과 메모리 압박은 가비지 컬렉션으로 인한 지연을 유발할 수 있다.

비효율적인 메시지 처리

많은 WebSocket 메시지를 한꺼번에 처리하거나, 메시지마다 무거운 검증과 직렬화를 반복하면 이벤트 루프의 처리 부담이 증가한다.

Node.js에서 이벤트 루프 지연 측정하기

Node.js는 perf_hooks 모듈의 monitorEventLoopDelay를 통해 이벤트 루프 지연을 측정할 수 있다.

import {
  monitorEventLoopDelay,
} from "node:perf_hooks";

const histogram = monitorEventLoopDelay({
  resolution: 20,
});

histogram.enable();

setInterval(() => {
  const p95Ms = histogram.percentile(95) / 1e6;
  const maxMs = histogram.max / 1e6;

  console.log({
    eventLoopDelayP95Ms: p95Ms,
    eventLoopDelayMaxMs: maxMs,
  });

  histogram.reset();
}, 10000);

이 코드는 이벤트 루프 지연의 p95와 최대값을 주기적으로 출력하는 예시다. 측정값은 나노초 단위이므로 밀리초로 변환하기 위해 백만으로 나눈다.

운영 환경에서는 console.log 대신 Prometheus 메트릭으로 기록해 시간에 따른 변화를 관찰하는 편이 적절하다. 또한 위 코드는 간단한 측정 예시이며, 실제 경고 기준은 서비스의 지연 시간 목표와 트래픽 특성에 따라 정해야 한다.

이벤트 루프 지연을 해석하는 방법

이벤트 루프 지연이 증가했다고 해서 반드시 CPU 사용률도 높아지는 것은 아니다. 메인 스레드에서 긴 작업을 수행하는 경우 CPU 사용률과 지연이 함께 증가할 수 있지만, 가상화 환경의 스케줄링 지연이나 시스템 부하 등 다른 원인도 존재한다.

따라서 이벤트 루프 지연은 다음 메트릭과 함께 분석해야 한다.

  • CPU 사용률과 프로세스별 CPU 사용량
  • 메모리 사용량과 힙 크기
  • 요청 처리량과 p95·p99 지연 시간
  • WebSocket 메시지 처리량
  • 활성 연결 수
  • 가비지 컬렉션 관련 지표

지연이 특정 시간대나 특정 게임 룸에서만 발생한다면 해당 작업의 연산량과 메시지 분포를 확인해야 한다. 부하가 커질수록 문제가 심해진다면 작업 분할, Worker Threads 활용, 메시지 배치 처리 등을 검토할 수 있다.

게임 서버에 적용하는 통합 모니터링 구조

실시간 게임 플랫폼에서는 각 관측 기술을 별도로 운영하기보다 하나의 흐름으로 연결하는 것이 중요하다.

Node.js 게임 서버
  ├─ Metrics
  │    └─ Prometheus
  │         └─ Grafana 대시보드
  │
  ├─ Traces
  │    └─ OpenTelemetry SDK
  │         └─ OpenTelemetry Collector
  │              └─ Tempo 또는 Jaeger
  │
  └─ Logs
       └─ 구조화된 로그
            └─ 로그 수집 시스템

이 구조는 개념적인 예시다. 실제 배포에서는 Collector의 수신기와 내보내기 설정, 인증, 네트워크 접근 정책, 저장소를 별도로 구성해야 한다.

장애 분석 과정에서는 다음과 같은 순서로 접근할 수 있다.

  • Grafana에서 요청 지연 시간과 오류율이 증가한 시점을 확인한다.
  • Prometheus에서 CPU, 메모리, 이벤트 루프 지연 등 관련 메트릭을 비교한다.
  • 오류 로그에서 해당 시점의 오류 유형과 요청 정보를 확인한다.
  • Trace ID를 이용해 분산 추적 화면에서 요청 경로를 확인한다.
  • 병목이 발생한 데이터베이스, Redis, 외부 서비스 또는 애플리케이션 로직을 조사한다.
  • 수정 이후 동일한 메트릭과 추적 데이터를 비교해 개선 여부를 검증한다.

이렇게 구성하면 단순히 장애 발생 여부를 감지하는 수준을 넘어, 장애의 원인을 추적하고 개선 결과를 검증하는 운영 체계를 구축할 수 있다.

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다