楊建:網站加速--服務器編寫篇(上)

楊建:網站加速--服務器編寫篇(上)(2008-12-08 20:04:03)
--提升性能的同時爲你節約10倍以上成本
From: http://blog.sina.com.cn/iyangjian

一,如何節約CPU
二,怎樣使用內存
三,減少磁盤I/O
四,優化你的網卡
五,調整內核參數
六,衡量Web Server的性能指標
七,NBA js直播的發展歷程
八,新浪財經實時行情繫統的歷史遺留問題 (7 byte = 10.68w RMB/year)
  -----------------------------------------------------------------------------------------

一,如何節約CPU

1,選擇一個好的I/O模型(epoll, kqueue)
3年前,我們還關心c10k問題,隨着硬件性能的提升,那已經不成問題,但如果想讓PIII 900服務器支撐5w+ connections,還是需要些能耐的。

epoll最擅長的事情是監視大量閒散連接,批量返回可用描述符,這讓單機支撐百萬connections成爲可能。linux 2.6以上開始支持epoll,freebsd上相應的有kqueue,不過我個人偏愛linux,不太關心kqueue。

邊緣觸發ET 和 水平觸發LT 的選擇:
早期的文檔說ET很高效,但是有些冒進。但事實上LT使用過程中,我苦惱了將近一個月有餘,一不留神CPU 利用率99%了,可能是我沒處理好。後來zhongying同學幫忙把驅動模式改成了ET模式,ET既高效又穩定。

簡單地說,如果你有數據過來了,不去取LT會一直騷擾你,提醒你去取,而ET就告訴你一次,愛取不取,除非有新數據到來,否則不再提醒。

重點說下ET,非阻塞模式,
man手冊說,如果ET提示你有數據可讀的時候,你應該連續的讀一直讀到返回 EAGAIN or EWOULDBLOCK 爲止,但是在具體實現中,我並沒有這樣做,而是根據我的應用做了優化。因爲現在操作系統絕大多數實現都是最大傳輸單元值爲1500。  MTU:1500 - ipheader:20 - tcpheader:20 = 1460 byte .  
HTTP header,不帶cookie的話一般只有500+ byte。留512給uri,也基本夠用,還有節餘。

如果請求的header恰巧比這大是2050字節呢?
會有兩種情況發生:1,數據緊挨着同時到達,一次read就搞定。 2,分兩個ethernet frame先後到達有一定時間間隔。
我的方法是,用一個比較大的buffer比如1M去讀header,如果你很確信你的服務對象請求比1460小,讀一次就行。如果請求會很大分幾個ethernet frame先後到達,也就是恰巧你剛剛read過,它又來一個新數據包,ET會再次返回,再處理下就是了。

順便再說下寫數據,一般一次可以write十幾K數據到內核緩衝區。
所以對於很多小的數據文件服務來說,是沒有必要另外爲每個connections分配發送緩衝區。
只有當一次發送不完時候才分配一塊內存,將數據暫存,待下次返回可寫時發送。
這樣避免了一次內存copy,而且節約了內存。

選擇了epoll並不代表就就擁有了一個好的 I/O模型,用的不好,你還趕不上select,這是實話。
epoll的問題我就說這麼多,關於描述符管理方面的細節請參見我早期的一個帖子,epoll模型的使用及其描述符耗盡問題的探討  大概討論了18頁,我剛纔把解決方法放在第一個帖子裏了。如果你對epoll有感興趣,我這有 一個簡單的基於epoll的web server例子

另外你要使用多線程,還是多進程,這要看你更熟悉哪個,各有好處。
多進程模式,單個進程crash了,不影響其他進程,而且可以爲每個worker分別幫定不同的cpu,讓某些cpu單獨空出來處理中斷和系統事物。多線程,共享數據方便,佔用資源更少。進程或線程的個數,應該固定在 (cpu核數-1) ~ 2倍cpu核數間爲宜,太多了時間片輪轉時會頻繁切換,少了,達不到多核併發處理的效果。

還有如何accept也是一門學問,沒有最好,只有更適用,你需要做很多實驗,確定對自己最高效的方式。有了一個好的I/O框架,你的效率想低也不容易,這是程序實現的大局。

關於更多網絡I/O模型的討論請見 <Scalable Network Programming > 中文版
另外,必須強調的是,代碼和結構應該簡潔高效,一定要具體問題具體分析,沒什麼法則是萬能的,要根據你的服務量身定做。

