Skip to Content

닫는 글 — 선은 벽이 아니다

여는 글의 그 화면으로 돌아가 봅시다. Exited (137) 한 줄이요. 처음엔 앱을 의심했죠. 지금은 그 줄만 보고 확인할 게 세 갈래로 좁혀집니다.

{/* 삽화 자리: 바닥에 테이프로 그은 선이 있고 그 안에 사람이 서 있다. 여는 글과 같은 구도인데, 이번에는 그 사람이 선을 내려다보며 손가락으로 그 자리를 짚고 있다. 선은 여전히 벽이 아니라 바닥에 이어진 표시일 뿐이고, 선 밖으로도 바닥이 끊기지 않고 계속된다. 프롬프트: docs/book/illustration-prompts-container.md */}


같은 화면, 다른 질문

처음 그 137을 봤을 때 떠오른 질문은 “앱이 왜 죽었지”였습니다.

지금은 다릅니다.

docker inspect의 OOMKilled가 true인가요. 그러면 메모리 이야기입니다. JDK가 컨테이너를 인식하는 버전인지부터 보고(13장의 그 끝자리), 맞다면 힙만 보고 한도를 정했을 가능성이 큽니다. 힙 바깥이 힙보다 클 수도 있으니까요.

false라면 대개 유예가 끝난 겁니다. 앱이 스스로 exit(137)을 불러도 화면은 똑같으니 그건 로그가 갈라줘야 하고요.

유예가 끝난 게 맞다면 컨테이너 안에서 ps를 쳐서 PID 1이 누군지 봅니다. /bin/sh가 앉아 있으면 신호가 거기서 멈춘 거예요. 12장의 보호막이 그 셸을 죽지도 않게 붙잡아 두고요. 자바가 1번이면 이번엔 앱이 정리하는 데 걸리는 시간과 유예를 비교할 차례입니다.

쿠버네티스라면 그 유예의 이름이 terminationGracePeriodSeconds이고, 제가 안 썼어도 30이 들어가 있습니다.

숫자 하나가 같은 자리에서 다른 것을 묻게 만듭니다. 이 책이 한 일이 그겁니다.

정말로 하나였나

21장 끝에서 물었습니다. 1장의 chroot부터 여기까지 정말 하나였느냐고요.

같은 파일이 몇 번씩 이름을 바꿔 나왔습니다.

2부에서3부에서5부에서
memory.max에 숫자를 손으로 썼다 (7장)JVM이 그걸 읽어 힙을 정했다 (13장)limits.memory가 거기 들어갔다 (21장)
cpu.max라는 파일이 있다는 것만 봤다 (7장)그 두 숫자를 읽고 제동을 셌다 (14장)limits.cpu: 500m이 거기 들어갔다 (21장)
/proc/<pid>/ns/로 소속을 읽었다. 여덟 종류 중 일곱이 호스트와 달랐다 (5장)그 pid 칸의 1번 자리가 왜 특별한지 봤다 (12장)같은 목록을 앱과 pause에 찍어 Pod의 정체를 봤다 (18장)
unshare로 namespace를 손수 만들었다 (6장)—pause가 그걸 붙잡고 있었다 (18장)
포트 매핑이 iptables NAT 규칙이었다 (9장)—ClusterIP도 같은 자리에 있었다 (19장)

왼쪽에서 오른쪽으로 갈수록 이름이 길어지고 도구가 바뀝니다. 그런데 커널이 보는 파일은 그대로예요.

파일이 아니라 판정이 이름을 바꾼 것도 있습니다. 15장의 HEALTHCHECK는 결과를 적어두기만 했고, 20장의 readinessProbe는 같은 판정으로 트래픽을 끊었습니다. 바뀐 건 검사가 아니라 그 결과를 누가 읽느냐였어요. 16장의 내장 DNS가 19장의 Service로 바뀐 것도 같은 모양이고요.

5부에서 쿠버네티스가 새로 만든 건 거의 없습니다. 이미 있던 선을 어디에 그을지 정하는 방법과, 그걸 기계 여러 대에 걸쳐 관리하는 방법을 얹었을 뿐이죠.

그래서 2·3부를 건너뛰고 5부만 배우면 cpu.max의 50000 100000이 무슨 뜻인지 모르는 채로 limits.cpu를 조절하게 됩니다. p99가 튀는데 CPU 사용률은 50%인 이유도요.

선은 벽이 아니다

여는 글에서 이렇게 썼습니다.

컨테이너는 가상머신도 상자도 아닙니다. 커널이 한 프로세스 둘레에 그은 선이에요. 선은 벽이 아니어서, 어디에 어떻게 그었는지 모르면 시그널도 메모리도 그 선을 넘어 새어 나갑니다.

스물한 장을 지나오는 동안 그 문장이 세 가지 구체적인 뜻을 갖게 됐습니다. 그리고 그 위에 주제가 하나 더 얹혔고요.

