{"id":"IL-0014","org":"loveholidays","date":"2019-03-19","docs":["https://cloud.google.com/kubernetes-engine/docs/how-to/upgrading-a-cluster","https://kubernetes.io/docs/concepts/workloads/pods/disruptions/#how-disruption-budgets-work","https://github.com/kubernetes/ingress-gce/releases/tag/v1.5.1","https://cloud.google.com/kubernetes-engine/docs/release-notes"],"title":{"en":"A GKE upgrade to a withdrawn version stalled on slow drains and a stuck node pool, then broke an ingress and confused the HPA","ko":"GKE 를 1.12.5-gke.10 으로 올리는 중 노드 풀 업그레이드가 멈추고 인그레스와 HPA 가 망가졌다"},"domain":["kubernetes","gcp"],"impact":{"scope":"one cluster: internal ingresses down, deployments slowed","users":"unknown","region":"unknown"},"review":{"written_by":"session","reviewed_at":null,"reviewed_by":null},"lessons":{"en":"Read the release notes before upgrading: the target version had been withdrawn five days earlier. Drain time depends on pod configuration, so emptyDir volumes, long grace periods and missing disruption budgets turn a NodePool upgrade into hours. Pre-production only catches problems like the ingress bug if it matches production down to resource names. Auto-upgrades in non-production surface problems early, and a mix of manual cluster upgrades and automatic node upgrades in production keeps clusters current without much effort.","ko":"클러스터를 업그레이드하기 전에는 GKE 릴리스 노트를 먼저 봐야 한다. 운영이 아닌 환경에는 자동 업그레이드를 써서 영향을 먼저 겪고, 운영에는 수동 클러스터 업그레이드와 자동 노드 업그레이드를 섞어 예측 가능성과 유지 부담 사이에서 균형을 잡는다. 사전 검증 환경은 이름까지 운영과 같게 둬야 한다. 노드 풀 업그레이드 시간은 노드 수와 드레인 시간에 달려 있다. emptyDir 를 쓰는 파드에 safe-to-evict 어노테이션을 달고, 지나치게 긴 종료 유예 시간을 다시 따져 보고, PodDisruptionBudget 을 두면 드레인이 빨라진다."},"related":["IL-0020"],"sources":[{"url":"https://deploy.live/blog/the-shipwreck-of-gke-cluster-upgrade/","kind":"blog","license":"unknown","archived":"http://web.archive.org/web/20260427131141/https://deploy.live/blog/the-shipwreck-of-gke-cluster-upgrade/"}],"summary":{"en":"During the migration of loveholidays' on-premises applications to GCP the team had no time for routine cluster and NodePool upgrades and fell two versions behind. The same upgrade had been tested on several other clusters and environments without signs of trouble for production stability. A GKE upgrade runs in two steps, masters and then NodePools, and a NodePool upgrade drains and upgrades nodes one by one, so its length depends on the number of nodes. The cluster had many NodePools, some without active nodes.\n\nThe slow drains came from the cluster's own configuration. Pods with emptyDir volumes block drain because it cannot tell whether evicting them is safe; the cluster-autoscaler.kubernetes.io/safe-to-evict: \"true\" annotation allows it. Some applications had termination grace periods of 5 to 10 minutes, such as terminationGracePeriodSeconds: 600. A Pod Disruption Budget with maxUnavailable would let drain evict several pods at once. When it became clear the sequential drains would not finish at a reasonable hour, the engineer drained the remaining nodes ahead of time with kubectl drain --ignore-daemonsets --delete-local-data, which can evict more pods than a service tolerates; creating a new NodePool and moving pods to it is the alternative. The zombie nodes were cleared by mapping the managed instance group to its NodePool, listing its instances with gcloud compute instance-groups managed list-instances and deleting the stuck ones with delete-instances. The stuck upgrade was found with gcloud container operations list and describe and cancelled with gcloud beta container operations cancel, which leaves already upgraded nodes on the new version.\n\nGoogle support explained the broken Ingress as a known bug that only hit Ingresses whose namespace plus name was between 44 and 47 characters long. The pre-production environments did not catch it because the production Ingress name was three characters longer; the fix shipped in GKE 1.12.6-gke.7 and the team used port-forward meanwhile. The cause of the HPA problem was not found. Scaling down the older of two active ReplicaSets, or triggering a dummy deployment by changing an environment variable, brought it back, and the team added Alertmanager alerts for HPAs near their maximum and for deployments with two active ReplicaSets. GKE had announced on 14 March that 1.12.5-gke.10 was withdrawn from new clusters and master upgrades because of elevated master error rates, and 1.12.6-gke.7, available from 26 March, patched the ingress bug and improved cluster autoscaling, so the author concludes several of these problems were preventable.","ko":"loveholidays 는 온프레미스 애플리케이션을 GKE 로 옮기느라 클러스터와 노드 풀 업그레이드를 미뤘고, 사용 가능한 버전보다 두 버전 뒤처져 있었다. 다른 클러스터와 환경에서 같은 업그레이드를 미리 테스트했고, 저녁에 2시간에서 2시간 30분이면 마스터와 노드 풀을 모두 올릴 수 있다고 봤다. 마스터를 당시 최신인 1.12.5-gke.10 으로 올린 뒤 노드 풀을 차례로 올렸다. 7노드짜리 노드 풀 두 개는 문제없이 끝났지만 그 뒤로 문제가 잇달았다.\n\n드레인이 느려 노드를 하나씩 올려서는 적당한 시간 안에 끝낼 수 없었다. 오래된 노드 풀의 관리형 인스턴스 그룹은 빈 노드 6개를 붙든 채 Pending 파드를 처리하지 못했다. 노드 풀 업그레이드 하나는 멈췄다. 네임스페이스와 이름을 합쳐 44자에서 47자인 인그레스만 망가지는 알려진 버그로 내부 인그레스 몇 개가 내려갔다. 사전 검증 환경이 있었지만 운영 인그레스 이름이 세 글자 더 길어서 사전 검증에서 걸러지지 않았다. 다음 날에는 HPA 가 CPU 부하를 읽지 못해 파드를 최대치까지 늘렸고, 팀은 정확한 원인을 찾지 못했다.\n\n구글은 1.12.5-gke.10 에서 마스터 오류율이 높아진다는 보고를 받고 3월 14일부터 이 버전을 내리는 중이었다. 3월 26일에는 인그레스 버그를 고치고 클러스터 오토스케일링을 개선한 1.12.6-gke.7 이 나왔다. 릴리스 노트를 먼저 확인했다면 일부 문제는 쉽게 막을 수 있었다."},"trigger":{"en":"A manual upgrade of the GKE masters and NodePools from 1.10.X to 1.12.5-gke.10, two versions at once, on 19 March 2019; GKE's release notes of 14 March had already said that version was no longer available for new clusters or master upgrades because masters showed elevated error rates, but it could still be selected.","ko":"마이그레이션에 쫓겨 미뤄 둔 업그레이드로 운영 클러스터를 1.10.X 에서 1.12.5-gke.10 으로 올렸다. 구글은 3월 14일 GKE 릴리스 노트에서, 이 버전으로 올린 마스터의 오류율이 높아진다는 보고가 있어 새 클러스터와 마스터 업그레이드에서 이 버전을 빼는 중이라고 밝혔다. 그런데도 3월 19일에 이 버전을 고를 수 있었다."},"severity":"unknown","symptoms":["upgrade-stuck","pod-pending","autoscaler-scale-up-failed","service-unavailable","autoscaler-misbehaving"],"timeline":{"detected_after":null,"resolved_after":null},"date_kind":"incident","detection":{"en":"The slow drains, zombie nodes and stuck NodePool were seen by the engineer running the upgrade; the broken Ingress was noticed after the upgrade and explained by Google support; the HPA problem was noticed the next day.","ko":"업그레이드하던 엔지니어가 드레인이 느리고 노드 풀 업그레이드가 멈추고 파드가 Pending 에 머무는 것을 직접 보았다. 인그레스 장애는 업그레이드 뒤에 드러났고, 로그로 원인을 찾지 못해 구글 지원팀에 물어 알려진 버그라는 것을 알았다. HPA 이상은 업그레이드 다음 날 발견했다."},"components":["gke-control-plane","node-pool","cluster-autoscaler","ingress","hpa"],"root_cause":"cloud-provider","remediation":[{"text":{"en":"drained the remaining NodePool nodes ahead of the upgrade with kubectl drain --ignore-daemonsets --delete-local-data","ko":"노드 풀의 노드 목록을 뽑아 kubectl drain 에 --ignore-daemonsets --delete-local-data 를 주어 노드를 미리 드레인했다."}},{"text":{"en":"deleted the zombie instances from the managed instance group with gcloud compute instance-groups managed delete-instances","ko":"노드 풀에 해당하는 관리형 인스턴스 그룹을 찾아 gcloud compute instance-groups managed delete-instances 로 멈춘 인스턴스를 지웠다."}},{"text":{"en":"cancelled the stuck UPGRADE_NODES operation with gcloud beta container operations cancel","ko":"gcloud container operations list 로 operationType: UPGRADE_NODES, status: RUNNING 인 작업을 찾아 gcloud beta container operations cancel 로 멈춘 노드 풀 업그레이드를 취소했다."}},{"text":{"en":"used kubectl port-forward in place of the broken internal Ingresses","ko":"동작을 멈춘 내부 인그레스 대신 한동안 port-forward 로 접근했다."}},{"text":{"en":"scaled down the older of two active ReplicaSets, or triggered a dummy deployment, to bring the HPA back","ko":"HPA 가 이상해진 배포에서 활성 레플리카셋이 두 개면 오래된 레플리카셋을 천천히 줄였고, 하나면 더미 환경 변수를 바꿔 다시 배포했다."}}],"action_items":[{"text":{"en":"Alert when a HorizontalPodAutoscaler is at 70% and at 100% of its capacity","ko":"HorizontalPodAutoscaler 가 최대치의 70% 와 100% 에 닿으면 Alertmanager, Prometheus, Slack 으로 경보를 보내게 했다."}},{"text":{"en":"Alert when a deployment has had two active ReplicaSets for 30 minutes and for 60 minutes","ko":"배포에 활성 레플리카셋 두 개가 30분 동안 남아 있을 때와 60분 동안 남아 있을 때 경보를 보내게 했다."}},{"text":{"en":"Re-evaluate termination grace periods that were set to arbitrary large values","ko":"임의로 크게 잡은 종료 유예 시간을 다시 따져 본다."}},{"text":{"en":"Read the GKE release notes before every cluster upgrade","ko":"클러스터를 업그레이드하기 전에 GKE 릴리스 노트를 먼저 확인한다."}},{"text":{"en":"Use auto-upgrade in non-production environments, and manual cluster upgrades with automatic node upgrades in production","ko":"운영이 아닌 환경에는 자동 업그레이드를 쓰고, 운영 환경은 클러스터를 수동으로 업그레이드하고 노드는 자동 업그레이드를 쓴다."}},{"text":{"en":"Keep environments as identical as possible, including resource names","ko":"사전 검증 환경을 이름까지 운영과 최대한 똑같이 맞춘다."}}],"contributing":["staging-not-representative","overlapping-failures","early-signal-ignored"],"recurrence_of":null,"symptoms_text":{"en":"On the evening of a long-overdue upgrade of a loveholidays production GKE cluster from 1.10.X to 1.12.5-gke.10, planned to take 2 to 2.5 hours, the master upgrade and the first two 7-node NodePools went smoothly. After that the NodePool upgrades slowed to a crawl. Each node is cordoned, drained and upgraded in turn. Even a 14-node cluster at about 5 minutes per node took over an hour, and with about 100 more nodes to go, some of them taking over 15 minutes to drain, the sequential drains were too slow to finish at a reasonable hour.\n\nOne older NodePool turned out to be in a confused state: six nodes ran no pods but were never terminated. Pods stayed Pending, never landing on these zombie nodes and never triggering a scale-up. Another NodePool upgrade simply stopped with no sign of nodes draining. Its operation, of type UPGRADE_NODES with status RUNNING, reported \"Done with 1 out of 20 nodes (5.0%): 1 being processed, 1 succeeded\", and while it ran the console blocked any further change to the cluster, including cancelling the upgrade.\n\nAfter the upgrade one of the internal convenience Ingresses stopped working, and its logs did not explain why. The next day the CPU-based HorizontalPodAutoscaler stopped reporting current CPU load and kept scaling up until it reached its maximum, which added load on downstream systems, cost money in underused nodes and slowed deployments. At times a single deployment had two active ReplicaSets for hours.","ko":"미뤄 둔 GKE 업그레이드를 하던 저녁, 마스터는 문제없이 올라갔지만 노드 풀 업그레이드가 예상보다 훨씬 오래 걸렸다. 노드가 14개이고 노드당 5분쯤 걸리는 클러스터도 한 시간 넘게 걸렸고, 일부 노드는 드레인에 15분 넘게 걸렸다. emptyDir 를 쓰는 파드와 5분에서 10분짜리 terminationGracePeriodSeconds 가 드레인을 늦췄다.\n\n오래된 노드 풀 하나는 이상한 상태였다. 파드를 하나도 돌리지 않는 노드 6개가 종료되지 않고 남아 있었다. Pending 상태의 파드는 이 빈 노드에 스케줄되지도 않았고, 클러스터 오토스케일러가 노드를 늘리게 하지도 못했다.\n\n노드 풀 하나의 업그레이드는 그냥 멈췄다. 노드가 드레인되는 기미가 없었고, 노드 풀 변경이 진행 중이라 UI 로는 클러스터를 바꿀 수도 업그레이드를 취소할 수도 없었다. gcloud container operations describe 출력에는 detail: 'Done with 1 out of 20 nodes (5.0%): 1 being processed, 1 succeeded' 와 status: RUNNING 이 찍혀 있었다.\n\n업그레이드 뒤에는 내부용 인그레스 몇 개가 동작을 멈췄고, 로그로는 원인을 알 수 없었다. 다음 날에는 CPU 기반 HorizontalPodAutoscaler 가 현재 CPU 부하를 보고하지 않아 최대치에 닿을 때까지 계속 스케일 아웃했다. 하위 시스템 부하와 비용이 늘었고 배포도 느려졌다. 배포 하나에 활성 레플리카셋 두 개가 몇 시간씩 남아 있는 경우도 보였다."},"updated_at":"2026-11-10T00:00:34.348696Z","url":"https://benchlet.com/incidentlib/app/cases/IL-0014","org_slug":"loveholidays"}