"Coolify에 배포한 백엔드가 갑자기 느려졌는데, CPU 때문인지 메모리 때문인지 어떻게 알 수 있지?"
개인 블로그 플랫폼을 운영하면서 마주친 질문이었다. 트래픽이 많지 않은 작은 서비스다. 모니터링 시스템을 구축하는 게 과한 건 아닐까? 그런 생각이 들었다.
하지만 곰곰이 생각해보니, 오히려 작은 서비스이기 때문에 모니터링이 필요했다.
리소스가 제한된 환경에서는 작은 변화도 큰 영향을 미친다. Coolify에서 CPU 0.5코어, 메모리 512MB로 운영하는 서비스라면, 메모리 누수 100MB는 전체 용량의 20%에 해당한다.
모니터링 없이는 다음 질문들에 답할 수 없다:
이런 판단을 위해서는 데이터가 필요하다. 그래서 모니터링 시스템을 구축하기로 했다.
Prometheus와 Grafana 조합을 선택했다. 처음에는 이 조합이 완전한 APM(Application Performance Monitoring)이라고 생각했는데, 정확히 말하면 그렇지 않다.
Observability(관측 가능성)는 세 가지 축으로 구성된다:
Prometheus + Grafana는 이 중 Metrics만 담당한다. 완전한 APM이 되려면 분산 추적, 코드 레벨 프로파일링, 에러 추적 등이 추가로 필요하다.
하지만 작은 서비스의 시작점으로는 충분하다. 나머지는 필요할 때 추가하면 된다.
NestJS 백엔드에 prom-client 라이브러리를 설치하고, PrometheusService를 만들었다.
// prometheus.service.ts
import { Injectable, OnModuleInit } from '@nestjs/common';
import * as client from 'prom-client';
@Injectable()
export class PrometheusService implements OnModuleInit {
private readonly register: client.Registry;
constructor() {
this.register = new client.Registry();
client.collectDefaultMetrics({ register: this.register });
}
async getMetrics(): Promise<string> {
return this.register.metrics();
}
getContentType(): string {
return this.register.contentType;
}
}
/metrics/prometheus 엔드포인트를 추가하면, Prometheus 형식의 메트릭을 수집할 수 있다. 기본적으로 Node.js 런타임 메트릭(메모리 사용량, 이벤트 루프 지연, GC 통계 등)이 자동으로 수집된다.
메트릭 엔드포인트를 그대로 공개하면 보안 문제가 된다. 시스템 내부 정보가 노출되기 때문이다.
두 가지 인증 방식을 지원하는 Guard를 만들었다:
// metrics-auth.guard.ts
@Injectable()
export class MetricsAuthGuard implements CanActivate {
canActivate(context: ExecutionContext): boolean {
// 개발 환경에서는 인증 없이 접근 가능
if (this.isDevelopment) {
return true;
}
const request = context.switchToHttp().getRequest<Request>();
const clientIp = this.getClientIp(request);
// IP 화이트리스트 체크
if (this.isIpAllowed(clientIp)) {
return true;
}
// Basic Auth 체크
if (this.isBasicAuthValid(request)) {
return true;
}
throw new UnauthorizedException('Metrics endpoint requires authentication');
}
}
Grafana Cloud와 연동할 때는 Basic Auth를 사용했다. IP 화이트리스트는 Grafana Cloud의 IP가 고정되지 않아 사용하기 어렵다.
Grafana Cloud의 무료 티어는 개인 프로젝트에 충분하다:
연동 방식은 Metrics Endpoint Integration을 선택했다. 별도의 에이전트 없이, Grafana Cloud가 직접 엔드포인트를 스크래핑하는 방식이다.
설정 과정:
https://api.example.com/metrics/prometheusCoolify 환경변수에 METRICS_USERNAME과 METRICS_PASSWORD를 설정하고, 재배포하면 연동이 완료된다.
스크래핑 간격을 얼마로 설정할지 고민했다. 너무 짧으면 불필요한 부하가 생기고, 너무 길면 순간적인 스파이크를 놓칠 수 있다.
결론은 60초가 적절하다는 것이었다:
나중에 트래픽이 늘어나면 15-30초로 줄이는 것을 고려하면 된다.
연동 후 Grafana Cloud의 Explore에서 다음 메트릭들을 확인할 수 있게 되었다:
process_resident_memory_bytes - 실제 메모리 사용량nodejs_eventloop_lag_seconds - 이벤트 루프 지연nodejs_heap_size_used_bytes - 힙 메모리 사용량nodejs_gc_duration_seconds - GC 소요 시간이제 "메모리가 왜 늘어났지?"라는 질문에 데이터로 답할 수 있다.
이번 작업을 통해 몇 가지를 배웠다:
다음 단계로는 커스텀 메트릭(API 응답 시간, 에러율 등)을 추가하고, 알림 규칙을 설정할 계획이다. 하지만 그건 실제로 필요해질 때 하면 된다.
일단은, 데이터를 볼 수 있게 된 것만으로 충분하다.