從奧運訂票系統癱瘓說起——談FastCGI 與IT 架構
2008年,對於首都人民來說,沒有什麼比奧運會更大的事情了。如何買到一張稱心如意的比賽門票,也成了很多人的一個夢想。然而,在奧運官網搶票購買的時候,這個夢想卻輕易地被網上購票系統的癱瘓擊成碎片,很多充滿熱情的老百姓們也因此鬱悶無比。由於搜狐承擔了奧運的官網,我又在那裡工作過相當長一段時間,很多兄弟搶票失敗,於是便認定是搜狐開發的系統太爛,而找我抱怨。其實當時我也很是鬱悶:首先這個系統並非搜狐開發;其次我也不在搜狐了。雖然如此,和我同行的一些朋友,又開始問我如何解決類似問題。我也反反覆覆講了很多次,為了讓廣大讀者能夠深入瞭解背後的原因和機制,寫出來,大家一起討論可能效果會更好。當然,這並不是我說的架構就一定能解決問題,僅僅是拋磚引玉而已。
在說架構之前,我先說一個老的技術,FastCGI。因為這個技術在後面的結構闡述中將起到非常重要的用處,原以為應該會有不少人會知道,但後來發現好像並非如此。
關於FastCGI的歷史我就不再贅述,好像自1993年便有了。目前最熱門的視訊網站YouTube體系結構中,就有fast-cgi的模組。它支援很多httpd伺服器,在官方網站上列了很多,如apache,aXesW3 ,Microsoft IIS,Zeus,近幾年才出的lighttpd沒寫,其實這個新的httpd也支援,但我個人覺得,支援最好的,可能還是Apache。
先講講FastCGI的原理,它和現在常用的執行請求不同,維基百科上有一個術語形容它,這裡借用一下:
◆ 短生存期應用程式
◆ 長生存期應用程式
CGI技術的機制是:每次當客戶請求一個CGI的時候,Web伺服器就請求作業系統生成一個新的CGI程序;當CGI滿足要求後,伺服器就殺死這個程序。並且伺服器對客戶端的每個請求都要重複這樣的過程。
而FastCGI技術的機制為:FastCGI程式一旦產生後,它可以持續工作,一直保持滿足客戶的請求直到自己被明確終止。如果你希望通過協同處理來提高程式的效能,你可以請求Web伺服器執行多個FastCGI 應用程式。
由此CGI就是所謂的短生存期應用程式,FastCGI就是所謂的長生存期應用程式。
由於FastCGI程式並不需要不斷產生新程序,可以大大降低伺服器的壓力。並且產生較高的應用效率。如今,流行的Java語言Servlet技術,在設計上就是參考FastCGI技術。
FastCGI 配置執行一般來說分三種,這三種都需要Apache的mod_fastcgi 進行處理。
1、Standalone FastCGI Server, 應該是獨立的伺服器。首先是需要把fastcgi作為單獨的守護程序:
$ script/myapp_fastcgi.pl -l /tmp/myapp.socket -n 5
以下是這個fastcgi的守護程序的引數:
-d -daemon #作為守護程序
-p -pidfile #管理程序的PID寫入到到檔案的名稱
-l -listen #SOCKET的路徑,機器名:埠, 或者埠
-n -nproc #起始接受請求的程序數
然後把下面的程式碼加入Apache的HTTPD.CONF:
FastCgiExternalServer /tmp/myapp -socket /tmp/myapp.socket
Alias /myapp/ /tmp/myapp/
# Or, 可以使用root的身份執行
Alias / /tmp/myapp/
# Optionally,(使用rewrite模組)
RewriteRule ^/myapp$ myapp/ [R]
然後重啟Apache就OK了
2、Static mode:靜態模式, 一般是用於單一確定的模式,就是在Apache 的httpd.conf 中間加上:
FastCgiServer /usr/local/apache/count/count.fcg -processes 1
Alias /c /usr/local/apache/count/count.fcg
此處建議再使用REWRITE的方式 重寫整個的URL匹配, 使之看起來像一個靜態頁面。
RewriteRule read-(.+)-(.+)-(.+).html$ /c?id=$1&sid=$2&port=$3 [L]
3、Dynamic mode:動態模式,可以使用各種各樣的fastcgi,加入到httpd.conf中間去,比如:
AddHandler fastcgi-script .fcgi
還有一個關鍵的設定:
<Directory /path/to/MyApp>
Options +ExecCGI
</Directory>
這個配置建議放在cgi-bin 這種類似的目錄裡面。
請注意第二種,伺服器起幾個程序,是由-processes 1 這個引數來控制的,所以起多少你可以自己來定,我們在下面的一個關鍵模組中將使用這個模式。
下面放一段FastCGI程式的C程式碼,來說明一下:
#include <fcgi_stdio.h>
#include <string.h>
void main(void){
int count = 0;
while(FCGI_Accept() >= 0) {
printf("Content-type:text/html ");
printf(" ");
printf("<HTML> "
"<HEAD>
"<TITLE>FastCGI</TITLE> "
"<META http-equiv="Content-Type" "
"content="text/html; charset=utf-8"> <body> " "Hello world!<br> ");
printf("Request number %d.",count++);
printf("</body></html> ”);
}
exit(0);
}
這是一個很簡單的例子,就是簡單的計數, 大家可以注意這一句:while(FCGI_Accept() >= 0)
這就是它和普通的短週期程式最大的不同,一般CGI都是執行完就退出了,這個FastCGI,在處理完一個請求完畢後,會回到初始狀態等待下一次請求;如果這個程式被設定成為只能啟動一個,那麼無論是否訪問這個頁面,都是在前一個的基礎上加一,而不會又產生新的程序;從而後來者是從零開始。當然,很多人也都注意到,此處就是一個死迴圈在不斷處理;如果程式比較複雜,存在記憶體洩露的問題,此處產生的問題也要比普通CGI要嚴重得多,所以使用它對於程式設計師的要求也更高。
上述方案應該是所有的Web應用解決方案中,執行效率和速度最高的。官方資料是說比一般的高15倍左右,在我的機器上測試,基本上每秒能夠處理大概2400次請求。
再回到我們說的正題:奧運訂票系統的癱瘓,關於訪問量,當時的說法是800萬/小時,那麼平均到每秒就是超過2200次。這對於訂票系統來說,確實是一個非常大的考驗。畢竟這種狀況下,資料庫是肯定承擔不住這個量級的訪問了。如何進行架構設計,是我們都需要面對的問題。
如果設計要應對這種高負載、高訪問量的結構,首先考慮這個系統的需求。其實具體過程比較簡單:
1.使用者認證
2.檢視所有可以訂票的專案和票的數量
3.選擇專案,放入購物車
4.確認並提交訂單
5.訂單成功扣款
過程雖然簡單,但其實裡面的東西也不少。
由於使用者的資料量很大,註冊使用者數百萬以上;而且這種系統,登入使用者在操作時應該不存在普通應用的2/8原則。在搶票的當天,絕大部分註冊使用者都會登入,而且時間會非常集中,所以併發會非常大。你如果預算充足,放一萬臺伺服器來做這個事情,做一個分佈演算法,然後每臺服務不超過十萬個使用者,這樣你就能充分保證你的使用者感受和體驗。可我想實際上沒有哪個公司和系統會這麼做,即使是財大氣粗的奧組委。
這個時候,很多人可能會想:上面提到的FastCGI這種高效率的程式就是針對類似狀況的解決方案,其實這是很常見的錯誤。我想這個訂票之所以會癱瘓,就是由於部分設計過於高效,而部分不可能那麼高效的緣故。比如登入這個模組的效率估計就非常高,因為登入只是在資料庫對比一下使用者名稱和密碼,而且資料更新也不頻繁,完全可以用分散式資料庫來解決。但使用者登入後,所有的壓力會全部壓在後面的功能上,從而造成系統的癱瘓。這個時候,由於人太多,你無論怎麼高效,在執行到後面複雜的購買功能時,都會出現瓶頸。而如果真的放一萬臺服務,你的資料如何分佈同步,然後真的做到先來先得,會很難,如果設計的不好,和抽籤也就沒什麼兩樣了。
所以這個系統設計的策略應該是:如何做到在保證使用者感受的情況下,合理控制進入系統的人數,這樣你後面的設計和開發的壓力會小的多,而且成本的控制非常清楚。
那麼剩下的做法就很清晰了:系統的重點是使用者登入,而不是一般理解的後面購票提交的系統功能。如何控制進入的人數,我覺得不妨參考銀行的叫號方式來設計:系統先給使用者發號,然後當了解到有資源空出來時,再讓使用者登入。
這個結構的重點就在呼號中心和序列號的分配上面。
1. 序列號分配中心,技術重點在於高效和唯一性。也就是說當用戶訪問數達到海量之後,你需要非常迅速地分配唯一的序列號給登入的使用者。這種狀況下,其他很多技術無法承擔這種需求。開始提到的FastCGI,就是這個模式下的唯一選擇。我們在開始安裝的時候,就可以使用這種只起單個程序的模式,所以分配使用者的序列號只會是唯一的。由於FastCGI的高效率,從而保證登入的使用者可以迅速分配到一個號,然後離開。當然如果你還不放心的話,還可以在前面再加一個負載均衡的裝置,完成對幾個不同伺服器負載分配,然後每個機器加不同的步長,並且起始數字不一樣。比如:如果你有2臺機器做發號工作,第一臺起始數字為1,第二臺為2,步長為二,就是每次累加2,這樣使用者在不同的機器上也會得到唯一的號,而效率就能提高兩倍。
至於記錄使用者序列號的方式,可以用cookie記錄在客戶端,然後進行加密。使用者記錄後,進入呼號中心,比對手裡的號和前面排隊的人數,然後提示使用者前面排隊人數。比方說,你上來就是排號在3千萬以後了,前面有2千多萬人,我想如果這個人頭腦正常的話,就不會說這個系統太爛,只能說自己起晚了,然後感嘆中國人實在是太多,就不會再上去反覆不停地登入。
2. 呼號中心,這裡大概是最麻煩,也是最關鍵的地方。由於訂票系統是B/S結構,伺服器端有動作的時候,如何通知客戶端是一個要點。也就是說,當有人訂票完畢,從系統中退出,此時,中控中心知道後,會通知呼號中心呼叫下一個。呼號中心如何找到應呼叫的號碼,有兩種解決方法,具體實現都不妨通過AJAX的區域性重新整理達成。
第一種,和叫號系統的號進行比對,如果發現匹配成功,就通知客戶端進入系統。
第二種,判斷這個使用者前面的排隊數量,如果發現為零,就觸發進入這個系統的動作。
還要注意一點,就是重新整理時間長短和叫號的失效問題。時間太短,伺服器壓力會很大;時間太長又會容易造成這個使用者感覺沒有變化,從而感受很差。所以這個時間的設定,個人覺得在5-15秒之間調整會比較合適。然後壓力需要分攤,也就是叫號伺服器需要設定多個。這樣的話,使用者重新整理會命中不同的伺服器,此時需要對資料的同步進行特殊處理,其架構如下:
這個訊息接受模組可以有兩種模式取資訊:短連線,每隔一段時間來傳遞資訊;長連線,就是在訊息接受和中控伺服器中建立一個長效的訊息通知機制。由於對於資訊及時性的要求比較高,所以採用長連線比較合適。
訊息接受模組和中控伺服器之間需要進行序列號的交換。由於你不知道捏著這個號的使用者命中哪臺伺服器,所以失效機制需要在幾個伺服器上同時進行。也就是說,當一個使用者退出,中控伺服器知道後,開始確認最後一個登入號,然後發給所有前端,前端要能保證通知到使用者,然後向用戶發出通知,說明如果在給定次數內使用者還不進行登入或者認證,就提示後端此號失效,系統再分配下一個號給前端進行通知,
如果要設計得更加精巧,還可以建立前端伺服器之間的訊息通知機制。就是當一臺伺服器發現這個號在自己上面,就通知幾個前端,不再對這個號進行判斷,儘量節約資源。
3. 中控伺服器。我在開發社群和直播間的時候,都用到了這種方式,此處也用到了。不過在這個系統中,中控伺服器不必使用單獨的物理伺服器,這裡可以只是一個模組,它的主要用途是通知這些叫號伺服器。由於資料很簡單,所以中控的分發比較容易,不用設計特別複雜的協議。
4. 認證中心:唯一需要改動的,就是判斷使用者的序列號是否可用並且是真正的號碼。
5. 購票中心:此處有很多種分佈的方法,有很多可以借鑑的結構,這裡就不贅述了。在這個架構中,購票唯一需要確認的就是可以同時承擔多少人同時線上購買
前三個部分是這個架構的核心部分,由於進入的人數可以控制,後面的系統就還可以使用老的訂票系統,只用確認同時放進來多少人就可以,也就是視窗沒變,只是大家不再一擁而上,都是文明人,請排隊拿號。
當然後面的架構還可以重新進行優化和設計,從而儘可能提高放進來人的數量,在進行設計購票功能時還可以借鑑這方面的模式。比如:籃球是愛好觀眾比較多的運動,大家都想到現場看看科比同學的扣籃,進來人後,可能大家都會一擁而上先搶這個,從而造成區域性的資料癱瘓,影響整個系統。此時也可以在裡面暗含這個模組。買票的人少,拿號看不出來,拿了就能進去;一旦人數到了極限,對不起,也請排隊。
限制人員進入後,未進入的人和購買的人不在同一個系統中,從而不會妨礙進入的人,買的人也會很快解決,他們可以迅速完成訂單。提交後,系統發現這個人無法再訂其他球票的時候,就可以認為再放一個人進來,或者乾脆做絕一點,馬上將其踢出去,以節約資源。
而且,由於你可以控制進入的使用者數量,從而系統其他部分的設計簡單多了。多大的錢辦多少事,如果領導想快一點了事,預算充足,那麼放入的人就多;如果心裡面沒底,那麼可以先放很少人進來,或者說大概估計一下,只放多少號,如就賣10萬張票,那麼只放50萬個號,放完了就沒有了。使用者來晚了,連號都沒有,也只能慨嘆自己不夠及時,這樣比系統癱瘓要好得多。
對於這個架構,其設計重點就是把系統整體的資源處於可控的狀態。很多類似系統,如:報名,考試,短時間搶購等等實際應用系統,都可以採用類似的方式解決。好的架構,並不是說能解決所有的問題,而是很清楚自己能做什麼,不能做什麼。
文/錢巨集武