Files

LiveKit + livekit-sip AI 外呼底座 — 部署与运维文档

目标机器: 39.106.106.246 (阿里云 2vCPU/3.5G, Debian 13) · 交付物: /opt/livekit-sip/ 项目: https://github.com/livekit/livekit · 插件: https://github.com/livekit/sip 范围: 仅外呼 (CreateSIPParticipant 工作流), Mock SIP 运营商 + Mock LLM/ASR, 已端到端验证双向语音。

1. 水平扩展结论 (先行)

结论: 共享 Redis 的无状态水平复制 —— livekit-server 与 livekit-sip 均不在本机存呼叫状态, 扩容 = 加副本, 无需协调器/迁移。

状态存放 扩容方式 调度机制
livekit-sip 网关 Redis (trunk/呼叫状态) + 各自持有 RTP 流 复制 service 块 (独立端口段) CreateSIPParticipant 经 Redis 随机派发到任一网关节点
livekit-server Redis (rooms/room_node_map/nodes) 加节点, 客户端连任一节点 房间归属节点经 Redis 自动路由, 跨节点媒体互通
AI Agent (生产) 无状态 worker --scale/K8s HPA AgentDispatch 按 agent_name 派发

最快扩容路径: docker compose 里复制一个 sipN service 块 (改宿主端口映射即可, 容器网内 5060/10000-10200 天然隔离); K8s 场景用 Deployment + NodePort/hostPort 固定 SIP+RTP 端口段。实测从 1 网关扩到 2 网关零配置变更 (共用同一 sip.yaml), 呼叫自动分摊 (见 §7)。

瓶颈与边界: 单网关受 CPU (RTP 打包/转发) 与 RTP 端口段限制; 本文配置 200 端口/网关, 实际每呼叫占 1 个 UDP mux 端口, 单节点并发上限≈端口数。LiveKit 官方口径单节点(4C8G)数百路通话; 水平扩展后容量线性叠加, 上限在 Redis 与出口带宽。

2. 架构

                          ┌────────────────────────── docker net: lksip ──────────────────────────┐
 lk CLI / 业务后端        │  ┌──────────┐   ws    ┌─────────────┐    ┌─────────────┐               │
 (host, :7880 API) ───────┼─►│ livekit  │◄───────►│ livekit2    │    │ redis       │               │
 CreateSIPParticipant     │  │ :7880    │  Redis  │ :7883(multinode)│ │ :6379       │               │
        │  trunk/呼叫状态 │  └────┬─────┘  集群    └─────────────┘    └──────┬──────┘               │
        ▼  全部经 Redis    │       │ RTC(ICE 50100-50200/udp)               │ 状态存储              │
 ┌─────────────┐          │  ┌────▼──────────┐  ┌──────────────┐            │                      │
 │ redis       │◄─────────┼──┤ sip1 (网关)   │  │ sip2 (网关)  │──► 随机被派发 CreateSIPParticipant│
 └─────────────┘          │  │ 5060/udp+tcp │  │ 5060/udp+tcp │            │                      │
                          │  │ RTP 10000-   │  │ RTP 10000-   │            │                      │
 mock-agent (host)        │  │ 10200/udp    │  │ 10200/udp    │            │                      │
 订阅=Mock ASR            │  └──────┬───────┘  └──────┬───────┘            │                      │
 发布=Mock LLM/TTS ───────┼─────────┘   SIP INVITE / RTP(PCMU)             │                      │
                          │            └──────────►┌────────────────┐      │                      │
                          │                        │ mock-provider  │──────┘ (SIP UAS, 440Hz 音) │
                          │                        │ 5060/udp 40000 │  纯 stdlib Python        │
                          │                        └────────────────┘                             │
                          └───────────────────────────────────────────────────────────────────────┘

外呼信令流 (仅出站):

业务后端 → lk sip participant create --trunk ST_xxx --call +1510... 
        → LiveKit API → Redis 派发到某 sip 网关
        → INVITE sip:+1510...@mock-provider → 100/180 → 200 OK(SDP, PCMU) → ACK
        → 被叫以 SIP participant 身份进房, 与房间内 agent(=AI) 双向 RTP

3. 端口规划

端口 组件 说明
7880/tcp livekit (node1) HTTP/WS API, 宿主映射 7880
7883/tcp→7880 livekit2 (node2) 同一 Redis, 双节点集群
50100-50200/udp livekit ×2 RTC ICE (各容器独立 netns 不冲突)
5060/udp+tcp sip1/sip2 SIP 信令 (容器网内, 未映射宿主)
10000-10200/udp sip1/sip2 RTP 媒体 (同上)
8081/8082 sip1/sip2 health /healthz + / (HTTP 200)
9091/9092 sip1/sip2 prometheus /metrics
5060/udp, 40000/udp mock-provider Mock 运营商 SIP+RTP

