1. 程式人生 > >從限流削峰到效能優化,談1號店抽獎系統架構實踐

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

這篇文章算是我在工作的第一個年頭裡關於架構方面的收穫與思考的一篇總結性的文章吧,感覺還是有些深度的,所以嘗試投稿到InfoQ上,果真被收錄了,很開心。從7月底開始動筆,中間因為各種偷懶和一些難以預料的事情拖了很久,終於填坑完畢了。回首過去的一年,還是搞了點事情的,這是一個結束,更是一個新的開始。

1.前言

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

2.整體設計詳解

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

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

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

整體架構如下圖:

2.1伺服器層的限流削峰

一般的分散式架構中,通用的伺服器層都是分為兩層,一是負載均衡器,如A10,F5,nginx等,二是web伺服器,如Tomcat,Jboss等。這裡我們優化了兩件事情。

a).防cc

負載均衡作為分散式系統的第一層,本身並沒有好說的。唯一值得一提的是針對此類大流量場景,我們特意引入了防cc機制。如在nginx可以通過配置limie_req_zone限制連線指令設定使用者連線數,超出的連結會返回503,這也是流量削峰的第一層。

b).Tomcat併發引數

一般Tomcat是使用預設的引數maxThreads=500,在流量沒有上來之前沒什麼感覺,但大流量情景下很可能會丟擲異常。所以這個引數需要通過效能壓測後設置為合理值,比如在壓測時發現併發請求超出400+後,響應速度明顯變慢,後臺開始出現數據庫,介面等連結超時,因此可以將maxThread改為了400,限制tomcat處理量,進一步削減流量,當然如果伺服器能扛住1000的併發,這個引數就可以設為1000。

2.2應用層的限流削峰

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

a).訊號量

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

semaphore=new Semaphore(350);

if (!semaphore.tryAcquire()) {
    return "error";
}
try {
    execute();
} finally {
    semaphore.release();
}

假設通過壓測得出的Tomcat最大執行緒數配置為400,那這裡的訊號量我們可以設成350,剩下50個執行緒用來響應超出的請求。在這種情景下,我們可以再用800甚至1000個併發做測試,由於請求還未抵達複雜的業務邏輯中,客戶端可以在短時間內收到錯誤響應,不會感到延遲或請求拒絕的現象。

b).使用者行為識別

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

  1. 實時人機識別:在使用者請求過程中,正常使用者跟機器的請求資料會有差異,如果請求資料跟實際的資料不一致,通過實時的識別,自然就可以將這個請求標識為非法請求,直接攔截。

  2. 風控列表:除了實時的人機識別,根據還可以根據一些賬號或者ip平時的購物等行為進行使用者畫像識別出其中的黃牛,機器賬號等等,維持著一個列表,對於列表中的賬號可以按風險等級進行額外的攔截。

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

可以看到,在未接入識別時流量峰值為60w ,接入識別後流量降為30w 。二者流量的峰值有一個明顯的差距,達到了預先設想中的削峰效果,有效緩解的伺服器峰值的併發壓力。
另一個比對是,在沒有接入識別時,我們一個活動數萬獎品,在活動開始3秒鐘就已經被抽光,而接入之後,當活動結束時剛好被抽完。
所以,如果沒有行為識別的攔截,不少正常使用者根本抽不到獎品,這點跟春節搶火車票是一樣的場景。

c).其他規則

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

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

2.3應用層的效能優化

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

a).快取

快取是降低資料庫壓力的有效手段,我們使用到的快取分為兩塊。

  1. 分散式快取:Ycache是1號店基於MemCache二次開發的一個分散式快取元件,我們將跟使用者相關的,資料規模大的資料快取在Ycache中,減少不必要的讀寫操作。

  2. 本地快取:使用分散式快取降低資料庫壓力,但仍然有一定的網路開銷,對於資料量小,無需更新的一些熱資料,比如活動規則,我們可以直接在web伺服器本地快取。代表性的是EhCache了,而我們那時比較直接粗暴,直接用ConcurrentHashMap造了個輪子,也能起到同樣的效果。

