Troubleshooting Stuck Upgrades & Testing Upgrade Strategy
Tư duy debugging upgrade: từ triệu chứng đến nguyên nhân gốc
Upgrade stuck là trạng thái cluster ở "đang upgrade" nhưng không progress. Có thể stuck ở nhiều điểm khác nhau:
- Control plane upgrade hung
- Một node pool stuck, không move sang node tiếp theo
- Một node bị cordon nhưng pods không drain được
- Surge node được tạo nhưng không join cluster
Mỗi điểm stuck có nguyên nhân khác nhau. Debugging phải bắt đầu bằng xác định stuck ở đâu trước khi tìm nguyên nhân.
Bước 1: Xác định vị trí stuck
Kiểm tra status upgrade operation
# Liệt kê tất cả operations đang active hoặc gần đây
gcloud container operations list \
--filter="status!=DONE" \
--format="table(name,operationType,status,startTime,targetLink)"
# Xem chi tiết một operation cụ thể
gcloud container operations describe OPERATION_ID \
--zone=ZONE
# Kiểm tra status của cluster
gcloud container clusters describe CLUSTER_NAME \
--zone=ZONE \
--format="value(status,conditions)"Output cần chú ý:
status: RECONCILING— Cluster đang trong quá trình upgrade/updatestatus: ERROR— Upgrade thất bạiconditions[].message— Mô tả tình trạng hiện tại
Kiểm tra node status
# Xem status tất cả nodes
kubectl get nodes -o wide
# Tìm nodes đang cordon (SchedulingDisabled)
kubectl get nodes --field-selector spec.unschedulable=true
# Xem events gần đây liên quan đến nodes
kubectl get events --field-selector involvedObject.kind=Node \
--sort-by='.lastTimestamp' | tail -20Dấu hiệu của stuck drain:
NAME STATUS ROLES AGE VERSION
node-abc-xyz Ready,SchedulingDisabled <none> 2h v1.28.5Node đã cordon (SchedulingDisabled) nhưng vẫn còn pods → drain bị blocked.
# Xem pods đang chạy trên node bị stuck
kubectl get pods --all-namespaces \
--field-selector spec.nodeName=NODE_NAME \
-o custom-columns=NAMESPACE:.metadata.namespace,NAME:.metadata.name,STATUS:.status.phaseBước 2: Diagnose nguyên nhân stuck
Nguyên nhân 1: PodDisruptionBudget blocking eviction
Đây là nguyên nhân phổ biến nhất.
# Liệt kê tất cả PDBs và trạng thái hiện tại
kubectl get pdb --all-namespaces -o wide
# Output cần xem: ALLOWED DISRUPTIONS = 0 là dấu hiệu bị blocked
# NAMESPACE NAME MIN AVAILABLE MAX UNAVAILABLE ALLOWED DISRUPTIONS
# default my-service 2 N/A 0 ← BUG: không ai được evictDiagnose chi tiết hơn:
# Xem events eviction bị rejected
kubectl get events --all-namespaces \
--field-selector reason=FailedEviction \
| grep -i "disruption budget"
# Xem PDB specific
kubectl describe pdb PDB_NAME -n NAMESPACEOutput điển hình khi PDB blocking:
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Warning FailedEviction 5m node-controller Cannot evict pod as it would violate the pod's
disruption budget. Conditions: [DisruptionAllowed
False reason:InsufficientPods message:Expected
number of pods: 2, current number of pods: 2,
ready pods: 2, allowed disruptions: 0, minimum
desired pods: 2]Giải quyết:
Option 1: Tạm thời relax PDB (nếu chấp nhận được disruption)
# Patch PDB để tăng maxUnavailable tạm thời
kubectl patch pdb my-service-pdb -n default \
--type=json \
-p='[{"op": "replace", "path": "/spec/maxUnavailable", "value": 2}]'
# Sau khi upgrade xong, restore
kubectl patch pdb my-service-pdb -n default \
--type=json \
-p='[{"op": "replace", "path": "/spec/maxUnavailable", "value": 1}]'Option 2: Scale up replicas trước khi patch tới node
# Scale up đủ để PDB cho phép eviction
kubectl scale deployment my-service --replicas=4 -n default
# Sau upgrade, scale back xuống 3Option 3: Xóa PDB tạm thời cho maintenance (chỉ cho critical emergency)
# DANGEROUS: Chỉ dùng khi không có option nào khác
kubectl delete pdb my-service-pdb -n default
# Nhớ recreate sau upgradeNguyên nhân 2: Node affinity constraints quá restrictive
# Tìm pods không thể schedule sang nodes khác
kubectl get pods --all-namespaces -o json \
| jq '.items[] | select(.spec.nodeName == "STUCK_NODE_NAME") | .metadata'
# Kiểm tra pending pods và lý do
kubectl get pods --all-namespaces --field-selector status.phase=Pending
kubectl describe pod PENDING_POD_NAME -n NAMESPACE | grep -A 10 "Events:"Dấu hiệu nodeAffinity/nodeSelector quá chặt:
Events:
Warning FailedScheduling 2m default-scheduler
0/5 nodes are available: 5 node(s) didn't match node selector.Giải quyết:
# Temporary: Remove restrictive nodeSelector cho maintenance
kubectl patch deployment my-app \
--patch '{"spec": {"template": {"spec": {"nodeSelector": null}}}}' \
-n NAMESPACE
# Hoặc thêm label vào surge node để match
kubectl label node SURGE_NODE_NAME my-custom-label=valueNguyên nhân 3: Quota exhaustion (surge node creation failed)
# Kiểm tra Cloud Logging cho quota errors
gcloud logging read \
'resource.type="k8s_cluster" AND
textPayload:"quota" AND
severity>=WARNING' \
--limit=20 \
--format="value(timestamp,textPayload)"
# Kiểm tra quota hiện tại
gcloud compute project-info describe \
--format="table(quotas.metric,quotas.limit,quotas.usage)" \
| grep -E "CPUS|INSTANCES"Dấu hiệu: Upgrade không progress, không có surge node được tạo, logs có lỗi về quota.
Giải quyết:
- Request quota increase:
gcloud compute project-info describe→ Cloud Console → IAM & Admin → Quotas - Temporary: Giảm maxSurge để fit trong quota hiện tại
- Xóa unused resources để free quota
Nguyên nhân 4: Admission webhook blocking pod operations
Khi upgrade node, GKE tạo các system pods và cần RBAC/ServiceAccount resources. Nếu admission webhook:
- Có
failurePolicy: Failvà webhook service down - Block creation của system resources
# Kiểm tra webhook configuration
kubectl get mutatingwebhookconfigurations
kubectl get validatingwebhookconfigurations
# Xem events liên quan đến webhook failures
kubectl get events --all-namespaces \
--field-selector reason=FailedCreate \
| grep -i webhook
# Kiểm tra webhook service health
kubectl get pods -n WEBHOOK_NAMESPACE -l app=WEBHOOK_APPDấu hiệu từ kube-apiserver logs (trong Cloud Logging):
logName="projects/PROJECT/logs/cloudaudit.googleapis.com%2Factivity"
protoPayload.methodName="io.k8s.core.v1.pods.create"
protoPayload.status.code=500Giải quyết:
# Temporary: Disable problematic webhook
kubectl delete mutatingwebhookconfiguration WEBHOOK_NAME
# Hoặc patch webhook để exclude system namespaces
kubectl patch mutatingwebhookconfiguration WEBHOOK_NAME \
--type=json \
-p='[{"op": "add", "path": "/webhooks/0/namespaceSelector",
"value": {"matchExpressions": [{"key": "kubernetes.io/metadata.name",
"operator": "NotIn", "values": ["kube-system", "gke-system"]}]}}]'Nguyên nhân 5: Maintenance window boundary
Upgrade bắt đầu trong maintenance window nhưng không hoàn thành trước khi window kết thúc. GKE pause upgrade và đợi window tiếp theo.
# Kiểm tra maintenance window hiện tại
gcloud container clusters describe CLUSTER_NAME \
--format="yaml(maintenancePolicy)"
# Kiểm tra cluster conditions
kubectl get nodes -o json | jq '.items[].status.conditions'Giải quyết: Manual upgrade để bypass window:
gcloud container clusters upgrade CLUSTER_NAME \
--node-pool=POOL_NAME \
--cluster-version=TARGET_VERSION \
--zone=ZONENguyên nhân 6: Long terminationGracePeriodSeconds
Pod có grace period dài (ví dụ: 600 giây). Drain đang đợi pod shutdown. Không phải stuck — chỉ là chậm.
# Xem pods đang terminate và termination time
kubectl get pods --all-namespaces -o json \
| jq '.items[] | select(.metadata.deletionTimestamp != null) |
{name: .metadata.name,
namespace: .metadata.namespace,
deletionTimestamp: .metadata.deletionTimestamp,
gracePeriod: .spec.terminationGracePeriodSeconds}'Phân biệt "chậm" vs "stuck":
- Pod có
deletionTimestampvà đang trong grace period → chậm, bình thường - Pod không có
deletionTimestampnhưng node đã cordon → stuck, cần investigate
Manual intervention: khi nào và làm thế nào
Cancel và retry upgrade
# Xem operation ID của upgrade đang chạy
gcloud container operations list \
--filter="status=RUNNING" \
--format="value(name)"
# Cancel operation (không phải luôn possible)
gcloud container operations cancel OPERATION_ID --zone=ZONE
# Retry upgrade sau khi fix nguyên nhân
gcloud container clusters upgrade CLUSTER_NAME \
--node-pool=POOL_NAME \
--cluster-version=TARGET_VERSION \
--zone=ZONEForce drain node trong emergency
CẢNH BÁO: Force drain bỏ qua PDB, có thể gây service disruption.
# Force drain node (bỏ qua PDB)
kubectl drain NODE_NAME \
--ignore-daemonsets \
--delete-emptydir-data \
--force \
--grace-period=30
# Sau khi manual fix, uncordon node nếu cần giữ lại
kubectl uncordon NODE_NAMEChỉ dùng force drain khi:
- Đã xác định nguyên nhân stuck và không thể fix bằng cách khác
- Đã thông báo cho stakeholders về potential disruption
- Có kế hoạch rollback nếu service bị impact
Rollback blue-green upgrade
Nếu cluster đang dùng blue-green upgrade và phát hiện regression:
# Rollback blue-green upgrade (chỉ hoạt động trong soak period)
gcloud container node-pools rollback NODE_POOL_NAME \
--cluster=CLUSTER_NAME \
--zone=ZONEOutput:
Rolling back upgrade of node pool [NODE_POOL_NAME] on cluster [CLUSTER_NAME]...
Updated [https://container.googleapis.com/v1/.../nodePools/NODE_POOL_NAME].Nếu soak period đã hết và blue pool bị xóa, không thể rollback qua command này. Cần manual rollback: tạo node pool mới với version cũ.
Testing upgrade strategy: staging cluster validation
Tại sao staging cluster cần khác với production
Test upgrade trên staging chỉ có giá trị khi staging đủ giống production để reproduce issues. Staging "nhỏ hơn nhiều và không có real workload" sẽ không catch các vấn đề như:
- PDB với nhiều replicas
- Topology spread constraints với nhiều zones
- Long-running jobs bị interrupt
- Admission webhooks với production config
Staging checklist:
□ Cùng GKE version (hoặc 1 major version ahead của production)
□ Cùng release channel với production (hoặc Rapid nếu muốn early warning)
□ Cùng node pool configuration (machine type, labels, taints)
□ Subset của production workload (ít nhất 1 instance của mỗi deployment type)
□ Cùng PDB configuration
□ Cùng admission webhooks
□ Production-like traffic (có thể dùng traffic shadowing hoặc load test)kubectl drain testing
kubectl drain với --dry-run cho phép simulate drain mà không thực sự evict pods:
# Dry-run drain để xem pods nào sẽ bị evict và warning gì
kubectl drain NODE_NAME \
--ignore-daemonsets \
--delete-emptydir-data \
--dry-run
# Output sẽ show:
# node/node-xyz cordoned (dry run)
# evicting pod default/my-app-abc123 (dry run)
# WARNING: deleting Pods not managed by ReplicationController, Job, or similarChú ý warnings:
WARNING: deleting Pods not managed by...— Pod sẽ bị xóa và không được recreate tự độngevicting pod ... (dry run)— Pod sẽ được evict (PDB check đã pass)error: cannot evict pod ... since it is being used by PDB...— PDB blocking, cần fix trước
Simulation upgrade đầy đủ trên staging
Script để test upgrade end-to-end:
#!/bin/bash
CLUSTER=staging-cluster
ZONE=us-central1-a
CURRENT_VERSION=$(gcloud container clusters describe $CLUSTER --zone=$ZONE \
--format="value(currentMasterVersion)")
TARGET_VERSION=TARGET_VERSION_HERE
echo "=== Bước 1: Upgrade control plane ==="
gcloud container clusters upgrade $CLUSTER \
--master \
--cluster-version=$TARGET_VERSION \
--zone=$ZONE
echo "=== Đợi control plane upgrade ==="
gcloud container operations wait OPERATION_ID --zone=$ZONE
echo "=== Bước 2: Verify control plane healthy ==="
kubectl get nodes
kubectl get pods --all-namespaces | grep -v Running | grep -v Completed
echo "=== Bước 3: Dry-run drain một node để check PDB ==="
NODE=$(kubectl get nodes -o jsonpath='{.items[0].metadata.name}')
kubectl drain $NODE --ignore-daemonsets --delete-emptydir-data --dry-run
echo "=== Bước 4: Upgrade node pool ==="
gcloud container clusters upgrade $CLUSTER \
--node-pool=default-pool \
--cluster-version=$TARGET_VERSION \
--zone=$ZONE
echo "=== Bước 5: Monitor pod restarts trong khi upgrade ==="
watch -n 5 "kubectl get pods --all-namespaces | grep -v Running | grep -v Completed"
echo "=== Bước 6: Chạy integration tests sau upgrade ==="
./run_integration_tests.sh
echo "=== Upgrade staging hoàn thành ==="Kiểm tra API deprecations trước minor version upgrade
Minor version upgrades thường đi kèm API deprecations. Cần kiểm tra trước:
# Dùng kubectl để check deprecated API usage
kubectl api-resources --verbs=list -o name \
| xargs -I {} kubectl get {} --all-namespaces -o json 2>/dev/null \
| jq '.items[].apiVersion' | sort | uniq
# Dùng pluto (open source tool) để detect deprecated APIs
# https://github.com/FairwindsOps/pluto
pluto detect-files -d . -t k8s=v1.29
pluto detect-helm -t k8s=v1.29
# Kiểm tra deployed resources
pluto detect-helm -n default -t k8s=v1.29Output điển hình:
NAME NAMESPACE KIND VERSION REPLACEMENT REMOVED DEPRECATED
nginx-ingress-controller default PodSecurityPolicy policy/v1beta1 N/A true trueValidate upgrade ảnh hưởng đến application behavior
Sau khi upgrade staging, chạy automated checks:
# Check 1: Tất cả Deployments đang ở desired replica count
kubectl get deployments --all-namespaces -o json \
| jq '.items[] | select(.spec.replicas != .status.readyReplicas) |
{name: .metadata.name, desired: .spec.replicas, ready: .status.readyReplicas}'
# Check 2: Không có pods bị CrashLoopBackOff
kubectl get pods --all-namespaces --field-selector 'status.phase!=Running,status.phase!=Succeeded' \
| grep -v Terminating
# Check 3: Services có endpoints
kubectl get endpoints --all-namespaces \
| awk '{print $1, $2, $3}' | column -t
# Check 4: PVCs bound
kubectl get pvc --all-namespaces \
| grep -v Bound
# Check 5: HPA status
kubectl get hpa --all-namespacesLoad testing trong upgrade window
Quan trọng: Chạy load test đồng thời với upgrade để validate no-disruption claims:
# Giữ load test chạy liên tục trong khi upgrade node pool
# Tool: hey, k6, vegeta, locust
# Ví dụ với vegeta
echo "GET http://staging-api.example.com/health" \
| vegeta attack -duration=30m -rate=100 \
| tee results.bin \
| vegeta report
# Trong terminal khác: trigger node pool upgrade
gcloud container clusters upgrade $CLUSTER \
--node-pool=default-pool \
--cluster-version=$TARGET_VERSION \
--zone=$ZONE
# Sau upgrade, analyze results
vegeta report -type=text results.bin
vegeta report -type=hist[0,5ms,10ms,50ms,100ms,200ms,500ms,1s] results.binMetrics cần theo dõi trong load test:
- Latency percentiles (p50, p95, p99) — không được tăng đột biến
- Error rate — không được vượt SLO
- Throughput — không được giảm đáng kể
Post-upgrade checklist
Sau khi upgrade hoàn thành trên staging và production:
#!/bin/bash
echo "=== Post-Upgrade Validation Checklist ==="
echo "1. Control plane version"
kubectl version --short
echo "2. Node versions"
kubectl get nodes -o custom-columns=NAME:.metadata.name,VERSION:.status.nodeInfo.kubeletVersion
echo "3. System pods healthy"
kubectl get pods -n kube-system
echo "4. GKE system pods healthy"
kubectl get pods -n gke-system
echo "5. All user pods running"
kubectl get pods --all-namespaces | grep -v "Running\|Completed" | grep -v "NAMESPACE"
echo "6. PVCs bound"
kubectl get pvc --all-namespaces | grep -v Bound
echo "7. Services có endpoints"
kubectl get svc --all-namespaces -o json \
| jq '.items[] | select(.spec.type != "ExternalName") | .metadata.name'
echo "8. HPA status"
kubectl get hpa --all-namespaces
echo "9. Recent events (warnings)"
kubectl get events --all-namespaces --field-selector type=Warning \
--sort-by='.lastTimestamp' | tail -20
echo "=== Done ==="Cloud Logging queries cho upgrade debugging
# Tất cả node upgrade events
gcloud logging read \
'resource.type="k8s_node" AND
protoPayload.methodName=~"nodePools\.(upgrade|create|delete)"' \
--limit=50 \
--format="value(timestamp,protoPayload.methodName,protoPayload.resourceName,protoPayload.status.code)"
# PDB-related eviction failures
gcloud logging read \
'resource.type="k8s_cluster" AND
jsonPayload.kind="Event" AND
jsonPayload.reason="FailedEviction"' \
--limit=20 \
--format="value(timestamp,jsonPayload.message)"
# Control plane upgrade audit
gcloud logging read \
'resource.type="k8s_cluster" AND
protoPayload.methodName="google.container.v1.ClusterManager.UpdateCluster"' \
--limit=10 \
--format="value(timestamp,protoPayload.request,protoPayload.response)"
# Surge node creation failures
gcloud logging read \
'resource.type="gce_instance" AND
textPayload=~"(quota|RESOURCE_EXHAUSTED|insufficient|limit exceeded)"' \
--limit=20 \
--format="value(timestamp,textPayload)"Upgrade upgrade estimate: tính thời gian cần thiết
Công thức ước tính thời gian upgrade node pool (surge, mặc định):
node_count = 100
startup_time_per_node = 3 phút # Thời gian provision node mới
drain_time_per_node = 5 phút # Thời gian drain pods (average, phụ thuộc workload)
max_surge = 1 # Mặc định
total_time ≈ node_count × (startup_time_per_node + drain_time_per_node) / max_surge
= 100 × (3 + 5) / 1
= 800 phút ≈ 13 giờ
Với max_surge = 5:
= 100 × (3 + 5) / 5
= 160 phút ≈ 2.7 giờĐây là lý do tại sao maintenance window phải đủ dài. Với 100 nodes và default surge setting, cần 13+ giờ window — thường không realistic. Tăng maxSurge hoặc chia nhỏ pool là giải pháp thực tế.
References
- Troubleshooting GKE upgrades — Official troubleshooting guide
- kubectl drain — Drain command reference
- Upgrade assist — GKE upgrade visibility tool
- pluto — Deprecated API detection tool
- GKE Release Notes — Breaking changes per version