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