很明確的一件事是,IPv6不允許中間設備對報文分片。具體爲什麼這麼設計,就是爲了簡單高效。因此,IPv6報頭簡潔了不少。
但TSO貌似並未違背取消IPv6分片的初衷,硬件把一些都處理的妥妥的,在路由軟件層看來,一切好像沒有發生過一樣。
我先簡單解釋一下TSO和IP分片的區別:
我們來看一個簡單的實驗,用IPv6從服務端拉一個大文件,服務端和客戶端的抓包如下:
在客戶端看來,沒有IP分片,因此它不需要做分片重組的動作,它實實在在就是收到了一個完整的7140字節的報文,就好像這個報文就是從服務端真實發出的一樣,而實際上,服務器顯然沒有發送過這麼大的報文,顯然,這是客戶端聚合了幾個小包的結果。
因此,下面的結論是合理的:
- 如果轉發設備覺得一個IPv6數據包大於出口的MTU,並且如果它的載荷是TCP段的話,啓用TSO將其分段而不是直接丟棄併發送ICMP too big,這並無不妥。
IPv6只是規定了中間設備不能分片,但是卻沒有規定必須保持原樣傳輸啊。
然而,Linux內核是這麼實現的嗎?嚴格來講,Linux對於上面的描述,僅僅實現了一半。
Linux內核做到了:
- 即便是轉發設備,只要使能了LRO/GRO,就會進行收包聚合,將多個小包儘可能聚合成一個大包,並且修改對應的IPv6頭。
- 即便是轉發設備,在轉發IPv6超過MTU大小的報文時,只要該報文元數據gso_size不大於MTU,就能成功轉發,依靠GSO/TSO在發送時對其進行分段發送,重新分割成獨立的小報文。
然而,這是不妥的。
gso_size可以認爲是聚合前小報文的大小,在我看來,即便它大於出口MTU,也是可以利用TSO/GSO機制將其分段的啊!只是Linux沒有實現而已。
具體看這段代碼:
int ip6_forward(struct sk_buff *skb)
{
...
if (ip6_pkt_too_big(skb, mtu)) {
/* Again, force OUTPUT device used as source address */
skb->dev = dst->dev;
icmpv6_send(skb, ICMPV6_PKT_TOOBIG, 0, mtu);
__IP6_INC_STATS(net, idev, IPSTATS_MIB_INTOOBIGERRORS);
__IP6_INC_STATS(net, ip6_dst_idev(dst),
IPSTATS_MIB_FRAGFAILS);
kfree_skb(skb);
return -EMSGSIZE;
}
...
}
當你看ip6_pkt_too_big的代碼時,裏面會找到這個不妥的邏輯。
這裏沒有必要擡槓說什麼GSO的實現之類就是如此,我的意思是,只要指定一個出口的MTU,無論什麼樣的TCP包,底層TSO/GSO都會按照該MTU的大小將其分成小段,每一個小段都是一個獨立完整的IPv6報文。這個和入口GRO設置的gso_size有個毛線關係啊!
但事實上,Linux內核對此的實現就是這麼殘!我用systemtap和bpftrace實地探測過的結論,後來我纔看的代碼。
浙江溫州皮鞋溼,下雨進水不會胖。