Serendipity96 commented on a change in pull request #578: URL: https://github.com/apache/apisix-website/pull/578#discussion_r705846402
########## File path: website/blog/2021-08-10-apisix-nginx.md ########## @@ -0,0 +1,514 @@ +--- +title: "Apache APISIX 架构分析:如何动态管理 Nginx 集群?" +keywords: +- API 网关 +- Apache APISIX +- Nginx +- - Lua +- 动态管理 +date: "2021-08-10" +description: 本文转发自陶辉个人博客,主要介绍了基于APISIX 2.8 版本、OpenResty 1.19.3.2 版本以及 Nginx 1.19.3 版本进行 Apache APISIX 实现 REST API 远程控制 Nginx 集群的原理讲解。 +tags: [technology] +--- + +<!--truncate--> +> 作者陶辉 + +开源版 Nginx 最为人诟病的就是不具备动态配置、远程 API 及集群管理的能力,而 Apache APISIX 作为 Apache 基金会毕业的开源七层网关,基于 etcd 和 Lua 实现了对 Nginx 集群的动态管理。 + + + +让 Nginx 具备动态、集群管理能力并不容易,因为这将面临以下问题: + +* 微服务架构使得上游服务种类多、数量大,这导致路由规则、上游 Server 的变更极为频率。而 Nginx 的路由匹配是基于静态的 Trie 前缀树、哈希表、正则数组实现的,一旦`server_name`、`location` 变动,不执行 reload 就无法实现配置的动态变更 +* Nginx 将自己定位于 ADC 边缘负载均衡,因此它对上游并不支持 HTTP2 协议。这增大了 OpenResty 生态实现 etcd gRPC 接口的难度,因此通过 watch 机制接收配置变更必然效率低下 +* 多进程架构增大了 Worker 进程间的数据同步难度,必须选择 1 个低成本的实现机制,保证每个 Nginx 节点、Worker 进程都持有最新的配置 + +Apache APISIX 基于 Lua 定时器及 lua-resty-etcd 模块实现了配置的动态管理,本文将基于APISIX 2.8 版本、OpenResty 1.19.3.2 版本以及 Nginx 1.19.3 版本进行 Apache APISIX 实现 REST API 远程控制 Nginx 集群的原理。 + +## 基于 ETCD watch 机制的配置同步方案 + +管理集群必须依赖中心化的配置,etcd 就是这样一个数据库。Apache APISIX 没有选择关系型数据库作为配置中心,是因为 etcd 具有以下 2 个优点: + +* etcd 采用类 Paxos 的 Raft 协议保障了数据一致性,它是去中心化的分布式数据库,可靠性高于关系数据库 +* etcd 的 watch 机制允许客户端监控某个 key 的变动,即,若类似 /nginx/http/upstream 这种 key 的 value 值发生变动,watch 客户端会立刻收到通知,如下图所示: + + +因此,不同于 [Orange](https://github.com/orlabs/orange) 和 [Kong](https://konghq.com/) ,Apache APISIX 采用了 etcd 作为中心化的配置组件。 + +因此,你可以在生产环境的 Apache APISIX 中通过 etcdctl 看到如下类似配置: + +```yaml +# etcdctl get "/apisix/upstreams/1" +/apisix/upstreams/1 +{"hash_on":"vars","nodes":{"httpbin.org:80":1},"create_time":1627982128,"update_time":1627982128,"scheme":"http","type":"roundrobin","pass_host":"pass","id":"1"} +``` + +其中,/apisix 这个前缀可以在 conf/config.yaml 中修改,比如: + +```yaml +etcd: + host: + - "http://127.0.0.1:2379" + prefix: /apisix # apisix configurations prefix +``` + +而 upstreams/1 就等价于 nginx.conf 中的 http { upstream 1 {} } 配置。类似关键字还有 /apisix/services/、/apisix/routes/ 等。 + +那么,Nginx 是怎样通过 watch 机制获取到 etcd 配置数据变化的呢?有没有新启动一个 agent 进程?它通过 HTTP/1.1 还是 gRPC 与 etcd 通讯的? + +## ngx.timer.at 定时器 + +Apache APISIX 并没有启动 Nginx 以外的进程与 etcd 通讯。实际上它是通过 `ngx.timer.at` 这个定时器实现了 watch 机制。为了方便对 OpenResty 不太了解的同学理解,我们先来看看 Nginx 中的定时器是如何实现的,它是 watch 机制实现的基础。 + +### Nginx 的红黑树定时器 + +Nginx 采用了 epoll + nonblock socket 这种多路复用机制实现事件处理模型,其中每个 worker 进程会循环处理网络 IO 及定时器事件: + +```c +//参见 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 方法。事实上,定时器是由[红黑树](https://zh.wikipedia.org/zh-hans/%E7%BA%A2%E9%BB%91%E6%A0%91)(一种平衡有序二叉树)实现的,其中 key 是每个事件的绝对过期时间。这样,只要将最小节点与当前时间做比较,就能快速找到过期事件。 + +### OpenResty 的 Lua 定时器 + +当然,以上 C 函数开发效率很低。因此,OpenResty 封装了 Lua 接口,通过 [ngx.timer.at](https://github.com/openresty/lua-nginx-module#ngxtimerat) 将 `ngx_timer_add` 这个 C 函数暴露给了 Lua 语言: + +```c +//参见 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。 + +下面我们来看看 Apache APISIX 是怎样使用 `ngx.timer.at` 的。 + +### APISIX 基于定时器实现的 watch 机制 + +Nginx 框架为 C 模块开发提供了许多钩子,而 OpenResty 将部分钩子以 Lua 语言形式暴露了出来,如下图所示: + + + +Apache APISIX 仅使用了其中 8 个钩子(注意,APISIX 没有使用 `set_by_lua` 和 `rewrite_by_lua`,rewrite 阶段的插件其实是 Apache APISIX 自定义的,与 Nginx 无关),包括: + +* init_by_lua:Master 进程启动时的初始化 +* init_worker_by_lua:每个 Worker 进程启动时的初始化(包括 privileged agent 进程的初始化,这是实现 Java 等多语言插件远程 RPC 调用的关键) +* ssl_certificate_by_lua:在处理 TLS 握手时,openssl 提供了一个钩子,OpenResty 通过修改 Nginx 源码以 Lua 方式暴露了该钩子 +* access_by_lua:接收到下游的 HTTP 请求头部后,在此匹配 Host 域名、URI、Method 等路由规则,并选择 Service、Upstream 中的插件及上游 Server +* balancer_by_lua:在 content 阶段执行的所有反向代理模块,在选择上游 Server 时都会回调 `init_upstream` 钩子函数,OpenResty 将其命名为 `balancer_by_lua` +* header_filter_by_lua:将 HTTP 响应头部发送给下游前执行的钩子 +* body_filter_by_lua:将 HTTP 响应包体发送给下游前执行的钩子 +* log_by_lua:记录 access 日志时的钩子 + +准备好上述知识后,我们就可以回答 Apache APISIX 是怎样接收 etcd 数据的更新了。 + +#### nginx.conf的生成方式 + +每个 Nginx Worker 进程都会在 `init_worker_by_lua` 阶段通过 `http_init_worker` 函数启动定时器: + +```lua +init_worker_by_lua_block { + apisix.http_init_worker() +} +``` + +关于 nginx.conf 配置语法,可以参考[《从通用规则中学习nginx模块的定制指令》](https://www.taohui.pub/2020/12/23/nginx/%E4%BB%8E%E9%80%9A%E7%94%A8%E8%A7%84%E5%88%99%E4%B8%AD%E5%AD%A6%E4%B9%A0nginx%E6%A8%A1%E5%9D%97%E7%9A%84%E5%AE%9A%E5%88%B6%E6%8C%87%E4%BB%A4/)。你可能很好奇,下载 Apache APISIX 源码后没有看到 nginx.conf,这段配置是哪来的? + +这里的 nginx.conf 实际是由 Apache APISIX 的启动命令实时生成的。当你执行 make run 时,它会基于 Lua 模板 apisix/cli/ngx_tpl.lua 文件生成 nginx.conf。请注意,这里的模板规则是 OpenResty 自实现的,语法细节参见 [lua-resty-template](https://github.com/bungle/lua-resty-template)。生成 nginx.conf 的具体代码参见 apisix/cli/ops.lua 文件: + +```lua +local template = require("resty.template") +local ngx_tpl = require("apisix.cli.ngx_tpl") +local function init(env) + local yaml_conf, err = file.read_yaml_conf(env.apisix_home) + local conf_render = template.compile(ngx_tpl) + local ngxconf = conf_render(sys_conf) + + local ok, err = util.write_file(env.apisix_home .. "/conf/nginx.conf", + ngxconf) +``` + +当然,Apache APISIX 允许用户修改 nginx.conf 模板中的部分数据,具体方法是模仿 conf/config-default.yaml 的语法修改 conf/config.yaml 配置。其实现原理参见 `read_yaml_conf` 函数: + +```conf +function _M.read_yaml_conf(apisix_home) + local local_conf_path = profile:yaml_path("config-default") + local default_conf_yaml, err = util.read_file(local_conf_path) + + local_conf_path = profile:yaml_path("config") + local user_conf_yaml, err = util.read_file(local_conf_path) + ok, err = merge_conf(default_conf, user_conf) +end +``` + +可见,ngx_tpl.lua 模板中仅部分数据可由 yaml 配置中替换,其中 conf/config-default.yaml 是官方提供的默认配置,而 conf/config.yaml 则是由用户自行覆盖的自定义配置。如果你觉得仅替换模板数据还不够,大可直接修改 ngx_tpl 模板。 + +#### APISIX 获取 etcd 通知的方式 Review comment: Apache APISIX -- This is an automated message from the Apache Git Service. To respond to the message, please log on to GitHub and use the URL above to go to the specific comment. To unsubscribe, e-mail: [email protected] For queries about this service, please contact Infrastructure at: [email protected]
