199 lines
13 KiB
Markdown
199 lines
13 KiB
Markdown
# 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. 从零部署步骤 (全部实测)
|
||
|
||
```bash
|
||
# ── 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):
|
||
```json
|
||
{ "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` 生效):
|
||
```conf
|
||
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 (本文已验证集群模式)。
|