1. 为什么企业通信非得“Kamailio + FreeSWITCH”分开跑
大概两年多前,我接了一个客户的语音项目:3000 分机,高峰期同时通话将近 800 路,要求注册和呼叫都要在 1 秒内完成,预算只够买两台物理服务器。当时我的第一直觉是直接上 FreeSWITCH 集群,毕竟 FreeSWITCH 本身就能做注册、路由、媒体处理,看起来一台就够。结果压测第一轮就翻了车:呼叫接通率只有六成,SIP 消息大量超时,FreeSWITCH 的 CPU 倒是没满,但进程的调度和日志已经把整个盒子拖得半死。
后来我把所有对外信令收敛到一台 Kamailio,把媒体处理和业务逻辑留给后面的 FreeSWITCH,整个系统才真正稳下来。这篇文章就把这套架构的完整搭建过程、配置模板和压测心得整理出来,给准备做企业通信系统、呼叫中心或者高并发 SIP 网关的人一个可以直接抄作业的参考。
1.1 单台 FreeSWITCH 直接对外的问题
很多人觉得 FreeSWITCH 什么都能干,就直接把公网 IP 绑到 Sofia profile 上,让终端注册进来。小规模确实没事,但规模一上来问题就非常明显。
首先,FreeSWITCH 的 Sofia 模块是单实例运行在用户态进程里的,所有 SIP 信令、认证、定位、路由决策都要走它的消息队列。信令风暴来的时候,进程忙不过来,最先崩的不是 CPU,而是它的定时器和事务状态机。你会在日志里看到大量Cannot send 200 SIP response、Invalid state之类的东西,呼叫还没建立,对话已经乱套了。
其次,FreeSWITCH 作为媒体服务器,真正吃资源的是 RTP 转发、编解码、录音、回声消除这些媒体操作。如果它同时还要承担高并发的注册请求和路由查询,媒体能力和信令能力就会互相抢进程资源。等于让一个前台接待员同时去搬货,两头都干不好。
1.2 Kamailio 在架构里的角色定位
Kamailio 是一个专职的 SIP 路由器。它不做媒体,不处理音频,不玩录音,它的任务就是把 SIP 消息以最快的速度接收、判断、转发。因为这个定位,它对并发信令的处理能力远高于 FreeSWITCH 的 Sofia。
在整套架构里,Kamailio 是唯一对外暴露的节点。终端只认识 Kamailio,REGISTER 发给它,INVITE 发给它,一切信令都在它这里完成入口控制和分发。FreeSWITCH 则躲在 Kamailio 后面,通过 dispatcher 网关池接收转发过来的呼叫请求,只专注处理媒体和业务。
这样做还有一个实际好处:FreeSWITCH 的 IP、端口、分机表对外完全不可见。攻击者拿到的永远只是 Kamailio 这个信令入口,而 Kamailio 可以非常轻量地做限流、黑名单、冗余丢弃,防护成本比在 FreeSWITCH 上做低得多。
1.3 这套组合适合什么场景
如果你只是搭一个几十人用的内部 IPPBX,单台 FreeSWITCH 完全够用,没必要引入 Kamailio。但下面这些场景,我觉得用 Kamailio + FreeSWITCH 的拆分架构是必须的:
- 注册用户量在 2000 以上,需要把注册信令和媒体处理分开扩容。
- 有多台 FreeSWITCH 做媒体集群,需要在前端做统一负载均衡和故障转移。
- 有公网接入需求,终端分散在各类 NAT 网络里,需要媒体代理统一处理穿透。
- 业务上要求高可用,不希望任何一台媒体服务器宕机导致全区呼叫失败。
我见过不少团队跳过 Kamailio 直接上 FreeSWITCH 集群,然后用 SLB 或者 DNS 轮询做负载均衡,结果信令来回不一致、注册状态不同步、RTP 串路,问题比不集群还多。与其这样,不如从一开始就让 Kamailio 承担信令分发,这本来就是它最擅长的事。
2. 看懂一次通话的全过程:信令与媒体是怎么分头走的
在写配置之前,先把一次电话怎么打通的路径讲清楚。很多人配置出错,不是因为参数写错,而是脑子里没有完整的呼叫模型。
2.1 注册与呼叫信令路径
终端 A 发起 REGISTER,消息先到 Kamailio。Kamailio 打开 usrloc 模块,把 A 的 contact 地址、过期时间、NAT 探测结果存进位置表,然后回 200 OK。这一步 FreeSWITCH 完全不参与,所以哪怕有几千个终端同时注册刷新,压力都在 Kamailio 身上,FreeSWITCH 只管睡觉。
呼叫建立时,终端 A 发 INVITE 给 Kamailio。Kamailio 在内存里查被叫 B 的位置,如果 B 也是注册用户,就可以直接把 INVITE 转发到 B;如果 B 是坐席、外线或者需要业务逻辑处理(比如 IVR、排队、录音),Kamailio 就把 INVITE 转发给后端的 FreeSWITCH 网关池。
在有媒体代理的情况下,INVITE 里的 SDP 会在 Kamailio 这一层被改写:终端 A 看到的媒体地址变成 RTPEngine 或者 RTPProxy 的地址,后端的 FreeSWITCH 也一样。两边都不会直接接触到对方的真实 IP。
2.2 媒体路径与 RTPEngine 介入时机
很多人不理解为什么非要让 RTP 绕一圈走媒体代理,而不是让终端和 FreeSWITCH 直连。原因很简单:企业通信系统里绝大多数终端在 NAT 后面,它们的私有 IP 无法被 FreeSWITCH 直接访问。媒体代理就是那个“中间人”,它把两端流都拉到自己这里中转,格式、端口、IP 全部由它统一协调。
RTPEngine 介入的时机在 Kamailio 里是通过rtpengine_manage()控制的。它会在 INVITE 转发前处理 offer SDP,在收到 200 OK 或 183 时处理 answer SDP。这样做的目的是让媒体端口在呼叫建立前就完成预分配,避免一听到回铃音才发现媒体不通。
2.3 Early Media 场景下的特殊处理
这里必须单独提 early media,因为企业通信里大量场景依赖它:被叫振铃时播放回铃音、IVR 放音、彩铃、坐席排队提示音,这些都是在被叫真正接听之前就要有媒体流的。
如果只是靠 Kamailio 在收到 180 Ringing 后本地生成一个回铃音,那很多业务场景是没法做的。正确做法是让 FreeSWITCH 出 183 Session Progress 并且带上 SDP,媒体代理在两方向建立媒体通道,用户才能听到真实的服务端媒体。
这就是为什么 FreeSWITCH 的 external profile 里要开p-early-media-support。这个参数开启后,Sofia 允许在早期对话阶段就发送带 SDP 的 183,而不是非得等到 200 OK。配合 Kamailio 的rtpengine_manage(),early media 的 RTP 才能正常中转。我见过有人在配置里漏了这个参数,结果所有 IVR 场景都是“接通后才出声”,用户打电话感觉像断线了一样。后面单独一节讲 FreeSWITCH 配置时我会给出具体模板。
3. 实施前的环境规划:网络、端口、服务器选型
配置模板只是最后落地的东西,前面如果规划不对,后面全是坑。我这里把环境准备阶段最容易出问题的几个点单独拎出来讲。
3.1 服务器与操作系统选择
Kamailio 对硬件要求很低,信令处理是 CPU 密集但内存占用很小的活。一台 4 核 8G 的机器,处理几千注册加几百并发呼叫信令完全没问题。FreeSWITCH 才是吃配置的主力,媒体转发、编解码、录音都靠它,建议至少 8 核 16G 起步,并按并发路数按比例加。
操作系统我只建议 Linux,发行版选 Debian 或者 Ubuntu 都行,CentOS 7 已经过了维护期,别再用老古董了。Windows 上虽然能装 FreeSWITCH,但性能和模块支持都差不少,后面我会专门说这个坑。生产环境一定要用 64 位系统,这个不需要解释。
我自己的习惯是:Kamailio 用独立的轻量实例,和 FreeSWITCH 分开部署。两台服务器之间走内网专线,延迟越低越好。如果预算允许,FR 后面再接一套备用的 FreeSWITCH,靠 Kamailio 的 dispatcher 探活自动切换。
3.2 网络端口清单与防火墙策略
端口规划是新手最容易翻车的环节。很多人只开了 5060 UDP,结果呼叫能通,但一放音就是单向音频。社区里天天有人问这种问题,十有八九是 RTP 端口段没放通。
下面是我常用的端口规划表,你按这个来基本不会漏:
| 服务 | 协议 | 端口 | 用途 |
|---|---|---|---|
| Kamailio | UDP/TCP | 5060 | SIP 信令入口 |
| RTPEngine | UDP | 2223 | 与 Kamailio 通信的控制端口 |
| RTPEngine | UDP | 10000-20000 | 媒体流端口段 |
| FreeSWITCH internal | UDP/TCP | 5060 | 内网 SIP 信令,从 Kamailio 访问 |
| FreeSWITCH external | UDP/TCP | 5080 | 对接 Kamailio 的信令端口 |
| FreeSWITCH RTP | UDP | 16384-32768 | 媒体端口段 |
注意,FreeSWITCH 的 internal profile 和 Kamailio 不要共用同一个 5060 端口,否则在同一台机器上会冲突。我习惯把 internal 留在 5060,external 改成 5080,Kamailio 只对接 external。
防火墙上的安全组策略最基本原则是:外部到 Kamailio 只放 5060,外部永远不应该能直接摸到 FreeSWITCH 的端口。云服务器上更要注意,安全组规则别图省事放行 0.0.0.0/0 的全部 UDP。
3.3 上云部署的核心注意点
现在大多数企业把通信系统部署在云上,我用阿里云 ECS 比较多。上云有一个和物理机完全不同的点:云服务器的公网 IP 和私网 IP 是分离的,网卡上绑的是私网 IP,公网 IP 靠 NAT 映射进来。
这意味着 Kamailio 和 FreeSWITCH 都必须显式配置“对外公告的地址”。Kamailio 在listen和alias里要写清楚,FreeSWITCH 那边就是external_sip_ip和external_rtp_ip这两个变量。我记得有次帮人排查,对方用的腾讯云,external_rtp_ip忘了写,结果注册全好,打电话全部单向音频,就是这个原因。
另外,云的带宽和连接数限制要提前确认。RTP 媒体流非常吃带宽,一路 G.711 通话大约 80-90kbps,800 路并发就是 70Mbps 左右,加上信令和开销,至少准备 100Mbps 的出口带宽。别只看 CPU 和内存,带宽被占满的时候,用户感知到的就是“电话能打通但声音断断续续”。
4. Kamailio 配置模板逐段解析
下面这套配置是我在生产环境里实际跑过的精简版,去掉了加密和数据库持久化这些外围内容,核心逻辑全部保留。你自己部署时,直接复制改 IP 就能用。
4.1 全局参数与模块加载
创建/etc/kamailio/kamailio.cfg,第一段是全局参数和模块。
#!KAMAILIO debug=2 log_stderror=no memdbg=5 memlog=5 listen=udp:0.0.0.0:5060 listen=tcp:0.0.0.0:5060 alias=pbcore.example.com # ----------- 模块加载 ----------- loadmodule "tm.so" loadmodule "sl.so" loadmodule "rr.so" loadmodule "pv.so" loadmodule "maxfwd.so" loadmodule "textops.so" loadmodule "siputils.so" loadmodule "xlog.so" loadmodule "usrloc.so" loadmodule "registrar.so" loadmodule "dispatcher.so" loadmodule "rtpengine.so" # ----------- 模块参数 ----------- modparam("rr", "enable_double_rr", 1) modparam("rr", "append_fromtag", 1) modparam("usrloc", "db_mode", 0) modparam("dispatcher", "list_file", "/etc/kamailio/dispatcher.list") modparam("dispatcher", "ds_ping_method", "OPTIONS") modparam("dispatcher", "ds_ping_interval", 30) modparam("dispatcher", "ds_probing_threshold", 2) modparam("rtpengine", "rtpengine_sock", "udp:127.0.0.1:2223")db_mode=0表示注册信息只存内存,不上数据库。这个选择在中小规模下是对的,因为 usrloc 查内存比查数据库快一个数量级,而且 Kamailio 本身是单点,重启后终端会自动重新注册,数据库持久化意义不大。如果后面做多台 Kamailio 负载均衡,再考虑加 Redis 或者 MySQL 做位置共享。
4.2 路由逻辑:REGISTER、INVITE、RELAY 三段路由
接下来是核心路由。Kamailio 的配置文件从上往下就是一个大路由,我用route[]拆成功能区,方便维护。
request_route { if (!mf_process_maxfwd_header("10")) { sl_send_reply("483", "Too Many Hops"); exit; } if (has_totag()) { route(RELAY); exit; } if (is_method("REGISTER")) { route(REGISTER); exit; } if (is_method("INVITE")) { route(INVITE); exit; } route(RELAY); } route[REGISTER] { if (nat_uac_test("19")) { force_rport(); fix_nated_contact(); setbflag(1); } if (!save("location")) { sl_reply_error(); exit; } exit; } route[INVITE] { set_dlg_flag(4); if (has_body("application/sdp")) { rtpengine_manage(); } if (!ds_select_dst("1", "4")) { sl_send_reply("503", "No available destination"); exit; } route(RELAY); } route[RELAY] { if (is_method("INVITE")) { rtpengine_manage(); } if (!t_relay()) { sl_reply_error(); exit; } exit; }这里有一个细节:nat_uac_test("19")的值是 1、2、4、8、16 这些标志位的组合,19 表示同时检测 Via 里有没有 RFC 1918 私网地址、Contact 是不是私网、收到的来源端口和 Via 端口是否一致、还有 rport 是否存在。只要命中其中一个,就按 NAT 终端处理,改 Contact、强制 rport。
set_dlg_flag(4)是启用 dialog 模块的追踪标志,rtpengine_manage()在转发前对 SDP 做改写。注意我在这里调用set_dlg_flag(4),但配置里其实还需要加载dialog.so,否则这个标志是不生效的。我这版模板为了展示核心逻辑省略掉了,你生产用的时候务必把dialog.so加上。
4.3 负载均衡:dispatcher 网关池的配置
dispatcher 是 Kamailio 做 FreeSWITCH 负载均衡的核心。新建/etc/kamailio/dispatcher.list:
# 组ID 目的地 优先级 权重 1 sip:10.0.0.11:5080 0 0 1 sip:10.0.0.12:5080 0 0ds_select_dst("1", "4")的第一个参数就是组 ID,第二个参数是选择算法。4 表示哈希,同一主叫的后续消息会尽量打到同一个 FreeSWITCH,保持对话亲和性。中小规模我用 4,规模再大可以改成 8(权重轮询)配合自定义参数。
Kamailio 会每隔 30 秒给这两个网关发 OPTIONS 探活包,连续探测失败两次后自动把故障节点摘掉,恢复后自动加回。这个探活机制是生产环境的高可用基础,千万不要为了省资源把它关了。
5. FreeSWITCH 侧对接配置:让 Sofia 乖乖听命
Kamailio 配置好只是第一步,FreeSWITCH 这边对接不对,前面全白搭。
5.1 external profile 与 ACL 收紧
FreeSWITCH 安装后默认的 external profile 是对公网开放的,这很危险。生产上我强烈建议把 external 网关只对 Kamailio 的 IP 开放,所有终端注册一律走 Kamailio,不直接打 FreeSWITCH。
在sip_profiles/external.xml里做三件事:改监听端口、关掉匿名呼叫、开 ACL。
<profile name="external"> <settings> <param name="sip-ip" value="$${external_sip_ip}"/> <param name="sip-port" value="5080"/> <param name="context" value="public"/> <param name="auth-calls" value="true"/> <param name="apply-nat-acl" value="kamailio.auto"/> <param name="aggressive-nat-detection" value="true"/> <param name="inbound-codec-prefs" value="PCMU,PCMA,G722,opus"/> <param name="outbound-codec-prefs" value="PCMU,PCMA,G722,opus"/> <param name="p-early-media-support" value="true"/> <param name="rtp-ip" value="$${external_rtp_ip}"/> <param name="rtp-timeout-sec" value="300"/> <param name="rtp-hold-timeout-sec" value="1800"/> <param name="disable-hold" value="false"/> </settings> </profile>对应在autoload_configs/acl.conf.xml里定义kamailio.auto这个 ACL:
<list name="kamailio.auto" default="deny"> <node type="allow" cidr="10.0.0.0/8"/> <node type="allow" cidr="192.168.1.0/24"/> </list>apply-nat-acl的意思是:只有匹配这个 ACL 的请求才做 NAT 穿透处理。Kamailio 来的请求都是内网地址,命中 ACL,所以 FreeSWITCH 拿到 Kamailio 改写后的 SDP 就能正确处理。外部直接打到 5080 的请求因为不在 ACL 里,会被直接拒绝。
auth-calls=true加上 ACL 默认 deny,这一步直接堵死了匿名的外部 INVITE 扫描,比在防火墙上过滤更可靠。
5.2 p-early-media-support 到底是什么
前面提到 early media,这里把p-early-media-support讲透。Sofia 的早期媒体有两个阶段:一个是 183 Session Progress 带 SDP,一个是 180 Ringing 之后才补 SDP。p-early-media-support控制的就是 183 阶段是否允许携带 SDP。
这个参数在默认模板里是false,很多人没在意。但在 Kamailio + RTPEngine 架构里,如果 FreeSWITCH 不回带 SDP 的 183,Kamailio 的rtpengine_manage()就收不到早期媒体的 answer SDP,媒体代理也就不会在早期阶段建立 RTP 通道。结果是用户打电话能听到回铃音(那是终端本地生成的),但 IVR 放音、排队提示音这类服务端早期媒体全部没声。
我排查过的早期媒体问题里,大约一半是这个参数没开。还有一半是 Kamailio 在转发 183 时把 SDP 丢掉了。日志里如果看到 183 没有 SDP,优先查这两处。
5.3 Park 与 Hold 的落地实现
企业通信里 Park(呼叫暂留)和 Hold(呼叫保持)是高频功能。FreeSWITCH 内置了这两个能力,但很多人不知道 dialplan 里怎么写。
Park 的核心是把这通电话放到一个“停车场”,其他话机可以拨指定号码把电话接走。在拨号计划里加一个简单的 park 分机:
<extension name="park"> <condition field="destination_number" expression="^(\d{2})$"> <action application="answer"/> <action application="park"/> </condition> </extension>Hold 则是在通话过程中由话机侧的 re-INVITE 触发,FreeSWITCH 收到a=sendonly的 SDP 后自动进入保持状态。如果你需要在拨号计划里强制把某路通话置为保持,可以用hold应用,配合hold_music指定保持音乐:
<action application="set" data="hold_music=local_stream://moh"/> <action application="hold"/>这里有个实际经验:保持状态下的 RTP 并不是断掉的,而是流方向变为单向,媒体代理必须感知到这个变化。RTPEngine 会在 re-INVITE 的 SDP 里看到sendonly,从而调整媒体转发方向。如果这时候媒体代理状态不同步,就会出现“对方听不到我,我能听到对方”这种怪问题。所以 Kamailio 侧一定要保证 re-INVITE 也走route(RELAY),不要在 has_totag 分支里把带 SDP 的 re-INVITE 直接裸转发。我这个模板里has_totag()直接进route(RELAY),就是为这个准备的。
6. 媒体代理选型:RTPEngine 还是 RTPProxy
媒体代理是整个系统中并发能力最容易成为瓶颈的一层。选型选错了,后面改起来是最痛苦的。
6.1 两者的核心差异
RTPProxy 是老牌方案,代码简单、稳定、部署容易,很多老项目都在用。RTPEngine 是它的后继者,功能上强很多:原生支持 ICE、SRTP 加密透传、T.38 传真、DTLS,还支持多端口复用。在高并发场景下,RTPEngine 的内核转发性能也更好,因为它的数据面支持多线程。
从趋势上讲,新项目没有理由再选 RTPProxy。RTPProxy 对 ICE 的支持基本靠补丁,而且多年没有大的更新;RTPEngine 还在持续演进,Kamailio 的rtpengine模块也比老的rtpproxy模块维护得更积极。
6.2 安装与对接步骤
以 Debian/Ubuntu 为例,RTPEngine 可以直接用官方仓库安装,也可以用源码编译。生产上我建议直接装发行版自带包,省去编译依赖的麻烦:
apt install rtpengine配置文件在/etc/rtpengine/rtpengine.conf,最简配置如下:
[rtpengine] table = 0 interface = 10.0.0.10 listen-ng = 127.0.0.1:2223 timeout = 60 silent-timeout = 600 tos = 184 port-min = 10000 port-max = 20000listen-ng就是 Kamailio 里rtpengine_sock对应的那个地址,两边必须一致。port-min和port-max是媒体端口段,要和防火墙放行的范围一致。
启动后验证一下:
systemctl start rtpengine kamcmd rtpengine.show all能看到NODE: 127.0.0.1:2223并且状态 UP 就说明对接成功了。
6.3 媒体端口预留与 NAT 穿透
RTPEngine 的端口段要预留充分。一路通话至少两个端口(双向 RTP 实际是一个五元组,但考虑到 RTCP,通常按每路 2 个端口估)。10000-20000 这个范围是 10000 个端口,理论承载 5000 路并发,中小规模够用了。
部署在云上的时候,interface参数填私网 IP,listen-ng填本机回环地址,注意别把回环地址填到 interface 上去。Kamailio 和 RTPEngine 在同一台机器时用回环控制通道最安全,RTP 媒体端口段单独在安全组里对终端所在的 CIDR 放行。如果你要对公网开放媒体端口,那就要在 RTPEngine 配置里把公网 IP 也加进 interface,不然 NAT 响应包会从错误的地址发出去,媒体直接断。
7. 高并发调优三板斧:内核、Kamailio、FreeSWITCH
前面配置都正确,系统能跑,但离“高并发”还差很远。同样的架构,调优前后并发上限可能差一倍。这一节讲我最常用的三个层面的调优。
7.1 内核参数:一开始就要调对
很多运维装完系统什么都不管,UDP 相关的内核参数全是默认值。高并发 SIP 系统一定要改这几项:
# /etc/sysctl.conf net.core.somaxconn = 65535 net.core.rmem_max = 16777216 net.core.wmem_max = 16777216 net.ipv4.ip_local_port_range = 1024 65535 net.ipv4.tcp_max_syn_backlog = 8192 net.netfilter.nf_conntrack_max = 1048576 net.netfilter.nf_conntrack_udp_timeout = 60nf_conntrack_max这行尤其重要。Linux 的 conntrack 模块会跟踪每一个经过的 UDP 流,默认表大小往往只有几万,高并发 SIP 环境下几分钟就打满。表满了之后新包直接丢,现象就是“呼叫随机失败,重启后立刻恢复正常,过一会又不行”。这个坑特别隐蔽,排查了好久才发现是 conntrack 满了。
7.2 Kamailio 侧的并发参数
Kamailio 默认使用单进程事件循环,你需要在启动参数里指定子进程数。多核机器上,children数量通常设置为核心数,TCP 进程数单独设:
kamailio -D -P /var/run/kamailio.pid -DD -n 8-n 8就是 8 个 UDP 子进程。不是越大越好,子进程之间的锁竞争在高并发下会抵消多进程优势,8 到 16 之间基本够用。
Kamailio 内部还有一个重要参数在kamailio.cfg里调:
modparam("tm", "fr_timeout", 10) modparam("tm", "fr_inv_timeout", 30)这是事务超时时间。生产环境默认值有时候偏长,导致一个下游 FreeSWITCH 卡住时,所有转发给它的请求都在内存里挂着。把超时压到合理范围,配合 dispatcher 的探活,故障转移速度会明显提升。
7.3 FreeSWITCH 侧的会话与定时器
FreeSWITCH 的并发调优主要在autoload_configs/switch.conf.xml:
<param name="max-sessions" value="2000"/> <param name="sessions-per-second" value="100"/> <param name="max-database-handles" value="500"/>sessions-per-second决定每秒钟最多接受多少新呼叫。这个值设太高会导致瞬时 CPU 峰值,我把 100 作为默认起点,实际压测后按曲线再调。真到了 100 都扛不住的时候,说明 FreeSWITCH 进程本身已经到极限了,该加机器而不是硬调。
Sofia profile 里别忘了关掉不必要的日志:
<param name="sip-trace" value="no"/> <param name="silent" value="true"/>很多团队上线时开着 sip-trace 忘了关,每个 SIP 消息的完整报文都被打进日志,高并发下磁盘 I/O 直接成为瓶颈。我在压测时见过日志 I/O 占掉整个系统 30% CPU 的情况,关掉后并发上限立刻上去了。
7.4 引入 Redis 解决分布式状态问题
单台 Kamailio + 单台 FreeSWITCH 不需要 Redis,但一旦 Kamailio 也要做集群,或者 FreeSWITCH 多台横向扩展,注册位置和对话状态的共享就成了问题。
Kamailio 可以借助db_redis模块把 usrloc 的位置表放到 Redis 里,多个 Kamailio 实例共享同一份注册数据,实现信令入口的横向扩展。FreeSWITCH 侧也可以用mod_redis做缓存查询,比如黑名单、话单补全这类高频读操作,避免每次请求都查数据库。
Redis 本身也要按高并发设计:开持久化(RDB + AOF),关闭save太频繁的策略,网络层和 Kamailio 走内网,别把 Redis 暴露到公网。缓存键的设计上,我习惯用业务前缀加主键的方式,比如usrloc:1001、blacklist:2001,这样后续按前缀做批量清理非常方便。
8. 压测与容量评估:别等上线了才暴露问题
配置完成了,不代表系统真的能扛住高并发。我见过太多项目在验收前才匆匆做压测,结果一堆问题暴露,上线日期一拖再拖。压测要前置,从搭建完成就开始。
8.1 SIPp 基础压测脚本思路
SIPp 是 SIP 压测的事实标准,命令行就能模拟大量注册和呼叫。基础压测场景可以这样写,这是注册压测:
sipp -sf register.xml -m 5000 -l 500 -r 50 -i 10.0.0.10 -p 5060 10.0.0.20:5060含义是:用register.xml场景,总共发起 5000 次注册,最大并发 500,每秒新增 50 个。-m是总请求数,-l是最大并发数,-r是每秒速率。这三个参数基本就是压测的“三板斧”,调它们就能控制压力曲线。
register.xml里核心就是发 REGISTER、收 200、发 BYE。压测的时候盯着 Kamailio 侧的kamcmd tm.stats和kamcmd corex.shm,如果事务堆积数持续上升,说明信令处理已经跟不上了。
8.2 JMeter 与真实业务混合压测
SIPp 适合单一场景压测,但真实业务是混合的:注册、呼叫、保持、转接、挂断交错发生。我们这边压测人员用的是 JMeter 配合 SIP 采样插件,可以编写包含多个业务流程的测试计划,更适合验证整个系统的端到端承载。
JMeter 的 SIP 插件支持 UAC 和 UAS 两种模式,可以在测试计划里用 CSV 数据文件驱动几百个不同主叫号码并发呼叫。压测时要注意 JMeter 本身也会成为瓶颈,一台压力机生成不了足够多的 SIP 消息,需要多台压力机分布式执行,不然测出来的瓶颈在压力机而不是被测系统。
8.3 关键指标与性能瓶颈判断
压测看完三个维度基本能判断瓶颈在哪:
第一是注册成功率。5000 个并发 REGISTER,成功率要到 99.9% 以上。低于这个数,先查 Kamailio 子进程数和 conntrack 表。
第二是呼叫建立时延。从 INVITE 发出到 200 OK 收到,正常内网环境应该在 100-300ms 之间。超过 1 秒,说明媒体代理或 FreeSWITCH 的处理已经积压。
第三是媒体质量。压测时用 RTP 质量统计,重点看丢包率和抖动。丢包超过 1%,用户就会明显感觉声音卡顿,这时候瓶颈大概率在带宽或 RTPEngine 的端口处理能力。
我自己习惯压测前先记录基线:CPU、内存、网络、进程数,压测中每 30 秒抓一次快照,压测后再对比。这样定位问题快得多,不然一屋子告警,根本不知道从哪看起。
9. 回看这些坑:我在真实环境里踩过的雷
最后分享几个我不太想在文档里看到、但实际踩过的坑。每一个都是线上真实教训。
9.1 Windows 上装 FreeSWITCH 只能用来学习
Windows 版 FreeSWITCH 官方一直在出,但性能表现和模块兼容性跟 Linux 差不少。我见过有人图省事在 Windows 服务器上跑生产,高峰期一到,媒体处理延迟暴涨,用户普遍反映声音“拖泥带水”。
如果你只是想在本地快速验证一下 FreeSWITCH 的功能、跑通拨号计划,Windows 版没问题,装好就能用,对学习很有帮助。但生产系统,尤其是要高并发的场景,老老实实上 Linux。这不是 Windows 不行,是 FreeSWITCH 的主体优化都集中在 Linux 生态上,没必要跟这个较劲。
9.2 SIP ALG 与 conntrack 的相爱相杀
企业网络里的路由器很多默认开启 SIP ALG,它会把 SIP 消息里的 IP 和端口自作主张地改写。但 ALG 的识别逻辑非常粗糙,遇到 Kamailio 已经改写过的 SDP 再改一遍,媒体地址就彻底乱了。症状是注册正常、呼叫正常,但媒体永远是单通或者双不通。
另一个和它形影不离的是 conntrack 超时问题。SIP 会话中间有长时间静默(比如保持状态),conntrack 的 UDP 会话超时后会被清掉,媒体代理转发的包就穿不过防火墙了。我把 UDP 超时调到 60 秒不是随便写的,是配合 RTPEngine 的心跳机制,确保媒体会话在 conntrack 表里始终活着。
9.3 100rel 与 Early Media 组合拳
SIP 的 100rel(可靠临时响应)机制在对接运营商线路时经常出现。如果终端发来带100rel的 INVITE,而 FreeSWITCH 侧不支持 PRACK,早期媒体就会异常,甚至出现“只听一遍提示音就断”的怪现象。
处理方式是在 FreeSWITCH 的 external profile 里显式支持:
<param name="enable-100rel" value="true"/>同时 Kamailio 侧不要做任何剥离100rel标志的操作。很多教程让你在转发前删掉 Require: 100rel 来“规避”问题,这治标不治本,遇到严格要求 100rel 的运营商线路就彻底断了。正确做法是两端都支持,让 PRACK 流程完整走完。
9.4 日志不是你想打就能打
开发阶段为了调试方便,我习惯在 Kamailio 的路由里加一堆xlog,FreeSWITCH 开着 debug 级别的日志。上线前必须清掉。压测时一台机器如果 xlog 打印每个消息,光写日志就能吃掉一个核。
我的做法是分级打日志:路由入口打一条 L_INFO 记录主叫被叫,异常分支打 L_WARN,正常转发不打。FreeSWITCH 侧把loglevel调到notice,保留alert和crit的审计需求。日志量立刻下降一个量级,问题排查反而更清晰,因为噪音少了,真正的异常一眼就能看到。
这套架构我前前后后在不同项目里落地过好几遍,踩过坑,也总结出了自己的习惯。每次新项目部署,我都是先把 Kamailio 的信令入口立起来,再把 FreeSWITCH 的媒体层挂上去,最后压测验证容量。按照这个顺序走,系统出问题的概率会小很多。配置模板都在上面了,改改 IP 就能跑起来,剩下的就是在你真实的业务场景里反复打磨。