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

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

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

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

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

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

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

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

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

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

ניתוב כחול-ירוק בין כמה אשכולות באמצעות 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%-Mirror traffic: Mirror user traffic to the canary cluster. בבדיקה הזו, המערכת מעתיקה 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. פורסים את השירות ואת 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 בלבד

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

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

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

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 הבא על אשכול ההגדרות, 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 הבא על אשכול ההגדרות, 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"
    }
    

שיקוף תנועה

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

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

  1. מחילים את HTTPRouteהמניפסטgke-west-1 הבא על אשכול ההגדרות, 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 -
    

חלוקת התנועה

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

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

  1. מחילים את HTTPRouteהמניפסטgke-west-1 הבא על אשכול ההגדרות, 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 הוא מעבר מלא לאשכול החדש והסרת האשכול הישן. אם בעל השירות באמת מפעיל אשכול שני באשכול קיים, השלב האחרון יהיה שונה כי התנועה תעבור לשני האשכולות. במקרה כזה, מומלץ להשתמש ב-store ServiceImport יחיד עם Pods מאשכולות gke-west-1 ו-gke-west-2. כך מאזן העומסים יכול להחליט לאן התנועה צריכה להגיע באפליקציה פעילה-פעילה, על סמך הקרבה, התקינות והקיבולת.

  1. מחילים את HTTPRouteהמניפסטgke-west-1 הבא על אשכול ההגדרות, 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",
    ...
    

בשלב האחרון הזה משלימים העברה מלאה של אפליקציה בשיטת blue-green מאשכול 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
    

פתרון בעיות

אין מקור תקין

התסמין:

יכולה להתרחש הבעיה הבאה כשיוצרים Gateway אבל אין גישה לשירותי ה-Backend (קוד תגובה 503):

no healthy upstream

הסיבה:

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

פתרון עקיף:

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

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