ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

把FreeSWITCH跑进Kubernetes:有状态SIP应用的容器化实战

把FreeSWITCH跑进Kubernetes:有状态SIP应用的容器化实战 把 FreeSWITCH 跑进 Kubernetes这件事我一开始是拒绝的。SIP 是典型的长连接有状态协议RTP 是实时性极强的媒体流而 Kubernetes 的调度、网络、探针机制几乎每一项都在跟这类应用拧着来。最早接这个需求时团队里一半人觉得直接上裸机最省事另一半人拿 Kubernetes 上部署 nginx 那套思维照葫芦画瓢结果端口冲突、SIP 注册抖动、录音文件丢失等一堆问题轮着炸。这篇文章把我后续在生产环境里稳定承载 FreeSWITCH 的完整经验整理出来从镜像构建、网络模型、StatefulSet 设计、探针配置到扩缩容和排障心法适合准备把电话系统容器化或者已经在容器里跑 FreeSWITCH 但被各种报错折磨的读者。1. 为什么把 FreeSWITCH 搬进 K8s弹性、发布与资源困境1.1 呼叫业务的弹性扩容需求FreeSWITCH 做软交换或者 IVR 时流量峰值往往集中在特定时段比如工作日的早高峰、营销活动的拨打量暴涨。裸机部署下硬件是按峰值采购的日常大部分时间资源白白闲置用虚拟机部署虽然能扩容但新增一台虚拟机从申请到系统初始化到安装依赖怎么都要小时级别根本扛不住突发流量。Kubernetes 在这里最大的价值是让 FS 实例的扩容从小时级变成分钟级。我在生产里维护过一个 50 并发规模的 FS 集群高峰期呼叫量能到 300 并发直接kubectl scale statefulset freeswitch --replicas6就能把媒体处理能力拉起来等流量退了再缩回去。FS 本身是 CPU 密集型应用尤其转码场景下压缩算法的计算开销很高容器化之后资源用量可以被 Kubernetes 精确统计扩缩容时的资源账单也清晰很多。但这里要有一个清醒的认知FreeSWITCH 不是无状态 Web 服务扩容并不是简单地多加几个 Pod 就完事。呼叫在进行中的时候SIP dialog 状态、媒体协商的结果、录音句柄都活在进程内存里Pod 被杀掉意味着这些通话直接断掉。所以我们后面重点讲 StatefulSet、亲和性调度和优雅退出这些才是 FS 上 K8s 的关键。1.2 版本发布与配置管理的规范化裸机阶段FS 的配置管理往往靠 SSH 上去改文件改完reloadxml或者重启服务。一次误操作可能就让线上系统起不来而且多台机器之间的配置漂移问题非常严重——常常是这台机器的 external profile 开了 NAT 参数另一台没开出问题才想起来对比。容器化之后镜像本身是不可变单元代码版本、依赖库、配置文件都固定在一个 Image 里。配置差异用 ConfigMap 或者 Helm 参数去表达改配置就是一个新的发布流程可以走 CI/CD 管道可以回滚。我现在的做法是基础镜像半年更新一次负责系统依赖和 FreeSWITCH 版本业务配置全部走 ConfigMap每次改动都能在 Git 里看到 diff出了事kubectl rollout undo一分钟内回滚。这种发布体验是裸机做不到的。1.3 故障域隔离与资源复用Kubernetes 让 FS 的故障域变得更可控。节点宕机了控制器会把 Pod 重新调度到健康节点配合前端 SIP 代理层的重试用户感知的影响被压缩到很小。同时多个业务共享一个集群FS 所在节点池可以跟 Web 服务隔离既避免资源争抢又提高了基础设施利用率。不过把 FS 放进 K8s 不代表自动获得高可用。FS 本身没有集群模式多实例之间如果不共享注册数据呼叫分发就会出现注册在 A 实例、通话落在 B 实例的问题。所以部署方案里必须设计好实例之间的关系要么在前面挂一层 SIP 负载均衡要么让 FS 作为纯媒体服务器接入已有软交换体系。这一点第 5 节里会详细展开。2. FreeSWITCH 的有状态约束SIP 会话与实时媒体流对调度的影响2.1 为什么不能拿 nginx 那套习惯来部署 FS很多刚接触容器化的同学会想既然 nginx 能做成 Deployment 跑得飞起FS 也应该差不多——把进程丢进容器挂几个端口配一个 Service完事。这恰恰是最容易掉进去的坑。nginx 是无状态的反向代理任何一个 Pod 死掉流量会被其他 Pod 接住用户层面几乎无感知。而 FS 一旦 Pod 被删除或者重启正在进行的通话立刻断掉已经录了一半的录音文件也会丢失。这就是有状态应用的典型特征运行时的会话上下文、业务数据、媒体资源都和进程生命周期强绑定。SIP 协议从设计之初就是面向一座座独立交换机的每个呼叫的 dialog 状态、鉴权信息、媒体地址都维护在设备本地。在 K8s 上跑 FS最先要接受的就是这个约束然后才能谈怎么让调度器跟它和平共处。2.2 CPU 实时性与资源配额设计FreeSWITCH 对 CPU 的敏感度远超普通 Web 服务。一路 G.711 通话大约消耗 8Kbps 左右的带宽资源但 CPU 占用很低可一旦涉及转码比如把 G.729 转成 G.711一路通话可能吃掉单核 30% 甚至更多的算力。在一个繁忙的 IVR 实例上抖动、时延超时、丢包都可能直接影响语音质量。Kubernetes 默认的 CPU 管理策略是有可能给到应用带来调度延迟的。生产环境我建议用专门的节点池跑 FS开启节点的 CPU Manager static 策略让 Guaranteed QoS 的 Pod 独占物理核至少也要保证requests和limits一致避免 CPU 配额被 CFS 周期强制限制后出现周期性的调度停顿。内存方面FS 每路通话的音频缓冲区可以估算通常给每路通话预留 64KB 到 128KB 的内存再用/usr/local/freeswitch/bin/freeswitch -rp这类参数启动避免容器 OOM 时内核直接给 FS 一刀。2.3 单节点实例数端口冲突是第一道坎FreeSWITCH 默认要监听一堆端口SIP 的信令端口 5060UDP/TCP、SIP over TLS 的 5061、内部等多 Profile 可能用到 5080、Event Socket 的 8021还有媒体流 RTP 的一大段 UDP 端口区间。这些端口在容器里没问题因为每个 Pod 有自己的网络命名空间但如果用 hostNetwork 模式第 4 节会解释为什么这是首选宿主机端口的独占性意味着一个节点上只能跑一个 FS 实例。因此在设计调度策略时我会给 FS 加 podAntiAffinity强制同一个节点的 FS Pod 只能有一个并且用 nodeSelector 指向专门的语音节点池。这样虽然牺牲了一点容器随意打包的灵活性却换来了网络模型的简单和数据面的稳定这笔账在生产里是划算的。3. 镜像与配置文件先解决能不能跑再谈怎么编排3.1 官方镜像与自建镜像的取舍Docker Hub 上有 SignalWire 官方维护的signalwire/freeswitch镜像版本跟随主线拿来做个概念验证非常快。我最早测试时就是直接docker pull signalwire/freeswitch:1.10.10起一个容器跑通的。但官方镜像并不适合直接上生产原因有三其一编译选项不一定覆盖你需要的模块比如某些商用编解码器、mod_odbc 之类的数据库模块可能没编译进去其二镜像内的非 root 用户、目录权限、locale 设置未必符合你的运行环境其三你没法保证镜像中的 FreeSWITCH 版本和内部测试过的一致。生产环境我更建议基于 Debian 或者 Rocky Linux 自建镜像。Debian 系可以直接引入 FreeSWITCH 官方软件源把freeswitch-meta-all和需要的外围模块一起装进去。下面是我一份可用的 Dockerfile 骨架FROM debian:bullseye RUN apt-get update apt-get install -y --no-install-recommends \ gnupg2 wget ca-certificates lsb-release tzdata procps iproute2 dnsutils \ wget -O /usr/share/keyrings/signalwire-freeswitch-repo.gpg \ https://files.freeswitch.org/repo/deb/debian-release/signalwire-freeswitch-repo.gpg \ echo deb [signed-by/usr/share/keyrings/signalwire-freeswitch-repo.gpg] https://files.freeswitch.org/repo/deb/debian-release/ bullseye main \ /etc/apt/sources.list.d/freeswitch.list \ apt-get update \ apt-get install -y --no-install-recommends freeswitch-meta-all \ apt-get clean ENV TZAsia/Shanghai RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime echo $TZ /etc/timezone WORKDIR /etc/freeswitch EXPOSE 5060/udp 5060/tcp 5080/udp 8021/tcp 16384-32768/udp CMD [/usr/local/freeswitch/bin/freeswitch, -nf, -nonat]启动参数里的-nf是让 FS 前台运行日志直接打到标准输出方便容器日志采集-nonat表示不自动判断 NAT 环境把 NAT 处理交给明确的配置去控制。镜像体积会比较大但换来的是按需裁剪的自由度以及生产环境与测试环境绝对一致的运行条件。3.2 配置文件注入策略整包 ConfigMap 与部分覆盖FreeSWITCH 的配置是一棵完整的目录树从freeswitch.xml到各个模块的autoload_configs/*.conf.xml动辄几十个文件。把它们全部塞进 ConfigMap 是不现实的ConfigMap 有 1MB 的单体大小限制而且把一整套与运行环境耦合很深的配置放进编排模板里会让后续维护变得痛苦。我推荐的方案是镜像内保留一份出厂默认配置启动时用 InitContainer 把默认配置复制到持久卷然后只把需要经常改动的关键文件用 ConfigMap 挂载覆盖。实际操作中需要覆盖的文件很少文件路径覆盖原因/etc/freeswitch/vars.xml设置对外 IP、RTP 起始端口、默认语言、录音目录等全局变量/etc/freeswitch/sip_profiles/external.xml定义 SIP 对外 Profile绑定 ext-sip-ip、ext-rtp-ip、NAT 策略/etc/freeswitch/autoload_configs/event_socket.conf.xml配置 Event Socket 监听地址、口令、ACL/etc/freeswitch/autoload_configs/acl.conf.xml声明内网网段、语音网关的访问控制清单/etc/freeswitch/dialplan/default.xml按业务场景定义拨号计划属于业务可变部分InitContainer 的思路很直接挂载同一个 PVC启动时把镜像里的默认配置拷贝进去然后主容器用 ConfigMap 的 volumeMount 把上述几个关键文件放到对应路径上。这样既能保证空白机器也能跑出可用 FS又能让配置变更走 GitOps 流程。3.3 时钟、时区与编解码器许可的两个隐藏坑容器镜像普遍默认 UTC 时区FreeSWITCH 生成 CDR、录音文件名时用的都是系统时间如果时区不设置成北京时间你第二天的日报统计会全部错位。上面的 Dockerfile 里已经处理了TZ但要注意如果容器以非 root 用户运行写入/etc/timezone可能没有权限这种情况下更稳妥的做法是在 ConfigMap 里设置timezone环境变量并通过 NTP 保证节点时间同步。另一个容易踩的坑是商业编解码器。G.729 等专利编码格式并非所有 FreeSWITCH 发行版都内置有些需要单独购买许可并放置许可证文件。容器化部署时许可证文件要作为 Secret 或者独立 ConfigMap 挂载并且确保启动 FS 的用户有权限读取。我在实际项目中遇到过镜像构建好了结果因为运行时用户是 nobody许可证文件权限是 600导致 G.729 编解码一直没有加载成功查了一整天才发现是权限问题。4. 网络模型选型SIP 信令与 RTP 媒体流的连通性设计4.1 hostNetwork首选但不是偷懒FreeSWITCH 的媒体面需要一个非常大的 UDP 端口区间默认配置是 16384 到 32768也就是整整 16384 个端口。如果把 FS 放进普通 Pod 网络CNI 分配的独立 IP再通过 NodePort 或者 LoadBalancer 转发要么你把这一万多端口一个个映射出去不现实要么只映射几个信令端口媒体流绕开 K8s 直接穿透又回到了外部网络层写死路由。所以对绝大多数场景来说hostNetwork 是唯一能把网络模型理清楚的选择。hostNetwork 模式下Pod 直接复用宿主机网络栈FS 监听的 5060、8021 以及整个 RTP 端口区间都直接从宿主机的物理网卡进出。这样就没有了 DNAT 带来的地址转换问题SIP 报文里的 Contact 头、SDP 里的媒体地址都可以直接填真实可路由的 IP。代价就是我前面说的一台节点只能跑一个 FS 实例。这个代价在语音业务里完全可以接受因为真正的扩容维度是节点数而不是容器数。4.2 用 NodePort 的场景明确 ext-sip-ip 和 ext-rtp-ip如果你的宿主机本身没有直接暴露公网或者对外的地址是某个内网 VLAN 里的虚 IP那么即便用 hostNetwork也要在 SIP Profile 里显式声明对外地址。FreeSWITCH 有两个关键参数管这事ext-sip-ip和ext-rtp-ip。我在一台处于内网、前面有防火墙做端口映射的节点上部署时就在 external.xml 里这样配param nameext-sip-ip value203.0.113.20/ param nameext-rtp-ip value203.0.113.20/这里填的必须是 SIP 对端能路由到你的 IP也就是防火墙映射后的公网地址而不是宿主机内网 IP。不填或者填错的话FreeSWITCH 生成 SDP 时会把内网地址告诉对端对端发来的 RTP 媒体包自然就到不了 FS表现为呼叫能建立但双方听不到声音。如果确实走的是 NodePort 形态还要注意 K8s Service 的externalTrafficPolicy要设为Local否则中间节点的 SNAT 会改写报文源 IP让 FS 看到的所有对端地址都变成一个虚拟 IP后续 ACL 和来源识别全会出问题。LoadBalancer 也是类似逻辑云厂商的 LB 最好开启保留客户端源 IP 的模式。4.3 ACL 与 NAT 参数的协同FreeSWITCH 里有一套 ACL 机制用来判断一个 SIP 请求是来自内网还是外网进而决定是否对媒体流做 NAT 处理。光设置了 ext-rtp-ip 还不够如果 ACL 里把语音网关的地址段识别成了本地网段FS 就不会对媒体做地址重写SDP 里依然放内网 IP。常见的做法是把语音网关所在的 IP 段加进内网域list namelan defaultdeny node typeallow cidr10.0.0.0/8/ node typeallow cidr172.16.0.0/12/ node typeallow cidr192.168.0.0/16/ /list然后在 external profile 里开启 NAT 穿透相关参数param nameapply-nat-acl valuewan.auto/ param namenat-mode valueforce/apply-nat-aclwan.auto的含义是对来自非内网的请求应用 NAT 重写nat-modeforce则强制把 SDP 和 Contact 里的地址改写成 ext-rtp-ip 和 ext-sip-ip。这种配置组合在云环境、混合网络里非常可靠。4.4 RTP 端口区间的资源规划RTP 端口默认是 16384 到 32768每个媒体会话按偶数端口分配一路通话至少占 2 个 UDP 端口RTP 和 RTCP。常规情况下一个节点按 500 并发通话规划其实只需要 1000 个端口完全不必要把 1.6 万个端口全部暴露在防火墙上。可以通过 vars.xml 收窄端口范围X-PRE-PROCESS cmdset datartp-start-port20000/ X-PRE-PROCESS cmdset datartp-end-port30000/收窄到 20000-30000 意味着大约 5000 个端口可以用换算成 2500 路并发通话对一个单节点 FS 来说已经非常宽裕。端口范围越小防火墙和安全组规则越好管理攻面也越小。5. StatefulSet hostNetwork 实测部署方案5.1 为什么选 StatefulSet 而不是 Deployment前文已经讲到 FS 有状态那控制器选择上顺理成章用 StatefulSet。除了稳定的网络标识和有序滚动之外StatefulSet 另一个核心价值是volumeClaimTemplates——每个 Pod 自动获得一个独立的 PVC录音、CDR、配置持久化天然和 Pod 生命周期绑定。用 Deployment 管理 FS 的话PVC 只能手动预先创建Pod 重建后要手工重新挂载一旦节点迁移就会在存储层面卡住。我给生产环境准备的最小可用清单大致是这个思路StatefulSet 管理 Pod每个 FS Pod 通过 hostNetwork 直接绑定节点网络配上 podAntiAffinity 保证同一节点只有一个 FSPVC 挂载录音和 CDR 目录ConfigMap 覆盖关键配置文件。5.2 一套可以直接抄的 YAML 骨架下面这份清单是精简过的可运行版本关键点我都加了备注。实际生产里我会用 Helm 把这些资源参数化方便多环境复用。apiVersion: v1 kind: Service metadata: name: freeswitch-headless namespace: voice spec: clusterIP: None selector: app: freeswitch --- apiVersion: apps/v1 kind: StatefulSet metadata: name: freeswitch namespace: voice spec: serviceName: freeswitch-headless replicas: 2 podManagementPolicy: Parallel selector: matchLabels: app: freeswitch template: metadata: labels: app: freeswitch spec: hostNetwork: true terminationGracePeriodSeconds: 120 nodeSelector: role: voice affinity: podAntiAffinity: requiredDuringSchedulingIgnoredDuringExecution: - labelSelector: matchExpressions: - key: app operator: In values: [freeswitch] topologyKey: kubernetes.io/hostname initContainers: - name: init-default-config image: myregistry/freeswitch:1.10.10 command: - /bin/sh - -c - | if [ ! -d /freeswitch-config/scripts ]; then cp -a /etc/freeswitch/. /freeswitch-config/ fi volumeMounts: - name: fs-config mountPath: /freeswitch-config containers: - name: freeswitch image: myregistry/freeswitch:1.10.10 imagePullPolicy: IfNotPresent ports: - containerPort: 5060 protocol: UDP - containerPort: 5060 protocol: TCP - containerPort: 5080 protocol: UDP - containerPort: 8021 protocol: TCP resources: requests: cpu: 2 memory: 2Gi limits: cpu: 2 memory: 4Gi securityContext: runAsUser: 998 capabilities: add: - SYS_NICE readinessProbe: exec: command: - /usr/local/freeswitch/bin/fs_cli - -t - 2000 - -x - sofia status initialDelaySeconds: 30 periodSeconds: 10 timeoutSeconds: 5 livenessProbe: exec: command: - /usr/local/freeswitch/bin/fs_cli - -t - 2000 - -x - status initialDelaySeconds: 60 periodSeconds: 30 timeoutSeconds: 10 lifecycle: preStop: exec: command: - /bin/sh - -c - fs_cli -t 5000 -x shutdown || true volumeMounts: - name: fs-config mountPath: /etc/freeswitch - name: recording mountPath: /var/lib/freeswitch/recordings - name: cdr mountPath: /var/lib/freeswitch/cdr - name: config-overlay mountPath: /etc/freeswitch/vars.xml subPath: vars.xml readOnly: true - name: config-overlay mountPath: /etc/freeswitch/sip_profiles/external.xml subPath: external.xml readOnly: true - name: config-overlay mountPath: /etc/freeswitch/autoload_configs/event_socket.conf.xml subPath: event_socket.conf.xml readOnly: true volumes: - name: fs-config emptyDir: {} - name: config-overlay configMap: name: freeswitch-config-overlay volumeClaimTemplates: - metadata: name: recording spec: accessModes: [ReadWriteOnce] resources: requests: storage: 200Gi - metadata: name: cdr spec: accessModes: [ReadWriteOnce] resources: requests: storage: 50Gi这份清单里有几处细节我要专门解释一下。init-default-config这个 InitContainer 先把镜像内默认配置拷到一个 emptyDir 上然后主容器把整个/etc/freeswitch指向这个 volume这样主容器启动时就有了完整可用的配置树同时 ConfigMap 的三个文件通过 subPath 方式覆盖到具体路径相互不冲突。资源配额上requests和limits的 CPU 我故意设成了相同值 2 核让 Pod 进入 Guaranteed QoS 级别配合 CPU Manager static 可以实现核级别的隔离。SYS_NICEcapability 是让 FS 能调用nice()调整自身线程优先级对媒体实时性有帮助。5.3 部署完成后的一分钟验证部署完成后不要急着断言成功了先在事件 Socket 层面做最基本的健康检查kubectl exec -it statefulset/freeswitch -n voice -- /usr/local/freeswitch/bin/fs_cli -x status看到版本号、运行时间、session 计数正常输出再执行kubectl exec -it statefulset/freeswitch -n voice -- /usr/local/freeswitch/bin/fs_cli -x sofia status这一步会列出所有 SIP profile 的注册名、状态和监听地址。如果 external profile 显示RUNNING并且能看到实际绑定的节点 IP说明 SIP 信令面已经就绪。接下来用软电话或者 SIP 话机向该 IP:5060 发起注册应该一两秒内就能完成。注册成功后再做一次拨号测试确认媒体流能打通语音双向正常。6. 探针、优雅停机与持久化别让 kubelet 杀掉你的通话6.1 探针设计要保守别把繁忙当异常探针是 K8s 里最常见的 FS 杀手。很多团队从 nginx 迁移过来习惯性用 HTTP 探针但 FreeSWITCH 默认没有 HTTP 健康检查端点探针自然会写错。即使改用 exec 探针也要小心livenessProbe并不适合做功能完整性检查它只需要回答一个问题——进程是否还活着。我在生产里用的探针策略是readiness 检查 event socket 是否响应、Sofia profile 是否加载完成也就是执行fs_cli -x sofia statusliveness 只检查 FS 主进程是否存活用fs_cli -x status就够。两个探针的超时时间都给了 5 秒以上并且periodSeconds压到 10 到 30 秒的梯度。呼叫高峰时 CPU 打满FS 对 fs_cli 的响应会变慢如果探针 timeout 设得太短比如 1 秒kubelet 会判定 Pod 不健康然后强制重启这一下可能直接把几百路通话全部挂断。6.2 preStop 与 terminationGracePeriodSeconds 的正确配合Pod 被删除或者被重新调度时kubelet 会先执行 preStop Hook再等待 terminationGracePeriodSeconds 秒数最后才发 SIGKILL。FS 不是那种可以随时被杀掉的应用所以我把终止宽限期设成了 120 秒并在 preStop 里执行fs_cli -x shutdown。这个 shutdown 会让 FreeSWITCH 进入正常关闭流程对正在进行的通话发送 BYE 消息跟对端协商好通道释放再收拾录音文件、写 CDR 落盘最后退出进程。如果没有这层优雅关闭kubelet 会在几十秒后直接 SIGKILL通话对端收到的可能只是网络超时录音文件也可能只写了一半。有个细节要提醒preStop 里的fs_cli -x shutdown执行完后容器主进程已经在退出过程中这时候如果 Kubernetes 还在等待进程完全终止不会有什么负面影响。但要注意 fs_cli 连接不上 event socket 的情况比如 FS 已经僵死所以我在命令后面加了|| true避免 preStop 一直卡着导致 Pod 无法完成删除。6.3 持久化布局录音、CDR、配置分开存放FS 的持久化数据主要就三类录音文件、CDR 话单、配置文件。录音文件和 CDR 对存储要求不一样录音是大文件流式写入CDR 是小文件高频写入混在一起容易让备份策略和 IO 规划变得模糊。我在清单里用两个 volumeClaimTemplates 分开挂载/var/lib/freeswitch/recordings挂 200GB/var/lib/freeswitch/cdr挂 50GB。如果用的是云盘或者分布式存储可以按不同性能档位配置。配置文件则走 ConfigMap 覆盖——严格说它不算持久化数据但它决定了 FS 的行为必须跟代码一样纳入版本管理。有一个现实问题要注意PVC 的ReadWriteOnce模式决定了它只能被一个节点挂载这正好和一个节点一个 FS 实例的调度策略匹配。如果哪天你确实要支持某实例在多个节点间漂移存储层就得换成支持 ReadWriteMany 的 NAS 类产品同时也会带来并发写冲突的风险一般不建议这么干。7. 扩容缩容与日常运维通话不掉线的秘诀7.1 多实例注册数据共享是扩容的第一道门槛很多团队扩容 FS 后会发现一部分用户死活注册不上或者注册成功了但打不通电话根源在于 FreeSWITCH 默认把注册信息存在本地 SQLite 里两个实例之间的注册表完全不互通。要支持多实例且让注册和通话都在同一个实例上闭环有三个方向可以用。第一把 FS 改成媒体服务器角色全部注册由前端的 OpenSIPS/Kamailio 这类 SIP Proxy 处理FS 只负责被转来的呼叫做媒体处理这种情况下扩缩容几乎无脑因为代理层会负责负载均衡。第二让 FS 使用外部数据库保存注册信息通过 mod_odbc 连 MySQL/PostgreSQL多实例共享注册表架构上要额外维护数据库的可用性。第三最省事但最受局限的方案用 DNS SRV 或者负载均衡把同一个域名的注册请求按实例权重分发每个实例各管各注册表但不保证注册在 A、呼叫过来时恰好还是 A。我这里实际生产中用的是第三类加上一点启发式规则通过 SIP 客户端的 sticky session 把同一注册源固定分配到同一个后端实例。但如果客户端会定期切换 IP或者负载均衡器不支持 UDP 会话保持这条路会走得很辛苦。如果有条件我更推荐第一类架构虽然要多持有一个代理层但把无状态和状态分离之后K8s 对 FS 的调度约束会小很多。7.2 指标采集与告警Event Socket PrometheusFreeSWITCH 本身不直接输出 Prometheus 格式指标但可以通过 mod_event_socket 暴露控制接口社区也有现成的 freeswitch_exporter它通过 ESL 协议连上 8021 端口周期性执行status、show calls、sofia status等命令把会话数、注册数、CPU 负载、网络丢包等关键指标转成 metrics。部署时我会把 exporter 作为 sidecar 容器放进同一个 Pod这样它可以共享 localhost 的 event socket不需要额外暴露端口。告警规则上有几条比较实用freeswitch_channel_count超过节点容量的 80% 时告警提前观察扩容阈值freeswitch_registration_count出现断崖式下跌大概率是某个实例被重启了探针连续失败kubelet 层面已有重启动作要立刻通知值班人员。FS 的运行日志我建议让进程前台运行输出到 stdout然后由集群级别的日志采集Loki Promtail 或者 ES 系统一收集。-nf启动参数让日志直接打到标准输出省掉了挂载日志文件的麻烦。7.3 排障心法先看网络再看配置最后查资源在 K8s 上排 FS 的问题我自己的排查顺序基本固定踩了多少次坑总结出来的规律。第一步是抓包或者看连接状态。FS 部署在 hostNetwork 模式下直接在节点上tcpdump -i any -s 0 port 5060看 SIP 信令有没有进来没有信令就先检查防火墙、安全组、节点路由别在 FS 配置里瞎折腾。第二步确认 FS 认为的对端 IP 是不是真实来源用fs_cli -x sofia status profile external看已注册会话的 contact 地址如果看到的 IP 跟实际客户端不一致百分之九十九是 NAT 配置或者负载均衡的 SNAT 问题。第三步才是看配置和资源比如 dialplan 是否匹配、网关认证是否通过、节点负载是否打满。有一次线上排查花了我整整两天症状是部分用户间歇性掉线。后来抓包发现某个节点的 SIP 报文里有大量重传节点 CPU 只有 20%但网络延迟抖得厉害。定位到是同一个节点上另一个容器的日志采集组件在高峰期抢占 CPU 导致网卡中断延迟变大。加了资源配额和 CPU 隔离之后问题立刻消失。这就是为什么我一直强调 FS 节点最好打上专用标签跟其他负载物理或逻辑隔离。结尾的一点个人体会把 FreeSWITCH 搬到 Kubernetes 上跟部署一个 Web 服务完全是两种心态。Kubernetes 不会改变 FS 实时性、有状态这些本质它只是让部署、发布、扩容这些运维动作变得可声明、可回滚、可重复。回过头看最有价值的反而不是那几份 YAML而是终于把端口怎么通、媒体怎么流、数据怎么存、挂了怎么退这些原本写在运维脑子和文档里的经验沉淀成了可审查的代码。后面如果再往前走一步我会把 OpenSIPS 这一层也加进去让注册与媒体处理彻底解耦到时候 FS 在 K8s 上就是一个真正可以随便扩缩的媒体计算节点了。
返回列表