最近在做線上架構的實作,線上架構和離線架構近線架構最大的差別是服務品質(sla,service level agreement,sla 99.99代表10k的請求最多一次失敗或者逾時)和延時。而離線架構在意的是吞吐,sla的不會那麼嚴苛,比如99.9。離線架構一般要有流控,以控制使用者發送請求的速度。以免多于服務端處理能力的請求造成大量的資料在buffer或者隊列裡堆積,造成大量的逾時。線上架構不可能有流控了,你不能限制使用者的請求。是以線上架構對于彈性擴容有很高的要求,在大量請求到來時自動擴充背景的服務能力。比如目前的請求已經占用了叢集的70%的資源時,系統需要自動的擴容;相反,目前的請求僅僅占用了叢集20%的資源時,有必要回收一部分資源了。要知道,公司機房的電費還是很貴的。
當然了線上和離線架構的相同和差別談起來完全是一個大文章。本文主要關注在處理高并發請求的鎖的使用上。幾個原則吧:
不要使用全局鎖。使用全局鎖代表在需要請求鎖時,其他為得到鎖的線程都會等待,這将導緻服務能力急劇下降。
一定要注意鎖的作用範圍,一定要保證鎖作用于足夠小的範圍。一定不要在鎖定區域有等待操作,比如io調用。
盡量的考慮修改架構,避免加鎖。
試想一個場景,為了服務品質,我們可能發送多個請求到背景,以達到:
高可用行,背景的某個節點挂了,有其他的backup request會被請求。如果節點的sla是99%(很低了),那麼發送2個請求到背景,sla可以達到99.99%;如果單個節點的sla是99.9%的話,sla可以達到99.9999了,即百萬次請求至多一次失敗。
低延時,第一個回來的請求會響應,這樣的話能夠保證某些慢的節點不會影響系統整體的延時。
那麼如何判斷第一個請求是第一個達到的呢?
先想一個比較粗暴的辦法:使用一個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)