أنواع خدمات Kubernetes - دليل شامل جداً

11 دقيقة

المشروع projet11-kubernetes-services · مستند مرجعي معمَّق.

يذهب هذا المستند أبعد بكثير من التصحيح: يفصّل كل أنواع الخدمات، والمفاهيم الداخلية (kube-proxy، Endpoints، EndpointSlices، DNS)، وحقول YAML المهمة، وسياسات الحركة، وتآلف الجلسة، وتعدد المنافذ، والمزالق الكلاسيكية وأفضل الممارسات.

جدول المحتويات

  1. تذكير: دور الخدمة
  2. تشريح خدمة (كل الحقول)
  3. النوع 1 — ClusterIP
  4. النوع 2 — NodePort
  5. النوع 3 — LoadBalancer
  6. النوع 4 — ExternalName
  7. خدمة Headless (بلا ClusterIP)
  8. خدمة بلا مُنتقي (Endpoints يدوية)
  9. port مقابل targetPort مقابل nodePort
  10. تعدد المنافذ والمنافذ المسمّاة
  11. كيف يعمل من الداخل: kube-proxy
  12. Endpoints و EndpointSlices
  13. DNS الخدمات (CoreDNS)
  14. سياسات الحركة (externalTrafficPolicy / internalTrafficPolicy)
  15. تآلف الجلسة
  16. البروتوكولات: TCP، UDP، SCTP، appProtocol
  17. Service مقابل Ingress مقابل Gateway API
  18. جدول موجز للأنواع
  19. مزالق كلاسيكية واستكشاف الأخطاء
  20. أفضل الممارسات
  21. تمارين صغيرة

1. تذكير: دور الخدمة

الـ Pod زائل: يمكن إعادة إنشائه في أي لحظة، بـ عنوان IP جديد. إذن لا يمكن الاعتماد على IP لـ Pod للتواصل.

الـ Service تجريد مستقر يقوم بـ:

  • توفير هوية شبكة دائمة (عنوان IP افتراضي و/أو اسم DNS)؛
  • انتقاء مجموعة Pods عبر وسومها؛
  • توزيع الحركة (load balancing) بين هذه الـ Pods؛
  • التحديث تلقائياً عندما تظهر الـ Pods أو تختفي.

الفكرة الأساسية: الخدمة لا «تحتوي» الـ Pods. هي تجدها باستمرار بفضل مُنتقي الوسوم، وتصون قائمة عناوينها في Endpoints.


2. تشريح خدمة (كل الحقول)

yaml
apiVersion: v1
kind: Service
metadata:
  name: mon-service
  labels:
    app: demo
  annotations: {}               # بيانات وصفية (غالباً يستخدمها LoadBalancer السحابي)
spec:
  type: ClusterIP               # ClusterIP | NodePort | LoadBalancer | ExternalName
  selector:                     # أي Pods تستهدفها هذه الخدمة (بالوسوم)
    app: demo
  ports:
    - name: http                # اسم المنفذ (مفيد إن وُجدت عدة منافذ)
      protocol: TCP             # TCP (افتراضي) | UDP | SCTP
      port: 80                  # منفذ الخدمة (ما يراه العملاء)
      targetPort: 5000          # منفذ الحاوية (أو اسم منفذ الحاوية)
      nodePort: 30080           # (NodePort/LoadBalancer) منفذ مفتوح على العقدة
  clusterIP: 10.96.0.10         # (اختياري) IP ثابت؛ "None" = headless
  sessionAffinity: None         # None | ClientIP
  externalTrafficPolicy: Cluster  # Cluster | Local (NodePort/LoadBalancer)
  internalTrafficPolicy: Cluster  # Cluster | Local
  ipFamilyPolicy: SingleStack   # SingleStack | PreferDualStack | RequireDualStack
  externalIPs: []               # عناوين IP خارجية موجَّهة نحو هذه الخدمة (متقدم)

