热更新-动态路由
前言
在传统微服务体系中,热更新是一个非常重要的功能,可以在网关运行,不重启的情况下,动态修改配置并生效。我们可以把路由分为三种模式
- nginx/openresty支持的静态路由,本质是修改nginx.conf,重启nginx生效
- 服务自动发现
- 热更新
热更新最重要的是可以动态的更新upstream,Tengine提供的第三方模块nginxngx_http_dyups_module
允许在运行时通过 HTTP 接口添加、删除或修改 upstream 配置,无需重启 Nginx。
1、ngx_http_dyups_module模块
ngx_http_dyups_module
最初由阿里巴巴基于 Tengine 分支开发,目标是在不重启或重载 Nginx/Tengine 进程的情况下,实现对upstream
集群的动态管理(增删、更新、查询)。- 传统 Nginx 要更新
upstream
配置,需要修改配置文件后执行nginx -s reload
,过程中会有瞬间连接中断风险,而 dyups 模块则提供了一个 “在线” 接口,能够在 HTTP 运行时通过 RESTful 或 Lua API 对upstream
进行增删改查
1.1、主要数据结构与共享内存设计
全局上下文:ngx_dyups_global_ctx
- 在初始化(
init_module
)阶段,模块通过ngx_shared_memory_add("dyups_shm_zone", size, &ngx_http_dyups_module)
分配一块共享内存。该内存段映射到主进程和所有 Worker,共同维护以下重要字段:shpool
(类型为ngx_slab_pool_t *
):专用于分配共享内存中的消息节点和字符串副本;shctx
(类型为自定义结构,通常称ngx_dyups_shctx_t *
):包含消息队列头指针、尾指针、当前版本号version
、消息计数msg_count
、以及存储Worker
状态的数组status[]
等。
消息节点:ngx_dyups_msg_t
每条对
upstream
的增删改查动作最终会被封装成一个ngx_dyups_msg_t
节点,写入共享内存中的链表:1
2
3
4
5
6
7
8typedef struct {
ngx_queue_t queue; // 链表节点
ngx_uint_t type; // 操作类型:ADD / DELETE / UPDATE
ngx_str_t name; // 上游名称
ngx_str_t content; // 包含实际 server 列表的字符串
pid_t pid[NGX_MAX_WORKERS]; // 标记哪些 Worker 已处理
ngx_uint_t count; // 已处理 Worker 数目
} ngx_dyups_msg_t;pid[]
用来记录各 Worker 进程对该消息的处理状态;当count
达到当前worker_processes
数量时,表示所有 Worker 均已读取该消息,无需再转发或保留。
Worker 状态数组:sh->status[]
shctx->status
是一个长度等于worker_processes
的数组,每个元素为:1
2
3
4typedef struct {
pid_t pid; // Worker 进程号
time_t time; // 最后一次心跳时间戳
} ngx_dyups_status_t;在
init_process
阶段,每个 Worker 会在共享内存中注册自己的pid
,并设置第一次的time = now
。这一信息用于:- 标记哪些 Worker 仍处于活跃状态;
- 防止由于某个 Worker 异常退出导致共享消息长时间无法累积至
count == worker_processes
而悬挂。
** 共享内存分配粒度**
- 共享内存池由
ngx_slab_pool_t
管理,所有对消息节点和动态存储upstream
字符串的分配都来自该 Slab。这样可以避免不同进程使用各自的堆内存,确保跨进程可见。 - 默认
dyups_shm_zone_size 2M
时,可能一次只能发送有限大小的upstream
信息;对于包含大量server
的集群,需要调大该参数以防止分配失败。
1.2、动态增删改查
本节梳理一次典型的 “更新(Add/Update)” 操作流程,删除操作类似,但在正式更新时会调用 ngx_dyups_delete_upstream
。
1、 外部 API 触发与初步检查
- 当客户端通过
curl -d "server X:Y;" http://host/dyups/upstream/<name>
发送更新请求时,进入 Nginx 的某个 Worker 并调用ngx_http_dyups_update_upstream(ngx_str_t *name, ngx_buf_t *buf, ngx_str_t *rv)
。 - 函数开头会检查全局
ngx_http_dyups_api_enable
标志;若未启用,则直接返回NGX_HTTP_NOT_ALLOWED (405)
,并在rv
中设置"API disabled"
提示。
2、 获取共享内存锁
- 根据主配置
dmcf->trylock
判断锁模式:- 若
trylock == 0
(阻塞模式),调用ngx_shmtx_lock(&shpool->mutex)
,若有其它 Worker 持锁,则本 Worker 阻塞,直到锁可用; - 若
trylock == 1
(非阻塞模式),调用ngx_shmtx_trylock(&shpool->mutex)
,若抢锁失败,则立即设置rv="wait and try again"
并返回NGX_HTTP_CONFLICT (409)
,告知客户端稍后重试。
- 若
- 上锁成功后,才能安全访问
shctx->msg_queue
以及共享upstream
数据。
3、 读取并清理历史消息
- 调用
ngx_http_dyups_read_msg_locked(timer)
(需在持锁状态下执行),先扫描共享内存中shctx->msg_queue
列表:- 如果某个
msg->count == total_worker
,则说明该消息已被所有 Worker 处理,可安全移出队列并调用ngx_dyups_destroy_msg
释放对应共享内存; - 否则,对于尚未被本 Worker 处理的消息节点(通过检查
pid[]
数组判断),调用相应内部函数(如ngx_dyups_do_update
或ngx_dyups_delete_upstream
)在本地内存执行对应操作,并将本 Worker PID 加入pid[]
,令msg->count++
。
- 如果某个
- 该步骤的目的是:先把其他 Worker 上次发送的更新都在本进程内同步完成,保证不会在后续操作中丢失或冲突。
4、 沙箱验证(ngx_dyups_sandbox_update
)
- 读取完历史消息后,函数调用
status = ngx_dyups_sandbox_update(buf, rv)
:- 该函数会在内存中临时创建一个 “伪”
ngx_http_upstream_srv_conf_t
结构,解析并检查buf
中包含的server ...;
列表; - 如果语法错误(例如缺少分号、IP:端口格式不对)或参数非法(如
weight=0
),则status != NGX_HTTP_OK
,并在rv
中返回相应错误信息,跳转至finish
标签,释放锁并返回错误。
- 该函数会在内存中临时创建一个 “伪”
- 通过沙箱验证意味着新配置符合 Nginx 语法要求且参数合法,但尚未正式写入到主内存结构。
5、 正式更新(ngx_dyups_do_update
)
- 执行
status = ngx_dyups_do_update(name, buf, rv)
:- 在本 Worker 进程的
upstream_conf_hash
中,查找是否已存在名为name
的upstream
;若不存在则创建新的ngx_http_upstream_srv_conf_t
对象; - 按照沙箱中的解析结果,构建或更新一个
ngx_http_upstream_server_t
链表,其中包含每个后端服务器节点的 IP、权重、最大连接数、失败重试等参数; - 对比新旧列表,执行 “新增”、“修改”、“删除” 三类操作,替换原有数据结构;
- 调用原生 Nginx 的
ngx_http_upstream_init_round_robin
、init_chash_peer
等函数,重建负载均衡相关状态,保证后续请求调度能使用最新集群。
- 在本 Worker 进程的
- 如果
ngx_dyups_do_update
返回非NGX_HTTP_OK
,说明更新过程中出现内存分配失败或其他异常,此时会跳到finish
,释放锁并返回错误码。
6、 消息广播到其他 Worker(ngx_http_dyups_send_msg
)
- 若正式更新成功(
status == NGX_HTTP_OK
),则调用ngx_http_dyups_send_msg(name, buf, NGX_DYUPS_ADD)
:- 通过
ngx_slab_alloc_locked
从shpool
分配一个ngx_dyups_msg_t
大小的内存; - 将
name
、更新内容buf
(通常是以字符串形式保存)、type = NGX_DYUPS_ADD
、count = 1
(本 Worker 自己先算一次)以及pid[0] = ngx_pid
填入消息结构; - 将新消息插入
shctx->msg_queue
链表尾部。
- 通过
- 如果写入共享内存失败(如剩余空间不足),则
ngx_http_dyups_send_msg
返回非零,调用者将rv="alert: update success but not sync to other process"
,并最终返回NGX_HTTP_INTERNAL_SERVER_ERROR (500)
,提醒运维重启或人工介入。
7、 释放共享内存锁并返回
- 不论上述哪个环节出错或成功,都会执行
finish:
标签中的ngx_shmtx_unlock(&shpool->mutex)
,释放独占锁。 - 最终返回值
status
可能是:NGX_HTTP_OK (200)
:更新成功且消息已写入共享内存;NGX_HTTP_BAD_REQUEST (400)
:沙箱测试失败;NGX_HTTP_CONFLICT (409)
:非阻塞锁未能获得;NGX_HTTP_INTERNAL_SERVER_ERROR (500)
:正式更新成功但无法写入共享内存;NGX_HTTP_NOT_ALLOWED (405)
:未启用 dyups API。
1.3、进程间同步机制(消息队列与定时器)
1、定时器驱动:ngx_add_timer(&msg_timer, read_msg_timeout)
- 在每个 Worker 的
init_process
阶段,模块会调用ngx_add_timer(&ngx_dyups_global_ctx.msg_timer, read_msg_timeout)
,设置一个定时事件。 - 定时器回调函数是
ngx_http_dyups_read_msg
,其内部会先判断当前时间与ev->timer.key
是否过期,若到期就调用ngx_http_dyups_read_msg_locked
。 - 该定时器默认以
dyups_read_msg_timeout 1s
周期触发,但可根据场景调整(如改为500ms
或2s
)。
2、持锁读取消息:ngx_http_dyups_read_msg_locked
- 该函数会首先调用
ngx_shmtx_lock(&shpool->mutex)
;若有其它 Worker 正在更新,会阻塞直到锁空闲;若设置了dyups_trylock on;
,读取方仍是阻塞获取锁,无法立即跳过。 - 遍历
shctx->msg_queue
:- 若
msg->count == total_worker
,说明所有 Worker 都已处理完毕,调用ngx_queue_remove
将该消息从链表中移除,并调用ngx_dyups_destroy_msg
释放内存; - 否则,检查
msg->pid[]
数组,如果当前 Worker PID 尚未出现,则根据msg->type
(ADD
/DELETE
)执行对应操作(调用ngx_dyups_do_update
或ngx_dyups_delete_upstream
),然后将本 PID 写入pid[]
,令msg->count++
;否则跳过。
- 若
- 处理完所有消息后,调用
ngx_shmtx_unlock(&shpool->mutex)
,并重置或延迟下一次定时器(ngx_add_timer(ev, read_msg_timeout)
)。
3、Worker 状态更新:shctx->status[]
- 在
ngx_http_dyups_read_msg_locked
开头,Worker 会先尝试在shctx->status[]
中找到或注册自己的pid
,并更新对应time = now
。 - 若某些
status[].pid
长时间无 “心跳” 更新(超时),模块会认为对应 Worker 已崩溃或退出,并在后续清理过程中剔除它对消息的未处理计数,以避免死消息永久挂起。
1.4、锁机制与并发控制(阻塞与非阻塞模式)
锁保护范围
- 每次写入或读取共享内存时,都必须先调用
ngx_shmtx_lock(&shpool->mutex)
;在写操作(如ngx_http_dyups_update_upstream
)中,整个流程包括读取旧消息、沙箱验证、正式更新、消息写入共享内存,全部在同一把锁内完成,保证对共享内存的原子性与一致性。
阻塞锁(默认模式)
- 当一个 Worker 开始更新时,其它尝试更新或读取消息的 Worker 都会在
ngx_shmtx_lock
处阻塞;在长时间更新(如包含大量节点的upstream
)期间,可能导致多个 Worker 长时间处于挂起状态,无法处理新来的业务请求。 - 这种场景在后文性能章节还会详细分析。
非阻塞锁(dyups_trylock on
)
- 若设置
dyups_trylock on;
,写操作中改用ngx_shmtx_trylock
,若锁被占用则立即返回NGX_HTTP_CONFLICT (409)
,上层接口告知客户端 “wait and try again”,无需 Worker 长时间阻塞。 - 但 定时器读取消息 的路径仍是阻塞式
ngx_shmtx_lock
,若锁被占用则轮询线程仍要等到锁空闲才能继续同步,可能短暂挂起业务。
锁粒度优化思考
- 将锁粒度最小化:沙箱验证、实际更新、消息写入间尽量迅速;
- 调整
dyups_read_msg_timeout
周期:过小会频繁抢锁,过大则延迟配置同步; - 在高并发场景下,优先使用
trylock on
,让更新请求自行重试,而不是让 Worker 一直挂起。
1.5、源码分析
1 |
|
1、注册location的处理函数
1 |
|
1 |
|
2、更新upstream
这是最主要处理函数,主要做了5步
- 获取共享内存锁(可选阻塞或非阻塞模式),以保证多个进程不会并发修改
- 从共享内存中读取之前未处理的消息,并根据需要在共享内存中进行更新或删除操作;
- 在“沙箱”中(sandbox)先行尝试本次上游配置更新,以验证新配置语法和结构是否正确;
- 如果沙箱测试通过,则按正式流程调用
ngx_dyups_do_update
实际更新上游链表; - 在更新成功后,将本次更新操作写入共享内存,使其他 Worker/进程能够同步到新的配置;
1 |
|
2、ingress-nginx的热更新实现
对于一个功能,一定要多学习借鉴同层次产品的实现。ingress-nginx也实现了热更新,但是是通过lua层面实现的,有一篇文章总结的非常好 https://xiaorui.cc/archives/7342
- Lua 负载均衡:在
balancer_by_lua_block
中使用lua-resty-balancer
,在共享内存中根据算法选取可用后端。 - 主动推送:当 Kubernetes 集群中 Ingress/Service/Endpoint 变更时,Control-Plane 通过 HTTP POST 将新的后端 JSON 发送到 OpenResty 内部接口
/configuration/backends
。 - 写入共享内存:该 Lua 接口读取请求体,使用
cjson.decode()
解析 JSON,并将后端列表序列化后写入ngx.shared.dynamic_upstreams
等lua_shared_dict
。 - Worker 周期性拉取:尽管 Control-Plane 会主动推送,仍需在每个 Worker 的
init_worker_by_lua_block
中注册定时器,每隔 1 秒调用sync_backends
与sync_backends_with_external_name
,从 Controller HTTP 接口拉取最新后端配置,保证所有新启动或错过推送的 Worker 都能补偿性地同步到共享内存 。 - 请求时读取共享内存:在每个请求的
balancer_by_lua_block
阶段,Lua 脚本通过ngx.shared.dynamic_upstreams:get(service_name)
读取最新后端列表,执行加权/轮询等算法后调用ngx.balancer.set_current_peer(host, port)
,完成动态路由决策,而无需重载 NGINX。 - 这样,Ingress-NGINX 能在高可用场景下实现“推送+拉取+读取”三步闭环,既能及时响应变更,又能弥补短暂的推送失效,最大化减少重载带来的抖动和中断。
2.1、前置三步确认
- Lua 负载均衡(lua-resty-balancer)
Ingress-NGINX 在
nginx.tmpl
中定义了若干lua_shared_dict
,例如:1
lua_shared_dict dynamic_upstreams 5m;
用于存储后端服务地址与权重等信息,供 Lua 层访问
在具体的
upstream
块中,使用balancer_by_lua_block
指令调用 Lua 脚本(如balancer.lua
)进行动态负载均衡:1
2
3
4
5
6upstream dynamic_upstream {
balancer_by_lua_block {
local balancer = require("lua_ingress").balancer
balancer("default-my-service") -- 传入服务名
}
}lua-resty-balancer
或自研脚本会读取ngx.shared.dynamic_upstreams
中同名键对应的 JSON 字符串,cjson.decode()
转为 Lua 表后,根据配置选择后端
- 路由变更时主动推送
当 Kubernetes 集群中 Ingress 或 Service/Endpoint 发生变更时,Ingress-NGINX Controller 会构建一个类似如下格式的 JSON:
1
2
3{
"default-my-service": ["10.0.0.5:80", "10.0.0.6:80"]
}然后向本地 OpenResty 暴露的
/configuration/backends
接口发起 HTTP POST 请求 。该接口在 Lua 层
1
2
3
4if ngx.var.request_uri == "/configuration/backends" then
handle_backends()
return
end成功后返回 HTTP 200,完成一次“主动推送+写入共享内存”的操作 。
- 接口解析与存储
/configuration/backends
接口对应的 Lua 实现在configuration.lua
中,核心为handle_backends()
函数:这段代码主要实现以下功能:
- 支持两种HTTP方法:
- GET:获取当前后端配置
- POST:更新后端配置
- 关键操作流程:
- 读取请求体数据
- 将新配置存入共享内存
- 记录同步时间戳
- 处理各种错误情况
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40local function handle_backends()
-- 处理GET请求:返回当前后端配置数据
if ngx.var.request_method == "GET" then
ngx.status = ngx.HTTP_OK
ngx.print(_M.get_backends_data())
return
end
-- 读取请求体中的后端配置数据
local backends = fetch_request_body()
if not backends then
ngx.log(ngx.ERR, "dynamic-configuration: unable to read valid request body")
ngx.status = ngx.HTTP_BAD_REQUEST
return
end
-- 将新的后端配置存入共享内存
local success, err = configuration_data:set("backends", backends)
if not success then
ngx.log(ngx.ERR, "dynamic-configuration: error updating configuration: " .. tostring(err))
ngx.status = ngx.HTTP_BAD_REQUEST
return
end
-- 更新同步时间戳
ngx.update_time() -- 确保获取当前精确时间
local raw_backends_last_synced_at = ngx.time()
-- 记录最后同步时间到共享内存
success, err = configuration_data:set("raw_backends_last_synced_at", raw_backends_last_synced_at)
if not success then
ngx.log(ngx.ERR, "dynamic-configuration: error updating when backends sync, " ..
"new upstream peers waiting for force syncing: " .. tostring(err))
ngx.status = ngx.HTTP_BAD_REQUEST
return
end
-- 返回创建成功的状态码
ngx.status = ngx.HTTP_CREATED
end如果需要同步
ExternalName
服务,则 Controller 还会调用/configuration/backends/external
接口,Lua 在sync_backends_with_external_name()
中提取域名并进行 DNS 解析,写入同一共享内存 。- 支持两种HTTP方法:
2.2、第4步:Worker 周期性拉取
sync_backends
& sync_backends_with_external_name
尽管 Controller 会主动推送,但需要在每个 Worker 内注册定时器,周期性(一般为 1 秒)拉取最新配置,原因如下:
4.1、 设计初衷
- Worker 冷启动补偿
- 新创建的 Worker 并未能经历之前 Controller 的推送;如果不主动拉取,就只能在下一次有变更时才有机会拿到配置,造成“冷启动白屏”或“脏数据路由”现象 。
- 网络或推送失败补偿
- Controller 在推送时可能因网络抖动、Lua 解析错误等意外导致部分 Worker 未能及时更新共享内存;定时拉取能在网络恢复后及时获取正确数据,保证“最终一致性” 。
- 并发推送竞态场景
- 当多个 Controller 同时推送不同版本的后端列表时,某些 Worker 可能接收 A 版本但错过 B 版本推送;定时拉取确保 Worker 能以“最新可用版本”为准,避免短暂竞态导致长时间漂移 。
4.2、 定时器注册
1、初始化时启动定时器,ngx.timer.at只会执行一次,ngx.timer.every会重复执行
1 |
|
2、sync_backends_with_external_name
- 作用:专门同步类型为
ExternalName
的 Kubernetes Service,先读取接口返回的数据,获取域名列表,再进行 DNS 解析后更新共享内存。 - 流程:
- HTTP GET
/configuration/backends/external
获取映射。(Controller 提供的接口) - 对每个域名调用
ngx.socket.dns.toip(domain)
获取 IP 列表。 - 调用
sync_backend()
(详见下文)将解析后的 IP 地址写入共享内存。
- HTTP GET
- 使用频率:定时器注册后每秒执行一次 。
1 |
|
3、sync_backends
- 作用:获取 Controller 暴露的普通 Service 后端配置(
Endpoints
),同步到共享内存(lua_shared_dict dynamic_upstreams
)。 - 数据来源:HTTP GET 请求
/configuration/backends
接口。(Controller 提供的接口) - 使用频率:由
init_worker_by_lua_block
注册的定时器每秒执行一次,确保实时同步。 - 不处理:对于类型为
ExternalName
的服务不会同步(将由sync_backends_with_external_name
处理)
1 |
|
ngx.timer.at(0, _M.sync_backends)
表示在 Worker 启动后立即执行一次sync_backends
;- 在
sync_backends
与sync_backends_with_external_name
的末尾,重新调用ngx.timer.at(1, ...)
注册下一次 1 秒延迟的定时任务; - 这样每个 Worker 都会每秒主动拉取一次后端配置,无论 Controller 推送是否成功,均能在最多 1 秒内同步到最新状态 。
2.3、第5步:请求阶段读取共享内存并路由
在完成“主动推送”与“定时拉取”后,共享内存 ngx.shared.dynamic_upstreams
中始终保存最新的后端列表。下一步,在 HTTP 请求到达时,Lua 负责从共享内存读取并选取后端。完整请求流程
- DNS/HTTP 解析:客户端请求到达 NodePort/LoadBalancer,转发到 NGINX Worker 进程;
- **匹配
location
**:Nginx 根据配置匹配到使用proxy_pass http://dynamic_upstream;
的location /
; - **执行
balancer_by_lua_block
**:Nginx 在进入 upstream 阶段前执行 Lua 脚本,Lua 从ngx.shared.dynamic_upstreams
中读取当前后端列表并选取一个后端; - 设置上游 Peer:Lua 调用
ngx.balancer.set_current_peer(host, port)
,指定本次请求的上游地址; - 转发给上游:Nginx 将请求转发至以上步骤得出的 IP:Port,完成路由。
这一路径实现了“读取-选取-转发”的动态更新,无需 nginx -s reload
,大幅提升了路由变更后的可用性与稳定性。
2.4、总结
- 主动推送:Control-Plane 在后端变更时将 JSON 配置推送至 Lua 接口,写入共享内存。
- 定时拉取:为了保证新 Worker 与部分推送失败的补偿,每个 Worker 在
init_worker_by_lua_block
中注册定时器,每秒调用sync_backends
和sync_backends_with_external_name
,从 Controller 接口再次拉取配置并更新共享内存。 - 请求时读取:在
balancer_by_lua_block
阶段,Lua 脚本从共享内存读取最新后端列表,执行负载均衡算法(lua-resty-balancer
)并调用ngx.balancer.set_current_peer()
,实现动态路由。
通过上述 “主动推送 + 周期性拉取 + 请求时读取” 的三步闭环,Ingress-NGINX 能在不重启 NGINX 的前提下,实现后端服务的动态热更新,兼顾了 低延迟、高可用 与 一致性。
3、扩展实现
对于自己实现网关,由于业务的复杂性,lua层面实现明显不符合需求。之前分析过服务自动发现的设计,2个功能都是在运行期更新路由信息,实际很多逻辑是重叠的。我们从一个经典的部署场景切入
1个网关集群,包含2台网关
1个zookeeper集群
3.1、整体架构
很明显,2台为网关的配置需要一致,整体需要考虑集群和多进程的更新
1、客户端调用A网关,变更dyups的配置,A网关更新后,同步到zk。 然后B网关定时感知到变更,然后进行更新。
2、单独抽一个0号进程,专注于配置更新,而work专注于业务请求的处理。
3、通过共享内存进行数据的共享,进行变更
3.2、具体设计
主要的链路分为2种
- 客户端调用 -> 解析客户端body -> 修改路由信息(内存+配置文件) -> 同步到zookeeper
- 定时器感知zookeeper路由信息变更 -> 获取变更的部分 -> 修改路由信息(内存+配置文件)
网关集群对数据的一致性保证,与nginx的多进程对数据的一致性保证,其实非常相似。
1、与服务自动发现一致,复用0号进程,使用0号进程监听特定端口,客户端可以调用该端口,进程配置更新
2、0号进程处理客户端的变更请求,更新本地文件、共享内存的版本号、配置数据写入共享内存
3、0号进程更新完数据后,将数据更新到zk,同时也要更新zk的版本号
4、0号进程开启定时器,比对zk的版本号与共享内存的版本号是否一致,不一致则进行变更
5、0号进程获取zk最新的数据,更新本地文件、共享内存的版本号、配置数据写入共享内存
6、work进程开启定时器,比对共享内存的版本号与本地内存中的版本号,不一致则进程更新
3.3、需要考虑的问题
涉及到多台网关就需要考虑竞争关系,还好nginx是多进程,考虑过很多类似的问题,2者的情况类似
1、2台网关同时主动式更新
假设配置的版本号为10,A网关被主动式调用,版本号为11,配置写到zk,此时修改zk的版本号为11
接着,B网关也被主动式调用,版本号也改为11,此时也要写到zk,这时就会存在问题,配置被覆盖了
所以实际网关在写配置到zk时,需要先获取一下版本号,是否相等,如果相等说明没有其他网关更新,然后再加一,更新到zk
2、多进程更新
事实上,只有0号进程会修改配置,work只是读而已,所以不存在竞争关系。但是0号进程也会有问题,因为他不仅要从zk读取数据(其他网关的变更),他也会被主动式调用,修改数据,所以存在竞争关系,其实又回到了第1个问题。
其实就是模仿ngx_dyups_update_upstream的逻辑,修改数据之前,先获取一下zk的版本号,确认当前拿到的是最新的数据,然后再进行主动式的更新。
如果zk的数据已经变更,那本次主动式调用,需要返回失败。同时要提示客户端,需要先拿到最新的数据,才能进程主动式调用更新。
4、主动式调用
upstream只是一个很基础的路由属性,实际上复杂的业务需要很多属性,比如uri,uri+upstream,可以组成一个路由的基本单元,即客户端请求到业务服务端的映射。当然还有权重、负载策略、失败重试等等属性,有些属性是nginx本身具有的,有些属性需要嵌入ngixn模块开发,或者重新实现模块实现。但总的思想是,虚拟一个路由对象,然后进行动态路由更新。
相比于ngx_dyups_update_upstream,也只是属性的不一样而已,所以按照同样的方式实现,其实就可以。当然实际实现起来,是有一些难度的。
编写一个完整的http模块,接收post请求,解析json的body,组装为特定数据格式,在检查过配置的正确性后,分别调用多个路由模块的更新,实现共享内存内存更新,然后更新配置文件。而实际的work通过定时器从共享内存更新最新的路由信息。
5、资料
参考:
官方文档:https://www.bookstack.cn/read/nginx-official-doc/5.md
GitHub仓库:https://github.com/yzprofile/ngx_http_dyups_module
openresty的balancer库:**lua-resty-balancer**