聊聊高并發(fā)之隔離術(shù)
隔離是指將系統(tǒng)或資源分割開,系統(tǒng)隔離是為了在系統(tǒng)發(fā)生故障時能限定傳播范圍和影響范圍,即發(fā)生故障后不會出現(xiàn)滾雪球效應(yīng),從而保證只有出問題的服務(wù)不可用,其他服務(wù)還是可用的;而資源隔離有臟數(shù)據(jù)隔離、通過隔離后減少資源競爭提升性能等。我遇到的比較多的隔離手段有線程隔離、進(jìn)程隔離、集群隔離、機(jī)房隔離、讀寫隔離、動靜隔離、爬蟲隔離等。而出現(xiàn)系統(tǒng)問題時可以考慮負(fù)載均衡路由、自動/手動切換分組或者降級等手段來提升可用性。
線程隔離
線程隔離主要有線程池隔離,在實際使用時我們會把請求分類,然后交給不同的線程池處理,當(dāng)一種業(yè)務(wù)的請求處理發(fā)生問題時,不會將故障擴(kuò)散到其他線程池,從而保證其他服務(wù)可用。
我們會根據(jù)服務(wù)等級劃分兩個線程池,以下是池的抽象:
- <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>
進(jìn)程隔離
在公司發(fā)展初期,一般是先進(jìn)行從0到1,不會一上來就進(jìn)行系統(tǒng)的拆分,這樣就會開發(fā)出一些比較大而全的系統(tǒng),系統(tǒng)中的一個模塊/功能出現(xiàn)問題,整個系統(tǒng)就不可用了。首先想到的解決方案是通過部署多個實例,然后通過負(fù)載均衡進(jìn)行路由轉(zhuǎn)發(fā),但是這種情況無法避免某個模塊因BUG而出現(xiàn)如OOM導(dǎo)致整個系統(tǒng)不可用的風(fēng)險。因此此種方案只是一個過渡,較好的解決方案是通過將系統(tǒng)拆分為多個子系統(tǒng)來實現(xiàn)物理隔離。通過進(jìn)程隔離使得某一個子系統(tǒng)出現(xiàn)問題不會影響到其他子系統(tǒng)。
集群隔離
隨著系統(tǒng)的發(fā)展,單實例服務(wù)無法滿足需求了,此時需要服務(wù)化技術(shù),通過部署多個服務(wù),形成服務(wù)集群來提升系統(tǒng)容量,如下圖所示
隨著調(diào)用方的增多,當(dāng)秒殺服務(wù)被刷會影響到其他服務(wù)的穩(wěn)定性,此時應(yīng)該考慮為秒殺提供單獨(dú)的服務(wù)集群,即為服務(wù)分組,從而當(dāng)某一個分組出現(xiàn)問題不會影響到其他分組,從而實現(xiàn)了故障隔離,如下圖所示
比如注冊生產(chǎn)者時提供分組名:
- <jsf:provider id="myService" interface="com.jd.MyService" alias="${分組名}" ref="myServiceImpl"/>
消費(fèi)時使用相關(guān)的分組名即可:
- <jsf:consumer id="myService" interface="com.jd.MyService" alias="${分組名}"/>
機(jī)房隔離
隨著對系統(tǒng)可用性的要求,會進(jìn)行多機(jī)房部署,每個機(jī)房的服務(wù)都有自己的服務(wù)分組,本機(jī)房的服務(wù)應(yīng)該只調(diào)用本機(jī)房服務(wù),不進(jìn)行跨機(jī)房調(diào)用;其中一個機(jī)房服務(wù)發(fā)生問題時可以通過DNS/負(fù)載均衡將請求全部切到另一個機(jī)房;或者考慮服務(wù)能自動重試其他機(jī)房的服務(wù)從而提升系統(tǒng)可用性。
一種辦法是根據(jù)IP(不同機(jī)房IP段不一樣)自動分組,還一種較靈活的辦法是通過在分組名中加上機(jī)房名解決:
- <jsf:provider id="myService" interface="com.jd.MyService" alias="${分組名}-${機(jī)房}" ref="myServiceImpl"/>
- <jsf:consumer id="myService" interface="com.jd.MyService" alias="${分組名}-${機(jī)房}"/>
讀寫隔離
如下圖所示,通過主從模式將讀和寫集群分離,讀服務(wù)只從從Redis集群獲取數(shù)據(jù),當(dāng)主Redis集群出現(xiàn)問題時,從Redis集群還是可用的,從而不影響用戶訪問;而當(dāng)從Redis集群出現(xiàn)問題時可以進(jìn)行其他集群的重試。
--先讀取從
- status, resp = slave_get(key)
- if status == STATUS_OK then
- return status, value
- end
如果從獲取失敗了,從主獲取
- status, resp = master_get(key)
動靜隔離
當(dāng)用戶訪問如結(jié)算頁時,如果JS/CSS等靜態(tài)資源也在結(jié)算頁系統(tǒng)中時,很可能因為訪問量太大導(dǎo)致帶寬被打滿導(dǎo)致出現(xiàn)不可用。
因此應(yīng)該將動態(tài)內(nèi)容和靜態(tài)資源分離,一般應(yīng)該將靜態(tài)資源放在CDN上,如下圖所示
爬蟲隔離
在實際業(yè)務(wù)中我們曾經(jīng)統(tǒng)計過一些頁面型應(yīng)用的爬蟲比例,爬蟲和正常流量的比例能達(dá)到5:1,甚至更高。而一些系統(tǒng)是因為爬蟲訪問量太大而導(dǎo)致服務(wù)不可用;一種解決辦法是通過限流解決;還一種解決辦法是在負(fù)載均衡層面將爬蟲路由到單獨(dú)集群,從而保證正常流量可用,爬蟲流量盡量可用。
比如最簡單的使用Nginx可以這樣配置:
- set $flag 0;
- if ($http_user_agent ~* "spider") {
- set $flag "1";
- }
- if($flag = "0") {
- //代理到正常集群
- }
- if ($flag = "1") {
- //代理到爬蟲集群
- }
實際場景我們使用了Openresty,不僅僅對爬蟲user-agent過濾,還會過濾一些惡意IP(統(tǒng)計IP訪問量,配置閥值),將他們分流到固定分組。還有一種辦法是種植Cookie,訪問特殊服務(wù)前先種植Cookie,訪問服務(wù)時驗證該Cookie,如果沒有或者不對可以考慮出驗證碼或者分流到固定分組。
熱點隔離
秒殺、搶購屬于非常合適的熱點例子;對于這種熱點是能提前知道的,所以可以將秒殺和搶購做成獨(dú)立系統(tǒng)或服務(wù)進(jìn)行隔離,從而保證秒殺/搶購流程出現(xiàn)問題不影響主流程。
還存在一些熱點可能是因為價格或突發(fā)事件引起的;對于讀熱點我使用多級緩存搞定;而寫熱點我們一般通過緩存+隊列模式削峰,可以參考《前端交易型系統(tǒng)設(shè)計原則》。
資源隔離
最常見的資源如磁盤、CPU、網(wǎng)絡(luò);對于寶貴的資源都會存在競爭問題。
在《構(gòu)建需求響應(yīng)式億級商品詳情頁》中我們使用JIMDB數(shù)據(jù)同步時要dump數(shù)據(jù),SSD盤容量用了50%以上,dump到同一塊磁盤時遇到了容量不足的問題,我們通過單獨(dú)掛一塊SAS盤來專門同步數(shù)據(jù)。還有如使用Docker容器時,有的容器寫磁盤非常頻繁,因此要考慮為不同的容器掛載不同的磁盤。
默認(rèn)CPU的調(diào)度策略在一些追求***性能的場景下可能并不太適合,我們希望通過綁定CPU到特定進(jìn)程來提升性能。如我們一臺機(jī)器會啟動很多個Redis實例,通過將CPU通過taskset綁定到Redis實例上可以提升一些性能;還有Nginx提供了worker_processes和worker_cpu_affinity來綁定CPU。還有如系統(tǒng)網(wǎng)絡(luò)應(yīng)用比較繁忙的話,可以考慮綁定網(wǎng)卡IRQ到指定的CPU來提升系統(tǒng)處理中斷的能力,從而提升性能。
還有如大數(shù)據(jù)計算集群、數(shù)據(jù)庫集群應(yīng)該和應(yīng)用集群隔離到不同的機(jī)架,并盡量隔離網(wǎng)絡(luò);因為大數(shù)據(jù)計算或數(shù)據(jù)庫同步時時會有比較大的網(wǎng)絡(luò)帶寬,可能擁塞網(wǎng)絡(luò)導(dǎo)致應(yīng)用響應(yīng)慢。
還有一些其他類似的隔離術(shù),如環(huán)境隔離(測試環(huán)境、預(yù)發(fā)布環(huán)境/灰度環(huán)境、正式環(huán)境)、壓測隔離(真實數(shù)據(jù)、壓測數(shù)據(jù)隔離)、ABTest(為不同的用戶提供不同版本的服務(wù))、緩存隔離(有些系統(tǒng)混用緩存,而有些系統(tǒng)會扔大字節(jié)值到如Redis,造成Redis慢查詢)、查詢隔離(簡單、批量、復(fù)雜條件查詢分別路由到不同的集群)等。通過隔離后可以將風(fēng)險降低到***、性能提升至***。
【本文是51CTO專欄作者張開濤的原創(chuàng)文章,作者微信公眾號:開濤的博客( kaitao-1234567)】