從限流削峰到性能優化,談1號店抽獎系統架構實踐「轉」

1. 前言

抽獎是一個典型的高併發場景應用,平時流量不多,但遇到大促活動,流量就會暴增,今年的週年慶期間的日均 UV 就超過百萬。在過去的一年裡,負責過這個項目的多次重構工作,期間各種踩坑無數,就以此文當做總結,來聊聊我們是如何架構這個高併發系統吧。



2. 整體設計詳解

在我看來,能提高服務器應對併發的能力的方式無非兩種:

  1. 限流削峰:通過降低實際抵達服務器的併發量,降低服務器處理壓力;
  2. 性能優化:從前臺到硬件,優化系統各方面性能,提高服務器處理能力。

接下來我們圍繞這兩個方面談談在 1 號店抽獎系統中所做的工作和遇到的坑。

整體架構如下圖:

從限流削峰到性能優化,談1號店抽獎系統架構實踐「轉」

2.1 服務器層的限流削峰

我們的負載服務器使用的是 A10,商業的負載均衡硬件,相比 nginx,雖然花不少錢,但在使用配置等方面簡單,便於維護,web 服務器自然是 Tomcat。這裡我們優化了兩件事情。

a). 防 cc

負載均衡作為分佈式系統的第一層,本身並沒有好說的。唯一值得一提的是針對此類大流量場景,我們特意引入了防 cc 機制,限制用戶每分鐘的最高訪問次數,超出頻率的請求直接拒絕,防止用戶使用腳本等方式刷請求。這個在我們使用的負載均衡器 A10 上可以自行配置,如果是 nginx 也有限制連接模塊可以使用,這也是流量削峰的第一層。

b).Tomcat 併發參數

我們之前線上的 Tomcat 是使用默認的參數 maxThreads=500,在流量沒有上來之前沒什麼感覺,但大流量情景下會拋出不少異常日誌。在通過性能壓測後發現,在併發請求超出 400+ 後,響應速度明顯變慢,後臺開始出現數據庫,接口等鏈接超時,因此將 maxThread 改為了 400,限制 tomcat 處理量,進一步削減流量。

2.2 應用層的限流削峰

從這裡開始,請求就進入應用代碼中了,在這一層,我們可以通過代碼來進行流量削峰工作了,主要包括信號量,用戶行為識別等方式。

從限流削峰到性能優化,談1號店抽獎系統架構實踐「轉」

a). 信號量

前面談到了通過 Tomcat 併發線程配置來攔截超出的流量,但這裡有一個問題是超出的請求要麼被阻塞,要麼被直接拒絕的,不會給出響應。在客戶端看到的是長時間沒有響應或者請求失敗,然後不斷重試,我們更希望在這個時候響應一些信息,比如說直接給出提示沒有中獎,通知客戶端不再請求,從而提高用戶體驗。因此在這裡我們使用了 java 併發包中的 Semaphore,偽代碼如下:

<code>semaphore=new Semaphore(350);if (!semaphore.tryAcquire()) {    return "error";}try {    execute();} finally {    semaphore.release();}/<code>

由於通過壓測得出的 Tomcat 最大線程數配置為 400,這裡的信號量我們設成了 350,剩下 50 個線程用來響應超出的請求。在這種情景下,我們曾用 800 個併發做過測試,由於請求還未抵達複雜的業務邏輯中,客戶端可以在 10ms 內收到錯誤響應,不會感到延遲或請求拒絕的現象。

b). 用戶行為識別

Tomcat 及信號量進行的併發控制我稱之為硬削峰,並不管用戶是誰,超出設置上限直接拒絕。但我們更想做的是將非法的請求攔截掉,比如腳本,黃牛等等,從而保證正常用戶的訪問,因此,在公司風控等部門同學的協助下,引入一些簡單的用戶行為識別。

  1. 實時人機識別: 在用戶請求過程中,正常用戶跟機器的請求數據會有差異,如果請求數據跟實際的數據不一致,通過實時的識別,自然就可以將這個請求標識為非法請求,直接攔截。
  2. 風控列表:除了實時的人機識別,根據還可以根據一些賬號或者 ip 平時的購物等行為進行用戶畫像識別出其中的黃牛,機器賬號等等,維持著一個列表,對於列表中的賬號可以按風險等級進行額外的攔截。

下圖一個接入用戶行為識別前後的一個流量對比圖

從限流削峰到性能優化,談1號店抽獎系統架構實踐「轉」

