這一篇拿 4.0 服務端監控的分層 的監控鏈盤點一個跑在託管 Kubernetes 上的電商 checkout:把它現有的每一種監控放到所屬的層,逐層判讀還開著的缺口是哪一種、補它要加什麼。盤點用到四種缺口:檢查範圍盲區是沒有觀察者檢查那個對象,位置盲區是觀察者跟被觀察的元件共用一個依賴、那個依賴停掉時一起停,讀取缺口是觀察者有結果而沒有規則讀它、或通知送不出去,訊號失真是觀察者自己壞掉沒人知道、或規則的寫法讓結果不可信。完整的定義與例子在 4.0 的〈一層監控檢查的對象與它的缺口〉,盤點時的判讀依據在 4.0 的〈以監控鏈盤點一個服務的判讀依據〉。服務的功能與需求見 Backend 範例系統 的〈電商 checkout〉;對照的案例是 4.0.2 以監控鏈盤點單機短網址服務,那個服務的觀察者全在一台主機上,沒有佇列也沒有第三方;撮合引擎單一執行緒、觀察者要從輸出日誌讀數字的系統,見 4.0.4 以監控鏈盤點交易撮合系統。

checkout 的元件與各自的失效域

這個 checkout 跑在一個雲端帳號、一個區域裡的託管 Kubernetes 叢集上。叢集裡有商品、訂單與付款三個服務,Redis 快取商品與價格,RabbitMQ 連接訂單服務與三個消費端(寄確認信、建立出貨單、更新報表);訂單存放在同一個區域的託管 PostgreSQL。付款服務呼叫第三方金流建立付款,金流業者在付款完成時對付款服務的回呼端點送出通知;寄確認信的消費端呼叫第三方寄信業者。

這些元件分屬不同的失效域。叢集裡的服務、Redis、RabbitMQ 與跑在叢集裡的監控,共用叢集的控制平面、節點與叢集網路,叢集出事時一起停。託管 PostgreSQL 與雲端業者的監控不在叢集裡,叢集停掉時這兩者還在,而它們與叢集共用同一個雲端帳號與區域:帳號被停權或區域故障時,兩者一起受影響。金流業者與寄信業者在服務的失效域以外,這兩家停掉時服務照常運作,只是付款或寄信失敗。所以這個服務的位置盲區要對三種失效分開判:叢集停掉、區域或帳號出事、第三方業者停掉。

checkout 現有的監控與各層的判讀

範例假設這個 checkout 接了以下監控(範例系統的設定,不是實測):每個服務的 liveness 與 readiness 探針(付款服務的 readiness 會呼叫金流業者的 API 確認連得到);跑在叢集裡的 kube-prometheus-stack(把 Prometheus、Alertmanager、kube-state-metrics、node_exporter 與一組預設告警規則一起安裝的 Helm chart),另加 Redis 與 RabbitMQ 的 exporter,以及一條以佇列訊息筆數的固定門檻寫的告警;blackbox_exporter 對每個服務的 /healthz 探測;服務程式輸出每個端點的請求指標,例外送到 Sentry;Alertmanager 依嚴重度送到值班的推播服務與聊天頻道;託管 PostgreSQL 由雲端業者的監控量測,業者的告警寄到團隊的共用信箱;一個第三方的合成監控服務每分鐘請求首頁、檢查狀態碼。各層的判讀:

監控鏈的層這個服務現有的監控判讀
容器與程序的自檢每個服務的 liveness 與 readiness;chart 內建的 KubePodCrashLooping 與 KubePodNotReady 規則讀取缺口(部分):持續 15 分鐘的 CrashLoopBackOff(容器反覆啟動失敗、kubelet 拉長重啟間隔的狀態)與持續 15 分鐘沒有就緒(含停在 Pending、Unknown)的 Pod 會送出,間隔較長的偶發重啟與反覆上線、下線的 Pod 不會;付款服務的 readiness 設計另見下文
服務網路內的探測blackbox_exporter 對各服務的 /healthz檢查範圍盲區:只確認各服務回應健康端點,「放進購物車、建立訂單、取得付款頁」這條下單路徑沒有任何探測
元件指標節點、容器、Redis、RabbitMQ 的 exporter;託管 PostgreSQL 由業者監控訊號失真:佇列的告警以訊息筆數的固定門檻寫;讀取缺口:資料庫的業者告警寄到沒有人值班看的共用信箱
應用層訊號每個端點的請求指標、Sentry檢查範圍盲區:對金流與寄信業者的呼叫沒有逐依賴記錄成功率與耗時,付款失敗時分不出是金流還是自己
告警規則與分送Prometheus 規則與 Alertmanager,送到值班推播與聊天頻道訊號失真:錯誤率的規則以固定門檻寫,門檻停在一個與 SLO 無關的數字,下單與付款這兩條有 SLO 的路徑還沒有以錯誤預算消耗的速度寫的規則(寫法見 4.6 SLI 量測與 SLO 訊號設計)
告警鏈的監看無心跳監看服務;chart 內建的 Watchdog 規則一直成立,預設路由把它送到不通知任何人的接收端檢查範圍盲區:沒有心跳監看服務;補的心跳監看服務要放在叢集與雲端帳號以外,放在叢集裡的話,叢集停掉時它跟告警鏈一起停
使用者路徑的合成監控第三方合成監控服務,每分鐘請求首頁、只檢查狀態碼檢查範圍盲區:首頁不經過下單與付款,回應內容也不比對;位置已在失效域以外

