如果由你來設計 12306.cn,你會怎麼設計?

請問:1、如G72從深圳北到北京西,經過粵湘鄂豫冀京六省,廣鐵、武漢、鄭州、北京4個鐵路局,其資料庫如何按照地區來分配呢?如有一人買到長沙-石家莊的票,那麼深圳北-武漢以及長沙-石家莊等對應票數也須同樣-1,這個演算法如何實現?2、在24小時都有退改票的情況下,是否必須通過隊列池的方式才能降低無效查詢的數量?


0. 確保後端事務處理能力的確足夠. 這個是根據業務指標可以反向推導出來的. 業務雖說足夠複雜(足夠複雜,但事務不複雜),但這幫魂淡有了這麼多年的 B/S 結構下的業務處理經驗,應該不是問題。

1.支持支付寶/QQ帳戶登錄, 本地盡量不存取用戶信息. 現在的 12306 安全堪憂. 一旦用戶信息泄露了怎麼辦? 另一個好處是節省大量開發成本.

2. 必須明確並且做好服務品質協議(Service-Level Agreement),畢竟這是公眾服務. 讓 IBM 專家來給噴噴啥是SLA,畢竟付了100多萬顧問費呢.

3. 做好壓力測試, 這麼大的系統沒有仔細測試系統壓力就放上來無異於找死. 做壓力測試的時候收集好基準數據.

4. 針對城市逐步開放. 遇到問題快速迭代改進. 據說春節後才能上三期,估計這幫孫子現在還是以軟體項目管理方式管理網站項目.

5. 公開業務數據和指標,尋求公眾技術支持,發揮群體智慧的力量.

最後,這個項目費用成本至少可以降低 80% 。(缺乏獨立思考的人才會問為什麼不是 30%,為什麼不是 79%之類的無聊問題)

... 我所說的都是錯的。


前幾天看過一個評論我比較贊同:這個系統幾乎相當於是每天海量秒殺,系統能容納的並發量多大是個大呀?即使這個並發量做到足夠大了,結果是什麼——所有票瞬間買完,然後這個系統全天剩餘時間無所事事。所以,個人以為這個系統首要解決的不是解決並發量,而是從業務上解決,把買票的時間規則改變了,分散了。然後才輪到解決並發量的問題。


這個問題我都不想再摻和了,但想了想還是沒忍住,什麼動態庫存啊,什麼余票更新啊,都是在把一件簡單的事情複雜化。

首先,所謂動態庫存,所謂每買一張車票都要更新其他受影響行程的車票剩餘票數,完全是一個偽命題,對於出票來說,知道一趟車某個行程還剩多少票是沒有意義的,因為出票不是說查詢北京-上海余票30張,然後賣出30張北京-上海車票就完了,每一張車票售出時都要確定好是幾車廂幾排幾號,出票的過程其實是找到一個在該行程起點和終點之間都沒有被售出的座位的過程。

假設一趟車2000個座位,經過上海,南京,鄭州,武漢4個車站,假設購票完全由人工來處理,會是怎樣的流程?我想是這樣的,畫一個2000行×4列的表格,行代表座位號,列代表站,如果售出一張車票,就把這張車票所在的那行從起點到終點所經過的所有車站打上一個勾(除了終點站,因為終點站就下車了)。如下圖,售出一張上海-武漢的車票,表格應該是這樣。

當有人要買票的時候,售票員應該怎麼做?當然是從1號座位開始,一行一行檢查,找到一個能夠滿足這個行程的座位(也就是這個行程上除了終點站以外其他所有站都沒有被打上勾)。還是以上圖舉例,這時來了一位顧客要買上海到南京的車票,售票員從1號座位開始檢查,嗯,1號座位上海站被售出了不滿足條件,跳到2號座位,2號上海站是空位滿足條件,於是售票員就把2號座位賣給了這位顧客,同時在2號座位的上海站打上一個勾(如下圖左)。繼續假設又有一位顧客要買南京-武漢的車票,售票員再次從1號座位開始檢查,1號座位不滿足條件,2號座位滿足條件(上一位顧客在南京站就下車了),於是把2號座位的南京-武漢段賣給第三位顧客,同時在2號座位的南京和鄭州站打上勾(如下圖右)。