2,關閉不必要的標準輸入和標準輸出
close(0);  //stdin
close(1);  //stdout
如果你不小心,有了printf輸出調試信息,這絕對是一個性能殺手。
一個高性能的服務器不出錯是不應該有任何輸出的,免得耽誤幹活。
這樣做,至少能爲你節約兩個描述符資源。

3,避免用鎖 (i++ or ++i )
多線程編程用鎖是普遍現象,貌似已經成爲習慣。
但各線程最好是獨立的,不需要同步機制的。
鎖會消耗資源,而且造成排隊,甚至死鎖,儘量想辦法避免。
非用不可時候,比如,實時統計各線程的負載情況,多個線程要對全局變量進行寫操作。
請用 ++i ,因爲它是一個原子操作。

4,減少系統調用
系統調用是很耗的,因爲它通常需要鑽進內核再鑽出來。
我們應該避免用戶空間和內核空間的切換。
比如我要爲每個請求打個時間戳,以計算超時,我完全可以在返回一批可用描述符前只調用一次time(),而不用每個請求都調用一次。 time()只精確到秒,一批請求處理都是毫秒級,所以也沒必要那麼做,再說了,計算超時誤差那麼一秒有什麼影響嗎?

5, Connection: close vs  Keep-Alive ?
談httpd實現,就不能不提長連接Keep-Alive 。
Keep-Alive是http 1.1中加入的,現在的瀏覽器99。99%應該都是支持Keep-Alive的。

先說下什麼是Keep-Alive:
這是基於tcp的connections說的,也就是一個描述符(fd),它並不代表獨立佔用一個進程或線程。一個線程用非阻塞模式可以保持成千上萬個長連接。

先說一個完整的HTTP 1.0的請求和響應:
建立tcp連接 (syn; ack, syn2; ack2; 三個分組握手完成)
請求
響應
關閉連接 (fin; ack; fin2; ack2  四個分組關閉連接)

再說HTTP 1.1的請求和響應:
建立tcp連接 (syn; ack, syn2; ack2; 三個分組握手完成)
請求
響應
...
...

請求
響應
關閉連接 (fin; ack; fin2; ack2  四個分組關閉連接)

如果請求和響應都只有一個分組,那麼HTTP 1.0至少要傳輸11個分組(補充:請求和響應數據還各需要一個ack確認),纔拿到一個分組的數據。而長連接可以更充分的利用這個已經建立的連接,避免的頻繁的建立和關閉連接,減少網絡擁塞。

我做過一個測試,在2cpu*4core服務器上,不停的accept,然後不做處理,直接close掉。一秒最多可以accept  7w/s,這是極限。那麼我要是想每秒處理10w以上的http請求該怎麼辦呢?
目前唯一的也是最好的選擇,就是保持長連接。
比如我們NBA JS直播頁面,剛打開就會向我的js服務器發出6個http請求,而且隨後平均每10秒會產生兩個請求。再比如,我們很多頁面都會嵌幾個靜態池的圖片,如果每個請求都是獨立的(建立連接然後關閉),那對資源絕對是個浪費。

長連接是個好東西,但是選擇 Keep-Alive必須根據你的應用決定。比如NBA JS直播,我肯定10秒內會產生一個請求,所以超時設置爲15秒,15秒還沒活動,估計是去打醬油了,資源就得被我回收。超時設置過長,光連接都能把你的服務器堆死。

爲什麼有些apache服務器,負載很高,把Keep-Alive關掉負載就減輕了呢?
apache 有兩種工作模式,prefork和worker。apache 1.x只有,prefork。
prefork比較典型,就是個進程池,每次創建一批進程,還有apache是基於select實現的。在用戶不是太多的時候,長連接還是很有用的,可以節約分組,提升響應速度,但是一旦超出某個平衡點,由於爲了保持很多長連接,創建了太多的進程,導致系統不堪重負,內存不夠了,開始換入換出,cpu也被很多進程吃光了,load上去了。這種情況下,對apache來說,每次請求重新建立連接要比保持這麼多長連接和進程更划算。


6,預處理 (預壓縮,預取lastmodify,mimetype)
預處理,原則就是,能預先知道的結果,我們絕不計算第二次。

預壓縮:我們在兩三年前就開始使用預壓縮技術,以節約CPU,偉大的微軟公司在現在的IIS 7中也開始使用了。所謂的預壓縮就是,從數據源頭提供的就是預先壓縮好的數據,IDC同步傳輸中是壓縮狀態,直到最後web server輸出都是壓縮狀態,最終被用戶瀏覽器端自動解壓。

