APISIX源码解析-ETCD-watch配置机制

ETCD-watch配置机制

转载自 https://www.sohu.com/a/516320573_121126896

基于 etcd watch 机制的配置同步方案

管理集群必须依赖中心化的配置,etcd 就是这样一个数据库。APISIX 没有选择关系型数据库作为配置中心,是因为 etcd 具有以下 2 个优点:

etcd 采用类 Paxos 的 Raft 协议保障了数据一致性,它是去中心化的分布式数据库,可靠性高于关系数据库;
etcd 的 watch 机制允许客户端监控某个 key 的变动,即,若类似/nginx/http/upstream 这种 key 的 value 值发生变动,watch 的客户端会立刻收到通知,如下图所示:
在这里插入图片描述
因此,不同于Orange采用 MySQL、Kong采用 PostgreSQL 作为配置中心(这二者同样是基于 OpenResty 实现的 API Gateway),APISIX 采用了 etcd 作为中心化的配置组件。

那么,Nginx 是怎样通过 watch 机制获取到 etcd 配置数据变化的呢?有没有新启动一个 agent 进程?它通过 HTTP/1.1 还是 gRPC 与 etcd 通讯的?

ngx.timer.at 定时器

APISIX 并没有启动 Nginx 以外的进程与 etcd 通讯。它实际上是通过 ngx.timer.at 这个定时器实现了 watch 机制。为了方便对 OpenResty 不太了解的同学,我们先来看看 Nginx 中的定时器是如何实现的,它是 watch 机制实现的基础。

Nginx 的红黑树定时器

Nginx 采用了 epoll + nonblock socket 这种多路复用机制实现事件处理模型,其中每个 worker 进程会循环处理网络 IO 即定时器事件:
//参见Nginx的src/os/unix/ngx_process_cycle.c文件

static void ngx_worker_process_cycle(ngx_cycle_t *cycle, void *data)
{

for ( ;; ) {

ngx_process_events_and_timers(cycle);

}

}

// 参见ngx_proc.c文件

void ngx_process_events_and_timers(ngx_cycle_t *cycle)

{

timer = ngx_event_find_timer();

(void) ngx_process_events(cycle, timer, flags);

ngx_event_process_posted(cycle, &ngx_posted_accept_events);

ngx_event_expire_timers();

ngx_event_process_posted(cycle, &ngx_posted_events);

}

ngx_event_expire_timers 函数会调用所有超时事件的 handler 方法。事实上,定时器是由红黑树(一种平衡有序二叉树)实现的,其中 key 是每个事件的绝对过期时间。这样,只要将最小节点与当前时间做比较,就能快速找到过期事件。

OpenResty 的 Lua 定时器

当然,以上 C 函数开发效率很低。因此,OpenResty 封装了 Lua 接口,通过ngx.timer.at将 ngx_timer_add 这个 C 函数暴露给了 Lua 语言:
//参见OpenResty /ngx_lua-0.10.19/src/ngx_http_lua_timer.c文件

void ngx_http_lua_inject_timer_api(lua_State *L)
{

lua_createtable(L, 0 /* narr */, 4 /* nrec */); /* ngx.timer. */

lua_pushcfunction(L, ngx_http_lua_ngx_timer_at);

lua_setfield(L, -2, "at");

lua_setfield(L, -2, "timer");

}

static int ngx_http_lua_ngx_timer_at(lua_State *L)
{

return ngx_http_lua_ngx_timer_helper(L, 0);

}

static int ngx_http_lua_ngx_timer_helper(lua_State *L, int every)
{

ngx_event_t *ev = NULL;

ev->handler = ngx_http_lua_timer_handler;

ngx_add_timer(ev, delay);

}

因此,当我们调用 ngx.timer.at 这个 Lua 定时器时,就是在 Nginx 的红黑树定时器里加入了 ngx_http_lua_timer_handler 回调函数,这个函数不会阻塞 Nginx。

APISIX 获取 etcd 通知的方式

APISIX 将需要监控的配置以不同的前缀存入了 etcd,目前包括以下 11 种:

  • /apisix/consumers/:APISIX 支持以 consumer 抽象上游种类;
  • /apisix/global_rules/:全局通用的规则;
  • /apisix/plugin_configs/:可以在不同 Router 间复用的 Plugin;
  • /apisix/plugin_metadata/:部分插件的元数据;
  • /apisix/plugins/:所有 Plugin 插件的列表;
  • /apisix/proto/:当透传 gRPC 协议时,部分插件需要转换协议内容,该配置存储 protobuf 消息定义;
  • /apisix/routes/:路由信息,是 HTTP 请求匹配的入口,可以直接指定上游 Server,也可以挂载 services 或者upstream;
  • /apisix/services/:可以将相似的 router 中的共性部分抽象为 services,再挂载 plugin;
  • /apisix/ssl/:SSL 证书公、私钥及相关匹配规则;
  • /apisix/stream_routes/:OSI 四层网关的路由匹配规则;
  • /apisix/upstreams/:对一组上游 Server 主机的抽象;