而事實上,計算機來出票完全可以使用同樣的思路,只不過大家可能會想,這樣一個個座位檢查,效率是不是太慢了啊? 這個我們完全可以檢驗一下,假定一個2000座位,經過30個車站的車次,售票流程用上面思路寫成的代碼如下:

int nSeatState[2000][30];
//座位狀態
int LockSeat(int nStart, int nEnd)
{
//依次遍歷所有座位
for (int i=0; i&<2000; i++) { bool bHaveSeat = true; //如果座位經過此旅程的站點沒有被佔用 for (int j=nStart; j&

這個代碼效率如何呢?見下圖:

全隨機出票的情況下,要賣完所有車票需要2秒多,每秒處理的請求是35萬次,這個效率顯然說不上多好,但是也不算差了,不過這個思路還有沒有可優化的空間呢?

假設一下,你做為一個售票員,剛剛賣出一張上海-合肥的車票,你花了半個小時挨個檢查所有座位,終於找到1000號座位滿足這張車票的要求,你把這張車票賣出去以後,精疲力盡的在表格上打了幾個勾,正準備休息一下。突然一個顧客跑過來:給我一張上海到合肥的車票!試想一下,這時你會從頭開始再檢查一遍所有座位嗎?顯然不會,既然剛剛找到1000號位置才找到座位,那同樣的行程現在就應該從1001號座位開始檢查才對嘛。

同樣的道理,假設這時你從1001號座位開始檢查,直到最後也沒有找到合乎要求的座位,顧客失望離去,正當你精疲力盡想休息的時候,又來一位新顧客,大聲嚷著也要買上海-合肥的車票,你會再去檢查表格嗎? 顯然你只會沒好氣地說沒票了!

換到計算機系統上也是如此,

  • 賣出一張車票後,下一張同樣行程的車票應該從這個座位的下一個座位開始檢查。

  • 如果某個行程的車票沒有找到座位,那說明這個行程已經沒有餘票了,後續所有這個行程的車票都可以直接返回無票。

我們用這個思路來修改一下代碼:

int nSeatState[2000][30];
int nSeatSell[30][30]; //不同旅程遍歷座位的起始點

int LockSeat(int nStart, int nEnd)
{
if (nSeatSell[nStart][nEnd] &>= 2000)
{
return -1;
}

//依次遍歷所有座位
for (int i=nSeatSell[nStart][nEnd]; i&<2000; i++) { bool bHaveSeat = true; //如果座位經過此旅程的站點沒有被佔用 for (int j=nStart; j&

現在效率如何?直接看結果吧

售罄一個2000座位30站的車次車票,只需要62毫秒,每秒可以處理的請求達到7000萬,這個速度,怎麼也該夠了吧?

當然,這裡還沒有考慮退票,多人同行座位盡量連號,不過這些也不過在細節上的一些處理,即使對效率有些影響,但應該也完全夠不成瓶頸。

一個出票線程 一個退票線程 一個查詢余票線程


我認為系統設計切勿照搬已有的模式,尤其是這種專業性比較強的系統。

目前遇到的困難來不一定完全來自於系統架構的軟體設計方面,需要完善的部分還有很多。

我具體講一下對用戶引導方面的問題:

1、系統上線正值春運,訂票用戶基本達到峰值;

2、出票時間比其他渠道提前兩天,這導致所有用戶都集中通過網路搶票;

3、出票時間分布不密切,造成「秒殺」現象;

4、系統使用宣傳不到位,造成網路訂票恐慌。

我提的解決方案:

1、目前架構不變,包括軟、硬體,因為這次已經做過峰值壓力測試!扛住了,沒有完全癱瘓;

2、出票時間和其他渠道出票時間同步,用其他渠道分流部分用戶;

3、出票時間應該按照車次、時間、鐵路分局做詳細的劃分,比如上海分局8點開放k561、9點開放t242,緩解秒殺現象;

4、通過各種渠道宣傳鐵路詳細的放票時間以及購票手續。

總的來說,善於引導用戶的系統才是一個完善的系統,鐵路部門應該在這方面加強。

技術上面:訂票系統大量js非同步交互框架,導致伺服器請求數量暴漲,應該優化系統處理流程,將一些不變得東西固化存儲,完善系統環節異常信息的處理工作。提高系統安全性和網路用戶體驗是矛盾的,要處理好這方面的關係。

最後希望大家支持我,同樣也想聽到不同的意見。


1、本輪10萬張火車票已在3秒鐘售罄,下一輪搶購在明天10:00;

2、購買我們的F碼可以獲得下一輪搶購的優先權,搶購依據F碼生成時間順序出票。

3、除了在線銷售火車票,我們還可以提前預定該趟火車的餐飲服務、移動充電寶、旅行三件套等,提前預定比現場購買最高可便宜50%。


從系統的架構設計來講

1、每天在某個時段(如夜晚1點左右)用緩存將所有的車票緩存起來,然後查詢都放到緩存中去查找,如果用一台redis緩存伺服器的話,就12306的查詢量來說,是完全沒有問題的。帶來的統一問題以後再做討論。

2、將查詢的站點(多個)、登錄的站點(一個)、註冊的站點(一個)、訂單接收處理的站點(多個)、付費處理(一個)、後台監控站點(一個)、後台處理站點(一個)分散式部署,使用單點登錄系統統一控制,然後用演算法將處理平均分布到各個站點。(一個或多個代表部署的數量,如果發現有不足的地方,可以在運行時再加,前期架構主要確保能夠動態伸縮)

3、資料庫採用讀寫分離技術。

4、靜態文件優化後交由CDN處理。

5、將選票-&>預訂-&>付款-&>出票這個不合理的邏輯改掉,不要將票源給獨佔住,減少開銷,用李明提出來的流程,比較合適。

以上為個人的經驗之談


自己畫的


舉例說明,用PHP+MySQL實現12306購票和退票以及余票查詢邏輯.

普通商品的庫存之間沒有關聯性,庫存量都是確定的.

火車票跟普通商品不同,同一車次不同路段的車票的庫存可能會相互影響.

所以資料庫中不應存儲某個車次某個路段的余票數量,而應存儲該車次該路段已售的車票數量.

也就是普通商品存的是剩下的,而火車票存的是已售的.

車票表:

(車次, 路段(區間), 已售, 限售, 已售座位, 出發日)

(1024, [1,2], 0)

(1024, [1,2,3], 0)

(1024, [1,2,3,4], 0)

(1024, [2,3], 0)

(1024, [2,3,4], 0)

(1024, [3,4], 0)

主鍵是自增欄位,"車次"+"路段"這2個欄位組成聯合索引,添加唯一約束.

優化並發時,可以考慮根據"出發日"或"車次"進行分表分庫.

路段[1,2,3]的含義是:從車站1上車,經過車站2(停站),到車站3下車,編號有序.

如果要給不同的路段配置不同的出票限額,可以添加一個"限售"欄位.

購票邏輯:

例如用戶購買路段[2,3,4]的車票時,

程序找出包含[2,3,4]的路段如[1,2,3,4]已售的票,

以及[2,3,4]包含的子路段如[2,3]和[3,4]已售的票,

如果三者的票合計小於500(這裡假設列車滿載為500人),

則用戶能夠購票,即[2,3,4]這個路段已售的票數+1.

SQL購票邏輯如下(以MySQL為例):

SET AUTOCOMMIT=0;

START TRANSACTION; 或 BEGIN; --開啟事務

SELECT 已售 FROM 車票表 WHERE 車次=1024

AND 路段 IN ("2,3,4", "2,3", "3,4", "1,2,3,4") FOR UPDATE;

UPDATE 車票表 SET 已售=(已售+1) WHERE 車次=1024 AND 路段="2,3,4";

COMMIT; --提交事務

SET AUTOCOMMIT=1;

上述購票邏輯,關鍵在於找到WHERE條件IN中的路段,步驟如下:

1.找出包含一個路段如[2,3,4]的其他路段:

SELECT 路段 FROM 車票表 WHERE 車次=1024 AND 路段 LIKE "%2,3,4%";

一個車次至多也就包含幾百個路段,所以LIKE模糊查詢幾百條記錄性能完全可以接受.

在本例中得到:

[1,2,3,4]

2.找出一個路段如[2,3,4]里的子路段演算法(以PHP為例):

function section(array $arr, array $tmp) {

$size = count($arr);

if ($size == 1) return;

for ($i = 2; $i &<= $size; $i++) {

// 從位置0開始,取$i個元素,不影響輸入的數組.

$tmp[] = array_slice($arr, 0, $i);

}

array_shift($arr); // 刪除數組開頭元素

section($arr, $tmp); // 遞歸

}

$arr = array(2,3,4);

$tmp = array();

section($arr, $tmp);

var_export($tmp);

在本例中得到:

[2,3]

[2,3,4]

[3,4]

第一輪得到[2,3],[2,3,4],摘掉頭元素2進行下一輪.

第二輪得到[3,4],摘掉頭元素3進行下一輪.

當數組元素數量等於1時,結束遞歸.

優化:跟某個路段相關的路段其實可以事先計算出來保存起來.

極端車次: 深圳北-北京北 K106車次 途徑26站

echo implode(",", range(1, 26));

找出路段[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26]的子路段:

$tmp = array();

section(range(1, 26), $tmp);

foreach ($tmp as $v) {

$v = implode(",", $v);

$v = "[{$v}]";

}

var_export($tmp);

當購買起點站到終點站的車票時,路段IN查詢中有326個條件,因為26個站包含325個子路段.

也就是該車次的所有路段都會被SELECT FOR UPDATE鎖定.

不過這時可以不用路段IN條件進行鎖定,直接用車次欄位進行鎖定即可.

退票邏輯:

退票邏輯比購票邏輯簡單得多,直接給對應車次,對應路段的已售車票-1即可:

UPDATE 車票表 SET 已售=(已售-1) WHERE 車次=1024 AND 路段="2,3,4";

余票查詢邏輯:

例如查詢車次1024上路段[2,3,4]的余票:

SELECT 已售 FROM 車票表 WHERE 車次=1024 AND 路段 IN ("2,3,4", "2,3", "3,4", "1,2,3,4");

這裡假設列車滿載為500人,用500減去上述路段已售車票的和就是路段[2,3,4]上的余票.

可以使用MySQL內存表或Redis緩存余票查詢結果,購票和退票時更新緩存.

座位分配邏輯:

一列火車,其座位都是固定的,存儲每列火車的座位的數據表沒什麼可說的.

至於座位分配,可以在車票表裡增加一個"已售座位"的欄位.

用戶成功購票時,從"待售座位"中取一個分配給用戶並更新"已售座位"欄位.

"待售座位"為"所有座位"去掉相關路段"已售座位"後的座位.

SET AUTOCOMMIT=0;

BEGIN;

SELECT 已售,已售座位 FROM 車票表

WHERE 車次=1024 AND 路段 IN ("2,3,4", "2,3", "3,4", "1,2,3,4") FOR UPDATE;

UPDATE 車票表 SET 已售=(已售+1), 已售座位=CONCAT(已售座位,分配座位)

WHERE 車次=1024 AND 路段="2,3,4";

COMMIT;

SET AUTOCOMMIT=1;

其中CONCAT相比直接賦值,能夠減少傳遞給MySQL的數據量.

車票表中"已售座位"欄位存儲"座位編號".

座位表: 座位編號, 列車, 車廂, 座位

補充(糾錯):

經評論里的網友提醒,糾錯如下:

應該找出跟目標路段[2,3,4]有交集(相交元素&>=2)的路段,比如還包括[1,2,3]和[3,4,5]之類的.

演算法如下:

&= 2) {
$related[] = $v;
}
}
var_export($related);
// 得到
[1,2,3] 之前沒有考慮類似這樣的情況
[1,2,3,4]
[2,3]
[2,3,4]
[3,4]


其實很簡單,

1.按地區分配時間, 現在時間間隔太長, 瞬間壓力太大。比如天津在12:30 石家莊在13:00。(壓力減小10倍)

2.提前預定,在秒殺時刻之前10分鐘就把要買哪趟車給預定好, 等到秒殺時刻點一下滑鼠就行了(早已有成熟的辦法對付按鍵精靈。點一下滑鼠秒殺那個用內存資料庫之類的也早有成熟技術。)之後成功的部分慢慢處理就行了

(用戶不會再頻繁刷新了, 以前刷新10次算少的, 現在只需1次。查詢服務壓力減小10倍)

( 極大的減小秒殺時的數據處理量,以前1000人搶100張車票, 需要在1分鐘時間內處理1000次買票請求,數據同步之類的一大堆。

現在只需要處理在10秒鐘之內在內存資料庫處理1000個秒殺請求而已, 然後把100個秒殺成功的請求在接下來的幾分鐘內後台慢慢處理就行了。後台處理有隊列, 井然有序,不會擁堵

減小100倍-1000倍算少的。)

3.禁止很多個人幫忙為一個身份證買票。(壓力減小三四倍)

4.缺點:如果沒秒到會沒時間再買其他車, 其實這個很公平。畢竟根本上就是一個供求矛盾的平衡工具。

還記得當年全國好n萬人秒殺淘寶紅包么? 3000個紅包1秒鐘內就搶光, 這壓力相對於分散成很多車次,很多席別的火車票而言, 可難度大多了。

如果國內有好的雲伺服器商,並且簽訂好協議的話, 每到節假日多弄些實例就行了。比自建伺服器省很多錢。


最坑爹的是,最近面試阿里巴巴,考官開始問這個問題了!

談到了鏈表等數據結構的問題。

其實我第一句回答是:「不了解鐵道部的內部系統和業務需求,毫無設計架構的前提」。

我覺得樓上很多高人侃侃而談一個自己不了解的東西實在是比較可笑的,不能因為它沒做好就以為自己就能幹得好。

聽說12306是調用鐵道部原來鐵道部科學院設計的訂票系統來售賣的,實情往往不是你想的那樣,多談無益。

2014-1-8 補充:從最近暴露的信息看,12306最近用了內存資料庫方案 Pivotal GemFire ,硬是用純內存堆積解決了問題,哈哈。


其實這是一個不完整的問題,完整的問題是:如何在保證現有售票系統(售票窗口)的情況下,不改動後台的設備,實現網路訂票功能?

這個基本上就是一個死胡同,網上給出的比較靠譜的解決方案都是一個變相限流,並且給終端用戶一個虛假的SLA。當然可以搞個秒殺啥的,弄個虛假的熱鬧。


轉一個答案,最近看到的一篇文

【轉載】12306鐵道部訂票網站性能分析

http://hi.baidu.com/1988wangxiao/item/204d7bce98f20dcb984aa0b5

寫的不錯,推薦給大家。

http://12306.cn 網站掛了,被全國人民罵了,以這個事來粗略地討論一下網站性能的問題。這是一篇長文,只討論性能問題,不討論那些UI,用戶體驗,或是是否把支付和購票下單環節分開的功能性的東西。

業務

任何技術都離不開業務需求,所以,要說明性能問題,首先還是想先說說業務問題。

一,有人可能把這個東西和QQ或是網遊相比。

但我覺得這兩者是不一樣的,網遊和QQ在線或是登錄時訪問的更多的是用戶自己的數據,而訂票系統訪問的是中心的票量數據,這是不一樣的。不要覺得網遊或是QQ能行你就以為這是一樣的。網遊和QQ 的後端負載相對於電子商務的系統還是簡單。

二,有人說春節期間訂火車的這個事好像網站的秒殺活動。

的確很相似,但是如果你的思考不在表面的話,你會發現這也有些不一樣。火車票這個事,還有很多查詢操作,查時間,查座位,查鋪位,一個車次不 行,又查另一個車次,其伴隨著大量的查詢操作,下單的時候需要對資料庫操作。而秒殺,直接殺就好了。另外,關於秒殺,完全可以做成只接受前N個用戶的請求(完全不操作後端的任何數據, 僅僅只是對用戶的下單操作log),這種業務,只要把各個伺服器的時間精確同步了就可以了,無需在當時操作任何資料庫。可以訂單數夠後,停止秒殺,然後批量寫資料庫。火車票這個豈止是秒殺那麼簡單。能不能買到票得當時告訴用戶啊。

三,有人拿這個系統和奧運會的票務系統比較。

我覺得還是不一樣。雖然奧運會的票務系統當年也一上線就廢了。但是奧運會用的是抽獎的方式,也就是說不存在先來先得的搶的方式,而且,是事後抽獎,事前只需要收信息,事前不需要保證數據一致性,沒有鎖,很容易水平擴展。

四,訂票系統應該和電子商務的訂單系統很相似。

都是需要對庫存進行: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通常伴隨著分散式存儲)。(關於負載均衡更為詳細的說明見「後端的負載均衡」)

二、減少前端鏈接數

我看了一下http://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這個系統有非常有意義的質的性能的提高。

四、後端系統負載均衡

前面說了數據分區,數據分區可以在一定程度上減輕負載,但是無法減輕熱銷商品的負載,對於火車票來說,可以認為是大城市的某些主幹線上的車票。這就需要使用數據鏡像來減輕負載。使用數據鏡像,你必然要使用負載均衡,在後端,我們可能很難使用像路由器上的負載均衡器,因為那是均衡流量的,因為流量並不代表伺服器的繁忙程度。因此,我們需要一個任務分配系統,其還能監控各個伺服器的負載情況。

任務分配伺服器有一些難點:

負載情況比較複雜。什麼叫忙?是CPU高?還是磁碟I/O高?還是內存使用高?還是並發高?還是內存換頁率高?你可能需要全部都要考慮。這些信息要發送給那個任務分配器上,由任務分配器挑選一台負載最輕的伺服器來處理。

任務分配伺服器上需要對任務隊列,不能丟任務啊,所以還需要持久化。並且可以以批量的方式把任務分配給計算伺服器。

任務分配伺服器死了怎麼辦?這裡需要一些如Live-Standby或是failover等高可用性的技術。我們還需要注意那些持久化了的任務的隊列如何轉移到別的伺服器上的問題。

我看到有很多系統都用靜態的方式來分配,有的用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)無論你怎麼設計,你的系統一定要能容易地水平擴展。也就是說,你的整個數據流中,所有的環節都要能夠水平擴展。這樣,當你的系統有性能問題時,「加3倍的伺服器」才不會被人譏笑。

