這篇說明一組用 Docker Compose 跑在同一台主機上的多容器服務(入口的反向代理、前端、後端、資料庫、快取),怎麼回答「每個服務還活著嗎」。範圍是單機的 Compose 環境;容器健康檢查本身的機制在 容器的健康檢查,跨多台機器的編排平台由 Kubernetes 這類平台的 probe 接手。

「活著」由三層回答

多容器服務裡,「某個服務還活著嗎」至少有三種問法,每一種要由不同位置的檢查回答:

問法由誰檢查檢查的位置
這個容器的程序在不在Docker容器的執行狀態(running、exited)
這個服務自己的端點能不能回應容器的健康檢查,Docker 在容器裡執行容器內部,打 127.0.0.1
從別的地方打不打得通外部探測,另一個容器發請求Compose 網路上,用服務名稱

三層彼此不能互相取代。程序在跑不代表端點能回應:一個容器裡同時跑 Nginx 與 PHP-FPM,PHP-FPM 死了而 Nginx 還在,容器是 running,每個請求都是 502。端點能回應也不代表從外面打得通:服務自己回應正常,入口的路由設定寫錯,使用者照樣進不去。本篇的外部探測從同一台主機上的另一個容器發出,主機停掉時跟著停;從另一台機器、照使用者路徑打對外網址的探測回答的是使用者從外面連不連得到,見 4.28 單台主機的監控盲區與失效域以外的觀察者。

外部探測:從另一個容器發請求

容器的健康檢查只看得到容器內部,外部探測補上的是網路與路由這一段。做法是在同一個 Compose 網路裡多跑一個探測工具的容器,它用服務名稱(go:8080、postgres:5432)定期對各服務發請求,把結果彙整成一個儀表板。

已經在跑 Prometheus 的環境,外部探測通常交給 Prometheus 的 blackbox_exporter(一種 exporter,替 Prometheus 從外面打端點並輸出結果),結果直接進既有的告警規則(Prometheus 的元件分工見 Prometheus)。這一節談的是還沒有監控堆疊的單機環境,要一個獨立的探測工具。選工具時主要的分別在設定放在哪裡:

工具設定方式適合
Gatus一份 YAML 設定檔,可以放進版本控制;每個端點寫一組判定條件環境要能從 repo 重建、設定要經過 review 的專案
Uptime Kuma在網頁介面上新增監看項目,設定存在掛載進容器的 volume(官方範例的 /app/data),重建容器不會遺失,但不在 repo 裡想在網頁上直接點選設定、不需要版控的環境

兩者都能做 HTTP、TCP 檢查與告警通知。設定存在工具自己資料目錄裡的,重建環境時監看清單要另外備份與還原;設定是一份檔案的工具,監看清單跟著程式碼一起版控,新增一個服務時,監看項目可以在同一個變更裡加上。

以 Gatus 為例,一個 HTTP 端點的檢查寫成一組條件,全部成立才算通過:

1endpoints:
2  - name: Go 後端
3    url: "http://go:8080/healthz"   # Compose 網路裡的服務名稱與健康端點
4    interval: 30s
5    conditions:
6      - "[STATUS] == 200"           # 狀態碼要是 200;回應時間不放進存活的條件

url 要換成手上服務在 Compose 網路裡的名稱、port 與健康端點;interval 依服務的特性調整。回應時間不放進存活探測的條件:同一台主機過載時,服務只是變慢也會被判成失敗,「慢」和「掛了」在結果裡長得一樣;延遲改以探測回應時間在一段時間窗內的分位數、或後端輸出的延遲分布判斷,寫法見 4.27 主機過載時的監控告警〈可用性與延遲分成兩個判斷〉。

TCP 檢查的保證範圍

資料庫與快取不講 HTTP,探測工具通常改用 TCP 檢查:連得上那個 port 就算通過。連得上只代表那個位址與 port 上有程序在聽、連線建立成功,Gatus 的文件也提醒這不保證服務本身健康。

能送一段資料再檢查回應的工具,可以把檢查往前推一步。Valkey(與 Redis)收到 PING 會回 PONG,所以檢查改成送 PING、回應裡要有 PONG:

1  - name: Valkey
2    url: "tcp://cache:6379"
3    body: "PING\r\n"                # 連上之後送出的內容
4    conditions:
5      - "[CONNECTED] == true"
6      - "[BODY] == pat(*PONG*)"     # 回應裡要包含 PONG

