Skip to content

Workload Disruption Readiness: PDB, Annotations & Upgrade Notifications

Disruption readiness là trách nhiệm của developer, không phải operator

Maintenance windows kiểm soát khi nào upgrade xảy ra. Upgrade strategy kiểm soát cách nodes được recreate. Nhưng workload có tồn tại được qua disruption hay không — đó hoàn toàn là trách nhiệm của developer.

Đây là ranh giới trách nhiệm quan trọng. Operator có thể cấu hình blue-green upgrade hoàn hảo với soak period 24 giờ, nhưng nếu workload không có đủ replicas và PDB được cấu hình sai, disruption vẫn xảy ra.

Disruption readiness không phải là checklist hoàn thành một lần — nó là yêu cầu thiết kế embedded vào mọi workload chạy trên GKE.

PodDisruptionBudget: semantics và failure modes

PDB hoạt động như thế nào

PodDisruptionBudget (PDB) là Kubernetes API object định nghĩa ràng buộc về số lượng pods có thể bị disrupted đồng thời từ nguồn gốc voluntary disruption (node drain, upgrade, admin eviction).

yaml
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: my-service-pdb
spec:
  # Phải specify một trong hai: minAvailable hoặc maxUnavailable
  minAvailable: 2       # Hoặc "50%" để relative
  # maxUnavailable: 1   # Hoặc "25%"
  selector:
    matchLabels:
      app: my-service

Khi node drain xảy ra:

  1. GKE eviction controller gửi Eviction request (không phải DELETE Pod) đến API server
  2. API server kiểm tra PDB: disruption budget có cho phép eviction này không?
  3. Nếu có → accept eviction request → kubelet terminate pod
  4. Nếu không → trả về HTTP 429 Too Many Requests → eviction bị từ chối
  5. GKE đợi và retry → sau 1 giờ, force evict bất kể PDB

Điểm quan trọng: PDB không ngăn được forced eviction sau 1 giờ. PDB chỉ slow down quá trình drain, không stop nó vĩnh viễn.

minAvailable vs maxUnavailable: khi nào dùng cái nào

minAvailable — số pods tối thiểu phải luôn available:

yaml
spec:
  minAvailable: 2   # Luôn phải có ít nhất 2 pods running

Phù hợp khi bạn biết rõ số lượng minimum cần thiết để service hoạt động bình thường.

maxUnavailable — số pods tối đa có thể unavailable đồng thời:

yaml
spec:
  maxUnavailable: 1   # Tối đa 1 pod được phép unavailable

Phù hợp hơn khi service có thể tolerate một số lượng pod down tỷ lệ với total replicas (ví dụ: caching layer có thể tolerate 1 instance down).

Percentage form:

yaml
# minAvailable percentage — cẩn thận với rounding
spec:
  minAvailable: "50%"  # Nếu có 3 replicas → ceil(3*0.5) = 2 available minimum

Gotcha với percentage và số lẻ:

  • 3 replicas, minAvailable 50% → 50% của 3 = 1.5, Kubernetes round lên → 2 available, max 1 unavailable
  • 3 replicas, minAvailable 100% → 3 available, 0 unavailable → KHÔNG AI ĐƯỢC EVICT
  • 1 replica, minAvailable 1 → không ai được evict → upgrade sẽ hung 1 giờ rồi force kill

PDB không bảo vệ khỏi involuntary disruption

PDB chỉ có tác dụng với voluntary disruption:

  • Node drain (upgrade, manual drain)
  • Admin eviction (kubectl delete pod)
  • Autoscaler scale-down (nếu tôn trọng PDB — Cluster Autoscaler có)

PDB không bảo vệ khỏi:

  • OOMKill
  • Node failure (hardware failure, zone outage)
  • Involuntary eviction từ node pressure (disk/memory)
  • SIGKILL từ kernel

Thiết kế PDB phù hợp với replica count

Anti-pattern nguy hiểm nhất:

yaml
# Workload chạy single replica
spec:
  replicas: 1
---
# PDB
spec:
  minAvailable: 1  # Equivalent với "không ai được evict"

Single-replica service với minAvailable: 1 là combination worst-case. Trong upgrade, GKE sẽ cố evict pod, bị PDB chặn 1 giờ, rồi force kill. Service bị down 1 giờ (đợi) + restart time. Không tệ hơn không có PDB, nhưng cũng không tốt hơn — vì với single replica, dù có PDB hay không thì vẫn down khi node drain.

Minimum viable pattern cho production:

yaml
# Service cần ít nhất 2 replicas để PDB có ý nghĩa
spec:
  replicas: 3
