第六部分: 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"]
我们附加了如下内容:
部署健康检查服务
现在我们准备部署我们更新后的带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",// 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{ "Testability","/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的机制,我们聚焦两个关键领域 - 服务发现和负载均衡。