EduFlow

الوحدة 4 · أساسيات Kubernetes

الـ Services: عناوين ثابتة لـ Pods متنقّلة

20 دقراءة 4 داكتشف الخطأ

الدرس في لمحة

أعطِ notes-api وقاعدة بياناته أسماء وعناوين ثابتة بالـ Services، واربط port وtargetPort وcontainerPort بشكل صحيح، واختر بين ClusterIP وNodePort وLoadBalancer.

  1. 1Service لـ notes-api
  2. 2ثلاثة منافذ، وثلاثة معانٍ
  3. 3إيجاد الـ Services بالاسم
  4. 4أنواع الـ Services
  5. 5الوصول إليه من حاسوبك

لا فيديو هنا، بل الشيء الحقيقي: درس مكتوب وأمثلة قابلة للتشغيل واختبار وتدريب.

شغّل kubectl get pods -o wide مرتين، مع rollout بينهما، وستجد كل عنوان IP مختلفًا. الـ Pods تُستبدل باستمرار: إصدارات جديدة، وإعادة جدولة، وتوسّع. كل ما كان يتذكّر عنوان IP لـ Pod صار يشير إلى لا شيء. الـ Service هو الشيء الثابت الذي تشير إليه بدلًا من ذلك.

Service لـ notes-api

k8s/service.yaml
apiVersion: v1
kind: Service
metadata:
  name: notes-api
  labels:
    app.kubernetes.io/name: notes-api
spec:
  selector:
    app.kubernetes.io/name: notes-api
  ports:
    - name: http
      port: 80
      targetPort: http

هذا ينشئ عنوان IP افتراضيًا واسم DNS، ‏notes-api، يدومان ما دام الـ Service موجودًا. الاتصالات إليه تُوزَّع على الـ Pods التي يطابقها الـ selector الخاص به، وهو استعلام الـ labels نفسه الذي يستخدمه الـ Deployment. يتتبّع Kubernetes المجموعة الحالية من الـ Pods الجاهزة المطابقة في EndpointSlices ويحدّثها كلما جاءت Pods وذهبت. والعملاء لا يلاحظون شيئًا.

الـ ClusterIP ليس جهازًا، ولا تستمع عليه أي عملية. طبقة الشبكة في كل node ‏(kube-proxy، أو بديل مبني على eBPF في كثير من الـ clusters) تبرمج قواعد تعيد كتابة الحزم المرسلة إلى عنوان IP الخاص بالـ Service ومنفذه لتذهب إلى أحد الـ Pods الجاهزة. لهذا لا تستطيع تنفيذ ping على عنوان IP الخاص بـ Service، ولا داعي للقلق حين لا يجيب: المنافذ المصرَّح بها هي وحدها الموجودة. ولهذا أيضًا لا يكلّف الـ Service شيئًا تقريبًا، فأنشئ واحدًا لكل حمل عمل يستقبل حركة.

ثلاثة منافذ، وثلاثة معانٍ

هنا تعيش معظم أخطاء الـ Services.

  • containerPort في مواصفات الـ Pod هو المنفذ الذي تستمع عليه عمليتك: 3000. إنه معلوماتي، مثل EXPOSE، لكن تسميته (name: http) تتيح للكائنات الأخرى الإشارة إليه.
  • targetPort في الـ Service هو المكان الذي تُسلَّم إليه الحركة على الـ Pod. يجب أن يطابق المكان الذي يستمع عليه التطبيق فعلًا. وهنا يستخدم الاسم http، الذي يُحلّ إلى 3000.
  • port في الـ Service هو ما يتّصل به العملاء: 80.
‏Pod عميل يستدعي notes-api على المنفذ 80. الـ Service يحوّل إلى targetPort ‏http، وهو containerPort ‏3000، على أحد ثلاثة Pods جاهزة من notes-api يختارها الـ label. Pod عميل notes-api:80 Service notes-api port: 80 targetPort: http pod :3000 pod :3000 pod :3000 selector: app.kubernetes.io/name=notes-api containerPort http = 3000
العملاء يستخدمون منفذ الـ Service؛ والـ Service يحوّل إلى targetPort على Pod جاهز

إيجاد الـ Services بالاسم

يعطي DNS الخاص بالـ cluster كل Service اسمًا بالشكل <service>.<namespace>.svc.cluster.local. من Pod في الـ namespace نفسه يكفي الاسم القصير notes-api؛ ومن namespace آخر استخدم notes-api.notes أو الاسم الكامل.

يحتاج notes-api إلى قاعدة بيانات، فأعطه واحدة بالطريقة نفسها. للتدرّب في kind، شغّل Postgres كـ Deployment بنسخة واحدة مع Service من نوع ClusterIP:

k8s/db.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: notes-db
spec:
  replicas: 1
  selector:
    matchLabels:
      app.kubernetes.io/name: notes-db
  template:
    metadata:
      labels:
        app.kubernetes.io/name: notes-db
    spec:
      containers:
        - name: postgres
          image: postgres:17
          env:
            - name: POSTGRES_USER
              value: notes
            - name: POSTGRES_PASSWORD
              value: notes          # moved to a Secret in the next lesson
            - name: POSTGRES_DB
              value: notes
          ports:
            - name: postgres
              containerPort: 5432