---
spec:
  maxUnavailable: 1  # Tại mọi thời điểm, tối đa 1 trong 3 pods down

Pattern cho stateful services:

yaml
# StatefulSet với 3 replicas
spec:
  replicas: 3
---
spec:
  minAvailable: 2  # Luôn có 2 trong 3 database nodes available
  selector:
    matchLabels:
      app: my-db

PDB và topology spread constraints

PDB bảo vệ về số lượng, topology spread constraints bảo vệ về phân bổ. Chúng cần hoạt động cùng nhau.

Scenario không có topology spread:

Node A (zone-1): pod-1, pod-2
Node B (zone-2): pod-3
PDB: minAvailable=2

Khi Node A drain:
- Evict pod-1 → 2 pods còn lại (pod-2 đang drain + pod-3) = PDB OK
- Evict pod-2 → pod-2 evicted → chỉ pod-3 còn lại → chỉ 1 pod = VIOLATE PDB
→ pod-2 eviction bị blocked, đợi 1 giờ, force evict
→ Momentarily chỉ có 1 pod (pod-3) trong zone-2 đang serve traffic

Với topology spread + PDB:

yaml
spec:
  replicas: 3
  template:
    spec:
      topologySpreadConstraints:
      - maxSkew: 1
        topologyKey: topology.kubernetes.io/zone
        whenUnsatisfiable: DoNotSchedule
---
spec:
  maxUnavailable: 1

Kết quả: Mỗi zone có tối đa 1 pod. Khi node drain, pod migrate sang zone khác, không bao giờ có zone nào mất tất cả pods.

pod-deletion-cost: kiểm soát thứ tự eviction

pod-deletion-cost là annotation cho phép chỉ định ưu tiên eviction khi có nhiều pods cần được evict:

yaml
# Pod nên được evict trước (cost thấp = ưu tiên evict trước)
metadata:
  annotations:
    controller.kubernetes.io/pod-deletion-cost: "-100"

# Pod nên được evict sau (cost cao = giữ lại lâu hơn)
metadata:
  annotations:
    controller.kubernetes.io/pod-deletion-cost: "100"

Giá trị: Integer, range không giới hạn. Pod có deletion cost thấp hơn được evict trước.

Use case thực tế:

1. Warm pods vs cold pods trong caching layer:

yaml
# Pod đã warm (cache đầy) — giữ lại lâu hơn
annotations:
  controller.kubernetes.io/pod-deletion-cost: "1000"

# Pod mới startup (cache còn cold) — evict trước
annotations:
  controller.kubernetes.io/pod-deletion-cost: "-1000"

2. Primary vs replica trong read/write split:

yaml
# Primary pod — evict cuối cùng
annotations:
  controller.kubernetes.io/pod-deletion-cost: "10000"

# Replica pod — evict trước
annotations:
  controller.kubernetes.io/pod-deletion-cost: "0"

3. Dynamic cost theo workload:

Pod có thể tự cập nhật annotation của mình trong quá trình chạy bằng cách patch Pod object qua Kubernetes API. Một sidecar hoặc lifecycle process có thể:

  • Set cost cao khi đang xử lý critical request
  • Set cost thấp khi idle
python
# Trong application code — update deletion cost dynamically
import kubernetes

def set_deletion_cost(cost: int):
    """Cập nhật pod-deletion-cost dựa trên trạng thái workload"""
    v1 = kubernetes.client.CoreV1Api()
    pod_name = os.environ["POD_NAME"]
    namespace = os.environ["POD_NAMESPACE"]
    
    body = {
        "metadata": {
            "annotations": {
                "controller.kubernetes.io/pod-deletion-cost": str(cost)
            }
        }
    }
    v1.patch_namespaced_pod(pod_name, namespace, body)

# Khi bắt đầu xử lý critical transaction
set_deletion_cost(1000)
# Khi hoàn thành
set_deletion_cost(0)

Giới hạn: pod-deletion-cost là hint, không phải guarantee. Kubernetes scheduler cố gắng tôn trọng nhưng không đảm bảo thứ tự exact khi có PDB constraints hoặc scheduling constraints.

safe-to-evict annotation

cluster-autoscaler.kubernetes.io/safe-to-evict là annotation kiểm soát Cluster Autoscaler có thể evict pod khi scale down không:

yaml
# Ngăn CA evict pod này khi scale down
metadata:
  annotations:
    cluster-autoscaler.kubernetes.io/safe-to-evict: "false"

