شبكة الخدمات: Istio وLinkerd

إدارة حركة المرور

18 دقيقة الدرس 4 من 27

إدارة حركة المرور

إدارة حركة المرور هي القيمة الجوهرية التي يقدمها شبكة الخدمات. بدون شبكة خدمات، يعيش توجيه الحركة داخل كود تطبيقك أو في قواعد موازن التحميل ذات الحبيبية الخشنة — وكلاهما غير مرن وخطير تشغيليًا على نطاق واسع. يُخرج Istio كل قرار توجيه إلى موردَين مخصصَين في Kubernetes: VirtualService وDestinationRule. فهم الحدود بينهما، وكيفية تركيبهما معًا، هو الشرط المسبق لأي عمل متقدم في مستوى البيانات — سواء كان Canary أو Blue/Green أو قطع الدائرة أو حقن الأخطاء أو التوجيه بالترويسات — إذ يعتمد كل ذلك على هذا الثنائي.

نموذج المَوردَين

من المفيد التفكير في هذين المَوردَين باعتبارهما طبقتَي تجريد متميزتَين:

  • VirtualService — طبقة التوجيه. يُجيب على السؤال "إلى أين يذهب هذا الطلب؟" يُطابق على أساس HTTP method وبادئة URI والترويسات ونطاق المصدر أو معاملات الاستعلام، ويُحيل الحركة المطابقة إلى وجهة واحدة أو أكثر. تُشير الوجهة إلى خدمة Kubernetes مع subset اختياري يُصنِّف نسخ العمل.
  • DestinationRule — طبقة السياسة. يُجيب على السؤال "كيف يجب أن تتصرف الحركة عند وصولها إلى هذه الوجهة؟" يُعرِّف الـ subsets (محددات تسمية البودات التي تُجمِّع نسخ العمل)، وخوارزميات موازنة التحميل، وحدود مجمع الاتصالات، وإخراج المنحرفات (قطع الدائرة). يجب الإعلان عن أسماء الـ subsets التي يرجع إليها VirtualService في DestinationRule الخاص بنفس الـ host.
اقتران جوهري يجب فهمه: مسار VirtualService الذي يُسمّي subset (مثل subset: v2) سيُسقط الحركة بصمت إذا لم يكن هناك DestinationRule يُعلن عن ذلك الـ subset لنفس الـ host. فببساطة، ليس لدى Envoy أي نقاط نهاية للتوجيه إليها. وهذا هو الخطأ في الإعداد الأكثر شيوعًا عندما تتبنى الفرق Istio لأول مرة.
VirtualService and DestinationRule composition — traffic flow Client Sidecar Proxy VirtualService Match: URI / headers Weight: 90% v1 / 10% v2 Retries / Timeouts Fault injection DestinationRule Subset: v1 (version=v1) Subset: v2 (version=v2) LB: LEAST_CONN Circuit breaker Pod v1 version=v1 Pod v2 version=v2 90% 10% VirtualService routes; DestinationRule defines subsets and policies.
تتدفق الحركة من sidecar العميل عبر طبقة التوجيه في VirtualService، ثم عبر الـ subsets في DestinationRule إلى إصدار البود الصحيح.

VirtualService: قواعد التوجيه بعمق

يُطبَّق VirtualService على حركة المرور المتجهة نحو host معين (يُعيَّن على اسم خدمة Kubernetes). يُقيَّم المصفوفة http من الأعلى إلى الأسفل؛ وتفوز القاعدة المطابقة الأولى. هذا الترتيب مهم — ضع المطابقات الأكثر تحديدًا (Canary بالترويسة) قبل المطابقات الشاملة بالأوزان، وإلا لن تُنفَّذ قواعدك المحددة أبدًا.

# virtualservice-checkout.yaml apiVersion: networking.istio.io/v1 kind: VirtualService metadata: name: checkout namespace: production spec: hosts: - checkout # Kubernetes service name (short form within namespace) http: # Rule 1: internal testers get v2 always (header-based canary) - match: - headers: x-canary-user: exact: "true" route: - destination: host: checkout subset: v2 weight: 100 # Rule 2: 10% of remaining traffic to v2 (weighted canary) - route: - destination: host: checkout subset: v1 weight: 90 - destination: host: checkout subset: v2 weight: 10 timeout: 5s retries: attempts: 3 perTryTimeout: 2s retryOn: 5xx,reset,connect-failure,retriable-4xx

