開源版 Nginx 最為人詬病的就是不具備動態(tài)配置、遠程 API 及集群管理的能力,而 APISIX 作為 CNCF 畢業(yè)的開源七層網(wǎng)關,基于 etcd、Lua 實現(xiàn)了對 Nginx 集群的動態(tài)管理。
讓 Nginx 具備動態(tài)、集群管理能力并不容易,因為這將面臨以下問題:
- 微服務架構使得上游服務種類多、數(shù)量大,這導致路由規(guī)則、上游 Server 的變更極為頻率。而 Nginx 的路由匹配是基于靜態(tài)的 Trie 前綴樹、哈希表、正則數(shù)組實現(xiàn)的,一旦 server_name、location 變動,不執(zhí)行 reload 就無法實現(xiàn)配置的動態(tài)變更;
- Nginx 將自己定位于 ADC 邊緣負載均衡,因此它對上游并不支持 HTTP2 協(xié)議。這增大了 OpenResty 生態(tài)實現(xiàn) etcd gRPC 接口的難度,因此通過 watch 機制接收配置變更必然效率低下;
- 多進程架構增大了 Worker 進程間的數(shù)據(jù)同步難度,必須選擇 1 個低成本的實現(xiàn)機制,保證每個 Nginx 節(jié)點、Worker 進程都持有最新的配置;
APISIX 基于 Lua 定時器及 lua-resty-etcd 模塊實現(xiàn)了配置的動態(tài)管理,本文將基于 APISIX2.8、OpenResty1.19.3.2、Nginx1.19.3 分析 APISIX 實現(xiàn) REST API 遠程控制 Nginx 集群的原理。
基于 etcd watch 機制的配置同步方案
管理集群必須依賴中心化的配置,etcd 就是這樣一個數(shù)據(jù)庫。APISIX 沒有選擇關系型數(shù)據(jù)庫作為配置中心,是因為 etcd 具有以下 2 個優(yōu)點:
- etcd 采用類 Paxos 的 Raft 協(xié)議保障了數(shù)據(jù)一致性,它是去中心化的分布式數(shù)據(jù)庫,可靠性高于關系數(shù)據(jù)庫;
- etcd 的 watch 機制允許客戶端監(jiān)控某個 key 的變動,即,若類似/nginx/http/upstream 這種 key 的 value 值發(fā)生變動,watch 的客戶端會立刻收到通知,如下圖所示:
因此,不同于Orange采用 MySQL、Kong采用 PostgreSQL 作為配置中心(這二者同樣是基于 OpenResty 實現(xiàn)的 API Gateway),APISIX 采用了 etcd 作為中心化的配置組件。
因此,你可以在生產(chǎn)環(huán)境的 APISIX 中通過 etcdctl 看到如下的類似配置:
$ 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 中修改,比如:
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 配置數(shù)據(jù)變化的呢?有沒有新啟動一個 agent 進程?它通過 HTTP/1.1 還是 gRPC 與 etcd 通訊的?
ngx.timer.at 定時器
APISIX 并沒有啟動 Nginx 以外的進程與 etcd 通訊。它實際上是通過 ngx.timer.at 這個定時器實現(xiàn)了 watch 機制。為了方便對 OpenResty 不太了解的同學,我們先來看看 Nginx 中的定時器是如何實現(xiàn)的,它是 watch 機制實現(xiàn)的基礎。
Nginx 的紅黑樹定時器
Nginx 采用了 epoll + nonblock socket 這種多路復用機制實現(xiàn)事件處理模型,其中每個 worker 進程會循環(huán)處理網(wǎng)絡 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 函數(shù)會調(diào)用所有超時事件的 handler 方法。事實上,定時器是由紅黑樹(一種平衡有序二叉樹)實現(xiàn)的,其中 key 是每個事件的絕對過期時間。這樣,只要將最小節(jié)點與當前時間做比較,就能快速找到過期事件。
OpenResty 的 Lua 定時器
當然,以上 C 函數(shù)開發(fā)效率很低。因此,OpenResty 封裝了 Lua 接口,通過ngx.timer.at將 ngx_timer_add 這個 C 函數(shù)暴露給了 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);
}
因此,當我們調(diào)用 ngx.timer.at 這個 Lua 定時器時,就是在 Nginx 的紅黑樹定時器里加入了 ngx_http_lua_timer_handler 回調(diào)函數(shù),這個函數(shù)不會阻塞 Nginx。
下面我們來看看 APISIX 是怎樣使用 ngx.timer.at 的。
APISIX 基于定時器實現(xiàn)的 watch 機制
Nginx 框架為 C 模塊開發(fā)提供了許多鉤子,而 OpenResty 將部分鉤子以 Lua 語言形式暴露了出來,如下圖所示:
APISIX 僅使用了其中 8 個鉤子(注意,APISIX 沒有使用 set_by_lua 和 rewrite_by_lua,rewrite 階段的 plugin 其實是 APISIX 自定義的,與 Nginx 無關),包括:
- init_by_lua:Master 進程啟動時的初始化;
- init_worker_by_lua:每個 Worker 進程啟動時的初始化(包括 privileged agent 進程的初始化,這是實現(xiàn) JAVA 等多語言 plugin 遠程 RPC 調(diào)用的關鍵);
- ssl_certificate_by_lua:在處理 TLS 握手時,openssl 提供了一個鉤子,OpenResty 通過修改 Nginx 源碼以 Lua 方式暴露了該鉤子;
- access_by_lua:接收到下游的 HTTP 請求頭部后,在此匹配 Host 域名、URI、Method 等路由規(guī)則,并選擇 Service、Upstream 中的 Plugin 及上游 Server;
- balancer_by_lua:在 content 階段執(zhí)行的所有反向代理模塊,在選擇上游 Server 時都會回調(diào) init_upstream 鉤子函數(shù),OpenResty 將其命名為 balancer_by_lua;
- header_filter_by_lua:將 HTTP 響應頭部發(fā)送給下游前執(zhí)行的鉤子;
- body_filter_by_lua:將 HTTP 響應包體發(fā)送給下游前執(zhí)行的鉤子;
- log_by_lua:記錄 access 日志時的鉤子。
準備好上述知識后,我們就可以回答 APISIX 是怎樣接收 etcd 數(shù)據(jù)的更新了。
nginx.conf 的生成方式
每個 Nginx Worker 進程都會在 init_worker_by_lua 階段通過 http_init_worker 函數(shù)啟動定時器:
init_worker_by_lua_block {
apisix.http_init_worker()
}
你可能很好奇,下載 APISIX 源碼后沒有看到 nginx.conf,這段配置是哪來的?
這里的 nginx.conf 實際是由 APISIX 的啟動命令實時生成的。當你執(zhí)行 make run 時,它會基于 Lua 模板 apisix/cli/ngx_tpl.lua 文件生成 nginx.conf。請注意,這里的模板規(guī)則是 OpenResty 自實現(xiàn)的,語法細節(jié)參見lua-resty-template。生成 nginx.conf 的具體代碼參見 apisix/cli/ops.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 模板中的部分數(shù)據(jù),具體方法是模仿 conf/config-default.yaml 的語法修改 conf/config.yaml 配置。其實現(xiàn)原理參見 read_yaml_conf 函數(shù):
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 模板中僅部分數(shù)據(jù)可由 yaml 配置中替換,其中 conf/config-default.yaml 是官方提供的默認配置,而 conf/config.yaml 則是由用戶自行覆蓋的自定義配置。如果你覺得僅替換模板數(shù)據(jù)還不夠,大可以直接修改 ngx_tpl 模板。
APISIX 獲取 etcd 通知的方式
APISIX 將需要監(jiān)控的配置以不同的前綴存入了 etcd,目前包括以下 11 種:
- /apisix/consumers/:APISIX 支持以 consumer 抽象上游種類;
- /apisix/global_rules/:全局通用的規(guī)則;
- /apisix/plugin_configs/:可以在不同 Router 間復用的 Plugin;
- /apisix/plugin_metadata/:部分插件的元數(shù)據(jù);
- /apisix/plugins/:所有 Plugin 插件的列表;
- /apisix/proto/:當透傳 gRPC 協(xié)議時,部分插件需要轉換協(xié)議內(nèi)容,該配置存儲 protobuf 消息定義;
- /apisix/routes/:路由信息,是 HTTP 請求匹配的入口,可以直接指定上游 Server,也可以掛載 services 或者 upstream;
- /apisix/services/:可以將相似的 router 中的共性部分抽象為 services,再掛載 plugin;
- /apisix/ssl/:SSL 證書公、私鑰及相關匹配規(guī)則;
- /apisix/stream_routes/:OSI 四層網(wǎng)關的路由匹配規(guī)則;
- /apisix/upstreams/:對一組上游 Server 主機的抽象;
這里每類配置對應的處理邏輯都不相同,因此 APISIX 抽象出 apisix/core/config_etcd.lua 文件,專注 etcd 上各類配置的更新維護。在 http_init_worker 函數(shù)中每類配置都會生成 1 個 config_etcd 對象:
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 函數(shù)中,則會循環(huán)注冊_automatic_fetch 定時器:
function _M.new(key, opts)
ngx_timer_at(0, _automatic_fetch, obj)
end
_automatic_fetch 函數(shù)會反復執(zhí)行 sync_data 函數(shù)(包裝到 xpcall 之下是為了捕獲異常):
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 函數(shù)將通過 etcd 的 watch 機制獲取更新,它的實現(xiàn)機制我們接下來會詳細分析。
總結下:
APISIX 在每個 Nginx Worker 進程的啟動過程中,通過 ngx.timer.at 函數(shù)將_automatic_fetch 插入定時器。_automatic_fetch 函數(shù)執(zhí)行時會通過 sync_data 函數(shù),基于 watch 機制接收 etcd 中的配置變更通知,這樣,每個 Nginx 節(jié)點、每個 Worker 進程都將保持最新的配置。如此設計還有 1 個明顯的優(yōu)點:etcd 中的配置直接寫入 Nginx Worker 進程中,這樣處理請求時就能直接使用新配置,無須在進程間同步配置,這要比啟動 1 個 agent 進程更簡單!
lua-resty-etcd 庫的 HTTP/1.1 協(xié)議
sync_data 函數(shù)到底是怎樣獲取 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 函數(shù)用于接收 etcd 發(fā)現(xiàn) key 目錄對應 value 變更后發(fā)出的通知。
watchcancel 函數(shù)又是做什么的呢?這其實是 OpenResty 生態(tài)的缺憾導致的。etcd v3 已經(jīng)支持高效的 gRPC 協(xié)議(底層為 HTTP2 協(xié)議)。你可能聽說過,HTTP2 不但具備多路復用的能力,還支持服務器直接推送消息,從 HTTP3 協(xié)議對照理解 HTTP2:
然而,**Lua 生態(tài)目前并不支持 HTTP2 協(xié)議!**所以 lua-resty-etcd 庫實際是通過低效的 HTTP/1.1 協(xié)議與 etcd 通訊的,因此接收/watch 通知也是通過帶有超時的/v3/watch 請求完成的。這個現(xiàn)象其實是由 2 個原因造成的:
- Nginx 將自己定位為邊緣負載均衡,因此上游必然是企業(yè)內(nèi)網(wǎng),時延低、帶寬大,所以對上游協(xié)議不必支持 HTTP2 協(xié)議!
- 當 Nginx 的 upstream 不能提供 HTTP2 機制給 Lua 時,Lua 只能基于 cosocket 自己實現(xiàn)了。HTTP2 協(xié)議非常復雜,目前還沒有生產(chǎn)環(huán)境可用的 HTTP2 cosocket 庫。
使用 HTTP/1.1 的 lua-resty-etcd 庫其實很低效,如果你在 APISIX 上抓包,會看到頻繁的 POST 報文,其中 URI 為/v3/watch,而 Body 是 編碼的 watch 目錄:
我們可以驗證下 watchdir 函數(shù)的實現(xiàn)細節(jié):
-- 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()
-- 發(fā)起TCP連接
endpoint, err = http_request_chunk(self, http_cli)
-- 發(fā)送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 的內(nèi)容,再基于上述的 watch 機制使得 Nginx 集群自動更新配置。然而,這樣做的風險很大,因為配置請求沒有經(jīng)過校驗,進面導致配置數(shù)據(jù)與 Nginx 集群不匹配!
通過 Nginx 的/apisix/admin/接口修改配置
APISIX 提供了這么一種機制:訪問任意 1 個 Nginx 節(jié)點,通過其 Worker 進程中的 Lua 代碼校驗請求成功后,再由/v3/dv/put 接口寫入 etcd 中。下面我們來看看 APISIX 是怎么實現(xiàn)的。
首先,make run 生成的 nginx.conf 會自動監(jiān)聽 9080 端口(可通過 config.yaml 中 apisix.node_listen 配置修改),當 apisix.enable_admin 設置為 true 時,nginx.conf 就會生成以下配置:
server {
listen 9080 default_server reuseport;
location /apisix/admin {
content_by_lua_block {
apisix.http_admin()
}
}
}
這樣,Nginx 接收到的/apisix/admin 請求將被 http_admin 函數(shù)處理:
-- /apisix/init.lua文件
function _M.http_admin()
local ok = router:dispatch(get_var("uri"), {method = get_method()})
end
admin 接口能夠處理的 API ,其中,當 method 方法與 URI 不同時,dispatch 會執(zhí)行不同的處理函數(shù),其依據(jù)如下:
-- /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 方法創(chuàng)建 1 個 Upstream 上游時:
$ curl "http://127.0.0.1:9080/apisix/admin/upstreams/1" -H "X-API-KEY: edd1c9f034335f136f87ad84b625c8f1" -X PUT -d '
> {
> "type": "roundrobin",
> "nodes": {
> "httpbin.org:80": 1
> }
> }'
{"action":"set","node":{"key":"/apisix/upstreams/1","value":{"hash_on":"vars","nodes":{"httpbin.org:80":1},"create_time":1627982128,"update_time":1627982128,"scheme":"http","type":"roundrobin","pass_host":"pass","id":"1"}}}
你會在 error.log 中會看到如下日志(想看到這行日志,必須將 config.yaml 中的 nginx_config.error_log_level 設為 INFO):
2021/08/03 17:15:28 [info] 16437#16437: *23572 [lua] init.lua:130: handler(): uri: ["","apisix","admin","upstreams","1"], client: 127.0.0.1, server: _, request: "PUT /apisix/admin/upstreams/1 HTTP/1.1", host: "127.0.0.1:9080"
這行日志實際是由/apisix/admin/init.lua 中的 run 函數(shù)打印的,它的執(zhí)行依據(jù)是上面的 uri_route 字典。我們看下 run 函數(shù)的內(nèi)容:
-- /apisix/admin/init.lua文件
local function run()
local uri_segs = core.utils.split_uri(ngx.var.uri)
core.log.info("uri: ", core.json.delay_encode(uri_segs))
local seg_res, seg_id = uri_segs[4], uri_segs[5]
local seg_sub_path = core.table.concat(uri_segs, "/", 6)
local resource = resources[seg_res]
local code, data = resource[method](seg_id, req_body, seg_sub_path,
uri_args)
end
這里 resource[method]函數(shù)又被做了 1 次抽象,它是由 resources 字典決定的:
-- /apisix/admin/init.lua文件
local resources = {
routes = require("apisix.admin.routes"),
services = require("apisix.admin.services"),
upstreams = require("apisix.admin.upstreams"),
consumers = require("apisix.admin.consumers"),
schema = require("apisix.admin.schema"),
ssl = require("apisix.admin.ssl"),
plugins = require("apisix.admin.plugins"),
proto = require("apisix.admin.proto"),
global_rules = require("apisix.admin.global_rules"),
stream_routes = require("apisix.admin.stream_routes"),
plugin_metadata = require("apisix.admin.plugin_metadata"),
plugin_configs = require("apisix.admin.plugin_config"),
}
因此,上面的 curl 請求將被/apisix/admin/upstreams.lua 文件的 put 函數(shù)處理,看下 put 函數(shù)的實現(xiàn):
-- /apisix/admin/upstreams.lua文件
function _M.put(id, conf)
-- 校驗請求數(shù)據(jù)的合法性
local id, err = check_conf(id, conf, true)
local key = "/upstreams/" .. id
core.log.info("key: ", key)
-- 生成etcd中的配置數(shù)據(jù)
local ok, err = utils.inject_conf_with_prev_conf("upstream", key, conf)
-- 寫入etcd
local res, err = core.etcd.set(key, conf)
end
-- /apisix/core/etcd.lua
local function set(key, value, ttl)
local res, err = etcd_cli:set(prefix .. key, value, {prev_kv = true, lease = data.body.ID})
end
最終新配置被寫入 etcd 中。可見,Nginx 會校驗數(shù)據(jù)再寫入 etcd,這樣其他 Worker 進程、Nginx 節(jié)點都將通過 watch 機制接收到正確的配置。上述流程你可以通過 error.log 中的日志驗證:
2021/08/03 17:15:28 [info] 16437#16437: *23572 [lua] upstreams.lua:72: key: /upstreams/1, client: 127.0.0.1, server: _, request: "PUT /apisix/admin/upstreams/1 HTTP/1.1", host: "127.0.0.1:9080"
為什么新配置不 reload 就可以生效?
我們再來看 admin 請求執(zhí)行完 Nginx Worker 進程可以立刻生效的原理。
開源版 Nginx 的請求匹配是基于 3 種不同的容器進行的:
- 將靜態(tài)哈希表中的 server_name 配置與請求的 Host 域名匹配;
- 其次將靜態(tài) Trie 前綴樹中的 location 配置與請求的 URI 匹配;
3.在上述 2 個過程中,如果含有正則表達式,則基于數(shù)組順序(在 nginx.conf 中出現(xiàn)的次序)依次匹配。
上述過程雖然執(zhí)行效率極高,卻是寫死在 find_config 階段及 Nginx HTTP 框架中的,**一旦變更必須在 nginx -s reload 后才能生效!**因此,APISIX 索性完全拋棄了上述流程!
從 nginx.conf 中可以看到,訪問任意域名、URI 的請求都會匹配到 http_access_phase 這個 lua 函數(shù):
server {
server_name _;
location / {
access_by_lua_block {
apisix.http_access_phase()
}
proxy_pass $upstream_scheme://apisix_backend$upstream_uri;
}
}
而在 http_access_phase 函數(shù)中,將會基于 1 個用 C 語言實現(xiàn)的基數(shù)前綴樹匹配 Method、域名和 URI(僅支持通配符,不支持正則表達式),這個庫就是lua-resty-radixtree。每當路由規(guī)則發(fā)生變化,Lua 代碼就會重建這棵基數(shù)樹:
function _M.match(api_ctx)
if not cached_version or cached_version ~= user_routes.conf_version then
uri_router = base_router.create_radixtree_uri_router(user_routes.values,
uri_routes, false)
cached_version = user_routes.conf_version
end
end
這樣,路由變化后就可以不 reload 而使其生效。Plugin 啟用、參數(shù)及順序調(diào)整的規(guī)則與此類似。
最后再提下 Script,它與 Plugin 是互斥的。之前的動態(tài)調(diào)整改的只是配置,事實上 Lua JIT 的及時編譯還提供了另外一個殺手锏 loadstring,它可以將字符串轉換為 Lua 代碼。因此,在 etcd 中存儲 Lua 代碼并設置為 Script 后,就可以將其傳送到 Nginx 上處理請求了。
小結
Nginx 集群的管理必須依賴中心化配置組件,而高可靠又具備 watch 推送機制的 etcd 無疑是最合適的選擇!雖然當下 Resty 生態(tài)沒有 gRPC 客戶端,但每個 Worker 進程直接通過 HTTP/1.1 協(xié)議同步 etcd 配置仍不失為一個好的方案。
動態(tài)修改 Nginx 配置的關鍵在于 2 點:Lua 語言的靈活度遠高于 nginx.conf 語法,而且 Lua 代碼可以通過 loadstring 從外部數(shù)據(jù)中導入!當然,為了保障路由匹配的執(zhí)行效率,APISIX 通過 C 語言實現(xiàn)了前綴基數(shù)樹,基于 Host、Method、URI 進行請求匹配,在保障動態(tài)性的基礎上提升了性能。
文章來源:云原生實驗室
推薦閱讀:
前端開發(fā)之Nginx單頁加載優(yōu)化
web開發(fā)基礎篇之Nginx的安裝與啟動
linux下Nginx的安裝方法與介紹
前端開發(fā)框架Vue之findIndex() 的使用