1)上述的技術不是一朝一夕能搞定的,沒有長期的積累,基本無望。我們可以看到,無論你用哪種都會引發一些複雜性。

2)集中式的賣票很難搞定,使用上述的技術可以讓訂票系統能有幾佰倍的性能提升。而在各個省市建分站,分開賣票,是能讓現有系統性能有質的提升的最好方法。

3)春運前夕搶票且票量供遠小於求這種業務模式是相當變態的,讓幾千萬甚至上億的人在某個早晨的8點鐘同時登錄同時搶票的這種業務模式是變態中的變態。業務形態的變態決定了無論他們怎麼辦干一定會被罵。

4)為了那麼一兩個星期而搞那麼大的系統,而其它時間都在閑著,有些可惜了,這也就是鐵路才幹得出來這樣的事了。

(本文轉載時請註明作者和出處,請勿於記商業目的)


我說一下資料庫方面的想法。

Web用集群,資料庫分庫。

如果瓶頸在資料庫,那麼根據火車票車站的地域特點完全可以進行資料庫的水平切分,將一個大庫切分成若干小庫(表結構一樣,但存的車票始發站不同)。

比如一個地級市的請求,最終會請求到一個對應的小集群上,可以是四五台機器加一個資料庫,這個資料庫存儲該地級市火車站開出的車票信息,用來售賣和查詢所有這個地級市開出地車輛信息,每天放票時更新這些資料庫里票的庫存。

