https://www.taohui.org.cn/2021/08/10/%E5%BC%80%E6%BA%90%E7%BD%91%E5%85%B3APISIX%E6%9E%B6%E6%9E%84%E5%88%86%E6%9E%90/#more
Nginx采用了epoll + nonblock socket这种多路复用机制实现事件处理模型,其中每个worker进程会循环处理网络IO及定时器事件,ngx_event_expire_timers函数会调用所有超时事件的handler方法。事实上,定时器由红黑树实现,其中key是每个事件的绝对过期时间。这样,只要将最小节点与当前时间做比较,就能快速找到过期事件。OpenResty封装了Lua接口,通过ngx.timer.at将ngx_timer_add这个C函数暴露给了Lua语言:当我们调用ngx.timer.at这个Lua定时器时,就是在Nginx的红黑树定时器里加入了ngx_http_lua_timer_handler回调函数,这个函数不会阻塞Nginx。
APISIX在每个Nginx Worker进程的启动过程中,通过ngx.timer.at函数将_automatic_fetch插入定时器。_automatic_fetch函数执行时会通过sync_data函数,基于watch机制接收etcd中的配置变更通知,这样,每个Nginx节点、每个Worker进程都将保持最新的配置。如此设计还有1个明显的优点:etcd中的配置直接写入Nginx Worker进程中,这样处理请求时就能直接使用新配置,无须在进程间同步配置,这要比启动1个agent进程更简单!
竟然在这里 config_etcd.lua 循环注册_automatic_fetch定时器
function _M.new(key, opts) ... if automatic then if not key then return nil, "missing `key` argument" end if loaded_configuration[key] then local res = loaded_configuration[key] loaded_configuration[key] = nil -- tried to load log.notice("use loaded configuration ", key) local dir_res, headers = res.body, res.headers load_full_data(obj, dir_res, headers) end ngx_timer_at(0, _automatic_fetch, obj) else local etcd_cli, err = get_etcd() if not etcd_cli then return nil, "failed to start a etcd instance: " .. err end obj.etcd_cli = etcd_cli end if key then created_obj[key] = obj end return obj end
local function _automatic_fetch(premature, self) ... local i = 0 while not exiting() and self.running and i <= 32 do i = i + 1 local ok, err = xpcall(function() if not self.etcd_cli then local etcd_cli, err = get_etcd() if not etcd_cli then error("failed to create etcd instance for key [" .. self.key .. "]: " .. (err or "unknown")) end self.etcd_cli = etcd_cli end local ok, err = sync_data(self) ... if not exiting() and self.running then ngx_timer_at(0, _automatic_fetch, self) end end
local function sync_data(self) ... local dir_res, err = waitdir(self.etcd_cli, self.key, self.prev_index + 1, self.timeout) log.info("waitdir key: ", self.key, " prev_index: ", self.prev_index + 1) log.info("res: ", json.delay_encode(dir_res, true)) ...
local function waitdir(etcd_cli, key, modified_index, timeout) ... local opts = {} opts.start_revision = modified_index opts.timeout = timeout opts.need_cancel = true local res_func, func_err, http_cli = etcd_cli:watchdir(key, opts) if not res_func then return nil, func_err end -- in etcd v3, the 1st res of watch is watch info, useless to us. -- try twice to skip create info local res, err = res_func() if not res or not res.result or not res.result.events then res, err = res_func() end if http_cli then local res_cancel, err_cancel = etcd_cli:watchcancel(http_cli) if res_cancel == 1 then log.info("cancel watch connection success") else log.error("cancel watch failed: ", err_cancel) end end