profile image

L o a d i n g . . .

728x90

최근 쿠버네티스에서 운영되는 프로덕션 애플리케이션을 배포(rolling update)하는 과정에서 클라이언트 요청이 중단되거나 거부되는 현상이 발생했습니다("client connection refused" 등의 에러 발생). 분명 deployment.yaml에 rollingUpdate를 수행하도록 설정했지만,  배포 과정에서 순단현상이 발생했습니다. 

spec:
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 0
  replicas: 4

 

위 설정에서의 배포는 다음과 같이 동작합니다. 

  • 총 4개의 pod가 실행됩니다. 
  • 배포 시 새로운 버전의 반영은 순차적으로 수행됩니다.
    1. 새로운 pod 생성
    2. 새로운 pod가 준비상태(Ready)가 되면 기존에 존재하는 pod 1개 종료(Terminate) 
    3. 1, 2번을 총 4번 반복 

Pod 1개씩 순차적으로 교체되므로 분명 클라이언트 요청을 모두 정상적으로 처리할 것으로 예상했으나, 알 수 없는 이유로 수백 개의 에러가 발생하고 있었습니다. 해당 이슈를 수정하지 않고 방치하면 배포할 때마다 순단 현상으로 인한 에러를 마주하기 때문에 반드시 수정할 필요가 있었습니다. 문제를 해결하는 방법과 해결방법을 도출하기까지의 과정에 대해 소개하고자 이번 포스팅을 작성합니다. 

 

Pod는 언제 준비(Ready)되는 걸까? 

첫 번째 궁금증은 "새로 생성된 pod는 언제부터 클라이언트의 요청을 수신받을 수 있을까?"였습니다. 만약 pod에서 실행 중인 서버 애플리케이션이 요청을 처리할 수 없는 상태에 클라이언트의 요청이 들어오면 어떻게 될까요? 요청을 서버에서 처리할 수 없기 때문에 커넥션 관련 에러가 발생합니다. 저는 "pod가 요청을 수신받는 시점"이 문제의 진원지라고 의심하면서 더 조사를 진행했습니다. 

A Pod is considered ready when all of its containers are ready.

 

 

쿠버네티스 공식문서에서는 pod 상의 모든 컨테이너가 준비 상태가 되면 pod가 준비된 것으로 간주합니다. 하지만 컨테이너가 준비되는 시점은 컨테이너 위에서 실행되는 서버 애플리케이션이 요청을 처리할 수 있는 시점보다 빠를 수 있습니다. 즉, 컨테이너가 준비 상태여도 그 위에서 실행되는 애플리케이션은 클라이언트의 요청을 처리할 수 없는 상태일 수 있습니다. 이 문제를 해결할 수 있는 방법으로 "readinessProbe" 설정이 있습니다. "readinessProbe"을 활용하면 pod의 준비 시점을 커스터마이징 할 수 있습니다. 

readinessProbe:
  httpGet:
    path: /actuator/health/readiness
    port: 8080
  initialDelaySeconds: 30

 

위 설정은 pod가 준비되는 시점을 컨테이너가 모두 준비된 시점이 아니라 애플리케이션의 "/actuator/health/readiness" 경로의 API 호출을 통해 판단합니다. 만약 애플리케이션에서 2xx, 3xx 상태 코드를 반환하면 해당 pod는 준비된 것으로 간주됩니다. 

 

문제 해결 완료? 

하지만 이상하게도 에러가 발생하는 빈도수는 줄었지만 여전히 에러가 발생하고 있었습니다. 추가 조사를 진행한 결과 쿠버네티스는 pod가 종료되는 시점에 클라이언트와의 순단이 발생할 수밖에 없는 구조적인 문제가 있음을 발견했습니다. 쿠버네티스가 pod를 어떻게 종료시키길래 에러가 발생하는지 살펴보겠습니다. 

쿠버네티스는 pod 종료 요청을 받으면 API 서버에 "pod 종료를 위한 API"를 호출합니다. API가 호출되면 다음과 같은 작업이 병렬적(동시에)으로 실행됩니다. 

  1. 종료 대상 pod에 SIGTERM 시그널을 보내 애플리케이션을 종료를 명령합니다. 애플리케이션이 SIGTERM을 수신하면 적절한 작업을 수행한 이후 종료됩니다. 참고로 스프링 부트의 graceful shutdown 설정을 추가하면 SIGTERM 수신 후 신규 요청을 거부하고 기존에 처리하던 요청만 처리하도록 동작합니다. 
  2. kube-proxy에게 iptable에 저장된 "종료 대상 pod와 관련된 정보"를 삭제(iptable rule을 변경하는 것입니다)하도록 명령합니다. 삭제 전까지 클라이언트의 요청은 pod로 전달됩니다. 

 

두 작업이 병렬적으로 실행되기 때문에 어떤 작업이 먼저 종료되는지 알 수 없습니다. 만약 2번이 1번보다 늦게 종료되면 어떻게 될까요? iptable에는 여전히 pod의 정보가 남아있기 때문에 클라이언트의 요청은 pod로 전달됩니다. 하지만 서버는 종료 또는 종료하는 과정에 있기 때문에 클라이언트의 요청을 처리할 수 없어 에러가 발생합니다. 

위에서 살펴본 두 작업은 병렬적으로, 즉 동시에 실행되기 때문에 이를 해결할 수 있는 근본적인 해결책은 없습니다(아직 제가 발견하지 못한 것일 수 있습니다). 그래도 위 문제를 조금이나마 해결하는 방법은 1번이 2번보다 나중에 실행되도록 설정하는 것입니다. 1번에서 SIGTERM 시그널이 전달되기 전 특정 동작이 실행되도록 설정할 수 있습니다. 바로 preStop 단계에 sleep 커맨드를 설정하는 방법입니다. preStop 단계에서 sleep을 오래 하면 할수록 1번이 2번 이후에 종료될 확률이 높습니다(그만큼 SIGTERM 시그널이 늦게 송신되기 때문입니다). 다만 pod가 iptable에서 제외됐지만 오랜 시간 동안 종료처리되지 않는다는 단점은 존재합니다. 저는 이 문제를 해결하기 위해 다음과 같은 설정을 추가했습니다. 

preStop:
  exec:
    command: ["/bin/sleep","300"]

 

위 설정을 추가하면 SIGTERM 시그널이 전달되기 전 command(300초 sleep)가 우선 수행됩니다. 요약하자면 애플리케이션이 SIGTERM을 수신받는 시점을 iptable에서 pod 정보를 삭제하는 시점보다 뒤로 늦추는 것입니다. 물론 위 설정을 추가하더라도 1번이 2번보다 먼저 실행될 가능성은 여전히 존재합니다. 하지만 그 확률을 최대한 낮춰 클라이언트 요청과 관련된 오류를 최소화할 수는 있습니다. 

 

요약 

쿠버네티스를 활용해 rolling update를 수행할 때 다음과 같은 설정을 추가해야 클라이언트 요청과 관련된 에러를 최소화할 수 있습니다. 

  • readinessProbe 추가 
  • preStop 설정 추가 
728x90
복사했습니다!