FreeSWITCH 架构设计之快与脆
Posted on 五 18 9月 2026 in Tech
| Abstract | FreeSWITCH 架构设计之快与脆 |
|---|---|
| Authors | Walter Fan |
| Category | Tech |
| Version | v1.0 |
| Updated | 2026-09-18 |
| License | CC-BY-NC-ND 4.0 |
大纲
展开看看
- 一句话:FreeSWITCH 把整套电话系统装进一个进程,用
switch_core做内核、dlopen加载的模块做扩展。这是它又快又脆的同一个根源。 - 状态边界在哪:状态归 session/channel,一路呼叫一个线程一台状态机;模块只是这台状态机上的插件,
switch_core_session_locate()是跨线程访问的唯一安全门。 - 为什么不拆微服务:信令和媒体如果跨进程,要么共享一大坨状态、要么把状态在网络上搬来搬去。FreeSWITCH 选择让它们同处一个地址空间,代价换来的是极低的信息传递成本。
- 一利一弊:媒体能直接读到信令协商好的编解码、DTMF、通道变量,几乎零成本;代价是模块之间、信令与媒体之间没有隔离墙,一个野指针拖垮全进程。
- 时代的印记:
fsctl crash一发,整个进程连同上面所有通话一起 core dump——这就是"没有进程边界"最直白的证据。 - 为什么做不成高性能 SFU:每路参会者两个线程的模型,是为几百路语音打磨的,不是为几千路视频转发设计的。
mod_conference是混音器思路,不是转发器思路。 - 收尾:怎么判断一个老架构还值不值得用——先看它的状态边界,再看它的时代假设有没有过期。
前段时间我给 FreeSWITCH 修过一个 mod_dialplan_xml 的 use-after-free,为了搞清楚那个悬空指针从哪来,把 switch_core_session.c 和状态机那几个文件翻来覆去读了好几遍。读着读着冒出来一个更大的问题:这么一套东西——SIP 协议栈、媒体处理、拨号计划、会议、录音、脚本引擎——它凭什么敢全塞进一个进程里跑?
放到今天,但凡有人画一版这样的架构图交上去评审,多半会被问一句:"为什么不拆微服务?"可 FreeSWITCH 不但没拆,也不是我们通常说的那种"单进程多线程"——它是 switch_core 内核加一堆 dlopen 进来的模块,介于两者之间的一个物种。
这篇想把这个问题讲清楚:它的状态边界到底划在哪里,这种架构为什么快、又为什么脆,以及为什么这套结构做不成今天意义上的高性能 SFU。 一句话先撂这儿——它又快又脆,是同一个原因。
SFU(Selective Forwarding Unit,选择性转发单元):多方视频里负责把每个人的视频流"转发"给其他人的服务器,只转发、不解码不混合,是现在实时视频的主流方案。后面会专门讲为什么 FreeSWITCH 干不好这活。
状态边界划在哪:session 是原子,模块是插件
要理解一个系统的架构,先别看它有多少个模块,先看它的状态归谁管。状态边界一划清楚,性能和故障的来龙去脉就都顺了。
FreeSWITCH 的答案很干脆:状态归 session,一路呼叫一个 session,一个 session 一个线程一台状态机。
一路呼叫(call leg)进来,switch_core 建一个 switch_core_session_t。这个结构体里挂着一个 switch_channel_t(通道,装呼叫状态、主被叫、通道变量)和一个 switch_media_handle_t(媒体句柄):
struct switch_core_session {
switch_channel_t *channel;
switch_media_handle_t *media_handle;
/* ... */
};
session 一旦建好,switch_core_session_thread_launch() 就给它拉一个专属线程,线程里跑 switch_core_session_run(),也就是那台有限状态机。呼叫的一生,就是在这些状态之间流转:
CS_NEW → CS_INIT → CS_ROUTING → CS_EXECUTE → CS_EXCHANGE_MEDIA
→ CS_HANGUP → CS_REPORTING → CS_DESTROY
每到一个状态,状态机先调创建这个 session 的 endpoint 模块(比如 mod_sofia,SIP 那一层)自己实现的 state handler;模块处理完返回 SWITCH_STATUS_SUCCESS,再走通用的处理逻辑,把 channel 推到下一个状态。
这里就是状态边界:
- session/channel 是状态的原子单位。 一路呼叫的全部状态,都在它自己的结构体里、它自己的线程里。呼叫之间天然隔离——A 的线程崩了,理论上不该动到 B 的状态。
- 模块不是独立的服务,是这台状态机上的插件。
mod_sofia、mod_dptools、mod_conference这些,都是往状态机的某个钩子上挂回调。它们没有自己的进程、自己的地址空间,跑的是同一个switch_core、访问的是同一批全局结构。
那如果另一个线程想动某个 session 呢?比如你在 fs_cli 里敲个命令要挂断某路呼叫,发命令的是另一个线程。FreeSWITCH 只留了一道安全门:
switch_core_session_t *switch_core_session_locate(const char *uuid);
拿 UUID 去全局的 session_manager.session_table 哈希表里查,查到就给你一个加了读锁的指针。你可以安全地读它、改它、给它发消息,用完必须 switch_core_session_rwunlock() 解锁,而且——官方文档明说了——别锁超过几毫秒,否则你就把别的线程都堵在门外了。
看懂这道门,你就看懂了整个架构的取舍:跨 session 的一切访问,都要过这把锁。 锁的存在说明它们共享地址空间;锁要求"快进快出",说明这个共享是有代价、需要小心伺候的。
状态归 session,模块是插件,跨线程访问只有
session_locate这一道加锁的门。 记住这三句,下面所有的"快"和"脆",都是从这里长出来的。
为什么不拆微服务:信令和媒体,谁跟谁住一起
现在回到那个评审必问的问题:既然模块之间没有隔离,为什么不干脆拆成微服务,一个服务管 SIP,一个服务管媒体,一个服务管会议?
答案藏在电话系统一个最要命的特征里:信令和媒体,是一对必须频繁对话、又各自吃不同资源的冤家。
- 信令(SIP:谁打给谁、用什么编解码、什么时候挂)是 I/O 密集的,逻辑复杂但数据量小。
- 媒体(RTP:真正的语音视频包,一包接一包地流)里,转码、混音是 CPU 密集的,数据量大、对时延极其敏感。
它俩必须协同:媒体处理要知道信令协商出来的编解码是什么、要往哪个 IP 端口发、对方按了哪个 DTMF 按键、这路通道的音量该调多少。这些信息,信令那边天天在变。
现在做个选择题——信令和媒体,让它们跨进程,还是同处一个地址空间?
如果跨进程(也就是微服务那条路),每次媒体要用信令的状态,都得走一趟进程间通信:要么把状态在网络上序列化搬过去,要么两边共享一大坨状态再想办法保持一致。一路呼叫也许还好,几千路并发、每路每 20 毫秒一个媒体包,这个“搬运”的成本会把你压垮。信令一变、媒体那边要立刻感知,这中间的延迟和一致性问题,足够让人头疼一整年。
FreeSWITCH 的选择是:让它们住在同一个地址空间里。 媒体线程要看信令协商的结果?直接读 session->channel 上的变量就行,一次内存访问,没有序列化,没有网络往返,没有跨进程一致性。
这就是那“一利”:信令与媒体之间的信息传递成本,被压到了几乎为零。 一个 mod_conference 的输入线程,能直接从同一个 session 里读到刚解码好的媒体帧;一个媒体钩子(media bug)能就地插进媒体流里做录音、做检测,不用把音频数据拷来拷去、传来传去。对一个语音系统来说,这种“零成本对话”是它敢做那么多实时功能的底气。
凡事有一利就有一弊。既然选了同处一室,那就得接受同处一室的所有麻烦。
一利换一弊:没有隔离墙,一个野指针拖垮全场
同一个地址空间,意味着没有隔离墙。这个弊端有两个层次。
第一层,模块之间互相影响。 所有模块跑在同一个进程,共享同一个堆、同一批全局表(session_manager、全局哈希、事件系统)。mod_conference 里一个越界写,踩坏的可能是隔壁 mod_sofia 的数据。你在 C 里写模块,拿到的是整个进程的信任,而不是一个受限的沙箱。我修的那个 mod_dialplan_xml 的 use-after-free 就是个缩影——一个模块里的悬空指针,在 C 这种没有安全网的语言里,后果是整个进程层面的未定义行为,不是“这个模块返回个错误”这么客气。
第二层,信令与媒体互相影响。 它俩住一起,好处是对话便宜,坏处是互相干扰也便宜。媒体那边一段 CPU 密集的转码卡住了线程,信令的时序可能就跟着受影响;信令处理里一个阻塞调用没处理好,媒体的节拍(每 20ms 一帧那种精确节拍)就可能被打乱。你想给它们分别做资源隔离、分别限流、分别扩容——对不起,它们在一个进程里,资源是揉在一起的。
最直白的证据,是 FreeSWITCH 自己提供的一条调试命令:
fs_cli -x 'fsctl crash'
这条命令干嘛的?主动让 FreeSWITCH 崩溃,产生一个 core dump 用来调试。 注意,崩的是整个 freeswitch 进程,连同它上面当时所有的通话,一起没了。生产环境上如果你想抓个运行时快照,官方推荐 gcore $(pidof freeswitch),文档里紧跟一句提醒:“This will briefly disrupt call processing in production”——它会短暂中断生产环境的呼叫处理。
一个进程,一个 core,所有通话共担生死。这就是“没有进程边界”最不加修饰的样子。放到微服务的世界里,一个媒体节点挂了,信令还在,别的媒体节点还在,受影响的是局部;放到 FreeSWITCH 里,进程就是那道唯一的边界,边界之内,同生共死。
用一张表把这笔账算清楚:
| 维度 | switch_core + 模块(FreeSWITCH 的选择) | 拆成微服务 |
|---|---|---|
| 信令↔媒体信息传递 | 一次内存访问,几乎零成本 | 序列化 / 网络往返 / 一致性维护 |
| 模块间隔离 | 无,共享地址空间 | 进程 / 服务级隔离 |
| 单个模块崩溃的影响面 | 整个进程 + 所有通话 | 局部,可降级 |
| 独立扩容 / 限流 | 难,资源揉在一起 | 天然支持 |
| 部署与运维复杂度 | 低,一个二进制 | 高,一套编排 |
| 实时媒体功能的开发成本 | 低,就地读写媒体流 | 高,数据要跨服务流动 |
没有哪一列是纯粹的赢。FreeSWITCH 在它诞生的那个年代,为它要解决的问题,选了左边这一列。这个选择本身没错,错的是——拿着它去解一个它当年没打算解的问题。
时代的印记:为语音打磨的架构,扛不动视频转发
FreeSWITCH 2006 年就有了,快二十岁了。它早期一门心思做语音:PSTN 互通、IVR(交互式语音应答,就是“普通话请按 1”那套)、语音会议、录音、TTS/ASR。后来视频起来了,它也扩展去支持视频,但核心架构没有大改。历史悠久的系统,好处是稳、是久经考验,坏处是它骨子里刻着诞生那个年代的假设。而那些假设,是会过期的。
最典型的过期假设,就藏在它做不成高性能 SFU这件事上。
先把话说清楚:下面这几条是架构层面的推理,不是我拿它扛过几千路视频得出的实测数字——我没有那样的一手数据,不编。但从前面那套线程模型,能推出几条相当确定的结论。
第一,线程模型是为“路数不多的语音”设计的。 前面说了,一路呼叫一个线程一台状态机。到了会议(mod_conference),成本还要翻倍——ClueCon 上官方讲过,每个参会者要两个线程:一个 session 线程(管呼叫状态和媒体输出),一个会议输入线程(从这个 session 读媒体)。几十上百路语音,这个模型跑得很好;可 SFU 的典型场景是一个大房间里几百上千路视频流互相转发,你按“每人两线程”去铺,线程数、上下文切换、锁竞争会先于带宽把你顶到天花板。
第二,mod_conference 是“混音器”思路,不是“转发器”思路。 传统语音会议要做的是把大家的声音混成一路(mixing),这是 CPU 密集的活,也正是 FreeSWITCH 把信令媒体放一起、方便就地处理媒体的用武之地。可现代视频 SFU 的核心恰恰是不混、不转码,只做选择性转发——原样把 A 的视频包转给 B、C、D,把重活(解码、渲染)甩给客户端。一个从“混音”长出来的媒体架构,天生就没往“海量流的高效转发”这个方向优化过。
第三,视频 SFU 的性能瓶颈,和 FreeSWITCH 的优势不在一个维度。 FreeSWITCH 的强项是“信令和媒体贴得近、实时处理媒体便宜”;而 SFU 拼的是海量 UDP 包的高效转发、拥塞控制、丢包重传(NACK)、关键帧请求(PLI)、带宽估计这些视频转发专属的功课。这些不是靠“信令媒体住得近”就能白得的,得专门为视频转发去设计数据面。这也是为什么真要做大规模视频,业界普遍另起炉灶用 mediasoup、Janus、Jitsi 这类专职 SFU,而不是把 FreeSWITCH 硬掰成 SFU。
说到底,FreeSWITCH 不是“不好”,是“它为之优化的问题,和高性能视频转发不是同一个问题”。 拿一把为语音会议打磨了快二十年的锉刀,去干视频转发的活,不趁手是必然的——不是刀不好,是刀不对。
怎么判断一个老架构还值不值得用
把 FreeSWITCH 这一问琢磨透,其实能提炼出一套看任何老架构的方法,两步:
-
先找它的状态边界。 状态归谁管、原子单位是什么、跨单位访问要过什么门(锁?消息?网络?)。FreeSWITCH 是“状态归 session、跨 session 过一把读锁”,这一句就决定了它又快又脆。接手任何一个系统,先把这句话找出来,后面大半的性能和故障问题都能对上号。
-
再查它的时代假设有没有过期。 它诞生时要解的是什么问题、当时的硬件和场景什么样、这些假设今天还成立吗?FreeSWITCH 的假设是“路数不算多、以语音为主、实时处理媒体的成本要低”——在语音时代成立得漂亮,搬到“海量视频流转发”就集体失效。
一利一弊,从来不是玄学。把边界和假设这两样摆上台面,一个架构的利在哪、弊在哪、还适不适合你手上的活,基本就清楚了。
回到最开始那个评审现场——“为什么不拆微服务?”FreeSWITCH 的答案是:为了让信令和媒体的对话成本趋近于零,我甘愿放弃隔离。 这笔交易在语音时代划算,在视频时代不一定。它没有对错,只有合不合时宜。
架构说到这儿,下一篇接着扒那台状态机本身:switch_core_session_run() 里的状态流转,到底是怎么把一次呼叫的生老病死串起来的。
全文思维导图
@startmindmap
<style>
mindmapDiagram {
node {
BackgroundColor #F8F9FA
RoundCorner 10
Padding 10
FontSize 13
}
:depth(0) {
BackgroundColor #1E3A5F
FontColor white
FontSize 18
FontStyle bold
}
:depth(1) {
FontSize 15
FontStyle bold
}
:depth(2) {
FontSize 13
}
}
</style>
* FreeSWITCH 架构:\n快与脆
** 状态边界
*** session/channel 是原子单位
*** 一路呼叫一线程一状态机
*** 模块是插件不是服务
*** session_locate 是唯一的加锁门
** 为什么不拆微服务
*** 信令 I/O 密集
*** 媒体 CPU 密集且对时延敏感
*** 同地址空间 = 对话成本趋零
** 一利一弊
*** 利:信息传递几乎零成本
*** 弊:模块间无隔离
*** 弊:信令媒体互相干扰
*** fsctl crash 拖垮全进程
** 做不成高性能 SFU
*** 每人两线程的语音模型
*** mod_conference 是混音器
*** SFU 要的是选择性转发
*** 大视频另起炉灶
** 判断老架构两步
*** 先找状态边界
*** 再查时代假设是否过期
@endmindmap

本作品采用知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议进行许可。 欢迎在我的个人网站 https://www.fanyamin.com 访问原文并评论。