標籤:style http color os io 使用 ar for 檔案
由於水平有限,以下僅僅是個人的一些心得,希望對新人有一點參考作用。另外由於時間關係,寫得有點雜,有些點可能並不是跟伺服器編程強相關的。
效能相關
1. 應用各種pool。
a) Mempool
比如為了提高記憶體配置效率,可以使用Mem pool。當對應的情境簡單時,可以自己定製私人的記憶體池管理。當記憶體池設計相對複雜的時候,可以考慮直接使用jemalloc、tcmalloc。
b) Socket pool
比如dns解析一般是基於udp協議,為了提高效能,避免反覆建立、銷毀socket帶來的開銷,可以建立一個udp socket pool,而且預先將這些fd的EPOLLIN事件註冊到epoll裡面。這樣有dns請求的時候,直接從udp socket pool取一個fd進行send,然後等待epoll的input事件,recv完成後將這個fd歸還到udp socket pool。可以參考http://code.oa.com/v2/weima/detail/7373,裡面有dns ipv4位址解析的完整的代碼。
2. 網路請求,在允許的前提下盡量用大量操作,可以大幅提高整體傳輸效能
3. 網路send/recv buffer避免多餘的memset,memset只會浪費cpu運算,其實只需要管理好相應的length欄位就好了。之前看到過一段代碼,開了一個非常大的buffer,不管三七二十一直接memset為0,僅memset就花費了幾百毫秒。
4. 避免重複運算。看到過很多這樣寫的:for (int I = 0; I < strlen(str); ++i) {… }。其實完全可以先計算strlen(str)並存到一個變數,然後再用這個變數,可以減少不必要的效能損耗。
5. 能用棧的盡量不用new、delete。提升效能的同時,還可以降低編碼複雜度,因為不用顯式地delete,最重要的是可以降低記憶體流失的風險,真是一舉三得。gcc支援變成數組,加之現在機器的預設棧都是好幾M,很多情境下可以直接這樣聲明數組:int arr[num]。看到過在同一個函數裡面反覆new、delete buffer的蛋疼寫法,即便這裡不能用棧,起碼可以new一個buffer反覆用。
6. 避免爛用值拷貝和引用。 拷貝大對象是非常耗時的,所以應該盡量避免。但是濫用引用一樣不好,比如傳遞一個int參數,用值拷貝比引用要好,引用本質上是一個指標,定址運算有一定的開銷。不過濫用引用比濫用值拷貝的害處小很多。
7. epollout事件使用是個技術活,使用不好很容易導致cpu彪得很高。個人感覺一般有兩種做法:
a) 只在非同步connect時監聽一次,串連成功後即可把out事件去掉。因為send操作只是將資料填充到socket send buffer,大部分情況下send操作可以立馬成功。即便不成功,也可以把未發送完成的資料放到一個隊列裡面,等下一輪epoll事件處理完成後再把資料拿出來發送,很可能發送成功(因為上次的資料很可能被完全收走了或者收走了一部分),沒有發送(完成)的資料則繼續等待下一輪epoll事件處理完後再處理。
b) 跟上面類似,非同步connect時監聽一次,串連成功後把out事件去掉。有發送任務時先試著發送一次,很可能就全部發送完成了。不能發送完成則將資料先放入一個隊列裡面,同時添加out事件到epoll裡面,等到out事件到來時發送餘下的資料。
8. 在封裝日誌介面時,最好將日誌介面封裝成一個宏,一個好處是可以自動的嵌入file、line、function等資訊。另外一個好處是,可以先檢查當前這個調用的log level有沒有被enable,如果沒有enable則不會產生任何調用;否則,debug log即使沒有被開啟,還是會進行函數的參數壓棧等操作,而且有些參數本身又涉及到複雜的運算,這個時候會無謂的犧牲大量cpu時間。
9. 很多時候需要用到(hash) map, 在key是一個複雜物件時效能是比較低的。在情境允許的情況下,可以先將對象進行一次hash預先處理,後序的比較操作直接用這個hash值作為key可以大幅提升效能。可以嘗試使用XXH64這個介面,效能好而且衝突率極低。
10. 空間換時間。有時候可能需要比較多的整數轉字串操作,可以利用字典的方式做預先處理,後序的轉換直接查字典就行了。字典大小有限,遇到比較大的整數時,先將整數除以某個值(比如100000)然後分治查字典,最後拼接。
11. 減少臨界資源的數量, 能線程私人的盡量私人化。避免非臨界資源放到鎖裡面,只會延遲鎖的持有時間,增加鎖衝突的機率。如果鄰臨界資源只涉及輕量的cpu運算,盡量用原子操作、自旋鎖、順序鎖、cas等。自旋鎖推薦使用intel 的tbb spin lock。當線程數較多時比pthread_spin_lock高效,而且tbb spin lock有讀寫鎖,而pthread版本則沒有。
12. 情境允許的前提下盡量用長串連,有時候可以大幅提高效能。
13. Stl使用時盡量先reserve,可以大幅降低記憶體配置和資料拷貝的次數。
14. 減少系統調用,比如使用accept4接收一個串連,用sendfile傳輸檔案等。搞不懂為啥accept有對應的accept4,而socket沒有對應的socket4?如果系統再提供一個介面sockctrl(fd, sndsize, rcvsize, sndtimeout, rcvtimeout)就更完美了,調用一次幹四件事情。適當設定send recv緩衝區大小,可以減少send、recv的次數
15. 當在傳送大量資料的時候,為了提高TCP發送效率,可以設定TCP_CORK
16. 對小包的即時性要求比較高的時候,應該設定TCP_NODELAY 以關閉Nagle演算法。
17. 線程在必要的時候綁定到cpu,可以避免環境切換和減少cache miss。
18. worker數目自動適配。外網的服務配置可能不盡相同,伺服器需要根據cpu數目自動分配對應數量的線程。
陷阱相關
1. 在迴圈裡面用迭代器進行刪除的時候要特別注意,關聯容器(map、set)應該用erase(it++), 順序容器(vector、list)應該用it = erase(it)。C++11裡面可以統一為it = erase(it)
2. 慎用tcp快速回收,很容易引起tcp reset。調整net.ipv4.tcp_fin_timeout參數代替快速回收更加靠譜。
3. 系統調用select是個坑,當fd超過1024後就會出問題。尤其當子進程隱式地繼承了父進程的fd後更容易莫名的出問題,盡量用poll、epoll代替。
4. 封裝日誌函數時,要加__attribute__((__format__(__printf__, x, y)))屬性,以便在編譯的時候做參數類型匹配檢查,不然遇到類似LOG(“%s”, 123)調用可以編譯過,但是運行就coredump。
5. 坑爹的時序問題。在多線程環境要尤其注意close(fd)的時序問題,如果有一個全域數組,用fd去索引的話,close(fd)操作必須要放到最後。先調用close(fd),然後再做array[fd]對應的清理會有問題,因為本線程關閉的這個fd很可能被快速分配給了其他線程,多線程同時操作array[fd]這個對象可能造成問題。
6. 過大的mtu值可能導致資料包無法穿過路由器,不要為了提高效能隨意調整。
7. 非同步串連必須加逾時管理。之前遇到過不給非同步串連加逾時管理,而某些事件永遠不通知epoll,導致不斷地建立socket,無形中造成socket控制代碼泄漏。
tcp串連失敗原因分析:
1. 網路不通。看看iptables防火牆規則,確認是否請求被drop。
2. 網路波動。用ping看看是否大量丟包導致。
3. client端分配不到“連接埠”。
a) 如果日誌顯示can not assigne requested address,可以cat /proc/sys/net/ipv4/ip_local_port_range確認一下,必要時調大區間值。
b) 調小tcp_fin_timeout(推薦)或者開啟快速回收(不推薦)
4. server繁忙、處理能力太弱,導致不能及時地accept用戶端的串連
5. 確認是否server端的隊列(核心參數)配置的過小
cat /proc/sys/net/ipv4/tcp_max_syn_backlog
cat /proc/sys/net/core/somaxconn
如果確定是這個問題,調大這2個值。
調試相關
1. 用Strace跟蹤系統調用,strace –etrace進行選擇性地監控系統調用。
2. 加上必要的debug log。平時關閉,出問題時開啟方便跟蹤問題
3. 可以通過prctl(PR_SET_NAME, name)給線程命名,方便調試跟蹤。用ps -eLo nlwp,vsz,sz,stat,wchan,%cpu,%mem,ppid,pid,tid,comm=THREADS,lstart,cmd可以查看各種詳盡資訊,包括線程名字。
推送服務相關
1. 推送伺服器一般需要維持大量的長串連,記憶體往往成為瓶頸,所以需要調整核心參數(下面是幾個基本的)。
a) 調大檔案控制代碼: fs.file-max
b) 調大串連隊列:net.ipv4.tcp_max_syn_backlog、net.core.somaxconn
c) 調小預設的接收、發送緩衝大小net.core.rmem_default、net.core.wmem_default,接收緩衝最好不低於1k,否則容易出問題。
2. 使用新的核心
新版核心做了一些最佳化,將per socket cache變成per task cache,可以大幅降低空閑記憶體的佔用,從而讓建立大量socket成為可能。
3. SO_REUSEPORT
如果要充分利用多cpu、多隊列網卡的優勢,一個接入線程可能是不夠的。但是開多個連接埠明顯是不利於使用的,所以新版的核心(貌似是3.9開始的)支援連接埠重用(SO_REUSEPORT)選項。使得不同的進程可以監聽同一個連接埠,不同的進程可以均勻地accept而不至於引起驚群效應和accept不均勻的問題。大概的原理是:通過對建立串連的(sip, sport, dip, dport)四元組做hash,通過hash值對應到多進程中的一個監聽socket,從而實現串連在多個進程上面的的均勻分配。
安全相關
1. defer accept。延遲accept,當接收到第一個資料之後,三向交握方能完成。同時也可以提高效能
2. 使用防火牆,擋住不需要開放的連接埠或者屏蔽某些黑名單ip、連接埠
3. 避免系統單點。盡量讓伺服器無狀態,有狀態的話可能需要提供主備的模式。或者主主的模式,平時2個都是主,各自處理不同的任務單元,互連心跳,如果一台發現另外一台宕機,則即時接管另外一台的任務,不過需要做好容量預留。
4. 主次邏輯分離、讀寫分離。心跳邏輯可以簡單的用udp實現,跟商務邏輯的tcp串連無關。
監控警示
1. 關鍵地方一定要加詳細的日誌、流水,不然遇到問題很可能無從下手
2. 對訪問量、失敗率、延時分布、錯誤碼等要做上報。如果統計項非常多,用特性統計效能更高(寫共用記憶體)。
3. 在網管、模調系統對特性統計、模調進行警示配置,當發現失敗率高過某個閥值,或者請求量陡增、陡降的時候進行警示。
一點編碼小技巧
1. C++有解構函式,可以方便的清理資源。C沒有解構函式,但是gcc對C做了擴充,藉助cleanup屬性可以完成資源的自動清理,舉例如下:
void Free(char** ptr)
{
If (*ptr != 0)
free(*ptr);
}
void func()
{
char* array __attribute__((cleanup(Free))) = (char*)malloc(1024);
/* 函數退出的時候array被自動釋放 */
}
2. 如果要在進入main之前自動完成某些初始化,可以將初始化操作放到__attribute__((constructor)) void Init() { .. }函數裡面。
3. typeof, 用gcc的typeof關鍵字簡化編碼:
std::map<int, std::map<int, int>> dict;
迭代遍曆的時候可以用typeof(dict.end()) it = dict.begin()代替 std::map<int, std::map<int, int>>::iterator it = dict.begin();
其他
1. 協議必須可擴充, 可以根據業務情境選擇json、protobuf等協議。
2. 逾時管理:
複雜情境下,可能需要用堆、紅/黑樹狀結構、timerfd等機制實現逾時管理。
但是大部分情境下可以用更簡單的方式,這些簡單方式往往也更高效。比如把串連資訊放到一個map裡面,只要定期(比如一秒一次)掃描這個map就行了,注意這裡一定要控制好頻率,否則頻繁的逾時檢測會耗費大量的cpu。一般我的習慣是,一輪epoll事件後取一次tsc(cpu寄存器的計數),然後減去tsc_prev,判斷這個差值有沒有達到閥值(比如1秒),達到閥值才進行逾時檢查。因為讀取tsc非常的快,而且最頻繁一秒才檢查一次逾時,所以這裡的逾時檢測效率還是比較高的。
3. worker間用無鎖隊列通訊。 生產者將訊息壓入隊列,消費者處理完一輪epoll事件後peek一下隊列資料,如果隊列有資料則將資料全部取出,該怎麼處理就怎麼處理; 如果隊列為空白則啥都不做,繼續epol_wait或者幹線程迴圈裡面的其他事情。
伺服器編程的一點心得