分片部署
本文定位
当单个信令进程的接入容量不够用时,可以同时运行多个 blade-server 信令实例来分担设备接入。本文讲清这套「多信令分片」是怎么工作的——设备如何归属到某个实例、点播 / 云台 / 回放等控制如何自动落到正确的实例、四个分片配置项各自配什么(含一份可直接照抄的双实例 demo),以及它在单体(boot)与微服务(cloud)两种部署形态下都能走通的原因。
单实例部署无需读本文:相关配置全部留空即零配置工作。只有要横向扩展信令接入容量时,才需要按本文配置多实例。配置项的页面查看见 信令节点,SIP 基础配置见 配置详解。
一、为什么要多实例,以及它解决什么
平台 SIP 是全局单端口:一个进程只在一个 SIP 端口上接入所有租户的设备(靠 server-id 区分租户,而非端口)。但「单端口」不等于「只能跑一个进程」——当设备规模增长、单进程的信令处理能力(注册、心跳、目录、点播下发)到达瓶颈时,可以再起几个 blade-server 实例,让设备分摊到不同实例上接入。
这背后有一条无法回避的根本约束:SIP 信令是有状态的,不能简单地多活。 设备和某个实例之间维持着一条活动的 SIP 会话(注册态、鉴权随机数、对话上下文),这些状态只存在于接收它注册的那个进程内存里,无法跨进程复制。所以多实例不是「把状态同步到所有实例」,而是「分片」——每台设备明确归属到一个实例,对它的所有下行控制都由这个实例负责。
这套方案的两条轴互相独立、各自扩展:
| 轴 | 扩展方式 | 决定什么 |
|---|---|---|
| 信令面(有状态) | 多个信令实例分片,每实例管一批设备 | 设备从哪里注册接入、由谁下发控制 |
| 媒体面(无状态) | 多台 ZLM 按并发自动负载 | 视频流落在哪台流媒体 |
媒体面的多节点负载见 媒体节点;本文只讲信令面的分片。
二、核心原理:属主路由
2.1 设备的「属主」是怎么确定的
设备端配置的「上级 SIP 服务器地址」填的是某一个具体实例的信令地址。注册报文按这个地址直达那个进程,接收注册的实例就成为这台设备的属主。归属不是谁分配的,而是由设备的注册物理落点天然决定的。
实例在设备注册 / 心跳落地时,把「设备 → 本实例编码」的归属写进 Redis,有效期取设备在线判定窗口再加一个离线扫描周期(多留一个扫描周期是为确保设备仍被判在线时归属必可解析、规避窗口边缘的瞬态误判)、随每次心跳续期;设备停止心跳后归属随键过期自动失效。所以归属是一个随设备在线状态自动维护的运行态,不落库——进程或 Redis 重启后,由设备下一次心跳自动重建。
2.2 控制请求如何落到正确的实例
关键问题:运维在管理页对某台设备点播 / 云台,这个 HTTP 请求可能落到任意一个实例(取决于负载均衡),但能下发 SIP 的只有属主。平台的做法是在每个实例的 HTTP 入口加一道属主路由:
- 请求落到的实例先解析属主:点播 / 云台 / 录像等按
channelId、停止 / 回放控制按sessionId、设备级查询按设备主键、查询结果拉取按命令序号 SN,求出设备编号再查 Redis 归属。 - 属主是本实例 → 直接放行,走本地 controller 发 SIP。
- 属主是其它实例 → 把请求原样内部 HTTP 转发到属主实例执行,再把属主的响应回写给前端。
所以无论运维操作哪个设备、请求落到哪个实例,控制最终都由属主下发。使用者无需关心设备具体接在哪个实例上。
单实例下这条路由是「空操作」
单实例部署时,属主恒为本实例,路由解析后恒走本地放行,永不发生转发。多实例的全部成本只在「确实需要跨实例」时才产生。
三、四个分片配置项详解
全部位于 vms.gb28181.sip.*。单实例可全部留空。
3.1 instance-id——本实例的编码
实例的逻辑编码,是设备归属与控制转发的路由键(也是 信令节点 列表里的「节点编码」)。
- 可以自己起名,是一个自定义字符串(如
signal-node-a、bj-node-1),不是国标 20 位编号(那是server-id)。 - 两条硬约束:集群内唯一(撞号会让归属路由错乱)、跨重启稳定(它是 Redis 归属键存的值与其它实例比对的目标,改了等于归属全部失配)。
- 留空则自动回退为「对外信令地址:端口」(如
10.0.0.11:5060)。能用,但绑在 host:port 上——改了信令 IP 或端口编码就变。多实例建议显式配一个稳定短编码。
3.2 internal-base-url——本实例的内部转发入口
供其它实例把「属主是本实例」的控制请求转发进来的 HTTP 基址(形如 http://内网IP:服务端口)。
- 必须是实例间内网可达的地址。
- 留空则按「对外信令地址 + 应用服务端口」自动推导。多网卡 / 容器部署务必显式配置:此时对外信令地址未必是实例间互通的内网地址,自动推导值可能连不上。
3.3 node-forward-secret——实例间转发口令
实例之间互相转发控制请求时的鉴权口令,防止内部转发入口被外部直接调用。
- 多实例部署必须配置,且各实例填同一个值。
- 缺配的后果是明确失败、不是安全漏洞:属主在其它实例的设备,其点播 / 云台 / 回放等控制会被直接拒绝并返回清晰提示(本实例的设备不受影响);伪造转发请求也会被拒(口令为空时内部转发通道整体关闭)。
- 多实例在线却未配置口令时,实例会在日志打印一次提示,便于及时发现并修正。
3.4 cascade-node——本实例是否承担级联
布尔标记,声明本实例是否承担级联(向上级平台注册 / 保活,国标级联 场景)。每个实例只声明自己,无需关心其它实例配了什么——这是它与路由键类配置的根本区别。
- 每个实例各自声明:置
true表示本实例愿意承担级联,false(或留空)表示不承担。进程启动时这个标记会自报进 信令节点 表的cascade_node列,供承担实例选主读取。 - 承担实例由共享表 + 确定性规则算出,集群内自动一致:各实例读同一张信令节点表,在「在线且标记为承担级联」的节点中取节点编码最小者为唯一承担实例;若无任何标记节点在线,则回退到「全体在线节点中编码最小者」兜底;仍无则回退本实例。因为输入(共享表)与规则(编码最小)对所有实例一致,各实例独立算出的承担实例必然是同一个,不存在配错字面值导致多实例重复注册的隐患。
- 单实例零配置:留空即
false,经兜底仍由本实例承担,本地开发与单实例部署无需任何配置即可工作。 - 生产多实例建议标记 2~3 个实例:既保证承担实例不可用时有节点轮替接管,又能让级联媒体的 ZLM 回调落在标记实例上(原因见下)。
cascade-node 必须打在接收级联媒体 ZLM 回调的实例上
级联是本平台作为下级向上级注册。上级点播取流时,上级的 SIP 会话(进程本地、不可跨进程迁移)和这路流的 ZLM 媒体回调必须落在同一个进程里,才能把流转推给上级。而 ZLM 回调落到哪个实例,由你给 ZLM 配的回调地址决定(部署层决定,平台自身感知不到)。所以承担级联的实例,必须同时是接收级联媒体 ZLM 回调的实例——把 cascade-node 标记打在这些实例上,让「承担级联」与「回调落点」同进程。标记打错位置,会表现为「上级注册成功但点播取流不通」。单实例部署无此问题。
四、完整 demo:两个实例
两台 blade-server,内网 10.0.0.11(实例 A)与 10.0.0.12(实例 B),A 接收级联媒体 ZLM 回调,故由 A 标记承担级联。除下列 sip 差异外,其余配置(数据库 / Redis / ZLM 等)两实例共享同一套。
# ===== 实例 A(10.0.0.11)=====
vms:
gb28181:
sip:
host: 10.0.0.11 # A 的对外信令 IP(设备上级地址填这个)
port: 5060
server-id: 34020000002000000001 # 平台编号,两实例相同
instance-id: signal-node-a # A 的编码,唯一且稳定
internal-base-url: http://10.0.0.11:80 # A 的内网转发入口
node-forward-secret: a8f3k29slx7q # 转发口令,两实例必须一致
cascade-node: true # A 承担级联(A 接收级联媒体 ZLM 回调)# ===== 实例 B(10.0.0.12)=====
vms:
gb28181:
sip:
host: 10.0.0.12 # B 的对外信令 IP
port: 5060
server-id: 34020000002000000001 # 与 A 相同
instance-id: signal-node-b # B 的编码,与 A 不同
internal-base-url: http://10.0.0.12:80 # B 的内网转发入口
node-forward-secret: a8f3k29slx7q # 与 A 一致
cascade-node: false # B 不承担级联(留空亦等价于 false)承担判定的结果:两实例都把自己的 cascade-node 标记自报进信令节点表,各自读同一张表按「在线且标记承担者中编码最小」算承担实例,得到一致结论。
| 实例 | cascade-node | 节点编码 | 是否承担级联 |
|---|---|---|---|
| A | true | signal-node-a | 是(在线标记者中编码最小) |
| B | false | signal-node-b | 否(未标记承担) |
设备侧:把一批设备的「上级 SIP 服务器地址」配成 10.0.0.11:5060(归属 A)、另一批配成 10.0.0.12:5060(归属 B),即完成接入分片。两批设备的点播 / 云台等控制在任一实例操作都能自动路由到各自属主。
设备如何分配到不同实例
分片粒度由你决定:可以按区域、按设备批次,把设备端的「上级 SIP 地址」分别指向不同实例的信令地址即可。在 信令节点 页可实时查看各实例名下的在线设备数,据此判断分布是否均衡。
五、实例间转发机制
跨实例转发用的是实例直连的内部 HTTP 调用,不经过网关。它的设计目标是「换传输不换语义」——属主端执行的是与直接请求完全相同的 controller 逻辑。
5.1 转发携带什么、属主端如何执行
| 随转发携带 | 作用 |
|---|---|
原始登录令牌(Blade-Auth) | 属主端正常鉴权,并从令牌还原出同一租户 |
转发口令(node-forward-secret) | 属主端校验来源合法,放行内部转发 |
| 转发标记头 | 属主端识别为内部转发,不再二次路由(杜绝来回转发) |
属主端凭标记头识别后直接走本地 controller,@RequestParam / @RequestBody 绑定、平台鉴权、租户还原、统一 R 响应全部原生复用——业务代码无需为分片改任何一行,新增受控端点也只是登记一行路由声明。
5.2 返回数据如何回到前端
属主执行后返回的统一 R JSON(如点播的播放地址),由发起转发的实例原样回写给最初请求的前端:透传 HTTP 状态码、Content-Type 与响应体字节。前端收到的响应,与设备属主原本就在本实例、未发生转发时完全一致,无从感知中间曾发生转发。
5.3 转发失败不会拖垮整条链路
转发发生在请求进入 controller 之前,本实例不产生任何中间副作用(不发 SIP、不写库),因此转发失败不留半执行态:
- 各类失败(口令缺失 / 属主未登记 / 属主离线 / 连接或读取超时)都收敛为统一的失败响应(业务失败,非服务 500),前端按普通失败提示,整条请求链不崩。
- 转发前先校验属主在线、连接超时取较短值,失联的属主快速失败,不会让前端长时间挂起、也不空占线程。
- 失败是按单次请求隔离的:某设备转发失败,不影响属主在本实例的其它设备的正常控制。
5.4 同时支持单体(boot)与微服务(cloud)
属主路由的端点匹配与转发地址重建都做了部署形态归一,因此在两种架构下都成立:
| 形态 | 请求到达实例时的路径 | 说明 |
|---|---|---|
| 单体 boot | 含服务名前缀 | 进程内路径形如 /blade-iot/vms/...,归一后匹配受控端点 |
| 微服务 cloud | 网关已剥首段服务名 | 到达实例时形如 /vms/...,归一后同样匹配 |
转发是实例直连属主、绕过网关,用的是「实例内实际收到的路径」拼接属主地址——两个实例同形态,路径天然对齐,不会因网关剥前缀而错配。鉴权身份编码在登录令牌里随请求头走、与网关无关,直连透传即可被属主端鉴权接受。
cloud 部署补充
微服务形态下网关已剥去首段服务名,到达实例的路径形如 /vms/...。cloud 工程已在网关与服务安全配置中预置放行 ZLM 回调路径 /vms/webhook/**(无令牌的流媒体回调);而 VMS 控制器模块目前尚在向 cloud 工程移植中,移植后控制器将映射在 /vms/...(去掉单体下的 /blade-iot 服务名前缀)。单体形态的回调放行前缀则是带服务名的 /blade-iot/vms/webhook/**,勿与 cloud 的 /vms/webhook/** 混淆。转发机制本身对两种部署形态都已适配,无需改动。
六、级联事件的跨实例投递:NOTIFY 与上级控制
§二 讲的是前端控制如何落到设备属主(内部 HTTP 转发)。国标级联另有两处动作,同样只能在特定实例发出、而触发点未必就在那个实例上,它们走的是另一条路——基于 Redis 发布订阅的事件投递。
6.1 为什么这两处发送不能就地进行
- 向上级发 NOTIFY(目录增量、位置、告警)只能由承担实例发出:上级是向承担实例登记的 Contact 发起订阅的,这条订阅对话是进程本地状态、只存在于承担实例;从别的实例拼一条 NOTIFY 发出去,来源地址与对话对不上,会被上级拒收。
- 把上级下发的控制发给设备只能由设备属主实例发出:设备只认可它注册落地的那个实例的来源地址,从别的实例发控制会被设备按来源不符拒绝。
而这两处的触发点恰恰未必落在归属实例上:目录 / 位置 / 告警的 NOTIFY 由设备上行事件触发(通道上下线、GPS、告警),跑在设备属主实例——未必是承担实例;上级控制的 SIP 报文落在承担实例,但目标设备的属主未必就是承担实例。因此两处都需要把「要发的动作」投递到正确的归属实例执行。
6.2 投递机制:发布订阅 + 归属自选
触发实例先判自己是不是该动作的归属实例——NOTIFY 判「是否承担实例」,控制判「是否目标设备的属主」:
- 是本实例 → 直接本地发出,不经 Redis;
- 不是 → 把动作按值发布到一个共享的 Redis 频道(广播,而非定向到某台实例)。
所有实例都订阅这个频道,收到后各自按当前归属实时核验:只有当前承担实例执行 NOTIFY、只有当前设备属主执行控制,其余实例静默跳过。因为归属是收到时才算的,承担权或设备归属在投递途中迁移,也由当前的归属实例接手,不会发到已不再归属的实例上。
动作按值携带发送所需数据:目录 / 位置带通道快照、告警带全部字段、控制带原始 SIP 报文。归属实例据此直接渲染发送,无需回查触发事件本身的通道数据,也因此不受触发实例的写库事务是否已提交影响。
6.3 与内部 HTTP 转发的分工
它与 §二、§五 的属主转发是两条互补的跨实例路径,勿混淆:
| 内部 HTTP 转发(§二、§五) | 级联事件投递(本节) | |
|---|---|---|
| 触发 | 前端一个已存在的 HTTP 请求 | 协议线程内的事件(设备上行 / 上级 SIP 报文),无请求可代理 |
| 传输 | 实例直连的内部 HTTP 调用 | Redis 发布订阅 |
| 定向 | 解析出属主后点对点转发 | 广播,由当前归属实例自选执行 |
| 用途 | 前端点播 / 云台 / 回放落到设备属主 | 级联 NOTIFY 落到承担实例、上级控制落到设备属主 |
6.4 投递保证:至多一次
发布订阅是至多一次投递,这对级联可接受:国标 NOTIFY 本无重传语义,偶发漏投一条,到下次同类事件、或上级的周期性目录查询即自愈;上级控制在超时未生效时由上级重发。发布失败(如 Redis 短暂不可达)只记录并放弃本轮,绝不冒泡打断触发它的业务流(设备上行处理、删除设备的写库事务)。
单实例下同样是「空操作」
单实例部署时,承担实例与任何设备的属主恒为本实例,判定后恒走本地发出,永不发布到 Redis。这条投递路径的成本同样只在「确实跨实例」时才产生。
七、故障转移与取舍
| 主题 | 行为 |
|---|---|
| 属主进程崩溃 | 其名下设备归属在一个在线窗口(再加一个扫描周期)内随 Redis 键过期自动失效;期间对这些设备的控制因属主离线快速失败(不会盲发到死实例);设备下次心跳重连到存活实例后归属重建即恢复 |
| 承担实例不可用 | 承担实例下线后,在线的标记承担节点中编码次小者轮替接管;新承担实例在承担权跃迁时立即补发向上级注册,使上级记录的本端 Contact 尽快指向新实例。若希望级联具备这层接管能力,生产建议标记 2~3 个(同时承载级联媒体 ZLM 回调的)实例 |
| 没有信令自动 HA | 设备与实例间的 SIP 会话是进程本地状态,无法在进程崩溃时迁移到另一进程;这是国标信令有状态的本质约束,行业通行做法相同 |
| 上级 / 设备地址须指向具体实例 | 设备「上级地址」、级联回调落点都须指向具体实例而非负载均衡 VIP,否则状态与落点错位 |
| Redis 不可达 | 归属解析降级:落到属主自身的请求仍可本地执行;落到其它实例的请求按设备离线处理,Redis 恢复或设备重连即自愈 |
运行时切换承担实例,须同步下线旧实例
把 cascade-node 从一个实例改标到另一个实例后,新实例会在下一轮判定为承担者时立即补发向上级注册,但这个切换存在一段无法消除的传播延迟:在上级把联系地址更新为新实例之前,仍可能按旧的联系地址把入站请求(点播 INVITE、注销 BYE、目录订阅 SUBSCRIBE、查询应答、控制指令转发)发给旧实例。级联的入站处理只按"是否命中级联配置"识别身份,不会再确认自己是否仍是承担实例,因此旧实例在这段窗口内会照常受理这些请求。
若这段窗口内恰好发生上级点播,而级联媒体的 ZLM 回调地址已经跟着新实例的部署配置改掉,旧实例受理的这次点播会停在"已建立会话"却从未真正把流转推给上级的状态,要等到会话超时兜底(默认 2 小时)才会被清理,期间上级看到的是取流卡住而非明确失败。
因此调整 cascade-node 标记不是单纯改配置热生效的操作:改完后请同步下线或重启被取消标记的旧实例,不要让它继续存活着监听 SIP 端口,以避免落入这段过渡窗口。仅有实例意外下线(承担实例崩溃)触发的自动接管不受此影响——这种情况下旧实例本就不可达,不存在"仍在监听却受理了过期请求"的可能。
上线前检查清单
小结
多实例部署的本质是:设备按上级地址分片到各实例,各实例管自己名下设备的信令;控制请求经属主路由自动落到正确实例,使用者无感。 配置上把握四点——instance-id 自定义命名(唯一+稳定)、internal-base-url 配内网可达、node-forward-secret 各实例一致、cascade-node 在接收级联 ZLM 回调的实例上置 true(每实例只声明自己,承担者由共享表确定性选出)。单实例则全部留空,零配置即工作。
