張旭東,蔣厚明,王 俊,陳星明
(南京南瑞信息通信科技有限公司,南京 210000)
隨著計算機技術迅猛發(fā)展和移動設備的普及,傳統(tǒng)的巨石架構系統(tǒng)已經不能滿足現(xiàn)有用戶訪問量指數(shù)級倍增的需求,系統(tǒng)處理高并發(fā)場景的能力已成為當前系統(tǒng)架構設計的核心指標和重要難題。單服務器設計由于軟硬件性能瓶頸問題,無法承載萬級及更高的用戶并發(fā)量,當大量用戶同時訪問系統(tǒng)服務時,往往會導致系統(tǒng)響應緩慢,甚至會因瞬間流量過大而導致系統(tǒng)崩潰,極大降低用戶使用體驗。
為了解決上述問題,分布式微服務架構應運而生。微服務架構核心思想是將原本復雜的單體應用系統(tǒng)根據(jù)業(yè)務功能拆分成多個服務,各業(yè)務服務可獨立存在且獨立部署,通過HTTP協(xié)議完成相互調用并對外提供完整系統(tǒng)功能。微服務系統(tǒng)架構可以根據(jù)各個微服務的訪問量進行微服務的水平擴展,提高系統(tǒng)的并發(fā)能力和吞吐量。各個微服務都可以單獨開發(fā)、部署,簡化了開發(fā)時間,但也增加了維護成本。微服務架構在解決設計問題的同時,也引入了許多不確定因素,原本一個服務可以執(zhí)行完所有流程,現(xiàn)在要調用多個微服務來協(xié)同工作。如果某個服務出現(xiàn)異?;蛘呔W(wǎng)絡出現(xiàn)擁堵,數(shù)據(jù)一致性和可靠性并不能得到保證,也會引發(fā)出一系列數(shù)據(jù)異常問題。因而需要對分布式系統(tǒng)的冪等性加以考慮。
為了解決分布式系統(tǒng)的冪等性問題,很多學者做了相關嘗試和研究。文獻[4]提出了使用數(shù)據(jù)庫鎖來保證系統(tǒng)冪等性,采用唯一索引和悲觀鎖的方式管理并發(fā)訪問;文獻[5]探討了使用Redis實現(xiàn)分布式鎖,各微服務通過競爭鎖獲取程序執(zhí)行權限,從而控制程序的并發(fā)訪問;文獻[6]對Redis實現(xiàn)分布式鎖進行了改進,使用了二次攔截,來保證程序的并發(fā)運行。
在設計分布式系統(tǒng)服務冪等方案時,要結合業(yè)務需求充分考慮高并發(fā)場景以及在該場景下服務接口的性能表現(xiàn),同時密切關注服務器各項性能指標及性能損耗。本文通過對多種冪等方法設計比較分析,提出一種改進的基于中間件的分布式鎖方法。實驗結果表明,該方法能夠保證在高并發(fā)場景下數(shù)據(jù)的準確性和一致性,且性能表現(xiàn)較好。
冪等(idempotence)是數(shù)學與計算機學概念,常見于抽象代數(shù)中。在編程中,一個冪等操作的特點是函數(shù)方法經過任意多次執(zhí)行所產生的影響與一次執(zhí)行所產生的影響相同。冪等函數(shù)或冪等方法是指可以使用相同參數(shù)重復執(zhí)行,并能獲得相同結果的函數(shù)。冪等函數(shù)不會影響系統(tǒng)狀態(tài),也不用擔心重復執(zhí)行會對系統(tǒng)造成任何改變。在實際系統(tǒng)中有很多操作,無論做多少次重復操作,系統(tǒng)都應該產生一樣的效果或返回一樣的結果。例如:①前端重復提交表單數(shù)據(jù),后臺服務應該只產生對應這條數(shù)據(jù)的一種響應結果;②同樣的消息應該只發(fā)送一次給用戶;③一次請求只能創(chuàng)建一個訂單等,很多重要場景都需要滿足服務冪等性。
分布式系統(tǒng)冪等性設計,可以從客戶端和服務端雙側考慮,由于系統(tǒng)冪等性問題的根本原因存是于服務端,所以從客戶端考慮意義不大,而且會影響用戶體驗。由上文可知,服務端冪等性設計方法有以下兩類:①對數(shù)據(jù)庫加鎖控制并發(fā)訪問;②在代碼層依賴第三方技術實現(xiàn)分布式鎖控制并發(fā)訪問。而第三方技術又有多種選擇,冪等設計方法隨著科學技術發(fā)展也在不斷進化。
對于系統(tǒng)服務的冪等性問題,主要出現(xiàn)在操作共享數(shù)據(jù)上,涉及數(shù)據(jù)庫及消息推送。對于數(shù)據(jù)庫操作來說,不涉及修改共享數(shù)據(jù)的查詢和刪除屬于冪等操作,不會引發(fā)數(shù)據(jù)一致性問題,而涉及修改共享數(shù)據(jù)的插入和更新操作則不符合冪等性,它們也是最容易出現(xiàn)冪等問題。對于消息推送存在的冪等性問題,主要發(fā)生在消費者在消費完一條消息后,要向消息發(fā)送方發(fā)送一個ack確認請求,如果此時由于網(wǎng)絡異?;蛘咂渌驅е孪l(fā)送方并沒有收到這個ack確認請求,那么此時消息發(fā)送方并不會將該條消息刪除,即使該條消息已經被消費者消費,當重新建立起連接后,消費者還是會再次收到該條消息,這就造成了消息的重復消費。由于類似的原因,消息在發(fā)送時,同一條消息也可能會發(fā)送多次,種種原因導致在消費消息時,也會存在冪等性問題。
由上文可知,可以通過為數(shù)據(jù)庫添加唯一索引和悲觀鎖等方法保證分布式系統(tǒng)的冪等性,還可借助第三方技術實現(xiàn)分布式鎖來解決系統(tǒng)冪等性問題,常用的第三方技術主要有Zoo-Keeper和Redis。
數(shù)據(jù)庫索引是一種輔助查詢的數(shù)據(jù)結構,該結構記錄數(shù)據(jù)表一列或多列值排序結果,類似于新華字典中音序表。信息搜索者通過索引可以快速高效地查詢并獲取符合條件的數(shù)據(jù),當然索引也需要占用數(shù)據(jù)庫資源,并不是索引構建越多,查詢越快。
數(shù)據(jù)庫索引類型包括:主鍵索引、普通索引、組合索引、全文索引和唯一索引。其中,唯一索引規(guī)定索引列的值必須唯一但可以為空,它不允許任意兩條數(shù)據(jù)具有相同索引值。當為已存在數(shù)據(jù)的表建立唯一索引時,若需要建立索引的列已經存在相同的鍵值,創(chuàng)建索引將會失敗,當向已建立唯一索引的數(shù)據(jù)表插入重復索引列的數(shù)據(jù)時也會失敗。
利用唯一索引上述特性可以保證數(shù)據(jù)表中每一條數(shù)據(jù)具有唯一性,故可以使用唯一索引來保證業(yè)務數(shù)據(jù)插入數(shù)據(jù)庫的唯一性和準確性,因此該方式可以作為系統(tǒng)冪等設計方法之一。在高并發(fā)場景下,通過對數(shù)據(jù)庫表添加唯一索引會使重復插入的數(shù)據(jù)插入失敗,但隨著時間推移,表數(shù)據(jù)量仍會增長,當表數(shù)據(jù)達到一定數(shù)量后,系統(tǒng)響應時間會相應增加。因為頻繁的I/O寫入將導致數(shù)據(jù)庫磁盤負載增加,數(shù)據(jù)庫性能開銷增大,在高并發(fā)場景下會給系統(tǒng)帶來比較大的壓力。
悲觀鎖(pessimistic locking)是基于數(shù)據(jù)庫事務實現(xiàn)的。在關系型數(shù)據(jù)庫中,事務有四個基本要素(ACID):原子性、一致性、隔離性和持久性。悲觀鎖認為,當某一事務操作數(shù)據(jù)資源時,其他事務也會操作該數(shù)據(jù)資源,所以在任何事務操作數(shù)據(jù)前都需要先對數(shù)據(jù)加鎖,在該事務操作數(shù)據(jù)期間,其他任何事務都不能操作該數(shù)據(jù),只有當前事務操作結束釋放鎖后,其他事務才能繼續(xù)操作數(shù)據(jù)資源。這種借助數(shù)據(jù)庫鎖機制在修改數(shù)據(jù)之前先鎖定、再修改的方式被稱之為悲觀并發(fā)控制,所以叫悲觀鎖。悲觀鎖從數(shù)據(jù)處理的安全性考慮,采用“先取鎖再訪問”的保守策略,但是在效率方面,悲觀鎖具有獨占和排他特性,某個事務處理占用鎖時,其它事務只能處于阻塞狀態(tài)。此外,悲觀鎖也會讓數(shù)據(jù)庫產生額外的開銷,還有增加產生死鎖的機會。同時還會降低數(shù)據(jù)庫并行性,只能適用于并發(fā)不高的場景。對于高并發(fā)場景下事務搶占資源效率并不是很高,嚴重時甚至會導致應用系統(tǒng)崩潰。
樂觀鎖(optimistic locking)是相對悲觀鎖而言,如果說悲觀鎖是一種避免沖突的手段,那樂觀鎖則是一種在最后提交數(shù)據(jù)時檢測沖突的手段。樂觀鎖并沒有借助數(shù)據(jù)庫事務和鎖機制,而是從系統(tǒng)應用層面和數(shù)據(jù)業(yè)務邏輯層面考慮,利用程序代碼處理并發(fā)問題。樂觀鎖主要是兩個步驟:沖突檢測和數(shù)據(jù)更新,其比較典型的實現(xiàn)方式是Compare and Swap(CAS)技術。
樂觀鎖假定當某一事務操作數(shù)據(jù)時,對其他事務操作該數(shù)據(jù)持樂觀態(tài)度。樂觀鎖通過對數(shù)據(jù)庫表增加“版本號”Version字段檢測事務沖突,在事務最后提交數(shù)據(jù)時會進行版本的檢查,以判斷在該事務操作過程中,是否有其他事務操作該數(shù)據(jù)。樂觀鎖只在更新數(shù)據(jù)那一刻鎖表,其他時間并不鎖表,減少了數(shù)據(jù)庫的壓力,所以相對于悲觀鎖,效率更高、開銷較小。
樂觀鎖大部分都是基于版本控制實現(xiàn)的,其工作過程如圖1所示。
圖1 樂觀鎖工作過程
A事務讀取數(shù)據(jù)并記錄當前Version為1,當A事務需要進行更新操作時,會將Version值自動加1更新為2;B事務依次執(zhí)行,B事務讀取數(shù)據(jù)時Version為2,更新數(shù)據(jù)時也會自動將Version值加1更新為3,此情況下A、B兩個事務提交不會發(fā)生沖突,流程如圖1左。如果A事務、B事務同時讀取同一條數(shù)據(jù)時的Version值是1,A事務先進行更新操作,將Version值自動加1變?yōu)?后立即進行更新操作,當B事務再執(zhí)行更新操作準備將Version更新為2時,此時已查詢不到Version值為1的數(shù)據(jù),則發(fā)生沖突,B事務更新失敗。
分布式鎖是解決分布式系統(tǒng)冪等性問題的又一方式。其核心思想是:所有服務實例通過競爭存放于同一個地方的同一把分布式鎖,服務示例通過競爭獲取鎖和釋放鎖,最終完成分布式系統(tǒng)的并發(fā)訪問。實現(xiàn)分布式鎖需借助第三方技術,常用的技術主要有Redis和Zookeeper等,本文主要基于Zookeeper實現(xiàn)分布式鎖,對于Redis實現(xiàn)分布式鎖給出實驗對比數(shù)據(jù)。
Zookeeper是使用層次樹型結構的命名空間數(shù)據(jù)模型,類似Unix系統(tǒng)文件目錄樹結構,如圖2所示。
圖2 Zookeeper數(shù)據(jù)模型
層次樹中的每個節(jié)點稱為一個Znode,所有Znode都可以儲存信息,并且所有Zonde都可以擁有子Znode,臨時節(jié)點除外。Zookeeper中的Znode有三類:①永久節(jié)點(persistent node),此類節(jié)點在創(chuàng)建完成后將永久存在,除非Client手動顯式刪除,否則該類節(jié)點將永久儲存于Zookeeper中;②臨時節(jié)點(ephemeral node),顧名思義,該類節(jié)點臨時有效,只有Client與Server保持連接時存在,一旦二者連接中斷,Zookeeper會自動清除此類型節(jié)點;③順序節(jié)點(sequence node),此類節(jié)點具有先后順序,當Zookeeper在創(chuàng)建該類型節(jié)點時,會自動在節(jié)點名稱末尾補充一個遞增序列,節(jié)點序列遞增且不會重復。例如,客戶端申請創(chuàng)建子節(jié)點“/lock/node-”并且指明有序,那么Zookeeper在生成子節(jié)點時會根據(jù)當前的子節(jié)點數(shù)量自動添加整數(shù)序號,也就是說,如果是第一個創(chuàng)建的子節(jié)點,那生成的子節(jié)點為“/lock/node-00000000”,下一個節(jié)點則為“/lock/node-00000001”,依次類推。
Zookeeper使用臨時節(jié)點和有序節(jié)點再配合Zookeeper提供的事件監(jiān)聽機制可以實現(xiàn)分布式鎖。Zookeeper在讀取Znode存儲信息,判斷Znode是否存在,獲取Znode子節(jié)點等操作時都可以設置相應的事件監(jiān)聽,此事件監(jiān)聽是一次性觸發(fā)器,當被監(jiān)聽數(shù)據(jù)發(fā)生改變時,服務器會向設置相應監(jiān)聽器的Client發(fā)送通知,但該通知并不會包含本次操作改變的內容。Zookeeper實現(xiàn)分布式鎖的具體步驟如下,流程如圖3所示。
圖3 Zookeeper分布式鎖流程圖
(1)創(chuàng)建一個鎖目錄“/lock”;
(2)客戶端A獲取鎖,會在“/lock”目錄下創(chuàng)建臨時順序節(jié)點,獲取鎖目錄下所有的子節(jié)點,然后獲取比自己小的兄弟節(jié)點,如果不存在,則說明當前線程順序號最小,獲得鎖;
(3)客戶端B創(chuàng)建臨時節(jié)點并獲取所有兄弟節(jié)點,判斷自己不是最小節(jié)點,設置監(jiān)聽(watcher)比自己次小的節(jié)點,這里客戶端B只關注比自己次小的節(jié)點是為了防止發(fā)生“羊群效應”;
(4)客戶端A處理完,刪除自己的節(jié)點,客戶端B監(jiān)聽到變更事件,判斷自己是最小的節(jié)點,獲得鎖;
正常情況下,上述分布式鎖是可以滿足需求的,但仍然存在兩個問題:
由于網(wǎng)絡出現(xiàn)異?;蛘呔€程GC停頓導致Zookeeper服務端長時間檢測不到客戶端心跳,使得臨時節(jié)點被刪除,鎖被提前釋放,但此時業(yè)務邏輯可能會依然并發(fā)執(zhí)行。如果改為使用永久節(jié)點,會出現(xiàn)因為會話異常關閉導致死鎖問題,具體如圖4所示。
圖4 分布式鎖異常流程圖
Zookeeper環(huán)境部署時既支持單機架構部署也支持集群架構部署。顯而易見,單機架構部署會存在一定風險,因為只要Zookeeper服務出現(xiàn)異常,整個系統(tǒng)也將變得不可訪問,此外,該部署方式對服務器配置要求較高,因此Zookeeper集群部署是需要考慮的。由于Zookeeper采用稱為Quorum Based Protocol的數(shù)據(jù)同步協(xié)議,該同步協(xié)議決定了Zookeeper數(shù)據(jù)同步的不強一致。假如Zookeeper集群有臺Zookeeper服務器,客戶端的一個寫操作,首先會同步到/2+1臺服務器上,然后返回給客戶端并提示寫成功。因為Zookeeper是同步寫/2+1個節(jié)點,還有/2個節(jié)點沒有同步更新,所以在集群環(huán)境部署情況下,Zookeeper存在數(shù)據(jù)不是強一致情況。
對于上面闡述的兩個問題,現(xiàn)提出一種改進的分布式鎖方法。在業(yè)務代碼中,根據(jù)對業(yè)務操作數(shù)據(jù)庫的類型進行攔截判斷,如果是插入、更新數(shù)據(jù)等操作,則在數(shù)據(jù)庫層面對數(shù)據(jù)表再建立數(shù)據(jù)鎖或唯一索引,增加異常情況下對分布式鎖的二次攔截。這樣可以有效避免因網(wǎng)絡異?;蛘遉ookeeper集群環(huán)境數(shù)據(jù)同步非強一致性問題而導致分布式鎖被提前釋放,核心業(yè)務代碼依然并發(fā)執(zhí)行的問題。具體改進步驟如下:對所有業(yè)務邏輯操作數(shù)據(jù)表的類型進行攔截判斷,若操作類型是插入數(shù)據(jù),則為該數(shù)據(jù)表建立唯一索引,以此解決業(yè)務數(shù)據(jù)多次插入問題;若業(yè)務操作類型是更新操作,則為相應數(shù)據(jù)表建立樂觀鎖,確保更新數(shù)據(jù)的正確性和準確性。由上文知,這里數(shù)據(jù)庫鎖選擇采用樂觀鎖是因為悲觀鎖屬于阻塞模式類型,無論是更新還是插入操作都會鎖定整張數(shù)據(jù)表,不適合用于高并發(fā)場景下數(shù)據(jù)的操作,而樂觀鎖對數(shù)據(jù)操作持樂觀態(tài)度,并不會對數(shù)據(jù)表進行加鎖處理,從而降低了數(shù)據(jù)庫的損耗,在高并發(fā)場景下對數(shù)據(jù)庫性能有一定提升。同時,將二次攔截失敗的業(yè)務操作存入消息隊列,以消息形式推送給運維人員及時排查問題。如果是網(wǎng)絡異??梢约皶r進行備機切換,如果是Zookeeper集群環(huán)境節(jié)點故障,可以迅速定位到Zookeeper集群問題。為了防止通知消息發(fā)生同步阻塞現(xiàn)象而影響核心業(yè)務處理,所以通知消息考慮以異步方式發(fā)送,基于此,消息隊列選用RabbitMQ實現(xiàn)消息的生產和消費。改進的分布式鎖設計整體工作過程如圖5所示。
圖5 基于Zookeeper改進的分布式鎖
下面通過使用JMeter測試工具模擬高并發(fā)場景,并驗證改進的分布式鎖在高并發(fā)場景下的數(shù)據(jù)一致性和性能表現(xiàn)。
服務器配置:CentOS7系統(tǒng),Intel Xeon E-2388G 8核16 G內存實驗軟件環(huán)境基于Docker容器實現(xiàn):Nginx服務、Zookeeper集群服務(5個節(jié)點)、多個微服務實例、RabbitMQ服務、MySQL數(shù)據(jù)庫。為了與Redis測試對比,同時部署Redis集群(5個節(jié)點)。
通過JMeter設置2000個并發(fā)線程數(shù),秒殺1000個商品庫存場景,使用唯一索引、樂觀鎖、悲觀鎖、Redis分布式鎖、Zookeeper分布式鎖五種冪等方法分別獨立測試,查看各個方法的性能表現(xiàn)及數(shù)據(jù)一致性,最后再對改進的Zookeeper分布式鎖進行測試,分別模擬在發(fā)生網(wǎng)絡異常及Zookeeper集群環(huán)境某個節(jié)點宕機情況下分布式鎖的性能問題。更新操作類型測試結果參見表1,插入操作類型測試結果參見表2。
表2 各冪等方法插入操作性能對比
在結果性能指標中,攔截次數(shù)及成功次數(shù)說明冪等方法保證數(shù)據(jù)準確性和一致性的能力,總耗時和平均響應時間反映了高并發(fā)場景下冪等方法的性能開銷。表1實驗結果表明:唯一索引、悲觀鎖和樂觀鎖都可以解決冪等性問題,但它們的平均響應時間較長,性能較低,系統(tǒng)開銷較大。Redis分布式鎖相對于前面三種方法性能有較大提升,改進Zookeeper分布式鎖與Redis性能相當。當斷開網(wǎng)絡或者關閉Zookeeper集群中一個節(jié)點和Redis集群中一個節(jié)點時,Redis鎖和Zookeeper鎖出現(xiàn)數(shù)據(jù)不一致的情況,而改進后的Zookeeper鎖數(shù)據(jù)一致性得到保證,同時RabbitMQ消費者收到多條推送過來的消息。
表1 各冪等方法更新操作性能對比
表2實驗結果表明,對于插入業(yè)務操作類型,鎖的二次攔截性能方面滿足預期效果。同樣斷開網(wǎng)絡或者關閉Zookeeper集群中一個節(jié)點和Redis集群中一個節(jié)點時,Redis鎖和Zookeeper鎖執(zhí)行數(shù)據(jù)操作時會存在重復數(shù)據(jù),改進后的Zookeeper鎖數(shù)據(jù)一致性達到預期,雖然改進后的Zookeeper鎖平均響應時間增加了,但數(shù)據(jù)一致性得到了保證,犧牲毫秒級的時間來保證數(shù)據(jù)一致性,此結果是可以令人接受的。
綜上所述,唯一索引方式與悲觀鎖方式性能接近,數(shù)據(jù)一致性也能得到保證,但效率較低;樂觀鎖效率好于前兩者,相比Redis分布式鎖和Zookeeper分布式鎖,性能又提升了不少,但數(shù)據(jù)一致性并不能保證;改進后的Zookeeper分布式鎖犧牲毫秒性能卻換來數(shù)據(jù)一致性,滿足實際需求。
本文通過對微服務架構模式的分析,引出了服務冪等性問題。隨后通過分析服務冪等性問題的原理及現(xiàn)有的解決方案,具體研究了Zookeeper分布式資源鎖的實現(xiàn),并對Zookeeper服務器處理過程進行分析,提出了改進的分布式鎖。根據(jù)對業(yè)務操作類型做判斷,進行鎖的二次攔截,解決因網(wǎng)絡異?;蚣汗?jié)點異常而導致數(shù)據(jù)不一致問題,改進的分布式鎖方案能夠保證服務冪等性和資源的強一致性,滿足大多數(shù)應用的需要。最后通過實驗模擬高并發(fā)場景,測試證明了優(yōu)化方案的有效性。