线上会议"能进、有声音、没画面",这种工单我在过去两年里处理了不下三十起。头几次我条件反射地往带宽、NAT 映射、浏览器兼容性上查,查到后面才发现,十次里有六七次的根因根本不在媒体通道本身,而在一条平时没人正眼看过的控制链路——Colibri。

Colibri 这个词在实时音视频圈子里出现得不算少,但中文资料一直很碎,多数是零散的抓包截图,或者一句"分配频道失败了"就没了下文。这篇东西我按自己排障的顺序来写:先把它在整套架构里的位置摆正,再把报文拆开看它到底说了什么,然后给一套能在本地复现的观测环境,最后把我踩过的坑、容量估算的方法摊开讲。做实时音视频运维、后端开发,或者正在自建一套会议系统的朋友,应该都能直接拿去用;如果只是想知道结论,看完第一节和第四节也够。全文不涉及任何账号体系或商业产品推荐,讲的都是协议语义和排查手法,你可以放心按自己的环境类比。

1. Colibri 站在整套实时音视频链路的哪一格

1.1 谁指挥、谁搬运,先把两个角色分清楚

一套典型的实时音视频系统里至少有三个角色。信令服务负责让参与者"找到彼此",交换会话描述和能力集;控制器(很多实现里叫 focus)负责决定这场会议放在哪台媒体桥上、谁上榜、谁的画面需要被转发;媒体桥负责真正把 RTP 包从一个端点搬到另外若干个端点。Colibri 就是后两者之间的私有控制协议,它从早期 XMPP 扩展的讨论中长出来,最终形成了一套基于 XML 元素、承载在 XMPP IQ 上的约定。

这里要强调一个位置关系:Colibri 报文只在控制器和媒体桥之间跑,它跟参与者客户端之间没有任何直接关系。你在浏览器里看到的画面,是从媒体桥直接以 UDP 过来的,中间不经过控制器。理解了这条分界线,后面所有排查思路都会顺很多——凡是"完全进不了会议"的问题,去看信令;凡是"进去了但画面不对"的问题,才轮到 Colibri。

1.2 三类主体:会议、频道、端点

Colibri 的语义高度收敛,几乎所有的报文都在描述三类主体。第一类是会议(conference),代表"某一场会议在某一台媒体桥上的实例",注意是"某一台上",同一场会议如果跨了多台桥,每台上都会有一个独立实例。第二类是频道(channel),它是一条逻辑上的收发通道,一条频道通常对应一个端点的一路媒体,频道本身不描述端口,端口信息藏在它携带的会话描述里。第三类是端点(endpoint),它是参会者在这台桥上的表示,带一个短 id,还带着"这个端点当前是否在被转发"的状态位。

这三者的关系是层层包含的:一个会议实例下面挂若干端点,每个端点下面挂若干频道。频道是"物理通道",端点是"人的身份"。这条区分特别重要,因为线上故障里最常见的混淆就是——频道建好了,但端点没上榜,于是你从抓包里看到端口在、SDP 在,可就是没画面。这不是链路坏了,是转发策略没把它选中。

1.3 一个必须先纠正的误解

有个说法流传挺广:"Colibri 是媒体转发协议"。这句话是错的,而且错得会误导排查方向。Colibri 不搬任何一个媒体包,它搬运的是"意图":控制器告诉桥"我要给这几个端点开通道,参数是这样",桥回复"行,我按这个会话描述来收,端口是这些"。真正搬包的是 RTP 栈,走的是另外一条完全不同的网络路径,通常是 UDP,端口可能是几百个也可能是单个大端口。

所以"Colibri 挂了"这种描述本身就不精确。它要么表现为会议始终建立不起来,要么表现为会议建起来了但某些端点没有画面,要么表现为画面切换迟钝。反过来,如果用户反馈的是"声音断断续续""画面花屏",那大概率跟 Colibri 没关系,应该去查丢包、抖动缓冲和拥塞控制。把这两个层面分开,是我做这套系统运维的第一个习惯。