生产外网仅需放行: SIP 5060/udp+tcp (+5061/tls) 与 RTP 段; LiveKit 客户端走 7880 + RTC 段。

4. 从零部署步骤 (全部实测)

# ── 4.1 Docker (Debian 13, 官方源; 国内需配 mirror) ─────────────────────
apt-get update && apt-get install -y ca-certificates curl gnupg
install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/debian/gpg -o /etc/apt/keyrings/docker.asc
echo "deb [arch=amd64 signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/debian trixie stable" \
  > /etc/apt/sources.list.d/docker.list
apt-get update && apt-get install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
# ⚠ 踩坑1(Debian 13): 安装后 docker group 缺失 → docker.socket 216/GROUP; 需 groupadd docker
# ⚠ 踩坑2(最小系统): 无 iptables → bridge 驱动失败 "iptables not found"; 需 apt install iptables nftables
cat > /etc/docker/daemon.json <<'EOF'
{ "registry-mirrors": ["https://docker.m.daocloud.io", "https://docker.1ms.run"] }
EOF
systemctl restart docker

# ── 4.2 内核调优 (见 §8, 已应用 /etc/sysctl.d/99-livekit-sip.conf) ─────

# ── 4.3 部署栈 ─────────────────────────────────────────────────────────
mkdir -p /opt/livekit-sip && cd /opt/livekit-sip   # = 仓库 deploy/ 目录
docker compose up -d                                # redis + livekit + sip1 + sip2 + mock-provider
docker compose --profile multinode up -d livekit2   # (可选) LiveKit 第2节点

# ── 4.4 Mock agent 依赖 (host python) ──────────────────────────────────
apt-get install -y python3-venv
python3 -m venv /opt/lk-venv
/opt/lk-venv/bin/pip install -i https://mirrors.aliyun.com/pypi/simple/ livekit livekit-api
# ⚠ 踩坑3: SDK 1.x 拆包 — rtc 在 `livekit`, AccessToken 在 `livekit-api`;
#          grants 必须传 VideoGrants() 对象; frame.data 是 buffer 非 bytes。

# ── 4.5 建 trunk + 发起外呼 ────────────────────────────────────────────
./scripts/call.sh <房间> <被叫E.164> [时长秒]   # 首次自动创建 trunk 并缓存于 Redis

trunk 定义 (mock/trunk.json, 换真实运营商时改 address/auth):

{ "trunk": { "name": "mock-provider", "address": "mock-provider", "numbers": ["+15105550123"] } }

5. 外呼链路验证 (实测输出)

./scripts/call.sh test-room-5 +15105550123 12:

== 2. Mock agent 加入房间 test-room-5 (发布TTS音, 订阅被叫音频)
[agent] joined room=test-room-5 peers=[]
[agent] publishing 440Hz tone (500ms on / 500ms off)
== 3. 发起 SIP 外呼 -> +15105550123
[agent] subscribed: callee-30395 track=TR_AMu2wATzwBLwqP
SIPCallID: SCL_LEGvhUjpz3N9      ← CreateSIPParticipant 成功, 被叫应答进房
== 4. 等待 agent 结束并输出双向统计
[agent] RX frames=1000 avg_rms=4667
RESULT room=test-room-5 rx_frames=1015 rx_avg_rms=4640 bidirectional=YES

mock-provider (被叫侧) 日志:

INCOMING_INVITE from=172.18.0.6:5060 uri="sip:+15105550123@mock-provider"
ANSWERED  media=172.18.0.3:40000 codec=PCMU peer=172.18.0.6:10034   ← SDP 协商 PCMU
ACK confirmed, RTP bridging
CALL_DONE sent_rtp=… recv_rtp=… recv_avg_amplitude=…                 ← 双向 RTP 计数

双向语音证明: agent RX=被叫→ASR 方向 (1015 帧, RMS 4640 = 440Hz 检测到); provider recv = LLM/TTS→被叫方向 (RTP 计数>0)。换真实 LLM/ASR 只需替换 mock-agent 内的发布/消费循环 (livekit-agents 框架同接口)。

6. 运行状态检测

./scripts/health.sh (实测输出):

== 容器状态        lk-livekit2/lk-sip1/lk-sip2/lk-livekit/lk-redis/lk-mock-provider  Up
== HTTP 健康       PASS livekit(200) PASS sip1(200) PASS sip2(200)   # sip /healthz
== API 功能        PASS lk room list   PASS lk sip outbound list
result: ok=5 fail=0

持续监控: watch -n5 ./scripts/health.sh; 指标: curl :9091/metrics, curl :9092/metrics (prometheus, 含活动呼叫/错误); 呼叫级状态: provider 容器日志 CALL_DONE 行; 房间级: lk room list