全國算300個地級市,這樣下來如果10秒內1000萬的流量,就被分流到每個集群上3萬,每秒3000,每台機器400~500,是有可能處理下的,集群不行了可以加機器,資料庫數據量通過分庫減少幾百倍,連接數也水平擴展了,應該能撐住的。

當然用戶信息是要統一驗證的,必須有一個巨大的共享緩存系統存儲用戶的賬戶信息。

如果分庫可行,那麼就可以化大為小,優化各方面都比較好做。


在另一個問題里回答了,再貼一次:

據說是現有系統的瓶頸。

那這樣做,在12306上開1w個窗口,相當於1w個槽,用戶登錄後可以選擇一個窗口進行排隊,只要用戶不退 出,並且一直有動作, 比如點某個按鈕之類的,就不把用戶踢出來,論到用戶購票時給用戶個通知,要求30秒或1分鐘內必須下好單,不然就踢出去重新排隊,下單後45分鐘內支付, 當前用戶下完單後,就可以讓這個窗口中的下一個用戶就能購票了,依此類推。

這樣用戶只用在家排隊購票,避免了去挨凍排隊了,也不會出現秒殺的情況了,現有系統也能支撐的住了。

關鍵一點,剩餘票量在用戶查詢時是要實時的。

同時,還可以開發一些附屬的應用,遊戲之類,佔用住用戶排隊的時間,還可以額外創收。。。 」


