أساسيات Kubernetes

الخدمات والاكتشاف

18 دقيقة الدرس 6 من 32

الخدمات والاكتشاف

الـ Pods مؤقتة بطبيعتها. يُزيل التحديث المتدحرج (rolling update) الـ Pods القديمة ويُنشئ جديدة بعناوين IP مختلفة تماماً. تُستبدَل حاوية في حلقة تعطّل (crash-loop) خلال ثوانٍ. إن كان كود العميل يُضمّن عنوان IP لـ Pod مباشرةً، فإنه يتعطل فور أن تُعيد Kubernetes جدولة ذلك الـ Pod. تحلّ الخدمات (Services) هذه المشكلة بتوفير عنوان IP افتراضي مستقر (ClusterIP) واسم DNS يوجّه دائماً إلى نقاط النهاية السليمة — بصرف النظر عن عدد الـ Pods أو مكان تشغيلها. يستعرض هذا الدرس كل نوع من أنواع الخدمات، وكيف يُبرمج kube-proxy طبقة البيانات، وكيف يجعل kube-dns الأسماء قابلة للحل داخل الكلاستر.

كائن Endpoint — حلقة الوصل المفقودة

قبل استكشاف أنواع الخدمات، افهم السباكة الداخلية. في كل مرة تُنشئ فيها خدمة، يراقب متحكم Endpoints (جزء من kube-controller-manager) الـ Pods التي تتطابق تسمياتها مع selector الخدمة ويكتب عناوين IP الخاصة بها في كائن Endpoints بالاسم ذاته للخدمة. عند توقف Pod عن الاستعداد أو موتها، يُزال عنوان IP تلقائياً من قائمة Endpoints.

# فحص نقاط النهاية التي تدعم خدمة اسمها "api" kubectl get endpoints api -o yaml # مراقبة تحديثات نقاط النهاية في الوقت الفعلي أثناء نشر متدحرج kubectl get endpoints api -w # مثال على المخرجات — كل "address" هو عنوان IP لـ Pod حي وجاهز # addresses: # - ip: 10.244.1.23 # - ip: 10.244.2.47 # notReadyAddresses: # - ip: 10.244.1.25 <-- فشل readinessProbe، مُستبعد من الحركة المرورية
EndpointSlices استبدلت الـ Endpoints API القديمة عند النطاق الواسع. في الكلاسترات التي تتجاوز ~100 نقطة نهاية لكل خدمة، يتحوّل kube-proxy والمستهلكون الآخرون تلقائياً إلى كائنات EndpointSlice. المفهوم ذاته — مُجزّأ فقط إلى أجزاء أصغر لتقليل أحداث المراقبة عند نطاق 10,000+ Pod.

ClusterIP — عنوان IP افتراضي داخلي

ClusterIP هو النوع الافتراضي للخدمة. يُخصّص Kubernetes عنوان IP افتراضياً من النطاق --service-cluster-ip-range (عادةً 10.96.0.0/12). هذا العنوان الافتراضي غير قابل للتوجيه خارج الكلاستر؛ لا وجود له إلا في قواعد iptables (أو جداول IPVS) التي يُبرمجها kube-proxy على كل عقدة. يمكن لأي Pod في الكلاستر الوصول إلى الخدمة عبر ClusterIP أو اسمها في DNS.

# بيان ClusterIP أدنى حد — يكشف المنفذ 80 لـ Deployment الخاص بـ nginx apiVersion: v1 kind: Service metadata: name: nginx-svc namespace: production spec: type: ClusterIP # افتراضي؛ يمكن حذفه selector: app: nginx ports: - name: http port: 80 # المنفذ الذي يتصل به العملاء targetPort: 8080 # المنفذ الذي تستمع عليه الحاوية protocol: TCP

يراقب kube-proxy الخدمات ونقاط النهاية ويكتب قواعد iptables من نوع DNAT: الحركة المرورية إلى 10.96.x.x:80 تُوزَّع عشوائياً (DNAT) إلى أحد عناوين IP الـ Pod الحية المُدرجة في Endpoints. في وضع IPVS (المفضّل عند النطاق الواسع) يحدث توازن الحمل ذاته داخل وحدة IPVS في نواة النظام مع خوارزميات جدولة أغنى (round-robin، least-connection، إلخ).

فعّل وضع IPVS لـ kube-proxy (mode: ipvs في KubeProxyConfiguration) عند وجود أكثر من ~1000 خدمة. قواعد iptables تتسع بمعدل O(n) — كل قاعدة جديدة تُضاف إلى سلسلة متنامية. يستخدم IPVS جدول تجزئة ويتوسّع إلى عشرات الآلاف من الخدمات بزمن استجابة ثابت.