7. 水平扩展实证 (同机双网关 + 双 LiveKit 节点)

① 双 LiveKit 节点集群 (共 Redis, 自动发现)

$ docker exec lk-redis redis-cli hlen nodes
2                                     ← node1 ND_BwShgqdiB8AQ + node2 ND_moj9RpSqq5Ap

并发验证中 agent 全部连 node2(:7883), CreateSIPParticipant 经 node1(:7880), 同房媒体互通 → 跨节点路由成立 (RESULT 见 ③)。

② 双 SIP 网关随机分摊 (无状态验证)

$ docker logs lk-sip1 | grep -oE '"room": "burst-[0-9]"' | sort | uniq -c
     15 "room": "burst-1"             ← sip1 处理 burst-1
$ docker logs lk-sip2 | ... 
     15 "room": "burst-2"             ← sip2 处理 burst-2
     15 "room": "burst-3"             ←      及 burst-3
$ docker logs lk-mock-provider | grep INCOMING | tail -3
from=172.18.0.5:5060  call_id=lIIip…  ← 两个来源 IP = sip1/sip2
from=172.18.0.6:5060  call_id=mw4Fq…
from=172.18.0.5:5060  call_id=BCVgl…

③ 3 路并发外呼全部双向 (agent 连 node2)

RESULT room=burst-1 rx_frames=810 rx_avg_rms=3162 bidirectional=YES
RESULT room=burst-2 rx_frames=866 rx_avg_rms=3066 bidirectional=YES
RESULT room=burst-3 rx_frames=778 rx_avg_rms=2228 bidirectional=YES

→ 加副本即扩容、Redis 自动调度、跨节点房间互通, 三项均实证。资源基线 (3 并发): livekit 2×6%CPU/36M, sip 2×20%CPU/30M, redis 1%/8M。

8. 系统调优

已应用 (/etc/sysctl.d/99-livekit-sip.conf, sysctl --system 生效):

net.core.rmem_max = 16777216          # UDP 收缓冲上限 (pion/WebRTC 推荐 16M; 默认 208K 高并发必丢包)
net.core.wmem_max = 16777216
net.core.rmem_default = 1048576
net.core.wmem_default = 1048576
net.core.netdev_max_backlog = 4096    # 网卡积压队列 (默认 1000)
net.ipv4.ip_local_port_range = 10000 65000   # 出向 SIP/RTP 源端口 (含 RTP 段)
net.ipv4.udp_mem = 8388608 12582912 16777216 # 全局 UDP 页缓存 (3.5G 内存安全值)
net.ipv4.udp_rmem_min/wmem_min = 16384

调优前基线: UdpRcvbufErrors=0 (3 并发, 默认缓冲已够); 上述参数为高并发 (>100 路) 预置。验证命令: nstat -az | grep -i udp

组件级:

  • sip 网关: rtp_port: 10000-10200 (每节点 200 并发余量); max_cpu_utilization: 0.9 默认即可, 超 90% 自动拒绝新呼叫 (背压); log_level: info 生产勿用 debug。
  • livekit: RTC 段 50100-50200/节点; 房间自动关闭 (empty timeout) 回收端口; logging.json: true 便于采集。
  • redis: 纯缓存用法 (--save ""), trunk/房间状态可丢 (重建即恢复); 若把 Redis 当持久层需开 AOF。
  • 容器: restart: unless-stopped 已配; 单机 3.5G 下建议给 livekit/sip 设 mem limit (如 512M) 防互挤。
  • 监控: 9091/9092 /metrics 接 Prometheus; 告警项: sip 活动呼叫数骤降、livekit 5xx、UdpRcvbufErrors>0、容器重启。

9. 生产化清单 (Mock → 真实运营商)

  1. trunk: address 换运营商 SIP 域名/IP, 加 auth_username/auth_password, 号码用 E.164; 我方 5060/udp+tcp、RTP 段需公网放行 (阿里云安全组)。
  2. NAT: 阿里云 ECS 为 1:1 NAT, sip.yaml 改 use_external_ip: true (STUN 自动探测公网 IP 写入 SDP), livekit.yaml 同理; 或显式 nat_1_to_1_ip
  3. 安全: SIP over TLS (tls.port: 5061) + SRTP (media_enc), 或 IP 白名单 ACL; LiveKit API key 轮换。
  4. AI 侧: mock-agent 替换为 livekit-agents 框架 (同房间/同 track 接口), STT/LLM/TTS 三段插拔。
  5. 高可用: Redis 单点 → 云 Redis 主从; 网关 ≥2 跨宿主机; LiveKit 节点 ≥2 (本文已验证集群模式)。