퇴근 유예도 예산이다 — preStop과 TERM 사이
한 줄 요약
preStop은 종료 유예 밖의 보너스 시간이 아닙니다. 종료 대상 Pod의 훅과 애플리케이션의 신호 처리, 남은 작업 시간을 함께 설계해야 합니다.
왜 이게 필요했나
readiness를 달고 preStop에서 잠깐 기다렸으니 무조건 안전하다고 생각하기 쉽습니다. 하지만 훅 뒤에 프로세스가 곧바로 종료하면 이미 받은 요청은 잘릴 수 있고, 프로세스가 요청을 기다려도 종료 유예가 먼저 소진되면 끝낼 수 없습니다. '설정을 넣었다'와 '원하는 결과가 났다' 사이를 메우는 것이 이번 실습의 목적입니다.
설정이 어느 Pod에 들어 있는지도 중요합니다. Deployment 템플릿에 새로 추가한 preStop은 그 템플릿으로 생성하는 Pod의 설정입니다. 지금 교체되어 내려가는 옛 Pod에 소급해서 들어간 것으로 해석하면 실험 비교가 틀립니다. 삭제 직전 대상 Pod의 UID와 spec.containers의 lifecycle을 읽어 보존해야 합니다. 새 템플릿만 캡처해 둔 보고서는 무엇이 실제로 실행됐는지 말해 주지 못합니다.
종료 유예가 10초이고 preStop 이 3초 쉬는 Pod 를 지운다고 하자. 시계는 한 번 시작해 한 번만 돈다.
- 삭제와 함께 시계가 돈다종료 유예 10초의 계산은 훅이 실행되기 전에 시작된다. 같은 무렵 컨트롤 플레인은 이 Pod 를 EndpointSlice 에서 빼는 절차를 밟는다.
- preStop 이 3초를 쓴다훅이 끝나야 종료 신호가 간다. 이 3초는 따로 적립되지 않고 10초에서 빠진다.
- TERM 뒤에 남은 시간앱은 새 요청을 거절하고 처리 중인 것을 끝내야 한다. 유예가 다 되도록 끝나지 않으면 강제로 종료된다.
여기서 구분할 것 훅이 도는 동안에도 이미 받은 요청은 계속 처리될 수 있다. 그래서 남은 작업을 단순히 더하지 말고 수락·삭제·훅·TERM·완료 시각을 따로 남겨 확인한다.
잠깐, 예측해 보세요 요청 하나가 최대 8초 걸리는데 종료 유예 10초에 preStop 5초 대기를 넣었다. 받은 요청은 모두 끝낼 수 있을까?
설명 확인 · 채점 없는 자가 점검
보장되지 않는다. 훅이 도는 동안 막 받은 요청은 8초를 채우기 전에 유예가 끝나 강제 종료될 수 있다. 훅 시간과 작업 상한을 같은 예산 안에서 계산해 유예를 정한다.
어떻게 동작하나
일반적인 TERM 종료 흐름에서 kubelet은 preStop 훅을 실행하고, 훅이 끝난 뒤 컨테이너 프로세스에 종료 신호를 보냅니다. 종료 유예 계산은 훅이 실행되기 전에 시작되므로 훅과 그 이후 정리를 같은 예산 안에 넣어야 합니다. 훅이 3초를 썼다면 그 3초가 애플리케이션에 별도로 적립되는 것이 아닙니다. 무작정 긴 sleep을 넣는 방법은 새 요청 제외에 시간을 줄 수 있지만 종료 예산도 소비합니다.
여기서 시간 합산을 잘못하기도 합니다. 이미 시작한 HTTP 작업은 훅이 실행되는 동안에도 진행될 수 있습니다. 따라서 전체 작업 시간이 10초이고 훅이 3초라고 해서 언제나 13초의 잔여 작업이 있는 것은 아닙니다. 설계할 때는 훅 실행 시간과 TERM 뒤 실제로 남는 처리·정리 시간의 상한을 따지고, 관측할 때는 수락·삭제·훅·TERM·완료의 시각을 따로 남깁니다. 작업 상한이 없다면 유예만 크게 늘려서는 안전한 종료를 보장할 수 없습니다.
이번 서버의 graceful 모드는 TERM을 받으면 새 work 요청을 거절하고, active가 0이 될 때까지 기다린 뒤 종료합니다. immediate 모드는 비교를 위해 종료 코드 17로 즉시 끝납니다. 코드 17 자체가 Kubernetes의 표준 종료 의미인 것은 아닙니다. 실습 서버가 일부러 고른 관측 표식입니다. 정상 완료에서는 응답 본문을 끝까지 보내고 종료 코드 0을 관측합니다.
graceful이어도 유예가 부족하면 컨테이너가 강제로 종료될 수 있습니다. 이번에 앞서 수행한 실제 VM 탐침에서는 유예 부족 시 137을 관측했습니다. 그러나 현장에서 137만 보고 종료 유예 때문이라고 단정하지 마세요. OOM 같은 다른 원인도 조사해야 합니다. Pod watch의 종료 사유, 실제 유예 설정, 신호 전후 시각, 요청 결과를 함께 연결하는 것이 핵심입니다.
현장에서 만나는 모습
kubectl delete를 실행한 벽시계 시각과 kubelet이 종료를 처리하기 시작한 시각은 정확히 같지 않습니다. API 처리, 관측 전달, 런타임 작업에도 지연이 있습니다. 그래서 '유예 5초니까 삭제 명령 후 정확히 5초에 연결이 끊겨야 한다'는 채점은 정상 환경에서도 틀릴 수 있습니다. 이번 실습은 소수점 몇 초를 맞히게 하지 않고, 수락된 동일 요청의 결과와 종료 상태를 검사합니다.
채점 시점에 Pod를 다시 조회하는 것만으로도 부족합니다. 이미 삭제된 Pod는 없고, 종료된 컨테이너도 런타임에서 회수됐을 수 있습니다. 탐침의 첫 구현에서는 종료 뒤에 확인하려다 근거를 잃었습니다. 그래서 요청을 보내기 전 Pod watch를 시작하고, 최종 컨테이너 상태와 DELETED 이벤트를 보존합니다. 관측 파일을 읽는 재채점은 Pod를 재생성하지 않습니다. 같은 실험을 몰래 한 번 더 실행하면 이전 결과를 검사한 것이 아니기 때문입니다.
종료 실험의 결과를 채점하려고 Pod 의 최종 상태를 확인한다. 두 방법은 기록을 시작하는 시점만 다르다.
- 끝난 뒤에 조회한다Pod 는 이미 지워졌고 종료된 컨테이너도 런타임이 회수했을 수 있다. 무엇이 실행됐는지 말해 줄 근거가 남아 있지 않다.
- 요청 전에 watch 를 건다삭제 직전 대상 Pod 의 UID 와 lifecycle, 최종 컨테이너 상태, DELETED 이벤트가 파일로 남는다. 재채점은 이 파일만 읽는다.
여기서 구분할 것 재채점을 위해 같은 실험을 몰래 한 번 더 돌리면 이전 결과를 검사한 것이 아니다. 새 템플릿만 캡처한 보고서도 교체되어 내려간 옛 Pod 에 무엇이 들어 있었는지 말해 주지 못한다.
잠깐, 예측해 보세요 유예를 5초로 두고 지웠으니 삭제 명령 후 정확히 5초에 연결이 끊겼는지로 채점하면 될까?
설명 확인 · 채점 없는 자가 점검
안 된다. API 처리와 관측 전달, 런타임 작업에 지연이 있어 벽시계 시각이 정확히 맞지 않는다. 수락된 같은 요청의 결과와 종료 상태를 근거로 판정한다.
다음 실습에서 할 것
정상 종료 설정에서 유예를 줄여 요청을 끊어 보고, 다시 유예를 보완해 같은 길이의 작업이 완료되는지 확인합니다. 이론을 읽고 예상한 결과와 실제 관측을 비교하되, 특정 한 번의 응답을 모든 운영 트래픽의 무중단 보증으로 확대하지 마세요. 요청 실패 뒤 자동 재시도를 붙일 때는 앞 단원의 Job 멱등성처럼 업무 부작용도 고려해야 합니다.
공식 문서: 컨테이너 생명주기 훅, Pod 종료 흐름.