伺服器編程的一點心得

來源:互聯網
上載者:User

標籤: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或者幹線程迴圈裡面的其他事情。


伺服器編程的一點心得

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.