الحقول الرئيسية التي يجب معرفتها في بيئة الإنتاج:

  • timeout — مهلة انتظار كل طلب. القيمة الافتراضية هي 15 ثانية في Istio 1.x. حدِّدها صراحةً؛ الاعتماد على الافتراضي يُفضي إلى تجاوزات صامتة لميزانية زمن الاستجابة عند تدهور الخدمة المنبثقة.
  • retries.retryOn — شروط إعادة المحاولة في Envoy مفصولة بفواصل. استخدام 5xx وحده خطير إذا كانت نقاط نهاية POST لديك غير idempotent؛ يُفضَّل استخدام connect-failure,reset,retriable-4xx للمسارات التي تُحدث تغييرات.
  • match.sourceLabels — التوجيه بناءً على تسميات بود المُستدعي، لا على ترويسات الطلب فحسب. مفيد عندما تشترك وظيفة دفعية وواجهة برمجية موجّهة للمستخدم في اسم خدمة لكنهما بحاجة إلى سياسات توجيه مختلفة.

DestinationRule: الـ Subsets وموازنة التحميل

يُعلن DestinationRule الخاص بخدمة checkout ذاتها عن الـ subsets التي أشار إليها VirtualService، ويضع السياسات لكل subset (أو بشكل عام):

# destinationrule-checkout.yaml apiVersion: networking.istio.io/v1 kind: DestinationRule metadata: name: checkout namespace: production spec: host: checkout trafficPolicy: loadBalancer: simple: LEAST_CONN # Better than ROUND_ROBIN for heterogeneous pod latency connectionPool: http: http2MaxRequests: 1000 http1MaxPendingRequests: 100 tcp: maxConnections: 200 outlierDetection: # Passive circuit breaking — eject unhealthy pods consecutiveGatewayErrors: 5 interval: 10s baseEjectionTime: 30s maxEjectionPercent: 50 # Never eject more than half the subset — prevents cascade subsets: - name: v1 labels: version: v1 trafficPolicy: loadBalancer: simple: ROUND_ROBIN # Override global policy for v1 specifically - name: v2 labels: version: v2 # Inherits global LEAST_CONN
حدِّد maxEjectionPercent صراحةً. القيمة الافتراضية هي 10%، وهي آمنة للمجموعات الكبيرة، لكنها تعني أن subset صغيرة (مثل 3 بودات) تُخرج 0 بودات بحد الافتراضي — إذ يُقرِّب Envoy للأسفل. في فرق Google على نطاق واسع يُعيَّن عادةً 50% للخدمات الحرجة، ويُستخدم 100% فقط للنسخ المتماثلة للقراءة عديمة الحالة حيث الإخفاق الجزئي مقبول.

نشر Canary بتقسيم الحركة

يُطلق الـ Canary نسخة جديدة لنسبة محكومة من الحركة قبل الطرح الكامل. يجعل توجيه الأوزان في Istio هذا دقيقًا بشكل بديهي — خلافًا لطرح Kubernetes Deployment الذي لا يستطيع سوى تقريب النسب عبر نسب نسخ البودات المتماثلة (10% يتطلب نسبة 9:1، أي 10 بودات على الأقل).

سير عمل Canary في الإنتاج:

  1. انشر v2 كـ Deployment منفصل بتسمية version: v2. اجعل عدد النسخ المتماثلة 1 أو 2 للـ Canary — Istio يتحكم في النسب، لا عدد البودات.
  2. حدِّث DestinationRule لإعلان الـ subset الخاص بـ v2.
  3. حدِّث VirtualService لتقسيم الحركة: ابدأ بنسبة 1-5%، راقب معدل الأخطاء وزمن الاستجابة p99، ثم تقدم بخطوات (10%، 25%، 50%، 100%).
  4. عند 100%، احذف Deployment الإصدار v1 وأزل تقسيم الأوزان من VirtualService.
# Automate canary progression with kubectl patch (or GitOps — update the manifest in git) # Step: advance canary from 10% to 25% kubectl patch virtualservice checkout -n production --type=json \ -p='[ {"op":"replace","path":"/spec/http/1/route/0/weight","value":75}, {"op":"replace","path":"/spec/http/1/route/1/weight","value":25} ]' # Verify the live weights via istioctl istioctl proxy-config routes deploy/checkout-v2 \ --name 80 \ -o json | jq '.[].virtualHosts[].routes[].route.weightedClusters'
يجب أن يساوي مجموع الأوزان دائمًا 100. يُتحقق Istio من ذلك عند الإدخال منذ الإصدار 1.14 (سيرفض الـ webhook المورد)، لكن المجموعات الأقدم تقبل الأوزان غير الصالحة بصمت ويُطبِّعها Envoy بطريقة غير محددة. تحقق دائمًا قبل التطبيق: istioctl analyze -f virtualservice-checkout.yaml يُصيد هذا وحوالي 50 خطأ إعداد شائعًا آخر قبل وصوله إلى المجموعة.

التوجيه بالترويسات لنشر Dark Launch

