張淼鑫, 任新會, 黨蘭學, 侯彥娥
(1.河南大學 計算機與信息工程學院, 河南 開封 475004; 2.河南大學 教務處,河南 開封 475004)
從小型門戶網(wǎng)站、聊天系統(tǒng),到大型的“12306”購票系統(tǒng)和“雙十一”電商的秒殺系統(tǒng),都體現(xiàn)了高并發(fā)系統(tǒng)的迫切需求。除了“雙十一”不斷刷新的交易記錄外,另一個重要看點是各大電商平臺如何處理峰值時刻的高并發(fā)情況[1-2]。許多用戶都曾遭遇“系統(tǒng)繁忙”這類異常,說明高并發(fā)技術仍然面臨挑戰(zhàn),具有相當大的提升空間。同樣,電子資料平臺也會面臨各種高并發(fā)場景。例如,隨著在線學習的普及,當老師在某一時間吸引大量學生時,學生們需要同時訪問不同的課程材料、教科書或講義,這就構成了大規(guī)模的高并發(fā)讀取場景[3]。
早期典型的架構設計基于經(jīng)典的技術棧進行開發(fā),但是因為缺乏模塊化設計,受單機性能制約較大,導致高并發(fā)的處理性能較差[4]。隨著硬件性能的大幅提升,在對單機進行最優(yōu)設計的前提下,目前架構設計普遍采取的方法是對系統(tǒng)進行橫向擴展,搭建服務器集群,對合法的請求進行分流,并通過使用消息隊列對請求進行“削峰填谷”的處理。為使數(shù)據(jù)庫穩(wěn)定地處理這些請求,往往利用緩存中間件減少對數(shù)據(jù)庫的訪問,并通過服務降級,減輕峰值期間的訪問壓力[5-6]。本文基于多級緩存、熱點探測、分布式鎖、分布式事務等技術,設計了一套高并發(fā)解決方案,并將該方案應用于電子資料系統(tǒng)進行測試。測試結果表明,該方案可為電子資料系統(tǒng)提供卓越的穩(wěn)定性和可擴展性。
本文設計的高并發(fā)電子資料平臺,主要支持文檔收集、整理、檢索、共享、閱讀、下載、統(tǒng)計分析等主要功能,以及人員信息維護、部門信息維護、權限維護等輔助的系統(tǒng)功能。系統(tǒng)架構如圖1所示,核心組成部分包括文檔管理模塊、檢索與查詢模塊、共享與權限模塊、用戶管理模塊、數(shù)據(jù)統(tǒng)計與分析模塊。為支持高并發(fā)和大規(guī)模數(shù)據(jù)處理,系統(tǒng)引入了多級緩存,包括本地緩存、分布式緩存等,以減輕數(shù)據(jù)庫負載;采用熱點探測算法,以識別熱門文檔,減少系統(tǒng)瓶頸;使用分布式數(shù)據(jù)庫和分布式事務處理,以確保數(shù)據(jù)的一致性和可靠性; 應用分布式鎖機制,以協(xié)調多個并發(fā)操作,避免數(shù)據(jù)沖突。
圖1 系統(tǒng)架構
目前在高并發(fā)架構中使用較多的是分布式緩存,但是數(shù)據(jù)需要從遠程緩存中獲取,導致吞吐量下降,所以本系統(tǒng)使用的是多級緩存[7]。多級緩存主要分為3層,Nginx應用層緩存,Redis分布式緩存以及Tomcat的堆內緩存。緩存流程如圖2所示。
圖2 多級緩存流程圖
Nginx集群分為接入和應用兩種,通過接入Nginx將請求分發(fā)到應用Nginx。為了提升緩存的命中率,這里的負載均衡算法在正常情況下采用的是一致性哈希算法,但是如果訪問量到達了極限,就降級為輪詢算法。接著在應用Nginx中讀取本地緩存,本地緩存用Lua Shared Dict實現(xiàn),并結合頁面模板生成頁面。如果Nginx本地緩存已經(jīng)過期,那么系統(tǒng)將嘗試從Redis中獲取,同時更新Nginx的本地緩存。如果Redis中的數(shù)據(jù)被LRU(least recently used)算法清理掉,那么系統(tǒng)將發(fā)出HTTP(hypertext transfer protocol)請求到后端服務,數(shù)據(jù)生成服務首先在本地Tomcat的JVM(Java virtual machine)堆棧內存中查找,使用Ehcache緩存。如果JVM堆棧內存中的數(shù)據(jù)也被LRU清理掉,那么系統(tǒng)將重新請求源頭服務以獲取數(shù)據(jù),然后再次更新Tomcat堆內存緩存和Redis緩存,并將數(shù)據(jù)返回給Nginx,Nginx再將數(shù)據(jù)緩存到本地。其中各級緩存的目標不同,由于Nginx本地緩存的容量有限,所以該級緩存用于支持對熱點數(shù)據(jù)的高并發(fā)訪問;Redis分布式緩存容量很大,可以支持海量的緩存數(shù)據(jù),所以重點針對高離散數(shù)據(jù)的訪問;最后一級Tomcat堆內緩存,主要用于對抗Redis大規(guī)模宕機,大量請求涌入數(shù)據(jù)生產服務時產生的壓力。
系統(tǒng)除了要處理一些常用的熱點數(shù)據(jù)之外,更重要的是要處理特殊場合出現(xiàn)的熱點數(shù)據(jù)。常見的熱點數(shù)據(jù)會被提前放入多級緩存,特殊的數(shù)據(jù)一般要通過分析、計算,在Nginx緩存層進行處理,即對熱點數(shù)據(jù)的實時監(jiān)測和追蹤。對于熱點數(shù)據(jù)的監(jiān)測,可以使用數(shù)據(jù)調研,對歷史數(shù)據(jù)進行分析做出策略,但是這種方法的緩存命中率并不能達到100%[8]。本文使用實時計算對熱點數(shù)據(jù)進行監(jiān)測,如圖2所示。Flume框架主要用于海量數(shù)據(jù)聚合,Flink框架在集群環(huán)境中對有邊界或者無邊界的數(shù)據(jù)流進行計算,可對Flume采集的Nginx層日志信息處理分析。用Flume直接對接實時計算框架,如果數(shù)據(jù)采集速度遠大于處理速度的話,會造成消息丟失或堆積,所以在它們之間引入消息中間件,不僅可在業(yè)務邏輯上進行解耦,而且相當于對數(shù)據(jù)進行了分桶處理,進行數(shù)據(jù)隔離。這里集成Flume和Kafka完成日志處理,隨后使用Flink實時處理技術,統(tǒng)計某一時間內的文件訪問量、點擊率,從而實現(xiàn)目標的解析——將某一時刻訪問量大的實體數(shù)據(jù)標識為熱點數(shù)據(jù)。當這些數(shù)據(jù)被識別出來后,根據(jù)數(shù)據(jù)自身的特點將其放入多級緩存的各個節(jié)點。除此之外,為應對大流量的沖擊,需要對Tomcat集群,也就是該數(shù)據(jù)所對應的業(yè)務處理服務進行擴充。本系統(tǒng)使用Kubernetes,程序自動調整參數(shù)值,對其相關服務擴容。
高并發(fā)系統(tǒng)中另一個重要的問題,是多線程場景下對并發(fā)數(shù)據(jù)進行更改和讀取。在“先更新數(shù)據(jù)庫、再更新緩存”的設計方案中,存在兩個線程,即線程 A 和線程 B,它們同時操作同一條數(shù)據(jù)時可能出現(xiàn)以下情況:線程 A 更新數(shù)據(jù)庫(X=1),線程 B 更新數(shù)據(jù)庫(X=2),接著線程B更新了緩存(X=2),而線程 A 也更新了緩存(X=1)。最終在緩存中的值為1,而在數(shù)據(jù)庫中的值為2,導致數(shù)據(jù)不一致。同樣,使用“先更新緩存,再更新數(shù)據(jù)庫”的方法也可能面臨類似的問題。本系統(tǒng)通過加分布式鎖解決此問題。傳統(tǒng)的設計方案是用MySQL做分布式鎖,但是MySQL做鎖時要讀取磁盤IO(input output),當訪問量較大時,系統(tǒng)性能會比較低,所以本系統(tǒng)使用Redis作為分布式鎖[9]。
結合業(yè)務流程設置分布式鎖,將Redis的相關節(jié)點傳入對象,調用該對象的trylock(waitTime, leaseTime, unit)對其加鎖,其中waitTime代表請求鎖的等待時間,leaseTime代表鎖的有效期,unit 代表時間單位。以下為嘗試獲取鎖的偽代碼示例:
Public boolean trylock(waitTime, leaseTime, unit){
newLeaseTime = getLeaseMillis(leaseTime);
remainTime = getRemainMillis(waitTime);
lockWaitTime = calcLockWaitTime(remainTime);//每個等待實例時間與1比較取最大值
failedLocksLimit = failedLocksLimit();//允許獲取鎖失敗的次數(shù)
for(ListIterator
lockAcquired = lock.tryLock();
If(lockAcquired)
acquiredLocks.add(lock);//獲取到該鎖了,加入隊列
failedLocksLimt = failedLocksLimit();//獲取鎖失敗重試機制
}
rFuture.syncUninterruptibly();//key過期后用中斷方式釋放期鎖
}
算法依次在N個實例上嘗試獲取鎖,使用相同的鍵(key)和隨機值,當客戶端將鎖設置到Redis時,再設定網(wǎng)絡連接和響應的超時時間。超時時間應短于鎖的自動失效時間,以避免客戶端陷入等待服務器端Redis響應的情況(即使服務器端的Redis已經(jīng)失效)。如果服務器端未在規(guī)定時間內響應,客戶端應該盡快嘗試另一個Redis實例。客戶端可以通過將當前時間減去獲取鎖時的起始時間,計算獲取鎖所用的時間。只有當超過一半的Redis節(jié)點成功獲取了鎖,并且所用時間短于鎖的失效時間時,才被認為成功獲取了鎖。如果成功獲取了鎖,那么鍵的實際有效時間則等于有效時間減去獲取鎖所用的時間;如果由于某些原因未能成功獲取鎖,在至少N/2+1個Redis實例中未能獲取鎖,或者獲取鎖所用時間已經(jīng)超過了有效時間,則客戶端應該在所有Redis實例上執(zhí)行解鎖操作(即使在某些Redis實例上根本沒有成功加鎖)。圖3是加鎖過程的流程圖。
圖3 加鎖流程圖
分布式文件系統(tǒng)中的一種數(shù)據(jù)操作過程,其中一個請求可能需要調用多個服務,并在這些服務中的每一個都連接到各自的數(shù)據(jù)庫。所以當其中某一個事務執(zhí)行失敗,就會出現(xiàn)數(shù)據(jù)不一致的問題。傳統(tǒng)的解決方發(fā)是采用“兩階段”提交 ,但是“兩階段”提交中只有協(xié)調者設置了超時機制,而參與者沒有設置,所以當協(xié)調者宕機,接受消息的參與者也宕機時,事務的狀態(tài)就不確定了[10]。所以本系統(tǒng)使用的是“三階段”提交,在參與者中也引入超時機制,并且在第一階段和第二階段之間引入一個中間狀態(tài)解決該問題,圖4是“三階段”提交的狀態(tài)機。
圖4 “三階段”狀態(tài)機
這里將提交分為3個階段:①CanCommit,②PreCommit,③DoCommit;角色為協(xié)調者和參與者。在CanCommit階段,協(xié)調者開始寫本地日志,進入WAIT狀態(tài),同時向參與者發(fā)送VOTE_REQUEST消息并等待參與者的響應,參與者會響應VOTE_ABORT或者VOTE_COMMIT消息。在PreCommit階段,協(xié)調者會通知參與者準備提交或者取消事務,并寫REDO和UNDO日志,但不做提交。此時如果協(xié)調者接收到參與者的VOTE_ABORT消息,會寫GLOBAL_ABORT日志,進入ABORT狀態(tài),并向參與者發(fā)送GLOBAL_ABORT消息。如果收到的是參與者發(fā)送的VOTE_COMMIT消息,協(xié)調者將會寫PREPARE_COMMIT日志,并進入PRECOMMIT狀態(tài),再向所有的參與者發(fā)送PREPARE_COMMIT消息,之后等待并接收確認消息,一旦收到GLOBAL_ABORT,則寫日志流程結束,不進入下一階段,如果收到PREPARE_COMMIT,則進入DoCommit階段。該階段協(xié)調者會向所有的參與者發(fā)送GLOBAL_COMMIT消息,并接收參與者的GLOBAL_COMMIT確認消息,寫END_TRANSACTION日志流程結束,如果其中參與者無法接收到來自協(xié)調者的請求,會在超時等待后,繼續(xù)進行任務提交。
將本文設計的高并發(fā)解決方案應用于電子資料系統(tǒng),并使用JMeter執(zhí)行教學資源訪問的性能負載測試,通過逐漸增加每秒并發(fā)請求數(shù),評估系統(tǒng)的整體性能。結果發(fā)現(xiàn),在不同請求數(shù)量的并發(fā)負載下,系統(tǒng)始終保持相對穩(wěn)定,具有良好的穩(wěn)定性和準確性。表1是不同并發(fā)數(shù)的系統(tǒng)性能測試數(shù)據(jù),通信消耗時間約為2 ms,即使在有10 000個并發(fā)訪問時,服務調用時間的平均值也保持在300 ms以下。隨著并發(fā)量的增加,系統(tǒng)沒有出現(xiàn)任務異常,可見該高并發(fā)設計解決方案在處理大流量式數(shù)據(jù)時,在數(shù)據(jù)處理平均耗時和延遲時間等方面表現(xiàn)出了理想的性能。這一性能表現(xiàn)可以滿足大部分企業(yè)的數(shù)據(jù)處理需求。
表1 系統(tǒng)性能測試表
高并發(fā)讀寫場景的有效處理是一項具有挑戰(zhàn)性的任務,對于在線教學至關重要。通過合適的系統(tǒng)設計和技術選擇,能夠確保系統(tǒng)穩(wěn)定處理大規(guī)模的并發(fā)請求,在電子教學資料系統(tǒng)中提供高效的教學資源訪問,為學生和教育工作者提供良好的學習和教學體驗。需要綜合考慮緩存、負載均衡、數(shù)據(jù)庫優(yōu)化、CDN(content delivery network)等策略,以確保系統(tǒng)性能和數(shù)據(jù)一致性,為在線教學提供更多便利。