디버깅 실전 · 자원이 바닥날 때의 증상 · 이론
바닥난 것은 여기인데 오류는 저기서 난다
한 줄 요약
자원이 바닥나면 새게 만든 코드가 아니라 그 뒤에 자원을 필요로 한 코드가 터진다. 그래서 트레이스백은 무고한 모듈을 가리키고, 조사는 계측으로만 끝난다.
왜 이게 필요했나
"오후 세 시쯤이면 죽습니다" 는 자원 고갈 신고의 전형적인 문장이다. 아침에는 멀쩡하고, 부하와도 정확히 비례하지 않고, 재시작하면 한동안 괜찮다. 마지막 조건이 결정적인 단서다 — 재시작이 고친다면 쌓이는 무언가가 있다.
그런데 로그를 열면 범인이 아니라 피해자가 보인다. 파일 기술자를 흘린 것은 세션 처리기인데, 오류는 데이터베이스 연결에서 나거나, 하위 프로세스 실행에서 나거나, 로그 파일을 여는 자리에서 난다. 기술자를 새게 만든 코드는 이미 자기 몫을 다 가져갔고, 바닥이 드러난 순간 그다음에 자원을 요청한 코드가 실패하기 때문이다.
이 어긋남 때문에 조사가 엉뚱한 곳으로 간다. "sqlite 가 데이터베이스 파일을 열지 못한다" 는 메시지를 보고 디스크와 권한과 경로를 몇 시간 뒤진다. 파일은 멀쩡하고 권한도 맞다. 그 프로세스가 더는 어떤 파일도 열 수 없을 뿐이다.
어떻게 동작하나
자원 고갈 조사의 뼈대는 세 가지다.
1. 한도를 안다. 리눅스는 프로세스마다 자원 상한을 걸고, 파이썬에서는 [resource 모듈](https://docs.python.org/3/library/resource.html)로 읽고 쓴다. RLIMIT_NOFILE 은 동시에 열 수 있는 기술자 수, RLIMIT_AS 는 주소 공간의 크기다. [getrlimit(2)](https://man7.org/linux/man-pages/man2/getrlimit.2.html)가 정하듯 상한은 소프트와 하드 두 값이고, 소프트는 하드 이하에서 프로세스가 스스로 낮출 수 있다. 낮출 수 있다는 점이 조사에 쓰인다 — 열 시간 뒤에 만날 바닥을 지금 만들 수 있다.
2. 쌓이는 것을 센다. 리눅스는 [proc(5)](https://man7.org/linux/man-pages/man5/proc.5.html)에 프로세스마다 /proc/<pid>/fd 디렉터리를 두고 열린 기술자마다 항목 하나를 놓는다. 그 항목 수를 세면 지금 몇 개를 들고 있는지 알 수 있다. 요청 수를 바꿔 가며 이 값을 재면 점 몇 개가 나오고, 그 점들이 직선 위에 있으면 기울기가 곧 요청 하나당 새는 개수다. 기울기가 0 이면 새지 않는다. 이것이 누수를 "느낌" 이 아니라 자료로 만드는 방법이다.
3. 증상과 원인을 갈라 적는다. 바닥난 뒤의 오류 메시지는 자원의 종류를 말해 줄 뿐 누가 썼는지는 말하지 않는다. 그래서 보고서에는 둘을 따로 적는다 — 관측된 증상 목록과, 계측으로 밝힌 원인.
한도 무엇을 막나 바닥났을 때의 얼굴RLIMIT_NOFILE 열린 파일·소켓 수 OSError errno 24 (EMFILE), 그리고 자원을 쓰는 남의 코드의 오류RLIMIT_AS 주소 공간 크기 MemoryError (트레이스백이 남는다)커널 OOM 킬러 머신 전체의 메모리 SIGKILL (트레이스백이 남지 않는다)메모리 쪽에서 특히 중요한 구분이 이 표의 마지막 두 줄이다. 한도에 걸린 할당은 예외로 올라와 트레이스백을 남기지만, 커널이 죽이는 경우는 프로세스가 SIGKILL 로 사라져 마지막 로그조차 남지 않는다. "로그가 중간에 끊겼다" 는 신고는 그래서 그 자체로 단서다. 다만 RLIMIT_AS 는 주소 공간의 상한이라 실제 사용량과 같지 않다는 점을 함께 적어야 한다 — 매핑만 하고 건드리지 않은 영역도 주소 공간은 차지한다.
현장에서 만나는 모습
첫째, 오류 메시지의 낱말을 검색한다. "unable to open database file" 로 검색하면 권한과 경로 이야기가 잔뜩 나온다. 전부 맞는 이야기지만 이 사건과는 상관이 없다. 자원 고갈을 의심하는 신호는 메시지가 아니라 패턴이다 — 시간이 지날수록 나빠지고, 재시작이 고치고, 서로 다른 모듈이 번갈아 실패한다.
둘째, 한 번의 관측으로 결론을 낸다. "지금 기술자가 900개입니다" 는 그 자체로 아무 말도 아니다. 원래 몇 개였는지, 요청이 늘 때 어떻게 변하는지를 함께 재야 한다. 두 점만 있어도 기울기가 나온다.
셋째, 한도를 올려 덮는다. 한도를 올리면 죽는 시각이 오후 세 시에서 밤 열 시로 옮겨 갈 뿐이다. 새는 쪽을 고치지 않으면 언젠가 반드시 다시 만난다. 다만 한도를 낮추는 것은 조사 도구로 아주 쓸모 있다 — 열 시간 걸릴 재현을 몇 초로 줄인다.
넷째, 파일만 센다. 기술자는 파일만이 아니다. 소켓, 파이프, 이벤트 알림, 그리고 하위 프로세스를 띄울 때 잠깐 쓰는 것까지 같은 한도를 나눠 쓴다. 그래서 바닥난 프로세스는 파일을 못 여는 것이 아니라 아무것도 못 한다.
실무에서 진짜 중요한 것
- 재시작이 고치면 쌓이는 것이 있다. 그 문장을 조사의 출발점으로 삼는다.
- 누수는 기울기로 증명한다. 한 번의 관측은 자료가 아니다.
- 한도를 낮춰 바닥을 앞당긴다. 재현 시간을 시간 단위에서 초 단위로 줄인다.
- 증상과 원인을 따로 적는다. 트레이스백이 가리키는 모듈은 대개 무고하다.
다음 실습에서 할 것
세션 처리기를 받아 열린 기술자 수를 세는 도구를 만들고, 요청 수를 바꿔 가며 재 요청 하나당 새는 개수를 기울기로 구한다. 한도를 낮춰 바닥을 앞당겨 재현하고, 바닥난 뒤 서로 다른 네 가지 일이 각각 어떤 얼굴로 실패하는지 기록한다. 메모리 쪽도 주소 공간 한도로 안전하게 재현해 예외로 받는 것과 죽임당하는 것의 차이를 적고, 마지막으로 고친 판의 기울기가 0 임을 같은 도구로 증명한다.