手机同时连着 Wi-Fi 和 4G,笔记本同时插着有线网卡和 Wi-Fi——大多数情况下,系统只会选其中一条路,另一条闲着。传统 TCP 把「一条连接 = 一个四元组」写死在协议里,链路切换意味着连接断开重建。多路径 TCP(Multipath TCP, MPTCP)要打破这个限制:让一条连接同时跑在多条路径上,聚合带宽、秒级故障切换。
本文先讲 MPTCP 在协议栈中的位置与设计目标,再拆解子流如何建立与通告地址,然后重点讲调度器与耦合拥塞控制这两个决定「多路径到底有没有用」的机制,最后落到 Linux 内核参数、ip mptcp 配置与当前部署现状。
一、MPTCP 的动机与协议栈位置
1.1 一条连接、多条路径
MPTCP 在 TCP 之上加了一层「多路径管理层」:应用看到的仍是一个普通 socket,但底层由多条 TCP 子流(subflow)承载。每条子流有自己的四元组与拥塞窗口,MPTCP 层负责把应用字节流按序重组。
协议栈位置:
应用层(socket API,无感知)
-----------------------------
MPTCP 层(序号映射、调度、重传)
-----------------------------
TCP 子流 1 TCP 子流 2 TCP 子流 3
(Wi-Fi) (4G) (有线)
-----------------------------
IP 层
1.2 设计目标
MPTCP(RFC 8684)有三个明确目标:吞吐聚合(多条路径同时传,总带宽相加)、可靠性(一条路径断了,流量无缝切到另一条)、向后兼容(对不支持 MPTCP 的对端,退化为普通 TCP)。
| 目标 | 机制 | 收益 |
|---|---|---|
| 吞吐聚合 | 多子流并行传输 | 带宽叠加 |
| 可靠性 | 子流独立故障恢复 | 秒级切换、零中断 |
| 向后兼容 | MP_CAPABLE 协商降级 | 不破坏现有网络 |
| 资源利用 | 同时用多张网卡 | 不浪费空闲链路 |
1.3 与应用层多连接的差异
应用层也能开多条 TCP 连接做并行(如 HTTP 多连接),但那要求应用自己感知多路径并做重组,且每条连接是独立拥塞控制。MPTCP 把它下沉到传输层:应用零改动,且多子流共享同一个拥塞控制目标,对网络更友好。
应用层多连接 vs MPTCP:
应用层:应用需自己分片、重组、处理乱序
多条连接各自拥塞控制 → 对共享瓶颈不友好
MPTCP :内核透明,应用无感
耦合拥塞控制 → 公平共享瓶颈
二、子流建立与地址管理
2.1 MP_CAPABLE 握手
一条 MPTCP 连接的建立始于普通 TCP 三次握手,但在 TCP 选项里携带 MP_CAPABLE:双方交换密钥与能力标志,协商出连接级的 MPTCP 密钥,用于后续子流的身份认证。第一个子流就叫「初始子流」。
MP_CAPABLE 握手(在 TCP 选项里):
SYN : MP_CAPABLE + 客户端密钥 + flags
SYN/ACK : MP_CAPABLE + 服务端密钥 + flags
ACK : MP_CAPABLE + 双方密钥(确认)
→ 建立 MPTCP 连接级密钥,用于 MP_JOIN 认证
若任一方不支持 MP_CAPABLE → 退化为普通 TCP
2.2 MP_JOIN:追加子流
要增加一条新路径(如从 Wi-Fi 换到 4G 再开一条),客户端发起带 MP_JOIN 选项的握手,用连接级密钥派生的 HMAC 证明自己属于同一条 MPTCP 连接。服务端验证通过后,新子流加入。
MP_JOIN 握手(子流 2,客户端发起):
SYN : MP_JOIN + token + nonce + HMAC(密钥, nonce)
SYN/ACK : MP_JOIN + HMAC(密钥, 双方 nonce)
ACK : MP_JOIN + HMAC
→ 新子流加入,共享同一个 MPTCP 连接
token = 密钥的哈希(对端用它找到对应连接)
HMAC = 证明「我有连接级密钥」
2.3 ADD_ADDR:通告新地址
服务端(或客户端)可通过 ADD_ADDR 通告自己的其他地址,让对端主动来连。这在多宿主服务器上很有用:服务端有两个 IP,客户端可以同时连两个,形成两条子流。REMOVE_ADDR 则用于撤销。
ADD_ADDR 语义:
发送方:「我还有一个地址 X,你也可以连过来」
接收方:可主动向 X 发起 MP_JOIN → 形成新子流
REMOVE_ADDR:某地址不再可用(如网卡 down)
地址可以是 IPv4 或 IPv6 → MPTCP 天然支持 v4/v6 混合
# 查看某条 MPTCP 连接的子流
ip mptcp endpoint show
ss -M # 显示 MPTCP 连接与其子流
三、调度器:包走哪条路
3.1 调度器的职责
MPTCP 层要为每个待发数据段选择一条子流。这个选择策略就是调度器(scheduler),它决定了「聚合」还是「冗余」。内核默认提供三种,各有取舍。
| 调度器 | 策略 | 特点 |
|---|---|---|
| default | 优先用 RTT 最低的子流 | 低延迟优先,易聚合 |
| redundant | 同一数据在多条子流上各发一份 | 极致可靠,浪费带宽 |
| backup | 主路径优先,备路径仅在主路径不可用时启用 | 省流量,适合计费链路 |
| fullmesh | 服务端建立全互联子流 | 多宿主服务器场景 |
3.2 default 调度器与聚合
default 调度器把数据优先发到「预计最早到达」的子流(用 RTT 估计)。当某条子流拥塞窗口填满时,剩余数据发到其他子流,从而实现带宽聚合。它的目标是最小化传输时间。
default 调度器逻辑:
1. 对每条子流估计 RTT
2. 选择「最早可发送」的子流(cwnd 有空位且 RTT 低)
3. 若所有子流都满 → 按最小 RTT 排入最空闲的
效果:多条高速路径叠加,吞吐接近之和
3.3 backup 与冗余的取舍
backup 模式把某些子流标记为「备用」:只要主路径可用,数据只走主路径;主路径失效才启用备用。这适合「主路径免费、备用路径计费」的场景(如 Wi-Fi 优先、蜂窝备用)。redundant 模式则相反,每条数据在所有子流上各发一份,换取极致可靠,代价是带宽浪费。
# 把某条子流设为 backup(如蜂窝链路备用)
ip mptcp endpoint add 192.0.2.1 dev wwan0 backup
# 查看 endpoint 标记
ip mptcp endpoint show
四、耦合拥塞控制
4.1 为什么不能各管各的
如果每条子流各自跑标准 TCP 拥塞控制,它们会在共享瓶颈上互相竞争,变得比单条 TCP 更「贪心」,损害其他流的公平性。MPTCP 的解法是「耦合」:让各子流的拥塞窗口联动,整体行为与一条 TCP 相当。
非耦合的问题:
两条子流共享同一瓶颈 → 各自独立增窗 → 合计占用 2 倍带宽
→ 对同链路的普通 TCP 不公平
耦合的目标:
多子流整体表现为「一条 TCP」
→ 与单路径流公平共享瓶颈
4.2 LIA 与 OLIA
LIA(Linked Increases Algorithm)是最早的耦合算法,以「总窗口增长不超过单路径」为约束。OLIA 在其基础上改进了窗口分配,减少某些子流被饿死的情况。两者都是内核可选算法。
# 查看/设置 MPTCP 拥塞控制算法
sysctl net.mptcp.available_congestion_control
sysctl net.mptcp.congestion_control
# 可选值通常有:lia、olia、reno(部分内核)
# 设为 OLIA
sysctl -w net.mptcp.congestion_control=olia
| 算法 | 思路 | 特点 |
|---|---|---|
| LIA | 总窗口增长 ≤ 单路径 | 公平性好,分配可能不均 |
| OLIA | 优化窗口分配 | 减少饿死,性能更均衡 |
| Reno/CUBIC | 各子流独立(非耦合) | 仅供对比,不推荐生产 |
4.3 与单路径拥塞控制的关系
MPTCP 的耦合算法约束的是「跨子流的窗口总量」,而每条子流内部的丢包恢复仍遵循其底层 TCP 算法(CUBIC、BBR 等)。理解单路径算法的行为,是调优 MPTCP 的基础——若底层用 BBR,其基于带宽时延积的模型会与 LIA/OLIA 的窗口耦合产生不同互动,需要实测才能确定组合效果。
五、部署现状与内核配置
5.1 Linux 内核支持
MPTCP 自 Linux 5.6 起并入主线内核。默认对「应用透明」:可以配置成全局对所有 TCP 连接尝试 MPTCP(需对端也支持,否则退化)。生产环境更推荐用 ip mptcp limits 与 endpoint 精细控制。
# 检查内核是否支持 MPTCP
sysctl net.mptcp.enabled
# net.mptcp.enabled = 1
# 设置子流数量上限(主动发起的 / 接受的)
ip mptcp limits set subflow 4 add_addr_accepted 4
# 添加本地 endpoint(可指定是否 backup)
ip mptcp endpoint add 10.0.0.2 dev eth0
ip mptcp endpoint add 192.0.2.1 dev wwan0 backup
# 全局开关
sysctl -w net.mptcp.enabled=1
5.2 应用侧透明与 mptcpize
对大多数应用,MPTCP 是透明的——只要内核与对端支持,TCP socket 会自动用上。但有些应用显式设置了 IPPROTO_TCP,需要用 mptcpize 包装,把 socket(AF_INET, SOCK_STREAM, IPPROTO_TCP) 改写成 IPPROTO_MPTCP。
# 用 mptcpize 运行一个原本不支持 MPTCP 的程序
mptcpize run curl https://example.com
# 或设置环境变量让部分程序自动启用
# 应用也可直接用 IPPROTO_MPTCP 创建 socket
/* 应用显式使用 MPTCP */
int fd = socket(AF_INET, SOCK_STREAM, IPPROTO_MPTCP);
5.3 中间设备的现实障碍
MPTCP 的落地难点不在两端,而在中间:不少防火墙、NAT、负载均衡器会丢弃带未知 TCP 选项的包,或改写序号导致 MPTCP 校验失败。此外,很多服务器与 CDN 尚未启用 MPTCP,导致「客户端支持、服务端不支持」而退化为普通 TCP。
| 障碍 | 表现 | 应对 |
|---|---|---|
| 中间盒丢弃 TCP 选项 | 握手失败或降级 | 选路径时避开;升级设备 |
| NAT 改写序号 | MPTCP 校验失败 | 现代 NAT 多数兼容 |
| 服务端未启用 | 退化为普通 TCP | 服务端也开 MPTCP |
| 计费链路被误用 | 流量费用上升 | 用 backup 标记 |
# 用 ss 确认连接是否真的跑在 MPTCP 上
ss -M -t
# 若显示 subflow 信息,说明 MPTCP 生效;否则已退化
5.4 与 QUIC 多路径的对比
QUIC 也支持多路径(Multipath QUIC),且天然带加密与连接迁移。MPTCP 的优势是「对应用完全透明、可在内核生效」,QUIC 多路径的优势是「用户态可迭代、与 HTTP/3 天然结合、加密无中间盒干扰」。二者并非替代关系,而是分别适合「改造现有 TCP 应用」与「新建基于 QUIC 的服务」。相关背景可参考 HTTP/3 与 QUIC 协议 。
| 维度 | MPTCP | Multipath QUIC |
|---|---|---|
| 层次 | 内核传输层 | 用户态 + UDP |
| 透明性 | 对应用完全透明 | 需 QUIC 栈支持 |
| 加密 | 无(依赖上层 TLS) | 内建 |
| 中间盒干扰 | 较敏感(TCP 选项) | 小(UDP + 加密) |
| 部署成熟度 | 内核 5.6+,服务端少 | 生态演进中 |
六、排错与验证
6.1 确认是否启用
MPTCP 最常见的问题是「以为启用了,其实退化了」。验证要分三层:内核开关、连接是否协商成功、子流是否建立。
# 1. 内核开关
sysctl net.mptcp.enabled
# 2. 连接是否 MPTCP(-M 只显示 MPTCP 连接)
ss -M -t
# 3. 子流与 endpoint
ip mptcp endpoint show
ip mptcp limits show
# 4. 内核统计(子流创建失败、降级次数)
nstat -az | grep -i mptcp
6.2 常见故障对照
| 现象 | 原因 | 排查 |
|---|---|---|
| 连接退化为普通 TCP | 对端不支持/中间盒丢选项 | ss -M 确认,抓包看 MP_CAPABLE |
| 有连接无子流 | 未配 endpoint 或 limits 为 0 | ip mptcp endpoint show、limits show |
| 子流建立失败 | MP_JOIN HMAC 校验失败 | 抓包看 MP_JOIN,检查密钥协商 |
| 吞吐未聚合 | 调度器选单路径/路径 RTT 悬殊 | 检查子流 RTT,考虑 backup 标记 |
| 蜂窝流量异常 | backup 未标记 | ip mptcp endpoint add ... backup |
6.3 用抓包确认子流
当 ss -M 显示的是 MPTCP 连接但吞吐没有聚合时,抓包看子流实际行为最直接。关注三类选项:MP_CAPABLE(建连)、MP_JOIN(加子流)、ADD_ADDR(地址通告)。若只看到 MP_CAPABLE 而没有 MP_JOIN,说明子流根本没建起来。
# 抓 MPTCP 相关 TCP 选项
tcpdump -i any -nn 'tcp[tcpflags] & tcp-syn != 0' -v
# 过滤带 MPTCP 选项的握手(Wireshark 可直接解析 mptcp 选项)
tshark -i eth0 -Y 'tcp.options.mptcp' -V | head -60
# 统计子流建立失败的内核计数
nstat -az | grep -iE 'MPTCP|Mptcp'
| 抓包现象 | 含义 | 动作 |
|---|---|---|
| 无 MP_CAPABLE | 一端未启用或中间盒剥离 | 检查 net.mptcp.enabled 与设备 |
| 有 MP_CAPABLE 无 MP_JOIN | 未配 endpoint 或路径不可达 | ip mptcp endpoint show |
| MP_JOIN 后无数据 | 子流建立但未被调度 | 检查 RTT 与 backup 标记 |
| ADD_ADDR 被忽略 | 对端不接受新地址 | 确认对端 MPTCP 实现 |
6.4 部署决策清单
在决定是否上 MPTCP 之前,按下面的清单逐项确认,能避免「配了半天其实已退化」的返工。
上线前检查:
[ ] 客户端与服务端内核均 >= 5.6 且 net.mptcp.enabled=1
[ ] 路径上的防火墙/NAT 不丢弃未知 TCP 选项
[ ] 需要聚合的接口都已 ip mptcp endpoint add
[ ] 计费链路已标记 backup,避免意外流量
[ ] 已设置子流上限(ip mptcp limits set),防止子流爆炸
[ ] 监控 ss -M 与 nstat,确认未静默退化
[ ] 明确底层单路径拥塞控制(CUBIC/BBR)与耦合算法的组合
七、小结
| 主题 | 核心要点 | 落地建议 |
|---|---|---|
| 定位 | 传输层多路径,应用透明 | 内核 5.6+,两端都要支持 |
| 握手 | MP_CAPABLE 建连、MP_JOIN 加子流 | 关注中间盒兼容性 |
| 地址 | ADD_ADDR 通告多地址 | 多宿主服务器配 endpoint |
| 调度 | default 聚合、backup 省流量 | 计费链路务必标 backup |
| 拥塞 | LIA/OLIA 耦合保公平 | 优先 OLIA |
| 排错 | 先确认没退化 | ss -M + nstat |
MPTCP 解决的是一个根本性错配:物理世界有多个网络接口,而传统 TCP 只认一条路径。它把「聚合、冗余、无缝切换」下沉到传输层,让应用零改动获得多路径能力。落地的主要阻力不在协议本身,而在中间设备与服务端支持度。想补齐传输层基础,可阅读 TCP 协议深度剖析 ;理解多子流的窗口耦合,则要回到 TCP 拥塞控制算法 ;若面向新建服务,也可以对照 HTTP/3 与 QUIC 协议 里的多路径与连接迁移设计。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。