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).
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-serviceKhi node drain xảy ra:
- GKE eviction controller gửi
Evictionrequest (không phải DELETE Pod) đến API server - API server kiểm tra PDB: disruption budget có cho phép eviction này không?
- Nếu có → accept eviction request → kubelet terminate pod
- Nếu không → trả về HTTP 429 Too Many Requests → eviction bị từ chối
- 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:
spec:
minAvailable: 2 # Luôn phải có ít nhất 2 pods runningPhù 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:
spec:
maxUnavailable: 1 # Tối đa 1 pod được phép unavailablePhù 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:
# minAvailable percentage — cẩn thận với rounding
spec:
minAvailable: "50%" # Nếu có 3 replicas → ceil(3*0.5) = 2 available minimumGotcha 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:
# 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:
# 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 downPattern cho stateful services:
# StatefulSet với 3 replicas
spec:
replicas: 3
---
spec:
minAvailable: 2 # Luôn có 2 trong 3 database nodes available
selector:
matchLabels:
app: my-dbPDB 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 trafficVới topology spread + PDB:
spec:
replicas: 3
template:
spec:
topologySpreadConstraints:
- maxSkew: 1
topologyKey: topology.kubernetes.io/zone
whenUnsatisfiable: DoNotSchedule
---
spec:
maxUnavailable: 1Kế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:
# 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:
# 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:
# 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
# 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:
# 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: falsekhi quyết định drain node
Khi nào dùng safe-to-evict: false:
- Pods với
emptyDirchứ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:
# 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ầnterminationGracePeriodSeconds 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.
spec:
terminationGracePeriodSeconds: 120 # 2 phút để graceful shutdown
containers:
- name: my-app
lifecycle:
preStop:
exec:
command: ["/bin/sh", "-c", "sleep 15"] # Đợi load balancer drainInteraction 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 timeoutKhô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 burstGiải pháp với preStop hook:
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 shutdownHoặc cho web servers với graceful drain endpoint:
lifecycle:
preStop:
httpGet:
path: /healthz/ready # Endpoint return 503 để báo LB không route vào đây nữa
port: 8080Thự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ướcChecklist 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 handlingUpgrade 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:
- Thấy pod eviction trong logs (quá muộn)
- Kiểm tra cluster version thủ công (tedious)
- 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
# 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_IDPhân tích các loại notification
UpgradeAvailableEvent — Version mới available cho channel:
{
"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:
{
"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:
{
"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:
# 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
passMonitor upgrade với Cloud Monitoring
Ngoài Pub/Sub, monitor metrics trong quá trình upgrade:
# 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_countAlert policy khuyến nghị cho upgrade window:
# 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útCommon mistakes khi chuẩn bị cho upgrade
1. PDB tham chiếu sai selector
# 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
# App cần 2 phút để flush data khi shutdown
terminationGracePeriodSeconds: 30 # 30 giây không đủ → data lossMeasure actual shutdown time trong staging với kubectl drain --dry-run.
3. preStop hook quá dài
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 shutdownLuô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:
# 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.shReferences
- PodDisruptionBudget concepts — Kubernetes PDB documentation
- Configure PDB — PDB configuration guide
- pod-deletion-cost annotation — Deletion cost annotation
- Cluster Autoscaler safe-to-evict — Autoscaler eviction annotations
- Container lifecycle hooks — preStop hook mechanics
- GKE cluster notifications — Pub/Sub notification setup