12306.cn網站掛了,被全國人民罵了。我這兩天也在思考這個事,我想以這個事來粗略地和你們討論一下網站性能的問題。由於倉促,並且徹底基於本人有限的經驗和了解,因此,若是有什麼問題還請你們一塊兒討論和指正。(這又是一篇長文,只討論性能問題,不討論那些UI,用戶體驗,或是是否把支付和購票下單環節分開的功能性的東西)css
任何技術都離不開業務需求,因此,要說明性能問題,首先仍是想先說說業務問題。html
多說幾句:前端
我說那麼多,我只是想從業務上告訴你們,咱們須要從業務上真正瞭解春運鐵路訂票這樣業務的變態之處。linux
要解決性能的問題,有不少種經常使用的方法,我在下面列舉一下,我相信12306這個網站使用下面的這些技術會讓其性能有質的飛躍。nginx
經過DNS的負載均衡器(通常在路由器上根據路由的負載重定向)能夠把用戶的訪問均勻地分散在多個Web服務器上。這樣能夠減小Web服務器的請求負載。由於http的請求都是短做業,因此,能夠經過很簡單的負載均衡器來完成這一功能。最好是有CDN網絡讓用戶鏈接與其最近的服務器(CDN一般伴隨着分佈式存儲)。(關於負載均衡更爲詳細的說明見「後端的負載均衡」)算法
我看了一下12306.cn,打開主頁須要建60多個HTTP鏈接,車票預訂頁面則有70多個HTTP請求,如今的瀏覽器都是併發請求的。因此,只要有100萬個用戶,就會有6000萬個連接,太多了。一個登陸查詢頁面就行了。把js打成一個文件,把css也打成一個文件,把圖標也打成一個文件,用css分塊展現。把連接數減到最低。數據庫
這個世界不是哪一個公司都敢作圖片服務的,由於圖片太耗帶寬了。如今寬帶時代很難有人能體會到當撥號時代作個圖頁都不敢用圖片的情形(如今在手機端瀏覽也是這個情形)。我查看了一下12306首頁的須要下載的總文件大小大約在900KB左右,若是你訪問過了,瀏覽器會幫你緩存不少,只需下載10K左右的文件。可是咱們能夠想像一個極端一點的案例,1百萬用戶同時訪問,且都是第一次訪問,每人下載量須要1M,若是須要在120秒內返回,那麼就須要,1M * 1M /120 * 8 = 66Gbps的帶寬。很驚人吧。因此,我估計在當天,12306的阻塞基本上應該是網絡帶寬,因此,你可能看到的是沒有響應。後面隨着瀏覽器的緩存幫助12306減小不少帶寬佔用,因而負載一下就到了後端,後端的數據處理瓶頸一下就出來。因而你會看到不少http 500之類的錯誤。這說明服務器垮了。後端
靜態化一些不常變的頁面和數據,並gzip一下。還有一個並態的方法是把這些靜態頁面放在/dev/shm下,這個目錄就是內存,直接從內存中把文件讀出來返回,這樣能夠減小昂貴的磁盤I/O。瀏覽器
不少人查詢都是在查同樣的,徹底能夠用反向代理合並這些併發的相同的查詢。這樣的技術主要用查詢結果緩存來實現,第一次查詢走數據庫得到數據,並把數據放到緩存,後面的查詢通通直接訪問高速緩存。爲每一個查詢作Hash,使用NoSQL的技術能夠完成這個優化。(這個技術也能夠用作靜態頁面)緩存
對於火車票量的查詢,我的以爲不要顯示數字,就顯示一個「有」或「無」就行了,這樣能夠大大簡化系統複雜度,並提高性能。
緩存能夠用來緩存動態頁面,也能夠用來緩存查詢的數據。緩存一般有那麼幾個問題:
1)緩存的更新。也叫緩存和數據庫的同步。有這麼幾種方法,一是緩存time out,讓緩存失效,重查,二是,由後端通知更新,一量後端發生變化,通知前端更新。前者實現起來比較簡單,但實時性不高,後者實現起來比較複雜 ,但實時性高。
2)緩存的換頁。內存可能不夠,因此,須要把一些不活躍的數據換出內存,這個和操做系統的內存換頁和交換內存很類似。FIFO、LRU、LFU都是比較經典的換頁算法。相關內容參看Wikipeida的緩存算法。
3)緩存的重建和持久化。緩存在內存,系統總要維護,因此,緩存就會丟失,若是緩存沒了,就須要重建,若是數據量很大,緩存重建的過程會很慢,這會影響生產環境,因此,緩存的持久化也是須要考慮的。
諸多強大的NoSQL都很好支持了上述三大緩存的問題。
前面討論了前端性能的優化技術,因而前端可能就不是瓶頸問題了。那麼性能問題就會到後端數據上來了。下面說幾個後端常見的性能優化技術。
關於數據冗餘,也就是說,把咱們的數據庫的數據冗餘處理,也就是減小錶鏈接這樣的開銷比較大的操做,但這樣會犧牲數據的一致性。風險比較大。不少人把NoSQL用作數據,快是快了,由於數據冗餘了,但這對數據一致性有大的風險。這須要根據不一樣的業務進行分析和處理。(注意:用關係型數據庫很容易移植到NoSQL上,可是反過來從NoSQL到關係型就難了)
幾乎全部主流的數據庫都支持鏡像,也就是replication。數據庫的鏡像帶來的好處就是能夠作負載均衡。把一臺數據庫的負載均分到多臺上,同時又保證了數據一致性(Oracle的SCN)。最重要的是,這樣還能夠有高可用性,一臺廢了,還有另外一臺在服務。
數據鏡像的數據一致性多是個複雜的問題,因此咱們要在單條數據上進行數據分區,也就是說,把一個暢銷商品的庫存均分到不一樣的服務器上,如,一個暢銷商品有1萬的庫存,咱們能夠設置10臺服務器,每臺服務器上有1000個庫存,這就好像B2C的倉庫同樣。
數據鏡像不能解決的一個問題就是數據表裏的記錄太多,致使數據庫操做太慢。因此,把數據分區。數據分區有不少種作法,通常來講有下面這幾種:
1)把數據把某種邏輯來分類。好比火車票的訂票系統能夠按各鐵路局來分,可按各類車型分,能夠按始發站分,能夠按目的地分……,反正就是把一張表拆成多張有同樣的字段可是不一樣種類的表,這樣,這些表就能夠存在不一樣的機器上以達到分擔負載的目的。
2)把數據按字段分,也就是豎着分表。好比把一些不常常改的數據放在一個表裏,常常改的數據放在另外多個表裏。把一張表變成1對1的關係,這樣,你能夠減小表的字段個數,一樣能夠提高必定的性能。另外,字段多會形成一條記錄的存儲會被放到不一樣的頁表裏,這對於讀寫性能都有問題。但這樣一來會有不少複雜的控制。
3)平均分表。由於第一種方法是並不必定平均分均,可能某個種類的數據仍是不少。因此,也有采用平均分配的方式,經過主鍵ID的範圍來分表。
4)同一數據分區。這個在上面數據鏡像提過。也就是把同一商品的庫存值分到不一樣的服務器上,好比有10000個庫存,能夠分到10臺服務器上,一臺上有1000個庫存。而後負載均衡。
這三種分區都有好有壞。最經常使用的仍是第一種。數據一旦分區,你就須要有一個或是多個調度來讓你的前端程序知道去哪裏找數據。把火車票的數據分區,並放在各個省市,會對12306這個系統有很是有意義的質的性能的提升。
前面說了數據分區,數據分區能夠在必定程度上減輕負載,可是沒法減輕熱銷商品的負載,對於火車票來講,能夠認爲是大城市的某些主幹線上的車票。這就須要使用數據鏡像來減輕負載。使用數據鏡像,你必然要使用負載均衡,在後端,咱們可能很難使用像路由器上的負載均衡器,由於那是均衡流量的,由於流量並不表明服務器的繁忙程度。所以,咱們須要一個任務分配系統,其還能監控各個服務器的負載狀況。
任務分配服務器有一些難點:
我看到有不少系統都用靜態的方式來分配,有的用hash,有的就簡單地輪流分析。這些都不夠好,一個是不能完美地負載均衡,另外一個靜態的方法的致命缺陷是,若是有一臺計算服務器死機了,或是咱們須要加入新的服務器,對於咱們的分配器來講,都須要知道的。
還有一種方法是使用搶佔式的方式進行負載均衡,由下游的計算服務器去任務服務器上拿任務。讓這些計算服務器本身決定本身是否要任務。這樣的好處是能夠簡化系統的複雜度,並且還能夠任意實時地減小或增長計算服務器。可是惟一很差的就是,若是有一些任務只能在某種服務器上處理,這可能會引入一些複雜度。不過整體來講,這種方法多是比較好的負載均衡。
異步、throttle(節流閥) 和批量處理都須要對併發請求數作隊列處理的。
因此,只要是異步,通常都會有throttle機制,通常都會有隊列來排隊,有隊列,就會有持久化,而系統通常都會使用批量的方式來處理。
雲風同窗設計的「排隊系統」 就是這個技術。這和電子商務的訂單系統很類似,就是說,個人系統收到了你的購票下單請求,可是我尚未真正處理,個人系統會跟據我本身的處理能力來throttle住這些大量的請求,並一點一點地處理。一旦處理完成,我就能夠發郵件或短信告訴用戶你來能夠真正購票了。
在這裏,我想經過業務和用戶需求方面討論一下雲風同窗的這個排隊系統,由於其從技術上看似解決了這個問題,可是從業務和用戶需求上來講可能仍是有一些值得咱們去深刻思考的地方:
1)隊列的DoS攻擊。首先,咱們思考一下,這個隊是個單純地排隊的嗎?這樣作還不夠好,由於這樣咱們不能杜絕黃牛,並且單純的ticket_id很容易發生DoS攻擊,好比,我發起N個 ticket_id,進入購票流程後,我不買,我就耗你半個小時,很容易我就可讓想買票的人幾天都買不到票。有人說,用戶應該要用身份證來排隊, 這樣在購買裏就必須要用這個身份證來買,但這也還不能杜絕黃牛排隊或是號販子。由於他們能夠註冊N個賬號來排隊,但就是不買。黃牛這些人這個時候只須要幹一個事,把網站搞得正常人不能訪問,讓用戶只能經過他們來買。
2)對列的一致性?對這個隊列的操做是否是須要鎖?只要有鎖,性能必定上不去。試想,100萬我的同時要求你來分配位置號,這個隊列將會成爲性能瓶頸。你必定沒有數據庫實現得性能好,因此,可能比如今還差
3)隊列的等待時間。購票時間半小時夠不夠?多很少?要是那時用戶正好不能上網呢?若是時間短了,用戶不夠時間操做也會抱怨,若是時間長了,後面在排隊的那些人也會抱怨。這個方法可能在實際操做上會有不少問題。另外,半個小時太長了,這徹底不現實,咱們用15分鐘來舉例:有1千萬用戶,每個時刻只能放進去1萬個,這1萬個用戶須要15分鐘完成全部操做,那麼,這1千萬用戶所有處理完,須要1000*15m = 250小時,10天半,火車早開了。(我並不是亂說,根據鐵道部專家的說明:這幾天,平均一天下單100萬,因此,處理1000萬的用戶須要十天。這個計算可能有點簡單了,我只是想說,在這樣低負載的系統下用排隊可能都不能解決問題)
4)隊列的分佈式。這個排隊系統只有一個隊列好嗎?還不足夠好。由於,若是你放進去的能夠購票的人若是在買同一個車次的一樣的類型的票(好比某動車臥鋪),仍是等於在搶票,也就是說系統的負載仍是會有可能集中到其中某臺服務器上。所以,最好的方法是根據用戶的需求——提供出發地和目的地,來對用戶進行排隊。而這樣一來,隊列也就能夠是多個,只要是多個隊列,就能夠水平擴展了。
我以爲徹底能夠向網上購物學習。在排隊(下單)的時候,收集好用戶的信息和想要買的票,並容許用戶設置購票的優先級,好比,A車次臥鋪買 不到就買 B車次的臥鋪,若是還買不到就買硬座等等,而後用戶把所需的錢先充值好,接下來就是系統徹底自動地異步處理訂單。成功不成功都發短信或郵件通知用戶。這樣,系統不只能夠省去那半個小時的用戶交互時間,自動化加快處理,還能夠合併相同購票請求的人,進行批處理(減小數據庫的操做次數)。這種方法最妙的事是能夠知道這些排隊用戶的需求,不但能夠優化用戶的隊列,把用戶分佈到不一樣的隊列,還能夠像亞馬遜的心願單同樣,讓鐵道部作車次統籌安排和調整(最後,排隊系統(下單系統)仍是要保存在數據庫裏的或作持久化,不能只放在內存中,否則機器一down,就等着被罵吧)。
寫了那麼多,我小結一下:
0)不管你怎麼設計,你的系統必定要能容易地水平擴展。也就是說,你的整個數據流中,全部的環節都要可以水平擴展。這樣,當你的系統有性能問題時,「加3倍的服務器」纔不會被人譏笑。
1)上述的技術不是一朝一夕能搞定的,沒有長期的積累,基本無望。咱們能夠看到,不管你用哪一種都會引起一些複雜性。
2)集中式的賣票很難搞定,使用上述的技術可讓訂票系統能有幾佰倍的性能提高。而在各個省市建分站,分開賣票,是能讓現有系統性能有質的提高的最好方法。
3)春運前夕搶票且票量供遠小於求這種業務模式是至關變態的,讓幾千萬甚至上億的人在某個早晨的8點鐘同時登陸同時搶票的這種業務模式是變態中的變態。業務形態的變態決定了不管他們怎麼辦幹必定會被罵。
4)爲了那麼一兩個星期而搞那麼大的系統,而其它時間都在閒着,有些惋惜了,這也就是鐵路才幹得出來這樣的事了。