容器與程序的自檢、服務網路內的探測、元件指標、應用層訊號、告警規則與分送這五列的缺口,補的是規則、指標與探測的對象;告警鏈的監看那一列,補的是一個位置在叢集與帳號以外的觀察者。偶發的重啟另以 increase(kube_pod_container_status_restarts_total[1h]) 這類以重啟次數寫的規則讀取,反覆上線、下線的 Pod 以 readiness 狀態在一段時間內的變化次數讀取(changes(kube_pod_status_ready{condition="true"}[30m]))。服務網路內的探測要加一條走下單路徑的合成交易:以一個測試帳號與一個測試商品建立訂單、取得付款頁為止,不實際付款,付款那一段改由金流業者的沙箱環境另外探測;測試訂單要能從報表與出貨裡排除,否則合成交易若每分鐘跑一次,就是每分鐘一筆出貨單。佇列的告警改以等待時間寫,因為訊息筆數的正常範圍隨流量變化,促銷時的正常筆數會讓固定門檻一直成立,平常的離峰時段卻可能積了一小時的訊息還在門檻以下;佇列沒有直接提供最舊一筆的等待時間時,以筆數除以消費端每秒處理的筆數估算排空所需的時間(它是估計值,不等於最舊一筆實際等了多久,lag 的意義在 Consumer Lag);消費端停掉時處理速率是零,這個估算失去意義,所以另寫一條「消費速率為零而筆數大於零,持續幾分鐘」的規則。每個消費端的等待時間門檻要多長,屬於下文〈排程工作與背景工作〉的業務預期。資料庫的業者告警改送到值班會收到的管道:雲端業者的告警多半能送到推播服務或 webhook。叢集內的 Alertmanager 與叢集共用失效域,而雲端業者對資料庫的監控是叢集停掉時少數還在運作的觀察者;把雲端業者的資料庫告警改送到叢集內的 Alertmanager,叢集停掉時這些告警也送不出去,所以直接送值班的推播服務。對外依賴的呼叫在付款服務與寄信的消費端各自記成請求指標,以依賴名稱當標籤,失敗率升高時就知道是哪一個依賴。

告警鏈的監看這一層,觀察者要放到叢集以外,放在哪裡決定它接得住哪幾種失效。Watchdog 規則已經在,通知送失敗的規則 chart 也附了(AlertmanagerFailedToSendAlerts 與 critical 的 AlertmanagerClusterFailedToSendAlerts,都以 alertmanager_notifications_failed_total 寫成),缺的是路由:一條子路由把 Watchdog 定期送到失效域以外的心跳監看服務,另一條子路由把兩條送失敗規則送到同一個心跳監看服務的另一個檢查項目,這條子路由的 matchers 要同時接得到兩條規則的名稱;只寫了 AlertmanagerFailedToSendAlerts 時,AlertmanagerClusterFailedToSendAlerts 不會被這條子路由接走,會照原本的路由走值班的推播服務,而那正是可能送不出去的那一條管道。心跳監看服務若跑在同一個雲端帳號的另一個區域,區域故障時它還在,帳號被停權時它一起停;用第三方的心跳監看服務則兩種都不受影響。心跳與送失敗的路由寫法見 第三方心跳監看與外部探測服務,chart 預設的路由與兩條送失敗規則的 matchers 見 在 Kubernetes 上接上監控 的〈chart 預設的 Alertmanager 路由:不送任何通知〉。

使用者路徑那一層的觀察者位置已經對了,補的是它檢查的對象:把首頁換成下單路徑的合成交易,並比對回應內容,例如付款頁回應裡有沒有金流業者的付款表單。同一個腳本從叢集內再跑一次,以 Service 名稱直接打訂單與付款服務、不經過入口控制器;兩個位置的同一條探測並排,就能分辨失效落在公開入口還是服務本身,判讀方式見 4.0 的〈使用者路徑的合成監控〉。