كل حقل مفصَّل أدناه. يمكن إنشاء خدمة دنيا في 8 أسطر؛ لبقية الحقول قيم افتراضية معقولة.


3. النوع 1 — ClusterIP

النوع الافتراضي. يخصّص عنوان IP افتراضياً داخلياً (في نطاق Service CIDR، مثلاً 10.96.0.0/12)، قابل للوصول من داخل العنقود فقط.

yaml
apiVersion: v1
kind: Service
metadata:
  name: demo-clusterip
spec:
  type: ClusterIP
  selector:
    app: demo-back
  ports:
    - port: 80
      targetPort: 5000

الخصائص:

  • غير قابل للوصول من الخارج (لا EXTERNAL-IP).
  • أساس التواصل الداخلي (واجهة → خلفية، تطبيق → قاعدة بيانات، خدمات مصغّرة فيما بينها).
  • قابل للوصول باسم DNS: http://demo-clusterip (انظر §13).

متى تستخدمه: لكل ما يبقى داخل العنقود. هذا النوع الأكثر شيوعاً.


4. النوع 2 — NodePort

يفعل كل ما يفعله ClusterIP (يحصل على IP داخلي)، وزيادة: يفتح منفذاً ثابتاً على كل عقدة في العنقود (النطاق الافتراضي 30000–32767).

yaml
apiVersion: v1
kind: Service
metadata:
  name: demo-nodeport
spec:
  type: NodePort
  selector:
    app: demo-back
  ports:
    - port: 80          # منفذ الخدمة (داخلي)
      targetPort: 5000  # منفذ الحاوية
      nodePort: 30082   # منفذ مفتوح على كل عقدة

الوصول: http://<ip-de-n-importe-quel-noeud>:30082 (مع Docker Desktop: http://localhost:30082).

نقاط مهمة:

  • إن لم تحدّد nodePort، يختار Kubernetes واحداً في النطاق.
  • يُفتح المنفذ نفسه على كل العقد (بفضل routing mesh / kube-proxy)، حتى تلك التي لا تستضيف أي Pod للخدمة.
  • قليل الأناقة للإنتاج (منافذ غير قياسية، إدارة يدوية)، لكنه مثالي في التطوير/المحلي وغالباً اللبنة تحت LoadBalancer.

5. النوع 3 — LoadBalancer

يفعل كل ما يفعله NodePort، وزيادة: يطلب من البنية التحتية (السحابة) توفير موزّع حمل خارجي بـ عنوان IP عام.

yaml
apiVersion: v1
kind: Service
metadata:
  name: demo-lb
spec:
  type: LoadBalancer
  selector:
    app: demo-back
  ports:
    - port: 8090
      targetPort: 5000

حسب البيئة:

البيئةالسلوك
AWS / GCP / Azureينشئ LB مُداراً حقيقياً (ELB/NLB، GCP LB…) ويملأ EXTERNAL-IP
Docker DesktopEXTERNAL-IP = localhosthttp://localhost:8090
minikubeيوفّر minikube tunnel العنوان الخارجي
kind / bare-metalيبقى <pending> بلا متحكّم مثل MetalLB

السلسلة الكاملة: LoadBalancer → NodePort → ClusterIP → Endpoints → Pods.

التعليقات التوضيحية (خاصة بالمزوّد) تقود الـ LB، مثلاً على AWS:

yaml
metadata:
  annotations:
    service.beta.kubernetes.io/aws-load-balancer-type: "nlb"
    service.beta.kubernetes.io/aws-load-balancer-internal: "true"

LoadBalancer لكل خدمة = مكلف في السحابة. في الإنتاج نفضّل غالباً نقطة دخول واحدة (Ingress/Gateway) أمام عدة خدمات (انظر §17).


6. النوع 4 — ExternalName

حالة خاصة: لا مُنتقي، لا Pod، لا IP. ينشئ ببساطة اسماً مستعاراً DNS (سجل CNAME) نحو اسم خارجي.

yaml
apiVersion: v1
kind: Service
metadata:
  name: base-externe
