【一起學原始碼-微服務】Hystrix 原始碼一:Hystrix基礎原理與Demo搭建
說明
原創不易,如若轉載 請標明來源!
歡迎關注本人微信公眾號:壹枝花算不算浪漫
更多內容也可檢視本人部落格:一枝花算不算浪漫
前言
前情回顧
上一個系列文章講解了Feign的原始碼,主要是Feign動態代理實現的原理,及配合Ribbon實現負載均衡的機制。
這裡我們講解一個新的元件Hystrix,也是和Feign進行融合的。
本講目錄
這一講開始講解Hystrix相關程式碼,當然還是基於上一個元件Feign的基礎上開始講解的,這裡預設你對Feign已經有了大致的瞭解。
使用過spring cloud的小夥伴對這個元件都不會陌生,Hystrix是保證系統高可用一個很重要的元件,主要提供一下幾個功能:
- 對依賴服務呼叫時出現的呼叫延遲和呼叫失敗進行控制和容錯保護
- 在複雜的分散式系統中,阻止某一個依賴服務的故障在整個系統中蔓延,服務A->服務B->服務C,服務C故障了,服務B也故障了,服務A故障了,整套分散式系統全部故障,整體宕機
- 提供fail-fast(快速失敗)和快速恢復的支援
- 提供fallback優雅降級的支援
- 支援近實時的監控、報警以及運維操作
目錄如下:
- Hystrix基礎原理
- Hystrix Demo搭建
- Hystrix原始碼閱讀及除錯說明
- Hystrix入口程式初探
元件分析
Hystrix基礎原理
命令模式
將所有請求外部系統(或者叫依賴服務)的邏輯封裝到 HystrixCommand 或者 HystrixObservableCommand 物件中。
Run()方法為實現業務邏輯,這些邏輯將會在獨立的執行緒中被執行當請求依賴服務時出現拒絕服務、超時或者短路(多個依賴服務順序請求,前面的依賴服務請求失敗,則後面的請求不會發出)時,執行該依賴服務的失敗回退邏輯(Fallback)。
隔離策略
Hystrix 為每個依賴項維護一個小執行緒池(或訊號量);如果它們達到設定值(觸發隔離),則發往該依賴項的請求將立即被拒絕,執行失敗回退邏輯(Fallback),而不是排隊。
隔離策略分執行緒隔離和訊號隔離。
執行緒隔離
第三方客戶端(執行Hystrix的run()方法)會在單獨的執行緒執行,會與呼叫的該任務的執行緒進行隔離,以此來防止呼叫者呼叫依賴所消耗的時間過長而阻塞呼叫者的執行緒。
使用執行緒隔離的好處:
- 應用程式可以不受失控的第三方客戶端的威脅,如果第三方客戶端出現問題,可以通過降級來隔離依賴。
- 當失敗的客戶端服務恢復時,執行緒池將會被清除,應用程式也會恢復,而不至於使整個Tomcat容器出現故障。
- 如果一個客戶端庫的配置錯誤,執行緒池可以很快的感知這一錯誤(通過增加錯誤比例,延遲,超時,拒絕等),並可以在不影響應用程式的功能情況下來處理這些問題(可以通過動態配置來進行實時的改變)。
- 如果一個客戶端服務的效能變差,可以通過改變執行緒池的指標(錯誤、延遲、超時、拒絕)來進行屬性的調整,並且這些調整可以不影響其他的客戶端請求。
- 簡而言之,由執行緒供的隔離功能可以使客戶端和應用程式優雅的處理各種變化,而不會造成中斷。
執行緒池的缺點
執行緒最主要的缺點就是增加了CPU的計算開銷,每個command都會在單獨的執行緒上執行,這樣的執行方式會涉及到命令的排隊、排程和上下文切換。
Netflix在設計這個系統時,決定接受這個開銷的代價,來換取它所提供的好處,並且認為這個開銷是足夠小的,不會有重大的成本或者是效能影響。
訊號隔離
訊號隔離是通過限制依賴服務的併發請求數,來控制隔離開關。訊號隔離方式下,業務請求執行緒和執行依賴服務的執行緒是同一個執行緒(例如Tomcat容器執行緒)。
觀察者模式
- Hystrix通過觀察者模式對服務進行狀態監聽
- 每個任務都包含有一個對應的Metrics,所有Metrics都由一個ConcurrentHashMap來進行維護,Key是CommandKey.name()
- 在任務的不同階段會往Metrics中寫入不同的資訊,Metrics會對統計到的歷史資訊進行統計彙總,供熔斷器以及Dashboard監控時使用
Metrics
- Metrics內部又包含了許多內部用來管理各種狀態的類,所有的狀態都是由這些類管理的
- 各種狀態的內部也是用ConcurrentHashMap來進行維護的
熔斷機制
熔斷機制是一種保護性機制,當系統中某個服務失敗率過高時,將開啟熔斷器,對該服務的後續呼叫,直接拒絕,進行Fallback操作。
熔斷所依靠的資料即是Metrics中的HealthCount所統計的錯誤率。
如何判斷是否應該開啟熔斷器?
必須同時滿足兩個條件:
- 請求數達到設定的閥值;
- 請求的失敗數 / 總請求數 > 錯誤佔比閥值%。
降級策略
當construct()或run()執行失敗時,Hystrix呼叫fallback執行回退邏輯,回退邏輯包含了通用的響應資訊,這些響應從記憶體快取中或者其他固定邏輯中得到,而不應有任何的網路依賴。
如果一定要在失敗回退邏輯中包含網路請求,必須將這些網路請求包裝在另一個 HystrixCommand 或 HystrixObservableCommand 中,即多次降級。
失敗降級也有頻率限時,如果同一fallback短時間請求過大,則會丟擲拒絕異常。
快取機制
同一物件的不同HystrixCommand例項,只執行一次底層的run()方法,並將第一個響應結果快取起來,其後的請求都會從快取返回相同的資料。
由於請求快取位於construct()或run()方法呼叫之前,所以,它減少了執行緒的執行,消除了執行緒、上下文等開銷。
Hystrix基礎原理總結
用一張簡單地流程圖總結:
Hystrix Demo搭建
Demo工程還是使用之前的專案,git地址:https://github.com/barrywangmeng/spring-cloud-learn
eureka-server:註冊中心
serviceA: 提供對外介面
serviceB: 通過feign呼叫serviceA介面
在serviceB專案中新增hystrix相關pom依賴及配置,這裡就不列出來了,小夥伴們可以直接下載這個專案看一下。
接著就是改造對serviceA呼叫的FeignClient:
我們可以調整serviceB中feign呼叫超時時間配置類模擬觸發Hystrix降級邏輯:
Hystrix原始碼閱讀及除錯說明
我們在除錯的過程中,為了方便走正常不降級邏輯的debug除錯,特地會修改feign及hystrix的超時時間。
因為hystrix中大量使用了響應式程式設計(rxJava),程式碼中包含大量的觀察者模式設計,各種回撥函式糅雜在一起,所以程式碼顯得很難懂。
這裡我們不糾結更多的rxJava原始碼,為了除錯,每個回撥方法都會打上斷點。
關於Hystrix daboard相關的內容這裡也不會講解,實際專案中會使用其他第三方元件來做服務監控,這裡不做更多研究。
Hystrix入口程式初探
之前我們講過,如果不配置feign.hystrix.enabled:true這個配置的話,預設用的是DefaultTargeter
, 配置了的話就改變為HystrixTargeter
。
我們來看看HystrixTargeter.target()
方法:
class HystrixTargeter implements Targeter {
@Override
public <T> T target(FeignClientFactoryBean factory, Feign.Builder feign, FeignContext context,
Target.HardCodedTarget<T> target) {
if (!(feign instanceof feign.hystrix.HystrixFeign.Builder)) {
return feign.target(target);
}
// 裡面包含encoder、decoder等feign的元件資訊
feign.hystrix.HystrixFeign.Builder builder = (feign.hystrix.HystrixFeign.Builder) feign;
// factory.getName: serviceA 返回的setterFactory 是null
SetterFactory setterFactory = getOptional(factory.getName(), context,
SetterFactory.class);
if (setterFactory != null) {
builder.setterFactory(setterFactory);
}
// 獲取設定的feignClient的fallback屬性
Class<?> fallback = factory.getFallback();
if (fallback != void.class) {
return targetWithFallback(factory.getName(), context, target, builder, fallback);
}
// 獲取設定的feignClient的fallbackFactory屬性
Class<?> fallbackFactory = factory.getFallbackFactory();
if (fallbackFactory != void.class) {
// 配置了降級factory的話,直接進入這個邏輯
return targetWithFallbackFactory(factory.getName(), context, target, builder, fallbackFactory);
}
return feign.target(target);
}
private <T> T targetWithFallbackFactory(String feignClientName, FeignContext context,
Target.HardCodedTarget<T> target,
HystrixFeign.Builder builder,
Class<?> fallbackFactoryClass) {
FallbackFactory<? extends T> fallbackFactory = (FallbackFactory<? extends T>)
getFromContext("fallbackFactory", feignClientName, context, fallbackFactoryClass, FallbackFactory.class);
// 呼叫我們自定義的fallback工廠中的create方法
Object exampleFallback = fallbackFactory.create(new RuntimeException());
Assert.notNull(exampleFallback,
String.format(
"Incompatible fallbackFactory instance for feign client %s. Factory may not produce null!",
feignClientName));
// target.type() 就是ServiceAFeignClient 這個feignClient介面的名稱 這裡就是做些判斷
if (!target.type().isAssignableFrom(exampleFallback.getClass())) {
throw new IllegalStateException(
String.format(
"Incompatible fallbackFactory instance for feign client %s. Factory produces instances of '%s', but should produce instances of '%s'",
feignClientName, exampleFallback.getClass(), target.type()));
}
// 執行HystrixFeign中的target方法
return builder.target(target, fallbackFactory);
}
}
我們設定的這個FallbackFactory負責在每次超時、拒絕(執行緒池滿)、異常的時候,create()方法返回一個降級機制的物件
從服務(ServiceA)的獨立的spring容器中取出來一個獨立的FallbackFactory,呼叫每個服務的時候,他對應的FallbackFactory都是存在於那個服務關聯的獨立的spring容器中的。
接著進入到Hystrix.target()
中:
public final class HystrixFeign {
public static Builder builder() {
return new Builder();
}
public static final class Builder extends Feign.Builder {
private Contract contract = new Contract.Default();
private SetterFactory setterFactory = new SetterFactory.Default();
/**
* @see #target(Class, String, FallbackFactory)
*/
public <T> T target(Target<T> target, FallbackFactory<? extends T> fallbackFactory) {
return build(fallbackFactory).newInstance(target);
}
Feign build(final FallbackFactory<?> nullableFallbackFactory) {
super.invocationHandlerFactory(new InvocationHandlerFactory() {
@Override public InvocationHandler create(Target target,
Map<Method, MethodHandler> dispatch) {
// 設定invocationHandlerFactory為HystrixInvocationHandler
return new HystrixInvocationHandler(target, dispatch, setterFactory, nullableFallbackFactory);
}
});
// 設定contact為HystrixDelegatingContract
super.contract(new HystrixDelegatingContract(contract));
// 呼叫父類的build方法
return super.build();
}
}
}
public class ReflectiveFeign extends Feign {
public <T> T newInstance(Target<T> target) {
Map<String, MethodHandler> nameToHandler = targetToHandlersByName.apply(target);
Map<Method, MethodHandler> methodToHandler = new LinkedHashMap<Method, MethodHandler>();
List<DefaultMethodHandler> defaultMethodHandlers = new LinkedList<DefaultMethodHandler>();
for (Method method : target.type().getMethods()) {
if (method.getDeclaringClass() == Object.class) {
continue;
} else if(Util.isDefault(method)) {
DefaultMethodHandler handler = new DefaultMethodHandler(method);
defaultMethodHandlers.add(handler);
methodToHandler.put(method, handler);
} else {
methodToHandler.put(method, nameToHandler.get(Feign.configKey(target.type(), method)));
}
}
// 和之前一樣,生成一個JDK動態代理物件
InvocationHandler handler = factory.create(target, methodToHandler);
T proxy = (T) Proxy.newProxyInstance(target.type().getClassLoader(), new Class<?>[]{target.type()}, handler);
for(DefaultMethodHandler defaultMethodHandler : defaultMethodHandlers) {
defaultMethodHandler.bindTo(proxy);
}
return proxy;
}
}
最終實際用來去處理這個請求的,其實是InvocationHandler,他是JDK動態代理的核心,基於JDK動態代理機制,生成一個動態代理的物件之後,對這個物件所有的方法呼叫,都會走關聯的那個InvocationHandler。
我們這裡設定的是HystrixInvocationHandler
,來看下它的構造引數:
- target:你要呼叫的服務,這裡是HardCodedTarget,裡面包含服務名稱等資訊
- dispatch:map,介面的每個方法的Method物件 -> SynchronousMethodHandler
- setterFactory:空
- nullableFallbackFactory:我們給的那個降級物件的工程,fallback工程
接下來還設定了contract資訊,Contract是解析第三方註解的元件,設定為了HystrixDelegatingContract,顧名思義,就是說,設定了這個元件之後,後面就可以解析你在各個介面上hystrix相關的一些註解。
總結
上面已經分析了Hystrix基礎原理與Demo的搭建,基礎原理中用一張簡單地圖畫了Hystrix實現的流程,後面會更加詳細的依據這個圖進行講解。
申明
本文章首發自本人部落格:https://www.cnblogs.com/wang-meng 和公眾號:壹枝花算不算浪漫,如若轉載請標明來源!
感興趣的小夥伴可關注個人公眾號:壹枝花算不算浪漫
相關推薦
【一起學原始碼-微服務】Hystrix 原始碼一:Hystrix基礎原理與Demo搭建
說明 原創不易,如若轉載 請標明來源! 歡迎關注本人微信公眾號:壹枝花算不算浪漫 更多內容也可檢視本人部落格:一枝花算不算浪漫 前言 前情回顧 上一個系列文章講解了Feign的原始碼,主要是Feign動態代理實現的原理,及配合Ribbon實現負載均衡的機制。 這裡我們講解一個新的元件Hystrix,也是和Fe
【一起學原始碼-微服務】Ribbon 原始碼一:Ribbon概念理解及Demo除錯
前言 前情回顧 前面文章已經梳理清楚了Eureka相關的概念及原始碼,接下來開始研究下Ribbon的實現原理。 我們都知道Ribbon在spring cloud中擔當負載均衡的角色, 當兩個Eureka Client互相呼叫的時候,Ribbon能夠做到呼叫時的負載,保證多節點的客戶端均勻接收請求。(這個有點類
【一起學原始碼-微服務】Ribbon 原始碼二:通過Debug找出Ribbon初始化流程及ILoadBalancer原理分析
前言 前情回顧 上一講講了Ribbon的基礎知識,通過一個簡單的demo看了下Ribbon的負載均衡,我們在RestTemplate上加了@LoadBalanced註解後,就能夠自動的負載均衡了。 本講目錄 這一講主要是繼續深入RibbonLoadBalancerClient和Ribbon+Eureka整合的
【一起學原始碼-微服務】Ribbon 原始碼三:Ribbon與Eureka整合原理分析
前言 前情回顧 上一篇講了Ribbon的初始化過程,從LoadBalancerAutoConfiguration 到RibbonAutoConfiguration 再到RibbonClientConfiguration,我們找到了ILoadBalancer預設初始化的物件等。 本講目錄 這一講我們會進一步往下
【一起學原始碼-微服務】Ribbon 原始碼四:進一步探究Ribbon的IRule和IPing
前言 前情回顧 上一講深入的講解了Ribbon的初始化過程及Ribbon與Eureka的整合程式碼,與Eureka整合的類就是DiscoveryEnableNIWSServerList,同時在DynamicServerListLoadBalancer中會呼叫PollingServerListUpdater 進
【一起學原始碼-微服務】Ribbon原始碼五:Ribbon原始碼解讀彙總篇~
前言 想說的話 【一起學原始碼-微服務-Ribbon】專欄到這裡就已經全部結束了,共更新四篇文章。 Ribbon比較小巧,這裡是直接 讀的spring cloud 內嵌封裝的版本,裡面的各種configuration確實有點繞,不過看看第三講Ribbon初始化的過程總結圖就會清晰很多。 緊接著會繼續整理學習F
【一起學原始碼-微服務】Feign 原始碼一:原始碼初探,通過Demo Debug Feign原始碼
前言 前情回顧 上一講深入的講解了Ribbon的初始化過程及Ribbon與Eureka的整合程式碼,與Eureka整合的類就是DiscoveryEnableNIWSServerList,同時在DynamicServerListLoadBalancer中會呼叫PollingServerListUpdater 進
【一起學原始碼-微服務】Feign 原始碼二:Feign動態代理構造過程
前言 前情回顧 上一講主要看了@EnableFeignClients中的registerBeanDefinitions()方法,這裡面主要是 將EnableFeignClients註解對應的配置屬性注入,將FeignClient註解對應的屬性注入。 最後是生成FeignClient對應的bean,注入到Spr
【一起學原始碼-微服務】Feign 原始碼三:Feign結合Ribbon實現負載均衡的原理分析
前言 前情回顧 上一講我們已經知道了Feign的工作原理其實是在專案啟動的時候,通過JDK動態代理為每個FeignClinent生成一個動態代理。 動態代理的資料結構是:ReflectiveFeign.FeignInvocationHandler。其中包含target(裡面是serviceName等資訊)和d
【一起學原始碼-微服務】Hystrix 原始碼二:Hystrix核心流程:Hystix非降級邏輯流程梳理
說明 原創不易,如若轉載 請標明來源! 歡迎關注本人微信公眾號:壹枝花算不算浪漫 更多內容也可檢視本人部落格:一枝花算不算浪漫 前言 前情回顧 上一講我們講了配置了feign.hystrix.enabled=true之後,預設的Targeter就會構建成HystrixTargter, 然後通過對應的Hystr
【一起學原始碼-微服務】Hystrix 原始碼三:Hystrix核心流程:Hystix降級、熔斷等原理剖析
說明 原創不易,如若轉載 請標明來源! 歡迎關注本人微信公眾號:壹枝花算不算浪漫 更多內容也可檢視本人部落格:一枝花算不算浪漫 前言 前情回顧 上一講我們講解了Hystrix在配合feign的過程中,一個正常的請求邏輯該怎樣處理,這裡涉及到執行緒池的建立、HystrixCommand的執行等邏輯。 如圖所示:
【一起學原始碼-微服務】Nexflix Eureka 原始碼二:EurekaServer啟動之配置檔案載入以及面向介面的配置項讀取
前言 上篇文章已經介紹了 為何要讀netflix eureka原始碼了,這裡就不再概述,下面開始正式原始碼解讀的內容。 如若轉載 請標明來源:一枝花算不算浪漫 程式碼總覽 還記得上文中,我們通過web.xml找到了eureka server入口的類EurekaBootStrap,這裡我們就先來簡單地看下: /
【一起學原始碼-微服務】Nexflix Eureka 原始碼三:EurekaServer啟動之EurekaServer上下文EurekaClient建立
前言 上篇文章已經介紹了 Eureka Server 環境和上下文初始化的一些程式碼,其中重點講解了environment初始化使用的單例模式,以及EurekaServerConfigure基於介面對外暴露配置方法的設計方式。這一講就是講解Eureka Server上下文初始化剩下的內容:Eureka Cli
【一起學原始碼-微服務】Nexflix Eureka 原始碼六:在眼花繚亂的程式碼中,EurekaClient是如何註冊的?
前言 上一講已經講解了EurekaClient的啟動流程,到了這裡已經有6篇Eureka原始碼分析的文章了,看了下之前的文章,感覺程式碼成分太多,會影響閱讀,後面會只擷取主要的程式碼,加上註釋講解。 這一講看的是EurekaClient註冊的流程,當然也是一塊核心,標題為什麼會寫上眼花繚亂呢?關於Eureka
【一起學原始碼-微服務】Nexflix Eureka 原始碼七:通過單元測試來Debug Eureka註冊過程
前言 上一講eureka client是如何註冊的,一直跟到原始碼傳送http請求為止,當時看eureka client註冊時如此費盡,光是找一個regiter的地方就找了半天,那麼client端傳送了http請求給server端,server端是如何處理的呢? 帶著這麼一個疑問 就開始今天原始碼的解讀了。
【一起學原始碼-微服務】Nexflix Eureka 原始碼八:EurekaClient登錄檔抓取 精妙設計分析!
前言 前情回顧 上一講 我們通過單元測試 來梳理了EurekaClient是如何註冊到server端,以及server端接收到請求是如何處理的,這裡最重要的關注點是登錄檔的一個數據結構:ConcurrentHashMap<String, Map<String, Lease<InstanceI
【一起學原始碼-微服務】Nexflix Eureka 原始碼九:服務續約原始碼分析
前言 前情回顧 上一講 我們講解了服務發現的相關邏輯,所謂服務發現 其實就是登錄檔抓取,服務例項預設每隔30s去註冊中心抓取一下注冊表增量資料,然後合併本地登錄檔資料,最後有個hash對比的操作。 本講目錄 今天主要是看下服務續約的邏輯,服務續約就是client端給server端傳送心跳檢測,告訴對方我還活著
【一起學原始碼-微服務】Nexflix Eureka 原始碼十:服務下線及例項摘除,一個client下線到底多久才會被其他例項感知?
前言 前情回顧 上一講我們講了 client端向server端傳送心跳檢查,也是預設每30鍾傳送一次,server端接收後會更新登錄檔的一個時間戳屬性,然後一次心跳(續約)也就完成了。 本講目錄 這一篇有兩個知識點及一個疑問,這個疑問是在工作中真真實實遇到過的。 例如我有服務A、服務B,A、B都註冊在同一個註
【一起學原始碼-微服務】Nexflix Eureka 原始碼十一:EurekaServer自我保護機制竟然有這麼多Bug?
前言 前情回顧 上一講主要講了服務下線,已經註冊中心自動感知宕機的服務。 其實上一講已經包含了很多EurekaServer自我保護的程式碼,其中還發現了1.7.x(1.9.x)包含的一些bug,但這些問題在master分支都已修復了。 服務下線會將服務例項從登錄檔中刪除,然後放入到recentQueue中,下
【一起學原始碼-微服務】Nexflix Eureka 原始碼十二:EurekaServer叢集模式原始碼分析
前言 前情回顧 上一講看了Eureka 註冊中心的自我保護機制,以及裡面提到的bug問題。 哈哈 轉眼間都2020年了,這個系列的文章從12.17 一直寫到現在,也是不容易哈,每天持續不斷學習,輸出部落格,這一段時間確實收穫很多。 今天在公司給組內成員分享了Eureka原始碼剖析,反響效果還可以,也算是感覺收