解決一個互斥問題,系統并發用戶數提升了10倍!
互斥鎖在實現并發場景下業務操作的原子性以及解決互斥訪問問題方面,是極為有效的手段之一。因其使用方式相對簡單且安全,所以在互聯網的分布式系統以及嵌入式并發場景中都有著廣泛的應用。
然而,若互斥鎖的選擇與使用不當,極有可能成為系統性能的瓶頸之一。因此,對互斥鎖進行合理優化,是系統性能優化的重要途徑。我將為你分享一個互聯網場景下互斥鎖優化的案例。按照優化前的軟件實現、性能瓶頸分析以及優化解決方案的思路,帶你深入剖析我是如何優化業務中的互斥鎖,以及如何將業務的 RPS(Requests per second,請求吞吐量)性能指標提升 10 倍以上的。
在這個案例中夠全面了解分析與優化互斥鎖的詳細過程,從而在業務中準確識別出哪些場景下的互斥鎖可以優化,哪些場景下不可以。此外,你還將掌握一種手動實現事務的機制(支持業務操作回滾機制),以此替代業務中的互斥鎖,進一步優化軟件性能。
接下來,我們先看看該案例中業務優化前的實現情況以及存在的性能問題。
優化前的業務實現為什么會有性能問題?
這個性能優化案例的業務場景如下:用戶向在線表單提交一條記錄,該記錄包含眾多字段內容。其中,部分字段在插入時有一個規則要求,即不能與已有的字段值重復。為了便于理解,這里我用一張圖來描述原業務系統中實現字段插入值不重復規則的實現邏輯,具體情況如下所示。
圖片
可見,在該業務中使用了一個 Redis 鎖來實現互斥訪問,從而實現了被加鎖的業務邏輯執行的原子性,所以這部分計算邏輯在系統中是串行執行的。而被加鎖的業務邏輯主要有三個關鍵操作,分別是:
一、字段不重復檢測
對插入的字段值進行檢測,查看其在數據庫中是否有重復情況。若出現重復值,則插入失敗并直接退出;若未出現重復值,則執行下一步操作。在這個過程中,系統會遍歷所有要求值不能重復的字段項,只要其中任何一個字段項出現值重復,就會退出。
二、其他操作
即用戶提交記錄過程中的一些關鍵業務操作。這些操作具有不能被拆分執行且不能被回滾的特點。若操作成功,則執行下一步操作;否則,也會直接退出。
三、所有字段插入
由于上述三個操作通過加鎖保證了原子性執行,所以前面檢測的 “字段值不重復” 的條件仍然有效。在這一步,會將有的字段進行插入。
除此之外,在優化前的代碼實現中,需要進行重復性校驗的字段都會記錄在 Redis 中。所以,圖中的操作 1、操作 3 都是基于 Redis 來實現的。
在看完這個業務實現邏輯圖后,你或許會感到好奇:這種字段唯一性檢測機制為何不使用關系數據庫中的字段唯一性檢測機制來實現呢?這確實是個好問題,我在剛看到這個業務邏輯實現時也同樣好奇,后來深入分析業務后才理解其如此實現的原因。
實際上,在這個業務系統中大約有 1000 萬張表單,且每張表單的字段唯一性規則可能各不相同,用戶還能隨意修改這個規則。所以,該系統在設計實現時,將所有表單中的所有字段都放到了一張很大的數據庫表中,因此無法使用數據庫表上的字段唯一性規則來處理這個問題。
原來的 Redis 加鎖實現方式較為簡單,且是按照單個表單來進行加鎖的,所以在單個表單并發提交請求吞吐量不是很大的情況下,不會對系統性能產生太大影響。
然而,隨著系統業務規模逐漸增大,會出現少量表單的并發請求吞吐量暴增的情況。此時,當單個表單提交請求超過并發請求吞吐量的上限值后,就會引發兩個較為嚴重的性能問題:
其一,針對超過并發請求吞吐量性能上限值的那個表單,用戶在提交表單的頁面會出現卡死現象,導致提交數據失敗;
其二,由于后端服務系統是基于進程模型的,而進程資源的數目有限,一旦個別表單提交數據請求的處理進程被阻塞,占用大量進程資源,就會導致整個系統無法正常處理所有的業務請求。
因此,提升單個表單提交請求吞吐量的性能指標,就成為了這個軟件系統性能優化的關鍵問題。那么接下來,我們就要先搞明白,這個互斥鎖是如何影響這個表單的請求吞吐量性能的。
互斥鎖是如何影響最大請求吞吐量的?
接下來,我就使用一個公式來描述下在這個案例中,使用了 Redis 互斥鎖以后,來計算 Max RPS(最大請求吞吐量)的計算方法,具體公式如下所示:
圖片
在這個公式中,由于 Lock 和 Unlock 是通過 Redis 的互斥鎖來實現的,其使用的 Redis 的 script 腳本實現如圖所示。經在真實系統中測量,Lock time 與 Unlock time 的操作時間之和約為 3ms。接著,可通過上面的公式進行計算。若中間加鎖的計算邏輯(resource competition)執行開銷約為 30ms,那么對應的 Max RPS = 1s / (3ms + 30ms),即大約為 30RPS 左右。也就是說,只有當把加鎖的計算邏輯降低極限值為 0 時,對應的 Max RPS 才可以達到 300RPS 左右。這里需要注意的是,因為業務中的互斥鎖是全局控制的,所以當系統達到最大 RPS 時,即便通過彈性擴展機制部署再多的后端服務實例進程,也無法再提升這個性能指標了。
至此,在這個性能優化案例中,我們經過測量得知加鎖的計算邏輯執行時間為 30RPS,然后根據上面的公式,計算出的最大 RPS 值也約為 30RPS 左右,這與真實的性能測試獲取的性能指標值完全一致。
好的,現在問題已經比較清楚了。那么,有沒有辦法可以優化提升這個系統的性能呢?下面我們來看一下。
性能優化解決方案
果這個業務邏輯沒有增加互斥鎖,在 99.9% 的情況下業務邏輯也是正確的。
所以,針對這種場景,我們可以采用手動實現事務機制,優化掉業務代碼中的互斥鎖,以提升請求吞吐量的性能。
我們已知在這個案例中,使用互斥鎖解決的核心問題是判斷字段不重復和字段插入操作的原子性問題。因此,我們可以考慮采用一些優化機制,單獨實現這兩個操作組合的原子性。
但要注意,如果在互斥鎖的使用場景中,被加鎖的業務操作還有更復雜的一致性要求,比如存在數據庫寫沖突的問題等,那么這種互斥鎖實現就不能被簡單地優化掉了。
那么對于這個案例中的互斥鎖而言,我們應該怎樣優化呢?
我來說說我想到的優化思路。這里呢,為了更清晰地描述該解決方案,我用了一個流程圖來給你詳細地介紹下性能優化后的具體實現過程,如下圖所示。
圖片
也就是說,我們可以將 “字段不重復檢測”“單個字段插入”“其他操作” 這三個操作綁定在一起,實現一種事務機制的能力,以便在后面操作失敗的情況下,能夠回滾到前面的操作中。實際上,原來的 Redis 互斥鎖主要是為了實現 “字段不重復檢測” 和 “字段的插入操作” 的原子性。而在手動實現事務機制之后,我們可以把這兩步操作放到開始處執行,然后使用 Redis 的 Pipeline 機制保證這兩步操作組合的原子性,從而不會被其他 Redis 操作干擾。
這樣,對于接下來的其他操作(即用戶提交數據過程中的一些不可拆分的關鍵業務操作),如果操作成功,就提交任務成功結束;如果操作失敗,則需要回滾之前的字段插入操作。另外,為了實現事務的機制和能力,我們還需要在前面字段插入時,同時記錄插入前的狀態和插入后的變更狀態,從而實現失敗后的回滾機制。
其實,這里我還考慮過另外兩種實現方案,分別是基于 Redis 的事務機制和基于 MongoDB 上的事務機制。但是,我最后在實現時并沒有采納,這背后有很多原因。比如,使用 MongoDB 的事務需要進行數據遷移,而且需要升級系統的 MongoDB 集群的數據庫版本等。以及使用 Redis 事務機制的代碼實現并不友好等等。不過,這里有一個最重要的原因就是,不管是使用 Redis 事務還是 MongoDB 上的事務,它們都把對字段插入操作的沖突時間,拉長到了步驟 3 “其他操作” 結束之后,而這樣就顯著增大了事務沖突失敗的概率。
所以最后,我們采用前面這種優化后的實現機制。因為去除了互斥鎖,所以用戶間的提交記錄可以更大程度地并行。而且優化后的實現方式,只有 Pipeline 操作會排隊處理,而由于單個 Pipeline 的執行時長在 1ms - 3ms 之間,所以最后優化后的表單最大請求吞吐量,就從原來的 30RPS,提升到了 300RPS 左右,這樣就實現了性能提升超過 10 倍的目標。