kube-dns — حل الأسماء داخل الكلاستر

يعمل CoreDNS (خلف kube-dns) كـ Deployment في مساحة الأسماء kube-system ومكشوف عبر خدمة ClusterIP خاصة به على العنوان الموجود في /etc/resolv.conf على كل Pod (عادةً 10.96.0.10). يُنتج مكوّن kubernetes في CoreDNS سجلات DNS من Kubernetes API:

  • خدمة باسم nginx-svc في مساحة الأسماء production تُحلَّل إلى ClusterIP الخاص بها عبر: nginx-svc.production.svc.cluster.local
  • من داخل مساحة الأسماء ذاتها، تعمل الأسماء المختصرة: nginx-svc أو nginx-svc.production.
  • تحصل عناوين IP للـ Pod الفردية على سجلات من الشكل 10-244-1-23.production.pod.cluster.local — نادراً ما تُستخدم مباشرةً.
Kubernetes Service Routing via ClusterIP and kube-dns Client Pod curl nginx-svc:80 1. DNS query CoreDNS 10.96.0.10:53 returns ClusterIP API Server Service registry Service: nginx-svc ClusterIP 10.96.4.22:80 2. connect kube-proxy DNAT (iptables/IPVS) 3. random backend Pod nginx Pod A 10.244.1.23 nginx Pod B 10.244.2.47 nginx Pod C 10.244.3.11 NodePort / LB External traffic enters here ext. ingress
مسار توجيه الخدمة: يُحلّل CoreDNS اسم الخدمة إلى ClusterIP الخاص بها، ثم تُعيد قواعد DNAT في kube-proxy توجيه الاتصال إلى Pod خلفي سليم.

NodePort — الكشف على كل العقد

يمتدّ NodePort فوق ClusterIP بفتح منفذ ثابت (النطاق الافتراضي 30000–32767) على كل عقدة في الكلاستر. الحركة المرورية الخارجية الوافدة إلى <any-node-ip>:<nodePort> يُعيد kube-proxy توجيهها إلى الخدمة ثم توزّع على Pod خلفي.

apiVersion: v1 kind: Service metadata: name: api-nodeport spec: type: NodePort selector: app: api ports: - port: 80 # منفذ ClusterIP (داخل الكلاستر) targetPort: 3000 # منفذ الحاوية nodePort: 31080 # منفذ ثابت على كل عقدة (احذفه للتعيين التلقائي)
NodePort ليس نمط دخول إنتاجياً. يكشف منفذاً عشوائياً عالياً، ويتطلب من العملاء معرفة عناوين IP للعقد (التي تتغيّر)، ويتجاوز موازنة الحمل فوق طبقة العقدة. استخدم NodePort فقط في بيئات الأجهزة المادية الخالية من متحكم LoadBalancer — وحتى في تلك الحالة، ضع موازن حمل خارجياً من طبقة L4 (HAProxy، keepalived) أمامه. في بيئات السحابة استخدم دائماً type: LoadBalancer أو متحكم Ingress.

LoadBalancer — الوصول الخارجي السحابي

LoadBalancer مجموعة عليا من NodePort. إضافةً إلى فتح NodePort على كل عقدة، يُرسل إشارةً إلى cloud-controller-manager لدى مزوّد السحابة لتوفير موازن حمل خارجي (AWS NLB، أو GCP Network LB، أو Azure LB) وتوجيهه إلى منافذ العقد. يُكتب عنوان IP أو اسم المضيف الخاص بموازن الحمل المُوفَّر في service.status.loadBalancer.ingress.

apiVersion: v1 kind: Service metadata: name: payments-lb annotations: service.beta.kubernetes.io/aws-load-balancer-type: "nlb" # AWS NLB (طبقة 4) service.beta.kubernetes.io/aws-load-balancer-cross-zone-load-balancing-enabled: "true" spec: type: LoadBalancer selector: app: payments ports: - port: 443 targetPort: 8443 protocol: TCP # بعد التطبيق، افحص عنوان IP أو اسم المضيف الخارجي المُعيَّن: # kubectl get svc payments-lb # NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) # payments-lb LoadBalancer 10.96.5.10 a1b2c3.elb.amazonaws.com 443:31443/TCP
كل خدمة من نوع LoadBalancer تُنشئ موازن حمل سحابياً واحداً — وهذا مكلف عند النطاق الواسع (لكل NLB تكلفة). عند نطاق Google أو Amazon، تكشف الفرق عشرات الخدمات الدقيقة عبر متحكم Ingress واحد مدعوم بموازن حمل واحد، وتوجّه بقواعد على أساس hostname/path بدلاً من إنشاء موازنات حمل لكل خدمة.

