摘要
当业务量发生变化时,需要对上游服务进行扩缩容,或者因服务器硬件故障需要更换服务器。如果网关是通过配置来维护上游服务信息,在微服务架构模式下,其带来的维护成本可想而知。再者因不能及时更新这些信息,也会对业务带来一定的影响,还有人为误操作带来的影响也不可忽视,所以网关非常必要通过服务注册中心动态获取最新的服务实例信息。架构图如下所示:
服务启动时将自身的一些信息,比如服务名、IP、端口等信息上报到注册中心;各个服务与注册中心使用一定机制(例如心跳)通信,如果注册中心与服务长时间无法通信,就会注销该实例;当服务下线时,会删除注册中心的实例信息;
网关会准实时地从注册中心获取服务实例信息;
当用户通过网关请求服务时,网关从注册中心获取的实例列表中选择一个进行代理;
常见的注册中心:Eureka, Etcd, Consul, Nacos, Zookeeper 等
关键
- 初始化的 _M.init_worker() 函数
- 获取服务实例节点列表的 _M.nodes(service_name) 函数
discover【dns】
关键属性
discovery:
dns:
servers:
- "127.0.0.1:8600" # 使用 DNS 服务器的真实地址
Embedded control api for debugging
有时我们需要发现客户端在运行调试时将在线数据快照导出到内存中,如果实现该_M. dump_data()功能:
function _M.dump_data()
return {config = local_conf.discovery.eureka, services = applications}
end
然后你可以调用它的控制api如下:
获取 /v1/discovery/{discovery_type}/dump
例如:
http://127.0.0.1:9090/v1/discovery/eureka/dump
源码实现
dns.lua
function _M.nodes(service_name)
local host, port = core.utils.parse_addr(service_name)
core.log.info("discovery dns with host ", host, ", port ", port)
-- 解析服务地址
--SRV例子:
--; under the section of blah.service
--A 300 IN A 1.1.1.1
--B 300 IN A 1.1.1.2
--B 300 IN A 1.1.1.3
--
--; name TTL type priority weight port
--srv 86400 IN SRV 10 60 1980 A
--srv 86400 IN SRV 20 20 1981 B
--注意 B 域名的两条记录均分权重。 对于 SRV 记录,低优先级的节点被先选中,所以最后一项的优先级是负数。
local records, err = dns_client:resolve(host, core.dns_client.RETURN_ALL)
if not records then
return nil, err
end
-- 转化成对应的数据
local nodes = core.table.new(#records, 0)
for i, r in ipairs(records) do
if r.address then
nodes[i] = {host = r.address, weight = r.weight or 1, port = r.port or port}
if r.priority then
-- for SRV record, nodes with lower priority are chosen first
nodes[i].priority = -r.priority
end
end
end
return nodes
end
-- 初始化客户端
function _M.init_worker()
local local_conf = config_local.local_conf()
local ok, err = core.schema.check(schema, local_conf.discovery.dns)
if not ok then
error("invalid dns discovery configuration: " .. err)
return
end
local servers = core.table.try_read_attr(local_conf, "discovery", "dns", "servers")
local opts = {
hosts = {},
resolvConf = {},
nameservers = servers,
order = {"last", "A", "AAAA", "SRV", "CNAME"},
}
-- 使用resty.dns.client初始化客户端
local client, err = core.dns_client.new(opts)
if not client then
error("failed to init the dns client: ", err)
return
end
dns_client = client
end
开放control API “/dump”
apisix.http_control():fetch_control_api_router
function fetch_control_api_router()
core.table.clear(routes)
for _, plugin in ipairs(plugin_mod.plugins) do
local api_fun = plugin.control_api
if api_fun then
local api_route = api_fun()
register_api_routes(routes, api_route)
end
end
local discovery_type = require("apisix.core.config_local").local_conf().discovery
if discovery_type then
local discovery = require("apisix.discovery.init").discovery
local dump_apis = {}
for key, _ in pairs(discovery_type) do
local dis_mod = discovery[key]
-- if discovery module has control_api method, support it
local api_fun = dis_mod.control_api
if api_fun then
local api_route = api_fun()
local format_route = format_dismod_control_api_uris(key, api_route)
register_api_routes(routes, format_route)
end
local dump_data = dis_mod.dump_data
-- 增加control API "/dump"
if dump_data then
local target_uri = format_dismod_uri(key, "/dump")
local item = {
methods = {"GET"},
uris = {target_uri},
handler = function()
return 200, dump_data()
end
}
core.table.insert(dump_apis, item)
end
end
if #dump_apis > 0 then
core.log.notice("dump_apis: ", core.json.encode(dump_apis, true))
register_api_routes(routes, dump_apis)
end
end
.......................
应用流程
初始化 ----> 匹配路由 ----->替换upstream
注意:配置后upstream.service_name, upstream.nodes将不再生效,而是替换为从注册表中获取的“nodes”。
http_init_worker
function _M.http_init_worker()
local seed, err = core.utils.get_seed_from_urandom()
if not seed then
core.log.warn('failed to get seed from urandom: ', err)
seed = ngx_now() * 1000 + ngx.worker.pid()
end
math.randomseed(seed)
-- for testing only
core.log.info("random test in [1, 10000]: ", math.random(1, 10000))
local we = require("resty.worker.events")
local ok, err = we.configure({shm = "worker-events", interval = 0.1})
if not ok then
error("failed to init worker event: " .. err)
end
-- 在http启动初始化时,执行服务发现组件初始化方法
local discovery = require("apisix.discovery.init").discovery
if discovery and discovery.init_worker then
discovery.init_worker()
end
................
http_access_phase
......
router.router_http.match(api_ctx)
-- run global rule
plugin.run_global_rules(api_ctx, router.global_rules, nil)
-- 匹配到对应的路由
local route = api_ctx.matched_route
if not route then
core.log.info("not find any matched route")
return core.response.exit(404,
{error_msg = "404 Route Not Found"})
end
........
-- 设置替换路由
local code, err = set_upstream(route, api_ctx)
if code then
core.log.error("failed to set upstream: ", err)
core.response.exit(code)
end
........
set_by_route
function _M.set_by_route(route, api_ctx)
if api_ctx.upstream_conf then
-- upstream_conf has been set by traffic-split plugin
return
end
local up_conf = api_ctx.matched_upstream
if not up_conf then
return 500, "missing upstream configuration in Route or Service"
end
-- core.log.info("up_conf: ", core.json.delay_encode(up_conf, true))
-- 有service_name代表配置了服务发现,discovery_type必然有值
if up_conf.service_name then
if not discovery then
return 500, "discovery is uninitialized"
end
if not up_conf.discovery_type then
return 500, "discovery server need appoint"
end
local dis = discovery[up_conf.discovery_type]
if not dis then
return 500, "discovery " .. up_conf.discovery_type .. " is uninitialized"
end
-- 获取实际发现的上游服务
local new_nodes, err = dis.nodes(up_conf.service_name)
if not new_nodes then
return HTTP_CODE_UPSTREAM_UNAVAILABLE, "no valid upstream node: " .. (err or "nil")
end
local same = upstream_util.compare_upstream_node(up_conf, new_nodes)
-- 如果和上次不一样则覆盖
if not same then
up_conf.nodes = new_nodes
local new_up_conf = core.table.clone(up_conf)
core.log.info("discover new upstream from ", up_conf.service_name, ", type ",
up_conf.discovery_type, ": ",
core.json.delay_encode(new_up_conf, true))
local parent = up_conf.parent
if parent.value.upstream then
-- the up_conf comes from route or service
parent.value.upstream = new_up_conf
else
parent.value = new_up_conf
end
up_conf = new_up_conf
end
end
........................
备注
使用的dns-client (“resty.dns.client”)是由Kong网关开源的项目
https://github.com/Kong/lua-resty-dns-client