構建api gateway之 健康檢查

2023-02-09 12:02:41

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

可以看到不可存取的服務節點只被存取了一次,後續都到了健康的節點上

目錄