海量併發的無鎖編程 (lock free programming)

最近在做在線架構的實現,在線架構和離線架構近線架構最大的區別是服務質量(SLA,Service Level Agreement,SLA 99.99代表10K的請求最多一次失敗或者超時)和延時。而離線架構在意的是吞吐,SLA的不會那麼嚴苛,比如99.9。離線架構一般要有流控,以控制用戶發送請求的速度。以免多於服務端處理能力的請求造成大量的數據在buffer或者隊列裏堆積,造成大量的超時。在線架構不可能有流控了,你不能限制用戶的請求。因此在線架構對於彈性擴容有很高的要求,在大量請求到來時自動擴展後臺的服務能力。比如當前的請求已經佔用了集羣的70%的資源時,系統需要自動的擴容;相反,當前的請求僅僅佔用了集羣20%的資源時,有必要回收一部分資源了。要知道,公司機房的電費還是很貴的。

當然了在線和離線架構的相同和區別談起來完全是一個大文章。本文主要關注在處理高併發請求的鎖的使用上。幾個原則吧:

  1. 不要使用全局鎖。使用全局鎖代表在需要請求鎖時,其他爲得到鎖的線程都會等待,這將導致服務能力急劇下降。
  2. 一定要注意鎖的作用範圍,一定要保證鎖作用於足夠小的範圍。一定不要在鎖定區域有等待操作,比如IO調用。
  3. 儘量的考慮修改架構,避免加鎖。

試想一個場景,爲了服務質量,我們可能發送多個請求到後臺,以達到:

  1. 高可用行,後臺的某個節點掛了,有其他的backup request會被請求。如果節點的SLA是99%(很低了),那麼發送2個請求到後臺,SLA可以達到99.99%;如果單個節點的SLA是99.9%的話,SLA可以達到99.9999了,即百萬次請求至多一次失敗。
  2. 低延時,第一個回來的請求會響應,這樣的話能夠保證某些慢的節點不會影響系統整體的延時。

那麼如何判斷第一個請求是第一個達到的呢?

先想一個比較粗暴的辦法:使用一個set記錄未返回的request 的id,然後在接到響應時,查看這個set有沒有這個id,如果有,刪除它,並且響應client;第二個以後的響應達到時,由於在set已經沒有這個id了,因此這些請求將被丟棄。

這個裏邊涉及到對set的讀和寫操作,這個需要加鎖;如果這個set是進程內可見的,那麼這個鎖就是進程級別的(或者說該進程或者說是線程的子線程都是可見的),加鎖時很多線程都會等待該鎖。這樣的話對性能會有很大損耗。

這個方法對於每秒幾百次請求是沒有問題的。但是如果達到千這個級別,那麼鎖的使用會達到數千次(比如1000個請求,發送3個請求到後臺,那麼每次寫set加一次鎖,3個請求回來都會加一次鎖,因此相當於一個真實的請求會加鎖4次,1000個請求就是4000次,想想都恐怖,1s要加鎖4000次,鎖的代價再小也很恐怖吧,別說set的插入和查詢,刪除也有不可忽略的性能損耗)。

那麼可不可以加線程級別的鎖?線程級別的鎖會減少對其他線程的影響。但是,set如果也是線程級別的,那麼得保證異步回調的藉口也得是在同一個線程纔可以。否則這個線程發出的請求,被其他的線程得到,那麼上述的邏輯是不通的,因爲set是線程級別的,對於其他線程來說是不可見的。這樣的話如果架構能夠保證一個異步請求的返回,也是在同一個線程處理就好了。那麼,如果架構可以這麼保證,那麼你根本不需要鎖,爲什麼呢?因爲一個線程都是順序執行的,不會有資源的競爭,因此讀寫set都是安全的,因此不需要加鎖。

那麼問題來了,架構如何支持這個異步回調也是走到相同的線程裏?

一個實現就是實現一個線程池,對於特定的request id,基於一定的規則將他調度給一個工作線程;等到異步返回時,再通過這個request id調度給相同的線程處理。

那麼如何實現一個線程池?boost 裏有; 如果調度,boost 支持調度給哪個線程。問題解決。

睡覺。


當然了,你以爲無鎖編程會涉及CAS,那麼可以移步 併發編程(三): 使用C++11實現無鎖stack(lock-free stack)


發佈了105 篇原創文章 · 獲贊 90 · 訪問量 224萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章