使用reuseport和recvmmsg優化UDP服務器_應用服務器

地址:http://www.shangxueba.com/jingyan/2216074.html


最近剛好完成了一個DNS服務器的開發,因此積累一點對高性能UDP服務器的開發經驗。如果你也遇到UDP服務器的性能不佳,遠不如你的預期,也許你也可以採用本文的手段去優化一下試試。

    udp不像tcp是有連接的,因此udp不能通過建立多個連接來提高對服務器的併發訪問,然後我就遇到了在多核環境下通過多線程訪問一個共享的udp socket時,無論如何我都無法將所有的cpu都利用起來,最後的結局當然就是無法壓測出機器的瓶頸,性能也上不去。Google爲了解決他們的DNS服務器性能問題,就給linux內核打了一個patch,這個patch就是SO_REUSEPORT,經過我的實戰體驗,reuseport對udp服務器在多核機器上的性能提升是非常大的,值得使用。

    REUSEPORT的目的如其名,就是爲了讓多線程/多進程服務器的每個線程都listen同一個端口,並且最終每個線程擁有一個獨立的socket,而不是所有線程都訪問一個socket。沒有reuseport這個patch的話,這麼做的後果就是服務器會報出一個類似“地址/端口被佔用的”錯誤信息。在沒有reuseport的時候,客戶端發給udp服務器的每個包都是被投遞到唯一的一個socket上了,使用reuseport後,服務器有了多個socket,那麼客戶端發過來的包投遞到哪個socket上呢?linux內核採用了一個四元組<客戶端ip,客戶端port,服務器ip,服務器port>的hash來進行包的分發,這樣做至少有兩個目的:一是保證同一個客戶度過來的包都被遞送到同一個socket上;二是在客戶端量足夠的時候,基本可以均衡到所有的socket上。在使用reuseport的時候需要注意:客戶端太少的話,是很難壓測出服務器的真實性能的,因爲reuseport使用的是hash值來分發請求到socket上,所以可能出現每個socket上接收包不均衡的情況,使用較多的客戶端機器來壓測服務器,目的就是讓每個socket儘可能的均衡。

    使用reuseport後,udp服務器的併發能力大幅度的提高了,這個時候還可以繼續使用recvmmsg來繼續降低系統調用的開銷。recvmmsg是一個批量接口,它可以從socket裏一次讀出多個udp數據包,不像recvfrom那樣一次只能讀一個。如果客戶端多、請求量大的話,recvmmsg的批量讀就很有優勢了。不過,使用recvmmsg一定要清楚,它從socket裏一次讀出的所有包不一定是來自同一個客戶端的,大多數情況應該都是來自不同客戶端的。這不像tcp,從同一個連接裏讀到的數據一定是同一個客戶端。我們的一個同學在使用recvmmsg的時候,就犯了這個錯誤,誤認爲一次收取的數據包都是同一個客戶端的,最後將所有的應答都發給了同一個客戶端,其他的客戶端全都超時了。高性能服務器開發中,系統調用是昂貴的,所以沒事就可以用strace看看一個請求週期內有哪些系統調用,盡一切可能去優化掉他們。

    開發一個UDP服務器,不是說使用了reuseport和recvmmsg後性能就高了。一個高性能的網絡服務器,是需要進行方方面面的優化才行的。

-----------------------------------------------------------

附上strace的用法:

1、直接strace運行命令:strace cat /dev/null
2、跟蹤已運行進程:strace -p pid
3、輸出結果到文件:strace -o filename -p pid
4、在轉載一篇文章:http://www.vimer.cn

用strace調試程序

     在理想世界裏,每當一個程序不能正常執行一個功能時,它就會給出一個有用的錯誤提示,告訴你在足夠的改正錯誤的線索。但遺憾的是,我們不是生活在理想世界裏,起碼不總是生活在理想世界裏。有時候一個程序出現了問題,你無法找到原因。

這就是調試程序出現的原因。strace是一個必不可少的調試工具,strace用來監視系統調用。你不僅可以調試一個新開始的程序,也可以調試一個已經在運行的程序(把strace綁定到一個已有的PID上面)。

首先讓我們看一個真實的例子:

啓動KDE時出現問題

前一段時間,我在啓動KDE的時候出了問題,KDE的錯誤信息無法給我任何有幫助的線索。

代碼:

_KDE_IceTransSocketCreateListener: failed to bind listener 
_KDE_IceTransSocketUNIXCreateListener: …SocketCreateListener() failed 
_KDE_IceTransMakeAllCOTSServerListeners: failed to create listener for local
Cannot establish any listening sockets DCOPServer self-test failed.