個人認為更有價值是體現在數據分析上,如得到寬頻數據、用戶流量、區域分布、請求特點、應用瓶頸點、伺服器的性能指標等等,這些數據對優化、改良現有架構非常有幫助。拋開寬頻因素,以下是對12306平台系統架構的幾點建議:

一、前端優化

具體參考:yahoo前端優化34條規則,針對12306平台,個人建議在沒有多運營商鏈路接入(如BGP)的情況下繼續使用CDN進行加速。動、靜態應用分離,靜態業務使用非http://12306.cn域名可以減少無用cookie帶來的流量。任何一個小細節在高並發下都會被無限放大(截止目前發現平台還是以http://dynamic.12306.cn域名做靜態引用)。查詢頁面的結果是通過Ajax非同步返回填充iframe框架來實現,這對動態CDN加速是一個挑戰,因為CDN節點並沒有真正緩存頁面中主要加速的內容。另外提高驗證碼的複雜度及多樣性,可以緩解刷票機給平台帶來的壓力。

二、運用緩存

緩存最大的好處是減少後端數據存儲的I/O壓力,從一個普通用戶訂票軌跡來看,查詢讀往往是入庫寫的好幾倍,如何減少資料庫的讀I/O對提高平台的整體性能至關重要,比較流行的緩存技術有針對頁面及數據級,頁面級緩存有varnish、squid等,如使用CDN,頁面級的緩存可以不用考慮,重點將精力放在數據級的緩存規划上,技術方面可以用Nosql來實現,比較成熟的Nosql有memcached、redis、mongodb等。可以根據班次、出發與目的地ID組合或出發日期進行hash分區,這樣可以很好地提高緩存命中率,減少後端資料庫的壓力。

