1. 程式人生 > >聊聊高併發之隔離術

聊聊高併發之隔離術

隔離是指將系統或資源分割開,系統隔離是為了在系統發生故障時能限定傳播範圍和影響範圍,即發生故障後不會出現滾雪球效應,從而保證只有出問題的服務不可用,其他服務還是可用的;而資源隔離有髒資料隔離、通過隔離後減少資源競爭提升效能等。我遇到的比較多的隔離手段有執行緒隔離、程序隔離、叢集隔離、機房隔離、讀寫隔離、動靜隔離、爬蟲隔離等。而出現系統問題時可以考慮負載均衡路由、自動/手動切換分組或者降級等手段來提升可用性。

執行緒隔離

執行緒隔離主要有執行緒池隔離,在實際使用時我們會把請求分類,然後交給不同的執行緒池處理,當一種業務的請求處理髮生問題時,不會將故障擴散到其他執行緒池,從而保證其他服務可用。

高併發

我們會根據服務等級劃分兩個執行緒池,以下是池的抽象:

<bean id="zeroLevelAsyncContext" class="com.jd.noah.base.web.DynamicAsyncContext" destroy-method="stop">
    <property name="asyncTimeoutInSeconds" value="${zero.level.request.async.timeout.seconds}"/>
    <property name="poolSize" value="${zero.level.request.async.pool.size}"/>
    <property name="keepAliveTimeInSeconds" value="${zero.level.request.async.keepalive.seconds}"/>
    <property name="queueCapacity" value="${zero.level.request.async.queue.capacity}"/>
</bean>
<bean id="oneLevelAsyncContext" class="com.jd.noah.base.web.DynamicAsyncContext" destroy-method="stop">
    <property name="asyncTimeoutInSeconds" value="${one.level.request.async.timeout.seconds}"/>
    <property name="poolSize" value="${one.level.request.async.pool.size}"/>
    <property name="keepAliveTimeInSeconds" value="${one.level.request.async.keepalive.seconds}"/>
    <property name="queueCapacity" value="${one.level.request.async.queue.capacity}"/>
</bean>

程序隔離

在公司發展初期,一般是先進行從0到1,不會一上來就進行系統的拆分,這樣就會開發出一些比較大而全的系統,系統中的一個模組/功能出現問題,整個系統就不可用了。首先想到的解決方案是通過部署多個例項,然後通過負載均衡進行路由轉發,但是這種情況無法避免某個模組因BUG而出現如OOM導致整個系統不可用的風險。因此此種方案只是一個過渡,較好的解決方案是通過將系統拆分為多個子系統來實現物理隔離。通過程序隔離使得某一個子系統出現問題不會影響到其他子系統。

隔離術

叢集隔離

隨著系統的發展,單例項服務無法滿足需求了,此時需要服務化技術,通過部署多個服務,形成服務叢集來提升系統容量,如下圖所示

執行緒隔離

隨著呼叫方的增多,當秒殺服務被刷會影響到其他服務的穩定性,此時應該考慮為秒殺提供單獨的服務叢集,即為服務分組,從而當某一個分組出現問題不會影響到其他分組,從而實現了故障隔離,如下圖所示

程序隔離

比如註冊生產者時提供分組名:

<jsf:provider id="myService" interface="com.jd.MyService" alias="${分組名}" ref="myServiceImpl"/>

消費時使用相關的分組名即可:

<jsf:consumer id="myService" interface="com.jd.MyService" alias="${分組名}"/>

機房隔離

隨著對系統可用性的要求,會進行多機房部署,每個機房的服務都有自己的服務分組,本機房的服務應該只調用本機房服務,不進行跨機房呼叫;其中一個機房服務發生問題時可以通過DNS/負載均衡將請求全部切到另一個機房;或者考慮服務能自動重試其他機房的服務從而提升系統可用性。

叢集隔離

一種辦法是根據IP(不同機房IP段不一樣)自動分組,還一種較靈活的辦法是通過在分組名中加上機房名解決:

<jsf:provider id="myService" interface="com.jd.MyService" alias="${分組名}-${機房}" ref="myServiceImpl"/>
<jsf:consumer id="myService" interface="com.jd.MyService" alias="${分組名}-${機房}"/>

讀寫隔離

如下圖所示,通過主從模式將讀和寫叢集分離,讀服務只從從Redis叢集獲取資料,當主Redis叢集出現問題時,從Redis叢集還是可用的,從而不影響使用者訪問;而當從Redis叢集出現問題時可以進行其他叢集的重試。

故障隔離

--先讀取從
status, resp = slave_get(key)
if status == STATUS_OK then
    return status, value
end
--如果從獲取失敗了,從主獲取
status, resp = master_get(key)

動靜隔離

當用戶訪問如結算頁時,如果JS/CSS等靜態資源也在結算頁系統中時,很可能因為訪問量太大導致頻寬被打滿導致出現不可用。

機房隔離

因此應該將動態內容和靜態資源分離,一般應該將靜態資源放在CDN上,如下圖所示

讀寫隔離

爬蟲隔離

在實際業務中我們曾經統計過一些頁面型應用的爬蟲比例,爬蟲和正常流量的比例能達到5:1,甚至更高。而一些系統是因為爬蟲訪問量太大而導致服務不可用;一種解決辦法是通過限流解決;還一種解決辦法是在負載均衡層面將爬蟲路由到單獨叢集,從而保證正常流量可用,爬蟲流量儘量可用。

動靜隔離

比如最簡單的使用Nginx可以這樣配置:

set $flag 0; 
if ($http_user_agent ~* "spider") { 
    set $flag "1"; 
} 
if($flag = "0") {
    //代理到正常叢集
}
if ($flag = "1") { 
    //代理到爬蟲叢集
}

實際場景我們使用了Openresty,不僅僅對爬蟲user-agent過濾,還會過濾一些惡意IP(統計IP訪問量,配置閥值),將他們分流到固定分組。還有一種辦法是種植Cookie,訪問特殊服務前先種植Cookie,訪問服務時驗證該Cookie,如果沒有或者不對可以考慮出驗證碼或者分流到固定分組。

熱點隔離

秒殺、搶購屬於非常合適的熱點例子;對於這種熱點是能提前知道的,所以可以將秒殺和搶購做成獨立系統或服務進行隔離,從而保證秒殺/搶購流程出現問題不影響主流程。

還存在一些熱點可能是因為價格或突發事件引起的;對於讀熱點我使用多級快取搞定;而寫熱點我們一般通過快取+佇列模式削峰,可以參考《前端交易型系統設計原則》。

資源隔離

最常見的資源如磁碟、CPU、網路;對於寶貴的資源都會存在競爭問題。

在《構建需求響應式億級商品詳情頁》中我們使用JIMDB資料同步時要dump資料,SSD盤容量用了50%以上,dump到同一塊磁碟時遇到了容量不足的問題,我們通過單獨掛一塊SAS盤來專門同步資料。還有如使用Docker容器時,有的容器寫磁碟非常頻繁,因此要考慮為不同的容器掛載不同的磁碟。

預設CPU的排程策略在一些追求極致效能的場景下可能並不太適合,我們希望通過繫結CPU到特定程序來提升效能。如我們一臺機器會啟動很多個Redis例項,通過將CPU通過taskset繫結到Redis例項上可以提升一些效能;還有Nginx提供了worker_processes和worker_cpu_affinity來繫結CPU。還有如系統網路應用比較繁忙的話,可以考慮繫結網絡卡IRQ到指定的CPU來提升系統處理中斷的能力,從而提升效能。

還有如大資料計算叢集、資料庫叢集應該和應用叢集隔離到不同的機架,並儘量隔離網路;因為大資料計算或資料庫同步時時會有比較大的網路頻寬,可能擁塞網路導致應用響應慢。

還有一些其他類似的隔離術,如環境隔離(測試環境、預釋出環境/灰度環境、正式環境)、壓測隔離(真實資料、壓測資料隔離)、ABTest(為不同的使用者提供不同版本的服務)、快取隔離(有些系統混用快取,而有些系統會扔大位元組值到如Redis,造成Redis慢查詢)、查詢隔離(簡單、批量、複雜條件查詢分別路由到不同的叢集)等。通過隔離後可以將風險降低到最低、效能提升至最優。

10

作者:張開濤

文章出處:開濤的部落格