spec:
  type: ExternalName
  externalName: db.exemple.com     # الـ Pods التي تستدعي "base-externe" تُوجَّه هنا

الاستخدام: توجيه اسم داخلي مستقر (base-externe) نحو خدمة خارج العنقود (قاعدة مُدارة، واجهة طرف ثالث). إن تغيّر العنوان، نعدّل مكاناً واحداً.

الحد: هذا DNS خالص، بلا توزيع حمل ولا تحكم في المنفذ. لا يناسب إن انتظرت الخدمة الخارجية Host HTTP خاصاً.


7. خدمة Headless (بلا ClusterIP)

بوضع clusterIP: None نحصل على خدمة بلا عنوان IP افتراضي. يعيد DNS إذن مباشرة عناوين IP لكل الـ Pods (قائمة سجلات A)، بدل عنوان واحد.

yaml
apiVersion: v1
kind: Service
metadata:
  name: demo-headless
spec:
  clusterIP: None        # <-- headless
  selector:
    app: demo-back
  ports:
    - port: 80
      targetPort: 5000

فائدتها:

  • عندما يريد العميل رؤية كل Pod على حدة (بلا LB مركزي).
  • ضرورية لـ StatefulSet: يحصل كل Pod على اسم DNS مستقر (pod-0.demo-headless، pod-1.demo-headless…)، مفيد لقواعد البيانات المنسوخة (Cassandra، Kafka، إلخ).
ClusterIP عاديHeadless (clusterIP: None)
عنوان IP افتراضينعم (واحد)لا
استجابة DNSعنوان واحد (عنوان الخدمة)N عناوين (عناوين الـ Pods)
التوزيععبر kube-proxyعلى عاتق العميل
حالة الاستخدامويب/واجهة بلا حالةقواعد منسوخة، StatefulSet

8. خدمة بلا مُنتقي (Endpoints يدوية)

يمكن ألا يكون للخدمة مُنتقٍ. عندئذ لا يملأ Kubernetes الـ Endpoints وحده: أنت تعرّفها يدوياً. عملي لعرض مورد خارجي تحت عنوان IP داخلي مستقر.

yaml
apiVersion: v1
kind: Service
metadata:
  name: api-legacy
spec:
  ports:
    - port: 80
      targetPort: 8080
---
apiVersion: v1
kind: Endpoints           # (أو EndpointSlice، أحدث)
metadata:
  name: api-legacy         # الاسم نفسه للخدمة
subsets:
  - addresses:
      - ip: 192.168.1.50   # خادم خارجي
    ports:
      - port: 8080

الفرق مع ExternalName: هنا نوجّه عبر IP (مع توزيع حمل ممكن على عدة عناوين)، لا عبر CNAME DNS.


9. port مقابل targetPort مقابل nodePort

هذا مصدر اللبس. ثلاثة منافذ مختلفة، ثلاثة أدوار:

الحقلأينالمعنى
portعلى الخدمةالمنفذ الذي يستخدمه العملاء للوصول إلى الخدمة
targetPortعلى الحاويةالمنفذ الذي يستمع عليه فعلاً التطبيق في الـ Pod
nodePortعلى العقدة(NodePort/LoadBalancer) المنفذ المفتوح على الآلة

مثال يُقرأ بصوت عالٍ: «يضغط العملاء على المنفذ 80 للخدمة، التي تنقل نحو المنفذ 5000 للحاوية؛ في NodePort ندخل أيضاً عبر المنفذ 30082 للآلة».

يمكن أن يشير targetPort إلى اسم منفذ معرَّف في الحاوية (انظر §10)، ما يتجنّب ترميز الرقم ثابتاً.


10. تعدد المنافذ والمنافذ المسمّاة

يمكن للخدمة عرض عدة منافذ (مثلاً HTTP + مقاييس). عندئذ يجب أن يكون لكل مدخل name.