三、代理層

引入代理層的目的是拆分業務,目前平台絕大部分功能都共用一組WEB伺服器(從域名及URI結構猜測,不一定準確)來對外提供服務,比如登錄、註冊、車票查詢、余票查詢、列車時刻表查詢、正晚點查詢、訂單管理等等,只要其中一個功能模塊出現堵塞,影響必定是全局性的。一個好的方法是優化、規範各業務URI,在代理層實現業務的劃分,可用的技術有Haproxy、Nginx等,如將/otsweb/regitNote/映射到註冊組WEB伺服器,/otsweb/AppQuery/映射到查詢組WEB伺服器,/otsweb/Pay/映射到支付組WEB伺服器等等,如此一來,當查詢業務出現延時堵塞時不會影響到用戶支付。

四、資料庫層

之前接觸過一些政府行業的業務,資料庫伺服器往往都使用一台高端的硬體,比如小型機,在互聯網行業,尤其是類似於12306訂票系統,這往往是最致命的,理由很簡單,在大流量並發下處理能力再強的伺服器也吐不出數據,因為受網路I/O、磁碟I/O、業務邏輯等方面的限制,所以必須將數據打散,方案有進行讀寫分離、分區、分片。主從模式可以很好實現讀寫分離,大部分資料庫都支持這點,除此之外還建議使用分區模式,分區策略可以根據業務特點進行,按地域進行分區是一個好主意,因為每個區域都是一個大分區,還可以從業務層面對它做二級甚至三級的"擴展分區"。需要在細化拆分與運營成本上做好平衡。另外I/O密集的點盡量使用SSD代替。

