尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Jitsi Colibri 控制信令、统计监控与故障排查实战

Jitsi Colibri 控制信令、统计监控与故障排查实战 Colibri 这个名字我第一次见到是在一台 Jitsi Videobridge 的日志里——一行 WebSocket 握手记录后面跟着一串看不出含义的 ID。当时我以为是某个新出的媒体库翻了一圈才发现它是自建会议系统里最关键、也最容易被跳过的一层控制信令协议。简单说JVBJitsi Videobridge只负责转发音视频Jicofo 负责当指挥而 Colibri 就是这两者之间的那根线——谁在建会、谁分配了哪路流、这路流转给谁、什么时候该把过期的资源清掉全部靠它传递。它同时还是 JVB 对外暴露运行指标的那套接口的统称也就是那个经典的/colibri/stats。大多数人是照着安装脚本一路回车把服务跑起来能开会就算完事。直到会议人数上去开始出现进不去房间画面全黑bridge 莫名其妙丢会议才被迫去读 JVB 日志然后被一堆 Colibri 相关的报错拦住。这篇文章就是写给这个阶段的你不重复官方文档里那几句架构介绍而是把 Colibri 的消息结构、两条通道的分工、/colibri/stats里每个字段该怎么用来做监控和容量估算以及我踩过的坑一条条摊开讲。读者不需要是 WebRTC 专家但至少要能登服务器、看日志、改配置文件。1. Colibri 在整套架构里的位置1.1 先认清角色谁在跟谁说话把自建会议拆开看参与的组件就那么几个前端页面jitsi-meet、XMPP 服务Prosody、协调者Jicofo、媒体转发JVB需要电话接入再加 Jigasi需要录制再加 Jibri。Colibri 只活在 Jicofo 和 JVB 之间以及 JVB 和浏览器之间那一段。这就带来一个很常见的误解很多人以为 Colibri 是媒体协议其实它跟 Opus、VP8、H.264 这些编码没有半点关系。它传输的是元信息——会议的 ID、端点的 ID、哪条 channel 属于哪个端点、这条 channel 是收还是发、什么时候过期。真正的音视频走的是 SRTP/RTP走的是另外的端口跟 Colibri 完全是两套东西。我习惯用一个类比Colibri 相当于会议室的排班表和门禁系统它决定谁在哪个房间、哪扇门开、几点关门而 RTP 是房间里实际说的话。排班表出了问题人会走错房间或者被关在门外但房间里的话本身可能一点毛病没有——这也是为什么 Colibri 类的故障经常表现为音频正常、画面全黑能听到人但进不去这种偏诡异的现象。理解这一点之后排查思路会立刻清晰只要是连不上、连得慢、莫名断先看 Colibri只要是花屏、卡顿、声音断续先看 RTP 和网络质量。1.2 为什么控制信令要单独拆成一层有人会问为什么不把建会逻辑直接塞进媒体转发里非要多一层协议答案在扩展性上。早期的会议室模式是每个参与者互相直连人数一多上行带宽就爆炸。换成 SFU选择性转发架构之后每个端点只往服务器发一路流服务器负责按需转发给其他人。这时候服务器就必须知道谁需要看谁的画面——而这个决策是动态的屏幕共享时要切换、有人静音了要降码率、网络差的端点要减少接收路数。这些决策每秒都可能变必须有一层轻量、可靠、可编排的信令来承载Colibri 就是干这个的。再往深一层说拆出控制面之后浏览器和 JVB 之间的连接方式就可以独立演进。控制面走 XMPP 或 WebSocket媒体面走 UDP两者互不绑定。JVB 可以横向扩展成多台Jicofo 只要知道每台的能力和负载就能把会议分派到不同的 bridge 上——这个分派动作本质上就是向目标 bridge 发一条 Colibri 建会请求。同理一台 bridge 挂了Jicofo 只要把会议重新分配到另一台即可不需要动媒体面的任何配置。这套设计带来的直接好处是你可以把 JVB 部署在多台机器上做负载均衡前端完全无感。但代价也很明显——多了一个必须稳定的链路。Jicofo 和 JVB 之间的连接一旦抖一下表现就是大面积掉会。后面讲故障排查时这一条是最高频的根因。2. Colibri 消息结构拆解2.1 conference / endpoint / channel 三层模型Colibri 的消息体本质上是一个嵌套三层的结构想看懂日志和调试接口先记住这三个词就够用了。最外层是conference代表一个会议带一个唯一的会议 ID。中间层是endpoint代表一个参与者终端每个 endpoint 有自己的 ID通常还带一个用于统计展示的标识。最内层是channel代表一路具体的媒体通道一个 endpoint 一般至少有两路 channel音频一路、视频一路开了数据通道还会有额外的一路。channel 上有几个字段值得单独拎出来讲id通道唯一标识日志里定位问题主要靠它。directionsendrecv、recvonly、sendonly三种。注意这是站在端点视角说的理解反了会把收发方向搞混。exp过期秒数默认 60。这是整个 Colibri 机制里最容易出事的一个数字下面单独讲。rtp-level-relay-type转发模式常见的是按流转发translator和混合mixer。现在的默认部署基本都是按流转发。initiator标识哪一端发起了这条通道的协商。三层模型的好处是粒度可控想在会议级别做操作就只更新 conference想单独调整某个人的接收路数就只改它对应的 endpoint 节点。这也解释了为什么 JVB 日志里经常出现整体会议信息没变、但某个 endpoint 被反复更新的情况——那是前端在根据实时网络状况动态调整接收策略。提示exp不是通道最长存活时间而是多久没收到续期就把这条通道删掉。它是 Keep-Alive 语义不是 TTL 语义别搞反。2.2 allocate、update、expire 的差别与续期机制Colibri 的操作可以归纳成三类理解它们的分工对定位为什么会议开一分钟就断这类问题特别有帮助操作类型触发时机关键特征常见故障表现建会 / 分配第一个参与者进入房间一次带上完整的 conference 结构和初始 channel会议建不起来报找不到会议更新有人加入、离开、切屏、网络变化只改变化的节点可能每秒多次画面不更新、共享黑屏过期清理超过exp秒没收到续期JVB 单方面删除资源会议在固定时间点集体掉线这里重点说续期。Jicofo 会在 channel 到期之前周期性地向 JVB 重新发送会议信息社区里俗称的 Colibri 心跳周期明显小于 60 秒的默认值所以正常情况下你永远看不到过期发生。一旦这条心跳断了事情就会按固定剧本发展先是个别通道被清掉表现为某个人听不到声音接着 endpoint 被回收最后整个会议被销毁所有人在差不多同一时间掉线。会议存活时间高度固定是这类故障最典型的特征——比如每次都是 60 秒左右断或者每次都是 90 秒左右断基本可以直接锁定续期链路不用再查媒体面。我实测下来最常见的三个诱因是Jicofo 与 JVB 之间的 XMPP 长连接被中间的网络设备掐掉、JVB 的进程因为内存压力被系统杀掉后重启这样内部状态全丢、以及多 bridge 环境下某台机器的时钟偏差过大导致续期判定异常。第三个最隐蔽症状是只有部分会议有问题换台机器就好了很容易被误判成偶发。3. XMPP 与 WebSocket 两条通道3.1 XMPP 通道为什么一直没被淘汰Colibri 最初就是跑在 XMPP 上的Jicofo 部署在 XMPP 域里JVB 作为一个组件连上来两者通过标准的 XMPP 消息交换会议信息。这种做法的好处是复用现成的服务发现、认证和路由能力——Jicofo 不需要提前知道有多少台 JVB谁上线了谁就在它的可用列表里。它的代价是延迟偏高、每条消息都有 XML 包装开销。对于建会、分配、回收这种低频操作完全可以接受但对于每秒都在变的接收策略就有点吃力了。所以后来才有了第二条通道。在实际部署里两条通道是并存的低频的编排走 XMPP高频的状态同步走 WebSocket。你去看配置文件会发现两边都得配少配一边服务也能起来但会有一类能用但体验很奇怪的问题比如连接建立特别慢、切屏后画面有十几秒的延迟。这类问题最难查因为服务确实在正常工作。3.2 Colibri WebSocket 与反代配置WebSocket 这条通道的作用是把端点侧的高频消息直接送到 JVB不必再绕一圈 XMPP。典型场景包括ICE 候选者的传递、接收约束也就是这次要收哪几路流的实时调整、屏幕共享时的流切换通知。它在 Nginx 上需要一段专门的反代配置结构大致是这样实际内容请以你机器上安装脚本生成的为准location ~ ^/colibri-ws/([0-9a-zA-Z-])/(.*) { proxy_pass http://$1:9090/$2$is_args$args; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection upgrade; proxy_set_header Host $host; tcp_nodelay on; }这段配置里几个细节值得解释一下。路径里的第一段被当作目标主机名来用所以会议 ID 里不能出现特殊字符Upgrade和Connection两个头是 WebSocket 握手必需的漏掉任何一个都会退化成普通的 HTTP 请求握手直接失败tcp_nodelay on是为了避免小包被 Nagle 算法攒起来视频会议这种小消息密集的场景必须打开我见过因为这一行没配导致消息延迟几百毫秒的案例。至于前端怎么知道该连哪个地址不同版本的下发方式不一样早期由前端配置里的字段直接指定后来改成由 bridge 自己在上线时把地址广播出去。这也是升级时最容易出问题的地方——如果你手动改过 Nginx升级脚本又生成了一份新的两份配置叠加之后就很难排查了。我的做法是升级前先备份配置文件升级后 diff 一遍确认 Colibri 相关的段落没有被意外改掉。3.3 端点上跑的消息类型WebSocket 通道里传的消息通常带一个类型字段作为区分标识常见的有这么几类端点消息参与者之间互发的小消息比如举手、表情、实时字幕走的是数据通道的通道但协调逻辑经过这里。接收约束告诉 bridge我这次只想收哪几路视频是自适应码率的核心。固定端点变更屏幕共享或手动固定某个人时通知所有人切换画面。最后 N 路变更接收路数上限变化时同步。这些消息的共同特点是频次高、单条小、允许丢。所以它们的传输优先级低于媒体流拥塞时会先被丢。这解释了一个常见现象网络稍差时画面还在走但举手、字幕这类功能先失灵。注意调试 WebSocket 时不要只看 Nginx 的访问日志那条连接升级之后就不再产生常规日志了得看 JVB 侧的日志或者直接用浏览器开发者工具看握手和帧。4. 把 /colibri/stats 变成可用监控4.1 打开统计通道JVB 的统计输出支持多种传输方式常见的是通过 XMPP 汇报给协调者以及通过 HTTP 暴露成接口。后者就是/colibri/stats也是最适合接监控的一种。要让这个接口可用需要在 JVB 的配置里显式启用。不同版本的配置项名称有变化早期是分散在几个属性里的新版本统一到一份结构化配置里。我不建议你直接抄网上的片段而是按下面的步骤确认登录 JVB 所在机器先用netstat -lntp或ss -lntp看 8080 端口有没有被 JVB 监听。直接请求一次curl -s http://127.0.0.1:8080/colibri/stats | head -c 200。有 JSON 返回说明已经开了返回 404 或者连接被拒绝再去配置文件里确认统计传输方式有没有包含 HTTP 这一项。接口能通之后第一时间做两件事一是把它绑到本地回环或内网地址绝不要让它监听在公网网卡上二是确认防火墙规则只允许监控机器的地址访问。4.2 字段逐个看哪些能直接做告警这个接口返回的字段有几十个全接进监控只会让自己看不过来。我的做法是按能不能直接对应一个线上问题来筛选下面这张表是我实际在用的一个子集字段含义建议用途conference_count当前会议数大盘展示配合最大容量算水位participant_count登记在册的端点总数容量水位主要依据largest_conference当前最大会议规模排查单会议异常很有用bit_rate_download对外发送的码率出口带宽水位bit_rate_upload从端点接收的码率入口带宽水位loss_rate_download下行丢包率大于 1% 开始关注大于 5% 告警stress_level过载等级整数 0/1/2大于 0 就告警这是最有价值的一个指标endpoints_ice_failed累计 ICE 失败数出现增量即告警几乎必有问题total_lost_conferences累计因超时销毁的会议数出现增量即告警对应续期链路故障total_failed_conferences累计建会失败数出现增量即告警通常是资源或配置问题video_channels / audio_channels当前通道数和 participant_count 对照异常比例能看出问题cpu_usage进程自报的 CPU 占用水位监控注意它是比例不是百分比used_memory已用内存配合堆大小看 GC 压力这里要提醒两个坑。第一stress_level是 0、1、2 三个整数值不是百分比别拿来当百分数画图。第二累计类的字段名字里带 total 的那些本身只会增长画曲线看不出问题必须用增量或者增长率来告警否则你只会看到一条永远向上的斜线。4.3 采集脚本与监控落地有了接口接下来就是定时拉取。我最初图省事直接用循环加命令行工具往日志里写后来发现数据没法做趋势分析还是得上时序库。下面是简化过的采集脚本逻辑很简单定时拉一次把关心的字段映射成指标暴露出一个 HTTP 端口交给现成的监控系统来抓。#!/usr/bin/env python3 jvb_stats_exporter.py 定时拉取 JVB 的 /colibri/stats暴露成 Prometheus 指标。 import json import time import urllib.request from prometheus_client import Gauge, start_http_server STATS_URL http://127.0.0.1:8080/colibri/stats INTERVAL 15 GAUGES { conference_count: Gauge(jvb_conference_count, 当前会议数), participant_count: Gauge(jvb_participant_count, 当前端点数), largest_conference: Gauge(jvb_largest_conference, 最大会议规模), bit_rate_download: Gauge(jvb_bit_rate_download_kbps, 发送码率), bit_rate_upload: Gauge(jvb_bit_rate_upload_kbps, 接收码率), loss_rate_download: Gauge(jvb_loss_rate_download, 下行丢包率), stress_level: Gauge(jvb_stress_level, 过载等级 0/1/2), endpoints_ice_failed: Gauge(jvb_endpoints_ice_failed, ICE 失败累计), total_lost_conferences: Gauge(jvb_total_lost_conferences, 超时销毁累计), total_failed_conferences: Gauge(jvb_total_failed_conferences, 建会失败累计), video_channels: Gauge(jvb_video_channels, 视频通道数), audio_channels: Gauge(jvb_audio_channels, 音频通道数), cpu_usage: Gauge(jvb_cpu_usage, 进程 CPU 占用比例), used_memory: Gauge(jvb_used_memory_bytes, 已用内存字节), } def fetch_stats(): # 一定要带超时否则 JVB 卡住时采集脚本会一直挂着 with urllib.request.urlopen(STATS_URL, timeout5) as resp: return json.loads(resp.read().decode(utf-8)) def main(): start_http_server(9812) while True: try: data fetch_stats() for key, gauge in GAUGES.items(): value data.get(key) if isinstance(value, bool): value int(value) if isinstance(value, (int, float)): gauge.set(value) except Exception as exc: print(f[warn] 拉取 stats 失败: {exc}, flushTrue) time.sleep(INTERVAL) if __name__ __main__: main()有几个地方是踩过坑之后才加上的。超时必须设JVB 在高负载下响应会变慢没有超时的客户端会把连接堆住最后自己变成内存泄漏源布尔字段要显式转成整数监控系统对布尔值的解析各版本不一采集周期别贪快15 秒足够1 秒一次在大会场景下自己就会给 JVB 增加可见的负担。采集端暴露出来之后监控侧加一段抓取配置就行scrape_configs: - job_name: jitsi-jvb scrape_interval: 15s static_configs: - targets: [127.0.0.1:9812]告警规则我建议从最保守的几条开始避免一上来就被噪音淹没groups: - name: jitsi-jvb rules: - alert: JvbStressHigh expr: jvb_stress_level 0 for: 2m labels: severity: warning annotations: summary: JVB 进入过载状态考虑加机器或限制单人上传码率 - alert: JvbIceFailures expr: increase(jvb_endpoints_ice_failed[10m]) 0 for: 5m labels: severity: warning annotations: summary: 出现 ICE 失败检查 NAT 与端口放通情况 - alert: JvbLostConferences expr: increase(jvb_total_lost_conferences[10m]) 0 for: 1m labels: severity: critical annotations: summary: 出现因超时销毁的会议优先检查 Colibri 续期链路 - alert: JvbCpuHigh expr: jvb_cpu_usage 0.7 for: 10m labels: severity: warning annotations: summary: JVB CPU 持续高于 70%4.4 容量估算从统计数字反推机器该配多大网上关于一台桥能带多少人的说法五花八门从几十到几百都有。这些数字之所以互相矛盾是因为它们隐含的前提完全不同码率、接收路数、是否开共享、CPU 型号都会让结果差出好几倍。所以我的建议是别抄数字自己算一遍。估算的核心公式只有一条单个端点的下行占用 ≈ 它实际接收的视频路数 × 单路平均码率。接收路数大致等于会议人数和接收上限中的较小值。拿一个具体场景算一遍。假设 20 人会议接收上限设为 5 路720p 单路码率约 1.6 Mbps出口方向桥发给端点20 × 5 × 1.6 Mbps ≈ 160 Mbps入口方向端点发给桥20 × 1.6 Mbps ≈ 32 Mbps音频部分20 路 × 约 50 kbps ≈ 1 Mbps量级上可以忽略但别忘加上 RTCP、重传、协议头开销按 10%~15% 放大结论很清楚出口带宽是第一个瓶颈而且是入口的五倍量级。很多人配机器时按人数 × 单路码率去算算出来的需求只有实际的五分之一上线必炸。规划时直接按 200 Mbps 出口预留再考虑留一倍余量应对突发。CPU 这边我没有办法给你一个通用数字因为不同代际的处理器差异太大。可行的做法是先做一次基线测试找 6 个人开 720p跑五分钟记录cpu_usage、bit_rate_download和系统层面的上下文切换次数然后按人数线性外推把告警线定在cpu_usage超过 0.7 的位置。另外注意 JVB 是单进程多线程模型核心数少于 4 的机器基本不要指望能上量这不是靠加内存能救的。内存方面used_memory要配合堆大小一起看。如果它长期贴近上限并且锯齿明显频繁 GC说明堆给少了或者有泄漏。我见过一次是采集脚本自己漏了连接三天后 JVB 被系统直接杀掉日志里一点线索都没有最后靠dmesg里那条内存不足的记录才定位到。5. 常见故障与排查实录5.1 建会失败与秒断的排查顺序这类问题的排查顺序我固定成四步能覆盖八成的场景第一步看时间特征。是永远建不起来还是建起来后固定时长断固定时长断直接跳到续期链路别浪费时间在别的地方。第二步对照指标。total_failed_conferences涨说明建会阶段就失败了通常是资源不足或配置错误total_lost_conferences涨说明是运行中丢的重点是心跳。两个都没动但用户报故障那大概率不是桥的问题去看前端或网络。第三步验证通道。在 JVB 机器上直接请求一次本地统计接口确认接口活着再确认 Jicofo 到 JVB 的连接还在可以看 Jicofo 的日志里是否还有这台桥的心跳记录。第四步看时钟。多桥环境下所有机器的时钟必须同步偏差过大会让续期判定失效。这一条排在最后但确实是最容易漏的。5.2 一份可以贴在工位上的速查表现象优先看什么大概率原因处理方向会议固定时长后全体掉线超时销毁计数续期链路中断检查协调者与桥的连接稳定性能听到声音但画面全黑视频通道数WebSocket 通道未生效检查反代配置是否被升级覆盖连接建立特别慢通道配置只走了一条通道确认两条通道都已启用参会人数与实际不符端点总数与 ICE 失败数僵尸端点未清理检查端点回收与前端重连逻辑单个人听不到别人音频通道数个别通道被回收让该用户重新加入观察是否复现建会直接报错建会失败计数端口或资源不足检查端口占用与系统资源只在大会议室出现卡顿过载等级、CPU单机容量到顶拆分会议到多台桥或降低接收路数这张表里我要特别强调第二行。画面黑但声音正常是 WebSocket 通道失效的典型表现因为 ICE 协商和质量反馈走不通媒体面虽然勉强建立起来了但视频流没法正确切换。很多人第一反应是去查编码和带宽绕一大圈才发现是升级时反代配置被覆盖了。5.3 别忽视安全加固Colibri 相关的接口是典型的方便但危险默认部署下有几个点必须收一下。统计接口和会议管理类的 REST 接口绝对不要暴露在公网。会议管理类接口早期能直接列出全部会议标识等于把所有房间号贴在门口。新版本默认关闭了这类接口但如果你的版本还开着务必关掉或者用防火墙限制来源。WebSocket 的反代规则不要图省事写成全通配。路径里分段匹配把会议标识和端点标识分开约束能挡掉一批扫描和乱连。顺手把不需要的对外接口关掉只保留必要的通道。另外桥与协调者之间的认证凭据要定期轮换并且保证监控系统里不出现明文。我见过把凭据写在采集脚本里、脚本又提交到代码仓库的情况这在自建环境里是很常见的疏忽。6. 我个人踩过的几个坑最后这部分没有体系就是这几年实打实踩出来的经验写出来给后来人省点时间。升级会覆盖你的反代配置。这是我最常遇到的坑。安装脚本重新生成配置文件时会把你手动加的段落冲掉表现是升级当天一切正常第二天开始陆续有人反馈画面黑。养成习惯升级前备份、升级后 diff重点看 WebSocket 相关的路径有没有变化。采集脚本本身会变成故障源。我给脚本加超时和周期限制之前它因为连接没释放三天吃掉了大半内存最后被系统的内存保护机制干掉顺带把同机的桥也拖累了。现在我的原则是采集端永远比被采集端更怂短超时、低频率、异常只记录不重试。别把采集代码写死在明细字段上。统计返回里确实有会议和端点的明细数组但字段构成跟版本走得很紧升级一次就可能变。我只用那些稳定的顶层数值字段明细信息需要的时候手动查一次不写进长期跑的代码里。指标单位要在接入时就统一。码率字段的单位容易记错我在 Grafana 上把 kbps 当 Mbps 画了两周直到有人问为什么你们带宽只用了百分之几才发现。建议在指标命名里直接把单位写进去虽然名字长一点但省心。过载等级是最值得告警的一个字段。它由桥自己根据负载算出来比你自己拼 CPU、带宽、丢包的组合规则要准确得多。我现在所有自有部署里这条告警的优先级都是最高的只要它一动先扩容再说别急着分析根因——会议正在断。
返回列表