الخدمات Headless — DNS Round-Robin بدون VIP

اضبط clusterIP: None لإنشاء خدمة headless. لا يُخصَّص VIP. بدلاً من ذلك يُعيد CoreDNS عناوين IP للـ Pod الفردية مباشرةً في رد سجل DNS من نوع A. يتلقى العملاء سجلات A متعددة ويجب أن يختاروا بأنفسهم. هذا هو النمط المستخدم مع StatefulSets (قواعد بيانات، Kafka، Zookeeper) حيث لكل Pod هوية مستقرة ويحتاج العملاء للوصول إلى نسخة محددة:

apiVersion: v1 kind: Service metadata: name: postgres-headless spec: clusterIP: None # headless selector: app: postgres ports: - port: 5432 # بحث DNS يُعيد عناوين IP لكل الـ Pods مباشرةً: # nslookup postgres-headless.production.svc.cluster.local # Name: postgres-headless.production.svc.cluster.local # Address: 10.244.1.5 # Address: 10.244.2.9 # Address: 10.244.3.2

ExternalName — اسم بديل DNS للخدمات الخارجية

يُنشئ ExternalName اسماً بديلاً CNAME داخل الكلاستر لاسم DNS خارجي. لا يملك selector ولا نقاط نهاية — يُعيد CoreDNS ببساطة CNAME. يتيح لك ذلك الإشارة إلى مثيل RDS أو API قديم أو نقطة نهاية SaaS مُدارة بنفس أسلوب تسمية DNS في Kubernetes كأي خدمة داخلية، مما يُسهّل التبديل بين خلفية داخل الكلاستر وأخرى خارجية دون تغيير إعدادات التطبيق:

apiVersion: v1 kind: Service metadata: name: rds-postgres namespace: production spec: type: ExternalName externalName: mydb.cluster-abc.us-east-1.rds.amazonaws.com # يتصل التطبيق بـ: rds-postgres.production.svc.cluster.local:5432 # يُحلّل CoreDNS CNAME -> mydb.cluster-abc.us-east-1.rds.amazonaws.com

أوضاع الفشل الإنتاجية التي يجب معرفتها

  • نقاط نهاية قديمة بعد انهيار سريع. يُحدّث kube-proxy قواعد iptables بعد أن يُزيل متحكم Endpoints الـ Pod الميت — هناك نافزة قصيرة (عادةً أقل من ثانية) قد تُرسَل فيها الحركة المرورية إلى Pod منتهية. خفّف ذلك بـ hook من نوع preStop ينتظر 2–5 ثوانٍ وعتبة فشل ضيّقة لـ readinessProbe.
  • مهلة تخزين DNS مؤقتاً. يُخزّن JVM وبعض عملاء Go استجابات DNS لفترة أطول بكثير من مهلة 5 ثوانٍ التي يُعيدها CoreDNS. بعد تغيير نقطة نهاية الخدمة، قد يوجّه العملاء القدامى إلى عناوين IP قديمة لدقائق. اضبط الـ JVM بـ -Dsun.net.inetaddr.ttl=5 وتحقق من إعدادات مهلة DNS لدى العميل.
  • تأخر مزامنة iptables في kube-proxy. في كلاستر كبير جداً (10 آلاف+ خدمة) مع وضع iptables قد تستغرق المزامنة ثوانٍ. وضع IPVS يُلغي هذا. راقب sync_proxy_rules_duration_seconds في مقاييس kube-proxy.
  • استنفاد نطاق ClusterIP. يُعطي النطاق الافتراضي /12 حوالي مليون عنوان لكن بعض الكلاسترات تُخصّص بإفراط. افحص بـ kubectl cluster-info dump | grep service-cluster-ip-range.
فضّل دائماً targetPort مُسمّى (مثلاً targetPort: http يُشير إلى منفذ مُسمّى في مواصفات الـ Pod) على منفذ رقمي. عند تغيير منفذ الحاوية، يكفي تحديث اسم منفذ مواصفات الـ Pod وينتشر التغيير تلقائياً — دون الحاجة لتحديث كل بيان خدمة يُشير إليه.