最後一個單節點模式是轉接器模式(adapter pattern):轉接器容器用來修改應用容器的介面,使其符合所有應用都被期望遵守的某個預定義介面。例如:確保應用實作一致的監控介面、確保日誌一律寫到 stdout 等慣例。

為什麼需要轉接器#

  • 真實世界的應用開發是異質、混搭的:有些部分由團隊從零開發、有些來自供應商、有些則是直接以預編譯二進位檔使用的現成開源或專有軟體。
  • 結果就是:任何實際部署的應用,都混雜多種語言、多種日誌與監控慣例。
  • 但要有效監控與營運應用,你需要共同介面——每個應用都用不同格式與介面提供指標時,很難把所有指標集中到一處做視覺化與告警。
  • 轉接器模式的做法:各個應用容器可以呈現各種不同的監控介面,由轉接器容器把這些異質性轉接為一致的介面,於是你只需部署一個預期單一介面的工具。

圖 4-1:通用的轉接器模式

監控#

  • 監控的理想是:單一解決方案能自動發現並監控環境中部署的任何應用——前提是每個應用實作相同的監控介面。
  • 現實是標準化監控介面眾多(syslog、Windows 的事件追蹤 etw、Java 的 JMX……),每種的通訊協定與通訊風格(推送 vs. 拉取)都不同,而你的系統又橫跨自寫程式碼與現成開源元件。
  • 幸好多數監控方案都提供各種外掛,能把某種監控格式轉接為共同介面。套用轉接器模式:應用容器就是要被監控的應用,轉接器容器則包含把應用暴露的監控介面轉換為通用監控系統所預期介面的工具
  • 這樣解耦的好處:
    • 系統更易理解、易維護:應用發新版不需要跟著發布監控轉接器。
    • 監控容器可與多個不同應用容器重用,甚至可以由監控系統的維護者獨立於應用開發者提供。
    • 轉接器作為獨立容器有自己專屬的 CPU 與記憶體資源,行為異常的監控轉接器不會拖垮面向使用者的服務
動手做:用 Prometheus 監控 Redis

Prometheus 是開源的監控聚合器(monitoring aggregator):收集指標並聚合到單一時間序列資料庫,其上提供視覺化與查詢語言。Prometheus 期望每個容器暴露特定的 metrics API,以便透過單一介面監控各式各樣的程式。

但許多熱門程式(如 Redis)並不以 Prometheus 相容的格式輸出指標。一個單純的 Redis Pod:

apiVersion: v1
kind: Pod
metadata:
  name: adapter-example
  namespace: default
spec:
  containers:
    - image: redis
      name: redis

此時 Prometheus 無法監控它。只要加入一個轉接器容器(現成的開源 Prometheus exporter),就能讓 Pod 輸出正確介面:

apiVersion: v1
kind: Pod
metadata:
  name: adapter-example
  namespace: default
spec:
  containers:
    - image: redis
      name: redis
    # Provide an adapter that implements the Prometheus interface
    - image: oliver006/redis_exporter
      name: adapter

這個例子同時展示了轉接器模式確保一致介面的價值,以及容器模式促成模組化重用的價值:現成的 Redis 容器加上現成的 Prometheus 轉接器,幾乎不費工就得到一台可被監控的 Redis 伺服器。若沒有轉接器模式,同樣的部署需要大量客製工作,而且 Redis 或轉接器任一方更新都得重工,運維性差得多。

日誌#

  • 系統輸出日誌的方式同樣高度異質:有的依等級(debug、info、warning、error)分寫到不同檔案,有的直接寫 stdoutstderr
  • 容器化世界普遍期望容器把日誌寫到 stdout,因為 docker logskubectl logs 等指令讀的就是它。
  • 日誌內的結構化資訊(如日期時間)也因日誌函式庫而異(Java 內建 logging vs. Go 的 glog)。但儲存與查詢分散式系統日誌時,你不在乎這些格式差異——你要的是每筆日誌最終都帶著正確的時間戳
  • 轉接器解法:應用容器寫到檔案,轉接器容器把檔案重導向 stdout;各應用以不同格式記錄,轉接器把資料轉換為日誌聚合器可消費的單一結構化表示——再一次,把異質的應用世界轉接為同質的共同介面世界。
動手做:用 fluentd 正規化不同的日誌格式

fluentd 是最受歡迎的開源日誌代理之一,主要優勢是豐富的社群外掛。