五、負載均衡層

保障一個業務平台的高可用性,採用負載均衡策略必不可少,即使是提供給CDN的源伺服器。目前有商用的F5、NetScaler、Radware等,也有開源的LVS,看成本的投入來選擇,此處不詳細展開討論。

六、業務層

此次12306網站癱瘓事件,業務層面有無優化的空間?12306網站平台是鐵道集團在互聯網上對外服務的窗口,與電話訂票、代售點都是平級的,後端肯定還關聯著很多複雜的業務系統,在沒有對整個集團業務系統做擴容的前提下(短期內估計不能實現),可以將網站業務平台剝離出來,當然,完全剝離也不太實際,至少可以延長同步、一致性校驗的時間。時間的長短隨班次的發車時間成正比,因為大部分的用戶都是提前一周以上就著手預定車票。

如大家有什麼疑問或感興趣的話題可以通過weibo與我交流:http://t.qq.com/yorkoliu


1.首先要分散請求,這個做法是必然的合理的選擇。分散式架設有5億人要購票,分布到幾十上百個城市中去的時候就分解成了千萬,或者更小的百萬級的簡單問題了。

2.必須將選票-&>預訂-&>付款-&>出票這個不合理的邏輯改掉,在這個流程中 預訂到出票必須是保證事務的原子性和隔離度的,但是這是一個長事務,且其中付款這個流程是人為的,不可控的。如果在預訂的時候鎖定一張票,再設置一個30分鐘超時解鎖,這樣對於非在線購票的人是很不公平的,因為一張票在網上鎖定了,售票點就不能出這張票,如果出現大量支付失敗,那麼對於整個購票系統的穩定性和處理吞吐量都有很大的影響。