선은 종류별로 따로 그어집니다. 1장의 chroot는 파일시스템에만 선을 그었고, 그래서 프로세스 목록도 호스트명도 시그널 보낼 권한도 다 그대로였습니다. 6장에서 unshare로 하나씩 켜보며 그걸 확인했죠. 하나를 그었다고 나머지가 따라 그어지지 않습니다.

선 안팎은 끊겨 있지 않습니다. 5장에서 호스트의 프로세스 목록에 컨테이너 안의 자바가 그대로 한 줄로 찍혀 있는 걸 봤습니다. 18장에서는 kind의 노드가 사실 컨테이너였고요. 선은 바닥에 그은 표시지 벽이 아닙니다.

선을 넘는 방향은 하나가 아닙니다. 12장의 시그널은 선 바깥에서 들어와 안의 첫 프로세스에게 전달됐습니다. 13장의 메모리는 선 안에서 자라 한도를 넘었고요. 16장의 DNS는 선을 넘어 이름을 물어봤습니다. 방향이 다 다릅니다.

여기에 주제가 하나 얹힙니다. 대부분의 선은 내가 안 그었다는 것. 여는 글의 유예 3초는 데몬 설정에 있었고, shell form에서 셸이 남느냐는 베이스 이미지의 /bin/sh가 정했습니다. 18장의 tolerationSeconds: 300, 21장의 terminationGracePeriodSeconds: 30, QoS 등급, maxSurge: 25% — 전부 제 매니페스트에 없던 값입니다.

그러니까 실무에서 하는 일의 상당 부분은 선을 긋는 게 아니라 이미 그어진 선이 어디 있는지 찾는 것입니다. docker inspect를 열고, cgroup 파일을 읽고, kubectl get pod -o yaml을 뒤지는 게 그 일이에요.

이 책이 안 한 것

정직하게 적어둡니다.

쿠버네티스는 Pod·Deployment·Service·프로브·resources·롤링 업데이트까지만 다뤘습니다. Ingress도, StatefulSet도, Helm도, Operator도, 네트워크 정책도 안 나왔어요. 5부는 “K8s 입문”이 아니라 “3부까지 배운 것이 K8s에서 어떤 이름으로 나오는지”였습니다.

10장에서 본 볼륨이 쿠버네티스에서 PV·PVC로 어떻게 갈라지는지도 안 다뤘습니다. 상태를 가진 컨테이너는 통째로 다음 이야기예요.

보안 축도 빠졌습니다. 5장에서 여덟 종류 중 user만 호스트와 같다는 걸 보여주고 그냥 넘어갔죠. rootless 컨테이너나 capability를 떼어내는 이야기가 거기서 시작하는데, 이 책은 그쪽으로 안 갔습니다.

배포 파이프라인도 통째로 빠졌습니다. 블루그린이나 카나리, CI에서 이미지를 굽고 레지스트리에 올리는 일, GitOps 같은 것들이요. 17장에서 latest 태그의 정체까지만 가고 멈춘 게 그 경계입니다.

스레드 풀과 블로킹은 『잠드는 스레드』 몫이라 14장에서 차이만 짚고 링크로 넘겼고요.

그리고 이 책의 실험은 전부 제 노트북 한 대에서 돌았습니다. 노드가 수백 개인 클러스터에서는 21장에서 잰 0.08초 같은 숫자가 훨씬 커집니다. 노드마다 kube-proxy가 규칙을 다시 써야 하고, 제일 늦은 노드가 끝나야 끝나거든요. 노드가 많을수록 그 꼬리가 길어집니다. 그 이야기는 제가 재보지 못했습니다.

다음에 할 것

세 가지를 권합니다.

첫째, 지금 운영 중인 앱의 Dockerfile 마지막 줄을 열어보세요. 대괄호가 있는지요. 없다면 베이스 이미지의 /bin/sh가 누구인지까지 확인해야 합니다. 여는 글의 베이스 이미지 표가 그 확인표입니다.

둘째, 컨테이너 하나에 들어가 /sys/fs/cgroup/memory.max와 cpu.max를 읽어보세요. 거기 적힌 숫자가 내가 의도한 값인지 보고, java -XX:+PrintFlagsFinal -version | grep MaxHeapSize로 JVM이 힙을 얼마나 잡았는지 재서 비교해 보세요. 13장의 그 간격이 보입니다.

셋째, kubectl get pod <이름> -o yaml을 열고 내가 안 쓴 필드를 찾아보세요. tolerations, terminationGracePeriodSeconds, qosClass, imagePullPolicy. 그게 이 책이 가르치려던 습관에 제일 가깝습니다.


여는 글의 마지막에 이렇게 썼습니다. 자바 프로세스는 정말 어딘가 안에 들어가 있는 거냐고요.

이제 답할 수 있습니다. 안에 들어간 게 아닙니다. 그 프로세스는 계속 같은 커널 위에서 돌고 있었고, 달라진 건 그 프로세스가 볼 수 있는 것과 쓸 수 있는 양뿐이었습니다.

선이 그어졌을 뿐이에요.

그리고 선은 벽이 아닙니다.

▶ 목차로 돌아가기

Last updated on