對我來說這個錯誤信息沒有太多意義,只是一個對KDE來說至關重要的負責進程間通信的程序無法啓動。我還可以知道這個錯誤和ICE協議(Inter Client Exchange)有關,除此之外,我不知道什麼是KDE啓動出錯的原因。

我決定採用strace看一下在啓動dcopserver時到底程序做了什麼:

代碼:

strace ---~/dcop-strace.txt dcopserver

這裏 -f -F選項告訴strace同時跟蹤fork和vfork出來的進程,-o選項把所有strace輸出寫到~/dcop-strace.txt裏面,dcopserver是要啓動和調試的程序。

再次出現錯誤之後,我檢查了錯誤輸出文件dcop-strace.txt,文件裏有很多系統調用的記錄。在程序運行出錯前的有關記錄如下:

代碼:

27207 mkdir(“/tmp/.ICE-unix”0777= -1 EEXIST (File exists) 
27207 lstat64(“/tmp/.ICE-unix”, {st_mode=S_IFDIR|S_ISVTX|0755, st_size=4096, …}) = 0 
27207 unlink(“/tmp/.ICE-unix/dcop27207-1066844596″= -1 ENOENT (No such file or directory) 
27207 bind(3, {sin_family=AF_UNIX, path=“/tmp/.ICE-unix/dcop27207-1066844596″}, 3 = -1 EACCES (Permission denied) 
27207 write(2“_KDE_IceTrans”13= 13 
27207 write(2“SocketCreateListener: failed to “…, 46= 46 
27207 close(3= 0 27207 write(2“_KDE_IceTrans”13= 13 
27207 write(2“SocketUNIXCreateListener: …Soc”…, 59= 59 
27207 umask(0= 0 27207 write(2“_KDE_IceTrans”13= 13 
27207 write(2“MakeAllCOTSServerListeners: fail”…, 64= 64 
27207 write(2“Cannot establish any listening s”…, 39= 39

其中第一行顯示程序試圖創建/tmp/.ICE-unix目錄,權限爲0777,這個操作因爲目錄已經存在而失敗了。第二個系統調用(lstat64)檢查 了目錄狀態,並顯示這個目錄的權限是0755,這裏出現了第一個程序運行錯誤的線索:程序試圖創建屬性爲0777的目錄,但是已經存在了一個屬性爲 0755的目錄。第三個系統調用(unlink)試圖刪除一個文件,但是這個文件並不存在。這並不奇怪,因爲這個操作只是試圖刪掉可能存在的老文件。

但是,第四行確認了錯誤所在。他試圖綁定到/tmp/.ICE-unix/dcop27207-1066844596,但是出現了拒絕訪問錯誤。. ICE_unix目錄的用戶和組都是root,並且只有所有者具有寫權限。一個非root用戶無法在這個目錄下面建立文件,如果把目錄屬性改成0777, 則前面的操作有可能可以執行,而這正是第一步錯誤出現時進行過的操作。

所以我運行了chmod 0777 /tmp/.ICE-unix之後KDE就可以正常啓動了,問題解決了,用strace進行跟蹤調試只需要花很短的幾分鐘時間跟蹤程序運行,然後檢查並分析輸出文件。

說明:運行chmod 0777只是一個測試,一般不要把一個目錄設置成所有用戶可讀寫,同時不設置粘滯位(sticky bit)。給目錄設置粘滯位可以阻止一個用戶隨意刪除可寫目錄下面其他人的文件。一般你會發現/tmp目錄因爲這個原因設置了粘滯位。KDE可以正常啓動 之後,運行chmod +t /tmp/.ICE-unix給.ICE_unix設置粘滯位。

解決庫依賴問題

starce 的另一個用處是解決和動態庫相關的問題。當對一個可執行文件運行ldd時,它會告訴你程序使用的動態庫和找到動態庫的位置。但是如果你正在使用一個比較老 的glibc版本(2.2或更早),你可能會有一個有bug的ldd程序,它可能會報告在一個目錄下發現一個動態庫,但是真正運行程序時動態連接程序 (/lib/ld-linux.so.2)卻可能到另外一個目錄去找動態連接庫。這通常因爲/etc/ld.so.conf和 /etc/ld.so.cache文件不一致,或者/etc/ld.so.cache被破壞。在glibc 2.3.2版本上這個錯誤不會出現,可能ld-linux的這個bug已經被解決了。

儘管這樣,ldd並不能把所有程序 依賴的動態庫列出來,系統調用dlopen可以在需要的時候自動調入需要的動態庫,而這些庫可能不會被ldd列出來。作爲glibc的一部分的NSS (Name Server Switch)庫就是一個典型的例子,NSS的一個作用就是告訴應用程序到哪裏去尋找系統帳號數據庫。應用程序不會直接連接到NSS庫,glibc則會通 過dlopen自動調入NSS庫。如果這樣的庫偶然丟失,你不會被告知存在庫依賴問題,但這樣的程序就無法通過用戶名解析得到用戶ID了。讓我們看一個例 子:

whoami程序會給出你自己的用戶名,這個程序在一些需要知道運行程序的真正用戶的腳本程序裏面非常有用,whoami的一個示例輸出如下:
代碼:

# whoami 
root

假設因爲某種原因在升級glibc的過程中負責用戶名和用戶ID轉換的庫NSS丟失,我們可以通過把nss庫改名來模擬這個環境:
代碼:

# mv /lib/libnss_files.so.2 /lib/libnss_files.so.2.backup 
# whoami 
whoami: cannot find username for UID 0

這裏你可以看到,運行whoami時出現了錯誤,ldd程序的輸出不會提供有用的幫助:
代碼:

# ldd /usr/bin/whoami 
libc.so.6 => /lib/libc.so.6 (0x4001f000) 
/lib/ld-linux.so.2 => /lib/ld-linux.so.2 (0×40000000)

你只會看到whoami依賴Libc.so.6和ld-linux.so.2,它沒有給出運行whoami所必須的其他庫。這裏時用strace跟蹤whoami時的輸出:
代碼:

strace -o whoami-strace.txt whoami
open(“/lib/libnss_files.so.2″, O_RDONLY) = -1 ENOENT (No such file or directory) 
open(“/lib/i686/mmx/libnss_files.so.2″, O_RDONLY) = -1 ENOENT (No such file or directory) 
stat64(“/lib/i686/mmx”, 0xbffff190) = -1 ENOENT (No such file or directory) 
open(“/lib/i686/libnss_files.so.2″, O_RDONLY) = -1 ENOENT (No such file or directory) 
stat64(“/lib/i686″, 0xbffff190) = -1 ENOENT (No such file or directory) 
open(“/lib/mmx/libnss_files.so.2″, O_RDONLY) = -1 ENOENT (No such file or directory) 
stat64(“/lib/mmx”, 0xbffff190) = -1 ENOENT (No such file or directory) 
open(“/lib/libnss_files.so.2″, O_RDONLY) = -1 ENOENT (No such file or directory) 
stat64(“/lib”, {st_mode=S_IFDIR|0755, st_size=2352, …}) = 0 
open(“/usr/lib/i686/mmx/libnss_files.so.2″, O_RDONLY) = -1 ENOENT (No such file or directory) 
stat64(“/usr/lib/i686/mmx”, 0xbffff190) = -1 ENOENT (No such file or directory) 
open(“/usr/lib/i686/libnss_files.so.2″, O_RDONLY) = -1 ENOENT (No such file or directory)

你可以發現在不同目錄下面查找libnss.so.2的嘗試,但是都失敗了。如果沒有strace這樣的工具,很難發現這個錯誤是由於缺少動態庫造成的。現在只需要找到libnss.so.2並把它放回到正確的位置就可以了。

[BOLD]限制strace只跟蹤特定的系統調用[/BOLD]

如果你已經知道你要找什麼,你可以讓strace只跟蹤一些類型的系統調用。例如,你需要看看在configure腳本里面執行的程序,你需要監視的系統調用就是execve。讓strace只記錄execve的調用用這個命令:

代碼:

strace --o configure-strace.txt -e execve ./configure

部分輸出結果爲:
代碼:

2720 execve(“/usr/bin/expr”, ["expr""a"":""(a)"], [/* 31 vars */]) = 0 
2725 execve(“/bin/basename”, ["basename""./configure"], [/* 31 vars */]) = 0 
2726 execve(“/bin/chmod”, ["chmod""+x""conftest.sh"], [/* 31 vars */]) = 0 
2729 execve(“/bin/rm”, ["rm""-f""conftest.sh"], [/* 31 vars */]) = 0 
2731 execve(“/usr/bin/expr”, ["expr""99""+""1"], [/* 31 vars */]) = 0 
2736 execve(“/bin/ln”, ["ln""-s""conf2693.file""conf2693"], [/* 31 vars */]) = 0

你已經看到了,strace不僅可以被程序員使用,普通系統管理員和用戶也可以使用strace來調試系統錯誤。必須承認,strace的輸出不總是容易理解,但是很多輸出對大多數人來說是不重要的。你會慢慢學會從大量輸出中找到你可能需要的信息,像權限錯誤,文件未找到之類的,那時strace就會成爲一個有力的工具了。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章