1

第六部分: 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的健康检查

clipboard.png

接下来,我们将使用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的机制, 我们聚焦两个关键领域 - 服务发现和负载均衡。

参考链接


老将廉颇
878 声望297 粉丝