?

高并發多線程競爭共享資源架構

2020-11-17 06:56林平榮陳澤榮施曉權
計算機工程與設計 2020年11期
關鍵詞:線程隊列消息

林平榮,陳澤榮,施曉權

(1.廣州大學華軟軟件學院 軟件研究所,廣東 廣州 510990; 2.華南理工大學 計算機科學與工程學院,廣東 廣州 510006)

0 引 言

隨著互聯網的快速發展和線上用戶的急劇增加,高并發請求產生的多線程競爭共享資源系統面臨著更大的挑戰。高并發是一種系統運行過程中出現同時并行處理大量用戶請求的現象。多線程競爭共享資源指的是在高并發場景下,多個請求產生多個線程,從而造成多個線程同時競爭訪問共享資源。共享資源應用對象可以是火車票或商品等有限數量資源,典型的場景有“12306搶火車票”、“天貓雙十一秒殺活動”等。由于并發訪問數量多且共享資源有限,因此對系統的并發性能以及資源一致性有更高的要求。

經過查閱相關文獻,目前已有大量解決Web系統性能難題的方案[1-9]被提出,解決方案主要圍繞以下幾個方面:負載均衡[5]、數據緩存[6,9]、數據庫優化[7]以及Web前端優化[8]。雖然這些方案在一定程度上可以提高系統性能,但考慮的點比較單一,并沒有過多提及多線程競爭共享資源時系統垂直或水平擴展下如何確保資源的安全性和一致性?;诖?,本文將從多方面綜合考慮,基于數據緩存、分布式鎖、消息隊列、負載均衡4個方面進行詳細的分析與研究,提出一種適合高并發多線程競爭共享資源應用場景的高性能通用系統架構。

1 系統架構

系統架構分為訪問層、應用層、存儲層。訪問層用于Web接入、反向代理、負載均衡等;應用層用于核心業務服務模塊處理,具備服務治理、調度、異步通信等核心服務能力;存儲層進行最終數據的落地及提供數據的能力。架構如圖1所示。

圖1 系統架構

架構部署后,應用服務器先從數據庫服務器將高并發請求中頻繁訪問的數據讀取到緩存服務器中??蛻舳说母卟l請求先由LVS(linux virtual server)在IP層均衡地轉發到負載均衡服務器,負載均衡服務器會先將資源返回給客戶端,再將請求轉發到應用服務器。應用服務器接收到請求時,在一定時間內不斷嘗試從分布式緩存服務器中獲取鎖的操作,獲取成功則會進行相應的業務邏輯處理。在緩存服務器中進行數據的增刪改查并將數據發送到消息隊列服務器中,最后由消費者進行消息監聽,將數據同步到數據庫服務器中,實現數據異步更新。

架構設計秉持“冗余”+“主動故障轉移”兩個原則,“冗余”是為了解決單一節點出現故障而backup節點能夠繼續提供服務,而主動故障轉移是探測到故障的發生則自動進行轉移,將故障節點流量進行引流。架構的設計從業務的角度出發,第一是純靜態資源的請求,另外一個是涉及業務處理的請求。

純靜態資源請求的處理:前端固化頁面展示采用靜態化的思想,將Redis緩存數據填充靜態模板中,形成靜態化頁面,再推送到Nginx服務器中,當客戶端訪問請求時,LVS均衡地在IP層轉發至處理服務的Nginx,從負載的Nginx直接返回沒有涉及業務邏輯處理的靜態頁面,減少與數據庫服務交互的次數,且不執行任何代碼,給予客戶端較快的響應速度。當頁面數據發生變更時,會觸發監聽器將變更的消息壓入消息隊列中,緩存服務感知數據變更,便會調用數據服務,將整理好數據重新推送至Redis中,更新Nginx頁面。Nginx本地緩存數據也是有一定的時限,為了避免頁面的集中獲取,采用隨機散列的策略來規避。

涉及業務請求的處理:倘若LVS負載均衡分配Nginx的高并發請求是攜帶業務性的,就會導致多線程競爭共享資源時數據不一致的窘境。為避免這種問題,采取Redis分布式鎖的方式以維護共享資源的數據一致和安全問題,然后再將操作后的相關緩存數據寫入消息隊列服務器中,最后交由消費者進行監聽消費,這樣的異步設計是為了實現緩存與數據庫的互通,逐級削減對數據庫服務器的訪問。此外,為了維護緩存和數據庫的雙寫一致性,堅持“Cache Aside Pattern”原則。

設計過程中,盡量考慮到分布式緩存服務和消息隊列服務在生產環境會產生的細節問題,針對所在問題依次采取下列措施:

(1)在分布式緩存服務中,考慮緩存應用于高并發場景易發生的雪崩、穿透和擊穿情況,分別提供相對應的方案如下:

緩存雪崩情況,分別在緩存雪崩前中后的各時間段,依據業務設計相應的解決方案:

1)在事前:利用Redis高可用的主從+哨兵的Redis Cluster避免全盤崩潰;

2)在事中:采用本地緩存+Nginx限流,避免數據庫壓力過大;

3)在事后:Redis開啟持久化,一旦重啟,自動加載磁盤數據,快速恢復數據。

緩存穿透是高并發請求的目標數據都不在緩存與數據庫的情況,易導致數據庫的直接崩盤,該情況采取Set -999 UNKNOWN,盡量設置有效期,避免下次訪問直接從緩存獲取。為了防止緩存擊穿,可采取一些熱點數據嘗試設置永不過期。

(2)在消息隊列服務中,共享資源的最終持久化是在消息隊列服務中進行的,為了維護共享資源的最終一致性,考慮寫入消息隊列服務器中消息的消費冪等性、可靠性傳輸、消費順序性和容器飽滿的情況,檢查數據庫主鍵的唯一性來確保消費的冪等性,避免重復消費的請求;消息隊列的可靠性傳輸,在生產者設置Ack響應,要求leader接收到消息之后,所有的follower必須進行同步的確認寫入成功,如果沒有滿足該條件,生產者會自動不斷地重試。為了避免中間件消息隊列自己丟失數據,必須開啟持久化功能;消息隊列的順序消費,采取一個queue對應一個consumer,然后這個consumer內部采用內存隊列做排隊,分發給底層不同的worker來處理;消息隊列容器飽滿時,將會觸發待命的臨時程序加入消費的行列中,加快消費速度,以免消息溢出的慘狀。

2 關鍵技術

2.1 數據緩存

現代Web系統一般都會采用緩存策略對直接進行數據庫讀寫的傳統數據操作方式進行改進。緩存大致主要分為兩種:頁面緩存和數據緩存。架構基于Redis集群實現數據緩存,具有故障容錯等功能。Redis完全基于內存,通過Key-Value存儲能獲得良好的性能,尤其表現在良好的并行讀寫性能[10]。由于投票容錯機制要求超過半數節點認為某個節點出現故障才會確定該節點下線,因此Redis集群至少需要3個節點,為了保證集群的高可用性,每個主節點都有從節點。實現Redis集群拓撲結構如圖2所示。

圖2 Redis集群網絡拓撲

Redis集群由6臺服務器搭建,分別由3個主節點和3個從節點組成,數據都是以Key-Value形式存儲,不同分區的數據存放在不同的節點上,類似于哈希表的結構。使用哈希算法將Key映射到0-16383范圍的整數,一定范圍內的整數對應的抽象存儲稱為槽,每個節點負責一定范圍內的槽,槽范圍如圖3所示。

圖3 槽范圍

集群啟動時,會先從數據庫服務器讀取高并發請求中頻繁訪問的數據,其中包括共享資源數據,將數據轉換為JSON數據格式或對象序列化初始化到Redis服務器中,數據會根據哈希算法新增到對應的節點中。應用服務器接收到高并發請求進行數據查詢、修改或刪除時,會隨機把命令發給某個節點,節點計算并查看這個key是否屬于自己的,如果是自己的就進行處理,并將結果返回,如果是其它節點的,會把對應節點信息(IP+地址)轉發給應用服務器,讓應用服務器重定向訪問。Redis返回結果后,應用服務器會將數據發送到消息隊列服務器中,并立即將返回結果給客戶端。

2.2 分布式鎖

在分布式系統中,共享資源可能被多個競爭者同時請求訪問,往往會面臨數據的一致性問題[11],因此必須保證資源數據訪問的正確性和性能。架構使用分布式鎖的方式解決該問題。分布式架構下的開源組件很多,如Zookee-per、Redis、Hbase等,相比之下,Redis的性能與成熟度較高。指定一臺Redis服務器作為鎖的操作節點,保證獲取鎖的操作是原子性的。Redis本身是單線程程序,可以保證對緩存數據操作都是線程安全的。當應用服務器集群接收到高并發產生的多線程同時請求訪問共享資源時,線程必須先從Redis服務器中獲取鎖,使用setnx指令獲取鎖成功后,其余的線程請求獲取鎖的操作會返回失敗,返回失敗后的線程在一定時間內不斷重試獲取鎖,只有等待已獲取鎖的線程執行成功后釋放鎖,才能讓下一個線程獲取鎖后訪問共享資源。線程獲取鎖成功后,若有線程報錯會中途退出,獲取鎖之后沒有釋放,就會造成死鎖。使用expire作為默認過期時間,如果線程獲取鎖后超過默認過期時間,則鎖會自動釋放,為了避免業務邏輯處理報錯,導致線程中途退出,因此需要再加上捕獲異常處理塊。示例代碼如下:

//獲取lock失敗

if(!set lock true ex 5 nx)

