构建api gateway之 健康检查
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
可以看到不可访问的服务节点只被访问了一次,后续都到了健康的节点上