2. 把报文拆开看:IQ、会话描述与同步源的对应关系

2.1 承载方式:XMPP IQ 加自定义命名空间

Colibri 报文最外层是标准的 XMPP IQ 节,里面套一个自定义命名空间的元素,命名空间是 http://jitsi.org/protocol/colibri 。控制器发的是 set 类型,桥返回 result 或者 error。下面这段是简化过的骨架,真实报文会长很多,但结构就是这个结构:

<iq type='set' id='a1b2c3' from='focus@example.org/focus'
    to='jvbbrewery@internal.example.org/bridge-1'>
  <conference xmlns='http://jitsi.org/protocol/colibri'
              id='7f3a9c1e'
              name='room-42'
              gid='7f3a9c1e'>
    <endpoint id='2d4e6f80' stats-id='alice-3f2' >
      <channel id='5a7b9c0d' />
    </endpoint>
  </conference>
</iq>

注意几个细节: conference 上的 id 是这场会议在这台桥上的实例标识,后面查 REST 接口、对日志都靠它; name 是人类可读的房间名,只用于日志; gid 用来做跨桥级联时的全局关联。端点上的 id 是桥和控制器之间约定的短标识,通常是 8 位十六进制,它才是排查时的第一关键词,比你手上的房间号有用得多。

2.2 频道分配:请求什么、应答什么

分配频道是 Colibri 里最关键的一次交互。控制器在请求里说明"这场会议要新增这些端点、每个端点要开哪些通道",桥在应答里把每个通道的会话描述回填进去。应答里的会话描述中包含端口、传输协议、负载类型、以及一系列同步源属性。这个同步源属性是整条链路的"身份证",媒体包里带的同步源标识必须和会话描述里声明的一致,否则接收端会直接把包丢掉。

我见过不少"端口通了但不出画面"的案例,最后都是同步源对不上:控制器在请求里生成了一组标识,桥的应答里声明的是另一组,或者某个中间层把属性重写掉了。判断方法很简单,拿抓到的 RTP 头里的标识去和 REST 接口返回的会话描述做对账,对不上就是这一层的问题,不用往下怀疑网络。

<iq type='result' id='a1b2c3' from='jvbbrewery@internal.example.org/bridge-1'>
  <conference xmlns='http://jitsi.org/protocol/colibri' id='7f3a9c1e'>
    <endpoint id='2d4e6f80' stats-id='alice-3f2'>
      <channel id='5a7b9c0d'>
        <transport>
          <candidate ... />
          <rtcp-mux />
        </transport>
        <payload-type id='100' name='VP8' clockrate='90000' channels='1' />
        <ssrc-group semantics='FID'>
          <source>12345678</source>
          <source>87654321</source>
        </ssrc-group>
      </channel>
    </endpoint>
  </conference>
</iq>

上面这段是应答的简化形态。 ssrc-group 里 FID 语义表示后面那个源是前一个源的重传通道, SIM 语义则表示多档位同时发送。这两个语义是排错时的高频疑点,因为不同实现的字段拼写和大小写习惯不一样,版本一升级就容易踩到。

2.3 为什么用会话描述而不是自定义参数表

第一次看这套设计的时候我觉得挺别扭:既然是自己定的协议,为什么不干脆用一张干净的参数表,非要把一整套会话描述塞进来?后来做了一次跨版本升级才明白,这是典型的"借力"设计。媒体栈、转发库、编解码模块本来就认这套描述,直接复用意味着不用再维护一层翻译,新增编解码、新增扩展头的时候只需要在描述里加行,协议本身不用动。

代价是排错时要在两个抽象层之间来回翻译。一条描述里的属性到底影响哪个模块,光看报文看不出来,必须结合桥侧的日志和运行时状态。这也是为什么我强烈建议在环境里把日志级别打开、把 REST 接口放出来——只看报文,你永远只能看到一半。

