홈

© 2026 Ki Chang. All rights reserved.

본 블로그의 콘텐츠는 CC BY-NC-SA 4.0 라이선스를 따릅니다.

소개JSON Formatter개인정보처리방침이용약관

© 2026 Ki Chang. All rights reserved.

콘텐츠: CC BY-NC-SA 4.0

소개|JSON Formatter|개인정보처리방침|이용약관

Prometheus 메트릭 최적화: 카디널리티 폭발과 공격 트래픽 필터링

정기창·2026년 2월 12일

Grafana 대시보드에서 이상한 패턴을 발견했습니다. Coolify로 운영 중인 백엔드 서버의 CPU 사용량이 정확히 1분 간격으로 30% 스파이크를 보이고 있었습니다. 처음엔 Grafana Alloy의 메트릭 스크래핑 때문이라고 생각했는데, 조사해보니 더 근본적인 문제가 숨어 있었습니다.

문제 발견: 카디널리티 폭발

Prometheus 메트릭을 쿼리해보니 충격적인 결과가 나왔습니다. HTTP 요청 경로 메트릭에 수백 개의 고유한 path가 기록되어 있었는데, 대부분이 이런 것들이었습니다:

/saas/public/.env
/saas/public/.git/config
/saas/public/wp-config.php
/saas/public/phpinfo.php
/saas/public/.env.backup
/saas/public/config.json
... (수백 개)

전형적인 보안 스캐닝/공격 시도들이었습니다. 봇들이 서버에서 민감한 파일을 찾으려고 무차별적으로 요청을 보내고 있었고, 이 모든 경로가 개별 메트릭으로 저장되고 있었습니다. Grafana Cloud Free tier의 시리즈 수 제한을 빠르게 소모하고 있었던 것입니다.

추가로 발견한 문제들

메트릭 코드를 분석해보니 다른 비효율도 있었습니다:

1. 중복 메트릭 수집

prom-client의 collectDefaultMetrics()가 이미 CPU, 메모리, 이벤트 루프 메트릭을 수집하고 있었는데, 커스텀 메트릭으로 동일한 데이터를 또 수집하고 있었습니다:

// 이미 자동 수집되는 메트릭들을 또 수집하고 있었음
this.memoryUsage = new client.Gauge({ name: 'blog_memory_usage_bytes', ... });
this.cpuUsage = new client.Gauge({ name: 'blog_cpu_usage_microseconds', ... });
this.eventLoopLag = new client.Gauge({ name: 'blog_event_loop_lag_milliseconds', ... });

2. 불필요한 5초 수집 루프

// 5초마다 CPU/메모리를 수집하는 루프가 있었음
setInterval(collectMetrics, 5000);

이것도 collectDefaultMetrics()와 중복이었고, 오히려 CPU 부하만 추가하고 있었습니다.

3. 서비스 구분 없음

백엔드가 여러 서비스(개인블로그, SaaS, 수영장 정보)를 처리하는데, 메트릭에서 이들을 구분할 방법이 없었습니다. 어떤 서비스가 느린지, 어디서 에러가 나는지 파악하기 어려웠습니다.

해결 방법

1. 공격 경로 필터링

404 응답이면서 알려진 엔드포인트가 아닌 경우, 그리고 공격 패턴에 매칭되는 경우 모두 /scan-attempt로 통합했습니다:

private normalizePath(path: string, statusCode?: number): string {
  // 404 + 알 수 없는 경로 → 통합
  if (statusCode === 404 && !this.isKnownEndpoint(path)) {
    return '/scan-attempt';
  }
  
  // 공격 패턴 감지
  if (this.isAttackPattern(path)) {
    return '/scan-attempt';
  }
  
  // 기존 정규화 로직...
}

private isAttackPattern(path: string): boolean {
  const attackPatterns = [
    /\.env/i,
    /\.git/i,
    /config\.(json|php|yml)/i,
    /\.php$/i,
    /wp-config/i,
    /backup\.(zip|tar|sql)/i,
    // ... 더 많은 패턴
  ];
  return attackPatterns.some(pattern => pattern.test(path));
}

수백 개의 공격 경로가 하나의 메트릭으로 통합되었습니다.

2. 서비스 라벨 추가

모든 HTTP 메트릭에 service 라벨을 추가해서 서비스별 분석이 가능하게 했습니다:

private getServiceFromPath(path: string): string {
  if (path.startsWith('/saas/')) return 'saas';
  if (path.startsWith('/swimming/')) return 'swimming';
  if (path.startsWith('/blog-posts') || path.startsWith('/admin')) return 'personal';
  if (path.startsWith('/metrics') || path.startsWith('/health')) return 'system';
  return 'other';
}

이제 Grafana에서 service="saas"로 필터링해서 글력 서비스만의 응답 시간을 볼 수 있습니다.

3. 중복 메트릭과 수집 루프 제거

collectDefaultMetrics()가 이미 수집하는 메트릭들과 5초 루프를 모두 제거했습니다. 코드가 간결해지고 CPU 부하도 줄었습니다.

결과

항목BeforeAfter
공격 경로 메트릭수백 개1개 (/scan-attempt)
중복 시스템 메트릭3개0개
5초 수집 루프있음없음
서비스 구분불가5개 라벨

교훈

돌이켜보면 몇 가지 교훈이 있었습니다:

1. 메트릭의 카디널리티를 항상 의식해야 한다
path, user_id, request_id 같은 고유값을 라벨로 쓰면 메트릭이 폭발합니다. 특히 공격 트래픽처럼 예측 불가능한 값은 반드시 필터링해야 합니다.

2. 기본 제공 메트릭을 먼저 확인하자
collectDefaultMetrics()가 이미 많은 것을 수집하고 있었는데, 확인하지 않고 중복으로 구현했습니다.

3. 공격 트래픽은 생각보다 많다
작은 개인 프로젝트라도 봇들은 가리지 않고 스캔합니다. 로깅과 메트릭에서 이런 노이즈를 걸러내는 게 중요합니다.

이제 Grafana 대시보드가 훨씬 깔끔해졌고, 실제로 의미 있는 메트릭만 남았습니다. 서비스별 응답 시간 비교도 가능해져서 성능 모니터링이 한결 수월해졌습니다.

PrometheusGrafana모니터링NestJS보안DevOps

관련 글

작은 서비스에도 모니터링이 필요한 이유 - NestJS + Prometheus + Grafana Cloud 구축기

작은 서비스도 리소스 제약 때문에 모니터링이 필수입니다. NestJS에 Prometheus와 Grafana Cloud를 연동하여 효율적인 리소스 관리 시스템을 구축하는 방법을 알아보세요.

관련도 93%

NestJS Cron으로 Grafana Cloud 서버 메트릭을 Slack에 자동 보고하기

NestJS 스케줄러와 Grafana Cloud Prometheus API를 연결해 매일 아침 서버 상태를 Slack으로 자동 보고하는 시스템을 구축한 과정을 정리했습니다. 이미 갖춰진 인프라를 활용해 최소한의 코드로 일일 리포트를 만드는 방법입니다.

관련도 91%

NestJS 일일 리포트에 GA4, GSC 데이터 통합하기

기존 Grafana Prometheus 기반 서버 리포트에 Google Analytics 4와 Search Console 데이터를 추가하여, 서버 상태부터 사용자 행동, 검색 성과까지 한눈에 파악할 수 있는 통합 일일 리포트를 구축한 과정을 정리했습니다.

관련도 88%