專欄 | 九章演算法
網址 | www.jiuzhang.com
學員面經題
一個類似ticket master的網站。說某個時間段開放某明星演唱會訂票,大概會同時有500K QPS 的訪問量,一共只有50K張票。訂票的過程是使用者開啟訂票網頁(不用考慮認證等問題),填一個text box說要訂幾張票,然後click一個button就開啟一個page,那個page會不停的spin直到系統能夠預留那幾張票,如果預留成功,使用者會有幾分鐘時間填寫使用者資訊已經完成支付,如果到期未支付,這些票就自動被系統收回了。每張票都是一樣的,沒有位置資訊什麼的。
求教怎麼design這個系統,我一開始看到500K QPS就有點慌亂了。
九章講師解答 SOLUTIONS
訂票網站一直都是世界難題。12306應該比這個還恐怖。
不要被 500k QPS嚇到。500k也好,5k也好,500也好,分析的方法和思路都是一樣的。
這個題的關鍵首先是給出一個可行解(無論任何系統設計題的關鍵都是如此),一個核心要實現的功能是,票的預留與回收。在設計可行解的時候,可以先將500k qps拋之腦後。假設現在只有10個使用者來買票。優化的事情,放到Evolve的那一步。
按照我們的SNAKE分析法來:
Scenario 設計些啥:
- 使用者提交訂票請求
- 客戶端等待訂票
- 預留票,使用者完成支付
- 票過期,回收票
- 限制一場演唱會的票數。
Needs 設計得多牛?
- 500k QPS,面試官已經給出
- 響應時間——是在使用者點選的一瞬間就要完成預定麼?不是,可以讓使用者等個幾分鐘。也就是說,500K的請求,可以在若干分鐘內完成就好了。因此所謂的 500k QPS,並不是Average QPS,只是說峰值是 500k QPS。而你要做的事情並不是在1秒之內完成500k的預定,而是把確認你收到了購票申請就好了。
Application 應用與服務
- ReservationService —— 使用者提交一個預定請求,查詢自己的預定狀態
- TicketService —— 系統幫一個預定完成預定,生成具體的票
Kilobyte 資料如何儲存與訪問
1.ReservationService ——使用者提交了一個訂票申請之後,把一條預定的資料寫到資料庫裡。所以需要一個Reservation的table。大概包含的columns有:
id(primary_key)
created_at(timestamp)
concert_id(foreign key)
user_id(foreign key)
tickets_count(int)
status(int)
簡單的說就是誰在什麼時刻預定了哪個演唱會,預定了幾張,當前預定狀態是什麼(等待,成功,失敗)。
2. TicketService —— 系統從資料庫中按照順序選出預定,完成預定,預定成功的,生成對應的Ticket。表結構如下:
id (primary key)
created_at (timestamp)
user_id (fk)
concert_id (fk)
reservation_id (fk)
status (int) // 是否退票之類的
另外,我們當然還需要一個Concert的table,主要記錄總共有多少票:
id (primary key)
title (string)
description (text)
start_at (timestamp)
tickets_amount (int)
remain_tickets_amount (int)
...
總結一下具體的一個Work Solution 的流程如下:
- 使用者提交一個預定,ReservationService 收到預定,存在資料庫裡,status=pending
- 使用者提交預定之後,跳轉到一個等待訂票結果的介面,該介面每隔5-10秒鐘像伺服器傳送一個請求查詢當前的預定狀態
- TicketService是一個單獨執行的程式,你可以認為是一個死迴圈,不斷檢查資料庫裡是否有pending狀態的票,取出一批票,比如1k張,然後順利處理,建立對應的Tickets,修改對應的Reservation的status。
Evolve
分析一下上述的每個操作在500k qps的情況下會發生什麼,以及該如何解決。
1. 使用者提交一個預定,ReservationService 收到預定,存在資料庫裡,status=pending
也就是說,在一秒鐘之內,我們要同時處理500k的預定請求,首先web server一臺肯定搞不定,需要增加到大概500臺,每臺web server一秒鐘同時處理1k的請求還是可以的。資料庫如果只有一臺的話,也很難承受這樣大的請求。並且SQL和NoSQL這種資料庫處理這個問題也會非常吃力。可以選用Redis這種既是記憶體級訪問速度,又可以做持久化的key-value資料庫。並且Redis自帶一個佇列的功能,非常適合我們訂票的這個模型。Redis的存取效率大概是每秒鐘幾十k,那麼也就是我們要大概20臺Redis應該就可以了。我們可以按照 user_id 作為 shard key,分配到各個redis上。
2. 使用者提交預定之後,跳轉到一個等待訂票結果的介面,該介面每隔5-10秒鐘像伺服器傳送一個請求查詢當前的預定狀態
使用了redis的佇列之後,如何查詢一個預定資訊是否在佇列裡呢?方法是reservation的基本資訊除了放到佇列裡,還需要同時繼續存一份在redis裡。佇列裡可以只放reservation_id。此時reservation_id可以用user_id+concert_id+timestamp來表示。
3. TicketService是一個單獨執行的程式,你可以認為是一個死迴圈,不斷檢查資料庫裡是否有pending狀態的票,取出一批票,比如1k張,然後順利處理,建立對應的Tickets,修改對應的Reservation的status。
為每個Redis的資料庫後面新增一個TicketService的程式(在某臺機器上跑著),每個TicketService負責一個Redis資料庫。該程式每次從Redis的佇列中讀出最多1k的資料,然後計算一下有需要多少張票,比如2k,然後訪問Concert的資料庫。問Concert要2k的票,如果還剩有那麼多,那麼就remain_tickets_amount - 2k,如果不夠的話,就返回還有多少張票,並把remain_tickets_acount 清零。這個過程要對資料庫進行加鎖,可以用資料庫自己帶的鎖,也可以用zookeeper之類的分散式鎖。因為現在是1k為一組進行處理,所以這個過程不會很慢,存Concert的資料庫也不需要很多,一臺就夠了。因為就算是500k的話,分成500組,也就是500個queries峰值,資料庫處理起來綽綽有餘額。
假如得到了2k張票的額度之後,就順序處理這1k個reservation,然後對每個reservation生成對應的tickets,並在redis中標記reservation的狀態,這裡的話,tickets的table大概就會產生2k條的insert,所以tickets的資料庫需要大概能夠承受 20 x 1k = 20k 的併發寫。這個的話,大概 20 臺 SQL資料庫也就搞定了。
從頭理一下
開放訂票,500k的請求從世界各地湧來
通過 Load Balancer 紛發給500臺 Web Server 。每臺Web Server大概一秒鐘處理1k的請求
Web Server 將1k的請求,按照 user_id 進行 shard,丟給對應的 redis 伺服器裡的佇列,並把 Reservation 資訊也丟給 Redis儲存。
此時,20臺 Redis,每臺 Redis 約收到 25k 的 排隊訂票記錄
每臺 Redis 背後對應一個 TicketService 的程式,不斷的檢視 Redis 裡的佇列是否有訂票記錄,如果有的話,一次拿出1k個訂票記錄進行處理,問Concert 要額度,然後把1k的reservation對應的建立出2k左右的tickets出來(假如一個reservation有2張票平均)。假如這個部分的處理能力是1k/s的話,那麼這個過程完成需要25秒。也就是說,對於使用者來說,最慢大概25秒之後,就知道自己有沒有訂上票了,平均等待時間應該低於10秒,因為當concert的票賣完了的時候,就無需生成1-2k條新的tickets,那麼這個時候速度會快很多。
儲存tickets的資料庫需要多臺,因為需要處理的請求大概是20k的qps,所以大概20臺左右的Ticket資料庫。
超時的票回收
增加一個RecycleService。這個RecycleService 不斷訪問 Tickets 的資料庫,看看有沒有超時的票,如果超時了,那麼就回收,並且去Concert的資料庫裡把remain_tickets_acount 增加。
總結如何攻破 500k QPS的核心點
核心點就是,500k QPS 我只要做到收,不需要做到處理,那麼500臺web伺服器+20臺Redis就可以了。
處理的的時候,分成1k一組進行處理,讓使用者多等個幾秒鐘,問題不大。使用者等10秒鐘的話,我們需要的伺服器數目就降低10-20倍,這是個tradeoff,需要好好權衡的。
一些可能的疑惑和可以繼續進化的地方
問:500臺Web伺服器很多,而且除了訂票的那幾秒種,大部分的時候都是閒置浪費的,怎麼辦?
答:用AWS的彈性計算服務,為每場演唱會的火爆指數進行評估,然後預先開好機器,用完之後就可以銷燬掉。
問:為什麼不直接用Redis也來儲存所有的資料資訊?
答:因為是針對通同一個Concert的預定,大家需要訪問同一條資料(remain_tickets_acount),shard是不管用的,Redis也承受不住500k QPS 對同一條資料進行讀寫,並且還要加鎖之類的保證一致性。所以這個對 remain_tickets_acount 的值進行修改,建立對應的 tickets 的過程,是不能在使用者請求的時候,實時完成的,需要延遲進行。
問:redis又用來做佇列,又用來做Reservation 表的儲存,是否有點亂?
答:是的,所以一個更好的辦法是,只把redis當做佇列來用 和 Reservation 資訊的Cache來用。當一個Reservation 被處理的時候,再到SQL資料庫裡生成對應的持久化記錄。這樣的好處是,Redis 這種結構其實不是很擅長做持久化資料的儲存,我們一般都還是拿來當佇列和cache用得比較多。
歡迎關注我的微信公眾號:九章演算法(ninechapter)。
精英程式設計師交流社群,定期釋出面試題、面試技巧、求職資訊等