把FreeSWITCH搬上Kubernetes这件事,我在不同团队里见过完全相反的评价:有人觉得这是自找麻烦,有人觉得这是VoIP基础设施现代化的必经之路。两边都有道理,因为这个组合恰好踩在容器编排和实时通信的交界处——一边是追求声明式、自动恢复、滚动发布的运维体系,另一边是SIP注册状态、UDP媒体流、RTP端口段这些"非常不云原生"的真实约束。这篇文章我会从镜像构建、K8s资源编排、网络媒体、伸缩高可用、日志排错这几个维度,把FreeSWITCH跑在K8s上真正值得注意的地方讲透。适合两类人:一类是已经决定迁移但还在踩坑的运维和平台工程师,另一类是正在评估可行性、想知道边界在哪里的技术负责人。
1. 为什么用Kubernetes跑FreeSWITCH
1.1 传统部署方式的四个痛点
我在实际中维护过FreeSWITCH的裸机和虚机集群,先说传统部署的真实体验。
第一个痛点是环境差异。每个节点的操作系统版本、动态库版本、内核参数、Firewall规则都可能不一样。同一个配置文件,在A机器上能跑,到B机器上就报缺模块或者目录权限不对。尤其FreeSWITCH依赖的库非常多,sndfile、opus、ssl、lua这些版本一漂移,行为就可能变。差环境这件事看着小,真排查起来能把人耗死。
第二个痛点是扩容慢。传统流程是准备模板机、改IP、改hostname、改配置文件、启动验证,一个人一下午能扩两三台就不错了。遇到突发注册量或者大促活动,根本来不及。而且手工改配置最容易出错,漏改一个IP地址,新节点就是废的。
第三个痛点是升级回滚难。改vars.xml之前要全量备份,升级二进制之前要留旧版本目录。出问题要人肉切回,经常手忙脚乱。如果同时改了配置和代码,回滚时到底恢复到哪一步,记忆非常容易混乱。
第四个痛点是监控告警割裂。不同机房、不同云厂商的虚拟机,监控口径不一样,安全补丁和系统基线也不统一。运维同学要维护好几套工作流,出问题时的定位路径也各不相同。
这些痛点累积到一定规模,比如几十台、上百台实例时,团队自然会开始看向Kubernetes。
1.2 K8s能解决什么,不能解决什么
K8s解决的是"交付和运维的一致性"。同一套Manifest,从测试环境到生产环境,最后跑起来的容器状态是一致的。配置在Git里,版本可追溯,发布会变成滚动加自动回滚,先拉起新Pod确认注册数正常,再摘旧Pod。Pod崩溃、节点宕机,ReplicaSet会自动补副本。这些都是实打实的好处,尤其对同时管多套环境的平台团队来说,省下的精力非常可观。
但这里要泼一盆冷水:K8s并不天然解决VoIP的状态问题。SIP注册、活动通话都在FreeSWITCH进程的内存里,Pod被销毁意味着这些状态全部丢失。HPA按CPU把副本数从2扩到10,新Pod也不会自动继承旧Pod的注册用户。你需要清醒地认识到,FreeSWITCH是有状态实时服务,不是普通无状态Web应用。K8s给你的是交付、编排和故障恢复的能力,不是状态迁移的魔法。
想清楚这一点,后面的所有设计和选型才有正确的方向。我见过不少团队把FreeSWITCH当Web应用部署,Deployment加NodePort一套,结果上线当天就翻车。我们下面从镜像开始,一步步把这个事情做稳。
2. 镜像构建与基础配置
2.1 镜像策略怎么定:官方镜像还是自建Dockerfile
官方镜像可以直接拉,适合快速验证、功能足够标准的场景。但实际生产里我建议自己构建,原因有三个。
第一是模块裁剪。官方镜像默认带了很多模块,WebRTC、XML、Event Socket、各种编解码器,不需要的模块会增加镜像体积,更关键的是扩大攻击面。VoIP系统直接暴露在网络上,镜像里每多一个模块,就多一分被利用的风险。
第二是定制编译参数。比如你要调整并发模型、线程池大小、特定编解码器优先级,或者植入自己的安全模块,官方镜像满足不了。源码编译让你对二进制有完全的控制权。
第三是安全基线。企业内部一般要求最小化镜像、非root运行、定期rebuild基础层,这些靠官方镜像不好落地。自建Dockerfile就能把这些要求写进构建流程。
下面给一个基于Debian的示意Dockerfile,采用多阶段构建,编译阶段和运行阶段分离,运行镜像只保留运行库。
FROM debian:bullseye-slim AS build RUN apt-get update && apt-get install -y \ build-essential cmake autoconf automake libtool \ libpcre3-dev libssl-dev liblua5.3-dev libopus-dev \ libsndfile1-dev libcurl4-openssl-dev zlib1g-dev \ libavformat-dev libswscale-dev libpq-dev WORKDIR /usr/src RUN git clone --depth 1 -b v1.10.9 https://github.com/signalwire/freeswitch.git WORKDIR /usr/src/freeswitch RUN ./bootstrap.sh -j 4 \ && ./configure --prefix=/usr/local/freeswitch \ && make -j 4 && make install FROM debian:bullseye-slim RUN apt-get update && apt-get install -y --no-install-recommends \ libssl1.1 libopus0 libsndfile1 libcurl4 libpq5 liblua5.3-0 \ ca-certificates tzdata \ && rm -rf /var/lib/apt/lists/* COPY --from=build /usr/local/freeswitch /usr/local/freeswitch ENV TZ=Asia/Shanghai RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone这个Dockerfile是示意,依赖库的清单要跟着你选定的FreeSWITCH版本和模块列表走。核心思路是:编译阶段装完整的编译链和头文件,生成二进制后,运行阶段只装运行库。镜像体积能小很多,编译工具链也不会留在生产镜像里。
2.2 构建周期里容易被忽略的几个配置项
镜像构建出来不代表能直接跑,有几个配置项很容易在容器环境里翻车。
时区和locale要提前设好。很多日志和录音文件名依赖时间,如果容器默认是UTC,日志时间和业务系统对不上,排查问题的时候会非常混乱。上面Dockerfile里已经设置了TZ,实际用的时候一定要确认/etc/localtime挂载正确。
控制台的日志格式要调整。FreeSWITCH默认的console日志带颜色转义字符,输出到stdout之后会被日志采集器当成乱码处理。可以在logfile里面把颜色关掉,或者在启动参数里加-nc(no color),让日志干净一点。
模块加载路径要确认。源码编译安装默认在/usr/local/freeswitch/mod,但如果你用发行版包安装,路径可能是/usr/lib/freeswitch/mod。容器里路径错了,启动直接报模块加载失败,这个坑我踩过不止一次。
如果启用了TLS或者SRTP,证书文件的位置要规划好。容器重建后证书不能丢,建议放到PVC里,或者直接托管在Secret里,启动时挂载进去。不要把证书留在镜像层里,一是不安全,二是镜像更新时证书就换了。
2.3 运行用户、健康检查和探针配置
容器里不要用root跑FreeSWITCH。虽然K8s默认也是非root容器,但很多镜像为了省事还是用root,这是安全漏洞。正确的做法是创建专用系统用户,把运行目录的owner设置好:
RUN useradd --system --home-dir /usr/local/freeswitch --shell /sbin/nologin freeswitch \ && chown -R freeswitch:freeswitch /usr/local/freeswitch USER freeswitch注意一点:如果FreeSWITCH需要监听1024以上端口,普通用户没问题。但是如果之前习惯在配置里写sip-port绑定类似 443 这种低端口,就需要额外加上NET_BIND_SERVICE能力,或者干脆换端口。这个在容器里最省事的做法是直接改用非特权端口。
健康检查的探针配置是另一个容易出问题的地方。FreeSWITCH自带的fs_cli -x status是最常用的探针命令,但有几个坑。
fs_cli连接事件socket时有可能会被锁住,如果上一个检查卡住,下一个检查就会排队,导致K8s误判Pod不健康。所以探针命令要加超时,比如timeout 5 fs_cli -x status。
liveness和readiness不要用同一个逻辑。readiness可以用sofia status检查SIP profile是否正常注册到上游;liveness用fs_cli -x status确认进程活着就行。liveness如果做得太重,比如调用外部API或者扫大目录,Pod会被K8s频繁重启,重启一次全量呼叫就断一次。
livenessProbe: exec: command: - /bin/sh - -c - "timeout 5 fs_cli -x status >/dev/null 2>&1" initialDelaySeconds: 30 periodSeconds: 30 timeoutSeconds: 5 readinessProbe: exec: command: - /bin/sh - -c - "timeout 5 fs_cli -x 'sofia status' >/dev/null 2>&1" initialDelaySeconds: 20 periodSeconds: 10 timeoutSeconds: 5探针的period别设太短,FreeSWITCH在高峰期处理大量呼叫时,fs_cli的响应时间会变长,太频繁的探针反而容易误杀。
3. K8s资源编排:从StatefulSet到Service
3.1 为什么选StatefulSet而不是Deployment
先说结论:绝大多数生产场景选StatefulSet。理由有三点。
第一是身份稳定。StatefulSet的Pod名是fs-0、fs-1、fs-2,配合headless service,每个实例有稳定的DNS名称。如果FreeSWITCH的配置里需要引用节点自身的地址或者做节点间通信,稳定身份能省很多事。
第二是存储持久化。录音、CDR、语音文件这些数据必须落到PVC,StatefulSet天然支持按副本序号绑定独立的PVC。Pod重建之后,PVC会重新挂载到新Pod,数据不丢。Deployment通常共用PVC或者依赖外部存储,配置起来麻烦且容易出权限问题。
第三是滚动顺序。StatefulSet更新的时候是逆序一个个来,也就是先更新最后一个实例,确认没问题再往前推进。对VoIP这种有状态服务来说,这种保守的滚动方式比Deployment的并发替换安全得多。
有一种情况可以考虑Deployment:你把注册状态全部外置,注册表放Redis,路由抽成独立服务,FreeSWITCH实例之间完全无状态。这种架构下Deployment是可以的,但这属于架构重构,不是普通部署。绝大多数团队没有这个必要,StatefulSet是更稳妥的默认选择。
3.2 Service端口规划:SIP、RTP、WebSocket
FreeSWITCH对外暴露的端口不算多,但每个端口都有特殊要求。核心端口如下表:
| 传输方向 | 端口 | 协议 | 用途 | 备注 |
|---|---|---|---|---|
| 信令 | 5060 | UDP/TCP | SIP注册和呼叫 | 必须暴露,通常同时开UDP和TCP |
| 媒体 | 16384-32768 | UDP | RTP音视频流 | 范围大,默认按配置文件rtp-start/end |
| WebSocket | 5066 | TCP | SIP over WebSocket | WebRTC接入用,按需打开 |
| Verto | 8082/7443 | TCP | mod_verto | 如果启用需要单独规划 |
NodePort是K8s最常见的服务暴露方式,但默认端口范围是30000-32767。如果你的RTP媒体范围配置成16384-32768,那NodePort只覆盖了一小段,媒体端口大部分映射不出去,呼叫会大面积单通。
解决办法有三个方向。第一个方向是修改kube-apiserver的启动参数,把NodePort范围调整成和RTP范围一致:--service-node-port-range=16384-32768。这样NodePort能覆盖到RTP端口,但这个范围很大,和集群里其他服务冲突的风险非常高,需要严格规划。
第二个方向是用LoadBalancer。云厂商LB可以拿到独立公网IP或者内网IP,FreeSWITCH通过L4负载均衡对外,不经过NodePort转发。媒体流路径干净,也是我比较推荐的方式。
第三个方向是直接用hostNetwork。让FreeSWITCH直接监听节点端口,完全不经过Service转发。这个方案后面会细讲,是很多VoIP生产集群的实际选择。
3.3 ConfigMap和PVC的职责边界
ConfigMap存静态配置,PVC存动态数据,这个边界要拎清楚。
ConfigMap适合放这些:vars.xml、sip_profiles/*.xml、dialplan、directory,以及一些不常变化的模块参数。把这些配置抽出来放到Manifest里,就能做到"配置即代码",版本可追踪,变更可审计。
PVC适合放这些:recordings/目录下的录音文件,voicemail/下的语音邮箱文件,CDR如果选择落地文件的话也需要PVC,一些模块运行期产生的缓存文件。这类数据是服务运行过程中产生的,Pod重建之后必须还在,只能走持久化存储。
一个配置陷阱:FreeSWITCH启动时用freeswitch.xml把多个XML include进来,所以ConfigMap挂载时一定要对齐目录结构。比如你把整个conf目录挂载进去,还是只挂载单文件,路径差一点模块就加载不到配置。常见的做法是把conf目录整个放进ConfigMap或者挂载到PVC,保持原来的相对引用关系,避免手工拼路径。
配置更新的两种做法,我推荐滚动重启。虽然fs_cli -x reloadxml可以热加载一部分配置,但有些模块参数必须重启进程才生效。而且热加载失败后很难定位是哪段配置出的问题,不如直接滚动重启Pod,让新配置随新Pod一起上线,干净利落。
4. 网络模型与媒体流处理
4.1 hostNetwork、NodePort、LoadBalancer三种路线怎么选
这一节是FreeSWITCH上K8s最核心的决策,比镜像和Manifest都重要。媒体流路径选错了,后面的优化全白搭。
hostNetwork方案。让FreeSWITCH直接监听节点IP的端口,完全绕过kube-proxy和NodePort转发层。媒体流路径最短,性能损耗最小,SIP的UDP行为最接近裸机部署。缺点是端口直接占用宿主机,多个Pod在同一节点会端口冲突,所以一般要配合nodeSelector把Pod固定到指定节点。适合对媒体质量要求高、并发量大的场景。
NodePort方案。适合信令量不大、并发不高的场景。媒体流会经过NodePort和kube-proxy转发,在大数据包、高PPS场景下,kube-proxy的iptables或者nftables转发是明显瓶颈。可以设置externalTrafficPolicy: Local来缓解,但会丢掉部分节点的负载均衡能力。
LoadBalancer方案。云厂商LB或者MetalLB都行。优点是可以拿到稳定外部IP,媒体路径相对干净,不用去改NodePort范围。缺点是UDP负载均衡的会话保持和超时参数必须仔细调,有些LB处理大UDP包会产生比较严重的性能问题,选型的时候要特别注意。
我个人的推荐顺序是:大规模生产优先hostNetwork,中小规模用LoadBalancer,NodePort只作为临时方案或内部测试。如果你问我为什么不用Ingress,UDP和RTP这种流量不是HTTP,用Ingress转发完全是在绕远路。
4.2 UDP、SDP、NAT三座大山的连环坑
就算Service配好了,媒体流还有一个天然的难点:SIP协议是应用层地址协商,不是传输层连接的逻辑。
第一个坑是SDP里的媒体地址。SIP的INVITE消息里,SDP部分包含了接收媒体的IP和端口。这个地址是从FreeSWITCH的local_ip、ext-rtp-ip、ext-sip-ip这些配置生成的。如果容器内网IP是10.233.x.x,而你希望外部媒体流直接发到节点IP或者公网IP,就必须在sip_profile里配置正确的对外地址。
<param name="ext-rtp-ip" value="$${external_rtp_ip}"/> <param name="ext-sip-ip" value="$${external_sip_ip}"/>不配置的话,对端会把媒体RTP包发到Pod的IP,Pod是没法从外部直接访问的,结果就是注册能过、信令能通,但媒体流完全没有,表现为单通或者无声。这个排查起来很迷惑,因为信令层面一切正常。
第二个坑是NAT超时。UDP没有连接状态,NAT表项只有持续有流量才会保鲜。如果FreeSWITCH的Keep-Alive间隔太长,比如Option Ping设成60秒,而NAT的表项超时是30秒,那注册着注册着就会突然掉线。这个问题的典型症状就是"用户反馈每隔一段时间就掉线,重试又好了"。把Option Ping和注册续约的时间调短到10到15秒,能解决绝大多数NAT掉线问题。
第三个坑是conntrack。kube-proxy的DNAT依赖conntrack维护映射关系。UDP的conntrack条目超时后,如果还有残余包到达,可能会走错路径,出现偶发性的呼叫失败或者单通。流量大的时候特别明显,需要调整nf_conntrack_udp_timeout和nf_conntrack_udp_timeout_stream这两个内核参数,或者干脆用hostNetwork绕开kube-proxy。
4.3 保留客户端真实IP:externalTrafficPolicy的讲究
如果FreeSWITCH后面要接SBC,或者需要按来源IP做ACL,那么externalTrafficPolicy必须设成Local。
默认的Service转发模式下,kube-proxy会在节点上做一次SNAT,把来源IP替换成节点IP。结果就是FreeSWITCH看到的所有客户端请求都来自同一个节点地址,你无法区分真实用户来源。把trafficPolicy设成Local之后,流量只在到达的节点本地转发,不做SNAT,源IP就能保留下来。
我遇到过真实的生产事故:SBC规则配得好好的,但FreeSWITCH里看到的来源IP全是节点地址,ACL全被误杀。排查了一个下午,最后发现就是externalTrafficPolicy默认值害的。这个参数从Layer视角看很小,但影响面非常大,尤其是对接SBC时几乎必踩。
5. 伸缩、滚动更新与高可用
5.1 为什么HPA在这里不好使
HPA按CPU或者内存自动伸缩,对无状态Web服务很方便。但FreeSWITCH这种"进程内存等于状态"的服务,HPA直接套用就是灾难。
原因很简单:你按CPU从1副本扩到5副本,新起的四个Pod是空的,既没有注册用户,也没有通话。用户还是注册在旧Pod上,旧Pod的CPU压力一点没减,新Pod空转,资源浪费了,问题也没解决。反过来,缩容的时候K8s随便挑一个Pod杀,如果是承载了几万注册用户的实例,那场面就是全量掉线。
正确做法是容量规划先行。根据"并发呼叫数乘单路资源开销再加20%到30%冗余"来确定副本数,同时按副本数预留节点池的CPU和内存。业务高峰期之前提前扩容,而不是等CPU被打满了再触发HPA。
如果你真的需要动态伸缩,那就必须架构上把状态外置:注册表放Redis、路由抽成独立服务、CDR走消息队列。这个时候FreeSWITCH实例本身接近无状态,可以用HPA。但这相当于重写通信核心,一般团队不值得为这个付出成本。
5.2 优雅终止和滚动更新的正确姿势
Pod被删除的瞬间,FreeSWITCH进程被杀,所有注册、所有通话全部断开。K8s默认没有给应用预留收尾时间,所以必须自己配置。
preStop hook是K8s提供的一个时机,在Pod被标记为Terminating、容器收到SIGTERM之前执行。你可以在这里调用FreeSWITCH的关闭命令,让服务先优雅收尾:
lifecycle: preStop: exec: command: - /bin/sh - -c - "sleep 5 && fs_cli -x 'fsctl shutdown'"fsctl shutdown会让FreeSWITCH停止接受新呼叫,并尝试正常结束现有通话。但注意,如果通话非常长,shutdown也可能会等很久,所以terminationGracePeriodSeconds要调大,比如60到120秒,给进程足够的收尾时间。
滚动更新的策略也要保守。maxUnavailable: 0保证任何时候都有旧实例在服务,maxSurge: 1先起一个新实例,确认ready之后才继续替换。这样即使新配置有问题,也只影响一个新实例,不会瞬间打满旧实例。
如果你的FreeSWITCH依赖上游SIP Proxy做路由,建议在preStop里向上游发一个带expiry=0的REGISTER注销请求,让新呼叫绕开即将下线的实例。这个细节很多人不知道,但对减少滚动更新时的呼叫失败率很有帮助。
5.3 PDB、反亲和性和跨可用区部署
有状态服务的高可用,不只是K8s层面的事,要从多个维度一起保障。
PodDisruptionBudget(PDB)保证在自愿中断(比如节点维护)时,集群中至少保留指定数量的实例。副本数是3的话,minAvailable设为2,即使运维要重启一个节点,K8s也会阻止这次操作把Pod数降到2以下。
apiVersion: policy/v1 kind: PodDisruptionBudget metadata: name: freeswitch-pdb spec: minAvailable: 2 selector: matchLabels: app: freeswitch反亲和性(podAntiAffinity)要设上,避免两个FreeSWITCH Pod落在同一个节点。否则一个节点宕机,可能就直接损失两路实例,容灾能力大打折扣。对于StatefulSet,设置podAntiAffinity的preferredDuringSchedulingIgnoredDuringExecution,尽量把Pod分散到不同节点就行。
跨可用区部署要看媒体路径。把Pod分散到不同可用区,运维视角是高可用,但如果两个可用区之间的RTP时延和抖动明显,用户视角的语音质量反而会下降。所以跨AZ之前一定要先测媒体路径,别为了"高可用"牺牲了用户实际体验。我见过一个案例,跨AZ部署之后,所有跨区呼叫的MOS分掉了0.5,用户投诉率翻了一倍。
6. 日志、监控与问题排查实录
6.1 容器日志怎么改才不丢
FreeSWITCH默认把日志写盘,路径在/usr/local/freeswitch/log/freeswitch.log。容器模式下这个文件落在可写层,Pod一删除,日志就没了。而且K8s本身不会去采集这个文件的内容,你得额外挂PVC再对接采集器,绕了一圈。
更符合容器规范的做法是让日志走stdout。FreeSWITCH可以配置把console日志级别调高,同时在logfile里关闭本地文件日志,让所有日志输出到stdout,由容器运行时接管。这样Fluent Bit或者Loki就能直接从stdout采集,不需要额外的采集器配置。
需要注意的是,容器场景下stdout已经由运行时托管,本地日志轮转可以不做。FreeSWITCH默认的日志轮转配置反而可能和容器的日志管理冲突,建议关掉。
如果一定要保留文件日志,比如出于审计需求,可以挂一个PVC专门放log目录,但要有完善的清理策略,不然录音加日志双写,PVC很快就满了。
6.2 CDR出口和核心监控指标
CDR(Call Detail Record,呼叫详单)是VoIP系统最重要的数据资产。不要把CDR只写在容器本地文件里,Pod一销毁数据就没了。生产环境建议直接用mod_json_cdr,把结构化话单通过HTTP POST推到Kafka、数据库或者内部数据平台。这样CDR和数据平台打通,后续的计费、业务分析都能直接在平台上做。
监控指标至少要有四类:实例状态(Pod层面)、注册用户数、并发呼叫数、通话质量指标。
注册用户数和并发呼叫数可以从FreeSWITCH的mod_sofia状态和事件接口里拉。可以写一个小的exporter,定时执行fs_cli -x "show registrations"和fs_cli -x "show calls",把统计数字暴露成Prometheus metrics,配好告警规则。通话质量一般要看端到端的MOS/RTT,这部分靠FreeSWITCH侧的数据不完整,通常要结合客户端的QoS统计,或者接SBC网关收集。
6.3 三个高频问题的排查顺序
我用真实案例写三个排错过程,大家在群里问得最多的也是这三类。
案例A:Pod状态Running,但外面注册不上。排查顺序:先确认NodePort或者LoadBalancer是否正在监听,再查安全组是否放行了UDP 5060(别只开TCP,UDP漏了非常常见),然后进容器执行fs_cli -x "sofia status"看profile是否在跑,最后tcpdump看SIP包有没有到达容器网卡。我遇到最多的情况就是安全组只放了TCP,忘了UDP,症状一模一样。
案例B:能注册但打不通,或者单通。排查顺序:先抓SIP信令,看INVITE的SDP里媒体地址是什么。如果是容器内网IP,说明ext-rtp-ip没配置,对端把RTP包发到了PodIP,直接不通。接下来放行RTP端口范围的UDP,最后检查NAT的端口映射是否把16384-32768整段都映射了。不少云环境默认只映射几百个端口,RTP一到范围外就丢。
案例C:呼叫时好时坏,负载并不高。这个大概率是conntrack。执行conntrack -L | wc -l看条目数是不是接近nf_conntrack_max。如果是,检查UDP超时参数,把nf_conntrack_udp_timeout调短,比如从180秒降到60秒。大流量时还要注意nf_conntrack_buckets和哈希表大小,否则频繁冲突会造成大量丢包。
这些问题的共同点是:信令层面看起来正常,但媒体层面暗藏问题。所以排查时不能只看sofia status,一定要看实际SIP包和媒体包路径。
6.4 上生产前要提前解决好的清单
如果前面的内容你都消化了,这里再给你一份可以直接当checklist用的清单。
第一,端口规划表。把信令端口、媒体端口、对外服务端口、集群内部端口全部列出来,确认无冲突。尤其是RTP端口范围和NodePort范围,必须在设计阶段就对齐。
第二,容量估算和节点标签规划。根据预期并发算出需要的副本数和节点数,给承载FreeSWITCH的节点打专用标签,配合hostNetwork和反亲和使用。
第三,配置模板化和基线管理。把ConfigMap纳入Git管理,配置变更走Code Review,避免线上改配置改到一半找不到历史版本。
第四,备份策略。配置通过ConfigMap已经做到可还原,录音和CDR的PVC要定期做快照,定义好恢复的RTO和RPO。
第五,灰度发布流程。先发一个实例,观察注册数、并发呼叫、CPU内存曲线,确认没问题再放量。不要一上来全量滚动,出了问题想回滚都来不及。
第六,安全基线。TLS和SRTP证书的安装、轮转、吊销要梳理好流程。ACL放行规则最小化,switcboard和event socket这些管理端口不要暴露到公网。
最后分享一个真实体会
聊到最后,分享一个我个人在实操中的体会。
我第一次把FreeSWITCH迁到K8s的时候,犯的最大错误就是照搬Web服务的经验:Deployment加NodePort加HPA一套组合拳打上去,结果媒体流全部绕了远路,SIP注册断断续续,排查了一个星期才定位到是网络转发层的问题。后来才想明白,K8s对你最大的价值是交付、编排和自动化,而不是让媒体流在转发层多绕几圈。
如果你正在做同样的事,我的建议是先别急着写一堆Manifest,先把媒体路径定下来。用hostNetwork也好,用L4的LoadBalancer也好,确认SDP地址、NAT映射、RTP端口整条链路是通的,再往上搭StatefulSet、ConfigMap、PDB这些上层编排。底层路由稳定了,上层自动化才有意义,否则你只是在自动化地复现一个错误配置。