隔離是指將系統或資源分割開,系統隔離是爲了在系統發生故障時能限定傳播範圍和影響範圍,即發生故障後不會出現滾雪球效應,從而保證只有出問題的服務不可用,其餘服務仍是可用的;而資源隔離有髒數據隔離、經過隔離後減小資源競爭提高性能等。我遇到的比較多的隔離手段有線程隔離、進程隔離、集羣隔離、機房隔離、讀寫隔離、動靜隔離、爬蟲隔離等。而出現系統問題時能夠考慮負載均衡路由、自動/手動切換分組或者降級等手段來提高可用性。前端
線程隔離主要有線程池隔離,在實際使用時咱們會把請求分類,而後交給不一樣的線程池處理,當一種業務的請求處理髮生問題時,不會將故障擴散到其餘線程池,從而保證其餘服務可用。web
咱們會根據服務等級劃分兩個線程池,如下是池的抽象:數據庫
<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"/>
消費時使用相關的分組名便可:async
<jsf:consumer id="myService" interface="com.jd.MyService" alias="${分組名}"/>
隨着對系統可用性的要求,會進行多機房部署,每一個機房的服務都有本身的服務分組,本機房的服務應該只調用本機房服務,不進行跨機房調用;其中一個機房服務發生問題時能夠經過DNS/負載均衡將請求所有切到另外一個機房;或者考慮服務能自動重試其餘機房的服務從而提高系統可用性。ide
一種辦法是根據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, valueend--若是從獲取失敗了,從主獲取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慢查詢)、查詢隔離(簡單、批量、複雜條件查詢分別路由到不一樣的集羣)等。經過隔離後能夠將風險下降到最低、性能提高至最優。
更多內容請關注微信公衆號:it_haha