这里每类配置对应的处理逻辑都不相同,因此 APISIX 抽象出 apisix/core/config_etcd.lua 文件,专注 etcd 上各类配置的更新维护。在 http_init_worker 函数中每类配置都会生成 1 个 config_etcd 对象
详细查看:

APISIX源码解析-执行阶段【init_worker】
APISIX源码解析-ETCD-new方法

总结:

APISIX 在每个 Nginx Worker 进程的启动过程中,通过 ngx.timer.at 函数将_automatic_fetch 插入定时器。_automatic_fetch 函数执行时会通过 sync_data 函数,基于 watch 机制接收 etcd 中的配置变更通知,这样,每个 Nginx 节点、每个 Worker 进程都将保持最新的配置。如此设计还有 1 个明显的优点:etcd 中的配置直接写入 Nginx Worker 进程中,这样处理请求时就能直接使用新配置,无须在进程间同步配置,这要比启动 1 个 agent 进程更简单!

lua-resty-etcd 库的 HTTP/1.1 协议

sync_data 函数到底是怎样获取 etcd 的配置变更消息的呢?先看下 sync_data 源码:

local etcd = require("resty.etcd")

etcd_cli, err = etcd.new(etcd_conf)

local function sync_data(self)

local dir_res, err = waitdir(self.etcd_cli, self.key, self.prev_index + 1, self.timeout)

end

local function waitdir(etcd_cli, key, modified_index, timeout)

local res_func, func_err, http_cli = etcd_cli:watchdir(key, opts)

if http_cli then

local res_cancel, err_cancel = etcd_cli:watchcancel(http_cli)

end

end

这里实际与 etcd 通讯的是lua-resty-etcd 库。它提供的 watchdir 函数用于接收 etcd 发现 key 目录对应 value 变更后发出的通知。

watchcancel 函数又是做什么的呢?这其实是 OpenResty 生态的缺憾导致的。etcd v3 已经支持高效的 gRPC 协议(底层为 HTTP2 协议)。你可能听说过,HTTP2 不但具备多路复用的能力,还支持服务器直接推送消息,从 HTTP3 协议对照理解 HTTP2:
在这里插入图片描述
然而,**Lua 生态目前并不支持 HTTP2 协议!**所以 lua-resty-etcd 库实际是通过低效的 HTTP/1.1 协议与 etcd 通讯的,因此接收/watch 通知也是通过带有超时的/v3/watch 请求完成的。这个现象其实是由 2 个原因造成的:

  1. Nginx 将自己定位为边缘负载均衡,因此上游必然是企业内网,时延低、带宽大,所以对上游协议不必支持 HTTP2 协议!
  2. 当 Nginx 的 upstream 不能提供 HTTP2 机制给 Lua 时,Lua 只能基于 cosocket
    自己实现了。HTTP2 协议非常复杂,目前还没有生产环境可用的 HTTP2 cosocket 库。

使用 HTTP/1.1 的 lua-resty-etcd 库其实很低效,如果你在 APISIX 上抓包,会看到频繁的 POST 报文,其中 URI 为/v3/watch,而 Body 是 编码的 watch 目录:
在这里插入图片描述
我们可以验证下 watchdir 函数的实现细节:

-- lib/resty/etcd/v3.lua文件

function _M.watchdir(self, key, opts)

return watch(self, key, attr)

end

local function watch(self, key, attr)

callback_fun, err, http_cli = request_chunk(self, 'POST', '/watch',

opts, attr.timeout or self.timeout)

return callback_fun

end

local function request_chunk(self, method, path, opts, timeout)

http_cli, err = utils.http.new()

-- 发起TCP连接

endpoint, err = http_request_chunk(self, http_cli)

-- 发送HTTP请求

res, err = http_cli:request({

method = method,

path = endpoint.api_prefix .. path,

body = body,

query = query,

headers = headers,

})

end

local function http_request_chunk(self, http_cli)

local endpoint, err = choose_endpoint(self)

ok, err = http_cli:connect({

scheme = endpoint.scheme,

host = endpoint.host,

port = endpoint.port,

ssl_verify = self.ssl_verify,

ssl_cert_path = self.ssl_cert_path,

ssl_key_path = self.ssl_key_path,

})

return endpoint, err

end

可见,APISIX 在每个 worker 进程中,通过 ngx.timer.at 和 lua-resty-etcd 库反复请求 etcd,以此保证每个 Worker 进程中都含有最新的配置。

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值