2.4 端点上榜与选择性转发

媒体桥的资源不是无限的。一个房间 50 个人,如果每个人都接收另外 49 路视频,任何一台机器都会被下行带宽打死。所以这套系统默认只转发最近活跃的若干个端点,业内习惯叫它 last-N。音频通常全转发,视频按策略裁剪,这个"裁剪"的动作就发生在 Colibri 这一层:控制器在报文的端点元素上标注哪些需要被转发,桥据此决定往哪个通道发什么。

这里有个坑值得专门说:last-N 的取值往往不是控制器自己写死的,而是客户端在入会时上报,控制器侧再有一个兜底默认值。不同发行版本的参数名差别很大,我不建议你去翻文档一个个对,更快的办法是在控制器日志里直接看它实际下发给桥的值是多少。我自己的经验是,只要在日志里看到"下发的 N 是 5",那"只有 5 个画面"这件事就完全不是故障,而是设计行为。

3. 让 Colibri 可见:搭一个最小可观测环境

3.1 组件清单与拓扑

要复现和观测 Colibri,你至少需要四个组件。下面这张表是我常用的最小实验环境清单,全部可以跑在同一台虚拟机上,内存 8G 起:

组件 角色 关键端口 说明
信令服务 转发 IQ、管理在线状态 5222 / 5347 控制器和桥都以组件身份接入
控制器 决策者,发起 Colibri 请求 无对外端口 与信令服务同机时通常走本地回环
媒体桥 执行者,回填会话描述 10000/udp、9090/tcp 9090 用于状态接口
一个 Web 客户端 产生端点 443 只需要能入会即可

拓扑本身没什么可讲的,关键在第二列的"说明":控制器和桥都是以独立身份接入信令服务的,而不是互相直连。这意味着如果你要抓 Colibri 报文,抓的其实是"控制器到信令服务"或者"桥到信令服务"这两段之一,而不是一条想象中的点对点连接。很多人抓不到包,就是抓错了网卡。

3.2 打开媒体桥自带的状态接口

媒体桥通常会暴露一个 HTTP 接口,用来查看当前所有会议和它们的频道分配情况。新版配置走的是结构化配置文件,旧版走的是键值属性文件,字段名各版本有出入,以你实际的文件为准。典型配置长这样:

videobridge {
  http-servers {
    public {
      port = 9090
    }
  }
  ice {
    udp {
      port = 10000
    }
    tcp {
      enabled = false
      port = 4443
    }
  }
}

配置完重启,先确认端口起来了:

ss -lntp | grep 9090
curl -s http://127.0.0.1:9090/about/health
curl -s http://127.0.0.1:9090/colibri/conferences | head -c 500

第一个接口返回健康状态,第二个返回当前所有会议实例的标识列表。拿到标识之后再打一次详细接口,就能看到这台桥上一场会议的全部端点、每个端点的通道、以及每条通道的会话描述。我几乎每天都会用这两个接口,它比翻日志快十倍。

3.3 抓控制器与桥之间的 XMPP

很多部署里,控制器、桥、信令服务三者在同一台机器上,这一段连接走本地回环,而且未必启用了加密。这种情况下你可以直接抓包看报文内容:

tcpdump -i lo -A -s 0 'tcp port 5347' -w colibri.pcap
# 或者不落盘,直接过滤关键字眼
tcpdump -i lo -A -s 0 'tcp port 5347' | grep -A 30 'protocol/colibri'

如果这一段启用了加密,抓包就只能看流量形状了,这时候改用日志:把桥侧的协议调试开关打开,它会把收发的每一节 XML 打到日志里。打开之后日志会暴涨,务必只在排障窗口期开,排完立刻关掉。