---
apiVersion: v1
kind: Service
metadata:
  name: notes-db
spec:
  selector:
    app.kubernetes.io/name: notes-db
  ports:
    - name: postgres
      port: 5432
      targetPort: postgres

المضيف في DATABASE_URL الخاص بـ notes-api صار الآن notes-db، تمامًا كما كان db في Compose. والفاصل --- يفصل بين كائنين في ملف واحد.

أنواع الـ Services

النوع يمكن الوصول إليه من استخدمه لـ
ClusterIP (الافتراضي) داخل الـ cluster الحركة بين الخدمات، مثل notes-api ← notes-db
NodePort عنوان IP لكل node على منفذ ضمن 30000–32767 الاختبارات السريعة، أو كلبنة بناء لموازنات الأحمال الخارجية
LoadBalancer عنوان موازن أحمال سحابي عرض خدمة واحدة للعموم على cluster مُدار

وهناك أيضًا الـ Service من نوع headless، مع clusterIP: None. لا يحصل على عنوان IP افتراضي؛ بل يُرجع اسم الـ DNS الخاص به عناوين IP للـ Pods المفردة. تستخدمه قواعد البيانات والأنظمة العنقودية الأخرى حين يحتاج كل عضو إلى أن يُخاطَب مباشرة. لن تحتاج إليه لـ notes-api.

يُبنى LoadBalancer فوق النوعين الآخرين: ينشئ مزوّد السحابة موازن أحمال يحوّل في العادة إلى منافذ الـ nodes، وهذه تحوّل إلى الـ Service (وبعض الـ controllers يرسل الحركة مباشرة إلى عناوين IP الخاصة بالـ Pods بدلًا من ذلك). واحد لكل خدمة عامة يصبح مكلفًا ومتكرّرًا حين تكون لديك خدمات HTTP كثيرة؛ والدرس 5.4 يغطّي Gateway API، الذي يضع مسارات كثيرة خلف نقطة دخول واحدة.

الوصول إليه من حاسوبك

Terminal
kubectl apply -f k8s/
kubectl get svc
kubectl get endpointslices -l kubernetes.io/service-name=notes-api
kubectl port-forward svc/notes-api 8080:80
curl localhost:8080/readyz

الأمر port-forward يحفر نفقًا من منفذ محلي إلى الـ Service عبر الـ API server. إنه للتشخيص، لا للحركة الحقيقية. إذا لم يسرد الـ EndpointSlice أي عناوين، فالـ Service لا يطابق أي Pods جاهزة: قارن الـ selector بمخرجات kubectl get pods --show-labels، ثم تحقّق من الجاهزية.

مشكلتان صارتا ظاهرتين الآن في الـ YAML: كلمة مرور مكتوبة نصًّا صريحًا، وإعدادات مخلوطة داخل الـ Deployment. الدرس التالي يُخرج الاثنتين.

أهم النقاط

  • الـ Service يعطي مجموعة متغيّرة من الـ Pods عنوان IP افتراضيًا واسم DNS ثابتين، ويوزّع الاتصالات على الـ Pods الجاهزة التي يطابقها الـ selector الخاص به.
  • الحقل port هو ما يتّصل به العملاء على الـ Service؛ وtargetPort هو وجهة الحركة على الـ Pod، ويجب أن يطابق المنفذ الذي يستمع عليه الـ container.
  • داخل الـ cluster، تصل الـ Pods إلى الـ Service باسمه، مثل notes-db في الـ namespace نفسه أو notes-db.notes.svc.cluster.local من أي مكان.
  • استخدم ClusterIP للحركة الداخلية، وLoadBalancer لعرض خدمة واحدة عبر موازن أحمال سحابي، وNodePort للاختبار أساسًا.
  • الـ EndpointSlice الفارغ يعني أن الـ Service لا يطابق أي Pods جاهزة؛ تحقّق من الـ selector والجاهزية أولًا.

قراءات إضافية

تحقّق من فهمك

4 أسئلة

السؤال 1 من 4

يستمع notes-api على 3000. الـ Service فيه port: 80 وtargetPort: 3000. أي عنوان تستخدمه الـ Pods الأخرى في الـ namespace نفسه؟

السؤال 2 من 4

الأمر kubectl get endpointslices -l kubernetes.io/service-name=notes-api لا يعرض أي endpoints، لكن ثلاثة Pods من notes-api في حالة Running. ما الذي تتحقّق منه أولًا؟

السؤال 3 من 4

تحتاج إلى أن يكون notes-api قابلًا للوصول من الإنترنت على cluster مُدار في AWS، وهو الخدمة العامة الوحيدة. أي نوع Service يناسب؟

السؤال 4 من 4

أي targetPort صحيح لـ container يصرّح بـ ports: [{ name: http, containerPort: 3000 }]؟

تدريب

اكتشف الخطأ

الـ endpoints موجودة، والطلبات تفشل

الـ Service أدناه له ثلاثة endpoints، والـ Pods في حالتَي Running وReady، ومع ذلك يحصل curl notes-api من Pod آخر على connection refused في كل مرة. اختر السطر المسبّب.

حدّد السطر أو الأسطر التي تظنها خاطئة، ثم تحقّق.