nature

nature is api gateway, just show you for how to bulid api gateway

View on GitHub

Healthcheck

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

所以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

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

目录