如何設計一個小而美的秒殺系統
作者 劉鵬 發佈於 2017年3月7日
現如今,春節搶紅包的活動已經逐漸變成大家過年的新風俗。親朋好友的相互饋贈,微信、微博、支付寶等各大平臺種類繁多的紅包讓大家收到手軟。雞年春節,鏈家也想給15萬的全國員工包個大紅包,於是我們構建了一套旨在支撐10萬每秒請求峰值的搶紅包系統。經實踐證明,春節期間我們成功的為所有的小夥伴提供了高可靠的服務,紅包總發放量近百萬,搶紅包的峰值流量達到3萬/秒,最快的一輪搶紅包活動3秒鐘所有紅包全部搶完,系統運行0故障。
紅包系統,類似于電商平臺的秒殺系統,本質上都是在一個很短的時間內面對巨大的請求流量,將有限的庫存商品分發出去,並完成交易操作。比如12306搶票,庫存的火車票是有限的,但暫態的流量非常大,且都是在請求相同的資源,這裡面資料庫的併發讀寫衝突以及資源的鎖請求衝突非常嚴重。就我們實現這樣一個紅包系統本身來說,面臨著如下的一些挑戰:
首先,到活動整點時刻,我們有15萬員工在固定時間點同時湧入系統搶某輪紅包,瞬間的流量是很大的,而目前我們整個鏈路上的系統和服務基礎設施,都沒有承受過如此高的輸送量,要在短時間內實現業務需求,在技術上的風險較大。
其次,公司是第一次開展這樣的活動,我們很難預知大家參與活動的情況,極端情況下可能會出現某輪紅包沒搶完,需要合併到下輪接著發放。這就要求系統有一個動態的紅包發放策略和預算控制,其中涉及到的動態計算會是個較大的問題(這也是為系統高吞吐服務),實際的系統實現中我們採用了一些預處理機制。
最後,這個系統是為了春節的慶祝活動而研發的定制系統,且只上線運行一次,這意味著我們無法積累經驗去對服務做持續的優化。並且相關的配套環境沒有經過實際運行檢驗,缺少參考指標,系統的薄弱環節發現的難度大。所以必須要追求設計至簡,儘量減少對環境的依賴(資料路徑越長,出問題的環節越多),並且實現高可伸縮性,需要盡一切努力保證可靠性,即使有某環節失誤,系統依然能夠保障核心的用戶體驗正常。
系統設計
紅包本身的資訊通過預處理資源介面獲取。運行中使用者和紅包的映射關係動態生成。底層使用內部開發的DB中介軟體在MySQL資料庫集群上做紅包發放結果持久化,以供非同步支付紅包金額到使用者帳戶使用。整個系統的絕大部分模組都有性能和保活監控。系統架構圖如圖所示。所有的靜態資源提前部署在了協力廠商的CDN服務上,系統的核心功能主要劃分到接入層和核心邏輯系統中,各自部署為集群模式並且獨立。接入層主要是對用戶身份鑒權和結果緩存,核心系統重點關注紅包的分發,紅色實線的模組是核心邏輯,為了保障其可靠性,我們做了包括資料預處理、水準分庫、多級緩存、精簡RPC調用、超載保護等多項設計優化,並且在原生容器、MySQL等服務基礎設施上針對特殊的業務場景做了優化,後面將為讀者一一道來。
優化方案
優化方案中最重要的目標是保障關鍵流程在應對大量請求時穩定運行,這需要很高的系統可用性。所以,業務流程和資料流程程要儘量精簡,減少容易出錯的環節。此外,緩存、DB、網路、容器環境,任何一個部分都要假設可能會短時出現故障,要有處理預案。針對以上的目標難點,我們總結了如下的實踐經驗。
1.數據預處理
紅包本身的屬性資訊(金額,狀態,祝福語,發放策略),我們結合活動預案要求,使用一定的演算法提前生成好所有的資訊,資料總的空間不是很大。為了最大化提升性能,這些紅包資料,我們事先存儲在資料庫中,然後在容器載入服務啟動時,直接載入到本地緩存中當作唯讀資料。另外,我們的員工資訊,我們也做了一定的裁剪,最基本的資訊也和紅包資料一樣,預先生成,服務啟動時載入。
此外,我們的活動頁面,有很多視頻和圖片資源,如果這麼多的用戶從我們的閘道即時訪問,很可能我們的頻寬直接就被這些大流量的請求占滿了,用戶體驗可想而知。最後這些靜態資源,我們都部署在了CDN上,通過資料預熱的方式加速用戶端的存取速度,閘道的流量主要是來自於搶紅包期間的小資料請求。
2.精簡RPC調用
通常的服務請求流程,是在接入層訪問用戶中心進行用戶鑒權,然後轉發請求到後端服務,後端服務根據業務邏輯調用其他上游服務,並且查詢資料庫資源,再更新服務/資料庫的資料。每一次RPC調用都會有額外的開銷,所以,比如上一點所說的預載入,使得系統在運行期間每個節點都有全量的查詢資料可在本地訪問,搶紅包的核心流程就被簡化為了生成紅包和人的映射關係,以及發放紅包的後續操作。再比如,我們採用了非同步拉的方式進行紅包發放到賬,用戶搶紅包的請求不再經過發放這一步,只記錄關係,性能得到進一步提升。
實際上有些做法的可伸縮性是極強的。例如紅包資料的預生成資訊,在當時的場景下我們是能夠作為本地記憶體緩存加速訪問的。當紅包資料量很大的時候,在每個服務節點上使用本地資料庫,或者本地資料檔案,甚至是本地Redis/MC緩存服務,都是可以保證空間足夠的,並且還有額外的好處,越少的RPC,越少的服務抖動,只需要關注系統本身的健壯性即可,不需要考慮外部系統QoS。
3.搶紅包的併發請求處理
春節整點時刻,同一個紅包會被成千上萬的人同時請求,如何控制併發請求,確保紅包會且僅會被一個用戶搶到?
做法一,使用加鎖操作先佔有鎖資源,再佔有紅包。
可以使用分散式全域鎖的方式(各種分散式鎖元件或者資料庫鎖),申請lock該紅包資源成功後再做後續操作。優點是,不會出現髒資料問題,某一個時刻只有一個應用執行緒持有lock,紅包只會被至多一個使用者搶到,資料一致性有保障。缺點是,所有請求同一時刻都在搶紅包A,下一個時刻又都在搶紅包B,並且只有一個搶成功,其他都失敗,效率很低。
做法二,單獨開發請求排隊調度模組。
排隊模組接收使用者的搶紅包請求,以FIFO模式保存下來,調度模組負責FIFO佇列的動態調度,一旦有空閒資源,便從佇列頭部把使用者的訪問請求取出後交給真正提供服務的模組處理。優點是,具有中心節點的統一資源管理,對系統的可控性強,可深度定制。缺點是,所有請求流量都會有中心節點參與,效率必然會比分散式無中心系統低,並且,中心節點也很容易成為整個系統的性能瓶頸。
做法三,巧用Redis特性,使其成為分散式序號生成器。(我們最終採用的做法)。
访问请求被LB分发到每个分组,一个分组包含若干台应用容器、独立的数据库和Redis节点。Redis节点内存储的是这个分组可以分发的红包ID号段,利用Redis单进程的自减数值特性实现分布式红包ID生成器,服务通过此获取当前拆到的红包。落地数据都持久化在独立的数据库中,相当于是做了水平分库。某个分组内处理的请求,只会访问分组内部的Redis和数据库,和其他分组隔离开。
分组的方式使得整个系统实现了高内聚,低耦合的原则,能将数据流量分而治之,提升了系统的可伸缩性,当面临更大流量的需求时,通过线性扩容的方法,即可应对。并且当单个节点出现故障时,影响面能够控制在单个分组内部,系统也就具有了较好的隔离性。
4.系统容量评估,借助数据优化,过载保护
由于是首次开展活动,我们缺乏实际的运营数据,一切都是摸着石头过河。所以从项目伊始,我们便强调对系统各个层次的预估,既包括了活动参与人数、每个功能feature用户的高峰流量、后端请求的峰值、缓存系统请求峰值和数据库读写请求峰值等,还包括了整个业务流程和服务基础设施中潜在的薄弱环节。后者的难度更大因为很难量化。此前我们连超大流量的全链路性能压测工具都较缺乏,所以还是有很多实践的困难的。
在这里内心真诚的感谢开源社区的力量,在我们制定完系统的性能指标参考值后,借助如wrk等优秀的开源工具,我们在有限的资源里实现了对整个系统的端到端全链路压测。实测中,我们的核心接口在单个容器上可以达到20,000以上的QPS,整个服务集群在110,000以上的QPS压力下依然能稳定工作。
正是一次次的全链路压测参考指标,帮助我们了解了性能的基准,并以此做了代码设计层面、容器层面、JVM层面、MySQL数据库层面、缓存集群层面的种种优化,极大的提升了系统的可用性。具体做法限于篇幅不在此赘述,有兴趣的读者欢迎交流。
此外,为了确保线上有超预估流量时系统稳定,我们做了过载保护。超过性能上限阈值的流量,系统会快速返回特定的页面结果,将此部分流量清理掉,保障已经接受的有效流量可以正常处理。
5.完善监控
系统在线上运行过程中,我们很需要对其实时的运行情况获取信息,以便能够对出现的问题进行排查定位,及时采取措施。所以我们必须有一套有效的监控系统,能够帮我们观测到关键的指标。在实际的操作层面,我们主要关注了如下指标:
服务接口的性能指标
借助系统的请求日志,观测服务接口的QPS,接口总的实时响应时间。同时通过HTTP的状态码观测服务的语义层面的可用性。
系统健康度
结合总的性能指标以及各个模块应用层的性能日志,包括模块接口返回耗时,和应用层日志的逻辑错误日志等,判断系统的健康度。
整体的网络状况
尽量观测每个点到点之间的网络状态,包括应用服务器的网卡流量、Redis节点、数据库节点的流量,以及入口带宽的占用情况。如果某条线路出现过高流量,便可及时采取扩容等措施缓解。
服务基础设施
应用服务器的CPU、Memory、磁盘IO状况,缓存节点和数据库的相应的数据,以及他们的连接数、连接时间、资源消耗检测数据,及时的去发现资源不足的预警信息。
对于关键的数据指标,在超过预估时制定的阈值时,还需要监控系统能够实时的通过手机和邮件实时通知的方式让相关人员知道。另外,我们在系统中还做了若干逻辑开关,当某些资源出现问题并且自动降级和过载保护模块失去效果时,我们可以根据状况直接人工介入,在服务不停机的前提前通过手动触发逻辑开关改变系统逻辑,达到快速响应故障,让服务尽快恢复稳定的目的。
6.服务降级
当服务器压力剧增的时候,如果某些依赖的服务设施或者基础组件超出了工作负荷能力,发生了故障,这时候极其需要根据当前的业务运行情况对系统服务进行有策略的降级运行措施,使得核心的业务流程能够顺利进行,并且减轻服务器资源的压力,最好在压力减小后还能自动恢复升级到原工作机制。
我们在开发红包系统时,考虑到原有IDC机房的解决方案对于弹性扩容和流量带宽支持不太完美,选择了使用AWS的公有云作为服务基础环境。对于第三方的服务,缺少实践经验的把握,于是从开发到运维过程中,我们都保持了一种防御式的思考方式,包括数据库、缓存节点故障,以及应用服务环境的崩溃、网络抖动,我们都认为随时可能出问题,都需要对应的自动替换降级策略,严重时甚至可通过手动触发配置开关修改策略。当然,如果组件自身具有降级功能,可以给上层业务节约很多成本资源,要自己实现全部环节的降级能力的确是一件比较耗费资源的事情,这也是一个公司技术慢慢积累的过程。
结束语
以上是我们整个系统研发运维的一些体会。这次春节红包活动,在资源有限的情况下成功抵抗超乎平常的流量峰值压力,对于技术而言是一次很大的挑战,也是一件快乐的事情,让我们从中积累了很多实践经验。未来我们将不断努力,希望能够将部分转化成较为通用的技术,去更好的推动业务成功。真诚希望本文的分享能够对大家的技术工作有所帮助。