付款服務的 readiness 納入金流業者時的後果

付款服務的 readiness 呼叫金流業者的 API,這個設計讓一次第三方故障擴大成兩個故障:付款服務整個下線,以及金流的付款完成回呼送不進來。金流業者的 API 從叢集連不到、或回應慢於探針的逾時時,每一個付款服務的 Pod 都判定自己沒準備好,Kubernetes 把它們全部從 Service 的端點名單拿掉,付款服務不再收到任何請求。使用者送出付款時,入口回的是沒有可用後端的錯誤,而不是付款服務自己寫的「付款暫時無法處理,訂單已保留」;更嚴重的是,付款服務同時也是接收金流回呼的端點,在這段期間金流業者若仍在送回呼(金流的 API 慢、而它的回呼系統正常時就是這樣),付款完成通知全部送不進來,訂單停在待付款,要等金流業者重送回呼,或由對帳找回。金流的 API 時好時壞時,探針反覆通過又失敗,付款服務也跟著反覆上線、下線。

決定一個依賴要不要放進 readiness,判斷標準是這個依賴停掉時,付款服務還能不能做出有用的回應。資料庫停掉時,付款服務連訂單都讀不到、回呼也記不下來,所有副本下線之後由入口回錯誤,金流業者之後重送回呼,資料庫是必要依賴,放進 readiness;金流業者停掉時,付款服務仍然能回應使用者「付款暫時無法處理,訂單已保留」、接收回呼、把訂單標成待付款,在 Liveness 與 Readiness 的依賴分類裡屬於可降級依賴,不放進 readiness,改在呼叫端以 Circuit Breaker 在金流連續失敗時直接回那個訊息、不再等逾時。兩種依賴都是所有副本共用的,共用只決定後果有多大:共用的依賴一停,放進 readiness 的話全部副本同時下線。Kubernetes 的文件把「確認必要的後端服務可用」列為 readiness 的用途,說的是前一種依賴。只有部分副本連不到依賴時(例如某一個可用區的對外網路出口壞掉),讓那幾個副本下線、其餘的接手是對的,但剩下的副本要承接全部流量,容量不夠時它們接著變慢、逾時;這種部分路徑的失效由入口或服務網格的異常偵測(outlier detection)把流量移開,不必靠 readiness。付款服務也可以把接收回呼的端點拆成另一個 Deployment,付款 API 下線時回呼照收,兩個故障就分開了。盤點時要看的「readiness 檢查了什麼」是探針打到的那個端點實際做了什麼:框架預設的健康端點會自動納入它偵測到的依賴,例如 Spring Boot Actuator 的 /actuator/health 依 classpath 納入資料庫、Redis 的檢查(專供探針用的 /actuator/health/readiness 預設不納入,探針打哪一個端點決定結果),依賴變慢到超過探針的逾時,結果與依賴停掉相同。金流業者的狀態則由逐依賴的請求指標與金流業者的狀態頁監看。這一項是探針本身把一個外部的故障轉成了服務自己的故障,與哪一層少了觀察者無關,所以盤點時要逐個看 readiness 與 liveness 檢查了什麼。

監控鏈事前偵測不到的異常:逐類查 checkout

4.0.1 監控鏈事前偵測不到的異常 那幾類,這個服務幾乎每一類都有對象,而且大部分沒有人監看。

業務結果:數的是兩個量,訂單數與付款完成數,分開數是因為兩者會單獨歸零。付款頁的前端腳本壞掉時,訂單建立的請求照常成功,付款完成數卻歸零,所有規則裡只有比較付款完成數的那一條會成立;比較的方式(和上週同一時段比)在 4.0.1。

資料正確性:每天以金流業者的對帳檔比對資料庫裡付款完成的訂單,兩邊的筆數與金額對不上時告警(Data Reconciliation)。金流回呼遺失、重複的回呼讓同一筆訂單入帳兩次,都要靠這一步發現。

