Files

199 lines
13 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 (本文已验证集群模式)。