You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@apisix.apache.org by GitBox <gi...@apache.org> on 2021/09/09 09:07:01 UTC

[GitHub] [apisix-website] yzeng25 commented on a change in pull request #578: docs: add transmitted blog of APISIX&Nginx

yzeng25 commented on a change in pull request #578:
URL: https://github.com/apache/apisix-website/pull/578#discussion_r705131502



##########
File path: website/blog/2021-08-10-apisix-nginx.md
##########
@@ -0,0 +1,512 @@
+---
+title: "Apache APISIX 架构分析:如何动态管理 Nginx 集群?"
+keywords: 
+- APISIX
+- Nginx
+- 动态管理
+- Lua
+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 作为 CNCF 毕业的开源七层网关,基于 etcd 和 Lua 实现了对 Nginx 集群的动态管理。
+
+![APISIX 架构图](https://static.apiseven.com/202108/1631170283612-ba5e27ff-726b-47a6-aa51-84731b067c44.png)
+
+让 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 客户端会立刻收到通知,如下图所示:

Review comment:
       这种 key

##########
File path: website/blog/2021-08-10-apisix-nginx.md
##########
@@ -0,0 +1,512 @@
+---
+title: "Apache APISIX 架构分析:如何动态管理 Nginx 集群?"
+keywords: 
+- APISIX
+- Nginx
+- 动态管理
+- Lua
+description: 本文转发自陶辉个人博客,主要介绍了基于APISIX 2.8 版本、OpenResty 1.19.3.2 版本以及 Nginx 1.19.3 版本进行 Apache APISIX 实现 REST API 远程控制 Nginx 集群的原理讲解。
+tags: [technology]

Review comment:
       add date

##########
File path: website/blog/2021-08-10-apisix-nginx.md
##########
@@ -0,0 +1,512 @@
+---
+title: "Apache APISIX 架构分析:如何动态管理 Nginx 集群?"
+keywords: 
+- APISIX
+- Nginx
+- 动态管理
+- Lua
+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 作为 CNCF 毕业的开源七层网关,基于 etcd 和 Lua 实现了对 Nginx 集群的动态管理。
+
+![APISIX 架构图](https://static.apiseven.com/202108/1631170283612-ba5e27ff-726b-47a6-aa51-84731b067c44.png)
+
+让 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 客户端会立刻收到通知,如下图所示:
+![基于 etcd 同步 nginx 配置](https://static.apiseven.com/202108/1631170345853-f020a64d-3e97-49c0-8395-c9e4e9cf4233.jpeg)
+
+因此,不同于 [Orange](https://github.com/orlabs/orange) 采用 MySQL、[Kong](https://konghq.com/) 采用 PostgreSQL 作为配置中心的方式(这二者同样是基于 OpenResty 实现的 API Gateway),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 语言形式暴露了出来,如下图所示:
+
+![openresty 钩子](https://static.apiseven.com/202108/1631170424663-53f56c99-aefc-4546-ac0b-76a25a6f0071.png)
+
+APISIX 仅使用了其中 8 个钩子(注意,APISIX 没有使用 `set_by_lua` 和 `rewrite_by_lua`,rewrite 阶段的插件其实是 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/)。你可能很好奇,下载 APISIX 源码后没有看到 nginx.conf,这段配置是哪来的?
+
+这里的 nginx.conf 实际是由 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)
+```
+
+当然,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 通知的方式
+
+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 对象:
+
+```lua
+function _M.init_worker()
+    local err
+    plugin_configs, err = core.config.new("/plugin_configs", {
+        automatic = true,
+        item_schema = core.schema.plugin_config,
+        checker = plugin_checker,
+    })
+end
+```
+
+而在 `config_etcd` 的 new 函数中,则会循环注册 `_automatic_fetch` 定时器:
+
+```lua
+function _M.new(key, opts)
+    ngx_timer_at(0, _automatic_fetch, obj)
+end
+```
+
+`_automatic_fetch` 函数会反复执行 `sync_data` 函数(包装到 xpcall 之下是为了捕获异常):
+
+```lua
+local function _automatic_fetch(premature, self)
+    local ok, err = xpcall(function()
+        local ok, err = sync_data(self)
+    end, debug.traceback)
+    ngx_timer_at(0, _automatic_fetch, self)
+end
+```
+
+`sync_data` 函数将通过 etcd 的 watch 机制获取更新,它的实现机制我们接下来会详细分析。
+
+所以总结来看,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` 源码:
+
+```lua
+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](https://github.com/api7/lua-resty-etcd) 库。它提供的 watchdir 函数用于接收 etcd 发现 key 目录对应 value 变更后发出的通知。
+
+watchcancel 函数又是做什么的呢?这其实是 OpenResty 生态的缺憾导致的。etcd v3 已经支持高效的 gRPC 协议(底层为 HTTP2 协议)。你可能听说过,HTTP2 不但具备多路复用的能力,还支持服务器直接推送消息,关于 HTTP2 的细节可以参考[《深入剖析HTTP3协议》](https://www.taohui.pub/2021/02/04/%E7%BD%91%E7%BB%9C%E5%8D%8F%E8%AE%AE/%E6%B7%B1%E5%85%A5%E5%89%96%E6%9E%90HTTP3%E5%8D%8F%E8%AE%AE/),从 HTTP3 协议对照理解 HTTP2 :
+
+![http2_stream_frame_conn](https://static.apiseven.com/202108/1631170499370-57a7c452-e97e-4ac0-b7bf-073e13946a21.png)
+
+然而,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 是 Base64 编码的 watch 目录:
+
+![APISIX 与 etcd 通过 HTTP1 通讯](https://static.apiseven.com/202108/1631170602368-d105d014-efe4-48c7-93b8-be5447c76a70.jpeg)
+
+我们可以验证下 `watchdir` 函数的实现细节:
+
+```lua
+-- 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 进程中都含有最新的配置。**
+
+## APISIX 配置与插件的远程变更

Review comment:
       ditto

##########
File path: website/blog/2021-08-10-apisix-nginx.md
##########
@@ -0,0 +1,512 @@
+---
+title: "Apache APISIX 架构分析:如何动态管理 Nginx 集群?"
+keywords: 
+- APISIX
+- Nginx
+- 动态管理
+- Lua
+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 作为 CNCF 毕业的开源七层网关,基于 etcd 和 Lua 实现了对 Nginx 集群的动态管理。
+
+![APISIX 架构图](https://static.apiseven.com/202108/1631170283612-ba5e27ff-726b-47a6-aa51-84731b067c44.png)
+
+让 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 客户端会立刻收到通知,如下图所示:
+![基于 etcd 同步 nginx 配置](https://static.apiseven.com/202108/1631170345853-f020a64d-3e97-49c0-8395-c9e4e9cf4233.jpeg)
+
+因此,不同于 [Orange](https://github.com/orlabs/orange) 采用 MySQL、[Kong](https://konghq.com/) 采用 PostgreSQL 作为配置中心的方式(这二者同样是基于 OpenResty 实现的 API Gateway),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 语言形式暴露了出来,如下图所示:
+
+![openresty 钩子](https://static.apiseven.com/202108/1631170424663-53f56c99-aefc-4546-ac0b-76a25a6f0071.png)
+
+APISIX 仅使用了其中 8 个钩子(注意,APISIX 没有使用 `set_by_lua` 和 `rewrite_by_lua`,rewrite 阶段的插件其实是 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/)。你可能很好奇,下载 APISIX 源码后没有看到 nginx.conf,这段配置是哪来的?
+
+这里的 nginx.conf 实际是由 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)
+```
+
+当然,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 通知的方式
+
+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 对象:
+
+```lua
+function _M.init_worker()
+    local err
+    plugin_configs, err = core.config.new("/plugin_configs", {
+        automatic = true,
+        item_schema = core.schema.plugin_config,
+        checker = plugin_checker,
+    })
+end
+```
+
+而在 `config_etcd` 的 new 函数中,则会循环注册 `_automatic_fetch` 定时器:
+
+```lua
+function _M.new(key, opts)
+    ngx_timer_at(0, _automatic_fetch, obj)
+end
+```
+
+`_automatic_fetch` 函数会反复执行 `sync_data` 函数(包装到 xpcall 之下是为了捕获异常):
+
+```lua
+local function _automatic_fetch(premature, self)
+    local ok, err = xpcall(function()
+        local ok, err = sync_data(self)
+    end, debug.traceback)
+    ngx_timer_at(0, _automatic_fetch, self)
+end
+```
+
+`sync_data` 函数将通过 etcd 的 watch 机制获取更新,它的实现机制我们接下来会详细分析。
+
+所以总结来看,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` 源码:
+
+```lua
+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](https://github.com/api7/lua-resty-etcd) 库。它提供的 watchdir 函数用于接收 etcd 发现 key 目录对应 value 变更后发出的通知。
+
+watchcancel 函数又是做什么的呢?这其实是 OpenResty 生态的缺憾导致的。etcd v3 已经支持高效的 gRPC 协议(底层为 HTTP2 协议)。你可能听说过,HTTP2 不但具备多路复用的能力,还支持服务器直接推送消息,关于 HTTP2 的细节可以参考[《深入剖析HTTP3协议》](https://www.taohui.pub/2021/02/04/%E7%BD%91%E7%BB%9C%E5%8D%8F%E8%AE%AE/%E6%B7%B1%E5%85%A5%E5%89%96%E6%9E%90HTTP3%E5%8D%8F%E8%AE%AE/),从 HTTP3 协议对照理解 HTTP2 :
+
+![http2_stream_frame_conn](https://static.apiseven.com/202108/1631170499370-57a7c452-e97e-4ac0-b7bf-073e13946a21.png)
+
+然而,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 是 Base64 编码的 watch 目录:
+
+![APISIX 与 etcd 通过 HTTP1 通讯](https://static.apiseven.com/202108/1631170602368-d105d014-efe4-48c7-93b8-be5447c76a70.jpeg)
+
+我们可以验证下 `watchdir` 函数的实现细节:
+
+```lua
+-- 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 进程中都含有最新的配置。**

Review comment:
       Apache APISIX

##########
File path: website/blog/2021-08-10-apisix-nginx.md
##########
@@ -0,0 +1,512 @@
+---
+title: "Apache APISIX 架构分析:如何动态管理 Nginx 集群?"
+keywords: 
+- APISIX
+- Nginx
+- 动态管理
+- Lua

Review comment:
       ```
   - API 网关
   - Apache APISIX
   - Nginx
   - Lua
   - 动态管理
   ```

##########
File path: website/blog/2021-08-10-apisix-nginx.md
##########
@@ -0,0 +1,512 @@
+---
+title: "Apache APISIX 架构分析:如何动态管理 Nginx 集群?"
+keywords: 
+- APISIX
+- Nginx
+- 动态管理
+- Lua
+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 作为 CNCF 毕业的开源七层网关,基于 etcd 和 Lua 实现了对 Nginx 集群的动态管理。
+
+![APISIX 架构图](https://static.apiseven.com/202108/1631170283612-ba5e27ff-726b-47a6-aa51-84731b067c44.png)
+
+让 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 客户端会立刻收到通知,如下图所示:
+![基于 etcd 同步 nginx 配置](https://static.apiseven.com/202108/1631170345853-f020a64d-3e97-49c0-8395-c9e4e9cf4233.jpeg)
+
+因此,不同于 [Orange](https://github.com/orlabs/orange) 采用 MySQL、[Kong](https://konghq.com/) 采用 PostgreSQL 作为配置中心的方式(这二者同样是基于 OpenResty 实现的 API Gateway),Apache APISIX 采用了 etcd 作为中心化的配置组件。

Review comment:
       “不同于 orange 和 kong”

##########
File path: website/blog/2021-08-10-apisix-nginx.md
##########
@@ -0,0 +1,512 @@
+---
+title: "Apache APISIX 架构分析:如何动态管理 Nginx 集群?"
+keywords: 
+- APISIX
+- Nginx
+- 动态管理
+- Lua
+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 作为 CNCF 毕业的开源七层网关,基于 etcd 和 Lua 实现了对 Nginx 集群的动态管理。
+
+![APISIX 架构图](https://static.apiseven.com/202108/1631170283612-ba5e27ff-726b-47a6-aa51-84731b067c44.png)
+
+让 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 客户端会立刻收到通知,如下图所示:
+![基于 etcd 同步 nginx 配置](https://static.apiseven.com/202108/1631170345853-f020a64d-3e97-49c0-8395-c9e4e9cf4233.jpeg)
+
+因此,不同于 [Orange](https://github.com/orlabs/orange) 采用 MySQL、[Kong](https://konghq.com/) 采用 PostgreSQL 作为配置中心的方式(这二者同样是基于 OpenResty 实现的 API Gateway),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 通讯的?

Review comment:
       agent 进程

##########
File path: website/blog/2021-08-10-apisix-nginx.md
##########
@@ -0,0 +1,512 @@
+---
+title: "Apache APISIX 架构分析:如何动态管理 Nginx 集群?"
+keywords: 
+- APISIX
+- Nginx
+- 动态管理
+- Lua
+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 作为 CNCF 毕业的开源七层网关,基于 etcd 和 Lua 实现了对 Nginx 集群的动态管理。
+
+![APISIX 架构图](https://static.apiseven.com/202108/1631170283612-ba5e27ff-726b-47a6-aa51-84731b067c44.png)
+
+让 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 客户端会立刻收到通知,如下图所示:
+![基于 etcd 同步 nginx 配置](https://static.apiseven.com/202108/1631170345853-f020a64d-3e97-49c0-8395-c9e4e9cf4233.jpeg)
+
+因此,不同于 [Orange](https://github.com/orlabs/orange) 采用 MySQL、[Kong](https://konghq.com/) 采用 PostgreSQL 作为配置中心的方式(这二者同样是基于 OpenResty 实现的 API Gateway),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 机制实现的基础。

Review comment:
       为了方便对 OpenResty 不太了解的同学理解

##########
File path: website/blog/2021-08-10-apisix-nginx.md
##########
@@ -0,0 +1,512 @@
+---
+title: "Apache APISIX 架构分析:如何动态管理 Nginx 集群?"
+keywords: 
+- APISIX
+- Nginx
+- 动态管理
+- Lua
+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 作为 CNCF 毕业的开源七层网关,基于 etcd 和 Lua 实现了对 Nginx 集群的动态管理。
+
+![APISIX 架构图](https://static.apiseven.com/202108/1631170283612-ba5e27ff-726b-47a6-aa51-84731b067c44.png)
+
+让 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 客户端会立刻收到通知,如下图所示:
+![基于 etcd 同步 nginx 配置](https://static.apiseven.com/202108/1631170345853-f020a64d-3e97-49c0-8395-c9e4e9cf4233.jpeg)
+
+因此,不同于 [Orange](https://github.com/orlabs/orange) 采用 MySQL、[Kong](https://konghq.com/) 采用 PostgreSQL 作为配置中心的方式(这二者同样是基于 OpenResty 实现的 API Gateway),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 语言形式暴露了出来,如下图所示:
+
+![openresty 钩子](https://static.apiseven.com/202108/1631170424663-53f56c99-aefc-4546-ac0b-76a25a6f0071.png)
+
+APISIX 仅使用了其中 8 个钩子(注意,APISIX 没有使用 `set_by_lua` 和 `rewrite_by_lua`,rewrite 阶段的插件其实是 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/)。你可能很好奇,下载 APISIX 源码后没有看到 nginx.conf,这段配置是哪来的?
+
+这里的 nginx.conf 实际是由 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)
+```
+
+当然,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 通知的方式
+
+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 对象:
+
+```lua
+function _M.init_worker()
+    local err
+    plugin_configs, err = core.config.new("/plugin_configs", {
+        automatic = true,
+        item_schema = core.schema.plugin_config,
+        checker = plugin_checker,
+    })
+end
+```
+
+而在 `config_etcd` 的 new 函数中,则会循环注册 `_automatic_fetch` 定时器:
+
+```lua
+function _M.new(key, opts)
+    ngx_timer_at(0, _automatic_fetch, obj)
+end
+```
+
+`_automatic_fetch` 函数会反复执行 `sync_data` 函数(包装到 xpcall 之下是为了捕获异常):
+
+```lua
+local function _automatic_fetch(premature, self)
+    local ok, err = xpcall(function()
+        local ok, err = sync_data(self)
+    end, debug.traceback)
+    ngx_timer_at(0, _automatic_fetch, self)
+end
+```
+
+`sync_data` 函数将通过 etcd 的 watch 机制获取更新,它的实现机制我们接下来会详细分析。
+
+所以总结来看,APISIX 在每个 Nginx Worker 进程的启动过程中,通过 `ngx.timer.at` 函数将 `_automatic_fetch` 插入定时器。`_automatic_fetch` 函数执行时会通过 `sync_data` 函数,基于 watch 机制接收 etcd 中的配置变更通知,这样,每个 Nginx 节点、Worker 进程都将保持最新的配置。如此设计还有 1 个明显的优点:etcd 中的配置直接写入 Nginx Worker 进程中,这样处理请求时就能直接使用新配置,无须在进程间同步配置,这要比启动 1 个 agent 进程更简单!

Review comment:
       Apache APISIX

##########
File path: website/blog/2021-08-10-apisix-nginx.md
##########
@@ -0,0 +1,512 @@
+---
+title: "Apache APISIX 架构分析:如何动态管理 Nginx 集群?"
+keywords: 
+- APISIX
+- Nginx
+- 动态管理
+- Lua
+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 作为 CNCF 毕业的开源七层网关,基于 etcd 和 Lua 实现了对 Nginx 集群的动态管理。
+
+![APISIX 架构图](https://static.apiseven.com/202108/1631170283612-ba5e27ff-726b-47a6-aa51-84731b067c44.png)
+
+让 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 客户端会立刻收到通知,如下图所示:
+![基于 etcd 同步 nginx 配置](https://static.apiseven.com/202108/1631170345853-f020a64d-3e97-49c0-8395-c9e4e9cf4233.jpeg)
+
+因此,不同于 [Orange](https://github.com/orlabs/orange) 采用 MySQL、[Kong](https://konghq.com/) 采用 PostgreSQL 作为配置中心的方式(这二者同样是基于 OpenResty 实现的 API Gateway),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 语言形式暴露了出来,如下图所示:
+
+![openresty 钩子](https://static.apiseven.com/202108/1631170424663-53f56c99-aefc-4546-ac0b-76a25a6f0071.png)
+
+APISIX 仅使用了其中 8 个钩子(注意,APISIX 没有使用 `set_by_lua` 和 `rewrite_by_lua`,rewrite 阶段的插件其实是 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/)。你可能很好奇,下载 APISIX 源码后没有看到 nginx.conf,这段配置是哪来的?
+
+这里的 nginx.conf 实际是由 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)
+```
+
+当然,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 通知的方式
+
+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 对象:
+
+```lua
+function _M.init_worker()
+    local err
+    plugin_configs, err = core.config.new("/plugin_configs", {
+        automatic = true,
+        item_schema = core.schema.plugin_config,
+        checker = plugin_checker,
+    })
+end
+```
+
+而在 `config_etcd` 的 new 函数中,则会循环注册 `_automatic_fetch` 定时器:
+
+```lua
+function _M.new(key, opts)
+    ngx_timer_at(0, _automatic_fetch, obj)
+end
+```
+
+`_automatic_fetch` 函数会反复执行 `sync_data` 函数(包装到 xpcall 之下是为了捕获异常):
+
+```lua
+local function _automatic_fetch(premature, self)
+    local ok, err = xpcall(function()
+        local ok, err = sync_data(self)
+    end, debug.traceback)
+    ngx_timer_at(0, _automatic_fetch, self)
+end
+```
+
+`sync_data` 函数将通过 etcd 的 watch 机制获取更新,它的实现机制我们接下来会详细分析。
+
+所以总结来看,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` 源码:
+
+```lua
+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](https://github.com/api7/lua-resty-etcd) 库。它提供的 watchdir 函数用于接收 etcd 发现 key 目录对应 value 变更后发出的通知。
+
+watchcancel 函数又是做什么的呢?这其实是 OpenResty 生态的缺憾导致的。etcd v3 已经支持高效的 gRPC 协议(底层为 HTTP2 协议)。你可能听说过,HTTP2 不但具备多路复用的能力,还支持服务器直接推送消息,关于 HTTP2 的细节可以参考[《深入剖析HTTP3协议》](https://www.taohui.pub/2021/02/04/%E7%BD%91%E7%BB%9C%E5%8D%8F%E8%AE%AE/%E6%B7%B1%E5%85%A5%E5%89%96%E6%9E%90HTTP3%E5%8D%8F%E8%AE%AE/),从 HTTP3 协议对照理解 HTTP2 :
+
+![http2_stream_frame_conn](https://static.apiseven.com/202108/1631170499370-57a7c452-e97e-4ac0-b7bf-073e13946a21.png)
+
+然而,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 是 Base64 编码的 watch 目录:
+
+![APISIX 与 etcd 通过 HTTP1 通讯](https://static.apiseven.com/202108/1631170602368-d105d014-efe4-48c7-93b8-be5447c76a70.jpeg)
+
+我们可以验证下 `watchdir` 函数的实现细节:
+
+```lua
+-- 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 进程中都含有最新的配置。**
+
+## APISIX 配置与插件的远程变更
+
+接下来,我们看看怎样远程修改 etcd 中的配置。
+
+我们当然可以直接通过 gRPC 接口修改 etcd 中相应 key 的内容,再基于上述的 watch 机制使得 Nginx 集群自动更新配置。然而,这样做的风险很大,因为配置请求没有经过校验,进面导致配置数据与 Nginx 集群不匹配。
+
+### 通过 Nginx 的 /apisix/admin/ 接口修改配置
+
+APISIX 提供了这么一种机制:访问任意 1 个 Nginx 节点,通过其 Worker 进程中的 Lua 代码校验请求成功后,再由 /v3/dv/put 接口写入 etcd 中。下面我们来看看 APISIX 是怎么实现的。
+
+首先,make run 生成的 nginx.conf 会自动监听 9080 端口(可通过 config.yaml 中 apisix.node_listen 配置修改),当 `apisix.enable_admin` 设置为 true 时,nginx.conf 就会生成以下配置:
+
+```conf

Review comment:
       "```yaml"

##########
File path: website/blog/2021-08-10-apisix-nginx.md
##########
@@ -0,0 +1,512 @@
+---
+title: "Apache APISIX 架构分析:如何动态管理 Nginx 集群?"
+keywords: 
+- APISIX
+- Nginx
+- 动态管理
+- Lua
+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 作为 CNCF 毕业的开源七层网关,基于 etcd 和 Lua 实现了对 Nginx 集群的动态管理。
+
+![APISIX 架构图](https://static.apiseven.com/202108/1631170283612-ba5e27ff-726b-47a6-aa51-84731b067c44.png)
+
+让 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 客户端会立刻收到通知,如下图所示:
+![基于 etcd 同步 nginx 配置](https://static.apiseven.com/202108/1631170345853-f020a64d-3e97-49c0-8395-c9e4e9cf4233.jpeg)
+
+因此,不同于 [Orange](https://github.com/orlabs/orange) 采用 MySQL、[Kong](https://konghq.com/) 采用 PostgreSQL 作为配置中心的方式(这二者同样是基于 OpenResty 实现的 API Gateway),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 语言形式暴露了出来,如下图所示:
+
+![openresty 钩子](https://static.apiseven.com/202108/1631170424663-53f56c99-aefc-4546-ac0b-76a25a6f0071.png)
+
+APISIX 仅使用了其中 8 个钩子(注意,APISIX 没有使用 `set_by_lua` 和 `rewrite_by_lua`,rewrite 阶段的插件其实是 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/)。你可能很好奇,下载 APISIX 源码后没有看到 nginx.conf,这段配置是哪来的?
+
+这里的 nginx.conf 实际是由 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)
+```
+
+当然,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 通知的方式
+
+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 对象:
+
+```lua
+function _M.init_worker()
+    local err
+    plugin_configs, err = core.config.new("/plugin_configs", {
+        automatic = true,
+        item_schema = core.schema.plugin_config,
+        checker = plugin_checker,
+    })
+end
+```
+
+而在 `config_etcd` 的 new 函数中,则会循环注册 `_automatic_fetch` 定时器:
+
+```lua
+function _M.new(key, opts)
+    ngx_timer_at(0, _automatic_fetch, obj)
+end
+```
+
+`_automatic_fetch` 函数会反复执行 `sync_data` 函数(包装到 xpcall 之下是为了捕获异常):
+
+```lua
+local function _automatic_fetch(premature, self)
+    local ok, err = xpcall(function()
+        local ok, err = sync_data(self)
+    end, debug.traceback)
+    ngx_timer_at(0, _automatic_fetch, self)
+end
+```
+
+`sync_data` 函数将通过 etcd 的 watch 机制获取更新,它的实现机制我们接下来会详细分析。
+
+所以总结来看,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` 源码:
+
+```lua
+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](https://github.com/api7/lua-resty-etcd) 库。它提供的 watchdir 函数用于接收 etcd 发现 key 目录对应 value 变更后发出的通知。
+
+watchcancel 函数又是做什么的呢?这其实是 OpenResty 生态的缺憾导致的。etcd v3 已经支持高效的 gRPC 协议(底层为 HTTP2 协议)。你可能听说过,HTTP2 不但具备多路复用的能力,还支持服务器直接推送消息,关于 HTTP2 的细节可以参考[《深入剖析HTTP3协议》](https://www.taohui.pub/2021/02/04/%E7%BD%91%E7%BB%9C%E5%8D%8F%E8%AE%AE/%E6%B7%B1%E5%85%A5%E5%89%96%E6%9E%90HTTP3%E5%8D%8F%E8%AE%AE/),从 HTTP3 协议对照理解 HTTP2 :
+
+![http2_stream_frame_conn](https://static.apiseven.com/202108/1631170499370-57a7c452-e97e-4ac0-b7bf-073e13946a21.png)
+
+然而,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 是 Base64 编码的 watch 目录:
+
+![APISIX 与 etcd 通过 HTTP1 通讯](https://static.apiseven.com/202108/1631170602368-d105d014-efe4-48c7-93b8-be5447c76a70.jpeg)
+
+我们可以验证下 `watchdir` 函数的实现细节:
+
+```lua
+-- 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 进程中都含有最新的配置。**
+
+## APISIX 配置与插件的远程变更
+
+接下来,我们看看怎样远程修改 etcd 中的配置。
+
+我们当然可以直接通过 gRPC 接口修改 etcd 中相应 key 的内容,再基于上述的 watch 机制使得 Nginx 集群自动更新配置。然而,这样做的风险很大,因为配置请求没有经过校验,进面导致配置数据与 Nginx 集群不匹配。
+
+### 通过 Nginx 的 /apisix/admin/ 接口修改配置
+
+APISIX 提供了这么一种机制:访问任意 1 个 Nginx 节点,通过其 Worker 进程中的 Lua 代码校验请求成功后,再由 /v3/dv/put 接口写入 etcd 中。下面我们来看看 APISIX 是怎么实现的。
+
+首先,make run 生成的 nginx.conf 会自动监听 9080 端口(可通过 config.yaml 中 apisix.node_listen 配置修改),当 `apisix.enable_admin` 设置为 true 时,nginx.conf 就会生成以下配置:
+
+```conf
+server {
+    listen 9080 default_server reuseport;
+
+    location/apisix/admin {
+        content_by_lua_block {
+            apisix.http_admin()
+        }
+    }
+}
+
+```
+
+这样,Nginx 接收到的 /apisix/admin 请求将被 `http_admin` 函数处理:
+
+```lua
+-- /apisix/init.lua 文件
+function _M.http_admin()
+    local ok = router:dispatch(get_var("uri"), {method = get_method()})
+end
+```
+
+admin 接口能够处理的 API 参见 [GitHub](https://github.com/apache/apisix/blob/release/2.8/docs/zh/latest/admin-api.md) 文档,其中,当 method 方法与 URI 不同时,dispatch 会执行不同的处理函数,其依据如下:
+
+```lua
+-- /apisix/admin/init.lua 文件
+local uri_route = {
+    {
+        paths = [[/apisix/admin/*]],
+        methods = {"GET", "PUT", "POST", "DELETE", "PATCH"},
+        handler = run,
+    },
+    {
+        paths = [[/apisix/admin/stream_routes/*]],
+        methods = {"GET", "PUT", "POST", "DELETE", "PATCH"},
+        handler = run_stream,
+    },
+    {
+        paths = [[/apisix/admin/plugins/list]],
+        methods = {"GET"},
+        handler = get_plugins_list,
+    },
+    {
+        paths = reload_event,
+        methods = {"PUT"},
+        handler = post_reload_plugins,
+    },
+}
+```
+
+比如,当通过 /apisix/admin/upstreams/1 和 PUT 方法创建 1 个 Upstream 上游时:
+
+```shell
+# curl "http://127.0.0.1:9080/apisix/admin/upstreams/1" -H "X-API-KEY: edd1c9f034335f136f87ad84b625c8f1" -X PUT -d '

Review comment:
       delete ”# “




-- 
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: notifications-unsubscribe@apisix.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org