פריסת שער מרובה אשכולות לחלוקת תנועה משוקללת

במסמך הזה מוסבר איך לבצע פריסת כחול-ירוק (blue-green deployment) של אפליקציית storeדוגמה בשני אשכולות GKE. פריסות כחול-ירוק הן אסטרטגיה יעילה להעברת האפליקציות שלכם לאשכולות GKE חדשים עם סיכון מינימלי. באמצעות העברה הדרגתית של תעבורת הנתונים מהאשכול הנוכחי (כחול) לאשכול החדש (ירוק), אתם יכולים לאמת את הסביבה החדשה בסביבת הייצור לפני שאתם מתחייבים למעבר מלא.

שערים מרובי אשכולות הם דרך יעילה לנהל את התעבורה של שירותים שנפרסו בכמה אשכולות GKE. באמצעות תשתית איזון העומסים הגלובלית של Google, אתם יכולים ליצור נקודת כניסה אחת לאפליקציות שלכם, וכך לפשט את הניהול ולשפר את האמינות.

במדריך הזה משתמשים באפליקציית store לדוגמה כדי לדמות תרחיש מהעולם האמיתי שבו שירות קניות אונליין נמצא בבעלות ובהפעלה של צוותים נפרדים, ונפרס בצי של אשכולות GKE משותפים.

לפני שמתחילים

כדי לפרוס שערים עם כמה אשכולות, צריך להכין את הסביבה. לפני שממשיכים, פועלים לפי השלבים שמפורטים בקטע הכנת הסביבה לשערים עם כמה אשכולות:

  1. פריסה של אשכולות GKE.

  2. רושמים את האשכולות ב-Fleet (אם הם עדיין לא רשומים).

  3. מפעילים את בקרי השירות מרובי האשכולות והשער מרובי האשכולות.

לסיום, לפני שמשתמשים בבקר בסביבה שלכם, כדאי לעיין במגבלות ובבעיות הידועות של בקר GKE Gateway.

ניתוב כחול-ירוק מרובה אשכולות עם Gateway

ל-GatewayClasses‏ gke-l7-global-external-managed-*, ‏gke-l7-regional-external-managed-* ו-gke-l7-rilb-* יש הרבה יכולות מתקדמות של ניתוב תעבורת נתונים, כולל חלוקת התנועה, התאמת כותרות, שינוי כותרות, שיקוף תנועה ועוד. בדוגמה הזו נראה איך להשתמש בחלוקת תנועה מבוססת-משקל כדי לשלוט באופן מפורש בחלוקת התנועה בין שני אשכולות GKE.

בדוגמה הזו נתאר כמה שלבים מציאותיים שבעלי שירותים יכולים לבצע כדי להעביר או להרחיב את האפליקציה שלהם לאשכול GKE חדש. המטרה של פריסות כחול-ירוק היא להפחית את הסיכון באמצעות כמה שלבי אימות שמאשרים שהאשכול החדש פועל בצורה תקינה. בדוגמה הזו מוסברים ארבעה שלבים של פריסה:

  1. ‫100%-Header-based canary: Use HTTP header routing to send only test or synthetic traffic to the new cluster.
  2. ‫100%-שיקוף תנועה: שיקוף תנועת המשתמשים לאשכול ה-canary. במהלך הבדיקה, 100% מתנועת המשתמשים מועתקת לאשכול הזה כדי לבדוק את הקיבולת שלו.
  3. ‫90%-10%: חלוקת תנועה מסוג Canary של 10% כדי לחשוף את האשכול החדש לתנועה חיה באופן הדרגתי.
  4. ‫0%-100%: מעבר חד למערכת אחרת (cutover) מלא לאשכול החדש עם אפשרות לחזור לאשכול הקודם אם מתגלות שגיאות.

חלוקת תנועה כחול-ירוק בין שני אשכולות GKE

הדוגמה הזו דומה לקודמת, אבל במקום זאת היא פורסת שער פנימי מרובה אשכולות. הפעולה הזו פורסת מאזן עומסים פנימי של אפליקציות (ALB) שאפשר לגשת אליו באופן פרטי רק מתוך ה-VPC. תשתמשו באשכולות ובאותו אפליקציה שפרסתם בשלבים הקודמים, אבל תפרסו אותם דרך שער שונה.

דרישות מוקדמות