{ return; }

try{#處理業務邏輯

……

//釋放lock

del lock }

catch(Exception ex)

{ //釋放lock

del lock }

由于采用分布式鎖,在系統垂直或水平擴展的情況下,保證同一時間的一個線程獲取到鎖,確保共享資源的安全性和一致性。在高并發場景下,如果有大量的線程不斷重試獲取鎖失敗的操作,會造成Redis服務器壓力過大,Redis服務器與應用服務器之間交互流量過高。因此,在應用系統上使用基于cas算法的樂觀鎖方式解決該問題。cas算法存在著3個參數,內存值V,預期值E,更新值N。當且僅當內存值V與預期值E相等時,才會將內存值修改為N,否則什么也不做。借助jdk的juc類庫所提供的cas算法,以及帶有原子性的基本類型封裝類AtomicBoolean,實現區別于synchronized同步鎖的一種樂觀鎖,線程不會阻塞,不涉及上下文切換,具有性能開銷小等優點。示例代碼如下:

long startTime = System.currentTimeMillis();

AtomicBoolean state=new AtomicBoolean(false);

//X秒內不斷重試調用compareAndSet方法修改內存

while ((startTime+X)>= System.currentTimeMillis()){

//預期值false,更新值true

if(!state.get()&&state.compareAndSet(false,true)){

//修改內存值成功

try{

#獲取分布式鎖

//將內存值重新修改為false

state.compareAndSet(true,false);

}catch(Exception ex){

state.compareAndSet(true,false);

}

}

}

各個應用系統的線程通過在一定時間內不斷重試修改內存值,如果修改成功,才可以繼續獲取分布式鎖,以解決Redis服務器的壓力和流量問題。

2.3 消息隊列

消息隊列是一種異步傳輸模式,其主要核心優點為以下3點:業務解耦、通信異步、流量削峰[12,13],因此可應用于很多高并發場景。目前主流的消息隊列中間件有Kafka、RabbitMQ、ActiveMQ及Microsoft MSMQ[12]等,其中RabbitMQ是一個開源的AMQP實現,支持多種客戶端,總共有6種工作模式,用于在分布式或集群系統中存儲轉發消息,具有較好的易用性、擴展性、高可用性。架構使用RabbitMQ實現數據庫與Redis緩存數據的同步,可以根據實際高并發場景進行判斷選擇合適的通信模式以及生產者與消費者的對應關系。下面以RabbitMQ的路由模式為例,具體實現的路由模式結構如圖4所示。

圖4 路由模式結構

按照實際業務,以明顯的類別劃分,聲明一個交換機,4個消息隊列,創建4個消費者分別監聽4個消息隊列,應用系統數量對應生產者數量。當系統接收到高并發請求資源時,會使用Redis分布式鎖確保所有系統產生的線程同步執行訪問共享資源,保證共享資源的安全性、一致性。釋放鎖后,再將處理完成的數據更新到Redis中,Redis返回結果后,系統會將數據轉化為JSON格式,按照消息路由鍵,確保同個客戶端請求的數據在同個消息隊列中,能夠在消費者進行數據增刪改時按照先后順序執行,保證系統公平性。設置路由鍵后將數據發送到交換機中,交換機會根據路由規則將不同類別的數據發送到綁定的對應消息隊列中,每個消息隊列都有對應的一個消費者,消費者會監聽對應的消息隊列,監聽到消息后會進行JSON格式數據的解析,再將數據同步到數據庫中。利用RabbitMQ消息隊列,進行強弱依賴梳理分析,將數據同步到數據庫的操作異步化,以解決數據庫的高并發壓力。

由圖1~3可知,3種水灰比的試件,經過凍融0次、25次和50次后,其峰值應力均隨著應變加載速率的增加而增加。

2.4 負載均衡

應對高并發訪問,負載均衡技術是構建高并發Web系統有效的方法[14]。常用負載均衡方法有:①DNS負載均衡;②NAT負載均衡;③軟件、硬件負載均衡;④反向代理負載均衡。①方法無法獲知各服務器差異,④方法在流量過大時服務器本身容易成為瓶頸,③方法中的軟件方式是通過在服務器上安裝軟件實現負載均衡,如LVS、PCL-SIS等。LVS是Linux虛擬服務器,從操作系統層面考慮,架構訪問層采用LVS及Nginx集群組成,分別在IP層和應用層進行請求的負載均衡轉發。通過OSI七層模型結構可知,在IP層實現請求的負載均衡比在應用層更加高效,減少了上層的網絡調用及分發。架構采用LVS的DR模式,由LVS作為系統整個流量的入口,采用IP負載均衡技術和基于內容請求分發技術,將請求均衡地轉發到不同的Nginx服務器上,但是只負責接收請求,結果由Nginx服務器直接返回,避免LVS成為網絡流量瓶頸。由多個客戶端發送的高并發請求,會先進行DNS尋址,找到對應機房的公網IP,由LVS接入,再將請求均衡的轉發到Nginx服務器,再由Nginx服務器將請求均衡的轉發到應用服務器,有利于Nginx服務器的擴展,可將整個系統進行水平拓展,加入更多的硬件支持,提高并發請求的處理效率。

