Kubernetes 환경에서 Keep-Alive 사용은 부하 분산을 방해한다고요?

JUN HO KO
IHFB  R&D 팀블로그
8 min readFeb 21, 2024

들어가며

안녕하세요. 밀당 팀의 백엔드 및 인프라 담당자 고준호 입니다. 오늘은 Kubernetes 환경에서 서비스를 운영하면서 겪었던 몇 가지 문제를 공유하고 해결했던 경험을 이야기해 보려고 합니다.

밀당의 Kubernetes 스케일링 환경 소개

저희 밀당은 K8S 환경에서 서비스를 운영하고 있습니다. Node Scaler는 Karpenter를 사용하고 있고, Horizontal Pod Autoscaler (HPA)는 KEDA를 사용하고 있습니다.

Karpenter는 내부 테스트 결과 평균적으로 70초 이내에 Node를 추가하고 Server Application이 Ready가 되기까지 평균 90초가 소요되는 것을 확인했습니다.

Keda를 이용하여 CPU 및 Network RX byte 기반 Metric 으로 HPA를 설정하고 증가 시에는 과감하게 Scale-Out 하고, 감소 시에는 보수적으로 천천히 Scale-In 할 수 있도록 하고, 서비스 특성상 피크 시간대를 예측이 가능한 경우에는 Cron 스케일러로 미리 Scale-Out 하고 있습니다.

또한 Over Provisioning Pod가 설정되어 있어 항상 약 8개의 WAS Pod 리소스를 미리 점유하고 있으며, 이를 통해 Scale-Out 상황에서 Node 추가 없이 WAS Pod가 신속하게 Scale-Out 될 수 있도록 설계했습니다.

워크로드 특성에 맞는 부하 테스트를 통해 Stage 환경에서 설계한 대로 잘 동작함을 확인하고 Production 환경에 적용했습니다.

문제

서비스를 운영함에 따라 부하가 증가하면서, 설계한 대로 Scaler가 제대로 작동하지 않아 요청의 Latency가 증가하거나, 일부 유저의 요청이 빈번히 실패하는 증상이 발견되었고 다음과 같은 2가지 현상을 확인하였습니다.

  1. 피크 시간대에 Datadog을 통해 확인해 보니, CPU Utilization이 1 이상의 수치를 보이는 Pod 들이 확인되고 나머지 대부분의 Pod 들은 대부분 Idle 상태였습니다. 바쁜 Pod들은 시간이 경과해도 계속 바쁜 상태를 유지했고, 이 양상은 트래픽이 줄어들 때까지 계속되었습니다.
  2. 트래픽이 없는 오전 시간대에 요청이 빈번히 실패하는 증상이 많이 보고 되었습니다. Socket Hang Up / Econnreset 등 Socket 에러가 확인이 되었습니다.

문제 해결 과정

과정 1

부하 분산이 제대로 이루어지지 않았던 문제의 객관적 상황은 아래와 같습니다.

  1. 특정 소수 Pod 들은 계속 Busy한 상태를 유지한다.
  2. 나머지 Pod 들은 Idle과 비슷한 상태를 보인다.

그렇다면 CPU 연산이 많이 필요한 요청을 받았던 게 아닐까…?

기존에도 요구사항이 있었기에 위와 같은 가설을 세우고 접근하였습니다. CPU 연산이 필요한 요청들을 리스트업 하고 해당 요청 들은 Resource가 넉넉한 Pod로 요청을 라우팅해 처리할 수 있도록 서버 분리 작업을 먼저 하였습니다.

하지만 분리작업을 해도 더 이상 해결되지 않는 것으로 판단했습니다.

점점 미궁 속으로…

추가로 리서치 진행해 보니 Kubernetes 환경에서 “Long-lived connections이 부하 분산 문제를 일으킬 수 있다고 합니다.

https://learnk8s.io/kubernetes-long-lived-connections

하지만 이전부터 이 문제 가능성에 대해 염두하고 있었고 웹소켓 기능들을 분리하여 서비스를 운영했습니다.

그래서 Long-lived connections 이 뭔데…?

위에서 중요한 부분은 처음 연결 요청 시에만 로드밸런싱을 수행한다는 것입니다. TCP 연결이 수립된 이후부터는 처음에 결정된 Path 로 통신을 수행합니다.

하지만 우리는 Long-lived connections을 일으킬 만한 범인이 없는 것 같은데…

과정 2

부하 분산 문제는 잠시 제쳐두고 Socket Hang Up / Econnreset 에러가 많이 발생하여 Socket Error에 먼저 집중해보기로 했습니다.

조사를 해보니 Socket Hang Up / Econnreset 은 소켓 관련 에러로 전송계층 에러이고 통신 중에 소켓이 비정상적으로 Close 되거나 네트워크 상황이 안 좋은 경우 발생한다고 합니다.

대략적인 트래픽 흐름은 아래와 같습니다.

