第六部分: Go微服務 - 健康檢查
隨著我們的微服務越來越多,越來越複雜, 需要一種可以讓Docker Swarm知道服務是否健康的機制就變得十分重要了。因此,本文重點看看如何為微服務加入健康檢查。
假如accountservice微服務不具備下面的能力,就毫無用處了:
- 提供HTTP服務。
- 連線到它自己的資料庫。
在微服務中處理這些情況的慣用方式是提供健康檢查路由(Azure Docs的好文章)。
在我們例子中,我們使用HTTP的,因此簡單建立一個/health路由。如果健康就返回200, 也可以返回機器可能性相關的訊息來解釋什麼OK。如果有問題,返回非200響應碼,也可以帶上一些不健康的原因。
注意,有些人認為檢查失敗應該也使用200,帶上說明不健康的原因資訊,我也同意那樣做。不過為了簡化,本文中就直接使用非200來檢查不健康。
因此我們需要在accountservice中新增一個/health路由。
原始碼
嚮往常一樣,我們一樣可以從git中籤出對應的分支,以獲取部分更改的程式碼。
https://github.com/callistaen...
給boltdb新增檢查
我們的服務如果不能正常訪問底層資料庫的話,就沒有什麼用。因此,我們需要給IBoltClient新增一個介面Check().
type IBoltClient interface {
OpenBoltDb()
QueryAccount(accountId string) (model.Account, error)
Seed()
Check() bool // NEW!
}
Check方法看起來可能比較單純,但是它在本文中還是很起作用的。它根據BoltDB是否可用而相應的返回true或false。
我們在boltclient.go檔案中Check的實現沒有現實意義,但是它已經足夠解釋問題了。
// Naive healthcheck, just makes sure the DB connection has been initialized.
func (bc *BoltClient) Check() bool {
return bc.boltDB != nil
}
mockclient.go裡邊的模擬實現也遵循我們的延伸/測試(stretchr/testify)標準模式:
func (m *MockBoltClient) Check() bool {
args := m.Mock.Called()
return args.Get(0).(bool)
}
新增/health路由
這裡非常直接。 我們直接在service/routes.go裡邊新增下面的路由:
var routes = Routes{
Route{
"GetAccount", // Name
"GET", // HTTP method
"/accounts/{accountId}", // Route pattern
GetAccount,
},
Route{
"HealthCheck",
"GET",
"/health",
HealthCheck,
},
}
/health請求讓HealthCheck來處理。下面是HealthCheck的內容:
func HealthCheck(w http.ResponseWriter, r *http.Request) {
// Since we're here, we already know that HTTP service is up. Let's just check the state of the boltdb connection
dbUp := DBClient.Check()
if dbUp {
data, _ := json.Marshal(healthCheckResponse{Status: "UP"})
writeJsonResponse(w, http.StatusOK, data)
} else {
data, _ := json.Marshal(healthCheckResponse{Status: "Database unaccessible"})
writeJsonResponse(w, http.StatusServiceUnavailable, data)
}
}
func writeJsonResponse(w http.ResponseWriter, status int, data []byte) {
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Content-Length", strconv.Itoa(len(data)))
w.WriteHeader(status)
w.Write(data)
}
type healthCheckResponse struct {
Status string `json:"status"`
}
HealthCheck函式代理檢查DB狀態, 即DBClient中新增的Check()方法。如果OK, 我們建立一個healthCheckResponse結構體。 注意首字母小寫,只在模組內可見的作用域。我們同時實現了一個寫HTTP響應的方法,這樣程式碼看起來簡潔一些。
執行
執行修改後的程式碼:
> go run *.go
Starting accountservice
Seeded 100 fake accounts...
2017/03/03 21:00:31 Starting HTTP service at 6767
然後開啟新的視窗,使用curl訪問/health介面:
> curl localhost:6767/health
{"status":"UP"}
It works!
Docker的健康檢查
接下來,我們將使用Docker的HEALTHCHECK機制讓Docker Swarm檢查我們的服務活躍性。 這是通過在Dockerfile檔案中新增一行來實現的:
FROM iron/base
EXPOSE 6767
ADD accountservice-linux-amd64 /
ADD healthchecker-linux-amd64 /
HEALTHCHECK --interval=1s --timeout=3s CMD ["./healthchecker-linux-amd64", "-port=6767"] || exit 1
ENTRYPOINT ["./accountservice-linux-amd64"]
healthchecker-linux-amd64是什麼東西?我們需要稍微幫助一下Docker, 因為Docker自己沒有為我們提供HTTP客戶端或類似的東西來執行健康檢查。 而是,Dockerfile中的HEALTHCHECK指令指定了一種命令(CMD), 它應該執行呼叫/health路由。 依賴執行程式的退出碼,Docker會確定服務是否健康。 如果太多後續健康檢查失敗,Docker Swarm將殺死這個容器,並啟動一個新的容器。
最常見的實現真實健康檢查的方式看起來類似curl。 然而,這需要我們的基礎docker映像來實際安裝有curl(或者任何底層依賴), 並且在這個時候我們不會真正希望處理它。取而代之的是讓Go語言來釀造我們自己的健康檢查小程式。
建立健康檢查程式
是時候在src/github.com/callistaenterprise/goblog下面建立一個新的子專案了。
mkdir healthchecker
然後在這個目錄下面建立一個main.go檔案, 其內容如下:
package main
import (
"flag"
"net/http"
"os"
)
func main() {
port := flag.String("port", "80", "port on localhost to check")
flag.Parse()
resp, err := http.Get("http://127.0.0.1:" + *port + "/health") // 注意使用 * 間接引用
// If there is an error or non-200 status, exit with 1 signaling unsuccessful check.
if err != nil || resp.StatusCode != 200 {
os.Exit(1)
}
os.Exit(0)
}
程式碼量不是很大,它做了些什麼?
- 使用flag包讀取-port命令列引數。如果沒有指定,回退使用預設值80。
- 執行HTTP GET請求http://127.0.0.1:[port]/health。
- 如果HTTP請求發生錯誤,狀態碼為非200,以推出碼1退出。 否則以退出碼0退出。0 == success, 1 == fail.
讓我們試試看,如果我們已經把accountservice停掉了,那麼重新執行它,然後執行healthchecker。
go build
./accountservice
然後執行這個程式:
> cd $GOPATH/src/github.com/callistaenterprise/goblog/healtchecker
> go run *.go
exit status 1
上面我們忘記指定埠號了,因此它使用的是預設80埠。讓我們再來一次:
> go run *.go -port=6767
>
這裡沒有輸出,表示我們請求是成功的。 很好,那麼我們構建一個linux/amd64的二進位制,然後將它新增到accountservice中,通過新增healthchecker二進位制到Dockerfile檔案中。 我們繼續使用copyall.sh指令碼來自動完成重新構建和部署。
#!/bin/bash
export GOOS=linux
export CGO_ENABLED=0
cd accountservice;go get;go build -o accountservice-linux-amd64;echo built `pwd`;cd ..
// NEW, builds the healthchecker binary
cd healthchecker;go get;go build -o healthchecker-linux-amd64;echo built `pwd`;cd ..
export GOOS=darwin
// NEW, copies the healthchecker binary into the accountservice/ folder
cp healthchecker/healthchecker-linux-amd64 accountservice/
docker build -t someprefix/accountservice accountservice/
最後我們還需要做一件事,就是更新accountservice的Dockerfile。它完整內容如下:
FROM iron/base
EXPOSE 6767
ADD accountservice-linux-amd64 /
# NEW!!
ADD healthchecker-linux-amd64 /
HEALTHCHECK --interval=3s --timeout=3s CMD ["./healthchecker-linux-amd64", "-port=6767"] || exit 1
ENTRYPOINT ["./accountservice-linux-amd64"]
我們附加了如下內容:
- 新增ADD指令,確保healthchecker二進位制包含到映象中。
- HEALTHCHECK語句指定我們的二進位制檔案以及引數,告訴Docker每隔3秒去執行一次健康檢查, 並接受3秒的超時。
部署健康檢查服務
現在我們準備部署我們更新後的帶healthchecker的accountservice服務了。如果要更加自動,將這兩行新增到copyall.sh檔案中,每次執行的時候,它會從Docker Swarm中自動刪除accountservice並且重新建立它。
docker service rm accountservice
docker service create --name=accountservice --replicas=1 --network=my_network -p=6767:6767 someprefix/accountservice
那麼現在執行./copyall.sh, 等幾秒鐘,所有構建更新好。然後我們再使用docker ps檢查容器狀態, 就可以列舉出所有執行的容器。
> docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS
1d9ec8122961 someprefix/accountservice:latest "./accountservice-lin" 8 seconds ago Up 6 seconds (healthy)
107dc2f5e3fc manomarks/visualizer "npm start" 7 days ago Up 7 days
我們查詢STATUS頭下面的"(healthy)"文字。服務沒有配置healthcheck的完全沒有health指示。
故意造成失敗
要讓事情稍微更加有意思, 我們新增一個可測試API, 可以允許我們讓端點扮演不健康的目的。在routes.go檔案中,宣告另外一個路由。
var routes = Routes{
Route{
"GetAccount", // Name
"GET", // HTTP method
"/accounts/{accountId}", // Route pattern
/*func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=UTF-8")
w.Write([]byte("{\"result\":\"OK\"}"))
},*/
GetAccount,
},
Route{
"HealthCheck",
"GET",
"/health",
HealthCheck,
},
Route{
"Testability",
"GET",
"/testability/healthy/{state}",
SetHealthyState,
},
}
這個路由(在生產服務中不要有這樣的路由!)提供了一種REST-ish路由的故障健康檢查目的。SetHealthyState函式在goblog/accountservice/handlers.go檔案中,程式碼如下:
var isHealthy = true // NEW
func SetHealthyState(w http.ResponseWriter, r *http.Request) {
// Read the 'state' path parameter from the mux map and convert to a bool
var state, err = strconv.ParseBool(mux.Vars(r)["state"])
// If we couldn't parse the state param, return a HTTP 400
if err != nil {
fmt.Println("Invalid request to SetHealthyState, allowed values are true or false")
w.WriteHeader(http.StatusBadRequest)
return
}
// Otherwise, mutate the package scoped "isHealthy" variable.
isHealthy = state
w.WriteHeader(http.StatusOK)
}
最後,將isHealthy布林作為HealthCheck函式的檢查條件:
func HealthCheck(w http.ResponseWriter, r *http.Request) {
// Since we're here, we already know that HTTP service is up. Let's just check the state of the boltdb connection
dbUp := DBClient.Check()
if dbUp && isHealthy { // NEW condition here!
data, _ := json.Marshal(
...
...
}
重啟accountservice.
> cd $GOPATH/src/github.com/callistaenterprise/goblog/accountservice
> go run *.go
Starting accountservice
Seeded 100 fake accounts...
2017/03/03 21:19:24 Starting HTTP service at 6767
然後在新視窗產生一個新的healthcheck呼叫。
> cd $GOPATH/src/github.com/callistaenterprise/goblog/healthchecker
> go run *.go -port=6767
第一次嘗試成功,然後我們通過使用下面的curl請求testability來改變accountservice的狀態。
> curl localhost:6767/testability/healthy/false
> go run *.go -port=6767
exit status 1
起作用了!然後我們在Docker Swarm中執行它。使用copyall.sh重建並重新部署accountservice。
> cd $GOPATH/src/github.com/callistaenterprise/goblog
> ./copyall.sh
嚮往常一樣,等待Docker Swarm重新部署"accountservice", 使用最新構建的"accountservice"容器映像。然後,執行docker ps來看是否啟動並執行了帶有健康的服務。
> docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS
8640f41f9939 someprefix/accountservice:latest "./accountservice-lin" 19 seconds ago Up 18 seconds (healthy)
注意CONTAINER ID和CREATED欄位。可以在你的Docker Swarm上呼叫testability API。(我的IP是: 192.168.99.100)。
> curl $ManagerIP:6767/testability/healthy/false
>
然後,我們在幾秒時間內再次執行docker ps命令.
> docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS NAMES
0a6dc695fc2d someprefix/accountservice:latest "./accountservice-lin" 3 seconds ago Up 2 seconds (healthy)
你可以看到,這裡有了全新的CONTAINER ID和CREATED和STATUS. 真正發生的是Docker Swarm監測到三個(重試的預設值)連續失敗的健康檢查, 並立即確定服務變得不健康, 需要用新的例項來代替, 這完全是在沒有任何管理人員干預的情況下發生的。
總結
在這一部分中,我們使用一個簡單的/health路由和healthchecker程式結合Docker的HEALTHCHECK機制,展示了這個機制如何讓Docker Swarm自動為我們處理不健康的服務。
下一章,我們深入到Docker Swarm的機制, 我們聚焦兩個關鍵領域 - 服務發現和負載均衡。