הדוגמה הבאה מבוססת על חלק מהשלבים במאמר בנושא פריסת שער חיצוני מרובה אשכולות. לפני שמתחילים ליישם את הדוגמה הזו, חשוב לוודא שביצעתם את הפעולות הבאות:

  1. הכנת הסביבה לשערים מרובי אשכולות

  2. פריסת אפליקציית הדגמה

    בדוגמה הזו נעשה שימוש באשכולות gke-west-1 ו-gke-west-2 שכבר הגדרתם. האשכולות האלה נמצאים באותו אזור כי gke-l7-rilb-mc GatewayClass הוא אזורי ותומך רק בבק-אנד של אשכולות באותו אזור.

  3. פורסים את Service ואת ServiceExports שנדרשים בכל אשכול. אם פרסתם את Services ו-ServiceExports מהדוגמה הקודמת, כבר פרסתם חלק מהם.

    kubectl apply --context gke-west-1 -f https://raw.githubusercontent.com/GoogleCloudPlatform/gke-networking-recipes/main/gateway/gke-gateway-controller/multi-cluster-gateway/store-west-1-service.yaml
    kubectl apply --context gke-west-2 -f https://raw.githubusercontent.com/GoogleCloudPlatform/gke-networking-recipes/main/gateway/gke-gateway-controller/multi-cluster-gateway/store-west-2-service.yaml
    

    הוא פורס קבוצה דומה של משאבים לכל אשכול:

    service/store created
    serviceexport.net.gke.io/store created
    service/store-west-2 created
    serviceexport.net.gke.io/store-west-2 created
    

הגדרת רשת משנה לשרת proxy בלבד

אם עדיין לא עשיתם זאת, הגדירו רשת משנה של שרתי proxy בלבד לכל אזור שבו אתם פורסים שערים פנימיים. תת-הרשת הזו משמשת להקצאת כתובות IP פנימיות לשרתי ה-proxy של מאזן העומסים, וחובה להגדיר אותה עם --purpose שמוגדר ל-REGIONAL_MANAGED_PROXY בלבד.

לפני שיוצרים שערים שמנהלים מאזני עומסים של אפליקציות (ALB) פנימיים, צריך ליצור רשת משנה של פרוקסי בלבד. לכל אזור ברשת של ענן וירטואלי פרטי (VPC) שבו אתם משתמשים במאזני עומסים של אפליקציות (ALB) פנימיים, צריכה להיות תת-רשת של פרוקסי בלבד.

הפקודה gcloud compute networks subnets create יוצרת רשת משנה של שרתי proxy בלבד.

gcloud compute networks subnets create SUBNET_NAME \
    --purpose=REGIONAL_MANAGED_PROXY \
    --role=ACTIVE \
    --region=REGION \
    --network=VPC_NETWORK_NAME \
    --range=CIDR_RANGE

מחליפים את מה שכתוב בשדות הבאים:

  • ‫SUBNET_NAME: השם של רשת המשנה שמשמשת רק כשרת proxy.
  • ‫REGION: האזור של רשת המשנה שמשמשת רק לשרתי proxy.
  • ‫VPC_NETWORK_NAME: השם של רשת ה-VPC שמכילה את תת-הרשת.
  • ‫CIDR_RANGE: טווח כתובות ה-IP הראשי של רשת המשנה. צריך להשתמש במסכה של רשת משנה בגודל של /26 לפחות, כדי שיהיו לפחות 64 כתובות IP זמינות לשרתי proxy באזור. מסכה של רשת משנה המומלצת היא /23.

פריסת השער

השער הבא נוצר מ-gke-l7-rilb-mc GatewayClass, שהוא שער פנימי אזורי שיכול לטרגט רק אשכולות GKE באותו אזור.

  1. מחילים את מניפסט Gateway הבא על אשכול ההגדרות, gke-west-1 בדוגמה הזו:

    cat << EOF | kubectl apply --context gke-west-1 -f -
    kind: Gateway
    apiVersion: gateway.networking.k8s.io/v1
    metadata:
      name: internal-http
      namespace: store
    spec:
      gatewayClassName: gke-l7-rilb-mc
      listeners:
      - name: http
        protocol: HTTP
        port: 80
        allowedRoutes:
          kinds:
          - kind: HTTPRoute
    EOF
    
  2. מוודאים שהשער פועל. כדי לסנן רק את האירועים מהשער הזה, משתמשים בפקודה הבאה:

    kubectl get events --field-selector involvedObject.kind=Gateway,involvedObject.name=internal-http --context=gke-west-1 --namespace store
    

    אם הפלט דומה לזה שמופיע בהמשך, סימן שהפריסה של שער הייתה מוצלחת:

    LAST SEEN   TYPE     REASON   OBJECT                  MESSAGE
    5m18s       Normal   ADD      gateway/internal-http   store/internal-http
    3m44s       Normal   UPDATE   gateway/internal-http   store/internal-http
    3m9s        Normal   SYNC     gateway/internal-http   SYNC on store/internal-http was a success
    