抓下来之后,最快的读法是按 IQ 的 id 去对齐请求和应答。我一般把报文导成一个文本文件,用 grep 反复扫同一个 id,把"请求—应答—错误"三件事拼成一条时间线。这个动作看起来笨,但它能让你在不看任何源码的情况下判断出问题出在哪一侧。

3.4 用状态接口做定时巡检

最后给一段巡检脚本的思路,不需要多复杂,定时拉一次会议列表,统计会议数和端点总数就够了:

#!/usr/bin/env bash
JVB="http://127.0.0.1:9090"
conf_ids=$(curl -s "$JVB/colibri/conferences" | grep -o '"id":"[^"]*"' | cut -d'"' -f4)
for cid in $conf_ids; do
  detail=$(curl -s "$JVB/colibri/conferences/$cid")
  eps=$(echo "$detail" | grep -o '"id":"[^"]*"' | wc -l)
  echo "$(date '+%F %T') conf=$cid endpoints=$eps"
done

这段脚本的价值不在于统计本身,而在于趋势。当某台桥的端点总数长期贴着上限跑的时候,你不用等故障发生,提前把它从调度池里摘出去就行。我在生产上就是靠这种最土的脚本提前躲过了两次大会高峰。

4. 四类高频故障的完整排查链路

4.1 有声音没画面:先确认频道分配到底成没成

这是最常见的一类。排查顺序我固定成四步,一步都不跳。

第一步,在客户端侧确认是否收到了远端同步源描述。如果压根没收到,问题在控制层;如果收到了但没画面,问题在媒体层,方向完全不同。

第二步,看控制器日志里针对这场会议的下发记录,重点找有没有错误应答。Colibri 的错误应答通常带着一个错误条件和一个文字说明,比如"没有可用的资源""指示的会议不存在"之类。看到这个基本就定位了。

第三步,如果分配成功了,去看桥侧的状态接口,确认端点和通道都在。这一步能区分"策略没选中"和"通道没建好"——没人上榜是策略问题,通道缺失是分配问题。

第四步,通道都在还没画面,才轮到查网络。这时候抓一下 UDP 端口,看有没有 RTP 包进来。有包进来但不出画面,多半是同步源标识对不上,回第 2.2 节的对账方法;没包进来,就是端口没放开或者地址通告错了。

提示:我见过的最隐蔽的一例,是桥通告给客户端的地址是内网地址,客户端在公网侧永远连不上,但音频因为走的是另一条早就打通的通道所以正常。查这类问题时,一定要把桥通告出去的地址和实际可达地址单独核对一遍。

4.2 只有固定几个人有画面:这是 last-N 在起作用

症状很有辨识度:房间里有 20 个人,但每个人屏幕上永远只有那几个人的画面,说话的时候会换,换的时候有点迟钝。这种一般不是故障,是 last-N 的默认策略。先看下发给桥的那个数字是多少,如果是 5,那一切正常。

真要调整,也不是直接把 N 拉满。N 拉满意味着每个端点的下行流量乘以参会人数,10 个人的房间拉到 9 档就已经很可观了,50 个人的房间拉满基本等于自杀。我的做法是分层:小房间(10 人以下)直接把 N 设成等于人数,中等房间保持 5 到 8,大房间老老实实维持默认,需要看谁就靠点名或主持人的"置顶"功能。

顺带说一个相关的现象:从"没画面"到"出画面"的切换变慢,往往不是 last-N 的取值问题,而是承载变更通知的那条长连接没通。这条连接走的是媒体桥上的另一个端口和路径,跟前面的状态接口不是一回事。判断方法是看浏览器开发者工具里那条长连接有没有建立成功,没建立成功的时候,画面切换只能等下一次全量刷新,体验就会明显发钝。

4.3 人一多就抖:端口、线程和跨桥的三重门

人数一多就出现抖动、卡顿、零星掉线,通常是三个瓶颈叠加,按这个顺序查最省时间。

