dev.to

A dead Kubernetes node is detected in 3 seconds and keeps receiving traffic for 13

죽은 Kubernetes 노드는 3초 만에 감지되지만 13초 동안 트래픽을 받습니다

DigitalOcean Kubernetes에서 워커 노드를 강제 종료하는 실험을 열 차례 반복했습니다. 노드 감지는 2.9초로 빨랐지만 Service 엔드포인트에서 해당 노드의 Pod를 빼기까지 중앙값 13.2초가 걸렸으며, 계획된 작업에서는 drain이 이 시간을 0.5초로 줄였습니다.

AI 요약

Kubernetes 노드 장애를 설명할 때 흔히 노드 감지에 40초, Pod 축출에 5분이 걸린다고 말합니다. 글쓴이는 이 수치가 관리형 클러스터에도 적용되는지 확인하려고 실제 트래픽을 보내며 워커 노드를 갑자기 종료했습니다. 실험 결과, 감지보다 Service 엔드포인트 갱신이 느렸습니다.

실험 구성과 측정

DigitalOcean Kubernetes 1.36.3 클러스터에 s-2vcpu-2gb 노드 세 대를 두고, 복제본 여섯 개를 노드마다 두 개씩 배치했습니다. Cloud Load Balancer 뒤에 type: LoadBalancer Service를 두고, keep-alive 연결을 유지하는 클라이언트 스무 개가 초당 약 175건을 요청하도록 구성했습니다. 각 실험은 3분 동안 진행했으며, 시작 30초 뒤 Provider API로 워커 노드의 전원을 강제로 껐습니다. 운영체제의 정상 종료나 drain은 사용하지 않았습니다.

요청 기록과 함께 노드 준비 상태, Service 엔드포인트 목록도 초당 두 번 확인했습니다. 열 차례 실험에서 노드는 종료 후 중앙값 2.9초 만에 NotReady로 바뀌었습니다. 범위는 2.7~3.7초였습니다. 일반적으로 언급하는 40초보다 훨씬 짧았으며, 글쓴이는 DigitalOcean이 기본 타이밍을 조정한 결과로 해석합니다.

감지보다 늦은 엔드포인트 갱신

노드가 NotReady로 바뀐 뒤 해당 노드의 Pod가 Service 엔드포인트에서 제거되기까지 중앙값 13.2초가 걸렸습니다. 즉, 클러스터가 노드 장애를 알아챈 뒤에도 약 10초 동안 트래픽이 그 노드의 Pod로 향했습니다. 실험에서 발생한 요청 실패는 모두 이 지연 구간에 해당했습니다.

계획된 종료에서는 결과가 달랐습니다. kubectl drain 뒤 엔드포인트 갱신은 0.5초 만에 끝났고, 31,652건 가운데 실패 요청은 한 건뿐이었습니다. 글쓴이는 노드 유지보수, 업그레이드, 스케일 다운처럼 미리 계획할 수 있는 작업마다 drain을 사용하라고 권합니다. 반면 예고 없이 발생한 노드 장애에서는 이 실험의 약 10초 공백을 설정만으로 없애지 못했습니다.

`externalTrafficPolicy` 비교

글쓴이는 처음에 externalTrafficPolicy: Local이 장애를 줄일 것으로 예상했습니다. 기본값인 Cluster에서는 노드가 다른 노드의 Pod로 요청을 전달할 수 있지만, Local에서는 노드가 자기 노드에 있는 Pod만 처리합니다. 따라서 장애 노드로 전달되는 요청이 줄어들 것으로 봤습니다.

정책별로 다섯 차례씩 측정하자 총 실패 요청의 중앙값은 둘 다 회당 10건이었습니다. Cluster는 10, 18, 7, 11, 9건, Local은 3, 0, 11, 10, 11건이었습니다. 정책은 실패량보다 실패가 발생하는 시점을 바꿨습니다. Local에서는 실패 대부분이 종료 뒤 약 10초 안에 몰렸고, 이후 추가 실패가 없었습니다. Cluster에서는 초기 실패가 상대적으로 적었지만, 다섯 번 중 세 번은 1~2분 뒤에도 실패 요청이 한꺼번에 발생했습니다.

추가 실패는 0.1초 안에 6~14건이 발생해 클라이언트 스레드 모두가 동시에 실패한 양상이었습니다. 이때 노드 상태와 엔드포인트 목록은 바뀌지 않았고 노드 교체도 없었습니다. 글쓴이는 이 현상이 노드 장애 자체보다 Load Balancer 경로에서 비롯됐을 가능성을 언급하지만, 원인을 단정하지 않고 실험을 마쳤습니다.

실험에서 확인한 주의점

초기 실험에서는 복제본 여섯 개가 한 노드에 몰렸습니다. 이 노드를 끄면 전체 서비스 장애가 되므로 노드 하나의 손실을 측정하는 실험으로는 적절하지 않았습니다. 다음 시도에서는 세 개씩 두 노드에 배치돼 노드 하나를 끌 때 용량의 절반이 사라졌습니다. 글쓴이는 두 결과를 버리고 복제본을 노드마다 두 개씩 배치해 다시 시작했습니다.

또한 정책별 세 차례 실험에서는 Local이 더 나아 보였지만, 각 정책에 두 차례를 더하자 총 실패량 차이가 사라졌습니다. 예상과 맞는 결과가 나왔을 때 실험을 멈추지 말고 반복해야 한다는 점도 글쓴이가 짚은 부분입니다. 재시도 기능이 있는 클라이언트에서는 이런 실패가 사용자에게 드러나지 않을 수 있지만, 재시도되지 않는 요청은 영향을 받습니다.

dev.to 반응

  • @kashif_manzer — 글에서 가장 흥미로운 부분은 나중에 몰려 발생한 실패입니다. 모든 클라이언트 스레드가 0.1초 안에 실패했다는 관찰은 Pod 수준의 문제를 사실상 배제합니다. 클라이언트 keep-alive 연결 풀, DigitalOcean Load Balancer, 엔드포인트 갱신 때 kube-proxy의 iptables 규칙이 바뀌는 과정처럼 여러 연결이 공유하는 경로에서 문제가 생겼을 수 있습니다. 저는 연결 풀 쪽에 걸겠습니다. 장시간 연결 스무 개를 둔 스레드 스무 개는 같은 NAT와 conntrack 항목을 통과합니다. 이 항목이 갱신되거나 만료되면, 그 항목을 건드린 사건이 발생하고 1~2분 뒤에 모든 연결이 동시에 끊길 수 있습니다. 다시 실험한다면 요청 로그와 함께 conntrack 시각 정보도 기록해 가설을 빠르게 확인하거나 반박할 수 있습니다.

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