# Cho phép CA evict (default cho pods không có annotation này)
metadata:
  annotations:
    cluster-autoscaler.kubernetes.io/safe-to-evict: "true"

Tương tác với node upgrades:

Annotation này chủ yếu ảnh hưởng Cluster Autoscaler scale-down. Khi node drain trong upgrade:

  • Surge/Blue-Green upgrade: Tôn trọng PDB nhưng không tôn trọng safe-to-evict: false
  • Autoscaled Blue-Green: CA-controlled drain sẽ tôn trọng safe-to-evict: false khi quyết định drain node

Khi nào dùng safe-to-evict: false:

  • Pods với emptyDir chứa dữ liệu quan trọng (cache, temp data) mà không thể mất
  • Daemonset pods không muốn bị recreate khi CA scale down (thường là monitoring agents)
  • Pods đang chạy long-running jobs không idempotent

Anti-pattern:

yaml
# Workload set safe-to-evict: false với mọi pods
# Cluster Autoscaler không thể scale down bất kỳ node nào
# Cluster bị stuck ở over-provisioned state
metadata:
  annotations:
    cluster-autoscaler.kubernetes.io/safe-to-evict: "false"  # Chỉ dùng khi thực sự cần

terminationGracePeriodSeconds và preStop hooks

terminationGracePeriodSeconds

Khi pod bị evict, Kubernetes gửi SIGTERM vào container. Container có terminationGracePeriodSeconds (default: 30 giây) để graceful shutdown trước khi bị SIGKILL.

yaml
spec:
  terminationGracePeriodSeconds: 120  # 2 phút để graceful shutdown
  containers:
  - name: my-app
    lifecycle:
      preStop:
        exec:
          command: ["/bin/sh", "-c", "sleep 15"]  # Đợi load balancer drain

Interaction với 1-giờ upgrade limit:

terminationGracePeriodSeconds: 3600  # 1 giờ
→ Khi node drain, pod nhận SIGTERM
→ Pod đang xử lý long-running job, cần đủ 1 giờ để finish
→ Nhưng GKE hard limit cũng là 1 giờ
→ Sau 1 giờ, GKE force delete pod bất kể
→ Pod bị kill sau khoảng 1 giờ - race condition với upgrade timeout

Không thể set terminationGracePeriodSeconds > 3600 và expect nó được tôn trọng trong node drain. Nếu cần graceful shutdown dài hơn 1 giờ, cần blue-green upgrade với extended soak period, không phải surge upgrade.

preStop hook: giải quyết load balancer drain race condition

Race condition phổ biến nhất khi upgrade: load balancer tiếp tục gửi requests đến pod sau khi pod nhận SIGTERM nhưng trước khi load balancer cập nhật backend list.

Timeline:
T=0: GKE gửi eviction request
T=0: kube-proxy cập nhật iptables/eBPF rules để remove pod khỏi Service endpoints
T=0: Pod nhận SIGTERM
T=~5s: Load balancer cập nhật backend health (có lag)
T=~5s: Requests vẫn đang được route đến pod đang shutdown

Nếu không có preStop hook:
T=0: SIGTERM → app bắt đầu shutdown → từ chối new connections
T=0-5s: Requests vẫn đến nhưng bị từ chối → HTTP 502/503 burst

Giải pháp với preStop hook:

yaml
lifecycle:
  preStop:
    exec:
      command:
      - /bin/sh
      - -c
      - |
        # Đợi load balancer drain connections (typically 10-15s)
        sleep 15
        # Sau đó application mới nhận SIGTERM và bắt đầu shutdown

Hoặc cho web servers với graceful drain endpoint:

yaml
lifecycle:
  preStop:
    httpGet:
      path: /healthz/ready  # Endpoint return 503 để báo LB không route vào đây nữa
      port: 8080

Thực tế với GKE load balancing:

  • NEG (container-native load balancing): Health check propagation nhanh hơn (~10s)
  • Classic load balancing qua kube-proxy: Có thể mất 30–60s để drain

sleep 15 trong preStop là minimum; với classic LB nên dùng sleep 30–60.

Tác động lên terminationGracePeriodSeconds:

terminationGracePeriodSeconds: 60
preStop: sleep 15

Timeline thực tế:
T=0: Pod bắt đầu terminate, preStop chạy
T=0-15s: preStop sleep (pod chưa nhận SIGTERM)
T=15s: preStop kết thúc, container nhận SIGTERM
T=15s-60s: Container có 45 giây còn lại để graceful shutdown
T=60s: SIGKILL nếu container vẫn còn chạy

