LabHub
배우기 러닝패스 코스

요청 하나가 아니라 전부가 느려졌다 · 관측과 경보 · 이론

무엇을 남기고, 무엇에 깨울 것인가

LabHub 에서 이어서 보기

한 줄 요약

루프 지연과 힙 사용은 프로세스 안에서만 정확히 보인다. 밖에서 보는 CPU 사용률은
막힌 프로세스와 바쁜 프로세스를 구분하지 못한다. 그래서 지표를 안에서 뽑아 남기고,
경보는 평균이 아니라 꼬리와 지속 시간에 건다.

왜 이게 필요했나

루프가 막혀 있는 동안 CPU 사용률은 100% 에 가깝다. 그런데 일을 아주 잘하고 있을
때도 100% 다. 밖에서 보는 지표만으로는 둘을 구분할 수 없고, 그래서 "CPU 가 높다"
는 경보는 이 사고에 대해 아무것도 말해 주지 않는다. 반대로 루프가 막혀 응답을
못 하는데 CPU 는 한산한 경우도 있다 — 스레드 풀이 꽉 차 기다리고 있을 때다.

지연 로그도 마찬가지로 부족하다. 응답 시간은 이미 일어난 일의 결과이고, 그
결과를 보고 원인을 되짚으려면 "같은 시각에 다른 요청도 느렸는가" 를 사람이 눈으로
맞춰 봐야 한다. 프로세스 안에서 루프가 멈춘 시간 자체를 재 두면 그 단계를
건너뛴다. 느려진 요청 목록 대신 "12시 04분 20초에 루프가 410ms 멈췄다" 는 한 줄이
남는다.

어떻게 동작하나

Node 는 이 목적의 도구를 두 개 준다. 하나는 앞에서 본
perf_hooks.monitorEventLoopDelay() 이고, 다른 하나는
performance.eventLoopUtilization() 이다. 둘은 서로 다른 질문에 답한다.

monitorEventLoopDelay()얼마나 늦었는가 를 분포로 준다. 히스토그램이라
percentile(99)max 를 바로 꺼낼 수 있고, 값은 나노초이며 표본 간격의
기본값은 10ms 다(1번 모듈에서 본 바닥이 여기서 온다). 주기적으로 읽고
reset() 하면 구간별 분포가 된다.

eventLoopUtilization()얼마나 바빴는가 를 준다. 공식 문서는 이 값을
"이벤트 루프가 이벤트 제공자(예: epoll_wait) 밖에서 보낸 시간의 비율" 이라고
설명한다([perf_hooks](https://nodejs.org/docs/latest-v22.x/api/perf_hooks.html)).
CPU 사용률과 비슷해 보이지만 다른 값이다 — 루프 통계만 보고 CPU 는 보지 않는다.
이전 호출의 결과를 인자로 넘기면 그 사이의 변화량을 돌려주므로, 구간 이용률을
얻는 데 뺄셈을 직접 할 필요가 없다. 이 API 는 Node 14.10.0 에서 들어왔다.

메모리 쪽은 process.memoryUsage() 가 맡는다. rss 는 운영체제가 이 프로세스에
붙여 둔 물리 메모리이고, heapUsed 는 V8 이 실제로 쓰고 있는 양이다. 배압
사고에서는 둘이 함께 올라가고, 특히 스트림 버퍼에 쌓인 Buffer 는
external 쪽에서도 보인다. 4번 모듈의 실측에서 배압을 무시한 쪽은 RSS 가
131MB 늘었고 지킨 쪽은 그러지 않았다 — 같은 코드, 같은 입력, 한 줄 차이다.

const h = monitorEventLoopDelay({ resolution: 20 });h.enable();setInterval(() => {  const p99ms = h.percentile(99) / 1e6;      // 나노초로 나온다  const maxMs = h.max / 1e6;  const rssMB = process.memoryUsage().rss / 1048576;  h.reset();                                  // 다음 구간을 위해 비운다  // 여기서 남긴다 — 로그 한 줄이든 지표 노출이든}, 10000);

현장에서 만나는 모습

경보를 어디에 걸지가 실제로 어려운 부분이다. 세 가지를 지키면 대체로 조용하면서
쓸모 있는 경보가 된다.

첫째, 평균에 걸지 않는다. 1번 모듈에서 봤듯 300ms 멈춤 한 번은 평균을 거의
움직이지 않는다. p99 와 max 를 쓴다.

둘째, 한 번 튄 것으로 깨우지 않는다. 시작 직후의 컴파일, 가끔 도는 큰 GC,
배포 직후의 캐시 채우기에서 큰 값이 한 번씩 나온다. "p99 가 임계를 넘은 상태가
5분 이상 이어지면" 처럼 지속 시간을 조건에 넣는다. 한 번의 튐은 기록으로 남기고
깨우지는 않는다.

셋째, 임계값은 재서 정한다. 이 코스의 실습에서 각자 기준선을 잰 이유가 그것이다.
조용한 상태의 p50 이 얼마인지, 정상 부하에서 p99 가 어디에 있는지 모르면 임계값은
추측이 된다. 기준선의 몇 배, 또는 응답 시간 예산의 몇 분의 일 같은 근거가 있어야
나중에 사람이 그 숫자를 고칠 수 있다.

메모리 경보도 같은 원칙이다. 절대값보다 증가 속도가 낫다. 배압 사고는
한도까지 선형으로 올라가므로, 4번 모듈에서 만든 산수로 "지금 속도면 몇 분 뒤"
를 계산해 두면 한도에 닿기 전에 손을 쓸 수 있다. 한도에 닿은 뒤에 오는 경보는
이미 재시작된 프로세스에 대한 부고에 가깝다.

마지막으로, 이 지표들은 프로세스마다 따로 봐야 한다. 워커를 여럿 띄우거나
클러스터로 돌리면 한 프로세스만 막혀도 전체 평균은 멀쩡하다. 지표에 프로세스
식별자를 붙이지 않으면 4번 모듈에서 본 것과 같은 종류의 눈속임이 생긴다.

다음 퀴즈에서 확인할 것

루프 지연과 이용률이 각각 어떤 질문에 답하는지, 왜 밖에서 보는 CPU 사용률로는
이 사고를 가릴 수 없는지, 그리고 경보를 평균이 아니라 꼬리와 지속 시간에 거는
이유를 확인합니다. 앞 네 모듈에서 직접 잰 숫자들이 그대로 근거가 됩니다.