Kubernetes Zero-Downtime Deployments: دليل عملي لعام
٩ يونيو ٢٠٢٦
يحتاج نشر Kubernetes بدون وقت توقف (zero-downtime) إلى أربعة أشياء تعمل معًا: مسبار جاهزية (readiness probe) بحيث تصل حركة المرور فقط إلى الـ pods الجاهزة بالفعل، وتحديث متدرج (rolling update) مع maxUnavailable: 0، وخطاف preStop يستمر لفترة أطول من انتشار نقاط النهاية (endpoint propagation)، وتطبيق يقوم بتصريف الطلبات الجارية عند استقبال SIGTERM. يبني هذا الدليل الأربعة جميعًا على عنقود (cluster) حقيقي.
ملخص
ستقوم ببناء خدمة Node.js صغيرة، وتشغيلها على عنقود kind محلي، وإثبات — من خلال اختبار حمل مباشر — أن الـ Deployment الساذج يُسقط الطلبات أثناء عملية التحديث. ثم ستقوم بإصلاحه خطوة بخطوة: مسبارات الجاهزية تتحكم في حركة المرور أثناء الصعود، و maxUnavailable: 0 يحافظ على السعة أثناء التحديث، ونوم preStop أصلي بالإضافة إلى تصريف SIGTERM السلس يغلق نافذة فقدان الاتصال أثناء الهبوط. خصص حوالي 30 دقيقة. كل إصدار هنا مثبت على الإصدارات الحالية — Kubernetes 1.36، و kind v0.32.0، و Node 24 LTS.
ما ستتعلمه
- لماذا تُسقط تحديثات Kubernetes المتدرجة الاتصالات حتى عندما يتعامل تطبيقك مع
SIGTERMبشكل مثالي - كيفية كتابة خادم HTTP بـ Node.js يقوم بتصريف الطلبات الجارية ويخرج بنظافة
- كيف تتحكم مسبارات الجاهزية في حركة المرور إلى الـ pods عند زيادة النطاق (ولماذا يختلف مسبار الحيوية liveness)
- كيفية ضبط
maxUnavailableوmaxSurgeلتحديث متدرج حقيقي بدون وقت توقف - كيف يقوم إجراء النوم
preStop(المتوفر بشكل عام في Kubernetes 1.34) بسد فجوة السباق في إزالة نقاط النهاية - كيف يتفاعل
terminationGracePeriodSecondsمعpreStopومنطق التصريف الخاص بك - كيف يحميك PodDisruptionBudget أثناء تفريغ العقد (node drains)
المتطلبات الأساسية
ثبّت هذه الإصدارات لكي تعمل الأوامر أدناه تمامًا كما هي مكتوبة:
- تشغيل Docker (أو Podman) محليًا — يقوم kind ببناء "آلة" العقدة الخاصة به كحاوية.
- kind v0.32.0، والذي يأتي افتراضيًا مع Kubernetes 1.36.1.1 تعمل إصدارات kind الأقدم أيضًا؛ إجراء النوم
preStopالأصلي المستخدم في الخطوة 7 متوفر بشكل عام في Kubernetes 1.34 ويتم شحنه مفعلاً افتراضيًا منذ 1.30 (بيتا).2 - kubectl متوافق مع العنقود الخاص بك (عميل 1.35 أو 1.36 مناسب للعمل مع خادم 1.36).
- Node.js 24 LTS (24.16.0 وقت كتابة هذا التقرير) فقط إذا كنت تريد تشغيل التطبيق خارج الحاوية أولاً.3
تحقق من أدواتك:
kind version # expect v0.32.0
kubectl version --client
Docker info >/dev/null && echo "Docker is running"
جديد على المنصة؟ يغطي دليل أساسيات Kubernetes الخاص بنا الـ Pods والـ Deployments والـ Services قبل أن تتعمق في أوضاع الفشل أدناه.
الخطوة 1 — خدمة Node تغلق بسلاسة
أهم قاعدة مفردة للإغلاق السلس: عندما تتلقى عمليتك SIGTERM، توقف عن قبول اتصالات جديدة، واترك الطلبات الجارية تنتهي، ثم اخرج. سلوك Node الافتراضي عند استقبال SIGTERM هو الموت فورًا، مما يقطع كل اتصال مفتوح — لذا عليك التعامل مع الإشارة بنفسك.
أنشئ src/server.js:
// src/server.js — minimal, zero-dependency HTTP server with graceful shutdown.
// Targets Node 24 LTS. On SIGTERM it stops accepting new connections,
// drains in-flight requests, then exits 0.
import http from 'node:http';
const PORT = Number(process.env.PORT ?? 8080);
const WORK_MS = Number(process.env.WORK_MS ?? 500);
const READY_DELAY_MS = Number(process.env.READY_DELAY_MS ?? 0);
const DRAIN_TIMEOUT_MS = Number(process.env.DRAIN_TIMEOUT_MS ?? 25_000);
const log = (msg, extra = {}) =>
console.log(JSON.stringify({ ts: new Date().toISOString(), msg, ...extra }));
const server = http.createServer((req, res) => {
if (req.url === '/healthz') {
res.writeHead(200, { 'content-type': 'text/plain' });
res.end('ok\n');
return;
}
// Simulate real work so draining is observable.
setTimeout(() => {
res.writeHead(200, { 'content-type': 'text/plain' });
res.end(`handled by ${process.pid\n`);
}, WORK_MS);
});
// Simulate warm-up (config load, DB pool, cache prime) before we start listening.
setTimeout(() => {
server.listen(PORT, () => log('listening', { port: PORT, pid: process.pid }));
}, READY_DELAY_MS);
let shuttingDown = false;
function shutdown(signal) {
if (shuttingDown) return;
shuttingDown = true;
log('signal received, draining', { signal });
// Stop accepting new connections; the callback runs once all
// in-flight requests have completed.
server.close(() => {
log('drain complete, exiting', { code: 0 });
process.exit(0);
});
// Close idle keep-alive sockets (server.close() also does this on Node 18.19+).
server.closeIdleConnections();
// Safety net: never hang past the grace period.
setTimeout(() => {
log('drain timeout, forcing exit', { code: 1 });
process.exit(1);
}, DRAIN_TIMEOUT_MS).unref();
}
process.on('SIGTERM', () => shutdown('SIGTERM'));
process.on('SIGINT', () => shutdown('SIGINT'));
هناك تفصيلان يقع فيهما الناس عادةً. أولاً، server.close() يتوقف عن قبول اتصالات جديدة وينتظر انتهاء الطلبات النشطة؛ منذ إصدار Node 18.19، يقوم أيضًا بإغلاق اتصالات keep-alive الخاملة تلقائيًا، لذا فإن استدعاء server.closeIdleConnections() الصريح هنا يهدف بشكل أساسي لتوثيق النية ويظل آمنًا في بيئات التشغيل الأحدث.4 ثانيًا، يقوم مقبض READY_DELAY_MS بمحاكاة بدء تشغيل بطيء؛ سنستخدمه لجعل فشل مسبار الجاهزية واضحًا.
تحقق من سلوك التصريف محليًا قبل وضعه في حاوية. ابدأ الخادم بتأخير طويل لكل طلب، أرسل طلبًا، ثم أرسل SIGTERM بينما الطلب لا يزال جاريًا:
WORK_MS=1500 node src/server.js & # prints {"msg":"listening",...}
SRV=$!
curl -s -w 'in-flight: HTTP %{http_code} in %{time_total}s\n' http://localhost:8080/ &
sleep 0.4
kill -TERM $SRV # terminate mid-request
النتيجة المتوقعة — الطلب الجاري ينتهي بـ 200 على الرغم من أن الخادم تلقى SIGTERM في منتصف الطلب، وتخرج العملية بنظافة:
{"ts":"...","msg":"listening","port":8080,"pid":8}
{"ts":"...","msg":"signal received, draining","signal":"SIGTERM"}
in-flight: HTTP 200 in 1.506991s
{"ts":"...","msg":"drain complete, exiting","code":0}
أي طلب جديد يبدأ بعد SIGTERM يتم رفضه (المستمع مغلق) — وهذا بالضبط ما نريده. التطبيق صحيح. الآن شاهد Kubernetes وهو يُسقط الطلبات على أي حال.
الخطوة 2 — وضعه في حاوية وتحميله في kind
أنشئ Dockerfile بجانب src/:
# syntax=Docker/dockerfile:1
FROM node:24-slim
ENV NODE_ENV=production
WORKDIR /app
COPY src/ ./src/
USER node
EXPOSE 8080
CMD ["node", "src/server.js"]
ابنِ الصورة وشغّل العنقود. يقوم kind بتشغيل كل عقدة كحاوية، لذا ستقوم بتحميل الصورة مباشرة في العنقود بدلاً من دفعها إلى سجل (registry):
Docker build -t zdt-demo:v1 .
kind create cluster --name zdt
kind load Docker-image zdt-demo:v1 --name zdt
يقوم kind load Docker-image بنسخ صورتك المحلية إلى كل عقدة بحيث يجدها imagePullPolicy: IfNotPresent بدون الحاجة لسجل. تأكد من أن العنقود يعمل:
kubectl get nodes # one control-plane node, STATUS Ready
الخطوة 3 — النشر بالطريقة الساذجة (ومشاهدة سقوط الطلبات)
إليك Deployment بدون أي من آليات عدم التوقف — لا يوجد مسبار جاهزية، استراتيجية تحديث افتراضية، ومحاكاة لبدء تشغيل يستغرق 5 ثوانٍ يحاكي تطبيقًا حقيقيًا يقوم بتحميل الإعدادات وتهيئة مجمع الاتصالات. أنشئ k8s/deployment-naive.yaml:
apiVersion: apps/v1
kind: Deployment
metadata:
name: web
spec:
replicas: 3
selector:
matchLabels:
app: web
template:
metadata:
labels:
app: web
spec:
containers:
- name: web
image: zdt-demo:v1
imagePullPolicy: IfNotPresent
ports:
- containerPort: 8080
env:
- name: READY_DELAY_MS
value: "5000" # 5s warm-up before the server listens
- name: WORK_MS
value: "500"
وخدمة (Service) لموازنة الحمل عبر الـ pods (k8s/service.yaml):
apiVersion: v1
kind: Service
metadata:
name: web
spec:
selector:
app: web
ports:
- name: http
port: 80
targetPort: 8080
قم بتطبيق كليهما وانتظر حتى تستقر الـ pods:
kubectl apply -f k8s/service.yaml
kubectl apply -f k8s/deployment-naive.yaml
kubectl rollout status deployment/web
المشكلة غير مرئية في حالة السكون — ثلاث pods، جميعها تعمل. تظهر فقط أثناء التحديث المتدرج، وهو بالضبط الوقت الذي يراقب فيه مستخدموك.
الخطوة 4 — قياس وقت التوقف
لا يمكنك إصلاح ما لا يمكنك رؤيته، لذا قم بتشغيل حلقة حمل مستمرة من داخل العنقود ضد الخدمة، ثم ابدأ عملية التحديث. تقوم pod busybox داخل العنقود بإرسال طلب تلو الآخر لمدة 60 ثانية وتحسب النجاحات والإخفاقات:
kubectl run probe --image=busybox:1.37 --restart=Never --rm -i -- \
sh -c 'ok=0; fail=0; end=$(( $(date +%s) + 60 ));
while [ $(date +%s) -lt $end ]; do
if wget -q -T 2 -O /dev/null http://web/; then ok=$((ok+1));
else fail=$((fail+1)); echo "drop at $(date +%T)"; fi
done; echo "RESULT ok=$ok fail=$fail"'
بينما تعمل هذه الحلقة، افتح نافذة أوامر ثانية وافرض إعادة تشغيل (هذا يعيد إنشاء جميع الـ pods الثلاثة، تمامًا كما يفعل شحن صورة جديدة):
kubectl rollout restart deployment/web
عندما تنتهي الحلقة، سترى عددًا غير صفري في fail، بالإضافة إلى سطر drop at ... لكل طلب فاشل. يحدث كل سقوط في النافذة التي تقوم فيها الخدمة بتوجيه طلب إلى pod جديد تمامًا لا تزال عملية Node الخاصة به في فترة بدء التشغيل التي تبلغ 5 ثوانٍ ولم تستمع بعد على المنفذ 8080. بدون مسبار جاهزية، يعتبر Kubernetes أن الـ pod "جاهز" بمجرد بدء تشغيل الحاوية، لذا ينضم الـ pod إلى نقاط نهاية الخدمة قبل أن يتمكن التطبيق فعليًا من الخدمة.5 عند ضرب الخدمة مباشرة بهذا الشكل، يكون كل فشل عبارة عن رفض للاتصال؛ مع وجود ingress أو موازن حمل في المقدمة، تظهر نفس الفجوة عادةً كخطأ 502. دعنا نغلقها.
الخطوة 5 — مسبارات الجاهزية: إرسال حركة المرور فقط إلى الـ pods الجاهزة
يخبر مسبار الجاهزية Kubernetes ما إذا كان يجب أن يتلقى الـ pod حركة مرور أم لا. عندما يفشل، يتم إزالة الـ pod من نقاط نهاية الخدمة ولكنه يستمر في العمل (لا يتم إعادة تشغيله). مسبار الحيوية (liveness probe) مختلف: عندما يفشل هو، يقوم kubelet بإعادة تشغيل الحاوية. الخلط بين الاثنين هو وسيلة كلاسيكية للتسبب في انقطاع الخدمة، لذا اجعل مسبار الحيوية متساهلاً واترك مسبار الجاهزية يقوم بمهمة التحكم في حركة المرور.5
أنشئ بيان الإنتاج k8s/deployment.yaml (سنستمر في الإضافة إليه حتى الخطوة 7):
apiVersion: apps/v1
kind: Deployment
metadata:
name: web
spec:
replicas: 3
selector:
matchLabels:
app: web
template:
metadata:
labels:
app: web
spec:
containers:
- name: web
image: zdt-demo:v1
imagePullPolicy: IfNotPresent
ports:
- containerPort: 8080
env:
- name: READY_DELAY_MS
value: "5000"
- name: WORK_MS
value: "500"
readinessProbe:
httpGet:
path: /healthz
port: 8080
initialDelaySeconds: 0
periodSeconds: 2
failureThreshold: 3
livenessProbe:
httpGet:
path: /healthz
port: 8080
initialDelaySeconds: 10
periodSeconds: 10
failureThreshold: 3
خلال فترة الإحماء التي تستغرق 5 ثوانٍ، لا يكون الخادم في حالة استماع، لذا فإن فحص الجاهزية httpGet إلى /healthz يتلقى رفضاً للاتصال ويفشل. يظل الـ pod خارج الـ Service حتى ينجح الفحص، وبعد ذلك يتدفق الزيارات. يبدأ فحص الحيوية (liveness probe) لاحقاً (initialDelaySeconds: 10) ويقوم بالفحص ببطء، لذا لا يتعطل أبداً خلال عملية الإحماء العادية.
قم بتطبيقه وأعد تشغيل حلقة التحميل من الخطوة 4 أثناء تنفيذ kubectl rollout restart. سيختفي عدد حالات الفشل الناتجة عن إحماء الـ pod الجديد. ولكن لا تزال هناك فجوة في جانب الإيقاف، وسؤال أدق حول السعة أثناء التحديث. خطوتان إضافيتان.
الخطوة 6 — التحديث دون خفض السعة (maxUnavailable و maxSurge)
يقوم التحديث التدريجي (rolling update) باستبدال الـ pods في مجموعات. هناك خياران يتحكمان في ذلك: maxUnavailable (كم عدد الـ pods التي يمكن أن تكون متوقفة عن العمل تحت العدد المطلوب) و maxSurge (كم عدد الـ pods الإضافية التي يمكن إنشاؤها فوق العدد المطلوب). القيم الافتراضية هي 25% لكل منهما.6 مع نسبة 25%، يتم تقريب maxUnavailable للأقل ويتم تقريب maxSurge للأعلى، لذا يعتمد السلوك الدقيق على عدد النسخ المتماثلة (replicas) لديك — وهذا هو بالضبط السبب في وجوب تعيينهما صراحةً بدلاً من الاعتماد على التقريب.
لضمان عدم التوقف عن العمل، اضبط maxUnavailable: 0 حتى لا ينخفض التحديث أبداً عن العدد الجاهز المطلوب، و maxSurge: 1 بحيث يضيف pod جديداً قبل إيقاف القديم. أضف كتلة strategy ووسادة minReadySeconds إلى أعلى الـ spec في ملف k8s/deployment.yaml:
spec:
replicas: 3
minReadySeconds: 5
strategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 0
maxSurge: 1
selector:
matchLabels:
app: web
# ... template unchanged ...
تخبر minReadySeconds: 5 الـ Deployment أن الـ pod الجديد يجب أن يظل جاهزاً لمدة 5 ثوانٍ قبل أن يُحسب ضمن التوافر ويتم إزالة الـ pod القديم التالي — وهي حماية بسيطة ضد الـ pods التي تصبح جاهزة ثم تنهار فوراً. مع maxUnavailable: 0، الطريقة الوحيدة للتحديث هي إنشاء pod جاهز جديد أولاً، لذا لا تنخفض السعة المقدمة أبداً.
الخطوة 7 — إغلاق سباق الإيقاف باستخدام preStop hook
هذا هو الجزء الذي يسهل الوقوع في الخطأ فيه. تطبيقك يقوم بالفعل بتصريف الطلبات عند تلقي SIGTERM، فلماذا قد يؤدي إنهاء pod إلى فقدان طلب؟
لأن شيئين يحدثان في نفس الوقت عند حذف pod (أو استبداله بتحديث):
- يبدأ الـ kubelet في إنهاء الـ pod: يبدأ مؤقت
terminationGracePeriodSeconds، ويقوم kubelet بتشغيلpreStophook الخاص بك، ثم يرسلSIGTERMإلى الحاوية، وأخيراً يرسلSIGKILLإذا كان الـ pod لا يزال حياً عند نفاد تلك الميزانية الزمنية المشتركة.7 - يقوم الـ control plane بإزالة الـ pod من EndpointSlice الخاص بالـ Service، ثم يقوم kube-proxy ومتحكم الـ ingress بمزامنة هذا التغيير في جداول التوجيه الخاصة بهم.
المسار رقم 2 متسق في النهاية (eventually consistent) ويعمل بالتوازي مع المسار رقم 1. لذا لفترة قصيرة، يمكن للعقدة (node) أن تظل توجه طلباً جديداً إلى pod تلقى بالفعل SIGTERM وأغلق مستمعه — ويحصل العميل على رفض للاتصال.8 التطبيق الذي يصرف الطلبات بشكل مثالي لا يمكنه المساعدة هنا، لأن المشكلة هي أن الزيارات لا تزال تُرسل إليه.
الحل هو preStop hook يقوم ببساطة بالنوم (sleep). فهو يؤخر SIGTERM لفترة كافية لانتشار إزالة نقطة النهاية (endpoint)، ولأن preStop يكتمل قبل إرسال SIGTERM، تظل الحاوية حية وتخدم الطلبات أثناء النوم.7 قامت Kubernetes 1.34 بترقية إجراء sleep الأصلي إلى GA، لذا لم تعد بحاجة إلى ملف sleep ثنائي في صورتك — فهو يعمل حتى على الصور التي لا تحتوي على نظام تشغيل (distroless).2
أضف كتلة lifecycle وفترة سماح إلى مواصفات الحاوية في k8s/deployment.yaml:
spec:
template:
spec:
terminationGracePeriodSeconds: 45
containers:
- name: web
# ... image, env, probes unchanged ...
lifecycle:
preStop:
sleep:
seconds: 15
resources:
requests:
cpu: 25m
memory: 32Mi
limits:
cpu: 100m
memory: 64Mi
تحديد الحجم مهم. تغطي terminationGracePeriodSeconds عملية الإنهاء بأكملها — الـ preStop hook بالإضافة إلى تصريف تطبيقك. يبدأ مؤقت السماح عند بدء الإنهاء، لذا يجب أن يتجاوز وقت نوم preStop بالإضافة إلى أطول طلب متوقع. هنا: 15 ثانية نوم + طلب لمدة 0.5 ثانية + هامش إضافي، وهو ما يقع ضمن 45 ثانية. التوجيه الرسمي صريح: إذا استغرق الـ hook الخاص بك 55 ثانية واحتاجت الحاوية إلى 10 ثوانٍ أخرى للتوقف، فإن terminationGracePeriodSeconds أقل من 65 سيؤدي إلى إرسال SIGKILL للحاوية قبل أن تنتهي.7
في المجموعات الأقدم من 1.30 (أو مع تعطيل بوابة
PodLifecycleSleepAction)، استبدل الإجراء الأصلي بالشكل القابل للنقل، والذي يحتاج إلى shell وsleepفي الصورة:preStop: { exec: { command: ["/bin/sh","-c","sleep 15"] } }.
إليك ملف k8s/deployment.yaml الكامل بعد الخطوات 5-7، جاهز للنسخ والتطبيق:
apiVersion: apps/v1
kind: Deployment
metadata:
name: web
spec:
replicas: 3
minReadySeconds: 5
strategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 0
maxSurge: 1
selector:
matchLabels:
app: web
template:
metadata:
labels:
app: web
spec:
terminationGracePeriodSeconds: 45
containers:
- name: web
image: zdt-demo:v1
imagePullPolicy: IfNotPresent
ports:
- containerPort: 8080
env:
- name: READY_DELAY_MS
value: "5000"
- name: WORK_MS
value: "500"
readinessProbe:
httpGet:
path: /healthz
port: 8080
initialDelaySeconds: 0
periodSeconds: 2
failureThreshold: 3
livenessProbe:
httpGet:
path: /healthz
port: 8080
initialDelaySeconds: 10
periodSeconds: 10
failureThreshold: 3
lifecycle:
preStop:
sleep:
seconds: 15
resources:
requests:
cpu: 25m
memory: 32Mi
limits:
cpu: 100m
memory: 64Mi
طبق البيان النهائي:
kubectl apply -f k8s/deployment.yaml
kubectl rollout status deployment/web
الخطوة 8 — النجاة من إفراغ العقد باستخدام PodDisruptionBudget
تغطي استراتيجية التحديث التدريجي من الخطوة 6 عمليات النشر التي تطلقها أنت. لكن إفراغ العقد (node drains) من أجل الترقيات أو القياس التلقائي (autoscaling) هو اضطراب منفصل طوعي — ويمكن لعملية إفراغ واحدة أن تحاول طرد العديد من الـ pods الخاصة بك في وقت واحد. يحدد PodDisruptionBudget (PDB) حداً أقصى لعدد الـ pods التي يمكن طردها طوعاً في المرة الواحدة، مما يجبر kubectl drain والقياس التلقائي للمجموعة على الانتظار بدلاً من خفض العدد عن الحد الآمن.9
أنشئ ملف k8s/pdb.yaml:
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: web
spec:
minAvailable: 2
selector:
matchLabels:
app: web
kubectl apply -f k8s/pdb.yaml
kubectl get pdb web # ALLOWED DISRUPTIONS should be 1
مع وجود ثلاث نسخ متماثلة و minAvailable: 2، يمكن طرد pod واحد فقط في كل مرة أثناء عملية الإفراغ. يقيد PDB الاضطرابات الطوعية فقط من خلال Eviction API: لا يمكنه إيقاف الاضطرابات غير الطوعية مثل تعطل العقدة (على الرغم من أنها لا تزال تُحسب ضمن الميزانية)، وتخضع التحديثات التدريجية للـ Deployment لاستراتيجية maxUnavailable/maxSurge الخاصة بك، وليس الـ PDB.9
التحقق: إثبات عدم التوقف عن العمل
قم بتشغيل نفس حلقة التحميل من الخطوة 4 مرة أخرى، وقم بتشغيل التحديث في نافذة terminal ثانية:
# terminal 1
kubectl run probe --image=busybox:1.37 --restart=Never --rm -i -- \
sh -c 'ok=0; fail=0; end=$(( $(date +%s) + 60 ));
while [ $(date +%s) -lt $end ]; do
if wget -q -T 2 -O /dev/null http://web/; then ok=$((ok+1));
else fail=$((fail+1)); echo "drop at $(date +%T)"; fi
done; echo "RESULT ok=$ok fail=$fail"'
# terminal 2
kubectl rollout restart deployment/web
هذه المرة يجب أن تكون النتيجة fail=0. تتلقى الـ pods الجديدة الزيارات فقط بعد إحمائها (الجاهزية)، ولا تنخفض السعة أبداً (maxUnavailable: 0/maxSurge: 1)، وتستمر الـ pods التي يتم إنهاؤها في الخدمة حتى يتم تحديث نقاط النهاية (نوم preStop) ثم تصريف العمل الجاري (إيقاف SIGTERM السلس). لاختبار بدقة أعلى، استبدل حلقة busybox المتسلسلة بأداة متزامنة مثل fortio أو hey وشاهد الرسم البياني لأكواد الاستجابة يظل 100% 200.
أمران يستحقان المعرفة لأي تحديث:
kubectl rollout status deployment/web # watch a roll complete
kubectl rollout undo deployment/web # roll back to the previous ReplicaSet
عند الانتهاء، قم بحذف المجموعة: kind delete cluster --name zdt.
الأخطاء الشائعة
التحديث يتوقف عند "waiting for deployment to finish." مع maxUnavailable: 0 و maxSurge: 1، يجب أن يصبح الـ pod الجديد جاهزاً قبل الخطوة التالية. إذا لم ينجح فحص الجاهزية أبداً (مسار خاطئ، منفذ خاطئ، تعطل التطبيق)، فسيتوقف التحديث للأبد حسب التصميم. تحقق من kubectl describe pod <new-pod> و kubectl logs <new-pod>.
لا تزال الطلبات تسقط عند الإغلاق. فترة السكون في preStop الخاصة بك أقصر من الوقت الذي يحتاجه ingress/kube-proxy لإيقاف التوجيه. قم بزيادة فترة السكون (10-20 ثانية أمر شائع) وتأكد من أن terminationGracePeriodSeconds أكبر من فترة السكون بالإضافة إلى أطول طلب لديك. تأكد أيضًا من أن التطبيق يتعامل بالفعل مع SIGTERM — فالعملية التي تتجاهله سيتم إنهاؤها عبر SIGKILL في نهاية فترة السماح، مما يؤدي إلى إسقاط كل الطلبات الجارية.
error: lifecycle.preStop.sleep ... unknown field. إجراء sleep الأصلي متاح بشكل عام (GA) في الإصدار 1.34 ومفعل افتراضيًا منذ الإصدار 1.30 (بيتا)؛ فقط المجموعات (clusters) الأقدم من ذلك — أو تلك التي تم تعطيل بوابة الميزات (feature gate) فيها — ترفض هذا الحقل. استخدم صيغة exec/sleep الموضحة في الخطوة 7، أو قم بالترقية.2
فحص الحيوية (Liveness probe) يعيد تشغيل الـ pods أثناء الضغط. فحص الحيوية الموجه إلى نقطة نهاية (endpoint) بطيئة أو تعتمد بشكل كبير على تبعيات أخرى سيفشل تحت الضغط ويعيد تشغيل pods سليمة، مما يفاقم المشكلة. وجه فحص الحيوية إلى فحص محلي بسيط (مثل /healthz هنا)، واجعل initialDelaySeconds كافية، واترك فحص الجاهزية (readiness) — وليس الحيوية — يتعامل مع حالات "الانشغال المؤقت".
kind load يقول إن الصورة غير موجودة. إما أنك قمت ببناء وسم (tag) مختلف عما يشير إليه ملف الـ manifest أو قمت بالتحميل في المجموعة (cluster) الخطأ. طابق Docker build -t zdt-demo:v1 مع image: في الـ manifest ومرر نفس الـ --name إلى kind load.
الخطوات التالية ومزيد من القراءة
لديك الآن Deployment يقوم بإصدار نسخ جديدة دون إسقاط طلب واحد. من هنا:
- ضع الخدمة (Service) خلف ingress حقيقي باستخدام Kubernetes Gateway API وتأكد من نفس سلوك عدم التوقف (zero-downtime) من البداية للنهاية.
- قبل الإنتاج، راجع قائمة أفضل ممارسات الأمان في Kubernetes — الحاويات التي لا تعمل بصلاحيات root (تم تنفيذ ذلك هنا بالفعل عبر
USER node)، وحدود الموارد (resource limits)، وسياسة الشبكة (network policy). - أضف التسليم التدريجي (progressive delivery) مثل (canary أو blue-green) فوق أساس التحديث المتداول (rolling-update) هذا بمجرد حصولك على مقاييس (metrics) لاتخاذ القرار.
Footnotes
-
kind v0.32.0 release notes (default node image
kindest/node:v1.36.1, published 2026-06-02): https://GitHub.com/Kubernetes-sigs/kind/releases/tag/v0.32.0 ↩ -
KEP-3960 "Pod lifecycle sleep action" (status: implemented; stable: v1.34): https://GitHub.com/Kubernetes/enhancements/blob/master/keps/sig-node/3960-pod-lifecycle-sleep-action/README.md and the v1.33 container-lifecycle update: https://Kubernetes.io/blog/2025/05/14/Kubernetes-v1-33-updates-to-container-lifecycle/ ↩ ↩2 ↩3
-
Node.js releases (24 LTS): https://nodejs.org/en/about/previous-releases ↩
-
Node.js HTTP
server.close()andserver.closeIdleConnections(): https://nodejs.org/API/http.html#servercloseidleconnections ↩ -
Kubernetes — Configure Liveness, Readiness and Startup Probes: https://Kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/ ↩ ↩2
-
Kubernetes — Deployments (rolling update
maxUnavailable/maxSurgedefaults are 25%): https://Kubernetes.io/docs/concepts/workloads/controllers/deployment/ ↩ -
Kubernetes — Container Lifecycle Hooks (preStop runs before SIGTERM; grace period covers hook + stop): https://Kubernetes.io/docs/concepts/containers/container-lifecycle-hooks/ ↩ ↩2 ↩3
-
Kubernetes — Pod Lifecycle, Termination of Pods (endpoint removal happens alongside the grace period): https://Kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle/#pod-termination ↩
-
Kubernetes — Specifying a Disruption Budget for your Application: https://Kubernetes.io/docs/tasks/run-application/configure-pdb/ ↩ ↩2