تُعرِّض Canary القائمة على الأوزان النسخة الجديدة للمستخدمين الحقيقيين بشكل تناسبي. أما التوجيه بالترويسات فهو استراتيجية تكاملية: ترويسة طلب محددة (تضبطها الأدوات الداخلية أو SDK العلامات المميزة أو ملف تعريف ارتباط) تُوجِّه حاملها إلى v2 بينما 100% من الحركة العادية تبقى على v1. يُسمى هذا Dark Launch — النسخة موجودة تقنيًا في الإنتاج لكنها غير مرئية للمستخدمين العاديين.

في Netflix وUber تُستخدم هذه النمط للتحقق متعدد المناطق: الموظفون الداخليون الذين يصلون إلى الإنتاج من شبكة الشركة يحصلون على ترويسة مُعاد توجيهها تُظلِّلهم على نسخ Canary من مئات الخدمات في آنٍ واحد، مما يُشغِّل حمل إنتاج حقيقي على الكود الجديد دون مخاطر على المستخدمين النهائيين.

# Header-based routing — route employees to v2 via cookie # The ingress gateway or API gateway injects x-canary-user: "true" for known employee IPs - match: - headers: cookie: regex: ".*employee_beta=1.*" route: - destination: host: checkout subset: v2 # Mirror traffic to v2 for passive testing (no user impact) - route: - destination: host: checkout subset: v1 weight: 100 mirror: host: checkout subset: v2 mirrorPercentage: value: 100.0 # Send a copy of ALL v1 traffic to v2 in the background

يُرسل حقل mirror نسخة fire-and-forget من الطلبات المطابقة إلى الوجهة الظل. تُتجاهَل الردود — يرى المستخدمون فقط ردود v1. هذا يُتيح التحقق من v2 في مواجهة أشكال حركة الإنتاج الحقيقية (الأنماط المتفجرة، والحمولات غير المعتادة، والترويسات الحدية) قبل تحويل أي مستخدم حقيقي إليه.

التحقق التشغيلي

بعد تطبيق أي تغيير على VirtualService أو DestinationRule، تحقق من أن الإعداد قد انتشر إلى جميع sidecars الـ Envoy قبل إعلان اكتمال الطرح:

# Check that pilot has distributed the latest config to all proxies in the namespace istioctl proxy-status -n production # If a proxy shows STALE, inspect its xDS config directly istioctl proxy-config cluster deploy/frontend-v1 -n production | grep checkout # Run the mesh-level analyzer against the whole namespace istioctl analyze -n production # Confirm actual routing weights from Envoy's perspective (not just the CRD) istioctl proxy-config routes deploy/frontend-v1 \ --name 80 \ -o json | jq '.[] | select(.name=="80") | .virtualHosts[] | select(.name | contains("checkout"))'
تطبيق CRD ليس دليلًا على تغيير التوجيه. نجاح kubectl apply يعني فقط أن خادم API قبل المورد. يجب على Istiod بعد ذلك ترجمته إلى xDS ودفعه إلى كل وكيل Envoy. عند أعداد بودات كبيرة (1000+) يمكن أن يستغرق هذا الانتشار 5-30 ثانية. تُظهر istioctl proxy-status حالة المزامنة — انتظر حتى تُظهر جميع الوكلاء SYNCED قبل تشغيل اختبارات الدخان.

أنماط الفشل في الإنتاج

أكثر حوادث الإنتاج شيوعًا الناتجة عن إعداد خاطئ في VirtualService وDestinationRule، مرتبة حسب التكرار في post-mortems المنشورة:

  • Subset مفقود في DestinationRule: يُشير VirtualService إلى subset: v2 لكن DestinationRule لا يحتوي على إدخال مطابق — يُسقط Envoy الحركة بصمت. استخدم istioctl analyze في خط أنابيب CD قبل الدمج.
  • عدم تطابق نطاق الـ namespace: VirtualService في namespace A يستهدف خدمة في namespace B يتطلب FQDN الكامل (checkout.production.svc.cluster.local)، لا الاسم المختصر. الأسماء المختصرة تُحلَّل داخل namespace الـ VirtualService الخاص بها فقط.
  • عواصف إعادة المحاولة: إعادة المحاولة العدوانية على خدمة منبثقة متدهورة تُضاعف QPS بعدد مرات إعادة المحاولة. خدمة تُعالج 10,000 طلب/ثانية مع 3 محاولات إعادة على 5xx ستولِّد 40,000 طلب/ثانية للخدمة الفاشلة المنبثقة. اقرن إعادة المحاولة بقطع الدائرة والتراجع الأسي، ولا تضبط retryOn: 5xx أبدًا على نقاط نهاية غير idempotent.
  • تقسيم الأوزان أثناء إعادة تشغيل البودات: إذا كان Deployment الإصدار v2 لا يحتوي على بودات جاهزة والحركة لا تزال لها وزن > 0 يتجه إلى subset، فسيُصطدم كل طلب في تلك النسبة بـ 503. تأكد دائمًا من صحة الـ Deployment المستهدف قبل زيادة وزنه.