預取lastmodify:  文件的lastmodify時間,如果不更新,我們不應該取第二次,別忘記了fsat這個系統調用是很耗的。

預取mimetype: mimetype,如果你的文件類型不超過256種,一個字節就可以標識它,然後用數組下標直接輸出,而且不是看到一個js文件,然後strcmp()了近百種後綴名後,才知道應該輸出Content-Type: application/x-javascript,而且這種方法會隨文件類型增加而耗費更多cpu資源。當然也可以寫個hash函數來做這事,那也至少需要一次函數調用,做些求值運算,和分配比實際數據大幾倍的hash表。

如何更好的使用cpu一級緩存
數據分解
CPU硬親和力的設置
待補充。。。。

二,怎樣使用內存

1,避免內存copy (strcpy,memcpy)
雖然內存速度很快,但是執行頻率比較高的核心部分能避免copy的就儘量別使用。如果必須要copy,儘量使用memcpy替代 sprintf,strcpy,因爲它不關心你是否遇到'/0'; 內存拷貝和http響應又涉及到字符串長度計算。如果能預先知道這個長度最好用中間變量保留,增加多少直接加上去,不要用strlen()去計算,因爲它會數數直到遇見'/0'。能用sizeof()的地方就不要用strlen,因爲它是個運算符,在預編的時被替換爲具體數字,而非執行時計算。

2,避免內核空間和用戶進程空間內存copy (sendfile, splice and tee)
sendfile: 它的威力在於,它爲大家提供了一種訪問當前不斷膨脹的Linux網絡堆棧的機制。這種機制叫做“零拷貝(zero-copy)”,這種機制可以把“傳輸控制協議(TCP)”框架直接的從主機存儲器中傳送到網卡的緩存塊(network card buffers)中去,避免了兩次上下文切換。詳細參見 <使用sendfile()讓數據傳輸得到最優化> 。據同事測試說固態硬盤SSD對於小文件的隨機讀效率很高,對於更新不是很頻繁的圖片服務,讀卻很多,每個文件都不是很大的話,sendfile+SSD應該是絕配。

splice and tee: splice背後的真正概念是暴露給用戶空間的“隨機內核緩衝區”的概念。“也就是說,splice和tee運行在用戶控制的內核緩衝區上,在這個緩衝區中,splice將來自任意文件描述符的數據傳送到緩衝區中(或從緩衝區傳送到文件描述符),而tee將一個緩衝區中的數據複製到另一個緩衝區中。因此,從一個很真實(而抽象)的意義上講,splice相當於內核緩衝區的read/write,而tee相當於從內核緩衝區到另一個內核緩衝區的memcpy。”。本人覺得這個技術用來做代理,很合適。因爲數據可以直接從一個soket到另一個soket,不需要經用戶和內核空間的切換。這是sendfile不支持的。詳細參見 <linux2.6.17以上內核中的 splice and tee> ,具體實例請參見  man 2  tee ,裏面有個完整的程序。

3,如何清空一塊內存(memset ?)
比如有一個buffer[1024*1024],我們需要把它清空然後strcat(很多情況下可以通過記錄寫的起始位置+memcpy來代替)追加填充字符串。
其實我們沒有必要用memset(buffer,0x00,sizeof(buffer))來清空整個buffer, memset(buffer,0x00,1)就能達到目的。 我平時更喜歡用buffer[0]='/0'; 來替代,省了一次函數調用的開銷。

4,內存複用  (有必要爲每個響應分配內存 ?)
對於NBA JS服務來說,我們返回的都是壓縮數據,99%都不超過15k,基本一次write就全部出去了,是沒有必要爲每個響應分配內存的,公用一個buffer就夠了。如果真的遇到大數據,我先write一次,剩下的再暫存在內存裏,等待下次發送。

5,避免頻繁動態申請/釋放內存(malloc)
這個似乎不用多說,要想一個Server啓動後成年累月的跑,就不應該頻繁地去動態申請和釋放內存。原因很簡單一,避免內存泄露。二,避免碎片過多。三,影響效率。一般來說,都是一次申請一大塊內存,然後自己寫內存分配算法。爲http用戶分配的緩衝區生命期的特點是,可以隨着fd的關閉,而回收,避免漏網。還有Server的編寫者應該對自己設計的程序達到最高支撐量的時候所消耗的內存心中有數。

6,字節對齊
先看下面的兩個結構體有什麼不同:
struct A {
        short size; 
        char *ptr;
        int left;
} a ;

