12306.cn網站掛了,被全國人民罵了。我這兩天也在思考這個事,我想以這個事來粗略地和大家討論一下網站性能的問題。因為倉促,而且完全基於本人有限的經驗和瞭解,所以,如果有什麼問題還請大家一起討論和指正。(這又是一篇長文,只討論性能問題,不討論那些UI,用戶體驗,或是是否把支付和購票下單環節分開的功能性的東西)
業務
任何技術都離不開業務需求,所以,要說明性能問題,首先還是想先說說業務問題。
·
其一,有人可能把這個東西和QQ或是網遊相比。但我覺得這兩者是不一樣的,網遊和QQ線上或是登錄時訪問的更多的是使用者自己的資料,而訂票系統訪問的是中心的票量資料,這是不一樣的。不要覺得網遊或是QQ能行你就以為這是一樣的。網遊和QQ 的後端負載相對於電子商務的系統還是簡單。
·
其二,有人說春節期間訂火車的這個事好像網站的秒殺活動。的確很相似,但是如果你的思考不在表面的話,你會發現這也有些不一樣。火車票這個事,一方面會伴隨著大量的查詢操作,更BT的是下單的時候需要對資料庫很多的一致性的操作,一方面是從起點到終點各個分段票的一致性,另一方面,買的人路線、車次、時間選擇有很多,會不停地改變下單方式。而秒殺,直接殺就好了,沒有那麼多查詢和一致性的問題。另外,關於秒殺,完全可以做成只接受前N個用戶的請求(完全不操作後端的任何資料, 僅僅只是對用戶的下單操作log),這種業務,只需要在記憶體cache中放好可秒殺的數量,還可以把資料分佈開來放,100商品,10台伺服器一台放10個,無需在當時操作任何資料庫。可以訂單數夠後,停止秒殺,然後批量寫資料庫。而且秒殺的商品不多。火車票這個不是像秒殺那麼簡單的,春運時間,幾乎所有的票都是熱門票,而且幾乎是全國人民都來了,而且還有轉車業務,多條線的庫存都要做事務操作,你想想吧,這有多難。(淘寶的雙十一也就3百萬用戶,而火車票暫態有千萬級別甚至是億級別的)(更新:2014年1月11日:來了淘寶後,對淘寶的系統有瞭解,淘寶的秒殺活動,本質上是用輸驗證碼並在CDN上把用戶直接過濾掉了,比如:1千萬個用戶過濾了只剩2萬個使用者,這樣資料庫就頂得住了)
·
其三,有人拿這個系統和奧運會的票務系統比較。我覺得還是不一樣。雖然奧運會的票務系統當年也一上線就廢了。但是奧運會用的是抽獎的方式,也就是說不存在先來先得的搶的方式,而且,是事後抽獎,事前只需要收資訊,事前不需要保證資料一致性,沒有鎖,很容易水準擴展。
·
其四,訂票系統應該和電子商務的訂單系統很相似,都是需要對庫存進行:1)占住庫存,2)支付(可選),3)扣除庫存的操作。這個是需要有一致性的檢查的,也就是在併發時需要對資料加鎖的。B2C的電商基本上都會把這個事幹成非同步的,也就是說,你下的訂單並不是馬上處理的,而是延時處理的,只有成功處理了,系統才會給你一封確認郵件說是訂單成功。我相信有很多朋友都收到認單不成功的郵件。這就是說,資料一致性在併發下是一個瓶頸。
·
其五,鐵路的票務業務很變態,其採用的是突然放票,而有的票又遠遠不夠大家分,所以,大家才會有搶票這種有中國特色的業務的做法。於是當票放出來的時候,就會有幾百萬人甚至上千萬人殺上去,查詢,下單。幾十分鐘內,一個網站能接受幾千萬的訪問量,這個是很恐怖的事情。據說12306的高峰訪問是10億PV,集中在早8點到10點,每秒PV在高峰時上千萬。
多說幾句:
·
庫存是B2C的惡夢,庫存管理相當的複雜。不信,你可以問問所有傳統和電務零售業的企業,看看他們管理庫存是多麼難的一件事。不然,就不會有那麼多人在問凡客的庫存問題了。(你還可以看看《約伯斯傳》,你就知道為什麼Tim會接任Apple的CEO了,最主要的原因是他搞定了蘋果的庫存週期問題)
·
對於一個網站來說,流覽網頁的高負載很容易搞定,查詢的負載有一定的難度去處理,不過還是可以通過緩存查詢結果來搞定,最難的就是下單的負載。因為要訪問庫存啊,對於下單,基本上是用非同步來搞定的。去年雙11節,淘寶的每小時的訂單數大約在60萬左右,京東一天也才能支持40萬(居然比12306還差),亞馬遜5年前一小時可支援70萬訂單量。可見,下訂單的操作並沒有我們相像的那麼性能高。
·
淘寶要比B2C的網站要簡單得多,因為沒有倉庫,所以,不存在像B2C這樣有N個倉庫對同一商品庫存更新和查詢的操作。下單的時候,B2C的 網站要去找一個倉庫,又要離用戶近,又要有庫存,這需要很多計算。試想,你在北京買了一本書,北京的倉庫沒貨了,就要從周邊的倉庫調,那就要去看看瀋陽或 是西安的倉庫有沒有貨,如果沒有,又得看看江蘇的倉庫,等等。淘寶的就沒有那麼多事了,每個商戶有自己的庫存,庫存就是一個數字,並且庫存分到商戶頭上了,反而有利於性能擴展。
·
資料一致性才是真正的性能瓶頸。有 人說nginx可以搞定每秒10萬的靜態請求,我不懷疑。但這只是靜態請求,理論值,只要頻寬、I/O夠強,伺服器計算能力夠,並支援的併發連接數頂得住10萬TCP連結的建立 的話,那沒有問題。但在資料一致性面前,這10萬就完完全全成了一個可望不可及的理論值了。
我說那麼多,我只是想從業務上告訴大家,我們需要從業務上真正瞭解春運鐵路訂票這樣業務的變態之處。
前端性能優化技術
要解決性能的問題,有很多種常用的方法,我在下面列舉一下,我相信12306這個網站使用下面的這些技術會讓其性能有質的飛躍。
一、前端負載均衡
通過DNS的負載等化器(一般在路由器上根據路由的負載重定向)可以把用戶的訪問均勻地分散在多個Web伺服器上。這樣可以減少Web伺服器的請求負載。因為http的請求都是短作業,所以,可以通過很簡單的負載等化器來完成這一功能。最好是有CDN網路讓使用者連接與其最近的伺服器(CDN通常伴隨著分散式存儲)。(關於負載均衡更為詳細的說明見“後端的負載均衡”)
二、減少前端連結數
我看了一下12306.cn,打開主頁需要建60多個HTTP連接,車票預訂頁面則有70多個HTTP請求,現在的流覽器都是併發請求的(當然,流覽器的一個頁面的併發數是有限的,但是你擋不住使用者開多個頁面,而且,後端伺服器TCP連結在前端斷開始,還不會馬上釋放或重要)。所以,只要有100萬個用戶,就有可能會有6000萬個連結(訪問第一次後有了流覽器端的cache,這個數會下來,就算只有20%也是百萬級的連結數),太多了。一個登錄查詢頁面就好了。把js打成一個檔,把css也打成一個檔,把圖示也打成一個檔,用css分塊展示。把連結數減到最低。
三、減少網頁大小增加頻寬
這個世界不是哪個公司都敢做圖片服務的,因為圖片太耗頻寬了。現在寬頻時代很難有人能體會到當撥號時代做個圖頁都不敢用圖片的情形(現在在手機端流覽也是這個情形)。我查看了一下12306首頁的需要下載的總檔大小大約在900KB左右,如果你訪問過了,流覽器會幫你緩存很多,只需下載10K左右的文件。但是我們可以想像一個極端一點的案例,1百萬用戶同時訪問,且都是第一次訪問,每人下載量需要1M,如果需要在120秒內返回,那麼就需要,1M * 1M /120 *
8 = 66Gbps的頻寬。很驚人吧。所以,我估計在當天,12306的阻塞基本上應該是網路頻寬,所以,你可能看到的是沒有回應。後面隨著流覽器的緩存幫助12306減少很多頻寬佔用,於是負載一下就到了後端,後端的資料處理瓶頸一下就出來。於是你會看到很多http 500之類的錯誤。這說明後端伺服器垮了。
四、前端頁面靜態化
靜態化一些不常變的頁面和資料,並gzip一下。还有一个变态的方法是把这些静态页面放在/dev/shm下,这个目录就是内存,直接从内存中把文件读出来返回,这样可以减少昂贵的磁盘I/O。使用nginx的sendfile功能可以讓這些靜態檔直接在內核心態交換,可以極大增加性能。
五、優化查詢
很多人查詢都是在查一樣的,完全可以用反向代理合併這些併發的相同的查詢。這樣的技術主要用查詢結果緩存來實現,第一次查詢走資料庫獲得資料,並把資料放到緩存,後面的查詢統統直接訪問快取記憶體。為每個查詢做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這個系統有非常有意義的質的性能的提高。
四、後端系統負載均衡
前面說了資料分區,資料分區可以在一定程度上減輕負載,但是無法減輕熱銷商品的負載,對於火車票來說,可以認為是大城市的某些骨幹上的車票。這就需要使用資料鏡像來減輕負載。使用資料鏡像,你必然要使用負載均衡,在後端,我們可能很難使用像路由器上的負載等化器,因為那是均衡流量的,因為流量並不代表伺服器的繁忙程度。因此,我們需要一個任務分配系統,其還能監控各個伺服器的負載情況。
任務分配伺服器有一些難點:
·
負載情況比較複雜。什麼叫忙?是CPU高?還是磁片I/O高?還是記憶體使用高?還是併發高?還是記憶體換頁率高?你可能需要全部都要考慮。這些資訊要發送給那個任務分配器上,由任務分配器挑選一台負載最輕的伺服器來處理。
·
任務分配伺服器上需要對任務佇列,不能丟任務啊,所以還需要持久化。並且可以以批量的方式把任務分配給計算伺服器。
·
任務分配伺服器死了怎麼辦?這裡需要一些如Live-Standby或是failover等高可用性的技術。我們還需要注意那些持久化了的任務的佇列如何轉移到別的伺服器上的問題。
我看到有很多系統都用靜態的方式來分配,有的用hash,有的就簡單地輪流分析。這些都不夠好,一個是不能完美地負載均衡,另一個靜態的方法的致命缺陷是,如果有一台計算伺服器死機了,或是我們需要加入新的伺服器,對於我們的分配器來說,都需要知道的。另外,還要重算雜湊(一致性hash可以部分解決這個問題)。
還有一種方法是使用搶佔式的方式進行負載均衡,由下游的計算伺服器去任務伺服器上拿任務。讓這些計算伺服器自己決定自己是否要任務。這樣的好處是可以簡化系統的複雜度,而且還可以任意即時地減少或增加計算伺服器。但是唯一不好的就是,如果有一些任務只能在某種伺服器上處理,這可能會引入一些複雜度。不過總體來說,這種方法可能是比較好的負載均衡。
五、非同步、 throttle 和 批量處理
非同步、throttle(節流閥) 和批量處理都需要對併發請求數做佇列處理的。
·
非同步在業務上一般來說就是收集請求,然後延時處理。在技術上就是可以把各個處理常式做成並行的,也就可以水準擴展了。但是非同步的技術問題大概有這些,a)被調用方的結果返回,會涉及進程執行緒間通信的問題。b)如果程式需要回滾,回滾會有點複雜。c)非同步通常都會伴隨多執行緒多進程,併發的控制也相對麻煩一些。d)很多非同步系統都用消息機制,消息的丟失和亂序也會是比較複雜的問題。
·
throttle 技術其實並不提升性能,這個技術主要是防止系統被超過自己不能處理的流量給搞垮了,這其實是個保護機制。使用throttle技術一般來說是對於一些自己無法控制的系統,比如,和你網站對接的銀行系統。
·
批量處理的技術,是把一堆基本相同的請求批量處理。比如,大家同時購買同一個商品,沒有必要你買一個我就寫一次資料庫,完全可以收集到一定數量的請求,一次操作。這個技術可以用作很多方面。比如節省網路頻寬,我們都知道網路上的MTU(最大傳輸單元),以態網是1500位元組,光纖可以達到4000多個位元組,如果你的一個網路包沒有放滿這個MTU,那就是在浪費網路頻寬,因為網卡的驅動程式只有一塊一塊地讀效率才會高。因此,網路發包時,我們需要收集到足夠多的資訊後再做網路I/O,這也是一種批量處理的方式。批量處理的敵人是流量低,所以,批量處理的系統一般都會設置上兩個閥值,一個是作業量,另一個是timeout,只要有一個條件滿足,就會開始提交處理。
所以,只要是非同步,一般都會有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)無論你怎麼設計,你的系統一定要能容易地水準擴展。也就是說,你的整個資料流程中,所有的環節都要能夠水準擴展。這樣,當你的系統有性能問題時,“加30倍的伺服器”才不會被人譏笑。
1)上述的技術不是一朝一夕能搞定的,沒有長期的積累,基本無望。我們可以看到,無論你用哪種都會引發一些複雜性,設計總是在做一種權衡。
2)集中式的賣票很難搞定,使用上述的技術可以讓訂票系統能有幾佰倍的性能提升。而在各個省市建分站,分開賣票,是能讓現有系統性能有質的提升的最好方法。
3)春運前夕搶票且票量供遠小於求這種業務模式是相當變態的,讓幾千萬甚至上億的人在某個早晨的8點鐘同時登錄同時搶票的這種業務模式是變態中的變態。業務形態的變態決定了無論他們怎麼辦幹一定會被罵。
4)為了那麼一兩個星期而搞那麼大的系統,而其它時間都在閑著,有些可惜了,這也就是鐵路才幹得出來這樣的事了。
更新2012年9月27日
Alexa 統計的12306的PV (注:Alexa的PV定義是:一個使用者在一天內對一個頁面的多次點擊只算一次)
(本文轉載時請注明作者和出處,請勿於記商業目的)
沒有留言:
張貼留言