Đây là lý do tại sao terminationGracePeriodSeconds phải = preStop duration + actual shutdown time.

Workload readiness checklist cho upgrade

Checklist cho stateless HTTP service

□ replicas >= 3 (đủ để tolerate 1 pod down với PDB)
□ PDB: maxUnavailable: 1 (hoặc minAvailable: replicas-1)
□ PDB selector match chính xác pod labels
□ topology spread: maxSkew=1, topologyKey=zone
□ terminationGracePeriodSeconds >= preStop time + shutdown time
□ preStop hook: sleep đủ dài cho LB drain (30s minimum với classic LB)
□ Readiness probe: return 503 sớm khi starting shutdown
□ Không có state trong container (chỉ stateless)

Checklist cho stateful service (database, cache)

□ StatefulSet thay vì Deployment (nếu cần pod identity bền vững)
□ replicas >= 3 (quorum-based) hoặc >= 2 (primary/replica)
□ PDB: minAvailable = quorum size (ví dụ: 2 cho 3-node cluster)
□ Xử lý SIGTERM gracefully: flush write buffers, close connections
□ terminationGracePeriodSeconds >= worst-case flush time
□ Volume data được backup: đừng assume PVC survive node failure
□ Pod anti-affinity: đảm bảo instances trải rộng zones
□ Test drain bằng kubectl drain trên staging trước

Checklist cho batch/ML jobs

□ Checkpoint thường xuyên (mỗi N steps, không phải chỉ cuối cùng)
□ Safe-to-evict: false nếu job không idempotent
□ Restart policy: OnFailure với backoff limit phù hợp
□ Resource requests đúng (không over-commit, không under-request)
□ Workload Identity để access GCS cho checkpoint storage
□ preStop hook: trigger checkpoint save trước khi SIGTERM
□ Test với spot nodes để validate preemption handling

Upgrade notifications: subscriber proactively

Tại sao cần subscribe notifications

Nếu không subscribe notifications, team chỉ biết upgrade sắp xảy ra khi:

  1. Thấy pod eviction trong logs (quá muộn)
  2. Kiểm tra cluster version thủ công (tedious)
  3. Nhận alert từ user về service degradation (worst case)

Subscribe Pub/Sub notifications cho phép team biết trước và chuẩn bị.

Setup Pub/Sub notifications cho cluster upgrades

bash
# Bước 1: Tạo topic
gcloud pubsub topics create gke-upgrade-notifications \
    --project=PROJECT_ID

# Bước 2: Cấp quyền cho GKE publish vào topic
gcloud pubsub topics add-iam-policy-binding gke-upgrade-notifications \
    --member="serviceAccount:container-engine-robot@system.gserviceaccount.com" \
    --role="roles/pubsub.publisher" \
    --project=PROJECT_ID

# Bước 3: Enable notifications cho cluster
gcloud container clusters update CLUSTER_NAME \
    --notification-config=pubsub=ENABLED,pubsub-topic=projects/PROJECT_ID/topics/gke-upgrade-notifications \
    --zone=ZONE

# Bước 4: Tạo subscription để consume notifications
gcloud pubsub subscriptions create gke-upgrade-sub \
    --topic=gke-upgrade-notifications \
    --project=PROJECT_ID

Phân tích các loại notification

UpgradeAvailableEvent — Version mới available cho channel:

json
{
  "payload": {
    "typeUrl": "type.googleapis.com/google.container.v1beta1.UpgradeAvailableEvent",
    "value": {
      "version": "1.29.5-gke.1234567",
      "resourceType": "MASTER",
      "releaseChannel": "REGULAR",
      "windowStartTime": "2024-03-15T02:00:00Z"
    }
  }
}

Đây là advance warning — version mới available nhưng upgrade chưa bắt đầu. Đây là thời điểm team nên:

  • Check changelog và release notes cho version mới
  • Schedule upgrade testing trên staging
  • Review deprecated APIs nếu có

UpgradeEvent — Upgrade bắt đầu:

json
{
  "payload": {
    "typeUrl": "type.googleapis.com/google.container.v1beta1.UpgradeEvent",
    "value": {
      "resourceType": "MASTER",
      "operation": "operation-1234",
      "operationStartTime": "2024-03-17T02:15:00Z",
      "currentVersion": "1.28.5-gke.1091000",
      "targetVersion": "1.29.5-gke.1234567",
      "resource": "projects/my-project/locations/us-central1-a/clusters/prod-cluster"
    }
  }
}

SecurityBulletinEvent — Security bulletin ảnh hưởng cluster:

