实践:服务器编写/系统架构/cache
從基本HTTP協議,服務器編寫(只講思路),到完整系統搭建(包括負載均衡LVS,IDC分布,DNS解析),到瀏覽器緩存的使用(重點講述),結合線上實例圖文講解如何以最低廉的成本構建快速,高并發,高可用,可擴展的Web服務。最后將拿本公司一些線上產品做實例分析。如果能靈活應用這些方法,你也許會發現對于某些網站來說節約10倍成本,其實是個很保守的說法。
- 一、服務器編寫篇
- 二、系統架構篇
- 三、Cache為王篇
- 四、實例分析篇
- 五、動態應用篇
-----------------------------------------------------------------------------------------
服務器編寫
一,如何節約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,也基本夠用,還有節余。
更正:如果只讀前1460個字節的header的話,99.999%的用戶都能正常訪問,但是有一兩個用戶是通過某http proxy來上網,這種用戶請求的特征是會帶2k以上的垃圾信息過來,類似于cookie,但又不是,重要的http?header被放在了最后,導致我認為是非法請求,所以現在來看,為了避免被投訴,還是建議讀取到"\r\n\r\n"作為結束,性能的影響也是比較有限的.???(最后修改時間 2010.05.11)
如果請求的header恰巧比這大是2050字節呢?
會有兩種情況發生:1,數據緊挨著同時到達,一次read就搞定。 2,分兩個ethernet frame先后到達有一定時間間隔。
我的方法是,用一個比較大的buffer比如1M去讀header,如果你很確信你的服務對象請求比1460小,讀一次就行。如果請求會很大分幾個ethernet frame先后到達,也就是恰巧你剛剛read過,它又來一個新數據包,ET會再次返回,再處理下就是了。
更正:?在追蹤通過http proxy上網的用戶問題中發現,這個方法并不嚴謹:1,?2050字節有可能會分兩個,以及兩個以上的ethernet frame到達,雖然我測試還沒遇到3個的情況,但是現在我斷定它是會存在的. 2,即使比1460字節小的數據,也不敢保證裝載在一個ethernet frame里,仍然有可能分多次到達.?所以用1M的buffer去讀,返回數據比1460小,并不代表數據已經讀完,只能說明,本次讀完了,下次再有數據到達時會ET提示你.測試中發現,對于大于1460字節的頭的讀區,如果分多次到達,第一次返回為1460字節的概率大一些而已. 測試中甚至還遇到了另外一個臨界情況,就是用1M的buffer剛讀完數據,立刻再接著讀,有可能會讀到數據,這個數據應該是緊接著到達,很小的時間間隔,如果你不去讀,我認為ET應該會再次返回(僅僅出現過一次,未能驗證,有時間再求證).??(最后修改時間 2010.05.11)
?
順便再說下寫數據,一般一次可以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 ,因為它是一個原子操作。
更正:?pinggao,同學對++i的原子性提出了質疑,并做了個測試程序,經過討論得出結論:++i在多線程的程序中,在單核環境下,不一定是原子的,在多核環境下肯定不是原子的,我把這個當作原子操作用在了統計中,所以我得出的統計值是要比實際處理能力小很多的。。。虧大了。。等有新的數據再更正過來。。想想犯這個基本錯誤的原因,當初我們大學教授在講++i是原子操作的時候,距離現在計算機環境發生了巨大變化。?++i原子性討論??(最后修改時間 2010.10.02)
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????0.00????0.00????0.00????0.00????0.00????0.00????0.00??100.00????346.00
01:51:28 PM????4????0.00????0.00????0.00????0.00????0.00????0.00????0.00??100.00????207.00
01:51:28 PM????5????0.00????0.00????0.00????0.00????0.00????0.00????0.00??100.00????167.00
01:51:28 PM????6????0.00????0.00????0.00????0.00????0.00????0.00????0.00??100.00????201.00
01:51:28 PM????7????0.00????0.00????0.00????0.00????0.00????0.00????0.00??100.00????339.00
沒優化過的應該是這個樣子:
yangjian2@xk-6-244-a8 ~]$ mpstat -P ALL 1
Linux 2.6.18-92.1.6.el5 (xk-6-244-a8.bta.net.cn)????????12/15/2008
02:05:26 PM??CPU???%user???%nice????%sys %iowait????%irq???%soft??%steal???%idle????intr/s
02:05:27 PM??all????0.00????0.00????0.00????0.12????0.00????0.00????0.00???99.88???1593.00
02:05:27 PM????0????0.00????0.00????0.00????0.00????0.00????0.00????0.00??100.00???1590.00
02:05:27 PM????1????0.00????0.00????0.00????0.00????0.00????0.00????0.00??100.00??????0.00
02:05:27 PM????2????0.00????0.00????0.00????0.00????0.00????0.00????0.00??100.00??????2.00
02:05:27 PM????3????0.00????0.00????0.00????0.00????0.00????0.00????0.00??100.00??????0.00
02:05:27 PM????4????0.00????0.00????0.00????0.00????0.00????0.00????0.00??100.00??????0.00
02:05:27 PM????5????0.00????0.00????0.00????0.00????0.00????0.00????0.00??100.00??????0.00
02:05:27 PM????6????0.00????0.00????0.00????0.00????0.00????0.00????0.00??100.00??????0.00
02:05:27 PM????7????0.00????0.00????0.00????0.00????0.00????0.00????0.00??100.00??????0.00
對于32bit的centos5,mpstat -P ALL 1表現跟第一種情況一樣,分布比較平均,但是一但有了訪問量,就可以看到差距。cat /proc/interrupts 看起來更直觀些,很清楚的知道哪個網卡的中斷在哪個cpu上處理。
其實,當你遇到網卡中斷瓶頸的時候證明你的網站并發度已經相當高了,每秒三五萬個請求還至于成為瓶頸。除非你的應用程序同時也在消耗cpu0的資源。對于這種情況,建議使用多進程模式,每個進程用 sched_setaffinity綁定特定的cpu,把cpu0從用戶事物中解放出來,專心處理系統事物,當然包括中斷。這樣你的極限應該能處理20w+ http req/s (2CPU*4Core服務器)。但是對于多線程模式來說,我們就顯得無能為力了,因為我們如果想使用多核,就沒法不用cpu0。目前的方法只有兩個:一,轉化為多進程,然后進程內再使用多線程。二,讓你的網卡中斷分散在多個cpu上(目前只有硬件解決方案,感謝xiaodong2提供的技術支持)。 (修正:后來仔細讀了幾遍man手冊,發現sched_setaffinity綁定特定的cpu對于多線程也是適用的,并且實驗通過,只需要將第一個參數置為0。這對cpu0的解放是個很好的發現。)
將網卡中斷分散在多個cpu硬件解決方案: 我們新加了一塊網卡(前提是這個網卡支持中斷分布),然后通過通過linux bonding將兩個網卡比如eth0,eth1聯合成一個通道bond0(當然這里還涉及到交換機的調整),然后bond0就有了2G的帶寬吞吐量。把eth0的中斷處理幫定在cpu 0-3,把eth1中斷處理幫定在cpu 4-7,這樣中斷就被分布開了。這樣會帶來一些額外的cpu開銷,但是跟好處相比可以忽略不計。我在網卡優化過的32bit服務器上測試http請求處理極限為 40w+ req/s,將近提升了一倍。
五,調整內核參數
我的內核心參數調整原則是,哪個遇到瓶頸調哪個,謹慎使用,不能憑想象亂調一氣。看下面例子,其中default是我們公司定做的系統默認的一些參數值。add by yangjian2并非全部都要調整,我只挑幾個比較重要的參數說明一下,更多TCP方面的調優請參見 man 7 tcp 。
#++++++++++++++++++default++++++++++++++++++++++++++++++
net.ipv4.tcp_syncookies = 1
net.ipv4.tcp_max_tw_buckets = 180000
net.ipv4.tcp_sack = 1
net.ipv4.tcp_window_scaling = 1
net.ipv4.tcp_rmem = 4096????????87380???4194304
net.ipv4.tcp_wmem = 4096????????16384???4194304
#++++++++++++++++++add by yangjian2++++++++++++++++++++++
net.ipv4.tcp_max_syn_backlog = 65536
net.core.netdev_max_backlog =??32768
net.core.somaxconn = 32768
net.core.wmem_default = 8388608
net.core.rmem_default = 8388608
net.core.rmem_max = 16777216
net.core.wmem_max = 16777216
net.ipv4.tcp_timestamps = 0
net.ipv4.tcp_synack_retries = 2
net.ipv4.tcp_syn_retries = 2
net.ipv4.tcp_tw_recycle = 1
#net.ipv4.tcp_tw_len = 1
net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_mem = 94500000 915000000 927000000
net.ipv4.tcp_max_orphans = 3276800
#++++++++++++++++++++++++++++++++++++++++++++++++++++++++
maxfd: 對于系統所能打開的最大文件描述符fd,可以通過以root啟動程序,setrlimit()設置maxfd后,再通過setuid()轉為普通用戶提供服務,我用的 int set_max_fds(int maxfds); 函數是zhongying提供的。這比用ulimit來的方便的多,不曉得為什么那么多開源軟件都沒這樣用。
net.ipv4.tcp_max_syn_backlog = 65536 : 這個參數可以肯定是必須要修改的,默認值1024,我google了一下,幾乎是人云亦云,沒有說的明白的。 要講明白得從man listen說起,int listen(int sockfd, int backlog);??早期的網絡編程都中描述,int backlog 代表未完成隊列SYN_RECV狀態+已完成隊列ESTABLISHED的和。但是這個意義在Linux 2.2以后的實現中已經被改變了,int backlog只代表已完成隊列ESTABLISHED的長度,在AF_INET協議族中(我們廣泛使用的就是這個),當int backlog大于SOMAXCONN (128 in Linux 2.0 & 2.2)的時候,會被調整為常量SOMAXCONN大小。這個常量可以通過net.core.somaxconn來修改。而未完成隊列大小可以通過net.ipv4.tcp_max_syn_backlog來調整,一般遭受syn flood攻擊的網站,都存在大量SYN_RECV狀態,所以調大tcp_max_syn_backlog值能增加抵抗syn攻擊的能力。
net.ipv4.tcp_syncookies = 1 :?當出現syn等候隊列出現溢出時象對方發送syncookies。目的是為了防止syn flood攻擊 , 默認值是 0。?不過man listen說當啟用syncookies時候,tcp_max_syn_backlog的sysctl調整將失效,和這個描述不是很符合。參見下面兩個描述分別是man listen和man 7 tcp:
When syncookies are enabled there is no logical maximum length and this tcp_max_syn_backlog sysctl??setting??is??ignored.
Send out??syncookies??when the syn backlog queue of a socket overflows.
但我可以肯定的說這個選項對你的性能不會有提高,而且它嚴重的違背TCP協議,不允許使用TCP擴展,除非遭受攻擊,否則不推薦使用。
net.ipv4.tcp_synack_retries = 2?:?對于遠端的連接請求SYN,內核會發送SYN + ACK數據報,以確認收到上一個 SYN連接請求包。這是所謂的三次握手( threeway handshake)機制的第二個步驟。這里決定內核在放棄連接之前所送出的 SYN+ACK 數目。如果你的網站SYN_RECV狀態確實挺多,為了避免syn攻擊,那么可以調節重發的次數。
net.ipv4.tcp_syn_retries = 2 :?對于一個新建連接,內核要發送多少個 SYN 連接請求才決定放棄。不應該大于255,默認值是5,對應于180秒左右。這個對防止syn攻擊其實是沒有用處的,也沒必要調節。
net.ipv4.tcp_max_orphans = 3276800 : 這個最好不要修改,因為每增加1,將消耗~64k內存。即使報錯 TCP: too many of orphaned sockets 也有可能是由于你的net.ipv4.tcp_mem過小,導致的Out of socket memory,繼而引發的。
net.ipv4.tcp_wmem = 4096 16384 4194304 :??為自動調優定義每個socket使用的內存。第一個值是為socket的發送緩沖區分配的最少字節數。第二個值是默認值(該值會被 wmem_default覆蓋),緩沖區在系統負載不重的情況下可以增長到這個值。第三個值是發送緩沖區空間的最大字節數(該值會被wmem_max覆蓋)。
net.ipv4.tcp_rmem = 4096 87380 4194304 : 接收緩沖區,原理同上。
net.ipv4.tcp_mem = 94500000 915000000 927000000 :
low:當TCP使用了低于該值的內存頁面數時,TCP不會考慮釋放內存。
pressure:當TCP使用了超過該值的內存頁面數量時,TCP試圖穩定其內存使用,進入pressure模式,當內存消耗低于low值時則退出pressure狀態。
high:允許所有tcp sockets用于排隊緩沖數據報的內存頁數。
一般情況下這個值是在系統啟動時根據系統內存數量計算得到的,如果你的dmesg報 Out of socket memory,你可以試著修改這個參數,順便介紹3個修改方法:
1, echo "94500000 915000000 927000000" > /proc/sys/net/ipv4/tcp_wmem
2, sysctl -w "net.ipv4.tcp_mem = 94500000 915000000 927000000"
3, net.ipv4.tcp_mem = 94500000 915000000 927000000??(vi /etc/sysctl.conf?然后?sysctl -p生效)
下面命令也許能提供些信息,在你修改tcp參數時做個參考:
[sports@xk-6-244-a8 nbahttpd_beta4.0]$ cat??/proc/net/sockstat
sockets: used 1195
TCP: inuse 1177 orphan 30 tw 199 alloc 1181 mem 216
UDP: inuse 0 mem 0
RAW: inuse 0
FRAG: inuse 0 memory 0
其他我就不多說了,知道這些基本就能解決絕大部分問題了。
六,衡量Web Server的性能指標
我認為一個好的Server應該能在有限的硬件資源上將性能發揮到極限。
Web Server的衡量指標并非單一,要根據具體應用類型而定。比如財經實時圖片系統,我們關注它每秒輸出圖片數量。NBA js直播放系統,我們關心他的同時在線connections和當時的每秒請求處理量。行情系統,我們關心它connections和請求處理量的同時還要關心每個請求平均查詢多少支股票。但總體來說同時在線connections和當時的每秒請求處理量是兩個最重要的指標。
對于圖片系統再說一句,我覺得大圖片和小圖片是應該區別對待的,小圖片不應該產生磁盤 I/O 。
Nginx是我見過的Web Server中性能比較高的一個,他幾乎是和我的server同時誕生,可能還更早些,框架很不錯,我覺得目前版本稍微優化下,支持10w connections不成問題。 lighttpd也不錯,我對他的認識還是停留在幾年前的性能測試上,它的性能會比nginx遜色一些。他們都支持epoll,sendfile,可以起多個進程worker,worker內部使用非阻塞,這是比較優良的I/O的模型。Squid,Apache,都是骨灰級軟件了,好處就是支持的功能多,另許多輕量級Server望塵莫及,可是性能太一般了,祝愿他們早日重寫。
插點小插曲,我在財經項目組的時候,有的同事來我們組一年多了,問我是不是管機器的,我點點頭,后來又有比較了解我的同事說我是系統管理員,我說“恩”。其實我的主業是寫程序的。也許是我太低調了,覺得那些陳年往事不值再提,以至于別人對我做的東西了解甚少, 今天我就高調一把,公布一些我寫的程序的性能指標。我們的系統近幾年來說在性能上是領先業內的(不爭世界第一,那樣壓力太大,第二就很好,也許正在看我blog的你一不留神就把我超了呢 ^-^ ),高效的原因很重要的一點是由于它是根據服務特點量身訂做的。
實驗環境數據: 我寫了個HTTP服務框架,不使用磁盤I/O,簡化了邏輯處理部分,只會輸出 "hello world!"?程序部署在192.168.0.1上(2cup*4Core,硬件和系統都做過優化),我在另外8臺同等配置服務器上同時執行命令??./apache/bin/ab -c 1000??-n 3000000???-k??"http://192.168.0.1/index.html" 幾乎同時處理完畢,總合相加 40w req/s,我相信這是目前硬件水平上的極限值 。
真實環境數據:2cup*4Core Mem 16G, 64bit centos5,單機23w+ connections, 3.5w req/s時,CPU總量消耗 1/8,內存消耗0.4%(相當于正好消耗了一個Core+64M Mem)。在30w+ connections, 4.6w req/s 時,CPU總量消耗 1/4,內存消耗 0.5% 。保守地說,只要把網卡中斷分散一下,單機50w+ connections很easy。??更多數據圖文參見“NBA js直播的發展歷程”一節。
有些人了解我是由于財經的實時行情系統,雖然每天處理近百億的http請求處理量還不錯,但那并非我的得意之作,相反我覺得那個寫的有些粗糙,至少有一倍以上的性能提升空間。對于行情系統,我還是很想把它做成push的,目標仍然是單機50w+在線,無延遲推送,可惜本人js功底太爛,所以要作為一個長期的地下項目去做,如果可能,我想一開始就把它作為一個開源項目來做。
我個人比較喜歡追求性能極限,公司對此暫時還不是很認可,或者說重視程度還不夠,可能是由于我們的硬件資源比較充裕吧。盡管如此,只要我認為對企業有價值的,就依然會堅持做下去,我的目標是獲得業界的認可。同時我相信中國的未來不缺乏互聯網用戶,當有人燒不起錢的時候想起了我,那我就是有價值的。
這里說的有點多了,不過放心,ppt我會做的相當簡單。
七,NBA js直播的發展歷程
這一節就談下這個項目發展過程中所遇到的瓶頸,以及如何解決的。
應該是06年吧,當時NBA 比賽比較火,woocall負責高速模式圖文直播放,普通模式和動態比分數據等都放在一群破服務器上,大概有十幾20臺,這些破服務器有些扛不住了。
因為第二天有一場比較大的比賽,我想埋連接在線上測一下效果,于是連夜把財經實時行情server改寫成了NBA JS直播server. 當時有兩臺 Intel(R) Xeon(TM) CPU 3.00GHz 雙cpu的服務器,在F5后面。先啟用一臺服務器,比賽開始前靜悄悄的,不一會,迅速串到了20w connections,再往上增長,就慢的幾乎不可訪問, ethtool eth0??, Speed: 100Mb/s, 網卡出口帶寬跑滿了(那時候支持千兆的交換機還不多)。迅速把另一臺服務器啟用,后來又卡了,好象是F5處理能力不足。 后來升級服務器出口帶寬為1G,當然這需要交換機支持千兆口,更換網線,服務器也從F5后面轉移出來,通過DNS直接輪詢。
后來又測試了幾次,等到新申請的Intel(R) Xeon(R) CPU 5120??@ 1.86GHz, 雙核雙cpu服務器一到,就開始大規模部署,比賽更火了,不巧的是行情也火了起來,我的財經實時圖片系統和行情系統也是帶寬殺手,同時我也成了服務器殺 手,到處蹭服務器。IDC帶寬出口開始告急,我每天開始關注哪個機房還有富余帶寬,有的機房被我們跑的太滿,開始有人勸我們遷移到別的機房。后來 yangguang4勸我支持gzip輸出,我覺得動態壓縮太耗費cpu,不知道預先壓縮是否可行,寫個程序試了一把,沒問題,于是NBA JS直播的的帶寬一下子被砍掉了70%,而且沒浪費一點我們的cpu,賺大了。
之后的兩年里NBA JS服務一直很穩定,我幾乎都沒怎么看過,2007年的一場體育賽事中,單機達到25w+ connections,2.86w req/s ,cpu空閑30% ,見下圖 (2CPU*2Core 1.86GHZ服務器) 。直到奧運期間,有場賽事,woocall瞬間仍給我近200w connections,網通的服務器被秒殺了1/3。 這其實就是善意的DDOS攻擊,這些用戶如果正常進入是沒有問題了,瞬間扔過來,超出了操作系統極限,系統掛掉了,我的服務也over了。在調整內核參數里有講,怎么修改內核參數提高服務器抗秒殺能力,但是不能杜絕。
下圖為2007年一場比賽時,單機25w+ connections,2.86w req/s,的狀態(2CPU*2Core 1.86GHZ):
?
奧運結束后,我對服務器程序和架構做了調整,砍掉了2/3的服務器。 但我沒留意的是,同樣connections,實際http請求增加了一倍,因為新上了一個flash方位圖,里面增加了3個js,增加就增加吧,既然砍了就不準備再恢復了。但是服務在2CPU*4Core centos5 32bit上的表現卻讓我很失望,跑不過2CPU*2Core centos4 32bit 。 我開始懷疑程序升級的時候是不是有什么地方沒考慮到,開始調程序,折騰幾天沒有結果,癥狀是單機支撐12.5萬時候沒有任何異常,內存使用1%左右,總cpu使用了5%左右,load 0.5,但是再增加0.1w用戶server肯定崩潰,每次都是相同的表現,我知道在什么地方卡住了。 后來看了下dmesg,才恍然大悟,我是被32bit centos 5的內核暗殺的(Out of memory: Killed process xxx)。
32位機上LowFree一般是會變化的(cat /proc/meminfo??| grep LowFree),最大不能超過880M(這個值不能改變除非用hugemem內核),在centos4 上有內核參數vm.lower_zone_protection(單位是M)來調節LowFree,默認vm.lower_zone_protection=0 ,LowFree=16M,但是在centos5上這個參數貌似被取消了,改變不了。 從使用經驗來看,也就是你能申請16M~880M這么大的連續內存,但是16M以上不保證你能申請的到。centos4用到64M以上都沒什么問題,centos5 用40M+ 就被OOM Killer給斃了。
本周開始使用64bit centos5.2進行測試,遷移很順利,沒有修改一行代碼,他們把整個16G物理內存都作為LowFree,這下可以隨便揮霍了(盡管如此我還是會節約的),前幾天的比賽,這個64bit機跑了18w connections,很安靜,未見異常,等有大比賽再檢驗下,沒問題的話就開始大規模使用64bit系統。
目前看來,如果成功遷移64bit系統似乎可以高枕無憂了,但是我還是有兩個憂慮:
1,再突然甩200w connections給我,我不敢保證能扛的住,因為現在服務器數量消減太多,需要yangguang4那邊做策略調整,在比賽結束后,平滑的把用戶丟給我,這應該有個持續過程,而不是一秒內的瞬間。
2,我猜這個系統,單機支撐到30w conections的時候會遇到瓶頸,因為網卡的中斷集中在cpu0上,沒有均衡開。我們有硬件解決方案已經實現(每個服務器會多2000RMB開銷)我只部署了一臺,但是軟的還沒實現,寄希望于xiaodong2 。
補充:
昨天的比賽中,一臺64bit機,單機支撐30w+ connections,cpu0空閑率只剩6%,和我的預料是一致的。當時的CPU總量被我用掉近 1/4,內存被我用掉 0.5% 。
下圖為30w+ connections, 4.6w req/s 的時候我的程序使用的資源情況(2cpu*4Core):
下圖為cpu使用分布情況,cpu0空閑率只剩 6% (2cpu*4Core):
另外附上一個23w connections, 3.5w req/s 的時候我的程序使用的資源情況(2cpu*4Core),當時cpu只被用掉1/8,內存被用掉 0.4% ,cpu沒有發揮線性增加的作用,我肯定不說能我可以支撐23w*8,但是保守地說,只要把網卡中斷分散一下,單機50w+ connections很easy。
?
八,新浪財經實時行情系統的歷史遺留問題 (7 byte = 10.68w RMB/year)
這點我還是提下吧,估計我不說,大家也想不到。
先感謝wangyun同學的大膽使用才有了今天的財經實時行情系統(當初是從一臺PIII 900服務器上發展起來的,前幾天剛被我下線)不過 "hq_str_"??這7個字節的前綴,也是他造成的,當初他說改抓取頁面有些麻煩,就讓我寫死在server里,雖然意識到將來也許會有隱患,但我還是照做了。見下面返回數據:
http://hq.sinajs.cn/list=s_sz000609,s_sz000723,s_sh000001
我算了一筆帳,行情好的時候每秒會產生30~40w個請求,一般一個請求會請求3~50只股票,保守點就按每個請求5只股票來計算,每秒會產生200w只股票查詢信息。 由于這7個字節產生的帶寬為: 200w??*??7byte??* 8bit / 1024 /1024 = 106.8 M??,而往往我們的帶寬要按峰值來準備,按1G帶寬100w RMB/year 計算,每年耗費10.68w RMB。把這筆錢拿給我做獎金,我會很happy的 ^-^ . 現在因為很多頁面都使用了行情數據,想修改,代價很高。
所以設計系統的時候一定要考慮的稍微遠一些,哪怕當時只是一點點微不足道的地方,要考慮將來訪問規模變大了會是什么后果。還有就是要敢于堅持自己的原則。
系統架構篇
-----------------------------------------------------------------------------------------
一,系統部署(高并發,可擴展)
二,負載均衡LVS(高可用,低成本)
三,IDC分布,DNS解析(快速)
-----------------------------------------------------------------------------------------
一,系統部署(高并發,可擴展)
本來想畫在手稿上然后掃描上去的,貌似方法太土,在朋友的幫助下費了n個小時用Visio畫了個,感覺很好看 ^-^ 。這一篇將主要圍繞這個圖來講述。
首先從數據源說起,所謂狡兔三窟,我們數據源也是按三路設計,以保證IDC內部和不同IDC之間實現災備。源頭轉發機A,B,C擁有往集群中任何一臺服務器同步數據的權限,所以他們三個有一個活著,數據就可以同步更新,而且可以自動切換。從源頭轉發機到其他各IDC的數據都是雙路的,然后每個IDC的前兩臺服務器具備轉發功能,往IDC內部其他服務器分發數據,同一IDC內部的主備轉發機可以自動切換。這樣就實現了數據同步更新的高可用性。
介紹下這個集群里的角色,備機A來自行情系統,兼任源頭轉發的異地備份。系統內的另外兩個備機屬于輕負荷服務器,80端口空出來,必要時候只要一啟動,就會立即自動加入到LVS后面服役。除了A以外所有具備轉發功能的機器同時也是集群內的普通成員,需要提訪問供服務的。各IDC的LVS本身也是有主備的,可以實現自動切換。
整個系統增減服務器非常方便,用戶根本感覺不到,備機的啟用更快,也就3~5秒,具備很好的擴展性。
我們的數據從源頭上就是使用我編寫的myzip壓縮好了的,后綴名用"*.mz" ,比如 a.js.mz ,一直到用戶的瀏覽器端才解壓。數據傳輸量小,速度快。 源頭轉發機上同時運行一個checkchange的程序,確保內容實際更新過的文件才往其他IDC轉發,這樣能有效的減少傳輸文件數量,以達到更快的更新速度。
另外,跨IDC系統部署,很重要的一點是,內網連通,路由選擇,這影響數據傳輸速度的關鍵。北京的各機房間一般都有比較好的專線連通,只需要把路由打通就ok了。跨IDC的,一般都使用vpn來做內網傳輸,有條件的使用專線,這個比較昂貴,省著點用。另外跨網通,電信,和移動機房的一般都從雙線機房路由,或者說,從到不同信息服務商連通性都比較好的機房路由。總之跨IDC數據傳輸,要做到各IDC之間的傳輸速度心中有數。
最后,請稍微注意下系統的安全性,包括數據傳輸的安全性,和網絡安全性,避免遭受攻擊。
二,負載均衡LVS(高可用,低成本)
LVS 有三種模式,NAT,TUN,DR,其中DR是最高效的,下面我將主要介紹DR的應用。更多LVS資料參見?LVS項目中文文檔。?目前我們公司的LVS應用規模在國內應該至少可以排前三,更多技術細節請咨詢我們的LVS大牛xiaodong2.
下面是DR單臂模式的系統結構圖:
下面引用一下官網的介紹: 在VS/DR 中,調度器根據各個服務器的負載情況,動態地選擇一臺服務器,不修改也不封裝IP報文,而是將數據幀的MAC地址改為選出服務器的MAC地址,再將修改后 的數據幀在與服務器組的局域網上發送。因為數據幀的MAC地址是選出的服務器,所以服務器肯定可以收到這個數據幀,從中可以獲得該IP報文。當服務器發現 報文的目標地址VIP是在本地的網絡設備上,服務器處理這個報文,然后根據路由表將響應報文直接返回給客戶。
我在財經時要使用LVS的初衷只是為了解決負載的均衡性,因為DNA輪詢各前端服務器上連接數有不小的差距,那時候我們老大阿圖對于這個項目給予了很大支持,還親自組織過幾次會議。話說恰巧yingyuan做了個新技術講座,我從中發現LVS/DR后想讓它幫忙修改負載均衡算法,后來部署上以后發現,不用修改,均衡的很,再后來xiaodong2接手后對性能和穩定性做了很大的提升,我們使用兩年來沒出過問題。另外lvs還額外帶來了兩個好處,高可用性,和可伸縮性。是可以隨時把lvs后面的一臺服務器下掉,扛走,用戶是不知道的。服務器壞了也不用著急修,也不用修改DNS(另外DNS的層層cache影響不是一時半會就能消除的)。新增加一臺服務器也是同理,最絕的就是備機的啟用可以用秒來衡量(這些F5都能實現,代價不菲)。財經應用對公司內lvs的項目推動有不可磨滅的貢獻 ,xiaodong2也這么說地 :) 。
三,IDC分布,DNS解析(快速)
這里思路跟CDN是一致的,盡量減少主干線路上的擁塞,讓用戶就近訪問,以達到最快的數據傳輸速率。
我們要做的就是了解自己應用的用戶分布情況,然后再結合現有資源以及各地網絡出口特征,信息服務商的特征來部署我們的服務。
1,各省市網絡用戶分布依次排名(數據來自cnnic2007年的統計):
廣東 13.4%
山東 8.2%
江蘇 7.5
浙江
四川
河北
河南
福建
上海
遼寧
北京
湖南
山西
黑龍江
2,運營商的網絡分布特點:
網通:以北京為超核心的放射性結構。山東應該是網通最大的用戶,但它的網絡存在瓶頸,會有丟包,造成外面訪問它慢,它訪問別人也慢。對此我們沒有必要浪費珍貴的主干帶寬,在濟南布個點,同步一份數據過去就,讓他們在自己省內訪問,訪問速度會立刻提升n倍。
電信:以幾大省市為核心的環狀結構,省市內部也是大環套小環。其中以廣東用戶最多,必須要部點的地方。記得很久以前我拿到一份數據說,上海人訪問本IDC的數據,不如訪問廣東的速度快,不曉得現在是否還存在這種情況。電信有7個主要核心,分布在廣州,上海,江蘇,西安,成都,武漢,北京。
教育網:以國內主要的八個結點為核心。這八個結點分布在北京,西安,成都,廣州,武漢,南京,上海,和沈陽。
3,DNS解析時候需要權衡的:
現在了解了這些信息,那我們開始討論如何部署我們的服務。要考慮兩個問題:一,要部署在哪幾個IDC。二,每個IDC部署的服務器數量。三,DNS如何按區域劃片。
要部署在哪幾個IDC ?
其實這里還涉及到規模化應用的好處,一個小應用就部署了N多個IDC顯然不劃算。如果我的應用上了規模,我可以在每個省都部署上,那樣用戶體驗將非常好,而且規模化以后會有專業人員對應用進行優化。所以公司里有動態池,和靜態池這樣的公用平臺是好事(也許將來還會有我的js池)。如果我們的服務還沒有上升到公司級別的規模,那就得考慮下取舍。
網通:東北三省,可以在沈陽和哈爾濱選擇一個部署,有條件可以都部署。沈陽到北京的速率比哈爾濱到北京的速率快一倍,而哈爾濱到沈陽的速率,還不如到北京的快。北京,如果只讓我在網通部署一個點的話,毫無疑問我會選擇北京,其他所有結點到它的速率都比較快,但是北京的帶寬比較昂貴。天津,這個點重要性僅次于北京,可以輻射河北,河南,江蘇,離北京也比較近,價錢便宜。太原,可以輻射到西北一帶。山東,前面已經說過,最好要部署的。
電信:那七個核心結點上部署了,速度就有保證。具體覆蓋范圍。廣州覆蓋周邊幾省,上海覆蓋本地,江浙一帶。 武漢覆蓋華中一帶,西安可以覆蓋西北5省,成都覆蓋西南5省。
每個IDC部署的服務器數量?
這要根據具體應用來決定。比如財經用戶網通,電信比例:3:4 而體育是 1:2 。教育網用戶一般占1/30左右。 這里還不能單純考慮用戶分布,還要考慮IDC內部災備和IDC間災備,是要有個取舍的。拿咱們的某個具體項目來說,教育網,夠不上一臺服務器,但是不得不部,因為它訪問外界實在太慢了,我就住在學校里,也為了方便自己。我把北京作為主要結點部署了3臺,天津,其實一臺就夠了,山東一臺有點多。但是考慮到北京IDC一旦倒了,實力相當的IDC可以災備,同時考慮到,天津,和山東只有一臺,idc內部,都無法實現災自動切換。所以,我選擇天津兩臺,山東不部署,以性能換安全。
DNS如何按區域劃片?
原則,就近分片,以達到最快傳輸速率。其次,考慮到各IDC間快速切換比較容易,DNS解析文件要寫的簡潔一些。另外,DNS解析有有個缺陷,每個單獨域名里寫在最前面的那個ip,它被輪詢到的概率要比同組的服務器高10%,而且隨著同組服務器的增多,這個差距會變大。所以最解析時候,每個IDC我都把硬件性能最好的服務器ip放在最前面。
另外:
做系統架構不提數據庫,有點過不去。這塊問題可以請教我們的DBA大牛zongwen同學。數據庫是我將來一年的學習重點,爭取一年后在DB方面能達到我們DBA六層功力。
?
Cache為王篇
?---------------------------------------------------------------------------------------
一,Cache, 王道也
二,Cache 基本原理介紹
三,我劃分的3個刷新級別
四,我對HTTP協議做的一點創新(?maxage=6000000)
五,Yslow優化網站性能的14條軍規點評
六,上線了 !=??Finished
七,提速度同時節約成本方法匯總
-----------------------------------------------------------------------------------------
一,Cache,王道也
我覺得系統架構不應該僅僅是搭建一個強硬的能承受巨大并發壓力的后臺,前端頁面也是需要架構的而且同等重要,不理解前臺的的后臺工程師是不合格的。中國人講究鋼柔相濟,后臺強硬只能說你內功深厚,前端用的巧,那叫四兩撥千斤。
一般后臺工程師很少關心前端如何使用自己的資源,而前端工程師,不知道自己的一個簡單的用法會對后端造成多大影響。我會給出一些數據,來震撼下你的眼球。
二,Cache 基本原理介紹?(參考Caching Tutorial)
為什么使用Cache?
1,減少延遲,讓你的網站更快,提高用戶體驗。
2,避免網絡擁塞,減少請求量,減少輸出帶寬。
補充一個cache的原則:不更新的資源就不應該讓它再次產生HTTP請求,如果強制產生了請求,那么就看看能否返回304。
Cache的種類?
瀏覽器Cache,代理Cache,網關Cache。
后端還有 disk cache ,server cache,php cache,不過不屬于我們今天討論范圍。
Cache如何工作的?
1,如果響應頭告訴cache別緩存它,cache不對它做緩存;
2,如果請求需要驗證的或者是需要安全性的,它將不被緩存;
3,如果響應頭里沒有ETag或Last-Modifed header這類元素,而且也沒有任何顯式的信息告訴如何對數據保鮮,則它被認為不可緩存。
4,在下面情況下,一個緩存項被認為是新鮮的(即,不需到原server上檢查就可直接發送給client):
????它設置了一個過期時間或age-controlling響應頭,而且現在仍未過期。
????如果瀏覽器cache里有某個數據項,并且被被設置為每個會話(session)過程中只檢查一次;
????如果一個代理cache里能找個某個數據項,并且它是在相對較長時間之前更新過的。
????以上情況會認為數據是新鮮的,就直接走cache,不再查詢源server。
5,如果有一項過期了,它將會讓原server去更新它,或者告訴cache這個拷貝是否還是可用的。
怎么控制你的Cache?
Meta tags :在html頁面中指定,這個方法只被少數瀏覽器支持,Proxy一般不會讀你html的具體內容然后再做cache決策的。
Pragma: no-cache : 一般被大家誤用在http響應頭中,這不會產生任何效果。而實際它僅僅應該用在請求頭中。 不過google的Server: GFE/1.3 響應中卻這樣用,難道人家也誤用了呢。
Date: 當前主機GMT時間。
Last-Modified : 文件更新GMT時間,我在響應頭中帶上這個元素的時候,通常瀏覽器在cache時間內再發請求都會稍帶上If-Modified-Since,讓我們判斷需要重新傳輸文件內容,還是僅僅返回個304告訴瀏覽器資源還沒更新,需要緩存策略的服務器肯定都得支持的。有了這個請求,head請求在基本沒太多用處了,除非在telnet上調試還能用上。
If-Modified-Since :??用在請求頭里,見Last-Modified 。
Etag: 標識資源是否發生變化,etag的生成算法各是各樣,通常是用文件的inode+size+LastModified進行Hash后得到的,可以根據應用選擇適合自己的。Last-Modified 只能精確到秒的更新,如果一秒內做了多次更新,etag就能派上用場。貌似大家很少有這樣精確的需求,浪費了http header的字節數,建議不要使用。
更正:Etag 其實在某種情況下可以很好的減少數據傳輸。在stonehuang的提醒下我才恍然大悟,轉眼好幾個月了也一直忘記更新。Etag應用場景。比如,數據為php的動態輸出。每次請求把上一次Etag帶來,跟本次計算的Etag進行比較,相等就可以避免一次數據傳輸。(最后修改時間 2009.12.07)
Expires :??指定緩存到期GMT的絕對時間,這個是http 1.0里就有的。 這個元素有些缺點,一,服務器和瀏覽器端時間不一致時會有問題。 二,一旦失效后如果忘記重新設置新的過期時間會導致cache失效。三,服務器端需要根據當前Date時間 + 應該cache的相對時間去計算這個值,需要cpu開銷。 我不推薦使用。
Cache-Control:
這個是http 1.1中為了彌補 Expires 缺陷新加入的,現在不支持http 1.1的瀏覽器已經很少了。
max-age:?指定緩存過期的相對時間秒數,max-ag=0或者是負值,瀏覽器會在對應的緩存中把Expires設置為1970-01-01 08:00:00 ,雖然語義不夠透明,但卻是我最推薦使用的。
s-maxage:?類似于max-age,只用在共享緩存上,比如proxy.
public:?通常情況下需要http身份驗證的情況,響應是不可cahce的,加上public可以使它被cache。
no-cache:?強制瀏覽器在使用cache拷貝之前先提交一個http請求到源服務器進行確認。這對身份驗證來說是非常有用的,能比較好的遵守 (可以結合public進行考慮)。它對維持一個資源總是最新的也很有用,與此同時還不完全喪失cache帶來的好處,因為它在本地是有拷貝的,但是在用之前都進行了確認,這樣http請求并未減少,但可能會減少一個響應體。
no-store:??告訴瀏覽器在任何情況下都不要進行cache,不在本地保留拷貝。
must-revalidate:?強制瀏覽器嚴格遵守你設置的cache規則。
proxy-revalidate:?強制proxy嚴格遵守你設置的cache規則。
用法舉例:??Cache-Control: max-age=3600, must-revalidate
其他一些使用cache需要注意的東西,不要使用post,不要使用ssl,因為他們不可被cache,另外保持url一致。只在必要的地方,通常是動態頁面使用cookie,因為coolie很難cache。至于apache如何支持cache和php怎么用header函數設置cache,暫不做介紹,網上資料比較多。
如何設置合理的cache時間 ?
http://image2.sinajs.cn/newchart/min/n/sz000609.gif?1230015976759
拿我分時圖舉例,我們需要的更新頻率是1分鐘。但為了每次都拿到最新的資源,我們在后面加了個隨機數,這個數在同一秒內的多次刷新都會變化。我們的js雖然能夠很好的控制,一分鐘只請求一次,但是如果用戶點了刷新按紐呢?這樣的調用是完全cache無關的,連返回304的機會都沒有。
試想,如果很多人通過同一個代理出去的,那么所有的請求都會穿透代理,弄不好被網管封掉了。如果我們做只做一秒的cache,對直接訪問源服務器的用戶沒太多影響,但對于代理服務器來說,他的請求可能會從10000 req/min 減少為 60 req/min ,這是160倍。
對于我們行情圖片這樣的情況,刷新頻率為1分鐘,比較好的做法是把后面的隨機數(num)修改為 num=t-t%60 其中t是當前時間戳 ,這樣你一分鐘內刷這個url是不變的,下一分鐘會增加1,會再次產生一個新請求。而我的max-age設置為默認59秒,即使設置120秒其實也沒什么影響。可能你會說萬一趕上臨界點可能拿不到最新的數據,其實對用戶來說,用那個多變的隨即數和我這個分鐘級的隨即數,看到的效果是相同的下面我給你分析一下: 如果用戶打開了我們的分時間頁面,當前隨即數對他來說是新的,所以他會拿到一個當前最新的圖片,然后他點了刷新按紐,用戶會產生http請求,即使url沒變,服務器有最新圖片也一定會返回,否則返回304,一分鐘后js刷新圖片,分鐘數加了1,會得到全新資源。這和那個隨時變化的隨即數效果有區別嗎?都拿到了最新的數據,但是卻另外收益了cache帶來的好處,對后端減少很多壓力。
三,我劃分的3個刷新級別
名詞解釋 全新請求: url產生了變化,瀏覽器會把他當一個新的資源(發起新的請求中不帶If-Modified-Since)。
更正:在firefox后來的版本中對此做了改進,傾向于更多的使用cache,曾經訪問過的都會盡量捎帶If-Modified-Since頭。這些表現和IE一致。修改部分用紅色標出。(最后修改時間 2009.12.07)
注: sports.sinajs.cn 在IE下的表現存在一個小bug,由于不是使用的strncpy,導致IE下難以返回304,
需要修改一行代碼,把比較字符串長度設置為29即可解決。不過目前本人已不在職,難以修改。
情況一 FF 捎帶的頭: If-Modified-Since????Mon, 07 Dec 2009 10:54:43 GMT
情況二 IE 捎帶的頭: If-Modified-Since????Mon, 07 Dec 2009 10:54:43 GMT; length=6
1,在地址欄中輸入http://sports.sinajs.cn/today.js?maxage=11地址按回車。重復n次,直到cache時間11秒過去后,才發起請求,這個請求會帶If-Modified-Since。
2,按F5刷新.??在你發起一個全新的請求以后,然后多次按F5都會產生一個帶If-Modified-Since的請求。
3, ctrl+F5 ,總會發起一個全新的請求。
下面是按F5刷新的例子演示: http://sports.sinajs.cn/today.js?maxage=11
( 如果這個值大于瀏覽器最大cache時間maxage,將以瀏覽器最大cache為準)
----------------------------------------------------------發起一個全新請求
GET /today.js?maxage=11 HTTP/1.1
Host: sports.sinajs.cn
Connection: keep-alive
HTTP/1.x 200 OK
Server: Cloudia
Last-Modified: Mon, 24 Nov 2008 11:03:02 GMT
Cache-Control: max-age=11????(瀏覽器會cache這個頁面內容,然后將cache過期時間設置為當前時間+11秒)
Content-Length: 312
Connection: Keep-Alive
---------------------------------------------------------- 按F5刷新
GET /today.js?maxage=11 HTTP/1.1
Host: sports.sinajs.cn
Connection: keep-alive
If-Modified-Since: Mon, 24 Nov 2008 11:03:02 GMT???(按F5刷新,If-Modified-Since將上次服務器傳過來的Last-Modified時間帶過來)
Cache-Control: max-age=0
HTTP/1.x 304 Not Modified???
Server: Cloudia
Connection: Keep-Alive
Cache-Control: max-age=11???(這個max-age有些多余,瀏覽器發現Not Modified,將使用本地cache數據,但不會重新設置本地過期時間)
----------------------------------------------------------
繼續按F5刷新n次.......
這11秒內未產生http請求.直到11秒過去了...............
----------------------------------------------------------按F5刷新
GET /today.js?maxage=11 HTTP/1.1
Host: sports.sinajs.cn
Connection: keep-alive
If-Modified-Since: Mon, 24 Nov 2008 11:03:02 GMT?(多次按F5都會產生一個帶If-Modified-Since的請求)
Cache-Control: max-age=0
HTTP/1.x 304 Not Modified
Server: Cloudia
Connection: Keep-Alive
Cache-Control: max-age=11
----------------------------------------------------------按F5刷新
GET /today.js?maxage=11 HTTP/1.1
Host: sports.sinajs.cn
Connection: keep-alive
If-Modified-Since: Mon, 24 Nov 2008 11:03:02 GMT??(同上 ...)
Cache-Control: max-age=0
HTTP/1.x 304 Not Modified
Server: Cloudia
Connection: Keep-Alive
Cache-Control: max-age=11
----------------------------------------------------------
四,我對HTTP協議做的一點創新(?maxage=6000000)
上面看到了url后面有???maxage=xx??這樣的用法,這不是一個普通的參數,作用也不僅僅是看起來那么簡單。他至少有以下幾個好處:
1,可以控制HTTP header的的 max-age 值。
2, 讓用戶為每個資源靈活定制精確的cache時間長度。
3, 可以代表資源版本號。
首先談論對后端的影響:
服務器實現那塊,不用再load類似mod_expires,mod_headers 這樣額外的module,也不用去加載那些規則去比較,它屬于什么目錄,或者什么文件類型,應該cache多少時間,這樣的操作是需要開銷的。
再說說對前端的影響:
比如同一個分時行情圖片,我們的分時頁中需要1分鐘更新,而某些首頁中3分鐘更新好。不用js控制的話,那我cache應該設置多少呢????有了maxage就能滿足這種個性化定制需求。
另一種情況是,我們為了cache,把某個圖片設置了一個永久cache,但是由于需求,我必須更新這個圖片,那怎么讓用戶訪問到這個更新了的圖片呢? 從yahoo的資料和目前所有能找到的資料中都描述了同一種方法,更改文件名字,然后引用新的資源。 我覺得這方法太土, 改名后,老的還不能刪除,可能還有地方在用,同一資源可能要存兩份,再修改,又得改個名,存3份,不要不把inode當資源。 我就不那樣做,只需要把maxage=6000000 修改成 maxage=6000001 ,問題就解決了。
maxage=6000000 所產生的威力 (內存塊消耗減少了250倍??,請求數減少了37倍) :
體育那邊要上一個新功能,一開始動態獲取那些數據,我覺得那樣太浪費動態池資源,就讓他們把xml文件到轉移到我的js池上來,為了方便,他們把那個84k的flash文件也放在了一起,而且是每個用戶必須訪問的。 說實在的,我不歡迎這種大塊頭,因為它不可壓縮,按正常來說,它應該代表一個3M的文件。我的服務器只這樣設計的,如果一次發送不完的就暫存在內存里,每個內存塊10k,如果不帶參數默認maxage=120 。 我發現,由于這個文件,10w connections的時候,我消耗了10000個內存塊。我自己寫的申請連續內存的算法也是消耗cpu地, 一個84k的文件,發送一次后,剩余的64k就應該能裝的下,于是我把最小內存塊大小改為64K。 這樣消耗10w conn的時候消耗1500個左右內存快,雖然內存消耗總量沒怎么變小,但是它能更快的拿到64K的連續內存資源,cpu也節約下來了。接下來我讓meijun把所應用的flash資源后面加上maxage=6000000 (大概=79天,瀏覽器端最長cache能達到著個就不錯了), 10w connections的時候,只消耗了不到40個內存塊,也就是說內存塊消耗減少了250倍??,請求數減少了37倍。??35w+ connections, 5.67w req/s的時候也就消耗100塊左右,比線性增加要少很多。也就是這點發現讓我有了做這個技術分享的沖動,其他都是順便講講。
五,Yslow優化網站性能的14條軍規點評
其中黑色部分,跟后端是緊密相連的,在我們的內容中都已經涉及到了,而且做了更深入的討論。蘭色部分,5,6,7是相關頁面執行速度的,構建前端頁面的人應該注意的。 11屬于避免使用的方法。 紅色部分我著重說一下:
gzip 我不推薦使用,因為有些早期IE支持的不好,它的表現為直接用IE訪問沒問題,用js嵌進去,就不能正常解壓。這樣的用戶占比應該在2%左右。這個問題我跟蹤了近一個月,差點放棄使用壓縮。 后來發現我以前用deflate壓縮的文件卻能正常訪問。 改用deflate問題解決。apache 1.x使用mod_gzip ,到了 2.x 改用cmod_deflate,不知道是否跟這個原因有關。 另外對于小文件壓縮來說,deflate 可比 gzip 省不少字節。
減少 DNS 查詢: 這里也是有個取舍的,一般瀏覽器最多只為一個域名創建兩個連接通道。 如果我一個頁面嵌了 image.xx.com 的很多圖片,你就會發現,圖片從上往下一張張顯示出來這個過程。這造成了瀏覽器端的排隊。 我們可以通過增加域名提高并發度,例如 image0.xx.com ,image1.xx.com ,image2.xx.com,image3.xx.com 這樣并發度就提上去了,但是會造成很多cache失效,那很簡單,假如我們對文件名相加,對4取mod,就能保證,某個圖片只能通過某個域名進行訪問。 不過,我也很反對一頁面請求了數十個域名,很多域名下只有一到兩個資源的做法,這樣的時間開銷是不劃算的。
另外,我在這里再添一個第15條:錯開資源請求時間,避免瀏覽器端排隊。
隨著ajax的廣泛使用,動態刷新無處不在,體育直播里有個頁面調用了我一個域名下的6個文件,3個js,3個xml。 刷新頻率大致是兩個10秒的,兩個30秒的,兩個一次性載入的。觀察發現正常響應時間都在7ms,但是每過一會就會出現一次在100ms以上的,我就很奇怪,服務器負載很輕呢。meijun幫我把刷新時間錯開,11秒的,9秒的,31秒的,這樣響應在100ms以上的概率減少了好幾倍,這就是所謂的細節決定成敗吧。
1. 盡可能的減少 HTTP 的請求數 ????[content]
2. 使用 CDN(Content Delivery Network) ????[server]
3. 添加 Expires 頭(或者 Cache-control ) ????[server]
4. Gzip 組件 ????[server]
5. 將 CSS 樣式放在頁面的上方 ????[css]
6. 將腳本移動到底部(包括內聯的) ????[javascript]
7. 避免使用 CSS 中的 expression_r_r_r_rs ????[css]
8. 將 JavaScript 和 CSS 獨立成外部文件 ????[javascript] [css]
9. 減少 DNS 查詢 ????[content]
10. 壓縮 JavaScript 和 CSS (包括內聯的) ????[javascript] [css]
11. 避免重定向 ????[server]
12. 移除重復的腳本 ????[javascript]
13. 配置實體標簽(ETags) ????[css]
14. 使 AJAX 緩存 ???
六,上線了 !=??Finished
奧運期間我按1500w~2000w connections在線,設計了一套備用系統,現在看來,如果用戶真達到了這個數目我會很危險,因為有部分服務器引入了32bit的centos 5未經實際線上檢驗,而我當時簡單的認為它應該和centos 4表現出一樣的特性。所以現在未經過完全測試的lib庫和新版本,我都很謹慎的使用。沒在真實環境中檢驗過,不能輕易下結論。
很多項目組好象不停的忙,做新項目,上線后又繼續下個新項目,然后時不時的轉過頭去修理以前的bug。如果一個項目上線后,用戶量持續上升,就應該考慮優化了,一個人訪問,和100w人訪問,微小的修改對后端影響是不能比較的,不該請求的資源就讓它cache在用戶的硬盤上,用戶訪問塊了,你也省資源。上線僅僅代表可以交差了而已,對于技術人員來說持續的對一個重要項目進行跟蹤和優化是必要的。
七,提速度同時節約成本方法匯總
1,編寫節約的HTTP服務器 (高負載下速度明顯提升,節約5~10倍服務器)
對一些重要的服務器量身定做。或者選用比較高效的開源軟件進行優化。
2,不同服務混合使用??(節約1~2倍服務器)
如果我們一臺服務器只支持30w conn的話,那么剩余的75% cpu資源,95%的內存資源,和幾乎所有的磁盤資源都可以部署動態池系統,我覺得DB對網卡中斷的消耗還是有限的,我也不用新買網卡了。
3,對于純數據部分啟用新的域名(速度有提升,上行帶寬節約1倍以上)
比如我們另外購買了sinajs.cn 來做數據服務,以避免cookie,節約帶寬. Cookie不但會浪費服務器端處理能力,而且它要上行數據,而通常情況上行比下行慢。
4, 使用長連接 (速度明顯提升,節約帶寬2倍以上,減少網絡擁塞3~無數倍)
對于一次性請求多個資源,或在比較短的間隔內會有后續請求的應用,使用長連接能明顯提升用戶體驗,減少網絡擁塞,減少后端服務器建立新連接的開銷。
5,數據和呈現分離,靜態數據和動態數據分離?(速度明顯提升,同時節約3倍帶寬)
div+css 數據和呈現分離以后,據說文件大小能降到以前的1/3。
把頁面中引用的js文件分離出來,把動態部分和靜態部分也分離開來。
6,使用deflate壓縮算法?(速度明顯提升,節約3.33倍帶寬)
一般來說壓縮過的文件大小不到以前的30% 。
將上面分離出來的數據進行壓縮(累計節約帶寬10倍)。
7, 讓用戶盡可能多的Cache你的資源 (速度明顯提升,節約3~50倍服務器資源和帶寬資源)
將上面分離出來的css和不經常變動的js數據部分cache住合適的時間。(理想情況,累計節約帶寬30~500倍) 。
以上改進可以讓速度大幅度提升的同時,服務器資源節約 5~20 倍 ,減少網絡擁塞3~無數倍, 上行帶寬節約1倍以上,下行帶寬節約30~500倍,甚至更多。
?
實例分析篇
--------------------------------------------------------------------------------------------------
一,自選股分析
二,NBA比賽分析
三,播客分析
四,開心網分析
-----------------------------------------------------------------------------------------
下面的圖片都是在教育網訪問的情況,我故意放大了某些缺陷,這樣可以很好的模擬沒有部署服務的地區對用戶體驗的影響。我只能針對我熟悉和了解的項目進行分析,另外還有我們經常訪問的網站也會被拿來做素材分析。我們老大也說問題暴露出來,能推動解決的話也很好,大家別拍我。
一,自選股分析
某天我終于在教育網部署了一臺行情服務,興致沖沖的回家一試,貌似沒啥變化,該慢還慢。打開頁面過程持續了幾十秒,然后終于露出了行情,我再電擊每個組合的時候就出現了上面的一幕。看了下firebug,最慢資源排名前三依次為:高效計數服務,secure-cn統計服務,動態池服務。
高效計數服務是早期我參與的項目,那時候資源有限,全部部署在了網通。
secure-cn統計服務: 這個服務不慢是不正常的,到處都嵌,還不能不嵌。
動態池數據庫很牛,但在偏遠地區也鞭長莫及。這個缺點比較典型:
一,沒有在教育網部署。
二,沒有保持長連接。
三,沒有使用cahce
四,沒有使用壓縮
五,長達2.46K的http 請求header,捎帶大量cookie,見下面。
解決方法:我分析了下,下面這個數據變化很慢的,主要放一些市盈率和用戶股票列表。市盈率可以通過去年的每股收益來計算,以年計,可以變通一下。用戶股票列表我也好幾個月沒更新了,大家并不是總更新。所以這部分數據是可以被設置一個很長的cache的,如果用戶更新了股票列表,我們也只需要在maxage版本號上加1就ok了。另外,用戶點了一個組合,接看來也都要看幾個別的組合,沒有維持長連接顯然不合理的。在沒有部點的idc,壓縮就能明顯的提升響應速度,這里就沒考慮。那個cookie太長點了吧,真的用的了那么長嗎。
http://vip.stock.finance.sina.com.cn/portfolio/stock.php?rn=1228707043897&pid=1245111&type=complete
----------------------------------------------------------------------------------------
GET /portfolio/stock.php?rn=1228707043897&pid=1245111&type=complete HTTP/1.1
Host: vip.stock.finance.sina.com.cn
User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; zh-CN; rv:1.9.0.4) Gecko/2008102920 Firefox/3.0.4
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: zh-cn,zh;q=0.5
Accept-Encoding: gzip,deflate
Accept-Charset: gb2312,utf-8;q=0.7,*;q=0.7
Keep-Alive: 300
Connection: keep-alive
Cookie: CurrentBar=attend; CurrentTab=state; CombinationSelected=154148; CommisionCookie=0; StampCookie=0; FeeCookie=0; BX=7t1oh653u6qvb&b=3&s=4k; SINA_NEWS_CUSTOMIZE_city=%u5317%u4EAC; userId=C7DHwoAi-ryCr69CGgyc3czekbyphdy5hcxQNhFcN6zCNe; FINA_VISITED_S=sh601988|-y?L,sh580989|W*JTP1,sh601988|-y?L,sh601988|-y?L,sh580989|W*JTP1,sh601988|-y?L,sh601988|-y?L,sh601988|-y?L,sh580989|W*JTP1; Iask2_visitID=10.217.21.44.177601199668733612; UNIPROCT=342-0-0:2; hold_sinabar_name=iyangjian2005997; UNIPROPATH=2:iyangjian2005997:0::1:|*|202.112.174.100.97191204115419966|pid:342-0-0-0-0|classad.sr/|st:25.906|et:1204118703312||hp:unkown|lb:1|*|; SINAPUID=10.217.21.64.250871201592749264; vjuids=-5600fbe60.117402dbc5e.0.42a2debdf9f46; VISITED_FANCHAN_SINA_ZHANGYQ=SINA_BEIJING; S_WC_USRTOK=SFyLe9; stat=0806201608589720436533; MY_STOCK_LIST_2=sh600602; visited_futures=SI%7CCL%7CGC%7CCAD%7CTRB%7Cau0812%7CCC%7CPBD%7CCF907%7CNID; SINA_FINANCE=iyangjian2005997%3A1181509184%3A2; visited_funds=000011%7Csh000011%7C159902%7C160314%7C377016%7C270005%7C202009; SINA_FINANCE_SELECT_TYPE=stock; vjuid=-12b4fad5c.1174d78e8a5.0.6099c257a27eb; vjlast=1199616063; vjlast=1199616063,1228706963,10; sina_sort_default=117; SHOW_TIP_BOX=1; FINA_V_S_2=sz000609,sz000723,sh000001,sz002242,sz002274,sz000049,sz002272,sh600432,sh601186,sh601390,sh600036,sz000625; hk_visited_stocks=HSI%7C04338%7CHSCEI%7CHSCCI; visited_cfunds=050007%7Csz161010; __utma=269849203.390390911.1226996335.1226996335.1226996335.1; __utmz=269849203.1226996335.1.1.utmccn=(direct)|utmcsr=(direct)|utmcmd=(none); SINAGLOBAL=202.112.174.100.224381203683121713; Apache=202.112.174.100.771641228691763829; SessionID=e9bc0f217040ae10439d85f422f3187a; SINA_PORTFOLIO=sz000514%2Csh600729%2Csh600438%2Csh600528%2Csh600678%2Csh600877%2Csh600039%2Csh601005%2Csh600875%2Csz001696%2Csz000628%2Csh600116
Cache-Control: max-age=0
HTTP/1.x 200 OK
Date: Mon, 08 Dec 2008 03:32:52 GMT
Server: Apache
Cache-Control: no-cache
Expires: Mon, 08 Dec 2008 03:34:52 GMT
Connection: close
Transfer-Encoding: chunked
Content-Type: text/html; charset=GBK
----------------------------------------------------------------------------------------
如果不是這幾個資源的引用,這個頁面的速度將非常快。
?
這里引用了某些未在教育網部署的服務,導致半天出不了數據。
?
由于引入了mark.sina.com.cn的數據導致整個頁面卡在那里。引用別人數據的時候你了解過他們是怎么分布自己服務的嗎?可能稍有不慎拖垮整個頁面。
二,NBA比賽分析
?
這里的js真的有必要每次都發起請求嗎?連續請求3同域個資源,為什么不維持下長連接?
?
這些圖片的304響應為什么都在秒級以上?
三,播客分析
這些圖片和視頻由于解析錯誤,教育網用戶被解析到廣州服務器組,導致不可訪問。
四,開心網分析
打開開心網,看到最多的就是人物圖片,我就僅僅針對圖片進行下分析:
1,瀏覽一個新人的頁面,大概要下載30~40張小圖片。使用單一的pic.kaixin001.com域名,不能提高并發,可以考慮多域名取模。
2,圖片請求帶了cookie,上行帶寬浪費點無所謂,但是會影響響應速度和用戶體驗。
3, /logo/10/51/50_105146_1.jpg ,他們設置了一個比較大的maxage,通過改名來實現更新大可不必,我用我的方法更好。
4,每次點刷新頁面,都會重新加載很多圖片,雖然很多是304,我覺得絕大部分就不應該發這個請求。
5,他用的是ChinaCache的CDN,Server: nginx,我不知道ChinaCache對這個server修改到什么程度。統計發現這個人物小圖片大都在2k左右。很多才1k多。沒有必要把他們當作圖片處理。盡量不產生磁盤i/o,包括fstat這樣的系統調用,甚至sendfile這樣的zero copy系統調用,我覺得都浪費. 同時還要保證圖片更新立刻被感應到。??
其他方面還有很多可以改進的,想讓他們的頁面響應速度上一個等級,節約更多帶寬和服務器資源并非難事。
動態應用篇
---------------------------------------------------------------------------------------
一, 引子
二,總體結構圖
三,系統結構綜述
四,環境配置以及底層基礎類庫
五, Memcache & Mysql 常用場景案例
六,更多待續 ......
-----------------------------------------------------------------------------------------
一, 引子
張三豐當初傳授傳授張無忌太極劍法的時候,剛傳授完,就問是否已經忘記。直到張無忌說招式已經忘光了,才算學會。當時很不理解,現在終于明白了,招隨心出,不應受到固定招式的限制。總有人問我什么樣的架構是不是就好,或者某個很有名的網站用什么架構,我們就要用嗎?甚至覺得直接拿個別人的配置文件或參數,性能就會突飛猛進。那么請問你知道別人當時為什么那么做嗎?做架構亦如學太極劍法,也請你學習別人的架構的時候,理解了就忘記掉,因為任何架構本身都有環境局限性,然后用心思考,分析,真正理解你所處的環境,從實際需求出發,你才能做出最適合你應用的架構。
很多人對寫server很感興趣,甚至一開始就直接考慮從底層協議進行優化,我說遠還沒到那個程度,請找出你真正的瓶頸。優化一定是從宏觀到微觀的,有時候一個系統結構,業務邏輯,或策略的優化,帶來的效果甚至更為可觀。對于每天請求處理量小于100億的系統,我都不建議自己去寫server。如果說寫高性能server的技能是我手中的那把太極劍的話,那么發揮出太極劍法的威力并不依賴于那把劍,而是取決于對心法的理解。那把劍既然可以是專用server,當然也可以是開源的那幾款經典server,取決于你對設計容量,以及硬件成本,維護成本,時間等多方面的預期。架構本身就是一個取舍的過程。
現在有這樣的一個項目,就拿我上一篇文章里提到的自選股來舉例吧,用戶可以在各個市場里創建自己的多個投資組合,然后在組合里定制自己關注的股票。描述一下環境以及需求。
環境:注冊用戶小幾千萬,同時在線預計峰值不到10萬。
需求:
1,3G,IM,Mail,Web 任何一個地方以及不同的主流瀏覽器更新數據,其他地方立即可見。
2,北京,天津,上海,深圳,任何一個IDC數據更新,其他三個IDC立即可見。
3,各IDC附近用戶,獲取股票列表的平均響應速度要控制在20ms以內。
4,IDC間專線中斷服務不能受影響。
5,IDC內部的相同功能的服務器,允許宕掉一半,服務完全不受影響。
6,IDC允許宕掉一到兩個,受災IDC 95%的用戶服務影響不超過5分鐘。
7,另外,需要開發人員能夠在這個系統上快速開發,要具備易用性,良好擴展性以及移植性。
基于以上的需求構建的實現,動態應用篇側重點主要是如何快速響應,同步更新,如何容災,消除安全隱患,讓系統更穩定,如何容易遷移和擴展應用,如何讓程序員容易使用這個平臺。這個系統性能不是主要關注的問題(10萬同時在線,確實比較微量,何況僅有的不多的壓力,也通過后面介紹的各種緩存機制轉移的差不多了),架構上采用當前主流經典架構(Nginx+PHP Fast-CGI+APC+Mysql+Memcache+LVS+Linux),各層次之間低偶合,每一層都具備單獨優化的空間,?隨著用戶量的增加,我再逐步推出動態應用處理并發和壓力方面的文章。
二,總體結構圖
(實際結構中去掉了中繼slave,改成所有slave直連master,這樣結構更簡單,易于管理和故障恢復。)
三,系統結構綜述
此系統主要分為幾個低偶合的層次組裝而成,具備多IDC分布的特性。從底往上依次為DB層,MC池子層,Nginx+PHP Fast-CGI層,LVS層,然后通過DNS接入用戶。每一層都具備良好的擴展性以及災備能力。
?
DNS:
從大的結構上來說,此系統分布在4個主要IDC,網通電信各2。?機器數量按照網通:電信?1:2的比例配置,同一運營商下的兩IDC機器數量等同。這樣在宕掉一個IDC的情況下,可以通過切換DNS,臨時訪問相近的IDC,達到IDC間災備。
?
LVS層:
每個IDC的接入層通過LVS作四層負載均衡。IDC內部任何接入機器宕掉,可以通過failover機制在一分鐘內自動摘除。
?Nginx+PHP Fast-CGI層:
通過fpm管理PHP Fast-CGI進程,Nginx通unix域協議與fpm通信。轉發?*.php的請求以及響應。使用APC作為OP代碼加速器,加速php響應。PHP直接與MC池子或Mysql層進行交互。
?
MC池子層:
每個IDC內部的多個MC機是作為一個整體來管理的,也就是說,假如你有100個數據,4個MC機,那么每個MC上只會存25個數據,而不會每個都存100個。好處是,獲得了相當于以前4倍的內存池,?增大了緩存命中率。更額外的好處是,減少了,本IDC內部各MC間數據同步的時間開銷,使得本IDC任何一服務器更新的數據,同IDC內其他服務器立即可見。也使得IDC之間MC的同步更為節約,每個IDC一個池子,每個池子只需要寫一份數據。
?MC池子增減機器的問題:如果是傳統的hash算法根據key值,將數據平均的hash到4臺機器上,那么按照新的hash規則,增加一臺機器后,將會有近80%的緩存失效,造成大量數據遷移。所以我們改用Consistent Hashing,首先求出memcached服務器(節點)的哈希值,并將其配置到0~2的32次方的圓上。然后用同樣的方法求出存儲數據的鍵的哈希值,并映射到圓上。然后從數據映射到的位置開始順時針查找,將數據保存到找到的第一個服務器上。如果超過2的32次方仍然找不到服務器,就會保存到第一臺memcached服務器上。它能最大限度地抑制鍵的重新分布。而且,有的實現還采用了虛擬節點的思想,使得分布更加均勻。由服務器臺數(m)和增加的服務器臺數(n)計算增加服務器后的命中率計算公式如下:(1 - n/(n+m) ) * 100??,按這個計算,增加一臺機器后還將得到80%的命中率。更較幸運的是這個算法已經被php所支持,可以通過php.ini設置。
?宕機重啟后初始化的問題:如果初始化的數據只從一臺DB上獲得,那么在高峰期間必將壓垮DB,所以,我們需要將這個壓力,分散到本IDC的多臺DB上。細節會在基礎類庫中進行講述。
?異地MC池子的數據同步:已經封裝在基礎類庫中,更新MC時指定特定的參數,就會在其他異地MC池子上也進行更新。所以我們如果在北京添了一個數據,那么在深圳也會立刻看到。
DB層:
主從結構集群,主庫可切換,從庫互為備份。從DB宕掉一臺,可以自動跳過,不影響服務,目前通過基礎類庫實現,以后會采用內部DNS。所有從庫直接和主庫相連,結構簡單,方便管理。不過寫的數據量相對比較小的,而且我們允許DB延遲(由MC來保證實時性,或者異地本來就可以接受短暫的延遲),這里將不是問題。
?MC和DB寫隊列:
在專線中斷時不影響寫操作(讀都是本地的,當然也不受影響),而且在更新數據時能夠快速返回(因為不需要遠程通信)。
具體來說就是將MC的異地寫,以及DB的寫操作封裝成直接追加寫本地隊列文件。每個機器上有個守護進程,每0.1秒檢查一次,有則rename,然后開始將隊列數據逐條往深圳數據中心post,同時每成功一條記錄當前offset,以便意外宕掉后接著上次的后面處理。(優化點的可以改為批量確認,冒進點的,可以使用內存盤優化寫速度)。如果線路不通,或者數據中心寫失敗,post程序將sleep and retry,線路恢復則繼續,不會丟數據。這里有個細節需要注意,就是往數據中心同步的時候,盡量發往同一臺機器,也即綁定,以保證序列的順序,這個保證可以通過對post守護進程的pid取mod來實現,如果綁定的機器出了故障,則應該選定另外一臺機器綁定,一旦檢測到以前的機器好了,則應該切回來,以保證應有的負載均衡。另外,最外層的接入端也同樣需要注意這個問題,我們對lvs做了會話保持,以確保同一用戶在相當長的一段時間內,會訪問到同一臺機器。對于僅僅使用DNS輪詢,又沒有使用長連接的服務,如果在短時間內做數據更新,肯定會出現亂序的問題。除非你各服務器時間都很精準,而且,每個記錄寫的時候打上精確到ms的時間戳。
?然后,深圳數據中心接收到post的數據后,根據消息類型按一定規則的命名分別存放,這樣即使增加一種新的消息,也可以方便的使用隊列。比如收到MC的消息后,要寫三份,分別發往三個IDC,各自不受影響。DB,可以根據不同的port實例子來創建隊列,免得有一個port宕了,影響其他。具體實現細節可以自己調整,同樣得有個程序定期(比如每0.1秒,0.01秒也無所謂,對cpu的消耗非常小)檢查隊列文件存在就rename,然后往master里寫。
?以上則能保證,DB的master宕掉,或者專線的中斷情況下,用戶服務基本不受影響,只是異地的同步會延遲一些。額外收獲是將來做DB升級調整的時候,可以用隊列分流,分別做不同的處理。
?祛除安全隱患:
不管你的系統設計的多么完美,疏忽這一條足以致命。
以上設計看起來似乎已經沒什么問題了,各種容災,異常也基本考慮到了,不幸的是,這個平臺并不是僅僅給設計者自己使用,如果有個新手使用了 file_get_contents($url);??如果url所依賴的服務器負載過重,那么整個系統都有被拖垮的危險。動態應用的一個鐵律就是,凡是依賴本系統外部資源的地方必須加超時限制,盡可能的減少依賴。file_get_contents的超時不是很精確,推薦使用curl的庫進行封裝,可以設置connect超時,也可以設置整個函數的執行時間超時。我一般會設置my_curl();函數的最大默認執行時間為一秒,因為是從內網拉取數據,一秒還拉不到,肯定有問題。另外,別忘記了mysql讀服務,當某一idc的一臺slave連接數滿了以后,如果沒設置過mysql.connect_timeout=1;那這個IDC的服務會整個被拖垮,因為默認的是60秒。(mysql的寫由隊列保證,不存在此問題)。
另外,我們項目還有個特殊性,即,每次拉取用戶股票信息時,需要從后臺專線到深圳進行身份合法性驗證,由于歷史的原因,這個驗證系統暫不具備多IDC分別的能力。假設北京的專線斷了,雖然用戶的數據是沒問題,但是驗證卻通不過,會導致獲取數據失敗。同樣這個驗證是要加超時的,另外為了在專線斷掉的情況下依然正常提供服務,就需要在得不到驗證的情況下,要通過代理的手段到鄰近的IDC去驗證。同時,我們也支持另外一種驗證方式,對于已經通過驗證的用戶,專線中斷一小段時間,比如一小時,服務是可以不受影響。
總之就是盡量低偶合,少依賴,加超時。另外一個維護的安全,則由安全中心把關,我就不多說了。
四,環境配置以及底層基礎類庫
?PHP網絡版環境:每IDC之間差異化的Nginx環境變量配置,使得相同的應用程序在不同的IDC運行時,使用各自IDC內部的MC以及DB等資源。
?PHP??Shell版環境:
PHP??Shell是指通過命令行方式執行的php腳本程序。
比如/path/php/bin/php test.php
或在php程序第一行加上??
#!/path/php/bin/php?
然后賦予php腳本可執行權限,使作為shell程序運行。
由于php作為shell運行時,無法繼承nginx配置的環境變量。所以它必須依賴一個獨立的配置文件。
?由于圖片里含有帳戶等敏感信息,就不在此貼了。
底層基礎類庫:
底層基礎類庫,起到粘合劑的作用,將環境配置,服務器資源等全部結合起來,使得這些資源以及配置信息對上層開發人員透明,無須考慮。總的來說有以下一些功能。
?
1,??兩環境融合,天衣無縫。?php?網絡環境和shell使用同一基礎類庫,代碼無任何一行差異。使得平時編寫的php網絡程序,以及類庫積累,可以方便的直接用來做shell編程,進行復用。具體原理是,類庫需要用到配置信息時,先通過if( isset ($_ENV["SERVER_SOFTWARE"]) )變量判斷自己是否網絡環境,如果是就直接使用配置項比如:_SERVER["DB_stock_host"]?,若不是,則先將配置文件數據項,section名和下面的字段相加轉化成?_SERVER["DB_stock_host"] = “m3306_sz_gtimg_cn” ;?跟網絡環境一致后,再繼續后面操作。
?[DB_stock]
host = m3306_sz_gtimg_cn
類庫目前除支持模擬DNS外,還直接兼容真實域名以及IP地址,方便將來進行數據遷移。之所以沒有直接使用 DNS是也有歷史原因的。
?
2,??對DB資源的封裝。
?在沒有內部DNS的情況下,將DB讀寫分離,帳戶選擇,連接的建立(包括?何時候真正建立連接,建立長連接,還是短連接,連接的綁定,以及生命周期),負載均衡以及failvoer等封裝成對用戶透明的如下簡單用法:
?//指定以讀(r)?或?寫(w)?的方式打開一個庫。不指定的情況下,默認是”r”方式打開。
$db_r =new MYSQL(“testdb”,“r”);?
?<?
require_once (dirname(__FILE__).'/../mysql.php');
?$db_w =new MYSQL("test","w");
?$arr = array(??"id" => "8",??"name" => "yangjian8"??);
?if( $db_w->insert("test",$arr) )
{
???????echo "query ok ...<br>";
}else
{
???????echo "query failed ...<br>";
???????echo "errno=$db_w->errno<br> errmsg=$db_w->errmsg<br>";
}
?echo "read ............<br>";
?$db_r =new MYSQL("test","r");
$sql = "select * from test";
?
if( $result = $db_r->query($sql))
{
???????while ($row = mysql_fetch_array($result, MYSQL_BOTH))
???????{
??????????????printf ("id: %s name: %s<br>", $row[id], $row[name]);
???????}
???????$db_r->free();??//free result. if you not free it,it will auto free at the end of the php script.
}else???//if not success,you can print the error info.
{
???????echo "errno=$db_r->errno<br> errmsg=$db_r->errmsg<br>";
}
3,??對MC池子資源的封裝。
簡化MC池子使用方法,支持異地MC數據同步。
//if rset=1,the mc data will sync to other idcs, default 0.
function set($key, $value, $flag, $expire, $r_set=0)
<?php
require_once (dirname(__FILE__).'/../memcache.php');
$mc = new MC("test");
for($i=0;$i<2;$i++)
{
???????$mc->set("key".$i,"value".$i,0,100);
}
for($i=0;$i<2;$i++)
{
???????echo $mc->get("key".$i);
???????echo "<br>";
}
DB以及MC的異地寫已經封裝進去,對上層開發人員來說都是透明的。
注意:我在引用頭文件時候,都會使用類似require_once (dirname(__FILE__).'/../mysql.php');?的方法。我暫且管它叫動態絕對路徑。它的好處是,
跟相對路徑比:當一個頭文件被多層引用時,目錄結構又不一致,不會找不到。
也不會去搜索所有可能的目錄,執行多余的fstat以及open操作。
跟絕對路徑相比:如果我將整個項目,包括頭文件全部平移到別的目錄,不需要挨個修改文件。
當然,你也可以采用另外一個方法,將本項目相關的配置信息絕對路徑放在一個統一的文件里,然后通過任何一種方法引用那個文件。
?
五,?Memcache & Mysql 常用場景案例
?經典篇:
更新數據:寫全局MC,然后再寫DB。
讀數據:先讀MC,命中返回數據。不命中則讀DB,更新到本地MC,然后返回數據。
(這幾個邏輯圖由kinggen同學提供,看起來比文字直觀多了,感謝一下)
為什么更新數據寫全局MC,而讀數據不命中只寫本地MC?
因為更新數據寫全局保證了只要MC中cache存在則肯定是最新的,要么就不存在。不存在的情況可以從DB中補充。
如果更新數據和不命中的情況下都只寫本地MC會有什么后果?
因為MC不會主動獲得數據更新,如果更新數據不寫全局,會造成其他IDC的cache在失效以前仍然是舊的。出現數據不同步的現象。
進階篇:
這里是在動態數據中引入靜態數據的last modify特性,以使得在動態應用中可以返回HTTP 304狀態。只比較最后更新時間便可以做出判斷,減少后續邏輯處理以及數據內容傳輸,快速做出響應。對于讀多,寫少的項目,意義巨大。
對于js調用的部分,并不等同與刷新,要想讓每次都產生請求,而且還帶If-Modified-Since過去,必須加個max-age=1。只能精確到1秒。
更新數據:將數據和LM寫到全局MC,然后將數據寫到DB,不用把LM也寫入,LM只存在于MC中。
讀數據:如果MC中存在LM: 比較瀏覽器請求帶過來的LM,?大于等于MC中LM則直接返回304。否則返回數據和最新的LM。如果MC中不存在LM:把當前的響應時間作為LM存在本地MC中,然后返回數據和此LM。
?
?
返回數據方法同經典篇里的讀數據。取時間請用 $_SERVER['REQUEST_TIME']。
LM的cache時間可以設置的盡量長些,比如一個月。
?PHP中動態數據使用Last-Modified加速原理詳細說明:
動態應用項目中充分利用LM來加速響應,減少邏輯處理以及數據傳輸。
最初考慮是用etag實現,引入這一機制并不僅僅是為了節約帶寬。它還用來減化應用程序邏輯。比如正常取一個數據,需要取好幾個表的東西,大概消耗200ms。如果我把etag作為數據版本來用,只需要取memcache里的版本號判斷一下,對于大多數用戶來說,都沒更新數據,就不用走后面的判斷了,直接返回304狀態。但是IE6里,如果同時使用gzip,又使用etag,etag就會失效。這是ie6的bug,沒有遵守http 1.1。
現在使用方法,把數據最后更改時間戳作為版本。模擬靜態數據使用Last-Modified 。這樣做的缺陷是,單位只能精確到秒,如果一秒內做多次修改,將不能區分。不過對我們目前應用來說,精確到一秒已經足夠用了,用戶的動作沒那么快。另外,還有一個細節,將決定這個機制能否應用在我們的項目中。我們既要使用緩存,又要其他任終端,或者瀏覽器通過js拿數據的時候立刻拿到最新的。大家知道,如果你使用了Last-Modified,通過js在當前瀏覽器下再次取數據的時候,瀏覽器不會發起任何請求,新數據當然無從拿到。如果能讓瀏覽器發送請求的時候帶上If-Modified-Since,又能每次都讓瀏覽器產生請求,便能解決問題。
于是,我們通過php輸出數據的時候同時使用這樣的兩個頭信息,便達到了目的。
Cache-Control: max-age=1
Last-Modified: Tue, 11 May 2010 10:58:11 GMT
這樣做的假設是:用戶點一個組合查看數據,然后用戶在手機上添加一個股票信息,然后用戶切到了別的組合,然后又切回這個組合查看數據,這4個動作不可能在同一秒內完成。我反正是做不到那么快,超人例外。
?
轉自:http://blog.sina.com.cn/s/articlelist_1181509184_0_1.html
擴展知識:一致哈希算法、delate壓縮算法、NAT/TUN/DR
總結
以上是生活随笔為你收集整理的实践:服务器编写/系统架构/cache的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 杨建:网站加速--实例分析篇
- 下一篇: 基于jQuery+cookie的视频断点