解決接收不到組播包的問題

   目前用的集羣是在應用層實現的,主要功能是實現在機器之間互轉請求。今天在部署的時候,發現請求沒有在節點之間互轉,相同的請求發送一次後miss,第二次發送的時候還是miss。正常來說,第一次miss後會在集羣內緩存一份,之後再有關於這個文件的請求不管發送到哪個機器都應該是hit的。
集羣之間的探活用的是組播消息,出現這種問題肯定是因爲接收組播報文出了問題。之前用的時候都沒有問題,所以先從環境入手來查找問題。
先使用tcpdump抓包,看是否能夠接收到組播報文。抓包的結果是,機器上接收到其他節點發送過來的組播報文。換了一臺機器,結果也一樣。現在是有數據包,下一步就是要找到數據包爲什麼被丟棄。之前遇到過一次是因爲網關配置的不一致導致的。這次檢查了幾臺機器,並且請運維的同事也幫忙看了一下,沒有發現有啥問題。
接着在機器上安裝了dropwatch,看看系統在哪些位置丟棄的數據包,結果如下圖所示(這個圖是在測試環境中重現問題後截的,結果是一樣的):

從上圖看來,比較靠譜的位置是在ip_rcv_finish()中丟包。ip_rcv_finish()中在查找路由緩存失敗和數據包IP首部出錯時纔會丟包。數據包損壞的可能性不大,因此確定是在查找路由緩存失敗丟的包。
後面使用"netstat -gn"命令來查看當前網卡上加入的組播組。用這個命令在機器上查看,發現加入的組播地址224.0.1.37綁定在eth0上,而本來要接收組播消息的fd綁定的IP地址是eth1上的地址。覺得應該是這裏的問題。
《IP Multicast Extensions for 4.3BSD UNIX and related systems》上看到,如果在加入組播組時,本地接口地址imr_interface設置的是INADDR_ANY時,選擇默認的組播接口,也就是讓內核來選擇。根據現在的情況來看,內核在選擇的時候會選擇默認網關使用的設備,我這裏使用的就是eth0。如果指定的接口地址的話,就會使用地址所在的網絡接口作爲組播組使用的網絡接口。
現在基本可以確定丟包的原因了。兩個機器的eth0和eth1網卡上設置的IP地址是不同網段的,eth0是9段的IP地址,eth1是4段的IP地址。發送組播消息時,使用的是4段的IP地址,所以接收組播消息的機器上數據包由eth1網卡來接收,但是加入組播組的網卡是eth0,所以數據包到達eth1時會查找路由失敗,在ip_rcv_finish()中會將數據包丟棄。
找到問題原因,立即修改代碼。在加入組播組時,將imr_interface設置爲指定的本地IP地址。重新編譯,啓動後,用“netstat -gn”發現現在組播地址所在的設備和綁定的接口相同,測試沒有問題。
爲了驗證上面的結論,寫了一個systemtap腳本,如下所示(比較醜陋,沒有封裝成函數,海涵):
%{
#include <linux/skbuff.h>
#include <linux/netdevice.h>
#include <linux/ip.h>
%}

global kaddr=0x250100e0
global iph
global daddrs, saddrs

function ip_rcv_finish_helper:long(arg:long) %{
struct sk_buff *skb = (typeof(skb))THIS->arg;
const struct iphdr *iph = ip_hdr(skb);

THIS->__retvalue = (long)iph;
return;
%}

probe kernel.statement("ip_rcv@net/ipv4/ip_input.c+12") {

iph = ip_rcv_finish_helper($skb);
func = probefunc();

saddrs[func] = @cast(iph, "iphdr")->saddr;
daddrs[func] = @cast(iph, "iphdr")->daddr;

}

probe kernel.statement("ip_rcv_finish@net/ipv4/ip_input.c+11") {

iph = ip_rcv_finish_helper($skb);
func = probefunc();

saddrs[func] = @cast(iph, "iphdr")->saddr;
daddrs[func] = @cast(iph, "iphdr")->daddr;

if ((daddrs[func] == kaddr) && $err) {
printf("err = %d\n", $err);
}

}

probe kernel.statement("ip_rcv_finish@net/ipv4/ip_input.c+35") {

if (daddrs[func] == kaddr) {
printf("The result is unexpected\n");
exit();
}
}



probe kernel.function("ip_rcv").return {

func = probefunc();

if (daddrs[func] == kaddr) {
printf("Packet from 0x%X to 0x%X is droped in %s, return=%d\n",
saddrs[func], daddrs[func], func, $return);
}
}

probe kernel.function("ip_rcv_finish").return {

func = probefunc();

if (daddrs[func] == kaddr) {
printf("Packet from 0x%X to 0x%X is droped in %s, return=%d\n",
saddrs[func], daddrs[func], func, $return);
}
}
輸出結果如下所示:

從上圖可以看出來,ip_rcv()和ip_rcv_finish()的返回值都是1,即爲NET_RX_DROP,表示要丟掉數據包。"ip_rcv_finish@net/ipv4/ip_input.c+35"這個probe點沒有任何輸出,也就是說獲取路由緩存項失敗。不過這個錯誤碼比較意外是22,即EINVAL,看了ip_route_input()在獲取組播報文的路由緩存項時確實是返回這個錯誤碼。這個輸出結果驗證了前面的結論。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章