到期事件:入口的 TLS 憑證由 cert-manager 自動續期,續期失敗時只在 cert-manager 的狀態與日誌裡留下紀錄。cert-manager 輸出兩個可以寫規則的指標(指標名稱出自 cert-manager 的原始碼,官方文件的指標頁沒有列出):certmanager_certificate_ready_status{condition="False"} 為 1 時,代表憑證沒有就緒、續期多半已經失敗;certmanager_certificate_expiration_timestamp_seconds 是到期時間,以它減去現在時間寫剩下天數的規則。表上那個失效域以外的第三方合成監控服務,多半也能加一項憑證到期的檢查(這一項要另外設定,現有的設定只檢查首頁的狀態碼),它取得的到期時間在叢集停掉時仍送得出預警。另外兩種到期在服務自己手上:對金流業者呼叫用的 API 金鑰、驗證回呼簽章用的密鑰,業者可能設有效期限或要求輪替,到期那一刻付款全部失敗,要記下期限、在剩下一段天數時告警。

排程工作與背景工作:三個消費端都沒有端點,blackbox_exporter 探測不到。寄確認信的消費端停掉時,訂單照常成立,使用者收不到信。這一類在這個服務要決定的是各消費端的業務預期:確認信晚幾分鐘使用者就會來問、出貨單晚一小時會錯過當天的物流收件、報表晚半天沒有人會發現,所以三個佇列的等待時間門檻分開定。規則以佇列的標籤涵蓋全部佇列、給一個預設門檻,再對個別佇列覆寫,之後新增的佇列與消費端才不會在下一次盤點之前都沒有規則;告警本身的寫法(以等待時間而不是筆數)在上文〈checkout 現有的監控與各層的判讀〉。更新報表的消費端以外,每晚另有一個彙整日報表的排程工作,它在完成時對心跳監看服務送一次心跳。

備份:託管 PostgreSQL 由業者自動備份,監看的是業者記錄的最近備份時間(做法在 4.0.1);這個服務另外要決定的是還原要回到多久以前的資料,訂單與付款狀態少了一小時就要人工對帳,所以除了每天的備份,還要確認業者的時間點還原(point-in-time recovery)有開,並定期實際還原一次到另一個資料庫。

安全:後台的管理操作與權限變更寫進稽核紀錄,大量匯出訂單或異常的登入來源時告警,見 4.12 Audit Log 邊界與 PII 治理。

第三方依賴:金流是同步呼叫的依賴,它停掉時付款請求失敗,錯誤率會上升,〈checkout 現有的監控與各層的判讀〉補上的逐依賴請求指標讓它分得出是哪一家;金流的回呼是反方向的依賴:金流業者的回呼停送、或回呼送不進付款服務時,服務的每個數字都正常,只有回呼的到達數下降,所以要比對「送出的付款數」與「收到的回呼數」,兩者的差距持續擴大時告警。寄信業者是非同步的依賴,它停掉時只有消費端呼叫失敗與佇列堆積,消費端的逐依賴指標與佇列的等待時間會一起反映。同時訂閱兩家業者的狀態頁,狀態頁可能落後或跟著故障一起停,判斷以自己的逐依賴指標為主;依賴方停掉時的處理見 8.15 Vendor / 第三方依賴事故處理。

成本:這個服務的帳單在兩個時候失控:促銷結束後自動擴充出來的節點沒有縮回,以及某個消費端卡在一筆反覆重試的訊息、讓日誌量暴增。節點沒縮回以「促銷結束後幾小時節點數仍高於平日」的規則監看,日誌暴增以每個服務的日誌量監看;預算告警與成本儀表板在 9.8 效能可觀測性 的〈Cost dashboard〉。

與單機短網址服務的盤點對照

兩個服務用同一套監控鏈盤點,缺口的性質不同。單機短網址服務的缺口多半是少了觀察者或規則:失效域以外沒有觀察者、結果沒有規則讀。這個 checkout 在叢集上,kube-prometheus-stack 把重啟規則、Watchdog 與送失敗的規則預先帶進來,觀察者與規則大多已經在,缺口藏在三個不容易看到的地方:chart 預設的路由把 Watchdog 送到不通知任何人的接收端,送失敗的告警則跟著一般的告警走值班的推播服務,也就是可能正在送不出去的那一條管道,規則都在而通知到不了該到的地方;探測與合成監控檢查的是健康端點與首頁,而不是下單與付款這條路徑;以及佇列的消費端、第三方的回呼與對帳這些不在請求路徑上的對象,每一類都要另外定義預期。

服務的組成越接近這個 checkout(有非同步的消費端、有外部業者、有金錢上的正確性要求),盤點的工作量越往 4.0.1 那幾類移動。那幾類用得到的指標多半拿得到(佇列的 exporter、憑證的到期時間、業者的備份紀錄),而預期要由了解業務的人決定:多少筆訂單算正常、哪兩份資料該一致、確認信晚多久算太晚,沒有工具能代為決定。監控鏈各層的補法多半可以照工具文件完成,那幾類的補法要從服務自己的需求出發。