小米搶購限流峰值系統「大秒」架構解密
馬利超
小米科技的系統研發與大數據工程師,2013年畢業于大連理工大學,畢業后有幸加入小米搶購系統團隊,并參與了小米搶購系統開發、重構與調優。其人熱愛技術,對分布式系統架構、高并發峰值系統、大數據領域、反作弊領域、搜索/廣告/推薦系統有濃厚的興趣。
整合的搶購限流峰值系統——「大秒」
2014年初,公司決定舉辦一場“米粉節”活動,全天6輪活動,多個國家、多款爆品同時參與搶購。業務場景將變得更加復雜,當天的并發壓力也會有 一個量級的提升,原有的搶購系統已經不能適應如此復雜的業務場景了。為此,小米網技術團隊基于對 golang 應對高并發、大規模分布式系統能力的肯定,完全基于 golang,重新設計了搶購系統,也就是我們目前使用的搶購限流峰值系統——“大秒”。
在整個系統設計的之初,我們充分考慮了
- 靈活性及可運營性;
- 可運維性及可伸縮性;
- 限流與搶購放號的精準性;
從大秒第一天誕生到演化至今有很多次重構與優化,但一直沿用了設計之初的結構,接下來我們一起了解下小米網搶購限流峰值系統當前的架構以及填過的一些坑。
大秒系統的架構設計
大秒系統主要由如下幾個模塊構成
- 限流集群 HTTP 服務
- 放號策略集群 Middle 服務
- 監控數據中心 Dcacenter
- 監控管理體系 Master
- 準實時防刷模塊 antiblack
- 基礎存儲與日志隊列服務: Redis 集群、Kafka 集群等
整個大秒體系中大秒前端模塊 (HTTP/middle/antiblack) 和監控數據中心使用 golang 開發,大秒監控管理體系使用 Python + golang 開發。
大秒的前端架構設計
大秒前端的架構設計從三個系統展開
- 限流集群 HTTP 服務
- 策略集群 Middle 服務
- 準實時反作弊 antiblack 服務
1、限流集群 HTTP 服務
搶購高峰時,通常會有幾百萬的用戶同時請求,瞬時流量非常大,HTTP 集群頂在最前線,接受用戶的請求,將合法的請求發送的處理隊列,處理隊列設置一定的長度限制,通常情況下,搶購用戶數與銷售商品的比例在100:1,甚至 更高,為了避免系統不被沖垮,保障絕大多數用戶的體驗,我們認為流量是部分可丟失的,當處理隊列滿時,丟棄入隊請求;
雖然設計上過載流量是部分可丟棄的,但是策略層處理能力是非常 power 的,即便是需要丟棄流量,也是按流量的惡意程度,逐級丟棄的,正常用戶購買請求不受影響。
我們使用基于規則的識別、離線畫像信息、機器學習邏輯回歸等方法,識別惡意用戶,在系統高負載的情況下,這部分請求可以優先阻擊其發送到策略層,優先處理正常用戶的請求,保障用戶體驗過。
HTTP集群中不同節點之間的所持用的狀態數據是一致的,處理邏輯也是一致的,所以整個集群中的任何一個節點掛掉,在前端負載均衡能力下,服務的準確性與一致性不受任何影響。
2、策略集群 Middle 服務
HTTP 模塊將滿足條件用戶的請求按照 uid 哈希的規則,轉發到 Middle 集群中相應的節點,Middle 集群根據商品放號策略判斷 (uid:sku:time) 組合是否可以分配購買資格,并返回給相應的 HTTP 服務;
使用 Middle 服務本地內存維護用戶的購買記錄信息,支持各種購買規則,比如:單次活動不限購買數量,單次活動僅限購買一款商品,單次活動每款商品僅限購買一次。
我們將 Middle 的放號邏輯抽象成一個有限狀態機,由商品的放號策略配置閾值來觸發放號狀態轉換,整個配置由 Master 節點統一管理與調度。
為了提升整個系統的處理能力,我們將用戶狀態數據局部化,單用戶(uid)的所有相關信息全部路由到一臺 Middle 節點上處理。
但是有一點風險是,Middle 集群中服務可能會出現活動過程中掛掉的風險,在搶購場景下,商品基本上是瞬時賣完,為了保障系統的處理能力,我們主要從代碼層面做優化,review 代碼邏輯,保證服務應對異常的處理能力。
雖然理論上存在風險,但是在實際工程中,經歷過幾百次活動,還沒出現 Middle 節點掛掉的情況。
3、準實時防刷 antiblack 服務
基于日志流的防刷架構,在每臺 HTTP 節點上部署日志收集 Agent,使用高吞吐量的 Kafka 做日志轉儲隊列,antiblack 模塊實時分析用戶請求日志,基于 IP 粒度、Uid 粒度等做防刷。
雖然此處將 antiblack 模塊定義為準實時防刷模塊,但是作弊信息識別的延遲時長在 1 分鐘之內,其中主要的時延發生在日志的轉儲過程中。
大秒的監控管理體系
1、監控數據中心 dcacenter
監控數據中心數據種類
(1) 業務級數據:過大秒的商品配置數據與實時狀態數據,當前活動的配置與狀態數據等;(2) 系統級數據: 大秒前端服務集群通信地址配置,限流隊列初始長度配置,系統服務資源占用情況,包括:CPU、MEM、連接數等;
數據采集方式
同時使用push和pull模式采集業務級監控數據和系統級監控數據,業務級數據越實時越好,做到1秒采集處理,3秒可視化;
對于 HTTP 節點和 Middle 節點采用pull的模式拉去系統監控數據和業務監控數據,優點如下
(1) 靈活性高由數據中心控制監控數據采集的粒度,在數據中心處理能力既定的情況下,可以根據前端集群的伸縮規模,靈活的調整數據采集的粒度,比如米粉節時,大 秒前端集群擴容至過百臺,管理的過大秒商品的數量在400個左右,業務級監控數據量很大,此時監控數據采集時間間隔很容易降配至 2s。
對于除Http服務和Middle服務之外的服務集群,如:redis,管理平臺各個模塊等可以使用監控數據采集agent,將采集到的數據周期 性的push到redis隊列,dcacenter采集協程實時的從redis隊列中拉去消息,對于基礎服務以及python實現的服務,增加了監控數據 采集靈活性。
(2) 增強服務的可靠性與伸縮性
大秒在設計之初采用push的方式,在每臺前端機器上部署一個數據采集agent,agent和大秒前端服務同時alive,才代表搶購系統健康 運行。這樣即增加了系統的不穩定因素,由不利于系統的伸縮,將監控數據采集邏輯內置到前端golang程序中,提供tcp管理端口,在數據中心使用 pull方式采集數據,很好的解決了這個問題。減少了服務的數量,增強了整個系統的可靠性與伸縮性。
數據ETL與數據緩存
dcacenter同時負責將采集到的業務級數據及系統級監控數據,實時清洗,提取,轉換,結構化,并將結構化的數據存儲在自身內存中,定制通信協議(golang實現類redis通信協議),作為一個數據中心,對整個管理體系Master及其他系統提供實時數據支持。
將dcacenter直接作為數據中心,主要是出于數據的實時性考慮,省去中間轉儲環節,上層可視化系統、自動化活動控制系統、規則引擎系統等可以第一時間獲得前端實時的銷售狀態數據及服務的狀態數據。
2、監控管理中心 Master
監控管理中心的主要模塊如下。
a.倉儲庫存同步服務StockKeeper同步商品的倉儲系統中的實時庫存到秒殺系統,大秒系統擁有雙庫存保障,一個是實時倉儲庫存,一個是虛擬庫存也就是資格號,在搶購場景下只有當兩個庫存都有貨時,才能正常銷售。
b.商品策略控制器PolicyKeeper基于相應的策略觸發器(時間區間與庫存區間),當策略觸發時,比如12點整,搶購開始,為相應的商品配置策略,并向大秒前端廣播商品配置變更命令,在通信基礎模塊的保障下,整個過程秒級內完成。
c.活動自動化控制ActKeeper基于監控數據中心獲取大秒前端的實時銷售數據,自動化的控制活動中的各個狀態,活動開始前逐層打開開關,活動開始時打開最后開關,活動過程中維護活動的售罄狀態,活動結束后初始化,整個搶購活動的過程無需人工介入;
d.數據可視化從監控數據中心提取實時的結構化系統級監控數據和業務級監控數據,將活動過程中的詳細數據實時可視化到管理頁面上,讓運營與銷售以及大秒管理員能夠及時了解當前活動狀態,并人工干預活動;
e.監控規則引擎監控規則引擎建立在監控數據中心之上,根據結構化監控數據判斷當前整個搶購系統的狀態,及時報警,以及半自動化控制。
f.其他大秒管理端管理大秒前端所有的數據、配置以及狀態,Master體系提供了詳細的管理工具與自動化服務。如果清理大秒前端Middle服務中的用戶購買信息等。
3、大秒配置管理數據流
整個搶購系統由 Master 體系中各個服務做統一的控制的,Master 控制商品狀態及配置數據的變更,控制當前活動的狀態,控制商品放號的策略等。
為了保證時效性,商品、活動、系統等配置狀態的變更都需要將變更命令廣播前端集群,這期間發生了大量的分布式系統間通信,為了保障命令及時下行, 我們提取出了命令轉發服務:MdwRouter,用于廣播控制命令到大秒前端集群。該服務模塊維護了到大秒前端長連接,接收 Master 下發的控制命令,并瞬時廣播,保障了整個控制流的處理能力。
舉個例子,2015 年米粉節,我們單機房大秒集群的規模在過百臺級別,假設為 100 臺,管理的獨立的商品id的數量在 400 個左右,在這種量級的活動下,商品的放行策略是批量管理的,比如我們根據后端交易系統的壓力反饋,調整所有商品的放行速度,這時候需要廣播的命令條數在: 100*400=40000 級別,Mdwrouter 很好的保障了系統命令下行的速度,秒級完成命令下行。
小米搶購技術架構
1、小米搶購服務閉環設計
小米網搶購系統服務見上圖
- bigtap體系中大秒前端服務負責搶購時限流放號,并控制放號策略以及維護用戶在本地緩存中的購買記錄。
- cart服務驗證token的有效性,并向counter服務發起銷量驗證請求;
- counter服務是整個搶購系統最終的計數器, 海量的請求在bigtap服務的作用下已經被限制在可以承受的壓力范圍內,并且復雜的放號策略已經在大秒Middle服務中實現,counter只負責最 終的計數即可。counter服務采用redis記錄相應商品的放號情況,根據預設的銷量,判斷當前請求加購物車商品是否有庫存余量,并維護商品銷量;
- bigtap體系中的dcacenter服務實時采集商品銷量,Master中活動自動化控制服務依據商品銷量判斷當前商品是否售罄,售罄則通過設置商品的售罄狀態,并通知大秒前端;
2、2015年米粉節介紹
從上述整個服務閉環設計可以看出,大秒的功能完全可以抽象成限流系統,只有在處理搶購活動時,數據的管理與一致性要求才使整個系統變得復雜。
2015年米粉節,我們完全使用大秒的限流功能,不限用戶的購買數量,很便捷的將系統部署在兩個機房,一個物理機房,一個公有云集群,兩者同時服 務,大秒系統作為整個商城的最前端,能夠根據后端服務的壓力狀態,瞬時調整整個集群放行流量大小,非常好的保障了整個米粉節的正常舉行。在上述文章中,已 經介紹了一些服務設計的出發點,每一次優化的背后,都至少有一次慘痛的經歷。
大秒系統架構的幾點經驗總結
1、Golang GC 優化方法
我們從 golang 1.2 版本開始在線上搶購系統中大規模使用,最初上線的 TC 限流集群在搶購的過程中通過過載重啟的方式瘸腿前行。
在當前的大秒系統中,對于限流集群主要是 goroutine 資源、HTTP 協議數據結構、TCP 連接讀寫緩沖區等頻繁動態開銷,造成內存 GC 壓力大,在現有 GC 能力下,我們對 GC 優化從以下幾個方面考慮
- 減少垃圾產生:降低數據結構或者緩沖區的開銷;
- 手動管理內存:使用內存池,手動管理內存;
- 臟數據盡快釋放,增大空閑內存比。
我們使用了以下 3 種 golang GC 優化方法
1)定制 golang HTTP 包
調整 HTTP 協議 conn 數據結構默認分配讀寫緩沖區的大小,以及手動維護讀寫緩存池,減少動態開辟內存的次數,降低 GC 壓力。
在 Go 語言原生的 HTTP 包中會為每個請求默認分配 8KB 的緩沖區,讀、寫緩沖區各 4K。而在我們的服務場景中只有 GET 請求,服務需要的信息都包含在 HTTP essay-header 中,并沒有 body,實際上不需要如此大的內存進行存儲,所以我們調小了讀寫緩沖區,將讀緩沖區調小到 1K,寫緩沖區調小到 32B,golang 的 bufio 在寫緩沖區較小時,會直接寫出。從 golang 1.3 開始,HTTP 原生的包中已經使用了sync.Pool 維護讀寫緩存池,但是 sync.Pool 中的數據會被自動的回收,同樣會小量的增加 GC 壓力,我們此處自己維護緩存池來減少垃圾回收。
2)加快資源釋放
原生的 HTTP 包默認使用 keep-alive 的方式,小米搶購場景下,惡意流量占用了大量的連接,我們通過主動設置 response essay-header 的 connection 為 close 來主動關閉惡意連接,加快 goroutine 資源的釋放。
3)升級版本
跟進使用 golang 最新的版本,golang 后續的每個版本都有針對 GC 能力的調整。
得益于開源技術力量,以及大秒系統在 GC 優化上的努力,以及系統層的調優,我們的 HTTP 限流層已經可以余量前行。
從上圖可以看出,得益于 GC 的優化,2015 年米粉節,每輪搶購,HTTP 服務的內存不會有特別大的抖動。
2、HTTP 服務器內存調優之操作系統參數調整
我們的服務場景下絕大多數的請求數都是惡意請求,惡意請求通常都是短連接請求,大量的短連接會處于 timewait 狀態,幾分鐘之后才會釋放,這樣會占用大量的資源,通過調整內核參數,盡快釋放或者重用 timewait 狀態的連接,減少資源的開銷。具體參數調整如下:
net.ipv4.tcp_tw_recycle = 1 (打開TIME-WAIT sockets快速回收) net.ipv4.tcp_tw_reuse = 1 (允許TIME-WAIT sockets復用) net.ipv4.tcp_max_tw_buckets=10000 (降低系統連接數和資源占用,默認為18w)
高并發場景下,操作系統層網絡模塊參數的調整,會起到事半功倍的效果。
3、沒有通信就談不上分布式系統
整個大秒系統模塊之間面臨的通信要求是非常苛刻的,Master 節點與 HTTP、Middle 節點要頻繁的廣播控制命令,dcacenter要實時的收集 HTTP、Middle 節點的監控管理數據,HTTP 要將用戶的購買請求路由到 Middle 節點之間,Middle 節點要返回給相應的 HTTP 節點放號信息;
我們基于 TCP 定制了簡單、高效的通信協議,對于 HTTP 層和 Middle 層通信,通信模塊能夠合并用戶請求,減少通信開銷,保障整個大秒系統的高效通信,增加服務的處理能力。
4、服務閉環設計
從上述搶購的服務閉環架構中可以看出,整個搶購流程處理bigtap系統之外,還有 cart 服務,中心 counter 服務,這三者與 bigtap 系統構成了一個數據流的閉環,但是在大秒最初的設計中,是沒有 counter 服務的,Middle層策略集群在放號的同時,又作為計數服務存在,但是整個搶購流程卻是以商品加入購物車代表最終的搶購成功,這在設計上有一個漏洞,假 如 bigtap 計數了,但是token 并沒有請求加購物車成功,這是不合理的。為了保證整個系統的準確性,我們增加了計數器服務,計數操作發生在加購物車下游,bigtap 在從計數中心取出商品實時銷量,由此,構成一個服務閉環設計。在提升了系統的準確性,同時也保證了用戶體驗。
5、技術的選擇要可控
我們一開始選擇使用 ZooKeeper 存放商品的配置信息,在搶購活動的過程伴隨著大量的配置變更操作,ZooKeeper 的 watch 機制不適合用于頻繁寫的場景,造成消息丟失,大秒前端集群狀態與配置不一致。
后來,我們將所有的配置信息存放在 Redis 中,基于通信模塊,在發生配置變更時,伴隨著一次配置項變更的廣播通知,大秒前端根據相應的通知命令,拉取 Redis 中相應的配置信息,變更內存中配置及狀態。
大秒的幾點設計原則
- 分治是解決復雜問題的通則;我們從第一代搶購系統演進到當前的大秒系統,衍生出了很多服務,每個服務的產生都是為了專門解決一個問題,分離整個復雜系統,針對每個服務需要解決的問題,各個擊破,重點優化。由此,才保障了秒殺體系整體性能、可靠性的提升;
- 服務化設計;系統解耦,增強系統的伸縮性與可靠性;
- 無狀態設計,增強系統的伸縮性,提升集群整體處理能力;
- 狀態數據局部化,相對于數據中心化,提升集群整體處理能力。
- 中心化監控管理,熱備部署,既保證了服務的高可用性,又能夠提升開發和管理效率。隨著集群規模的增大以及管理數據的增多,分離管理信息到不同的數據管理節點,實現管理能力的擴容。通常情況下,中小型分布式系統,單機管理能力即可滿足。
- 避免過度設計,過早的優化;小步快跑,頻繁迭代。
- 沒有華麗的技術,把細小的點做好,不回避問題,特別是在高并發系統中,一個細小的問題,都可以引發整個服務雪崩。
Q&A
1、實時倉庫怎么避免超賣?
我們的搶購系統以加入購物車代表購買成功,因為用戶要買配件等,庫存是由計數器控制的,先限流,在計數,在可控的并發量情況下,不會出現超賣。
2、有了放號系統計算放號規則,為什么還需要一個外圍的 counter?
主要是 bigtap 到 cart 的環節 token 有丟失,在 cart 之后再加一個計數器,保障銷量,bigtap 再讀取計數器的數據控制前端商品銷售狀態,整個延遲不超 3s。
3、HTTP 集群通過 uuid hash 到 Middle,如果目標 Middle 已經死掉怎么應對?
這個問題在文章中有強調,在我們的場景下,商品迅速賣完,這塊沒有做高可用,只是從代碼層面做 review,完善異常處理機制,并且通常情況下,middle 負載不是特別高,幾百次活動下來,還沒出現過掛掉情況。
4、防刷系統是離線計算的嗎,還是有在線識別的策略?
基于日志,準實時,因為請求量比較大,專門搭了一套 Kafka 服務轉儲日志,基于 golang 開發 logcollect 與 antiblack 模塊,可以達到很高的處理性能。
5、請問如何模擬大量請求做測試?
我們遇到的情況是,由于壓測機單機端口限制造成早期不好測試,我們這邊壓測團隊基于開源模塊開發了能夠模擬虛擬IP的模塊,打破了單機端口的限制。
6、即使廣播和 Redis 拉取商品配置信息,仍有可能配置信息不一致如何解決?
這個主要是商品的配置和狀態信息,不涉及到強一致性要求的場景,我們這樣可以在秒級達到最終一致性。