有没有遇到过这种情况:领导丢过来一句话:“三天内搞一个语音机器人出来,用户打电话进来能正常对话,能查订单、能办理挂失。”你的第一反应是去啃ASR识别、NLP对话、TTS合成这一整套AI链路,但实际上你手头早就有资源了:一个开源的FreeSWITCH,一个阿里云SDM(MRCP-SERVER)接口。把这两者接起来,语音机器人的骨架当天就能跑通,剩下的事情只是把对话逻辑往里面填而已。
本文就是记录这条路怎么走通。我会从FreeSWITCH和MRCP-SERVER各自的分工讲起,然后重点拆解Docker化部署FreeSWITCH的细节,再一步步把阿里云SDM接入配置、拨号计划、P-Early-Media支持这些关键节点全部过一遍。最后是我自己在容器环境下踩过的四个真实坑,排查思路和解决办法一并奉上。不管你是刚接触FreeSWITCH的新手,还是已经在生产环境维护过一阵子的老手,这篇文章的实操密度应该都能帮上忙。
1. 语音机器人的电话接入层:FreeSWITCH与MRCP-SERVER各自扮演什么角色
1.1 从“总机”和“坐席大脑”理解FreeSWITCH+MRCP的组合
很多人第一次接触语音机器人,容易被一堆协议缩写搞晕:SIP、RTP、MRCP、ASR、TTS、NLP……但剥开来看,整个系统其实只有两类角色。
第一类是电话接入层。用户从手机或座机拨进来的电话,本质是一路SIP呼叫,媒体流是RTP音频。这个层面要解决的是:呼叫怎么进来、音频怎么传输、怎么播放录音、怎么转人工、怎么挂断。这就是FreeSWITCH干的活。它像一个总机接线员,把所有电话线揽在自己手里。
第二类是AI能力层。用户说了一句“我想查余额”,这句话要被识别成文字(ASR),然后理解意图(NLP),再把“您的余额是五百元”这句话合成音频播出去(TTS)。这个层面就是MRCP-SERVER提供的。它像一个坐席大脑,负责听懂和回答。
MRCP(Media Resource Control Protocol)就是连接这两层的协议。FreeSWITCH通过mod_unimrcp模块把RTP音频流送到MRCP-SERVER,MRCP-SERVER处理完再把结果或合成音频送回来。整个对话过程中,FreeSWITCH不关心AI模型怎么跑,它只需要按照MRCP协议把音频送过去、把结果拿回来。
用生活类比就是:你打电话给银行,总机把你的话音接给后台坐席,坐席帮你查业务,再把答案传回给总机,由总机播给你听。FreeSWITCH是总机,MRCP-SERVER是坐席,MRCP协议就是总机和坐席之间的内部电话线路。
1.2 阿里云SDM(MRCP-SERVER)到底提供了什么能力
阿里云SDM全称很长,但从使用角度只看一件事:它给你开好了一个现成的MRCP服务器,你只需要知道IP、端口、鉴权信息,就能用MRCP协议调用ASR和TTS能力。
如果不用这种云端MRCP服务,你会发现面前摆着两条非常折腾的路。
一条是自己部署开源ASR/TTS引擎。Kaldi、Mozilla DeepSpeech、PaddleSpeech、espeak这些我都碰过,光是声学模型、语言模型、发音字典、音频特征提取这些概念就够喝一壶的。更要命的是打电话进来的音频是8kHz采样率、G.711编码的窄带语音,很多模型在窄带数据集上的表现会明显下滑,识别率惨不忍睹。
另一条是直接调用云厂商的API。好处是识别率高,问题是API通常是HTTP接口,不是MRCP协议。你需要自己写桥接服务:一边接FreeSWITCH的媒体,一边调云API,还要处理异步回调、音频格式转换、并发会话管理。相当于产品还没上线,先造了一个中间件,后续还得不停维护。
阿里云SDM把这条路径缩短了,它对外开放MRCP协议接口,FreeSWITCH端只要配置好unimrcp就能直接连过去,ASR、TTS能力相当于“内嵌”进了通话链路,不需要你自己写一行桥接代码。
1.3 动手前需要准备的清单
在开始之前,如果你不想中途卡壳,先花几分钟把下面这些准备好:
- 一台能运行Docker的Linux服务器,内存建议不低于2GB,4GB更稳。如果是个人测试,Windows 10/11 + Docker Desktop也可以,但后面会有专门的坑要处理。
- 阿里云账号,并开通智能语音交互相关服务,进入控制台找到MRCP-SERVER接入信息,包括接入地址、端口、账号、密钥,以及分配给语音机器人的话术模板/音色等其他配置。不同地域、不同产品版本控制台界面可能不太一样,以你实际看到的为准。
- 版本信息:Docker镜像我建议直接用社区维护的signalwire/freeswitch或者safarov/freeswitch镜像,不要自己从头编译。自己编译FreeSWITCH三件套(FreeSWITCH本体、mod_unimrcp、依赖库)的时长足够出去吃顿饭了。
- 一个SIP软电话,比如MicroSIP、Zoiper、Linphone,用于实际呼叫测试。没有软电话用手机装了SIP客户端也行,目的就是真实地“拨一通电话”来验证链路。
- 一个支持SIP标准的命令行工具,比如sngrep、tcpdump,这些在排错阶段非常有用。
准备工作的核心逻辑很简单:提前把可变因素收敛,后面出问题时才有清晰的排查边界。实际配置MRCP接入时,最省心的做法是先在阿里云控制台用官方工具跑通一句话识别,确认账号权限和网络连通性没问题,再往下走。否则后面FreeSWITCH连不上的时候,你都不知道该查网络还是查鉴权。
2. Docker化部署FreeSWITCH:从镜像拉取到一次启动成功
2.1 为什么我强烈建议用Docker跑FreeSWITCH
我最早一次部署FreeSWITCH是老老实实按官方文档编译安装的。下载源码、解决依赖冲突、编译mod_unimrcp、配置mod_sofia,再把RTP端口范围放通,一套下来折腾了大半天。后来同一台机器上又因为库版本更新,导致某个模块加载崩溃,修复成本比重新部署还高。
Docker版本把这堆问题全隔离掉了。镜像是别人已经编译好的、验证过的运行环境,拉下来直接跑。升级和回退都简单:换一个镜像tag再启动就是了。更重要的是,临时测试环境可以随时销毁重建,完全不污染宿主机。
如果你正在开发一个语音机器人的POC(概念验证),用Docker部署FreeSWITCH能让你把有限的精力集中在语音链路上,而不是耗费在环境搭建上。这也是标题里“5分钟搞定”能成立的根本原因。
2.2 端口、目录、网络:容器启动命令逐项拆解
先给一份完整的Docker运行命令作为基准:
docker run -d \ --name freeswitch \ --network host \ --restart always \ -v /data/freeswitch/conf:/etc/freeswitch \ -v /data/freeswitch/recordings:/var/lib/freeswitch/recordings \ -v /data/freeswitch/log:/var/log/freeswitch \ -v /etc/localtime:/etc/localtime:ro \ safarov/freeswitch:latest逐个拆解这条命令里的关键点,你会发现每个选择背后都是有原因的。
网络模式用host而不是bridge映射端口。这是FreeSWITCH容器化里最容易被忽略的决策。FreeSWITCH的RTP媒体端口是一个很大的动态范围(默认16384-32768),如果你用bridge模式,就要在docker run里手动映射几百个端口-p 16384-32768:16384-32768/udp,既笨拙又容易撞上Docker的端口限制。改成host模式后,容器直接复用宿主机网络,SIP的5060端口和RTP的UDP范围天然就通,完全不需要端口映射的噩梦。这也是我踩过一次错之后,后续所有FreeSWITCH容器都默认host模式的原因。
为什么挂载conf目录。FreeSWITCH的配置目录包含全部xml配置:拨号计划、SIP profile、unimrcp配置、vars定义。不挂载的话,容器一删配置就全没了。挂载之后,你可以直接在宿主机上改配置,再进容器执行fs_cli -x "reloadxml"热加载,非常方便。
为什么挂载recordings和log。一个是保存通话录音,一个方便排查问题。这两个目录不挂载虽然在容器刚启动时不会报错,但你一旦开始调IVR流程,需要翻日志、听录音时,就会后悔没挂载。挂载后宿主机的日志和录音就是持久化的,排查效率高一个量级。
为什么挂载localtime。容器默认时区是UTC,日志时间比北京时间慢8小时。你半夜排查通话问题时,日志时间对不上会很痛苦。挂载宿主机的localtime文件后,容器内时间戳与本地一致。这个操作只需要一行,但能省掉无数脑细胞。
2.3 启动后必须做的三个健康检查
容器起来后别急着配MRCP,先确认FreeSWITCH本身是健康的。
第一个检查:模块加载情况。进入容器执行:
docker exec -it freeswitch fs_cli -x "module_exists mod_unimrcp"如果返回true,说明unimrcp模块已经在镜像中编译并加载。注意,不是所有的FreeSWITCH镜像都默认带mod_unimrcp,如果你用的镜像没有这个模块,要么换一个镜像,要么自己补装。这比什么都先确认。
第二个检查:SIP端口是否正常监听:
docker exec -it freeswitch fs_cli -x "sofia status"看到internal-ipv4和external-ipv4都是RUNNING状态,说明SIP协议栈起来了。
第三个检查:用软电话真实拨打一个FreeSWITCH自带demo。镜像里通常默认有拨号计划,软电话注册后拨打某个测试号码,能听到欢迎语音说明媒体链路没问题。这一步做完,证明“电话能进得来、声音能出得去”,后面接MRCP-SERVER时才不会混淆问题范围。
2.4 Windows装Docker时最常见的Virtualization报错
很多朋友是在Windows上开始玩FreeSWITCH的。Docker Desktop装完后点启动,直接报错:Docker Desktop failed to start because virtualization support wasn't detected。这个问题几乎每天都在各种技术群里被问。
这个报错的根因就一句话:Windows上跑Linux容器,依赖WSL2或Hyper-V,而WSL2需要CPU虚拟化支持并且BIOS里必须打开。
处理路径按顺序排查:
- 重启进BIOS/UEFI,找到Intel VT-x或AMD-V/SVM选项,设置为Enabled。
- 在Windows功能里勾选“适用于Linux的Windows子系统”和“虚拟机平台”,重启电脑。
- 在PowerShell(管理员)里执行
wsl --set-default-version 2,确保WSL版本是2而不是1。 - 最后重新启动Docker Desktop。
在BIOS里打开虚拟化这个步骤,是很多人在软件层面折腾半天没有结果的根源。我第一次遇到时把Docker Desktop重装了三遍,最后发现是BIOS里的VT-x没开,那一刻的心情想必你能体会。
Windows下跑FreeSWITCH容器,音频链路有个天然短板:如果软电话也运行在同一台Windows上,SIP和RTP流量在容器和本机之间穿梭,偶尔会出现音频单向或时断时续。我的建议是Windows上只用来做开发和调试验证,真正常态测试和生产环境还是放Linux服务器。
3. 接入阿里云MRCP-SERVER:unimrcp配置改这几个地方就够了
3.1 确认mod_unimrcp已经就位
FreeSWITCH里和MRCP相关的模块是mod_unimrcp,它依赖libunimrcp库。模块加载后,系统里会出现unimrcp这个拨号计划应用,以及unimrcp:synth(TTS合成)和unimrcp:recog(语音识别)这两个核心接口。
如果前面健康检查时已经确认module_exists返回true,那这一步就跳过了。这里要提醒一点:镜像默认的conf配置里,unimrcp默认连接的是本地MRCP服务器(通常127.0.0.1),这个默认值不会自动适配阿里云,所以下面手动改配置文件是逃不掉的。
3.2 unimrcp.conf.xml核心字段的“翻译”与填法
在宿主机上打开/data/freeswitch/conf/autoload_configs/unimrcp.conf.xml,核心结构是<unimrcp>根节点下挂着若干个profile,每个profile就是一个MRCP服务器连接配置。我通常建议单独为阿里云建一个新的profile,比如叫aliyun_sdm,不要动默认的base,方便以后切回本地测试。
一个基本可用的profile配置长这样(以实际控制台地址和端口为准):
<profile name="aliyun_sdm" version="2"> <param name="server-ip" value="your-mrcp-server-ip"/> <param name="server-port" value="your-mrcp-server-port"/> <param name="server-transport" value="tcp"/> <param name="auth-name" value="your-access-key-id"/> <param name="auth-password" value="your-access-key-secret"/> <param name="sip-ip" value="your-freeswitch-external-ip"/> <param name="sip-port" value="5090"/> <param name="rtp-ip" value="your-freeswitch-external-ip"/> <param name="rtp-port-min" value="40000"/> <param name="rtp-port-max" value="40003"/> <param name="codec" value="PCMU PCMA"/> <param name="speech-synth-engine" value="aliyun_tts"/> <param name="speech-recog-engine" value="aliyun_asr"/> </profile>重点解释几个看起来不起眼但影响很大的参数。
version="2":MRCP协议有v1和v2两个版本,阿里云SDM通常使用MRCPv2(基于SIP),所以这里必须填2。填错了最典型的症状是连接建立不上或者交互行为异常。
auth-name和auth-password:云服务的鉴权凭据。有些人会漏掉这两个参数,结果FreeSWITCH能连上IP和端口,但每次请求都被拒绝。MRCP的鉴权过程类似HTTP Basic Auth,但它在SIP level进行。拿不到正确凭据,后面永远是一连串的401/403。
rtp-port-min/max:因为前面Docker用的是host网络,这里RTP端口范围只需要在宿主机允许的范围内即可。我把范围缩小到4个端口,方便安全组和防火墙规则收敛。
speech-synth-engine和speech-recog-engine:这两个参数名可能在不同镜像里略有差异,有些版本里是通过resource-map的方式映射的。如果你看到配置里已经有synth和recog的resource映射,按要求修改里面的server即可。总之,目的就是让FreeSWITCH知道“TTS和ASR都走这个阿里云profile”。
3.3 针对阿里云的编码与资源类型配置
通话中FreeSWITCH和MRCP-SERVER之间的RTP音频编码,直接影响识别率和TTS音质。
电话网络里最基础的是PCMU(G.711 μ-law)和PCMA(G.711 A-law),这两种编码是RTP世界的通用语言,兼容性最好。阿里云MRCP-SERVER对这两种编码的支持也最稳定。
但要注意一个细节:窄带音频(8kHz)与宽带音频(16kHz)的识别体验差异。8kHz是传统电话的采样率,带宽有限,ASR模型在此条件下的表现天然受限。如果你的场景是纯移动网络通话,那就只能用8kHz;如果是VoIP终端之间通话,可能支持G.722等宽带编码。在unimrcp配置里,codec参数如果填了多种编码,需要在双方协商一致时才生效。我的经验是初期调试阶段就固定用PCMU,排除编码不协商导致的哑音问题,等链路稳定后再尝试换成宽带编码来对比识别率。
另外,unimrcp配置里通常还有tasking-timeout、max-timeout这类的超时参数。这些值的设置取决于你对用户语音输入时长的预期。比如对于“请说出你要办理的业务”这类开放式引导,识别等待时间过短会导致用户还没说完就超时;但如果你的业务场景是“请输入密码”,密码通常只有几位,等待太久又会拖慢流程。我建议初值设置为5-8秒,然后根据实际用户行为统计来调整,不要拍脑袋设一个10秒的固定值。
3.4 控制台验证MRCP连接是否打通
配置改完后,在宿主机上先重启容器或者用fs_cli热加载:
docker exec -it freeswitch fs_cli > reloadxml > unload mod_unimrcp > load mod_unimrcp加载完成后,观察控制台日志,看看有没有报错。
更直接的办法是先在拨号计划里模拟一次调用。在FreeSWITCH控制台执行:
> originate {unimrcp_profile=aliyun_sdm}loopback/1234 &unimrcp(recog)如果配置正确,你会看到MRCP会话建立成功的日志,FreeSWITCH会发起SIP INVITE到阿里云MRCP-SERVER,然后媒体协商完成。如果配置有误,控制台会打印具体的SIP错误码或MRCP错误信息,比如401(鉴权失败)、488(编码不支持)等。
这一步验证完成后,语音机器人的“底座”就算通了。接下来就是设计拨号计划,把对话流程串起来。
4. 一分钟跑通语音机器人拨号计划:P-Early-Media是关键
4.1 基础拨号计划:让机器人先说话再听你说话
FreeSWITCH的拨号计划(dialplan)是用XML定义的。每个呼叫进来后,FreeSWITCH根据被叫号码匹配对应的extension,然后执行一系列应用。语音机器人的核心流程就是放招呼语-TTS、开始识别-ASR、根据识别结果走分支,这个循环在FreeSWITCH里可以用unimrcp应用来实现。
一个极其简单的demo拨号计划看起来像这样:
<extension name="voicebot"> <condition field="destination_number" expression="^9000$"> <action application="answer"/> <action application="unimrcp" data="synth aliyun_sdm '欢迎致电测试中心,请说出您要办理的业务'"/> <action application="unimrcp" data="recog aliyun_sdm 'param1=session_timeout:10s'"/> <action application="log" data="ERR 识别结果: ${unimrcp_result}"/> </condition> </extension>看起来很简单对不对?这里其实就是FreeSWITCH的答案(answer)先接通电话,然后调用MRCP把TTS合成的招呼语播放给用户听,接着进入ASR识别状态,把用户的语音转成文本,存进unimrcp_result变量。后面就可以根据这个变量用条件判断去匹配“查余额”“办挂失”等意图,路由到不同业务节点。一个最简单的语音机器人对话循环就是这个样子。
4.2 p-early-media-support参数:解决“答非所问”的底层机制
在实际调优过程中,有一个参数对语音机器人的体验影响极大,那就是p-early-media-support。
先说现象。最初我按上面的demo搭完,发现用户打电话进来后,TTS播放“欢迎致电”的时候,用户说话是听不到的,必须等TTS播完、通道完全answer之后,ASR才开始采集声音。这在真实业务里很要命:很多用户听到一半就抢话,结果抢的那句话被系统丢弃,机器人只识别到后半句甚至没识别到,用户又得重复一遍。
根本原因在于,默认情况下FreeSWITCH在呼叫真正接通(200 OK + ACK)之后,媒体路径才完全建立。而MRCP识别如果等到answer之后才开始,用户提前说的话自然就丢了。
p-early-media-support就是为了解决这个时间差。这个参数需要在SIP profile里开启,它允许FreeSWITCH在early media阶段(也就是用户还没接通、还在听回铃音的阶段)就建立双向媒体路径。MRCP里的某些识别引擎也支持early media模式,一旦开启,用户说第一句话时ASR就能收到音频流,不用等机器人把导播语全部播完。
在FreeSWITCH里,启用这个参数的位置和含义因版本而异。一种典型配置是在conf/sip_profiles/internal.xml的profile节点里加:
<param name="p-early-media-support" value="true"/>同时在vars.xml里设置:
<X-PRE-PROCESS cmd="set" data="outbound_early_media=true"/>开启之后,FreeSWITCH会尽早向主叫方发送183 Session Progress告知媒体已经准备好了。但注意,仅仅开启SIP profile这个参数还不够。实际工程里还涉及媒体缓冲(media bug)、dyntrack等机制的配合。最简单有效的姿势是:在拨号计划中,先执行ring_ready,然后调用MRCP识别或播放,再在合适的时机才执行answer。这样电话一进来,媒体流是活的,用户随时可以说话。
我遇到过不少同行的坑是:参数改了,重启了SIP profile,但拨号计划仍然是先answer再做MRCP,导致early media的设置形同虚设。所以要记住核心逻辑:要让用户在电话还未正式接通前就能与MRCP交互,而不是等接通后再放语音。
4.3 状态机与超时设计:把识别逻辑变稳定
拨号计划里写死一段单向的TTS+ASR并不难,难的是把对话流程做成状态机,让机器人不会“一问三不知”或者陷入死循环。
这里我分享一个比较实用的三层状态设计:
第一层是引导与超时。每次进入ASR识别前,先定义一个全局或session级的超时变量,比如session_timeout=8000,也就是8秒内没有说话就触发超时。超时后播放“对不起,我没有听到您的声音”,然后重新进入识别节点或转人工。这里要注意,超时时间不宜全局统一。对于开放式问题可以给8-10秒,对于确认类问题可以给3-5秒。
第二层是拒识处理。ASR识别完成但置信度低或识别结果为空时,不能直接走错误分支,更不能不理会。标准做法是重试次数计数,同一轮对话最多重试2-3次,超过就转人工或者自动挂断。否则用户对着机器人喊三遍“你好”,机器人还搁那重复同一句导播语,体验非常差。
第三层是会话上下文。FreeSWITCH的session变量天然支持在同一个通话内保存状态。比如用户先说“我要查余额”,你把它存进intent变量;然后追问“请说出账号后四位”,用户说话再识别,识别结果存进auth_code变量。后面的分支逻辑就能同时使用这两个变量。虽然FreeSWITCH不是专业的NLU平台,但做固定流程的语音导航和事务办理,这套方案完全够用。
状态机的意义在于把翻车率控制在一个可接受的范围。语音机器人上线初期,识别率不可能是100%,但只要你把超时、拒识、转人工的路径设计好,用户感受到的“智能程度”会提升一大截。这其实比单纯追求更高识别率更划算。
5. Docker容器下最常踩的四个坑:我的排错实录
5.1 双方听不到媒体流:RTP端口和网络模式
有一段时间我在测试服务器上部署,宿主机安全组只放行了TCP 5060端口,结果软电话注册成功,但一通话就断,或者听到了对方的“幽灵音”却一句话都传不过去。后来一查,FreeSWITCH的RTP媒体流走的是UDP端口,而安全组只开了UDP 5060,RTP动态端口范围全部被防火墙拦截了。
这就是为什么前面我说Docker要使用host网络模式,但在host网络模式下,宿主机防火墙/SG(安全组)的配置仍然决定一切。
排查媒体流问题最快的方式是用抓包sngrep或tcpdump看SIP信令里SDP协商的IP和端口:
tcpdump -i any udp portrange 16384-32768 -nn -vvv如果看到RTP包在发送,但对方收不到,基本可以断定是中间防火墙丢包。如果RTP包根本没从本机发出,那大概率是FreeSWITCH的external-ip设置问题,或者路由配置把媒体引到了错误网卡。
在Docker host网络下,建议在vars.xml里显式设置:
<X-PRE-PROCESS cmd="set" data="external_rtp_ip=your_server_public_ip"/> <X-PRE-PROCESS cmd="set" data="external_sip_ip=your_server_public_ip"/>很多语音机器人在本地测试正常,一到云服务器上就不通,原因就在这两个外部IP没有设置成云服务器的公网IP或正确的内网IP。
5.2 MRCP鉴权失败/连接被拒:常见的三种原因
阿里云MRCP-SERVER接入失败,日志里最常见的几种情况我都遇到过,原因和解决思路各不相同。
第一种是401 Unauthorized。这说明网络是通的,但鉴权凭据不对。别急着怀疑AccessKey被禁用,先检查unimrcp.conf.xml里的auth-name和auth-password是否配置在了正确的profile下。我就干过一次把两个profile搞混,结果怎么改都报401,最后发现还是默认profile生效。
第二种是timeout。这说明TCP连接可能被中间网络拦截,或者阿里云侧的控制台里没有放通你的来源IP白名单。典型案例是MRCP-SERVER控制台里默认只允许配置的几个公网IP访问,你换了一台服务器部署就忘了加白名单,直接timeout。解决方法是登录阿里云控制台检查并添加来源IP白名单。
第三种是488 Not Acceptable Here。这是SIP协商不匹配,通常是编码格式或MRCP版本不对。前面提到的codec配置里如果填了不支持的编码,或者MRCP version写成了1,就会出现这个错。
排查这类问题时,你可以在FreeSWITCH控制台开启更详细的模块日志来定位:
> console log level debug5.3 识别总是静音超时:P-Early-Media没生效的排查路径
有一段时期,我的语音机器人已经可以接通并识别了,但有个诡异现象:用户必须在听到一段“嘟”声后才说话,否则说早了识别不到。原因就是前面讲的early media配置没有真正生效。
排查路径要从头梳理:
第一步,确认SIP profile中确实设置了p-early-media-support=true,并且你已经重启了sofia profile(注意是重启profile,不是只reloadxml)。这个参数如果改在internal.xml里,正确执行是:
> sofia profile internal restart第二步,用软电话发起呼叫后,观察FreeSWITCH日志里是否输出了sofftia级别的Early Media相关记录。如果在INVITE阶段就发送了183响应,说明early media已经建立,问题是出在MRCP引擎侧;如果没有183响应,说明SIP profile层就没生效。
第三步,检查拨号计划中ring_ready和answer的调用顺序。如果在MRCP识别之前就执行了answer,那Early Media阶段已经被跳过,p-early-media-support就算开启也白搭。正确顺序是:ring_ready→ 执行MRCP TTS/ASR → 合适的时机再answer。
5.4 容器重启后配置没丢但行为变了:时区与资源限制
有段时间我把容器做了docker restart之后,发现FreeSWITCH的日志时间对了,但通话录音的命名时间戳还是UTC,早上8点的通话在文件系统里看起来是凌晨0点。这虽然不影响通话功能,但因为是语音机器人,录音的归档时间在业务审计里是敏感的,很可能出错。
这个问题来自容器内应用读取时区的方式:挂载/etc/localtime只解决了C库层面的本地时间,但FreeSWITCH的某些模块会额外读取TZ环境变量。所以一个更稳的做法是在docker run时加上:
-e TZ=Asia/Shanghai这样时区在两个层面保持一致。
另外,容器默认没有资源限制,但宿主机一旦内存紧张,FreeSWITCH进程可能被OOM kill。语音机器人上线后往往是7x24小时运行的,我建议在docker run时加上:
--memory=2g --cpus=2限制内存可以有效防止容器内存膨胀拖垮宿主机。你可能会担心限制后性能不够,但实测FreeSWITCH处理一个并发语音会话的内存占用通常在几十到一百多MB,限制2GB对于测试环境绰绰有余,生产环境按峰值会话数估算再加。
5.5 用一个docker-compose.yml把部署固化下来
单个docker run命令够用,但配置项一多就容易漏。我后来把所有部署配置全部迁到docker-compose.yml里,好处是每次重建环境不用回忆命令,而且可以把p-early-media-support等所有配置改动一并记录在文档里。
一个参考版的docker-compose.yml:
version: '3.8' services: freeswitch: image: safarov/freeswitch:latest container_name: freeswitch network_mode: host restart: always environment: - TZ=Asia/Shanghai volumes: - /data/freeswitch/conf:/etc/freeswitch - /data/freeswitch/recordings:/var/lib/freeswitch/recordings - /data/freeswitch/log:/var/log/freeswitch - /etc/localtime:/etc/localtime:ro mem_limit: 2g cpus: 2如果你用了这个compose文件,注意一点:FreeSWITCH的xml配置尽量放在宿主机目录统一管理,不要在容器里手动改。因为compose up重建容器后,容器内所有改动都会消失,只有挂载进宿主机目录的配置才是持久化的。这点和Docker常规的最佳实践是一致的。
最后再分享一点个人经验。语音机器人从“能通”到“好用”,中间隔着的往往不是AI能力,而是对话流程设计和对超时、拒识、early media这些细节的把控。我目前这套FreeSWITCH + 阿里云SDM + Docker的方案,已经在多个测试项目中稳定运行,其中对我帮助最大的一次调试,就是花了一晚上把P-Early-Media参数彻底搞明白。你如果也在这个组合上折腾,建议先跑通最简demo,再逐步加业务逻辑,这样每一次出问题都能很快定位到是哪一层掉了链子。