json
{
  "payload": {
    "typeUrl": "type.googleapis.com/google.container.v1beta1.SecurityBulletinEvent",
    "value": {
      "resourceTypeAffected": "NODE",
      "bulletinId": "GCP-2024-001",
      "severity": "HIGH",
      "suggestedUpgradeTarget": "1.29.5-gke.1234567"
    }
  }
}

Automation dựa trên notifications

Pattern: Auto-trigger staging upgrade khi production nhận UpgradeAvailableEvent:

python
# Cloud Function consumer
import base64
import json
from google.cloud import container_v1

def handle_notification(event, context):
    """Xử lý GKE upgrade notification"""
    message = json.loads(base64.b64decode(event['data']).decode('utf-8'))
    
    notification_type = message.get('payload', {}).get('typeUrl', '')
    
    if 'UpgradeAvailableEvent' in notification_type:
        payload = message['payload']['value']
        
        # Nếu production cluster nhận available version mới
        if payload['releaseChannel'] == 'REGULAR':
            target_version = payload['version']
            
            # Trigger staging cluster upgrade ngay lập tức
            client = container_v1.ClusterManagerClient()
            client.update_cluster(
                name="projects/my-project/locations/us-central1/clusters/staging-cluster",
                update=container_v1.ClusterUpdate(
                    desired_master_version=target_version
                )
            )
            
            # Gửi Slack notification
            send_slack_message(
                f"⚠️ GKE {payload['releaseChannel']} nhận version {target_version}. "
                f"Staging đang upgrade. Review changelog: https://..."
            )
    
    elif 'UpgradeEvent' in notification_type:
        # Upgrade đang diễn ra — alert on-call nếu ngoài maintenance window
        pass

Monitor upgrade với Cloud Monitoring

Ngoài Pub/Sub, monitor metrics trong quá trình upgrade:

bash
# Dashboard metric queries để watch trong upgrade
# (dùng trong Cloud Monitoring)

# Số nodes Ready
kubernetes.io/node/ready_node_count

# Pod restart count (tăng đột biến = problem)
kubernetes.io/container/restart_count

# HTTP error rate
http/server/response_count {response_code!=2xx}

# PDB disruption events
kubernetes.io/pod/disruption_count

Alert policy khuyến nghị cho upgrade window:

yaml
# Alert khi ready nodes giảm xuống dưới threshold
displayName: "GKE Node Readiness Drop During Upgrade"
conditions:
- displayName: "Ready node count"
  conditionThreshold:
    filter: 'metric.type="kubernetes.io/node/ready_node_count"'
    comparison: COMPARISON_LT
    thresholdValue: <cluster_size * 0.8>  # Alert nếu <80% nodes ready
    duration: 300s  # 5 phút

Common mistakes khi chuẩn bị cho upgrade

1. PDB tham chiếu sai selector

yaml
# Deployment label
metadata:
  labels:
    app: my-service
    version: v1

# PDB selector chỉ match app, không match version
spec:
  selector:
    matchLabels:
      app: my-service
      version: v2  # BUG: v2 không tồn tại → PDB không match bất kỳ pod nào → không protect gì

Test PDB trước upgrade: kubectl get pods -l app=my-service phải match kubectl get pdb my-service-pdb -o yaml.

2. terminationGracePeriodSeconds quá ngắn

yaml
# App cần 2 phút để flush data khi shutdown
terminationGracePeriodSeconds: 30  # 30 giây không đủ → data loss

Measure actual shutdown time trong staging với kubectl drain --dry-run.

3. preStop hook quá dài

yaml
lifecycle:
  preStop:
    exec:
      command: ["/bin/sh", "-c", "sleep 120"]  # 2 phút preStop

terminationGracePeriodSeconds: 60  # 1 phút total

# BUG: preStop chạy 120s nhưng total grace period chỉ 60s
# → preStop bị SIGKILL sau 60s
# → App không có thời gian graceful shutdown

Luôn đảm bảo terminationGracePeriodSeconds > preStop duration + app shutdown time.

4. Không test trên staging trước minor version upgrade

Minor version upgrade thường có API deprecations và behavior changes. Test trên staging bằng cách trigger manual upgrade:

bash
# Upgrade staging cluster trước production
gcloud container clusters upgrade staging-cluster \
    --master \
    --cluster-version=TARGET_VERSION \
    --zone=ZONE

# Chờ nodes upgrade
gcloud container clusters upgrade staging-cluster \
    --node-pool=default-pool \
    --cluster-version=TARGET_VERSION \
    --zone=ZONE

# Chạy full regression test suite
./run_integration_tests.sh

References