struct B {
        char *ptr;
        short size; 
        int left;
} b ;

僅僅是一個順序的變化,結構體B順序是合理的:
在32bit linux系統上,是按照32/8bit=4byte來對齊的, sizeof(a)=12 ,sizeof(b)=12 。
在64bit linux系統上,是按照64/8bit=8byte來對齊的, sizeof(a)=24 ,sizeof(b)=16 。
32bit機上看到的A和B結果大小是一樣的,但是如果把int改成short效果就不一樣了。

如果我想強制以2byte對齊,可以這樣:
#pragma pack(2)
struct A {
        short size; 
        char *ptr;
        int left;
} a ;
#pragma pack()
注意pack()裏的參數,只能指定比本機支持的字節對齊標準小,而不能更大。

7,內存安全問題
先舉個好玩的例子,不使用a,而給a賦上值:
int main()
{
        char a[8];
        char b[8];
        memcpy(b,"1234567890/0",10);
        printf("a=%s/n",a);
        return 0;
}
程序輸出  a=90 。
這就是典型的溢出,如果是空閒的內存,用點也就罷了,可是把別人地盤上的數據覆蓋了,就不好了。
接收的用戶數據一定要嚴格判斷,確定不會越界,不是每個人都按規矩辦事的,搞不好就掛了。

8,雲風的內存管理理論 (sd2c大會所獲 blog & ppt
沒有永遠不變的原則
大原則變化的慢
沒有一勞永逸的解決方案
內存訪問很廉價但有代價
減少內存訪問的次數是很有意義的
隨機訪問內存慢於順序訪問內存
請讓數據物理上連續
集中內存訪問優於分散訪問
儘可能的將數據緊密的存放在一起
無關性內存訪問優於相關性內存訪問
請考慮並行的可能性、即使你的程序本身沒有使用並行機制
控制週期性密集訪問的數據大小
必要時採用時間換空間的方法
讀內存快於寫內存
代碼也會佔用內存,所以、保持代碼的簡潔

物理法則
晶體管的排列
批量回收內存
不釋放內存,留給系統去做
list map  vector (100次調用產生13次內存分配和釋放)
長用字符串做成hash,使用指針訪問
直接內存頁處理控制

三,減少磁盤I/O
這個其實就是通過儘可能的使用內存達到性能提高和i/o減少。從系統的讀寫buffer到用戶空間自己的cache,都是可以有效減少磁盤i/o的方法。用戶可以把數據暫存在自己的緩衝區裏,批量讀寫大塊數據。cache的使用是很必要的,可以自己用共享內存的方法實現,也可以用現成的BDB來實現。歡迎訪問我的公益站點 berkeleydb.net ,不過我不太歡迎那種問了問題就跑的人。BDB默認的cache只有256K,可以調大這個數字,也可以純粹使用Mem Only方法。對於預先知道的結果,爭取不從磁盤取第二次,這樣磁盤基本就被解放出來了。BDB取數據的速度每秒大概是100w條(2CPU*2Core Xeon(R) E5410 @ 2.33GHz環境測試,單條數據幾十字節),如果你想取得更高的性能建議自己寫。


四,優化你的網卡
首先ethtool ethx 看看你的外網出口是不是Speed: 1000Mb/s 。
對於多核服務器,運行top命令,然後按一下1,就能看到每個核的使用情況。如果發現cpuid=0的那顆使用率明顯高於其他核,那就說明id=0的cpu將來也許會成爲你的瓶頸。然後可以用mpstat(非默認安裝)命令查看系統中斷分佈,用cat /proc/interrupts 網卡中斷分佈。

下面這個數據是我們已經做過優化了的服務器中斷分佈情況:
[yangjian2@D08043466 ~]$ mpstat -P ALL 1
Linux 2.6.18-53.el5PAE (D08043466)      12/15/2008
01:51:27 PM  CPU   %user   %nice    %sys %iowait    %irq   %soft  %steal   %idle    intr/s
01:51:28 PM  all    0.00    0.00    0.00    0.00    0.00    0.00    0.00  100.00   1836.00
01:51:28 PM    0    0.00    0.00    0.00    0.00    0.00    0.00    0.00  100.00    179.00
01:51:28 PM    1    0.00    0.00    0.00    0.00    0.00    0.00    0.00  100.00    198.00
01:51:28 PM    2    1.00    0.00    0.00    0.00    0.00    0.00    0.00  100.00    198.00
01:51:28 PM    3  
發佈了183 篇原創文章 · 獲贊 4 · 訪問量 38萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章