What Do You Look At First When a Container Dies
한국어 원문으로 표시합니다.
한 줄 요약
컨테이너 진단은 로그 → 종료코드 → 상태 필드 세 개를 순서대로 보는 일입니다. 이 셋으로 안 풀리면 그때 컨테이너 안으로 들어갑니다.
왜 이게 필요했나
"컨테이너가 안 떠요" 라는 신고는 실제로는 최소 네 가지 서로 다른 상황을 한 문장으로 뭉쳐 놓은 것입니다. 이미지가 없거나, 명령이 없거나, 앱이 설정을 못 읽어 스스로 끝냈거나, 커널이 죽였거나. 이 넷은 조치가 전부 다르고, 다행히 아주 싼 신호로 구분 됩니다.
종료코드가 가장 먼저 범위를 좁힌다
종료코드 하나로 조사 범위가 절반이 됩니다. 규칙은 단순합니다. 128 보다 크면 신호로 죽은 것이고, 빼기 128 이 신호 번호입니다.
| 코드 | 뜻 | 먼저 볼 곳 |
|---|---|---|
| 0 | 정상 종료 | 앱이 할 일을 마치고 끝났다. 서버라면 이것도 이상하다 |
| 1 | 앱이 낸 일반 오류 | 로그 마지막 줄 |
| 125 | 도커 자체의 오류 | 명령 옵션이 틀렸다 |
| 126 | 명령을 실행할 수 없음 | 실행 권한이 없다 |
| 127 | 명령을 찾을 수 없음 | ENTRYPOINT 경로 오타, 또는 셸이 없는 이미지 |
| 137 | SIGKILL (128+9) | OOM 인지 아닌지 반드시 구분 |
| 139 | SIGSEGV (128+11) | 네이티브 라이브러리 충돌 |
| 143 | SIGTERM (128+15) | 정상 종료 요청을 받았다. 사고가 아닐 수 있다 |
137 이 가장 헷갈립니다. 커널의 OOM killer 가 죽인 것일 수도 있고, 사람이
docker kill 한 것일 수도 있습니다. 구분은 State.OOMKilled 한 줄입니다.
docker inspect dk-err --format '{{.State.ExitCode}} {{.State.OOMKilled}}'
137 true
true 면 메모리 한도를 올리거나 앱의 사용량을 줄이는 문제이고, false 면
누가 왜 죽였는지를 찾는 문제입니다. 완전히 다른 조사입니다.
로그를 읽는 법
컨테이너의 표준 출력과 표준 오류는 런타임이 가로채 저장합니다. 그래서
컨테이너가 이미 죽었어도 docker logs 로 마지막 순간을 볼 수 있습니다.
두 스트림은 섞여서 보이지만 실제로는 분리되어 있어서, 셸 리다이렉션으로
따로 뽑아낼 수 있습니다.
docker logs dk-err 2>/dev/null # 표준 출력만
docker logs dk-err 2>&1 1>/dev/null # 표준 오류만
docker logs --tail 50 -t dk-err # 마지막 50줄에 시각을 붙여서
docker logs --since 10m dk-err # 최근 10분만
시각(-t)을 붙이는 것이 중요합니다. 로그 자체에 시각이 없는 앱이 많고,
그러면 "이 오류가 죽기 직전 것인가 한참 전 것인가" 를 알 수 없습니다.
docker inspect 는 JSON 하나에 상태 전체를 담고 있습니다. 진단에서
실제로 쓰는 필드는 몇 개 되지 않습니다.
| 필드 | 무엇을 말해 주나 |
|---|---|
State.Status |
created / running / exited / paused |
State.ExitCode |
앱이 낸 값인지, 신호로 죽었는지 |
State.Error |
런타임이 아예 시작하지 못한 이유 |
State.OOMKilled |
137 이 OOM 때문인지 |
State.StartedAt / FinishedAt |
몇 초 만에 죽었는지 — 즉사면 설정 문제다 |
RestartCount |
조용히 재시작을 반복하고 있는지 |
StartedAt 과 FinishedAt 의 차이가 1초 미만이면 앱 로직이 아니라
시작 조건 의 문제입니다. 설정 파일, 환경변수, 포트 충돌 순으로 봅니다.
현장에서 만나는 모습
가장 자주 밟는 함정은 로그가 비어 있는 경우 입니다. 원인은 셋입니다.
- 앱이 파일로 로그를 씁니다. 컨테이너에서는 로그를 파일이 아니라 표준
출력으로 내보내는 것이 원칙인데, 이 규칙이 지켜지지 않으면 진단 도구
전체가 무력해집니다. 임시 조치로
docker exec해서 파일을 읽을 수는 있지만, 컨테이너가 죽으면 그마저 못 합니다. - 버퍼링에 갇혔습니다. 파이썬은 표준 출력이 터미널이 아니면 블록
버퍼링을 합니다. 4KB 가 차기 전에 죽으면 그 안의 로그가 통째로 사라집니다.
PYTHONUNBUFFERED=1이나python -u로 끕니다. Node 는 기본이 비버퍼링, Java 는System.out이 줄 단위라 대개 괜찮습니다. - 로그 드라이버가 다릅니다.
--log-driver가none이거나 외부 수집기로 설정돼 있으면docker logs는 아무것도 못 보여 줍니다.
두 번째 함정은 셸이 없는 이미지 입니다. distroless 나 scratch 기반
이미지에는 sh 가 없어서 docker exec -it ... sh 가 동작하지 않습니다.
OCI runtime exec failed: exec: "sh": executable file not found 를 보고
"컨테이너가 이상하다" 고 판단하면 안 됩니다. 정상입니다.
이럴 때는 같은 네임스페이스에 도구가 든 컨테이너를 붙여서 들여다봅니다.
docker run -it --rm --pid container:myapp --network container:myapp nicolaka/netshoot
--pid 로 프로세스를, --network 로 네트워크를 공유하므로 ps 와 ss 가
대상 컨테이너의 것을 그대로 보여 줍니다. 파일 시스템은 공유되지 않으므로
/proc/1/root/ 를 통해 봅니다.
세 번째는 재시작 반복 입니다. RestartCount 가 계속 늘고 있으면
docker logs 는 지금 인스턴스의 것만 보여 줍니다. 죽은 직전 인스턴스의
로그를 봐야 하는데, 도커에는 쿠버네티스의 --previous 같은 옵션이 없습니다.
--restart 정책을 잠시 no 로 바꿔 한 번만 뜨게 하고 조사합니다.
다음 실습에서 할 것
로그를 꼬리부터 잘라 보고, 두 스트림을 분리하고, 죽은 컨테이너의
종료코드와 로그를 함께 놓고 원인을 특정하는 연습을 합니다. 메모리 한도를
낮게 걸어 137 을 직접 만들어 보고, OOMKilled 가 true 로 뜨는 것을
확인합니다.