yaml
spec:
  selector:
    app: demo
  ports:
    - name: http
      port: 80
      targetPort: web          # يشير إلى منفذ مسمّى للحاوية
    - name: metrics
      port: 9090
      targetPort: 9090

من جهة الحاوية، نسمّي المنافذ:

yaml
containers:
  - name: app
    ports:
      - name: web              # <-- يعيد استخدامه targetPort: web
        containerPort: 5000
      - name: metrics
        containerPort: 9090

فائدة المنافذ المسمّاة: إن تغيّر منفذ الحاوية، لا شيء يُعدَّل في الخدمة.


11. كيف يعمل من الداخل: kube-proxy

الخدمة كائن مجرّد: ليست عملية تستقبل الحركة. تُنجَز المعجزة بواسطة kube-proxy، مكوّن موجود على كل عقدة، يبرمج قواعد الشبكة في النواة لإعادة توجيه «IP:port الخدمة» نحو «IP:port لـ Pod».

أوضاع kube-proxy:

الوضعالمبدأملاحظات
iptables (افتراضي)قواعد iptables، اختيار عشوائي لـ Podبسيط، متين، شائع جداً
IPVSجدول تجزئة في النواة، خوارزميات LB حقيقية (rr، lc، sh…)أعلى أداء على العناقيد الكبيرة
nftablesخليفة iptablesأحدث

آثار عملية:

  • توزيع iptables عشوائي (ليس round-robin مرتَّباً حقيقياً).
  • لا يرى kube-proxy طبقة HTTP: هذا L3/L4 (IP/منفذ). لتوجيه HTTP (حسب المسار، حسب المضيف)، يلزم Ingress (§17).

12. Endpoints و EndpointSlices

يتجسّد الرابط خدمة ↔ Pods في كائنات:

  • Endpoints (تاريخي): كائن واحد يسرد كل IP:port للـ Pods الجاهزة.
  • EndpointSlices (حديث، موصى به): تُقسَّم القائمة إلى شرائح (حد أقصى ~100 نقطة نهاية لكل منها) ← قابلية توسّع أفضل بكثير على الخدمات الكبيرة.
bash
kubectl get endpoints demo-clusterip
kubectl get endpointslices -l kubernetes.io/service-name=demo-clusterip

من يحدّث القائمة؟ endpoint controller: ما إن يصير Pod Ready (readinessProbe سليم) ويطابق المُنتقي، يدخل عنوانه؛ إن سقط، يخرج.

يُسحب Pod غير Ready من Endpoints ← لا يتلقى حركة. لذلك readinessProbe أساسي: يتحكم في من هو «داخل» الخدمة. (استثناء: publishNotReadyAddresses: true ينشر أيضاً الـ Pods غير الجاهزة — استخدام headless خاص.)


13. DNS الخدمات (CoreDNS)

يشغّل Kubernetes CoreDNS. تتلقى كل خدمة اسماً DNS حتمياً:

<service>                                   # النطاق نفسه
<service>.<namespace>                        # نطاق آخر
<service>.<namespace>.svc.cluster.local      # الاسم الكامل FQDN

مثال من Pod:

bash
curl http://demo-clusterip                       # النطاق نفسه
curl http://demo-clusterip.default               # صريح
curl http://demo-clusterip.default.svc.cluster.local

السجلات المنتَجة:

  • خدمة عادية ← سجل A نحو ClusterIP.
  • خدمة headlessعدة A، واحد لكل Pod.
  • منافذ مسمّاة ← سجلات SRV: _http._tcp.demo-clusterip….
  • ExternalNameCNAME نحو الهدف.

تاريخياً، كان Kubernetes يحقن أيضاً متغيرات بيئة (DEMO_CLUSTERIP_SERVICE_HOST، ..._PORT) في الـ Pods المنشأة بعد الخدمة. يبقى DNS الطريقة الموصى بها (تعمل مهما كان ترتيب الإنشاء).


14. سياسات الحركة

externalTrafficPolicy (حركة واردة خارجية، NodePort/LoadBalancer)