קנרי מבוסס-כותרת

בדיקות קנרית שמבוססות על כותרות מאפשרות לבעלי השירות להתאים תנועה של בדיקות סינתטיות שלא מגיעה ממשתמשים אמיתיים. זו דרך קלה לוודא שהרשת הבסיסית של האפליקציה פועלת בלי לחשוף את המשתמשים באופן ישיר.

  1. מחילים את מניפסט HTTPRoute הבא על אשכול ההגדרות, gke-west-1 בדוגמה הזו:

    cat << EOF | kubectl apply --context gke-west-1 -f -
    kind: HTTPRoute
    apiVersion: gateway.networking.k8s.io/v1
    metadata:
      name: internal-store-route
      namespace: store
      labels:
        gateway: internal-http
    spec:
      parentRefs:
      - kind: Gateway
        namespace: store
        name: internal-http
      hostnames:
      - "store.example.internal"
      rules:
      # Matches for env=canary and sends it to store-west-2 ServiceImport
      - matches:
        - headers:
          - name: env
            value: canary
        backendRefs:
          - group: net.gke.io
            kind: ServiceImport
            name: store-west-2
            port: 8080
      # All other traffic goes to store-west-1 ServiceImport
      - backendRefs:
        - group: net.gke.io
          kind: ServiceImport
          name: store-west-1
          port: 8080
    EOF
    

    אחרי הפריסה, קובץ ה-HTTPRoute מגדיר את התנהגות הניתוב הבאה:

    • בקשות פנימיות אל store.example.internal ללא כותרת ה-HTTP‏ env: canary מנותבות לפודים של store באשכול gke-west-1
    • בקשות פנימיות אל store.example.internal עם כותרת ה-HTTP‏ env: canary מנותבות אל תרמילי store ב-cluster‏ gke-west-2

    ה-HTTPRoute מאפשר ניתוב לאשכולות שונים על סמך כותרות ה-HTTP

    כדי לוודא ש-HTTPRoute פועל בצורה תקינה, שולחים תנועה לכתובת ה-IP של Gateway.

  2. מאחזרים את כתובת ה-IP הפנימית מ-internal-http.

    kubectl get gateways.gateway.networking.k8s.io internal-http -o=jsonpath="{.status.addresses[0].value}" --context gke-west-1 --namespace store
    

    מחליפים את VIP בשלבים הבאים בכתובת ה-IP שמתקבלת כפלט.

  3. שולחים בקשה ל-Gateway באמצעות כותרת ה-HTTP ‏env: canary. כך תוכלו לוודא שהתנועה מנותבת אל gke-west-2. כדי לוודא שהבקשות מנותבות בצורה נכונה, משתמשים בלקוח פרטי באותו VPC כמו אשכולות GKE. צריך להריץ את הפקודה הבאה במכונה שיש לה גישה פרטית לכתובת ה-IP של השער, אחרת היא לא תפעל.

    curl -H "host: store.example.internal" -H "env: canary" http://VIP
    

    הפלט מאשר שהבקשה הוגשה על ידי Pod מאשכול gke-west-2:

    {
        "cluster_name": "gke-west-2", 
        "host_header": "store.example.internal",
        "node_name": "gke-gke-west-2-default-pool-4cde1f72-m82p.c.agmsb-k8s.internal",
        "pod_name": "store-5f5b954888-9kdb5",
        "pod_name_emoji": "😂",
        "project_id": "agmsb-k8s",
        "timestamp": "2021-05-31T01:21:55",
        "zone": "us-west1-a"
    }
    

שיקוף תנועה

בשלב הזה, תעבורת הנתונים נשלחת לאשכול המיועד, אבל גם משוכפלת לאשכול ה-Canary.