第一个是端口池。单个大端口模式下这个问题不明显,但如果你的部署用的是端口段模式,端口池耗尽会直接导致新端点无法分配通道。查法是看桥侧日志里有没有"无法分配"的记录,同时用 ss -lunp 数一下监听端口个数。

第二个是线程和内存。桥是 JVM 程序,堆给得太小会在高并发下频繁做垃圾回收,表现出来就是周期性卡顿。我的经验是堆内存不低于 4G,CPU 核心数和并发端点的比例控制在 1:15 到 1:25 之间,超过这个区间就该考虑拆了。

第三个是跨桥级联。当一场会议被分到多台桥上时,桥之间还要互相转发一路媒体,这条链路的带宽和质量会被放大到整场会议。这时候你要看的不是单个桥的负载,而是桥与桥之间的平均往返时延和丢包率。

症状 先看哪里 常见根因 处置
全员无画面,音频正常 桥通告的地址 地址通告错误或端口未放通 修正地址通告,放通 UDP 端口
只有固定几人出画面 下发的 last-N 值 默认策略裁剪 按房间规模分层调整
画面切换迟钝 客户端长连接 变更通知通道未建立 打通端口并核对路径配置
人数一多就抖 端口池、堆内存、CPU 资源见底 提前摘除调度或拆分实例
改配置后会议起不来 配置文件字段 版本间字段名或语义变化 回滚配置,逐字段比对

4.4 改完配置反而起不来:版本差异是主因

第四个坑最气人,因为它是自己造成的。典型场景是这样:为了调一个参数,改了配置文件,重启之后发现新会议完全建立不起来,日志里是协议层的报错。

根因通常有两个。一是字段名在小版本之间改了,旧文档抄过来的键名其实已经被废弃,程序读不到就走默认值,默认值又偏偏不适用于你的网络环境。二是字段语义变了,比如同步源分组的大小写习惯、重传语义的表达方式,升级后控制器和桥对同一份描述的理解不一致,结果就是分配请求被拒。

处理这类问题,我的流程固定为"一次只改一个键,改完立刻用状态接口验证"。听起来很笨,但比一次改五个键然后花三小时二分定位要快得多。

5. 容量与参数:把配置项换算成可用的数字

5.1 端点带宽估算表

容量规划的第一步是知道一个端点值多少带宽。下面这组数字是我自己在 720p、单档位、开启重传的实际环境里测出来的,可以当起点用,具体到你的编解码和档位设置肯定有偏差:

场景 上行(每端点) 下行(每端点,N=5) 备注
纯音频会议 50 到 70 kbps 50 kbps 乘以人数 音频一般全转发
360p 视频会议 0.6 到 1.0 Mbps 3 到 5 Mbps 小房间可用
720p 视频会议 2.2 到 3.5 Mbps 12 到 18 Mbps 主流配置
多档位同时发送 上行的 1.5 到 2 倍 与 N 基本无关 档位越多上行越高

注意最后一行。多档位同时发送是个双刃剑:它让桥可以按接收端能力挑档位,从而省下行,但发送端的上行会明显增加。移动端入会比例高的场景要特别小心,很多手机上行撑不住三档同时发。

5.2 单台桥能承载多少端点

把上面的数字和一个经验系数放在一起就能估。我的算法是:先算网卡,再算 CPU,取两者的小值。

网卡口径:单台桥千兆网卡实际可用按 800 Mbps 算,留 50% 余量就是 400 Mbps 稳态。按每端点上下行合计 6 Mbps 估,大约 65 个端点。CPU 口径:4 核虚拟机在 720p 场景下我实测能撑 50 到 80 个端点,8 核能到 120 到 160。两者取小,所以 4 核千兆的机器,我一般把调度上限设在 50,超过就分流到下一台。

这个数字不用追求精确,它的作用是让你在写调度策略的时候有个抓手,而不是等告警来了才凭感觉摘机器。

5.3 什么时候该级联,什么时候该加机器