القيمةالأثرالمقايضة
Cluster (افتراضي)يمكن إعادة توجيه الحركة نحو عقدة أخرى للوصول إلى Podتوزيع جيد، لكن عنوان IP المصدر للعميل يُخفى (SNAT) وقفزة شبكة إضافية
Localيخدم فقط الـ Pods في العقدة التي تستقبل الحزمةيحافظ على IP المصدر للعميل، بلا قفزة؛ لكن اختلال إن وُزِّعت الـ Pods سيئاً

internalTrafficPolicy (حركة داخلية، بين الـ Pods)

القيمةالأثر
Cluster (افتراضي)يوجّه نحو أي Pod للخدمة
Localيوجّه فقط نحو الـ Pods في العقدة نفسها (مفيد للكمون / المحلية)

externalTrafficPolicy: Local هو الإعداد الأساس عندما تحتاج إلى معرفة عنوان IP الحقيقي للعميل (سجلات، أمن، تحديد موقع).


15. تآلف الجلسة

افتراضياً، يمكن لكل طلب أن يذهب نحو أي Pod. لـ «لصق» عميل بـ Pod نفسه:

yaml
spec:
  sessionAffinity: ClientIP
  sessionAffinityConfig:
    clientIP:
      timeoutSeconds: 10800     # 3 ساعات
  • None (افتراضي): توزيع في كل طلب.
  • ClientIP: كل طلبات عنوان IP نفسه تذهب إلى الـ Pod نفسه (جلسات لاصقة أساسية L4).

لجلسات HTTP أدق (حسب ملف تعريف الارتباط)، نستخدم Ingress (L7).


16. البروتوكولات: TCP، UDP، SCTP، appProtocol

  • protocol: TCP (افتراضي)، UDP (DNS، ألعاب، بث)، SCTP (اتصالات).
  • يمكن خلط عدة بروتوكولات على الخدمة نفسها (منافذ متمايزة).
  • يحدّد appProtocol (إرشادي) البروتوكول التطبيقي (http، https، grpc) للأدوات/الـ LB.
yaml
ports:
  - name: dns-udp
    port: 53
    protocol: UDP
    targetPort: 53
  - name: dns-tcp
    port: 53
    protocol: TCP
    targetPort: 53

17. Service مقابل Ingress مقابل Gateway API

تعمل الخدمة في L4 (IP/منفذ). لا تعرف التوجيه حسب URL أو اسم المضيف، ولا إدارة TLS. لذلك:

الكائنالطبقةالدور
Service (ClusterIP/NodePort/LB)L3/L4عنوان مستقر + LB بسيط نحو Pods
IngressL7 (HTTP/HTTPS)توجيه حسب المضيف والمسار، TLS، نقطة دخول واحدة لـ عدة خدمات
Gateway APIL7 (خليفة Ingress)أكثر تعبيراً، فصل الأدوار، متعدد البروتوكولات

النموذج النموذجي في الإنتاج: LoadBalancer واحدIngress → عدة ClusterIP داخلية. نوفّر الـ LB المكلفة ونمركز TLS/التوجيه.


18. جدول موجز للأنواع

النوعIP داخليوصول خارجيDNSمُنتقيحالة الاستخدام
ClusterIPنعملاسجل A واحد (ClusterIP)نعمتواصل داخلي (الأكثر شيوعاً)
NodePortنعممنفذ العقدةسجل A واحدنعمتطوير/محلي، لبنة LB
LoadBalancerنعمIP عامسجل A واحدنعمخدمة عامة في السحابة
ExternalNameلا— (CNAME)CNAMEلااسم مستعار نحو خدمة خارجية
Headless (clusterIP: None)لالاN سجلات A (Pods)نعمStatefulSet، قواعد منسوخة
بلا مُنتقينعمحسب النوعسجل A واحدلاEndpoints يدوية (مورد خارجي)

19. مزالق كلاسيكية واستكشاف الأخطاء