Datadog Trace를 분석하니 Federation Server에서 Sub-Graph Server로 요청 시 에러가 다수 확인 되었고 Federation Server의 설정에 대한 문제를 의심하고 Apollo gateway github 에서 이슈가 있나 확인해 봤습니다.

다행히 동일한 이슈를 겪고 솔루션까지 제시한 리포트를 확인할 수 있었습니다.

https://github.com/apollographql/federation/issues/2313

원인

Apollo gateway github Issue에서 원인을 정리해 보면 다음과 같습니다.

1. Client가 서버에 요청을 보냅니다.

2. 서버가 Client에 응답합니다.

3. 서버의 Keep-Alive Free Socket Timeout 설정이 5000ms 입니다.

4. Client가 추가 요청을 보내지 않아 서버는 Free Socket Timeout을 기다립니다.

5. 5000ms 후, 서버는 소켓을 정리하기 시작합니다.

6. 하지만 이때, Client가 서버에 새 요청을 보내면 소켓 정리와 타이밍이 겹치게 됩니다.

7. 서버는 소켓을 닫고, Client는 소켓 오류를 발생합니다.

Federation Server와 Sub-Graph Server 사이에서는 Federation Server가 서버, Sub-Graph Server가 Client에 해당합니다. Client의 Keep-Alive Free Socket Timeout이 서버의 Free Socket Timeout 보다 길기 때문에 발생하는 문제였습니다.

해결 방법

해결 방법은 2가지가 있습니다.

1. Client의 Keep-Alive Free Socket Timeout이 서버보다 짧게 설정

Client의 Keep-Alive Free Socket Timeout이 서버보다 짧다면 통신이 없을 때 항상 Client가 서버보다 Socket Close를 먼저 하기 때문에 위와 같은 문제가 사라집니다.

2. Keep-Alive를 비활성화

Keep-Alive를 사용할 때보다 Latency가 증가합니다. 매 요청마다 TCP 연결 수립 과정, SSL 수립 과정 등을 거쳐야 하기 때문에 신중하게 선택해야 합니다.

서로 다른 문제라고 생각했으나 사실은 하나의 문제였다.

Socket Error 문제를 파악하는 과정에서 Socket Error를 일으키고 있었던 Keep-Alive가 결국에는 Long-lived connections를 유발한다는 사실을 알게 되었습니다. 글 초반부에 언급한 첫 번째 현상에 대한 원인을 알게 되었습니다.

부하 분산 현상에 대한 파악

  • Keep-Alive Timeout 시간 내에는 첫 요청 시 결정된 Path로 통신합니다.
  • 트래픽 증가 전에는 소수의 Pod이 요청을 처리합니다.
  • Scale-Out이 끝나도 Keep-Alive로 인해 Socket을 재사용 하면서 처음 존재하던 Pod만 계속 요청을 처리합니다.
  • 결과적으로 부하 분산 문제가 발생합니다.

결론

우리는 테스트 후에 Keep-Alive를 끄기로 결정했습니다.

Keep-Alive 활성 / CPU Metric
Keep-Alive 비활성 / CPU Metric

오버헤드가 있다는 부분 때문에 Keep-Alive를 Close로 설정하고 테스트를 진행했습니다. 결과는 요청당 40ms 정도 더 걸리는 것을 확인했고 이는 서비스 이용에 크게 문제가 되지 않는다고 판단 해서 Keep-Alive를 끄고 운영하기로 했습니다.

Keep-Alive를 끄면서 요청이 끝나면 Socket를 Close하기 때문에 Socket Error도 사라지게 되었고, 매 요청 마다 로드밸런싱을 하게 되므로 부하 분산 문제또한 해결되었습니다.

글을 마치며…

서비스를 운영하면서 다양한 장애를 경험하고 해결했었는데 이번에 소개한 문제는 간헐적으로 발생하고 임시 해결방법이 있었던 만큼 시간이 오래 걸렸던 문제였습니다.

시간은 오래 걸렸으나 결국 해결하면서 밀당은 좀 더 안정적인 서비스를 제공할 수 있게 되었습니다.

이번 글을 작성하면서 Kubernetes 환경에서 서비스를 운영하는 다른 개발자분들에게 유익한 정보가 되길 기대하면서 글을 작성하였습니다.

혹시 틀린 내용이 있다면 피드백은 언제나 환영합니다.

밀당에서 동료를 찾고 있습니다!

IHFB가 최근 급성장하여 에듀테크 유니콘이 되기 위해 새로운 시스템을 개발하고 있습니다. 데이터 파이프라인 구축부터 인프라 개선 등 기존 레거시에 존재하는 다양한 문제를 해결, 개선하고 있습니다. 새로운 도전을 즐기시는 분들과 함께 하면 교육의 혁신을 더욱 빠르고 멋지게 이룰 수 있을 것 같습니다!

밀당과 함께할 동료를 찾습니다.

--

--