构建api gateway之 健康检查

时间:2023-02-09 11:09:14

由于服务无法保证永远不会下线,而且下线时不一定能有人员能及时发现,

所以api gateway 一般会引入一个监工 Healthcheck, 像大家每年体检一样定时确认服务是否存活。

这样就可以在上游节点发生故障或者迁移时,将请求代理到健康的节点上,最大程度避免服务不可用的问题。

一般其分为主动检查和被动检查。

主动检查

其一般为使用单独的线程、进程、甚至独立的程序的探针,不断轮休式主动检查服务存活性。

一般支持 HTTP、HTTPS、TCP 三种探针类型, 也就是实际存不存活就是访问大家服务,看能不能得到正常结果。

其判定存活逻辑一般为:当发向健康节点 A 的 N 个连续探针都失败时(取决于如何配置),则该节点将被标记为不健康,不健康的节点将会被 api gateway忽略,无法收到请求;若某个不健康的节点,连续 M 个探针都成功,则该节点将被重新标记为健康,进而可以被代理。

(PS: 一般很多api gateway 为了方便大家使用,程序自带主动检查,所以api gateway 实例很多时,这样主动检查的请求就会过于大量,有些就会独立搭建独立的检查服务,减少请求量级)

被动检查

其一般为根据上游服务返回的情况,来判断对应的上游节点是否健康。相对于主动健康检查,被动健康检查的方式无需发起额外的探针,但是也无法提前感知节点状态,可能会有一定量的失败请求。

同理一般也是发向健康节点 A 的 N 个连续请求都被判定为失败(取决于如何配置),则该节点才被标记为不健康。

实践

由于篇幅关系,这里就只介绍被动检查的实现例子。

更具体实现可以参考 简化的healthcheck 或者 完整的lua-resty-healthcheck (ps: lua-resty-healthcheck 被动检查被标记为不健康之后无法恢复健康状态)

worker_processes  1;        #nginx worker 数量
error_log logs/error.log;   #指定错误日志文件路径
events {
    worker_connections 1024;
}

http {
    log_format main '$remote_addr [$time_local] $status $request_time $upstream_status $upstream_addr $upstream_response_time';
    access_log logs/access.log main buffer=16384 flush=3;            #access_log 文件配置

    lua_package_path  "$prefix/deps/share/lua/5.1/?.lua;$prefix/deps/share/lua/5.1/?/init.lua;$prefix/?.lua;$prefix/?/init.lua;;./?.lua;/usr/local/openresty/luajit/share/luajit-2.1.0-beta3/?.lua;/usr/local/share/lua/5.1/?.lua;/usr/local/share/lua/5.1/?/init.lua;/usr/local/openresty/luajit/share/lua/5.1/?.lua;/usr/local/openresty/luajit/share/lua/5.1/?/init.lua;";
    lua_package_cpath "$prefix/deps/lib64/lua/5.1/?.so;$prefix/deps/lib/lua/5.1/?.so;;./?.so;/usr/local/lib/lua/5.1/?.so;/usr/local/openresty/luajit/lib/lua/5.1/?.so;/usr/local/lib/lua/5.1/loadall.so;";
    # 开启 lua code 缓存
    lua_code_cache on;
    lua_shared_dict http_healthcheck 20m;  

    upstream nature_upstream {
        server 127.0.0.1:6699; #upstream 配置为 hello world 服务

        # 一样的balancer
        balancer_by_lua_block {
            local balancer = require "ngx.balancer"
            local upstream = ngx.ctx.api_ctx.upstream
            local ok, err = balancer.set_current_peer(upstream.host, upstream.port)
            if not ok then
                ngx.log(ngx.ERR, "failed to set the current peer: ", err)
                return ngx.exit(ngx.ERROR)
            end
        }
    }

    init_by_lua_block {

        -- 初始化 lb
        local roundrobin = require("resty.roundrobin") 
        local nodes = {k1 = {host = '127.0.0.1', port = 6698}, k2 = {host = '127.0.0.1', port = 6699}}
        local ns = {}
        for k, v in pairs(nodes) do
            -- 初始化 weight 
            ns[k] = 1
        end
        local picker = roundrobin:new(ns)

        -- 被动检查
        local shm_healthcheck = ngx.shared["http_healthcheck"]
        local check_healthcheck = function()
            for _ in pairs(nodes) do
                local k, err = picker:find()
                if not k then
                    return nil, err
                end
                local node = nodes[k]
                -- 检查是否不健康
                local status = shm_healthcheck:get(node.host..':'..tostring(node.port))
                if not status then
                    return node
                end
            end
            return nil, 'no health node'
        end

        -- 初始化路由
        local radix = require("resty.radixtree")
        local r = radix.new({
            {paths = {'/aa/d'}, metadata = {
                find = check_healthcheck
            }},
        })

        -- 匹配路由
        router_match = function()
            local p, err = r:match(ngx.var.uri, {})
            if err then
                log.error(err)
            end

            -- 执行 roundrobin lb 选择
            local k, err = p:find()
            if not k then
                return nil, err
            end
            return k
        end
    }

    server {
		#监听端口,若你的8699端口已经被占用,则需要修改
        listen 8699 reuseport;

        location / {

            # 在access阶段匹配路由
            access_by_lua_block {
                local upstream = router_match()
                if upstream then
                    ngx.ctx.api_ctx = { upstream = upstream }
                else
                    ngx.exit(404)
                end
            }

            proxy_http_version                  1.1;
            proxy_pass http://nature_upstream; #转发到 upstream

            # 上游节点 502 记录到不健康列表,这里为了理解简单,失败一次就写入
            log_by_lua_block {
                local s = ngx.var.upstream_status
                if s and s == '502' then
                    ngx.shared["http_healthcheck"]:incr(ngx.var.upstream_addr, 1, 5)
                end
            }
        }
    }


    #为了大家方便理解和测试,我们引入一个hello world 服务
    server {
		#监听端口,若你的6699端口已经被占用,则需要修改
        listen 6699;
        location / {
            default_type text/html;

            content_by_lua_block {
                ngx.say("HelloWorld")
            }
        }
    }
}

启动服务并测试

$ openresty -p ~/openresty-test -c openresty.conf #启动
$ curl --request GET 'http://127.0.0.1:8699/aa/d'  #第一次
<html>
<head><title>502 Bad Gateway</title></head>
<body>
<center><h1>502 Bad Gateway</h1></center>
</body>
</html>
$ curl --request GET 'http://127.0.0.1:8699/aa/d'  #第二次
HelloWorld
$ curl --request GET 'http://127.0.0.1:8699/aa/d'  #第三次
HelloWorld
$ curl --request GET 'http://127.0.0.1:8699/aa/d'  #第四次
HelloWorld

可以看到不可访问的服务节点只被访问了一次,后续都到了健康的节点上

目录