Skip to content

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

bash
# 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/update
  • status: ERROR — Upgrade thất bại
  • conditions[].message — Mô tả tình trạng hiện tại

Kiểm tra node status

bash
# 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 -20

Dấu hiệu của stuck drain:

NAME          STATUS                     ROLES    AGE   VERSION
node-abc-xyz  Ready,SchedulingDisabled   <none>   2h    v1.28.5

Node đã cordon (SchedulingDisabled) nhưng vẫn còn pods → drain bị blocked.

bash
# 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.phase

Bướ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.

bash
# 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 evict

Diagnose chi tiết hơn:

bash
# 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 NAMESPACE

Output đ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)

bash
# 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

bash
# Scale up đủ để PDB cho phép eviction
kubectl scale deployment my-service --replicas=4 -n default
# Sau upgrade, scale back xuống 3

Option 3: Xóa PDB tạm thời cho maintenance (chỉ cho critical emergency)

bash
# DANGEROUS: Chỉ dùng khi không có option nào khác
kubectl delete pdb my-service-pdb -n default
# Nhớ recreate sau upgrade

Nguyên nhân 2: Node affinity constraints quá restrictive

bash
# 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:

bash
# 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=value

Nguyên nhân 3: Quota exhaustion (surge node creation failed)

bash
# 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:

  • failurePolicy: Fail và webhook service down
  • Block creation của system resources
bash
# 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_APP

Dấ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=500

Giải quyết:

bash
# 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.

bash
# 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:

bash
gcloud container clusters upgrade CLUSTER_NAME \
    --node-pool=POOL_NAME \
    --cluster-version=TARGET_VERSION \
    --zone=ZONE

Nguyê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.

bash
# 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ó deletionTimestamp và đang trong grace period → chậm, bình thường
  • Pod không có deletionTimestamp nhưng node đã cordon → stuck, cần investigate

Manual intervention: khi nào và làm thế nào

Cancel và retry upgrade

bash
# 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=ZONE

Force drain node trong emergency

CẢNH BÁO: Force drain bỏ qua PDB, có thể gây service disruption.

bash
# 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_NAME

Chỉ 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:

bash
# Rollback blue-green upgrade (chỉ hoạt động trong soak period)
gcloud container node-pools rollback NODE_POOL_NAME \
    --cluster=CLUSTER_NAME \
    --zone=ZONE

Output:

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:

bash
# 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 similar

Chú ý warnings:

  • WARNING: deleting Pods not managed by... — Pod sẽ bị xóa và không được recreate tự động
  • evicting 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:

bash
#!/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:

bash
# 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.29

Output điển hình:

NAME                        NAMESPACE   KIND                      VERSION    REPLACEMENT   REMOVED   DEPRECATED
nginx-ingress-controller    default     PodSecurityPolicy         policy/v1beta1   N/A     true      true

Validate upgrade ảnh hưởng đến application behavior

Sau khi upgrade staging, chạy automated checks:

bash
# 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-namespaces

Load testing trong upgrade window

Quan trọng: Chạy load test đồng thời với upgrade để validate no-disruption claims:

bash
# 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.bin

Metrics 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:

bash
#!/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

bash
# 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