單連接多路徑,合并多服務(wù)器公網(wǎng)帶寬---MPTCP---aggligator
https://sliphua.work/try-mptcp/#%E5%BC%80%E5%8F%91%E8%80%85%E6%8E%A5%E5%85%A5
對單一連接進(jìn)行拆分發(fā)送合并接收,有拓展傳輸層 TCP 的 MPTCP 標(biāo)準(zhǔn),也有使用普通 TCP 自定義應(yīng)用層協(xié)議來實(shí)現(xiàn)的純上層工具。理論上來說,前者的上限更高,后者兼容度更廣。
這幾天分別嘗試了一下 Linux 內(nèi)核支持的 MPTCP 和基于一般 TCP 的應(yīng)用層工具 aggligator,來合并兩臺(tái)內(nèi)網(wǎng)互聯(lián)的服務(wù)器的帶寬,公網(wǎng)帶寬一臺(tái) 5 Mbps,一臺(tái) 4 Mbps。測試結(jié)果振奮人心,帶寬真的合并了;同時(shí)出乎意料,內(nèi)核級的 MPTCP 慢了一些??赡軠y試次數(shù)不夠,可能內(nèi)核優(yōu)化還不完善,也可能部分配置項(xiàng)需要精調(diào)。

合成大烏龜
標(biāo)準(zhǔn) MPTCP
兼容性考量
標(biāo)準(zhǔn)多路徑 TCP 應(yīng)當(dāng)可以概括為對 TCP 協(xié)議的一種拓展,是在 TCP 協(xié)議頭中定義的一堆專用的 TCP Options 和相關(guān)邏輯。
在握手階段,任一端就可以通過收到的相關(guān) Option 知道另一端是否支持 MPTCP,對端支持則后續(xù)接著使用 MPTCP 專用字段溝通需要的額外信息;對端不支持則無縫切換一般 TCP 行為邏輯。
Apple 部分內(nèi)置應(yīng)用 2013 年就開始使用 MPTCP v0(RFC 6824),Linux 內(nèi)核自 5.6 開始支持 MPTCP v1(RFC 8684)。但終端碼農(nóng)角度的文章不論中外目前還是很少,好多問題需要自己啃規(guī)范原文找到答案。幸運(yùn)的是,相關(guān)內(nèi)核開發(fā)者為了進(jìn)一步降低接入門檻,寫了很多文檔和代碼工具,大大降低了理解和接入成本。
“多路徑”理解
MPTCP 在握手期間雙方生成一個(gè) key 并交換,用于判斷后續(xù)對話的是哪個(gè)連接。
握手后,不論包從哪里哪個(gè) IP 來,從哪個(gè)端口來,只要拿著對應(yīng)的 key,就可能與內(nèi)核建立一條新的子流 Subflow,內(nèi)核幫你把數(shù)據(jù)合并入一個(gè) socket。流與流之間另用一個(gè) Address ID 字段區(qū)分。
發(fā)包的時(shí)候,內(nèi)核看看已有的子流,選一個(gè)發(fā)出去。如果對面有建議過還可走哪條道、自己有多張網(wǎng)卡等,就可能嘗試新建一條子流。也可以發(fā)建議給對面,建議對面主動(dòng)與建議地址建立子流。
子流的建立、維護(hù)、關(guān)閉等行為與一般 TCP 連接類似,但是“連接”是一個(gè)整體的概念,子流是連接的一部分。對內(nèi)核來說,子流是 MPTCP 連接的一條路徑;對應(yīng)用層來說,子流是完全透明的,只需要像使用一般 TCP socket 一樣使用 MPTCP socket。
完整步驟還要加上哈希認(rèn)證、排序重傳、路徑管理模式、調(diào)度策略等,精準(zhǔn)的行為邏輯建議閱讀規(guī)范原文,RFC 8684: TCP Extensions for Multipath Operation with Multiple Addresses。
服務(wù)端接入
Linux MPTCP 開發(fā)者提供了一些實(shí)用工具幫助接入 MPTCP,包括:
mptcpd: 提供用戶空間的路徑管理接口。mptcpize: 讓目標(biāo)程序或服務(wù)創(chuàng)建的 TCP socket 都變?yōu)?MPTCP socket。
假設(shè),要在某臺(tái) Debian 服務(wù)器 2024 端口搭建一個(gè) Minecraft Java 服務(wù)器,稱為主服;另一臺(tái)服務(wù)器稱為中轉(zhuǎn)服。
中轉(zhuǎn)配置
規(guī)范允許服務(wù)端以中轉(zhuǎn) IP 主動(dòng)與客戶端建立子流,也允許將中轉(zhuǎn)地址作為建議發(fā)給客戶端,讓客戶端主動(dòng)建立。
考慮到公網(wǎng),NAT 普及,太多時(shí)候只能客戶端主動(dòng)與服務(wù)器建聯(lián)。所以最實(shí)用簡單的,我們將中轉(zhuǎn)服某一端口 NAT 內(nèi)網(wǎng)轉(zhuǎn)發(fā)到主服,即完成中轉(zhuǎn)配置。這樣也使不經(jīng) NAT 的少數(shù)客戶端的體驗(yàn)一致了,都是客戶端主動(dòng)建立子流,避免偶現(xiàn)奇奇怪怪的問題。
具體而言,配置中轉(zhuǎn)服轉(zhuǎn)發(fā)到主服,又有兩種模式可選:
- 單轉(zhuǎn)發(fā)單服務(wù)模式:一個(gè) NAT 端口轉(zhuǎn)發(fā),對應(yīng)一個(gè)主服 MPTCP 服務(wù)端口。這種模式目前配置起來最簡單,就是需要為每一個(gè)服務(wù)端口都配置一個(gè) NAT 中轉(zhuǎn)地址。
- 單轉(zhuǎn)發(fā)多服務(wù)模式:一個(gè) NAT 端口轉(zhuǎn)發(fā),可以對應(yīng)多個(gè)主服 MPTCP 服務(wù)端口。這種模式要求主服分配一個(gè)專用的端口給內(nèi)核,配置中轉(zhuǎn)服轉(zhuǎn)發(fā)到主服這個(gè)專用端口上。內(nèi)核目前尚未完全支持這種模式,Linux 內(nèi)核開發(fā)者有提出暫時(shí)的變通方案,但是筆者沒有試驗(yàn)成功。
這兩種轉(zhuǎn)發(fā)模式,在中轉(zhuǎn)服上的配置不同。不過正好,多服務(wù)模式本人沒有嘗試成功,所以后文只介紹單轉(zhuǎn)發(fā)單服務(wù)模式的配置。
假設(shè)中轉(zhuǎn)服同樣選用 2024 端口,NAT 內(nèi)網(wǎng)轉(zhuǎn)發(fā)到主服的 2024 端。
值得注意的是,由于 MPTCP 的相關(guān)參數(shù)含于 TCP 數(shù)據(jù)包頭,L7 層的端口轉(zhuǎn)發(fā),即通過與目的端新建一條連接,然后不斷中轉(zhuǎn)兩條連接的數(shù)據(jù)體實(shí)現(xiàn)的轉(zhuǎn)發(fā),不能用于服務(wù)端 MPTCP 中轉(zhuǎn)。
主服準(zhǔn)備
首先,升級內(nèi)核至 6.1+,并確保 MPTCP 已經(jīng)啟用。舊內(nèi)核可能不支持后續(xù)操作。
sudo -i sysctl net.mptcp.enabled
可選地,打開 MPTCP 的校驗(yàn)和功能。規(guī)范建議在不可控環(huán)境中開啟??蛻舳朔?wù)端只要有一端開啟,這個(gè)功能就是雙端生效的。幾次測試下來沒看到速度差別。看自己需求了。
sudo -i sysctl -w net.mptcp.mptcp_checksum=1
你可能需要采用某種方法使這處變更在重啟后依然生效。
主服路徑管理
接下來,我們配置主服向 MPTCP 客戶端發(fā)送中轉(zhuǎn) IP 和相關(guān)端口,告訴客戶端還有中轉(zhuǎn)路徑可走。
這一步可以通過單純調(diào)整內(nèi)核配置來實(shí)現(xiàn),也可以通過使用 mptcpd 用戶空間應(yīng)用來實(shí)現(xiàn)。
純內(nèi)核配置
單純調(diào)整內(nèi)核配置的優(yōu)點(diǎn)是簡單快捷,不需要編寫編譯額外代碼,缺點(diǎn)是
- 要求中轉(zhuǎn)端口號與主服端口號一致;
- 會(huì)針對所有的 MPTCP 連接生效,即不區(qū)分本地端口是否為 2024。
假設(shè)中轉(zhuǎn)服 IP 為 2.2.2.2,執(zhí)行下面的命令,即可。
sudo ip mptcp endpoint add 2.2.2.2 signal
用戶空間接口
或者,我們使用 mptcpd,在用戶空間自定義靈活的路徑管理策略。mptcpd
- 不要求中轉(zhuǎn)端口號與主服端口號一致;
- 支持特定連接特定處理,可以僅針對 2024 端口的連接,向?qū)Χ税l(fā)送地址建議。
不過,目前 mptcpd 尚不支持建議會(huì)經(jīng) NAT 中轉(zhuǎn)的 IP,需要像下面這樣調(diào)整。
安裝編譯依賴。
sudo apt install build-essential autoconf automake libtool autoconf-archive libell-dev
克隆 mptcpd。
git clone https://github.com/multipath-tcp/mptcpd.git
cd mptcpd
找到 upstream_announce 函數(shù)(截至發(fā)文,src/netlink_pm_upstream.c 第 219 行),該函數(shù)用于向?qū)Χ税l(fā)送地址建議。注意開頭的偵聽調(diào)用 (232-235 行),由于無法偵聽外部 IP 會(huì)失敗,由此阻止了我們對外發(fā)送中轉(zhuǎn)建議。調(diào)用上方 @todo 也提到這處偵聽不是強(qiáng)制的。為了盡早嘗鮮,我們直接將這塊調(diào)用注釋掉,保存。
編譯,安裝。
./bootstrap
./configure
make
sudo make install
接著,我們就可以通過創(chuàng)建 mptcpd 路徑管理插件,自定義路徑管理策略,使得每建立一個(gè) 2024 端口的 MPTCP 連接,我們都向?qū)Χ税l(fā)送中轉(zhuǎn)地址建議。
各連接地址之間是平級的,內(nèi)核允許客戶端先走中轉(zhuǎn)地址創(chuàng)建連接,再走主服地址新增子流。要做到這一點(diǎn),可以直接向?qū)Χ税l(fā)送所有可用地址建議,對端會(huì)自行篩選;也可以自行編寫邏輯判斷當(dāng)前地址,再向?qū)Χ税l(fā)送剩余地址建議。本文不作展開。
閱讀 mptcpd 插件開發(fā) wiki,跟隨示例,編寫我們需要的插件。下面給出一個(gè)簡單的例子,新建一個(gè)文件夾,把下面的三個(gè)文件放進(jìn)去。
1/3,suggest.c:
#include <arpa/inet.h>
#include <ell/ell.h>
#include <mptcpd/id_manager.h>
#include <mptcpd/path_manager.h>
#include <mptcpd/plugin.h>
#define PLUGIN_NAME suggest // 插件名,以 suggest 為例
#define MATCH_PORT 2024 // 主服端口
#define SUGGEST_IP "2.2.2.2" // 中轉(zhuǎn)機(jī) IP
#define SUGGEST_PORT 2024 // 中轉(zhuǎn)機(jī)端口
// 在 MPTCP 連接建立時(shí),
static void on_connection_established(
mptcpd_token_t token,
struct sockaddr const *laddr,
struct sockaddr const *raddr,
bool server_side,
struct mptcpd_pm *pm)
{
// 獲取本地端口,
struct sockaddr_in *sin = (struct sockaddr_in *)laddr;
uint16_t port = ntohs(sin->sin_port);
// 不是服務(wù)端口,不處理;
if (port != MATCH_PORT) {
return;
}
// 是服務(wù)端口,則生成中轉(zhuǎn)地址的結(jié)構(gòu)體,
struct sockaddr_in alt_addr = {};
alt_addr.sin_family = AF_INET;
alt_addr.sin_addr.s_addr = inet_addr(SUGGEST_IP);
alt_addr.sin_port = htons(SUGGEST_PORT);
// 為其注冊一個(gè) Address ID 用于給內(nèi)核分辨,
struct mptcpd_idm *const idm = mptcpd_pm_get_idm(pm);
mptcpd_aid_t const id = mptcpd_idm_get_id(idm, (struct sockaddr *)&alt_addr);
if (id == 0) {
l_error("Unable to map suggesting address to ID.");
return;
}
// 然后向?qū)Χ税l(fā)送。
if (mptcpd_pm_add_addr(pm, (struct sockaddr *)&alt_addr, id, token) != 0) {
l_error("Unable to suggest address to connection with token '%d'.", token);
return;
}
l_info("Suggested subflow address to connection with token '%d'.", token);
}
// 把上面的函數(shù)存到一個(gè)路徑管理器的操作集中,
static struct mptcpd_plugin_ops const pm_ops = {
.connection_established = on_connection_established,
};
// 在插件初始化時(shí),
static int suggest_init(struct mptcpd_pm *pm)
{
static char const name[] = L_STRINGIFY(PLUGIN_NAME);
// 注冊上述操作集。
if (!mptcpd_plugin_register_ops(name, &pm_ops)) {
l_error("Failed to initialize suggest path manager.");
return -1