可以明顯的看到,兩天的同一時刻,在未接入識別時流量峰值為60w ,接入識別後流量降為30w 。也就意味著有人通過腳本等工具貢獻了超過一半的請求量;另一個比對是,在沒有接入識別時,我們一個活動數萬獎品,在活動開始3 秒鐘就已經被抽光,而接入之後,當活動結束時剛好被抽完。所以,如果沒有行為識別的攔截,不少正常用戶根本抽不到獎品,這點跟春節搶火車票是一樣的場景。

c). 其他規則

其他規則包括緩存中的活動限制規則等等,根據一些簡單的邏輯,也起到一定作用的流量削峰。

至此,我們所有的流量削峰思路都已經解釋完了,接下來是針對性能優化做的一些工作。

2.3 應用層的性能優化

性能優化是一個龐大的話題,從代碼邏輯,緩存,到數據庫索引,從負載均衡到讀寫分離,能談的事情太多了。在我們的這個高併發系統中,性能的瓶頸在於數據庫的壓力,這裡就聊下我們的一些解決思路。

a). 緩存

緩存是降低數據庫壓力的有效手段,我們使用到的緩存分為兩塊。

  1. 分佈式緩存:Ycache 是 1 號店基於 MemCache 二次開發的一個分佈式緩存組件,我們將跟用戶相關的,數據規模大的數據緩存在 Ycache 中,減少不必要的讀寫操作。
  2. 本地緩存:使用分佈式緩存降低數據庫壓力,但仍然有一定的網絡開銷,對於數據量小,無需更新的一些熱數據,比如活動規則,我們可以直接在 web 服務器本地緩存。代表性的是 EhCache 了,而我們那時比較直接粗暴,直接用 ConcurrentHashMap 造了個輪子,也能起到同樣的效果。

b). 無事務

對於併發的分佈式系統來說,數據的一致性是一個必須考慮的問題。在我們抽獎系統中,數據更需要保證一致,活動獎品是 1 臺 iphone,就絕不能被抽走兩臺。常見的做法便是通過事務來控制,但考慮到我們業務邏輯中的如下場景。

從限流削峰到性能優化,談1號店抽獎系統架構實踐「轉」

在 JDBC 的事務中,事務管理器在事務週期內會獨佔一個 connection,直到事務結束。

假設我們的一個方法執行 100ms,前後各有 25ms 讀寫操作,中間向其他 SOA 服務器做了一次 RPC,耗時 50ms,這就意味著中間 50ms 時 connection 將處於掛起狀態。

前面已經談到了當前性能的瓶頸在於數據庫,因此這種大事務等於將數據庫鏈接浪費一半,所以我們沒有使用事務,而是通過以下兩種方式保證數據的一致性。

  1. 樂觀鎖:在 update 時使用版本號的方式保證數據唯一性,比如在用戶中獎後減少已有獎品數量,update award set award_num=award_num-1,version=version+1 where id=#{id} and version=#{version} and award_num>0
  2. 唯一索引:在 insert 時通過唯一索引保證只插入一條數據,比如建立獎品 id 和用戶 id 的唯一索引,防止 insert 時插入多條中獎記錄。

2.4 數據庫及硬件

再往下就是基礎層了,包括我們的數據庫和更底層的硬件,之所以單獨列一節,是為了聊聊我們踩的一個坑。

當時為了應對高併發的場景,我們花了數週重構,從前臺服務器到後臺業務邏輯用上了各種優化手段,自認為扛住每分鐘幾十萬流量不成問題,但這都是紙上談兵,我們需要拿數據證明,因此用 JMeter 做了壓測。

首先是流量預估,我們統計了過往的數據,預估的流量是 15w/ 分鐘,單次請求性能指標是 100ms 左右,因此吞吐量為 150000/60~2500tps,每次請求 100ms,即併發數為 250,這只是平均的,考慮活動往往最開始幾秒併發量最大,所以峰值併發估計為平均值的 3-5 倍。

第一次我們用 50 個併發做壓測:

從限流削峰到性能優化,談1號店抽獎系統架構實踐「轉」

壓測結果簡直難以置信,平均耗時超 600ms,峰值輕鬆破 1000ms,這連生產上日常流量都扛不住,我們做了這麼多手段,不應該性能反而降低了,當時都有點懷疑人生了,所以我們著手開始排查原因。

首先查看日誌發現數據庫鏈接存在超時

從限流削峰到性能優化,談1號店抽獎系統架構實踐「轉」

排查發現配置的數據庫鏈接數為 30,50 個線程併發情景下會不夠,將最大鏈接數設為 100. 數據庫鏈接超時問題沒有了,但問題沒這麼簡單,測試下來還是一樣的結果。

然後通過 VisualVM 連上壓測的 JVM,我們查看了線程的快照。

(點擊放大圖像)

從限流削峰到性能優化,談1號店抽獎系統架構實踐「轉」