合理的流程應該是 查詢票價-&>在線充值-&>查票-&>定票扣費,這樣字那個不可控的超長事務就合併成了一個《定票扣費》。這樣子不需要很複雜的結構,按照常規的設計方式也能支撐起超大量的業務

不過最好的辦法還是把這個業務外包給淘寶吧,反正都快國有化了


如果是我做,我會做成B2B系統、做標準,不做直銷,①讓分銷商競爭來提升整體的服務水平②解決黃牛問題


火車票難買的根本在於車少人多,不管你再怎麼優化12306網站的性能,一到放票的時間點,那麼多人集中下單,肯定要卡。

我覺得與其讓那麼多人在某個時間點集中登錄搶購,還不如搞成搖號的方式,提前24-72小時可以下單,到了放票的時候開獎。用戶實名+保證金+預付票款+中標後不準退票。


這種事,簡單,外包給馬雲就行了,淘寶雙十一的能力已經看出來了,況且,馬總可是說過的,只要國家需要,支付寶就奉上,很好的雙贏嘛


核心觀點:

這段時間看了些寫鐵路售票網的設計思路,有些人(架構師)提出的方案我卻很不敢苟同,太慣性了,總想著從技術去解決壓力問題,就像之前我比喻的的總想張大嘴咬硬骨頭。春運這種特殊場景搶票不可避免,開票後把用戶理解成DDOS攻擊者一點都不過分,要保證系統性能必須通過業務手段來規避!

在春運這種特殊時期,我更傾向於採取分時分線路的策略進行售賣,或是網上提的排隊隊列,都可以有效緩解瞬間峰值。

挑戰:

網上數據:每日10億PV,200萬成交訂單,500萬人同時在線,具體細節的業務數據不太清楚。

  1. 春運期間,同時在線、查詢、訂票的人數峰值高。
  2. 多種網點銷售,電話、網點、互聯網,都反映到同一個票源上,不能出現重複賣,付了錢卻沒票這樣的事。
  3. 因為票源太緊張,只要一放票,大家想盡辦法最快速度上來搶票,用什麼手段的都有,造成DDOS。

方案:

普通頁面:

這個說得人很多了,提一下。

  1. 靜態內容,CDN需要足夠給力。
  2. 動態內容,查詢、下單、支付、取票這幾個環節需要足夠給力,特別是後面三個環節涉及到嚴格的事務。

查票:

中心票池多半採用Oracle或者Sybase這樣的關係資料庫保管,位於老系統內。因此首要解決新舊系統之間數據對接的問題。很明顯,像查票這種需求,查明顯多於寫,假設500:1,可以在新系統內架設cache層,採用memcached、redis或者BDB,讓查詢請求經由cache層吸收掉,只透過寫請求,寫完同步到cache層。

cache層需要考慮按線路、日期將數據進行切分,分攤壓力。

下單:

從下單開始,所有的處理都是實打實的交易(事務),必須系統一個一個去處理。 下單先判斷是否有票,向中心票池「預定」一張車票,獲取到一個流水號。然後寫入新系統訂單資料庫內,完成下單。

網支:

由網銀處理。

出票:

事務:調單驗證更新狀態,發放取票憑證。

交易環節,重點考慮春運期間的突發峰值,假設每天出售200萬張。

1、業務上分時分線路售賣[必須的],緩解突發流量,每日的200萬應儘可能的分攤到12小時內。

2、支持橫向擴展,分級隊列,切割數據,方法蠻多的,不多說。


推薦閱讀:

為什麼現在還有網站在使用明文保存密碼?
網站突發大流量怎麼做預警?
鐵路客戶服務中心網站 12306.cn 有哪些地方要改進?
為什麼很多網站的內容儲存用別的域名?有什麼好處?

TAG:網站架構 | 12306中國鐵路客戶服務中心 |