שיקוף תנועה עוזר לקבוע איך עומס התנועה ישפיע על ביצועי האפליקציה, בלי להשפיע על התגובות ללקוחות. יכול להיות שלא תצטרכו להשתמש בזה בכל סוגי ההשקות, אבל זה יכול להיות שימושי כשמשיקים שינויים גדולים שיכולים להשפיע על הביצועים או על העומס.

  1. מחילים את המניפסט הבא של HTTPRoute על אשכול ההגדרות, gke-west-1 בדוגמה הזו:

    cat << EOF | kubectl apply --context gke-west-1 -f -
    kind: HTTPRoute
    apiVersion: gateway.networking.k8s.io/v1
    metadata:
      name: internal-store-route
      namespace: store
      labels:
        gateway: internal-http
    spec:
      parentRefs:
      - kind: Gateway
        namespace: store
        name: internal-http
      hostnames:
      - "store.example.internal"
      rules:
      # Sends all traffic to store-west-1 ServiceImport
      - backendRefs:
        - name: store-west-1
          group: net.gke.io
          kind: ServiceImport
          port: 8080
        # Also mirrors all traffic to store-west-2 ServiceImport
        filters:
        - type: RequestMirror
          requestMirror:
            backendRef:
              group: net.gke.io
              kind: ServiceImport
              name: store-west-2
              port: 8080
    EOF
    
  2. באמצעות הלקוח הפרטי, שולחים בקשה אל internal-http Gateway. משתמשים בנתיב /mirror כדי לזהות באופן ייחודי את הבקשה הזו ביומני האפליקציה בשלב מאוחר יותר.

    curl -H "host: store.example.internal" http://VIP/mirror
    
  3. הפלט מאשר שהלקוח קיבל תשובה מ-Pod באשכול gke-west-1:

    {
        "cluster_name": "gke-west-1", 
        "host_header": "store.example.internal",
        "node_name": "gke-gke-west-1-default-pool-65059399-ssfq.c.agmsb-k8s.internal",
        "pod_name": "store-5f5b954888-brg5w",
        "pod_name_emoji": "🎖",
        "project_id": "agmsb-k8s",
        "timestamp": "2021-05-31T01:24:51",
        "zone": "us-west1-a"
    }
    

    כך תוכלו לוודא שהאשכול הראשי מגיב לתנועה. עדיין צריך לוודא שהאשכול שאליו אתם מעבירים את הנתונים מקבל תנועה משוכפלת.

  4. בודקים את יומני האפליקציות של Pod‏ store באשכול gke-west-2. היומנים צריכים לאשר שה-Pod קיבל תנועה משוכפלת ממאזן העומסים.

    kubectl logs deployment/store --context gke-west-2 -n store | grep /mirror
    
  5. הפלט הזה מאשר שרכיבי ה-Pod באשכול gke-west-2 מקבלים גם הם את אותן בקשות, אבל התשובות שלהם לבקשות האלה לא נשלחות חזרה ללקוח. כתובות ה-IP שמופיעות ביומנים הן כתובות ה-IP הפנימיות של מאזן העומסים, שמתקשרות עם ה-Pods שלכם.

    Found 2 pods, using pod/store-5c65bdf74f-vpqbs
    [2023-10-12 21:05:20,805] INFO in _internal: 192.168.21.3 - - [12/Oct/2023 21:05:20] "GET /mirror HTTP/1.1" 200 -
    [2023-10-12 21:05:27,158] INFO in _internal: 192.168.21.3 - - [12/Oct/2023 21:05:27] "GET /mirror HTTP/1.1" 200 -
    [2023-10-12 21:05:27,805] INFO in _internal: 192.168.21.3 - - [12/Oct/2023 21:05:27] "GET /mirror HTTP/1.1" 200 -
    

חלוקת התנועה

פיצול תנועה הוא אחת מהשיטות הנפוצות ביותר להשקת קוד חדש או לפריסה בסביבות חדשות בצורה בטוחה. בעל השירות מגדיר אחוז תנועה מפורש שנשלח לשרתי הקצה של קנרי. בדרך כלל מדובר בכמות קטנה מאוד מכלל התנועה, כדי שאפשר יהיה לקבוע את הצלחת ההשקה עם סיכון מקובל לבקשות של משתמשים אמיתיים.

פיצול תעבורה עם חלק קטן מהתעבורה מאפשר לבעלי השירות לבדוק את תקינות האפליקציה ואת התגובות. אם כל האותות נראים תקינים, אפשר להמשיך למעבר המלא.

  1. מחילים את מניפסט HTTPRoute הבא על אשכול ההגדרות, gke-west-1 בדוגמה הזו:

    cat << EOF | kubectl apply --context gke-west-1 -f -
    kind: HTTPRoute
    apiVersion: gateway.networking.k8s.io/v1
    metadata:
      name: internal-store-route
      namespace: store
      labels:
        gateway: internal-http
    spec:
      parentRefs:
      - kind: Gateway
        namespace: store
        name: internal-http
      hostnames:
      - "store.example.internal"
      rules:
      - backendRefs:
        # 90% of traffic to store-west-1 ServiceImport
        - name: store-west-1
          group: net.gke.io
          kind: ServiceImport
          port: 8080
          weight: 90
        # 10% of traffic to store-west-2 ServiceImport
        - name: store-west-2
          group: net.gke.io
          kind: ServiceImport
          port: 8080
          weight: 10
    EOF
    
  2. באמצעות הלקוח הפרטי, שולחים בקשת curl רציפה לשער של internal- http.

    while true; do curl -H "host: store.example.internal" -s VIP | grep "cluster_name"; sleep 1; done
    

    הפלט יהיה דומה לזה, ויציין שמתרחשת חלוקת תנועה של 90/10.

    "cluster_name": "gke-west-1",
    "cluster_name": "gke-west-1",
    "cluster_name": "gke-west-1",
    "cluster_name": "gke-west-1",
    "cluster_name": "gke-west-1",
    "cluster_name": "gke-west-1",
    "cluster_name": "gke-west-1",
    "cluster_name": "gke-west-1",
    "cluster_name": "gke-west-2",
    "cluster_name": "gke-west-1",
    "cluster_name": "gke-west-1",
    ...
    