b).無事務

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

在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左右,那麼吞吐量指標(TPS)為為150000/60~2500tps,每次請求100ms,即併發數為250,這只是平均的,考慮活動往往最開始幾秒併發量最大,所以峰值併發估計為平均值的3-5倍。

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

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

首先檢視日誌發現數據庫連結存在超時

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

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

如圖,發現在幾個資料庫寫方法以及一個RPC介面上的耗時佔比最大。

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

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

  1. 走讀對應部分程式碼,排查是否有鎖,或者嚴重的邏輯錯誤如死迴圈等。
  2. dump虛擬機器記憶體快照,排查是否存在死鎖。
  3. 檢視sql語句及其執行計劃,確保業務邏輯合理,並走到索引。

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

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

  1. 連線阻塞;
  2. 磁碟io瓶頸;

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

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

3.其他優化想法

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

  1. 訊息佇列:由於抽獎一般會有個轉盤效果,意味著我們不需要馬上給出結果,如果引入訊息佇列,無疑可以有效削峰,降低伺服器壓力。如果說Tomcat的併發配置和訊號量的硬削峰是把1000併發直接拒掉500來做到,而這種是把1000併發排隊每次處理500來實現,也就是說結果上是會處理掉所有請求,相對來說更合理。類似的秒殺系統便接入了這個功能,但由於當時重構時間只有兩週,評估下來時間上來不及做,因此擱置了。

  2. 非同步:前面談到了一個RPC接口占用了近50%的耗時,經過業務邏輯上的評估這個介面是可以非同步的,所以如果有必要的時候這是一個可行的方案。

  3. 讀寫分離:主備庫的同步還是有延遲的,基於一致性考慮,讀寫分離的方案被我們拋棄了,但在其他高併發場景,讀寫分離是一個比較常見的優化方案。

  4. 活動拆庫:效能的瓶頸還是在資料庫,如果多個活動並行,並且互不相干,我們完全可以按活動拆庫,分擔資料庫壓力,不過這次的壓力還沒有達到這個量。

  5. 記憶體資料庫:資料庫的IO效率影響很大,把資料庫所在的機械硬碟換成SSD後有數倍效能的提升,但記憶體的速度更快,相關文章已經介紹到12306已經全面應用了。

  6. 升級硬體:換了SSD後效能就上來了,在未來如果有了瓶頸,可以預見的是如果硬體的有了新的發展,通過升級硬體是比較省力的方式。

4.幾點思考:

  1. 警惕流量,使用者量的增長:在沒有引入行為識別前,看著流量的大量上漲無疑是很高興的,但引入使用者行為識別後,發現不少流量可能來自於指令碼。假設我們沒有做行為識別,一個普通使用者,稍微慢幾秒就得不到獎品,來這麼兩三次,估計就不會來參加你的活動了,正常使用者就這麼一個個流失了,這種負面影響想想就讓人背後發涼。所以當看到使用者量快速增長,在高興的同時,一定要意識到其中可能的風險,引入必要風控手段,保證真正的使用者的使用者體驗。

  2. 效能優化是系統性的問題:從前臺到後臺我們考慮了很多優化方式,但最後壓測不通過,一頭栽在了老化的硬碟上,真是一個活生生的短板理論例子,所以優化不能單單侷限程式碼,JVM的層次,從頁面到硬碟,一定要通盤考慮。在遇到效能瓶頸時,不要只從表面的程式碼排查問題,要深入,網路,硬體都有可能瓶頸。

5.致謝

衷心的感謝過去的一年裡同事們給與的幫助與指導,特別是一起加班奮鬥過的小夥伴們,這是一個結束,也是一個新的開始。

作者:初龍

本文由MetaCLBlog於2017-07-17 09:14:17自動同步至cnblogs