如圖,發現在幾個數據庫寫方法以及一個 RPC 接口上的耗時佔比最大。

所以一方面我們自己著手查原因,另一方面也推動接口提供方減少耗時。

首先是一些常規的排查手段

  1. 走讀對應部分代碼,排查是否有鎖,或者嚴重的邏輯錯誤如死循環等。
  2. dump 虛擬機內存快照,排查是否存在死鎖。
  3. 查看 sql 語句及其執行計劃,確保業務邏輯合理,並走到索引。
  4. ...

當時花了兩天時間毫無進展,代碼上沒發現任何問題,也請教了很多同事,感覺已經陷入了思維誤區,然後有位同事說這不是我們程序的問題,會不會是數據庫本身或者硬件問題。我們馬上找了 DBA 的同事,查看測試數據庫的執行情況,如圖:

從限流削峰到性能優化,談1號店抽獎系統架構實踐「轉」

log file sync 的 Avg wait 超過了 60ms,查閱資料後瞭解到這種情況的原因可能有:

  1. 連接阻塞;
  2. 磁盤 io 瓶頸;

然後我們一看,壓測環境的服務器的硬盤是一塊老的機械硬盤,而其他環境早已 SSD 遍地了。我們連夜把壓測環境切換到了 SSD,問題解決了,最後壓測結果:單機 441 個併發, 平均響應時間 136ms,理論上能扛住 19w/ 分鐘的流量,比起第一次壓測有了數十倍的提升,單機即可扛住預估流量的壓力,生產上更不成問題了,可以上線了。

至此,整個抽獎系統的架構,以及我們限流削峰和調優的所有手段已經介紹完了,接下來展開下其他的優化想法和感悟吧。

3. 其他優化想法

這裡還有一些曾經考慮過的想法供參考,可能由於時間,不適用等原因沒有做,但也是應對高併發場景的思路。

  1. 消息隊列:由於抽獎一般會有個轉盤效果,意味著我們不需要馬上給出結果,如果引入消息隊列,無疑可以有效削峰,降低服務器壓力。如果說 Tomcat 的併發配置和信號量的硬削峰是把 1000 併發直接拒掉 500 來做到,而這種是把 1000 併發排隊每次處理 500 來實現,也就是說結果上是會處理掉所有請求,相對來說更合理。1 號店的秒殺系統便接入了這個功能,但由於當時重構時間只有兩週,評估下來時間上來不及做,因此擱置了。
  2. 異步:前面談到了一個 RPC 接口占用了近 50% 的耗時,經過業務邏輯上的評估這個接口是可以異步的,所以如果有必要的時候這是一個可行的方案。
  3. 讀寫分離:主備庫的同步還是有延遲的,基於一致性考慮,讀寫分離的方案被我們拋棄了,但在其他高併發場景,讀寫分離是一個比較常見的優化方案。
  4. 活動拆庫:性能的瓶頸還是在數據庫,如果多個活動並行,並且互不相干,我們完全可以按活動拆庫,分擔數據庫壓力,不過這次的壓力還沒有達到這個量。
  5. 內存數據庫:數據庫的 IO 效率影響很大,把數據庫所在的機械硬盤換成 SSD 後有數倍性能的提升,但內存的速度更快,相關文章已經介紹到 12306 已經全面應用了。
  6. 升級硬件:換了 SSD 後性能就上來了,在未來如果有了瓶頸, 可以預見的是如果硬件的有了新的發展,通過升級硬件是比較省力的方式。

4. 幾點思考

  1. 警惕流量,用戶量的增長:在沒有引入行為識別前,看著監控裡流量十萬十萬的上漲無疑是很高興的,但引入用戶行為識別後,我們發現一大半的流量可能來自於腳本。假設我們沒有做行為識別,一個普通用戶,稍微慢幾秒就得不到獎品,來這麼兩三次,估計就不會來參加你的活動了,正常用戶就這麼一個個流失了,這種負面影響想想就讓人背後發涼。所以當看到用戶量快速增長,在高興的同時,一定要意識到其中可能的風險,引入必要風控手段,保證真正的用戶的用戶體驗。
  2. 性能優化是系統性的問題:從前臺到後臺我們考慮了很多優化方式,但最後壓測不通過,一頭栽在了老化的硬盤上,真是一個活生生的短板理論例子,所以優化不能單單侷限代碼,JVM 的層次,從頁面到硬盤,一定要通盤考慮。在遇到性能瓶頸時,不要只從表面的代碼排查問題,要深入,網絡,硬件都有可能瓶頸。



原文地址:https://www.cnblogs.com/wchukai/p/6133477.html


分享到:


相關文章: