最近在做在線架構的實現,在線架構和離線架構近線架構最大的區別是服務質量(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 支持調度給哪個線程。問題解決。
睡覺。