很多人一遇到容量问题就加机器,这不一定对。判断标准是"瓶颈在哪一层"。

如果瓶颈是单台机器的 CPU 或网卡,加机器后把会议拆到多台上,配合跨桥级联确实能解决。但如果瓶颈是单个房间的人数——比如一场 300 人的全员大会——加机器只能解决机器侧的问题,解决不了"一个房间里 300 个人"这个事实,因为 last-N、下行聚合、信令风暴这些压力是跟房间绑定的。这种情况下更实际的做法是限流:控制单个房间的入会规模,或者把大会拆成主会场加分会场。

我的判断口诀是:压力跟着房间走,就限房间;压力跟着机器走,就加机器。这句话帮我在两次架构评审里省掉了很多争论。

5.4 灰度与回滚:改这套系统的唯一正确姿势

最后说流程。Colibri 相关的改动,无论是换桥的版本、调 last-N、还是改端口配置,都属于"影响面很大但验证成本很低"的类型——验证成本低是因为你只需要开两个客户端入会看一眼。所以我坚持的做法是:改动前先把当前配置整个备份下来,改完只在一台桥、一个房间上验证,两个客户端分别在不同网络环境下入会,确认有声音有画面、并且状态接口返回的端点数和通道数符合预期,再推到全量。

回滚也要预演。配置文件改坏了桥是起不来的,这时候最快的恢复方式是把备份文件覆盖回去重启,而不是现场找问题。我给自己定的规矩是:任何一次改动,先算一遍"如果三分钟后必须回滚,我需要执行哪几条命令",想清楚了再动手。

6. 三个我改了很久才改对的细节

第一个是端点标识和同步源的对应关系。我早期排障时喜欢用房间号去搜日志,结果发现同一场会议跨了多台桥之后,房间号根本定位不到具体是哪台桥上的问题。改成用端点标识去搜之后,日志、状态接口、抓包三个来源一下子就能串起来。这个习惯转变花了我差不多两个月,但从那以后我的平均排障时间直接砍了一半。

第二个是变更通知那条约定的长连接。我一度以为画面切换迟钝是客户端性能问题,查了浏览器的渲染耗时、查了编解码器的切换耗时,全都正常。后来才意识到,那条承载变更通知的长连接根本没建立起来,所以"谁在上榜"这个信息只能靠全量刷新传递。打通它之后,切换体验的变化是肉眼可见的。这件事让我记住一条:这类系统里"延迟体现出来的问题",十有八九是某条通道压根没通,而不是通道慢。

第三个是端口池的规划。我接手的一套环境里,端口段只留了几百个,日常够用,一到大房间就零星有人分不到通道,表现是"偶发有人进不来画面"。这种偶发问题最难查,因为你怎么试都复现不了。最后是看了状态接口里通道数的峰值才发现端口不够。现在的做法很土:把端口池按"峰值端点数的三倍"来留,宁可多占一点系统资源。

写到这里内容其实够了,最后补一句我个人的做法:这套东西我不建议你靠读文档去学,最好的方式是在一台闲置的机器上把最小环境搭起来,开一个房间,两个人入会,然后一边抓包一边刷状态接口。你会看到端点是怎么一个一个出现的、通道是怎么被回填的、last-N 是怎么把不相干的人裁掉的。这三个画面在脑子里连成一条线之后,剩下所有的排查都只是在这条线上找断点而已。

Logo

火山引擎视频云技术社区,是面向 AI 音视频开发者的技术交流平台。这里汇聚源自抖音、豆包等亿级 DAU 产品的 RTC、直播、点播、AI 媒体处理、音视频互动技术,提供接入指南、最佳实践、性能调优、场景案例、Demo 代码、开源项目、白皮书和 API 文档。社区汇聚官方工程师与一线开发者,为 AI 视频通话、数字人、AI 视频处理等应用的开发与落地提供技术支持。

更多推荐