העברת התנועה

השלב האחרון בהעברה מסוג blue-green הוא מעבר מלא לאשכול החדש והסרת האשכול הישן. אם בעלי השירות היו מוסיפים אשכול שני לאשכול קיים, השלב האחרון היה שונה כי התנועה הייתה מגיעה לשני האשכולות. במקרה כזה, מומלץ להשתמש ב-ServiceImportstore יחיד שיש לו Pods משני האשכולות gke-west-1 ו-gke-west-2. כך מאזן העומסים יכול להחליט לאן להפנות את התנועה באפליקציה פעילה-פעילה, על סמך הקרבה, התקינות והקיבולת.

  1. מחילים את המניפסט הבא של HTTPRoute על אשכול ההגדרות, gke-west-1 בדוגמה הזו:

    cat << EOF | kubectl apply --context gke-west-1 -f -
    kind: HTTPRoute
    apiVersion: gateway.networking.k8s.io/v1
    metadata:
      name: internal-store-route
      namespace: store
      labels:
        gateway: internal-http
    spec:
      parentRefs:
      - kind: Gateway
        namespace: store
        name: internal-http
      hostnames:
      - "store.example.internal"
      rules:
        - backendRefs:
          # No traffic to the store-west-1 ServiceImport
          - name: store-west-1
            group: net.gke.io
            kind: ServiceImport
            port: 8080
            weight: 0
          # All traffic to the store-west-2 ServiceImport
          - name: store-west-2
            group: net.gke.io
            kind: ServiceImport
            port: 8080
            weight: 100
    EOF
    
  2. באמצעות הלקוח הפרטי, שולחים בקשת curl רציפה לשער של internal- http.

    while true; do curl -H "host: store.example.internal" -s VIP | grep "cluster_name"; sleep 1; done
    

    הפלט שיתקבל יהיה דומה לזה, ויציין שכל התנועה מועברת עכשיו אל gke-west-2.

    "cluster_name": "gke-west-2",
    "cluster_name": "gke-west-2",
    "cluster_name": "gke-west-2",
    "cluster_name": "gke-west-2",
    ...
    

השלב האחרון הזה משלים העברה מלאה של אפליקציה כחולה-ירוקה מאשכול GKE אחד לאשכול GKE אחר.

הסרת המשאבים

אחרי שמסיימים את התרגילים במסמך הזה, צריך לבצע את השלבים הבאים כדי להסיר משאבים ולמנוע חיובים לא רצויים בחשבון:

  1. מחיקת האשכולות.

  2. ביטול הרישום של האשכולות ב-Fleet אם אין צורך לרשום אותם למטרה אחרת.

  3. השבתת התכונה multiclusterservicediscovery:

    gcloud container fleet multi-cluster-services disable
    
  4. השבתת Multi Cluster Ingress:

    gcloud container fleet ingress disable
    
  5. משביתים את ממשקי ה-API:

    gcloud services disable \
        multiclusterservicediscovery.googleapis.com \
        multiclusteringress.googleapis.com \
        trafficdirector.googleapis.com \
        --project=PROJECT_ID
    

פתרון בעיות

אין שרת upstream תקין

הסימפטום:

הבעיה הבאה יכולה להתרחש כשיוצרים שער אבל לא מצליחים לגשת לשירותים לקצה העורפי (קוד תגובה 503):

no healthy upstream

הסיבה:

הודעת השגיאה הזו מציינת שכלי הבדיקה של בדיקת תקינות לא יכול למצוא שירותי קצה עורפיים תקינים. יכול להיות שהשירותים שלכם בבק-אנד תקינים, אבל תצטרכו להתאים אישית את בדיקות התקינות.

פתרון עקיף:

כדי לפתור את הבעיה, התאימו אישית את בדיקת התקינות בהתאם לדרישות של האפליקציה (לדוגמה, /health) באמצעות HealthCheckPolicy.

המאמרים הבאים