工程師與貓
ESC
Content
    ↑↓ navigate open esc close
    Published on

    ALB 又回 502 了:先別急著加機器,log 會告訴你是哪一種

    Authors
    • avatar
      Name
      Alex Yu

    前陣子監控又跳出 ALB 的 502/504:59 筆,90 秒後自動恢復。上一次看到 5xx 是爬蟲把 search 頁打爆那次——7,262 筆,錯誤率一度衝到 50%。

    兩次的 alert 在 dashboard 上長得一模一樣。但一種加機器有用,另一種加了純浪費錢。

    分辨方法藏在 ALB access log 的兩個欄位裡,30 秒就能判完。

    同一套環境,我遇過三種 ALB 5xx

    同樣的 K8s + ALB,三種完全不同的成因:

    1. 健檢誤殺:app 自己的 bug 害 readiness 失敗,Pod 被 ALB 判死——IntersectionObserver 跑進 server side 那次
    2. 容量過載:流量真的把所有 Pod 打爆——search 頁被爬蟲打掛那次
    3. Pod 輪替的 race condition:Pod 下線和 ALB 停止導流沒對上時間——這篇要講的

    前兩種寫過了,這篇補第三種,順便整理一張三種通用的判讀表。

    先看 target_ip:ALB 把 request 送去哪了

    ALB 的 access log 每一行都記著這個 request 被送到哪個目標(target_ip),以及對方花了多久處理(target_processing_time-1 代表連線根本沒建立、或沒等到回應)。出 5xx 的時候,這兩個欄位會透露 ALB 當下的處境:

    # 容量型:ALB 手上找不到任何健康的目標,request 連送都沒送出去
    elb_status_code=503  target_ip="-"        target_processing_time=-1
     
    # Pod 輪替型:ALB 有明確目標,但對方正在關門、拒接
    elb_status_code=502  target_ip="10.x.x.x" target_processing_time=-1

    再搭配量和發生時間:

    log 特徵代表什麼該查什麼
    量大、錯誤率高、target_ip="-"沒有健康的 Pod 可送(過載或健檢全掛)容量、健檢為什麼失敗
    量少、集中在部署或 scale-in 的時間點、target_ip 集中在特定幾個 Pod流量被送給關閉中的 Podgraceful shutdown 設定
    平時零星出現、跟著特定 endpoint 走app 層 bug 拖垮健檢應用程式本身

    這次的 59 筆對著表看:全部指向同兩個 Pod、發生在 Pod 輪替當下、同時段其他 8 個 Pod 零錯誤、90 秒自動恢復。教科書等級的 Pod 輪替型——加機器?其他 Pod 明明閒著。

    為什麼 Pod 輪替會掉 502

    K8s 關 Pod 有個流程:先送一個叫 SIGTERM 的訊號給程式,意思是「準備收工,把手上的事收尾」。大多數 web server 收到後會立刻拒收新連線,跑完手上的 request 就退出。

    問題是 ALB 不知道這件事:

    Pod 收到 SIGTERM → 拒收新連線
          ↓(同一時刻)
    ALB 的導流名單還沒更新 → 新 request 照送
    
    connection refused → 502/504

    K8s 和 ALB 是兩套獨立系統,「這個 Pod 要下線了」從 K8s 傳到 ALB 需要時間。空窗期內送過去的 request,全部變 502。

    白話版:店員到點下班先鎖了門,但門口的取號機還在發號碼牌。

    修法:先停止被導流,再真正退出

    這套做法叫 graceful shutdown。關鍵是 K8s 和 ALB 兩端各設一半,只設一邊等於沒設。

    Pod 端preStop hook——K8s 送出 SIGTERM 之前,會先執行你指定的指令,跑完才真的通知程式關閉。設定寫在 Deployment 裡:

    apiVersion: apps/v1
    kind: Deployment
    spec:
      template:
        spec:
          # ↓ 整個關閉流程的時限(含 preStop 的 30 秒)。超時 K8s 直接 SIGKILL
          #   強制終止,所以這個值必須大於 sleep
          terminationGracePeriodSeconds: 40
          containers:
            - name: web
              lifecycle:
                preStop:
                  exec: # ← SIGTERM 之前先跑這段
                    command: ['sh', '-c', 'sleep 30'] # ← 掛 30 秒等 ALB 把流量排開

    ALB 端——ALB 移除一個目標前會先進入排空(draining)階段:新 request 不再送過去,處理中的讓它跑完。願意等多久,這就是 target group 的 deregistration_delay(預設 300 秒)。用 AWS Load Balancer Controller 的話,這個值寫在 Ingress 的 annotation 上:

    apiVersion: networking.k8s.io/v1
    kind: Ingress
    metadata:
      annotations:
        # ↓ 排空最多等 30 秒,跟 preStop 的 sleep 對齊
        alb.ingress.kubernetes.io/target-group-attributes: deregistration_delay.timeout_seconds=30

    這個值要大於等於 preStop 的 sleep。Pod 撐著不關的時間就是留給 ALB 排空用的,排空時間比 sleep 短的話,還沒跑完的連線會在 Pod 明明活著時就被切斷,前面的 sleep 等於白設了。

    三個值的關係,帶上這次的設定:

    preStop sleep (30) ≤ deregistration_delay (30) < terminationGracePeriodSeconds (40)

    最後加一道保險。K8s 有個資源叫 PodDisruptionBudget(PDB),用來規定「同一時間最多允許幾個 Pod 下線」——node 回收、kubectl drain 這類要趕走 Pod 的操作,動手前都得先過這條限制。設了它,就不會一次把半數 Pod 同時帶進上面說的空窗期:

    apiVersion: policy/v1
    kind: PodDisruptionBudget
    metadata:
      name: web-pdb
    spec:
      maxUnavailable: 1 # ← 同一時間最多允許 1 個 Pod 下線
      selector:
        matchLabels:
          app: web # ← 套用在哪一群 Pod 上

    順手把探針分層

    K8s 的探針(probe)是定期戳 Pod 的健康檢查,三種各管一件事:liveness 問「還活著嗎」(失敗直接重啟 Pod)、readiness 問「現在能接流量嗎」(失敗只是暫時移出導流名單)、startup 問「開好機了嗎」(通過之前另外兩個不會開始戳)。

    這次順手調了兩件事:

    • liveness 改淺層(nginx 自己回 200 就好)、readiness 維持深層(真的去戳應用)
    • startupProbe 給冷啟動 90 秒預算,避免 Pod 還在暖機就被判不健康

    分層的理由是「忙」不該被判成「死」。過載時 app 回應變慢,深層的 liveness 也會跟著 timeout——K8s 一判定失敗就重啟 Pod,容量瞬間又少一台,剩下的 Pod 分到更多流量、變得更慢、更容易被判死。本來只是忙不過來,被 liveness 重啟一輪之後,變成真的全掛。

    還有一個試了又拆的設定:slow_start。它跟 deregistration_delay 一樣是 target group 的屬性(寫在同一個 annotation 裡),作用是新 Pod 進導流名單後,流量從低漸進拉升,避免一進來就分到滿載。排查時順手加了,但它防的情境——Pod 還沒暖好就被打滿——startupProbe 加 readiness 已經擋掉了,留著只是多一個要對齊的參數,後來便拆了。

    「90 秒自動恢復」不是沒事

    這種小事故最容易被當雜訊:量少、自動恢復、dashboard 上一個小小的 spike。但它的觸發條件是 Pod 輪替——rollout、scale-in、node 回收,每一次都會重演。

    也就是說,只要有部署,就固定有一批使用者的 request 會失敗:這次是 59 筆,等於 90 秒內有 59 次操作直接看到錯誤畫面。不會多花錢、機器也不會變慢,但這個數字會跟著部署次數一直累積。花一次功夫把 graceful shutdown 對齊,之後每一次部署都少掉這批錯誤。

    重點整理

    • ALB 5xx 先看 target_ip"-" 是找不到人送(容量或健檢),具體 IP 是送錯人(Pod 輪替)
    • graceful shutdown 要兩端對齊:preStop sleep ≤ deregistration_delay < terminationGracePeriodSeconds,只設一邊等於沒設
    • liveness 淺、readiness 深——「忙」和「死」分開判
    • 自動恢復的零星 5xx 不是雜訊,每次部署都會重演一輪

    延伸閱讀

    同一套環境的另外兩種 5xx:

    如果上面那張表判出來是容量型,下一步:

    參考資料