cache:6379 同樣要換成手上快取服務的名稱與 port。PostgreSQL 的協定沒有這種一行就能完成的問答,TCP 檢查只能證明它在聽;它能不能執行查詢,由容器自己的健康檢查(pg_isready)與後端的錯誤率回答。

負載平衡後面的檢查涵蓋哪一個後端

入口後面有多個同一份 API 的後端、由負載平衡輪流分配時,經過入口的一次檢查只會打到其中一個。某一個後端壞了,經過入口的檢查可能剛好落在好的那一個而通過,也可能落在壞的那一個而失敗,結果時好時壞,看不出是哪一個壞了。

所以兩種檢查都要有、各自回答不同的問題:直接打每一個後端(用各自的服務名稱),回答「哪一個後端壞了」;經過入口打一次,回答「入口本身與路由是不是通的」。

SSH 進主機時的指令列查詢

儀表板適合一直開著看;SSH 進主機排查時,要的是一條指令列出每個服務的狀態,而且能被腳本與排程呼叫。docker compose ps 已經有執行狀態與健康狀態兩欄,缺的是兩件事:沒有容器的服務不會被列出來,以及沒有一個結束碼可以讓呼叫的一方判斷結果。

補上這兩件事的腳本,邏輯是拿 docker compose config --services 列出定義過的所有服務,逐一查它的狀態,再依最嚴重的那一個決定結束碼。結束碼的設計值得多分一層:

結束碼情況
0全部服務在跑,設了健康檢查的都是 healthy
1有服務沒在跑(沒啟動、已結束),或有服務 unhealthy
2沒有 1 的情況,但有服務還在 starting

把 starting 分出來,是因為它在兩種呼叫情境下的意思相反:部署流程剛重建完容器,starting 是預期中的狀態,流程可以重試直到變成 0;定時執行的檢查看到某個服務一直停在 starting,代表它卡在啟動階段,該當成警告。只有 0 與 1 兩種結束碼時,呼叫的一方分不出這兩件事。

監看工具自己掛掉時

外部探測工具也是一個容器。它掛掉時檢查就停了,告警也不會送出,而沒有任何機制會發出「監看停了」的通知。常見的補法有兩種:

  • 讓另一套機制監看它:探測工具多半有自己的健康端點(Gatus 的 /health 回 {"status":"UP"}),可以交給主機上的另一套機制(例如 systemd 的計時器、或另一台機器上的探測)去戳。
  • 反過來監看訊號是否消失:讓探測工具定期送出一個「我還在」的訊號給外部服務,外部服務在訊號停止時告警,這就是 dead man’s switch。它的好處是外部服務不需要連進服務所在的網路,設計見 Watchdog / dead man’s switch。

本篇的外部探測是從同一台主機上另一個容器發出的,主機整台停掉、使用者到主機的路徑中斷時它跟著停或照樣成功;探測端放在主機的失效域以外的做法見 4.28 單台主機的監控盲區與失效域以外的觀察者。

練習或開發用的環境可以不補,但要知道這是一個沒被涵蓋的位置;儀表板全綠的意思是「最後一次檢查時全部正常」,前提是探測工具本身還在跑。

三層的分工

回到開頭的三種問法:容器的健康檢查告訴 Docker 每個容器自己好不好,並讓 Compose 決定啟動順序;外部探測回答從網路與入口打不打得通,並把結果集中到一個地方;指令列查詢讓人在主機上快速確認、讓腳本拿到一個可判斷的結束碼。三者都不會自己修好壞掉的服務——Compose 管理的容器不會因為 unhealthy 被重啟,探測工具只負責看與通知——自動恢復要另外設計,見 容器的健康檢查 與 Process supervisor 選型。三種做法回答的都是服務活不活著;服務活著而變慢、或某一層的資源快用完,要靠各層的指標與時間窗判斷,各層的訊號從哪裡收見 4.26 單機容器服務的流量與資源監控。本篇的容器健康檢查與外部探測,在 4.0 服務端監控的分層 分別是容器與程序的自檢、服務網路內的探測兩層;它們與元件指標、應用層訊號、告警規則與分送、告警鏈的監看、使用者路徑的合成監控各自回答什麼問題、看不到什麼,見那一篇。指令列查詢是給人臨時確認用的,不在那一篇的分層裡。