dev.to

The same app dropped 51 requests on Kubernetes and none on DigitalOcean's App Platform

같은 앱, Kubernetes에서는 요청 51건을 놓쳤지만 DigitalOcean App Platform에서는 놓치지 않았습니다

SIGTERM을 받자마자 종료하는 Python 앱으로 롤아웃을 비교했습니다. Kubernetes에서는 요청이 끊겼지만 DigitalOcean App Platform에서는 일반 요청 278,014건이 배포 중 실패하지 않았습니다. 다만 기본 드레인 시간은 15초라 더 긴 요청에는 설정 변경이 필요합니다.

AI 요약

Kubernetes 롤링 업데이트에서 maxUnavailable: 0을 설정해도 요청이 끊기는 문제를 확인한 작성자가, 같은 앱을 DigitalOcean App Platform에 배포해 비교했습니다. 앞선 실험에서는 종료 전 대기용 preStop, 애플리케이션의 graceful shutdown, 연결을 정리하며 종료하는 처리가 모두 필요했습니다. 이번에는 반대로 SIGTERM을 받자마자 종료하고 요청 드레인도 하지 않는 앱을 사용해 플랫폼이 롤아웃을 어떻게 처리하는지 측정했습니다.

테스트 구성과 결과

앱은 Python 파일 하나로 구성했고, 응답에 컨테이너 식별자와 환경 변수에서 가져온 버전 번호를 넣었습니다. 테스트 환경은 App Platform에서 제공하는 가장 작은 인스턴스인 공유 vCPU 1개, 메모리 512MB를 사용하는 단일 인스턴스였습니다. 클라이언트는 스레드 20개에서 keep-alive 연결을 유지하며 HTTPS 요청을 계속 보냈습니다. 부하를 30초간 실행한 뒤 앱 사양의 버전 번호를 바꿔 재배포를 시작하고, 새 버전이 올라온 뒤에도 요청을 이어갔습니다.

배포하지 않은 대조 테스트에서는 2분 동안 요청 22,450건이 모두 성공했습니다. 재배포 4회에서는 각각 85,892건, 62,040건, 66,981건, 63,101건을 보냈습니다. 마지막 테스트에서 배포가 끝난 뒤 167초가 지나 502 응답 하나가 발생했지만, 작성자는 이를 배포가 일으킨 실패로 보지 않았습니다. 전체 표에는 이 요청도 포함했습니다. 네 번의 재배포 중 배포 중 요청 실패는 없었고, 배포는 모두 1분 안에 완료됐습니다.

연결과 트래픽 전환

응답 헤더에는 server: cloudflare와 x-do-app-origin이 들어 있었습니다. App Platform은 앱 앞단에 Cloudflare를 두며, 클라이언트 연결은 컨테이너가 아니라 소피아(Sofia)의 Cloudflare 엣지에 맺렸습니다. 따라서 종료되는 컨테이너에 클라이언트의 keep-alive 연결이 직접 붙어 있다가 중간에 끊기는 Kubernetes 실험의 실패 방식과 다릅니다. 작성자는 컨테이너 내부 구성을 엣지 너머에서 확인할 수는 없다고 덧붙였습니다.

전환 구간에서는 약 10초 동안 구버전과 새 버전이 거의 절반씩 요청을 처리했습니다. 이후 구버전은 새 요청을 받지 않았습니다. 전환 중 지연 시간은 양쪽 구간과 큰 차이가 없었고, 중앙값은 전환 전 44.3ms에서 전환 구간 42.9ms였습니다.

기본 드레인 시간의 한계

짧은 요청만으로는 플랫폼이 기존 요청을 기다리는지, 실패한 요청을 프록시가 새 버전으로 재시도하는지 구분할 수 없습니다. 이를 확인하려고 작성자는 응답 전에 20초 대기하는 엔드포인트를 추가하고, 매초 별도 연결로 요청을 보냈습니다. 테스트 결과 플랫폼은 요청을 재시도하는 대신 기존 인스턴스가 처리하도록 기다렸습니다. 전환이 시작된 뒤 4.1초에 구버전에 도착한 요청도 20초를 모두 실행해 구버전에서 응답했습니다.

다만 기본 설정에서는 구버전이 마지막 새 요청을 받은 뒤 15초가 지나면 종료됐습니다. 그때까지 처리 중이던 요청 두 건은 동시에 504로 실패했습니다. 같은 기본 설정 테스트를 두 번 실행했으며, 두 번 모두 드레인 시간은 15초였고 요청 150건 가운데 두 건이 끊겼습니다.

서비스 사양의 종료 설정을 다음처럼 바꾸자 동일한 테스트에서 진행 중인 요청 150건이 모두 완료됐습니다.

``yaml termination: drain_seconds: 60 grace_period_seconds: 90 ``

구버전이 마지막에 받은 요청도 전체 20초를 실행한 뒤 응답했습니다. 대신 새 배포가 활성화되기까지 69.8초가 걸렸습니다. 기본 설정 테스트에서는 42~56초가 걸렸습니다. 긴 드레인 설정은 한 번만 측정했으므로 참고값으로 봐야 하지만, 진행 중인 요청을 더 오래 기다리면 배포 시간이 늘어난다는 점은 결과에 드러납니다.

비교 결과를 해석할 때 주의할 점

작성자는 Kubernetes와 App Platform의 구성이 같지 않다고 명시했습니다. Kubernetes 테스트에는 로드 밸런서 뒤에 파드 네 개가 있었고, keep-alive 연결은 파드에 직접 이어졌습니다. App Platform 테스트는 인스턴스 하나 앞에 Cloudflare 엣지가 있었습니다. 따라서 이 결과는 두 플랫폼을 동일 조건에서 비교한 벤치마크가 아닙니다. 앱이 종료돼도 클라이언트 연결이 컨테이너와 분리되는 App Platform의 구조가, 이 실험에서 요청 손실을 막은 이유라고 설명합니다.

또한 원문은 테스트 과정의 오류와 재현되지 않은 현상도 공개합니다. 첫 느린 요청 테스트에서는 코드 변경 뒤 환경 변수만 바꿔 배포하면서 새 코드가 반영되지 않았습니다. Git 소스에서는 기존 빌드를 재사용할 수 있어 doctl apps create-deployment --force-rebuild로 강제 재빌드해야 했습니다. 응답에 slept=가 없다는 점을 보고 문제를 발견했습니다. 첫 분석에서는 요청 시각과 전환 시각의 기준 단위를 맞추지 않아 결과 표가 비어 있었고, 코드 오류를 수정했습니다. 또 첫 느린 요청 테스트의 처음 20초 동안 시작한 요청들이 모두 40초씩 걸리는 현상은 이후 재현되지 않았으며 원인을 설명하지 못했다고 밝혔습니다.

운영 시 참고할 점

일반적인 짧은 요청은 App Platform에서 별도의 종료 처리 없이 네 차례 재배포를 거쳤고, 총 278,014건 중 배포로 실패한 요청은 없었습니다. 반면 업로드, 보고서 생성, 느린 외부 서비스 호출, long polling처럼 15초보다 오래 걸릴 수 있는 요청은 기본 드레인 시간 안에 끝나지 않을 수 있습니다. 이런 요청을 처리한다면 termination.drain_seconds를 가장 느린 요청보다 길게 설정해야 합니다. 더 긴 드레인을 기다리는 만큼 배포 시간도 늘어날 수 있습니다.

원문: dev.to / 번역·요약: Trawling