第六部分: 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的机制, 我们聚焦两个关键领域 - 服务发现和负载均衡。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。