技术分享会结束后,走廊里有人追上来问了一个问题。
会上,某个团队展示了他们用 KCP 优化控制信号的成果:在 4G 网络、2% 丢包的条件下,P99 延迟从 TCP 的 450ms 降到了 180ms。曲线漂亮,结论简洁——"KCP 已经满足云控 <250ms 的 P99 要求"。
追问的那个工程师是这样说的:"你们测试时,4G 卡切了基站没有?"
沉默了几秒。
"没有,是模拟的静态丢包。"
"那如果基站切换,4G 信号中断 5 秒,KCP 的数据怎么办?"
这个问题没有当场回答。这篇文章是那个问题的完整答案——不是 KCP 不好,而是那次测试有一个关键的隐藏假设,而这个假设在云控真实场景下通常不成立。
第一章:KCP 是什么,为什么在低延迟场景受欢迎
1.1 KCP 的设计目标
KCP(快速可靠传输协议)是 skywind3000 开发的一个纯应用层 ARQ(自动重传请求)协议,2019 年开源。设计目标非常明确:以 10%-20% 的带宽额外消耗,换取比 TCP 低 30%-50% 的平均延迟。
实现方式是对 TCP 慢的两个根源动手:
根源一:RTO 计算保守。TCP 的重传超时(RTO)有 200ms 的最小值(Linux 内核 TCP_RTO_MIN),加上退避算法,一次丢包导致的延迟可以达到 200ms 到几秒。KCP 在 nodelay=0 时最小 RTO 是 100ms,nodelay=1 时降到 30ms(源码常量 IKCP_RTO_NDL=30);同时退避策略从"近似翻倍"变成"加半",避免 RTO 指数爆炸。interval=10 只是 flush 检查频率,不是最小 RTO。
根源二:等待三个 SACK 再重传。TCP 的快速重传需要收到 3 个重复 ACK,才认定包丢失并重传。KCP 在 resend=2 模式下,只需要 2 个 ACK gap(后面的包先收到了)就触发重传。
这两个改动让 KCP 在单路径、静态丢包的场景下表现出色。那次分享的 2% 丢包 P99=180ms,是 KCP 调优的真实能力,数据没有造假。
1.2 搭建基准测试环境
在讨论延迟对比之前,先把测试环境固定下来。后续所有数据基于以下 netem 配置,参数来自真实 4G/5G 网络统计。
# 清理现有的 netem 配置
sudo tc qdisc del dev eth0 root 2>/dev/null
sudo tc qdisc del dev eth1 root 2>/dev/null
# 配置模拟 4G 网络(eth0,主路径)
# 40ms 单程延迟(RTT ≈ 80ms),2% 随机丢包,100ms 延迟抖动范围
sudo tc qdisc add dev eth0 root netem \
delay 40ms 10ms distribution normal \
loss random 2%
# 配置模拟 5G 网络(eth1,备路径)
# 25ms 单程延迟(RTT ≈ 50ms),1% 随机丢包
sudo tc qdisc add dev eth1 root netem \
delay 25ms 8ms distribution normal \
loss random 1%
# 验证配置
sudo tc qdisc show dev eth0
sudo tc qdisc show dev eth1
# 用 ping 确认 RTT 基准
ping -c 50 -i 0.1 <target_ip_via_eth0> | tail -2
ping -c 50 -i 0.1 <target_ip_via_eth1> | tail -2
第二章:KCP 的延迟机制与带宽代价
2.1 KCP 快速重传的工作原理
当一个包在网络中丢失时,发送方的选择:等待还是重传?等多久?
KCP 的核心配置(nodelay=1, interval=10, resend=2, nc=1)对应的行为:
nodelay=1:影响两件事——(1)最小 RTO 从 100ms 降到 30ms(rx_minrto = IKCP_RTO_NDL = 30);(2)超时重传的退避策略从"近似翻倍"(rto += max(rto, rx_rto))变成"加半"(rto += rto/2)。注意:KCP 本身没有 ACK 延迟机制,ACK 在下次 flush 时批量发出,nodelay 不影响 ACK 发送时机。
interval=10:flush 检查间隔 10ms,决定 KCP 多久扫描一次发送队列。这影响了重传触发的时钟精度——超时重传最多晚 interval/2 发出,而不是 TCP 的 200ms 最小等待。
resend=2:快速重传阈值。如果发出的包 N 后面的包 N+1、N+2 已经被 ACK,但 N 的 ACK 还没来,KCP 认为 N 可能丢了,触发重传。阈值越低,越激进。
nc=1:不做流量控制(no congestion control)。KCP 原版不做拥塞控制,发送方按固定窗口大小发包,不根据网络状况动态调整。
一次丢包的处理时序(nodelay=1, interval=10, resend=2, RTT=80ms):
t=0: 发送包 N
t=40ms: 包 N 在网络中丢失(或被丢弃)
t=80ms: 包 N+1 的 ACK 回来(N+1 没丢),fastack[N]++ = 1
t=88ms: 包 N+2 的 ACK 回来,fastack[N]++ = 2,达到 resend=2 阈值
但快速重传不是"立刻"发出——要等到下一次 flush 才发
最坏情况:下次 flush 在 t=88+10=98ms,即额外最多 10ms 延迟
t=98ms: flush 扫描到 fastack[N]>=resend,重发包 N
t=138ms: 重发的包 N 到达接收方
t=140ms: 接收方应用层收到包 N 的数据
总延迟:约 138-140ms(首次发出到最终交付)
注意:实际重传时机是"ACK 触发快速重传判定 + 等到下次 flush",不是收到 ACK 立刻重传。interval=10ms 是 flush 检查频率,引入最多 10ms 的额外抖动。fast3 模式(interval=10ms)把这个抖动控制在 10ms 以内。
相比之下,TCP 的 RTO(最小 200ms)情况下,同样的包从 t=0 到应用层收到约需 350-450ms。KCP 的优势确实显著。
2.2 KCP 的带宽代价
KCP 的带宽放大是动态的,随丢包率升高而增加。估算:
无丢包情况:ACK 无延迟(ACK 包数量增加),加上滑动窗口可能发一些预防性重传。带宽放大约 1.1-1.2 倍。
2% 丢包:每 50 个包重传 1 个,加上 ACK overhead。带宽放大约 1.5-1.7 倍。
5% 丢包:每 20 个包重传 1 个,触发更多快速重传,部分场景下重传一个包多次。带宽放大约 2.0-2.5 倍。
重要:KCP 的带宽代价随丢包率非线性增长。在丢包严重时,KCP 会陷入"重传激活 → 更多包发出 → 更多丢包 → 更多重传"的正反馈循环。这是 KCP 在高丢包场景表现退化的原因之一。
# 用 tcpdump 观察 KCP 的 ACK 发包频率
tcpdump -i eth0 'udp port <kcp_port>' -l -q 2>/dev/null | \
awk '{count++} NR%100==0 {print NR/100 "s: " count " pkts/100lines", count/100 " pps"; count=0}'
# 对比 QUIC 的 ACK 频率(设置 QUIC 端口)
tcpdump -i eth0 'udp port 443' -l -q 2>/dev/null | \
awk '{count++} NR%100==0 {print NR/100 "s: " count " pkts/100lines"; count=0}'
2.3 关键定性结论:KCP 是单路径加速器
用一句话概括 KCP 的本质:KCP 是在单路径上,用更多带宽换更快的重传响应,从而降低丢包带来的延迟代价。
这里有一个隐含前提:路径存在。KCP 的所有优化都建立在"有一条路径,这条路径偶尔丢包"的假设上。当这条路径完全中断时,KCP 的所有快速重传机制都没有用——没有路,重传多快都发不出去。
这个隐含前提,正是那次技术分享里没有测试到的场景。
第三章:MP-QUIC 冗余发送的设计哲学
3.1 冗余发送的核心思路
冗余发送(Redundant Scheduling)的逻辑非常直接:
T-Box 发送一条控制指令
│
├── 路径 A(4G,RTT≈80ms)→ 发送一份
└── 路径 B(5G,RTT≈50ms)→ 发送同一份
│
↓
云控网关:先收到哪份用哪份,丢弃重复
接收方不需要等待两份都到,哪份先到就处理哪份。另一份到了之后,接收方通过 STREAM 层的 offset 区间表去重——QUIC 每条路径有独立的 Packet Number 空间,去重不能靠 PN,而是靠 STREAM 数据偏移量(offset)的区间记录,重复区间的数据直接丢弃。
3.2 延迟的数学
冗余发送的延迟由先到的那条路径决定,而不是两条路径的平均值或最大值。
设路径 A 的单次延迟(单程)为随机变量 ,路径 B 的单次延迟为 ,且两条路径相互独立(不同运营商的 4G/5G 链路)。
端到端延迟
期望值: 且 。即使路径 B 平均比路径 A 快,冗余发送的平均延迟也不会退化到更慢的那条路径。
P99 分位数:
如果路径 A 的 P99 是 150ms,路径 B 的 P99 是 120ms:
- (5G 比 4G 更快)
这意味着:冗余发送的 P99 不是 150ms,大约在 80-100ms(取决于具体分布)。P99 显著低于单路径。
关键区别:KCP 是"丢了补送,让一次传输尽量快";冗余发送是"同时用两条路,让一次传输大概率先到"。这是定性的架构区别,不只是优化程度的差异。
3.3 带宽代价是固定的
冗余发送的带宽代价是固定的:恰好 2 倍,无论丢包率是 0% 还是 10%。
这与 KCP 的动态带宽代价形成对比:KCP 在丢包率高时带宽放大可达 2-3 倍,而且还不能保证 P99 满足要求;冗余发送在任何丢包率下都是固定 2 倍,但 P99 不受单路径丢包率影响。
控制信号的带宽消耗:50 条/秒 × 600 字节/条 × 2(冗余)= 48Kbps。4G 上行典型 2-20Mbps,48Kbps 只占 0.24%-2.4%。这个代价在控制信号场景完全可接受。
# 用 tcpdump 验证冗余发送(同一个 QUIC packet number 出现在两个网卡)
# 同时抓两个网卡的 QUIC 流量
tcpdump -i eth0 'udp port 443' -w /tmp/path_a.pcap &
PID_A=$!
tcpdump -i eth1 'udp port 443' -w /tmp/path_b.pcap &
PID_B=$!
sleep 10
kill $PID_A $PID_B
# 统计两个抓包文件中的包数量
echo "路径A包数:" && tcpdump -r /tmp/path_a.pcap -q 2>/dev/null | wc -l
echo "路径B包数:" && tcpdump -r /tmp/path_b.pcap -q 2>/dev/null | wc -l
# 如果冗余发送生效,两个文件的包数应该接近(冗余包数量相同)
# 包大小分布也应该相似
第四章:四种场景的量化对比
数据说明:以下 P50/P99/P999 数据基于 kcptun 弱网实测(双向 netem,40Mbps 限速,sockperf ping-pong 测量),结合理论模型推算 P50/P999 分位。 实测基准:TCP RTT 约 60ms,KCP 对应 kcptun(ARQ 模式,datashard=3, parityshard=2),MP-QUIC 冗余对应 kcptun(FEC 1+1 冗余,datashard=1, parityshard=1)。文章中路径 RTT 设定为 80ms/50ms(4G/5G),与实测 60ms 基准存在约 20ms 系统偏移,数据已做对应校正。以下数据可用于定性比较两种方案的行为差异。
4.1 场景一:轻微丢包(0.5%,RTT=80ms)
这是"好日子"场景:信号不错,偶尔有点干扰。
| 指标 | KCP(nodelay=1, resend=2) | MP-QUIC 冗余发送 |
|---|---|---|
| P50(中位延迟) | 68ms | 45ms |
| P99 延迟 | 80ms | 52ms |
| P999 延迟 | 130ms | 80ms |
| 带宽放大 | 约 1.3x | 固定 2x |
在这个场景下,KCP 的 P99(80ms)和 QUIC 冗余(52ms)都满足 <250ms 的 SLA 要求。QUIC 冗余有优势,但差距不大(P99 差 28ms)。
如果丢包率就是 0.5%、没有切换中断、带宽也够用,KCP 是完全合理的选择。
# 配置场景一 netem
sudo tc qdisc change dev eth0 root netem delay 40ms 8ms distribution normal loss random 0.5%
sudo tc qdisc change dev eth1 root netem delay 25ms 5ms distribution normal loss random 0.3%
4.2 场景二:中等丢包(2%,RTT=80ms)
这是 4G 城市道路或高速公路的典型场景。
| 指标 | KCP | MP-QUIC 冗余发送 |
|---|---|---|
| P50 | 72ms | 46ms |
| P99 | 103ms | 52ms |
| P999 | 220ms | 85ms |
| 带宽放大 | 约 1.7x | 固定 2x |
这个场景是那次技术分享里的测试条件。KCP 的 P99 = 103ms——在 250ms SLA 下有充裕余量,看起来"够用"。但这是静态丢包的实验室条件:没有基站切换、没有信号波动。真实高速场景下,4G RTT 从 80ms 跳到 194ms 的瞬间,KCP 的 P99 会跟着飙升,余量瞬间消失。
QUIC 冗余的 P99 = 52ms,SLA 余量超过 190ms,对 RTT 抖动有充分缓冲。
P99 在静态测试中"够用",不代表在真实网络中"够用"——这是那次技术分享的核心盲点。
# 配置场景二 netem
sudo tc qdisc change dev eth0 root netem delay 40ms 10ms distribution normal loss random 2%
sudo tc qdisc change dev eth1 root netem delay 25ms 8ms distribution normal loss random 1.5%
4.3 场景三:高丢包(5%,RTT=100ms)
这是高速公路边缘区域、隧道出入口、或者信号弱的农村地区。
| 指标 | KCP(nodelay=1, nc=1) | QUIC 单路(BBR) | MP-QUIC 冗余发送 |
|---|---|---|---|
| P50 | 75ms | 80ms | 50ms |
| P99 | 105ms | 120ms | 55ms |
| P999 | 240ms | 280ms | 90ms |
| 带宽放大 | 约 2.5x | 约 1.3x | 固定 2x |
场景三有两个值得深挖的现象:KCP 在 5% 丢包下的 P99 并不崩溃,以及KCP 在 5% 附近反而比 QUIC 单路表现更好。
为什么 KCP 的 P99 在 5% 丢包下仍然稳住?
回到 KCP 的重传机制:5% 丢包意味着每 20 个包丢 1 个。KCP 的 resend=2 快速重传在收到 2 个后续 ACK 时立即触发,这通常在丢包后约 1 个 RTT 内完成(100ms),而不需要等 RTO(TCP 最小 200ms,且退避翻倍)。实测(kcptun datashard=3, parityshard=2)验证了这一点:
- P50(大多数包没丢):约 75ms,接近基础 RTT
- P99(约 1% 的包经历了一次重传):约 105ms,即基础 RTT + 1 次快速重传延迟
- P999(极少数包经历了两次重传或 RTO 超时):约 240ms
KCP 在 5% 丢包下的 P99 仍然满足 250ms SLA,这是 nc=1 + nodelay=1 组合的真实能力。
为什么在 5% 附近 KCP 比 QUIC 单路(BBR)略好?
从源码层面看,QUIC 的丢包检测阈值(INITIAL_PACKET_THRESHOLD = 3)比 KCP 的 resend=2 更保守——需要 3 个乱序包才触发快速重传,而 KCP 只需要 2 个。尽管 QUIC 同时有时间阈值检测(max(latest_rtt, smoothed_rtt) × 1.125)弥补了部分差距,但在 5% 这个丢包率下,KCP 的激进重传策略反而占优。
更关键的是拥塞控制策略的差异:QUIC 默认使用 BBR,BBR 基于带宽-延迟模型,不把随机丢包当作拥塞信号,cwnd 不会因为随机丢包而缩减。而 KCP 的 nc=1 也是禁用拥塞控制,两者在维持发送速率上殊途同归。但 KCP 的更低重传阈值(resend=2)让它在中等丢包率下抢先完成重传,P99 略低于 QUIC 单路。
但有一个关键前提:nc=1(禁用拥塞控制)在 5% 丢包是随机丢包(信号干扰)时有效;如果丢包是拥塞引起的,nc=1 会让 KCP 持续向已经拥塞的链路注入流量,形成"重传激活 → 更多拥塞丢包 → 更多重传"的正反馈循环,P99 会急剧恶化。4G/5G 信号干扰引起的丢包以随机丢包为主,因此 KCP 在这个场景下的表现是可靠的。
QUIC 冗余的 P99 = 55ms,比 KCP 的 105ms 和 QUIC 单路的 120ms 都有明显优势,而且 5G 路径的丢包率(我们设为 2.5%,比 4G 低)提供了额外保障。
注意带宽放大:场景三 KCP 的带宽放大达到 2.5 倍,代价不低。QUIC 单路仅约 1.3 倍(BBR 不重复发送,只精确重传丢失包)。但 KCP 的 P99 仍在 SLA 范围内——这是 KCP 的真实能力,不应低估。KCP 的真正短板不是高丢包下的延迟,而是路径中断(场景四)。
# 配置场景三 netem
sudo tc qdisc change dev eth0 root netem delay 50ms 15ms distribution normal loss random 5%
sudo tc qdisc change dev eth1 root netem delay 30ms 10ms distribution normal loss random 2.5%
4.4 场景四:路径切断(5 秒中断,模拟基站切换)
这是 KCP 和 QUIC 冗余发送最本质的差异点,也是那个走廊追问的核心。
测试方法:先建立稳定的 2% 丢包通信,然后把 eth0(4G 路径)模拟完全中断 5 秒,再恢复。
# 恢复到场景二基准(2% 丢包)
sudo tc qdisc change dev eth0 root netem delay 40ms 10ms distribution normal loss random 2%
sudo tc qdisc change dev eth1 root netem delay 25ms 8ms distribution normal loss random 1.5%
# 模拟 5 秒路径中断
echo "=== 制造路径 A 中断 ===" && date
sudo tc qdisc change dev eth0 root netem loss 100%
sleep 5
sudo tc qdisc change dev eth0 root netem delay 40ms 10ms distribution normal loss random 2%
echo "=== 路径 A 恢复 ===" && date
KCP(单路径 eth0)的表现:
中断期间(t=0 到 t=5s):所有包全部丢失。KCP 的重传机制在重发,但 loss=100% 的路径上重传也没用。5 秒内发出的所有控制指令,接收方都没有收到。
恢复后(t=5s 之后):KCP 的发送窗口有积压(5 秒内未 ACK 的包),会集中重传。接收方在几个 RTT 内收到大量乱序的重传包,需要重新排序。P99 延迟在路径恢复后持续高于基准值,持续 10-20 秒才恢复正常。
MP-QUIC 冗余发送的表现:
中断期间(t=0 到 t=5s):eth0(4G)路径中断,但 eth1(5G)路径不受影响,继续正常传输。P99 延迟从约 52ms 增加到约 45ms(5G 比 4G 更快,单独走 5G 延迟反而略低)。控制信号从未中断。
恢复后(t=5s 之后):eth0 路径恢复,冗余发送重新在两条路径上并发。P99 延迟恢复到 52ms。整个过程对应用层完全透明,没有延迟 spike。
这就是场景四给出的核心结论:KCP 无法处理路径中断,5 秒中断就是 5 秒数据丢失;MP-QUIC 冗余发送对路径中断无感知,只要两条路径不同时中断就没有影响。
而 T-Box 的 4G 基站切换,正是一个"单条路径短暂中断"的典型场景,时长通常 1-3 秒。
第五章:KCP 仍然有优势的场景
这一章很重要,必须写。如果全文都在说"KCP 不行,QUIC 好",读者会认为这是软文,失去信任。
5.1 低丢包 + 带宽紧张 + 单路径
KCP 在以下条件下比 QUIC 冗余发送更优:
条件一:丢包率长期 < 0.5%。在这个丢包率下,KCP 的重传触发很少,带宽放大约 1.2 倍,而 QUIC 冗余发送固定 2 倍。
条件二:上行带宽紧张。如果 T-Box 所在区域的 4G 上行带宽只有 500Kbps(农村边缘地区),控制信号 50Kbps × 2 = 100Kbps 占 20%,已经是可感知的比例。KCP 在低丢包下只需 60Kbps(1.2 倍),省出 40Kbps。
条件三:没有路径切换需求。如果车辆在固定区域运行(厂区、港口、特定路线),没有频繁基站切换,单路径 KCP 完全够用。
这种场景下,KCP 是更合理的选择。
5.2 遗留系统不想动架构
KCP 是纯应用层 ARQ,代码简单(核心代码不到 2000 行),集成只需要替换 send/recv 接口。没有握手,没有连接状态,没有流量控制,接口语义和 UDP 几乎一样。
相比之下,QUIC 集成需要处理:QUIC 握手(1-RTT/0-RTT)、连接状态机、stream 创建和关闭、事件循环(quic_endpoint_recv 等 API)、双路径初始化(MPQUIC 配置)。
如果一个团队的控制信号系统已经稳定运行两三年,KCP 调优过、P99 也在 SLA 边界内,没有路径切换中断诉求——那就没有必要动它。
5.3 加密是一个真实的开销
KCP 无加密,QUIC 强制 TLS 1.3(AEAD 加密)。在 ARM Cortex-A55 上:
AES-128-GCM 加密性能(带 ARM 硬件加速指令):
# 在 T-Box 上测量 AES-128-GCM 吞吐量
openssl speed -evp aes-128-gcm 2>&1 | grep "1024 bytes"
# 典型输出:约 200-400MB/s(ARM A55 带 AES 扩展指令)
控制信号场景(50 包/秒 × 600 字节):加密吞吐 30KB/s,A55 的 AES-GCM 在 200MB/s 的速度下处理这 30KB/s 只需 0.015ms/秒的 CPU 时间。基本可以忽略。
但如果场景扩展到大量并发连接(服务端接收 1000 辆 T-Box 的 QUIC 包),加密开销不可忽视。服务端选型时需要考虑。
第六章:选型建议
6.1 决策树:到底选哪个
用两个维度做决策:
维度一:是否有路径切换中断的需求?
车辆经常跑高速、经常穿越基站密集区、T-Box 信号会定期短暂中断?
如果是:MP-QUIC 冗余发送,无论丢包率如何。路径中断是 KCP 无法解决的根本问题。
维度二:丢包率通常在哪个水平?
长期 P99 丢包率 < 0.5%,而且路径稳定:KCP 够用。
丢包率在 1%-5% 之间:QUIC 冗余发送有明显优势,P99 能保持在 60ms 以内。
| 路径切换需求 | 丢包率 | 推荐方案 |
|---|---|---|
| 有(高速/城市) | 任意 | MP-QUIC 冗余发送 |
| 无 | <0.5% | KCP(带宽更省,P99 更低) |
| 无 | 0.5%-5%(随机丢包) | KCP 仍可用(P99≈80-105ms,SLA 内);QUIC 单路 P99 略高(≈100-120ms);QUIC 冗余余量最充裕(≈52-55ms) |
| 无 | 5%-10%(随机丢包) | KCP 略优于 QUIC 单路;QUIC 单路带宽代价更小(1.3x vs 2.5x) |
| 无 | >10%(随机丢包)或 任意(拥塞丢包) | QUIC 单路或 QUIC 冗余(KCP 连续丢包时快速重传失效,nc=1 在拥塞丢包下有正反馈风险) |
结尾
回到那个走廊里的问题:"如果基站切换,4G 信号中断 5 秒,KCP 的数据怎么办?"
答案现在很清楚:KCP 单路径,5 秒中断就是 5 秒数据丢失,恢复后还有 10-20 秒的延迟 spike。
但这个答案不代表 KCP 不好——它代表的是 KCP 设计目标的边界。KCP 是单路径加速器,用激进重传换低延迟,这个设计在低丢包、无切换的场景下是出色的。把 KCP 用在有频繁路径切换需求的场景,相当于用一辆跑车走越野路线,不是跑车的问题,是场景选型的问题。
MP-QUIC 冗余发送的优势,不在于在任何单一场景下都比 KCP 延迟更低,而在于它的延迟是确定的——不随丢包率上升而退化,不因路径中断而失效。对于 SLA 要求严格的云控控制信号,延迟的确定性比最低延迟更重要。
这是这篇文章最想说的一件事。