알툴즈 AI API의 JVM 메모리 장애에서는 Heap 사용량이 정상인데 RSS가 증가해 OOM kill과 재시작이 반복됐습니다. 서버가 다시 요청을 받을 수 있어도 종료 전에 받은 요청의 처리 완료까지 보장되지는 않습니다. 종료 원인과 요청의 처리 상태를 구분해 대응할 필요가 있습니다.
Pod 교체 중 요청을 마무리하도록 종료 시간 배분하기
롤링 업데이트로 새 Pod를 준비해도 기존 Pod가 처리 중인 요청은 새 Pod로 옮겨지지 않습니다. 기존 Pod에서 요청을 마무리할 수 있도록 종료 유예 시간이 필요합니다.
Pod 삭제에 따른 트래픽 경로 변경과 종료 처리는 함께 진행됩니다. 경로 변경이 반영되기 전에는 기존 Pod로 새 요청이 들어올 수 있으므로, 새 요청의 유입을 멈추는 시간과 처리 중인 요청을 마무리하는 시간을 구분해야 합니다.
JVM의 종료 훅은 JVM이 종료될 때 실행하는 스레드입니다. Spring은 종료 훅에서 컨텍스트를 닫으며, graceful shutdown이 켜져 있으면 설정된 종료 대기 시간을 한도로 처리 중인 요청의 완료를 대기합니다.
종료 유예 시간 30초, preStop 실행 시간 10초 조건
종료 유예 시간은 preStop 실행 전부터 계산됩니다. preStop의 대기가 길어질수록 JVM이 처리 중인 요청과 자원 정리를 완료할 시간은 줄어듭니다. 따라서 트래픽 경로 변경을 기다리는 시간과 JVM의 종료 시간을 같은 유예 시간 안에서 배분해야 합니다.
종료 유예 시간 30초 중 preStop이 10초를 사용하는 경우
강제 종료로 응답을 받지 못한 요청의 복구 방식 결정하기
커널이 JVM을 OOM kill하면 SIGKILL로 강제 종료하므로, 종료 훅을 실행할 수 없습니다. graceful shutdown으로 처리 중인 요청을 마무리하거나 @PreDestroy로 자원을 정리할 기회가 사라집니다.
종료 코드만으로는 종료 원인과 요청 완료 여부를 확정할 수 없습니다. OpenJDK는 종료 훅을 거친 뒤에도 143으로 종료할 수 있습니다. 137은 종료 유예 시간 초과와 OOM kill에서 모두 나타날 수 있으므로, 종료 사유와 메모리 상태를 함께 확인해야 합니다.
| 종료 상황 | 종료 신호 | 종료 훅과 요청 처리 | 함께 확인할 기록 |
|---|---|---|---|
| Pod 교체 중 유예 시간 안에 종료 | SIGTERM | graceful shutdown 설정 시 처리 완료 대기 | 143과 배포 이력 |
| 종료 유예 시간 초과 | SIGTERM 이후 SIGKILL | 처리 중인 요청과 종료 훅 강제 중단 | 137과 종료 유예 시간 초과 기록 |
| 커널의 OOM kill | SIGKILL | 종료 훅 실행 불가 | 137, reason: OOMKilled, 메모리 상태 |
종료 원인을 확인해도 개별 요청의 처리 결과까지 알 수는 없습니다. 응답이 없어도 이미 처리 완료 상태일 수 있습니다. 처리 결과를 모른 채 재시도하면 같은 요청이 중복 실행될 수 있으므로, 요청 식별자로 완료 기록과 외부 처리 상태를 대조해야 합니다.
처리 기록은 프로세스가 종료된 뒤에도 조회할 수 있어야 합니다. 같은 요청 식별자를 보내는 것만으로 중복 실행이 방지되지는 않으므로, 서버의 상태 변경과 외부 처리까지 중복 실행을 방지할 수 있는 요청으로 자동 재시도를 한정할 필요가 있습니다.
| 확인한 상태 | 필요한 대응 |
|---|---|
| 처리 완료 | 요청 식별자와 요청 내용이 일치하는 기존 결과를 반환해야 합니다. |
| 외부 처리 진행 중 | 대기 한도를 넘겨도 실패로 단정하지 않고 외부 처리 상태를 다시 확인해야 합니다. |
| 처리 결과 불명 | 처리 기록과 외부 처리 결과를 대조할 경로가 없거나, 대조 후에도 처리 결과를 확정할 수 없으면 수동으로 확인해야 합니다. |
| 외부 처리가 없고 중복 실행 방지 가능 | 재시도 가능한 오류와 횟수, 대기 시간을 제한해야 합니다. |
| 중복 실행 방지 불가 | 이미 반영된 변경과 재시도로 추가될 변경을 구분해 복구 방법을 결정해야 합니다. |