例一:為 Redis 加上慢查詢日誌。 Redis 的 SLOWLOG 指令會列出最近超過特定時間的查詢,對除錯應用效能很有用;可惜它只是伺服器上的指令,若問題發生當下沒有人在場除錯,事後就難以追查。解法:以 redis 為應用容器、fluentd 為轉接器容器,搭配 fluent-plugin-redis-slowlog 外掛監聽慢查詢:

<source>
  type redis_slowlog
  host localhost
  port 6379
  tag redis.slowlog
</source>

因為兩個容器共享網路命名空間,設定只需 localhost 與預設的 Redis 埠(6379)。從此隨時都能查閱慢查詢日誌。

例二:監控 Apache Storm 的日誌。 Storm 透過 RESTful API 提供資料,同樣有「問題發生當下沒在監控就查不到」的限制。部署啟用 fluent-plugin-storm 的 fluentd 轉接器,把 Storm 程序轉換為可查詢的時間序列日誌:

<source>
  type storm
  tag storm
  url http://localhost:8080
  window 600
  sys 0
</source>

加上健康監視器#

  • 情境:監控現成資料庫容器的健康狀態。容器由資料庫專案供應,我們不想只為了加健康檢查而修改它。
  • 容器編排器內建的健康檢查只能確認「程序在跑、有在監聽某埠」;若想要真正對資料庫跑查詢的豐富健康檢查呢?
  • Kubernetes 等編排系統允許用 shell 腳本作為健康檢查,我們可以寫一個對資料庫執行多個診斷查詢的腳本——但腳本要存放在哪裡?如何版本化?
  • 答案不出所料:轉接器容器。資料庫跑在應用容器中,與轉接器共享網路介面;轉接器是只包含健康檢查腳本的簡單容器,把它設為資料庫容器的健康檢查,就能執行應用需要的任何豐富檢查。檢查失敗時,資料庫會被自動重啟。
動手做:為 MySQL 加上豐富健康監控

想對 MySQL 執行能代表實際工作負載的查詢作為健康檢查。直接更新 MySQL 容器加入應用專屬的健康檢查並不吸引人——你得修改既有的 MySQL 基底映像檔,還得在新版 MySQL 映像檔釋出時持續更新。

轉接器做法:在既有 MySQL 容器旁加一個轉接器容器,執行測試資料庫健康的查詢,並實作預期的 HTTP 健康檢查介面;再把 MySQL 程序的健康檢查定義在這個轉接器暴露的介面上即可。轉接器的 Go 原始碼相當直觀(其他語言的實作當然也可行):

package main

import (
        "database/sql"
        "flag"
        "fmt"
        "net/http"

        _ "github.com/go-sql-driver/mysql"
)

var (
        user   = flag.String("user", "", "The database user name")
        passwd = flag.String("password", "", "The database password")
        db     = flag.String("database", "", "The database to connect to")
        query = flag.String("query", "", "The test query")
        addr   = flag.String("address", "localhost:8080",
                            "The address to listen on")
)

// Basic usage:
//   db-check --query="SELECT * from my-cool-table" \
//            --user=bdburns \
//            --passwd="you wish"
//
func main() {
        flag.Parse()
        db, err := sql.Open("localhost",
                            fmt.Sprintf("%s:%s@/%s", *user, *passwd, *db))
        if err != nil {
                fmt.Printf("Error opening database: %v", err)
        }

  // Simple web handler that runs the query
        http.HandleFunc("", func(res http.ResponseWriter, req *http.Request) {
                _, err := db.Exec(*query)
                if err != nil {
                        res.WriteHeader(http.StatusInternalServerError)
                        res.Write([]byte(err.Error()))
                        return
                }
                res.WriteHeader(http.StatusOK)
                res.Write([]byte("OK"))
                return
        })
 // Startup the server
        http.ListenAndServe(*addr, nil)
}

建成容器映像檔後放入 Pod:

apiVersion: v1
kind: Pod
metadata:
  name: adapter-example-health
  namespace: default
spec:
  containers:
    - image: mysql
      name: mysql
    - image: brendanburns/mysql-adapter
      name: adapter

mysql 容器完全不變,卻能從轉接器容器獲得所需的健康回饋。

有人會覺得這裡套用轉接器模式是多此一舉——大可自建一個懂得健康檢查 MySQL 的客製映像檔。確實可以,但這忽略了模組化帶來的強大效益:若每位開發者都把健康檢查寫死在自己的容器裡,就沒有重用與共享的機會。用轉接器模式開發的「MySQL 健康檢查」模組可以被許多人共享重用,使用者甚至不需要深入了解如何健康檢查 MySQL 資料庫就能套用。設計模式有時不只服務套用它的開發者,更能催生出在成員之間、乃至更廣大開發者生態系中協作與共享解決方案的社群。