3 測試與分析

為了驗證架構設計的有效性,選擇具有高并發資源競爭需求的選課場景為例,課程的額定容量就是共享資源,學生選課的過程其實就是搶占額定容量的過程。把案例分別部署在普通集群架構和本文提出的集群架構,基于內網同個網段搭建實驗平臺環境。普通集群采用Nginx技術實現負載均衡,使用了基于表記錄的新增與刪除操作,利用字段唯一性約束的方式實現數據庫分布式鎖以及使用Redis實現會話共享??紤]到測試的公平性以及簡化測試復雜度,保證兩個集群的物理配置以及應用配置參數一致,其中設置Nginx主要參數 keepAlivetimeout、fastcgi_connect_timeout、fastcgi_send_timeout、fastcgi_read_timeout均為8000,以服務器配置為準分配權重,設置 tomcat主要參數connectionTimeout為80 000,maxConnections為80 000,maxThreads為8000,minSpareThreads為100,maxIdleTime為6000。在測試中本文集群排除了LVS,直接以Nginx服務器作為請求負載均衡以及Redis單機方式完成整個測試流程。沒有引入LVS的原因是本次實驗是以搶課業務為測試場景,LVS的加入只是為了保障Nginx容錯性,且目前的并發數實在難以使其出現宕機情況,故剔除LVS的引入,選擇直接利用Nginx負載均衡、轉發的特性,對深層次的服務進行并發測試,更能獲取到架構內部整體的性能指標。

普通集群一共用了5個節點,其中節點2、3作為Tomcat服務節點,其余的分別為節點1(Nginx節點)、節點5(Mysql節點)、節點4(Redis節點)。本文集群與普通集群的區別是在節點4加入了RabbitMq,節點4 作為數據緩存、消息隊列和分布式鎖節點。兩個集群的各節點配置和容器版本等見表1。

表1 服務器節點配置

測試過程采用JMeter測試工具進行性能數據采集,通過不斷調高并發數量得到的各項性能指標值見表2和表3。測試過程中通過服務代理的方式監控節點機器,實時抓取各節點的資源使用情況,并重點記錄了核心節點的資源使用率,具體見表4和表5,列表頭的1-cpu表示節點1的CPU資源使用特征。由于篇幅所限,只羅列比較關鍵的數據。

表2 普通集群架構指標值

表3 本文集群架構指標值

表4 普通集群下的節點表現/%

表5 本文集群下的節點表現/%

根據不同并發數測試得到的數據,轉換成兩種集群架構的TPS和響應時間變化曲線,分別如圖5、圖6所示,并且將相應的節點表現轉換為曲線,如圖7、圖8所示。

圖5 TPS變化曲線

圖6 響應時間變化曲線

圖7 Memory變化曲線

圖8 CPU變化曲線

此次測試中并未針對Tomcat容器、數據庫連接池等進行過多的配置參數優化,相信后續加強對容器調優,架構的性能還有一定的上升空間。

4 結束語

本文從多角度挖掘高并發請求的痛點,從數據緩存、分布式鎖、消息隊列、負載均衡4個方面進行分析與研究,提出了一種適合高并發多線程競爭共享資源場景的高性能系統通用架構。架構基于Redis集群實現數據緩存,避免直擊數據庫;攔截失敗請求,提高系統吞吐量;采用分布式鎖,在系統垂直或水平擴展的情況下,保證同一時間的一個線程獲取到鎖,確保共享資源的安全性和一致性,同時采用基于cas算法的樂觀鎖方式避免Redis服務器與應用服務器之間交互流量過高導致服務器壓力過大;借助消息隊列組件進行異步處理操作,降低數據庫服務陷入堵塞的風險;從LVS調度轉發解決Nginx負載均衡的單點故障。本文架構面臨流量沖擊仍可維持服務高可用,組件服務皆可縱向擴展,具有廣泛的通用性,可適用于高校搶課、高峰訂票、商品秒殺等典型場景,對于構建高并發應用具有一定參考價值。

猜你喜歡
線程隊列消息
基于C#線程實驗探究
隊列里的小秘密
基于多隊列切換的SDN擁塞控制*
一張圖看5G消息
基于國產化環境的線程池模型研究與實現
線程池調度對服務器性能影響的研究*
在隊列里
豐田加速駛入自動駕駛隊列
消息
消息
91香蕉高清国产线观看免费-97夜夜澡人人爽人人喊a-99久久久无码国产精品9-国产亚洲日韩欧美综合