العَرَضالسبب الشائعالحل
الخدمة لا تجيبالمُنتقي ≠ وسوم الـ Podsمحاذاة spec.selector و labels لقالب الـ Pod
Endpoints فارغةلا Pod Ready أو لا Pod مطابقkubectl get endpoints <svc>؛ تحقق من readinessProbe والوسوم
اتصال مرفوض داخلياًtargetPort خاطئtargetPort = المنفذ الفعلي للحاوية
يبقى EXTERNAL-IP على <pending>لا متحكّم LB (kind/bare-metal)Docker Desktop سليم؛ وإلا MetalLB / port-forward
عنوان IP المصدر للعميل مخفيexternalTrafficPolicy: Clusterالانتقال إلى Local
NodePort غير متاحمنفذ خارج النطاق / مشغولاستخدم 30000–32767، غيّر nodePort
DNS لا يحلنطاق خاطئ / CoreDNS متعطلاختبر FQDN؛ kubectl -n kube-system get pods (coredns)

أوامر التشخيص:

bash
kubectl get svc <nom> -o wide
kubectl describe svc <nom>
kubectl get endpoints <nom>
kubectl get endpointslices -l kubernetes.io/service-name=<nom>
kubectl run test --rm -it --image=busybox:1.36 -- sh   # nslookup <svc>, wget -qO- http://<svc>

20. أفضل الممارسات

  • افتراضياً، ClusterIP. لا تعرض للخارج إلا ما يجب عرضه.
  • LoadBalancer واحد + Ingress أمام عدة ClusterIP (تكلفة + TLS مركزي).
  • سمِّ منافذك (تعدد المنافذ، وtargetPort بالاسم ← فك الارتباط).
  • اعتنِ بـ readinessProbe: هي التي تقرّر من هو داخل Endpoints.
  • اتساق الوسوم/المُنتقيات: هذا الخطأ رقم 1. أبقِ وسوماً مستقرة (app، tier، version).
  • استخدم externalTrafficPolicy: Local عندما يهم عنوان IP الحقيقي للعميل.
  • فضّل EndpointSlices (مفعَّلة افتراضياً في الإصدارات الحديثة) لقابلية التوسّع.
  • لا ترمّز أبداً عنوان IP لـ Pod ثابتاً: استخدم اسم DNS للخدمة.

21. تمارين صغيرة

التمرين 1 — تحويل NodePort إلى ClusterIP

اسحب type: NodePort (أو ضع ClusterIP)، أعد التطبيق، ثم أثبت أنه لم يعد قابلاً للوصول من المضيف لكنه قابل للوصول بالاسم من Pod (curl http://demo-nodeport... بعد إعادة التسمية). لاحظ kubectl get svc: لا عمود nodePort بعد.

التمرين 2 — كسر Endpoints ثم إصلاحها

غيّر مُنتقي الخدمة إلى app: inexistant، أعد التطبيق، ولاحظ kubectl get endpoints فارغاً + خدمة غير قابلة للوصول. أعد app: demo-back: تعود Endpoints.

التمرين 3 — خدمة headless

أنشئ خدمة بـ clusterIP: None، ثم من Pod: nslookup demo-headless. يجب أن ترى عدة عناوين IP (واحد لكل Pod) بدل واحد.

التمرين 4 — تعدد المنافذ

أضف منفذاً metrics (9090) مسمّى للحاوية وللخدمة. تحقّق بـ kubectl describe svc أن المنفذين يظهران، وأن targetPort يشير فعلاً إلى اسم المنفذ.

التمرين 5 — ExternalName

أنشئ خدمة ExternalName نحو example.com. من Pod: يجب أن يعيد nslookup mon-alias CNAME نحو example.com.


العودة إلى تصحيح المشروع · المفاهيم الأساسية: 01-CONCEPTS-SERVICES.md · الأوامر: 02-COMMANDES.md.


دورة من إعداد د. هيثم رحومة — تطوير ونشر حلول البيانات