文章目錄
-
- 概述
-
- 分布式系統面臨的問題
- Hystrix是什麼
- Hystrix能幹嘛?
- 官網資料
- Hystrix停更進維
- Hystrix重要概念
-
- 服務降級(FallBack)
- 服務熔斷(Break)
- 服務限流(FlowLimit)
- Hystrix案例
-
- 建構
- 高并發測試
-
- **JMeter測試**
- JMeter壓力測試結論
- 建立model消費者80加入
- 高并發測試
- 故障現象和導緻原因
- 如何解決?解決的需求?
- 服務降級
-
- 8001自身找問題
- 8001FallBack
- 80FallBack
- 目前問題
- 解決問題
- 服務熔斷
-
- 熔斷是什麼?
- 實操
- 總結
- 服務限流
- 服務監控HystrixDashboard
-
- 概述
- 儀表盤9001
- 斷路器示範
概述
分布式系統面臨的問題
複雜的分布式體系結構中應用程式有數十個依賴關系,每個依賴關系在某些時候将不可避免地失敗

服務雪崩
多個微服務之間調用的時候,假設微服務A調用微服務B和微服務C,微服務B和微服務C又調用其它的微服務,這就是所謂的
扇出
,如果扇出的鍊路上某個微服務的調用響應時間過長或者不可用,對微服務A的調用就會占用越來越多的系統資源,進而引起系統崩潰,所謂的
雪崩效應
對于高流量的應用來說,單一的後端依賴可能會導緻所有伺服器上的所有資源都在幾秒鐘内飽和。比失敗更糟糕的是,這些應用程式還可能導緻服務之間的延遲增加,備份隊列,線程和其它系統資源緊張,導緻整個系統發生更多的級聯故障。這些都表示需要對故障和延遲進行隔離和管理,以便
單個依賴關系的失敗不能取消整個應用程式或系統
是以,通常當你發現一個子產品下的某個執行個體失敗以後,這時候這個子產品依然還會接受流量,然後這個有問題的子產品還調用了其他的子產品,這樣就會發生級聯故障,或者叫
雪崩
Hystrix是什麼
Hystrix是一個使用者處理分布式系統的
延遲
和
容錯
的開源庫,在分布式系統裡,許多依賴不可避免地會調用失敗,比如逾時、異常等。Hystrix能夠保證在一個依賴出問題的情況下,
不會導緻整體的服務失敗,避免級聯故障,提高分布式系統的彈性
“斷路器”本身是一種開關裝置,當某個服務單元發生故障之後,通過斷路器的故障監控(類似熔斷保險絲),
向調用方傳回一個符合預期的可處理的備選響應(FallBack),而不是長期等待或者抛出調用方無法處理的異常
,這樣就保證了服務調用放方的線程不會被長時間不必要的占用,進而避免了故障在分布式系統中的蔓延,乃至雪崩
Hystrix能幹嘛?
- 服務降級
- 服務熔斷
- 接近實時的監控
- …
官網資料
https://github.com/Netflix/Hystrix/wiki/How-To-Use
Hystrix停更進維
官方推薦替代品:resilience4j 或者 sentinel
Hystrix重要概念
服務降級(FallBack)
伺服器忙,請稍後再試,不讓用戶端等待并立刻傳回一個友好提示
哪些情況下會發生服務降級?
- 程式運作異常
- 執行開始,但沒有在允許的時間内完成
- 服務熔斷觸發服務降級( 斷路器打開,不嘗試執行)
- 線程池拒絕,不嘗試執行
- 信号量拒絕,不嘗試執行
服務熔斷(Break)
類比保險絲達到最大服務通路後,直接拒絕通路,拉閘停電,然後調用服務降級的方法并傳回友好提示
就是保險絲 —— 服務的降級 -> 進而熔斷 -> 恢複調用鍊路
服務限流(FlowLimit)
秒殺高并發等操作,嚴禁一窩蜂的過來擁擠,大家排隊,一秒鐘N個,有序進行
Hystrix案例
建構
//建立PaymentServiceImpl
@Service
public class PaymentServiceImpl implements PaymentService {
@Override
public String paymentInfo_OK(Integer id) {
return "線程池:" + Thread.currentThread().getName() + "paymentInfo_OK - ID:" + id;
}
@Override
public String paymentInfo_TimeOut(Integer id) {
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "線程池:" + Thread.currentThread().getName() + "paymentInfo_TimeOut - ID:" + id + "耗時3秒";
}
}
//建立PaymentController
@RestController
public class PaymentController {
@Resource
private PaymentService paymentService;
@Value("$(server.port)")
private String serverPort;
@GetMapping("/payment/hystrix/ok/{id}")
public String paymentInfo_OK(@PathVariable("id") Integer id){
return paymentService.paymentInfo_OK(id);
}
@GetMapping("/payment/hystrix/timeout/{id}")
public String paymentInfo_TimeOut(@PathVariable("id") Integer id){
return paymentService.paymentInfo_TimeOut(id);
}
}
Yaml檔案配置
server:
port: 8001
spring:
application:
name: cloud-provider-hystrix-payment
eureka:
client:
register-with-eureka: true
fetch-registry: true
service-url:
defaultZone: http://eureka7001.com:7001/eureka/
通路http://localhost:8001/payment/hystrix/ok/11 可以正常通路,立即成功
通路http://localhost:8001/payment/hystrix/timeout/11 延遲三秒後通路成功
上述情況下在非高并發場景下還能滿足,但是。。。
高并發測試
JMeter測試
開啟JMeter,來2W并發壓死8001,兩萬請求都去通路/payment/hystrix/timeout/11
再來通路一下http://localhost:8001/payment/hystrix/ok/11 發現這個也在轉圈圈
為什麼會被卡死呢?是因為tomcat的預設的工作線程數被打滿了,沒有多餘的線程來分解壓力和處理
JMeter壓力測試結論
上面還是服務提供者8001自己測試,假如此時的外部消費者80也來通路,那消費者隻能幹等,最終導緻消費者端80不滿意,服務端8001直接被拖死
建立model消費者80加入
采用OpenFeign進行服務調用
//Service
@FeignClient("CLOUD-PROVIDER-HYSTRIX-PAYMENT")
public interface PaymentHystrixService {
@GetMapping("/payment/hystrix/ok/{id}")
String paymentInfo_OK(@PathVariable("id") Integer id);
@GetMapping("/payment/hystrix/timeout/{id}")
String paymentInfo_TimeOut(@PathVariable("id") Integer id);
}
//Controller
@RestController
public class OrderHystrixController {
@Resource
private PaymentHystrixService paymentHystrixService;
@GetMapping("/consumer/payment/hystrix/ok/{id}")
public String paymentInfo_OK(@PathVariable("id") Integer id) {
return paymentHystrixService.paymentInfo_OK(id);
}
@GetMapping("/consumer/payment/hystrix/timeout/{id}")
public String paymentInfo_TimeOut(@PathVariable("id") Integer id) {
return paymentHystrixService.paymentInfo_TimeOut(id);
}
}
高并發測試
再次使用JMeter開啟2W線程壓服務提供者8001,然後消費端微服務80再去通路正常的OK微服務8001位址
http://localhost/consumer/payment/hystrix/ok/1
發現消費者端要麼轉圈圈等待時間長,要麼報逾時錯誤
故障現象和導緻原因
8001同一層次的其它接口服務被困死,因為tomcat線程池裡面的工作線程
已經被擠占完畢
80此時調用8001,用戶端通路響應變慢,導緻轉圈圈
結論:正因為有上述故障或表現不佳,才有我們的降級/容錯/限流等技術産生
如何解決?解決的需求?
逾時導緻伺服器變慢(轉圈) —> 逾時不再等待
出錯(當機或程式運作出錯) —> 出錯要有兜底
解決方案
- 對方服務(8001)逾時了,調用者(80)不能一直卡死等待,必須有服務降級
- 對方服務(8001)當機了,調用者(80)不能一直卡死等待,必須有服務降級
- 對方服務(8001)OK,調用者(80)自己出故障或需求(自己等待時間 < 服務提供者),自己處理降級
服務降級
降級配置:
@HystrixCommand
8001自身找問題
設定自身調用逾時時間的峰值,峰值内可以正常運作,超過了需要有兜底的方案,做服務降級FallBack
8001FallBack
@Service
public class PaymentServiceImpl implements PaymentService {
@Override
public String paymentInfo_OK(Integer id) {
return "線程池:" + Thread.currentThread().getName() + "paymentInfo_OK - ID:" + id;
}
@Override
@HystrixCommand(fallbackMethod = "paymentInfo_TimeOutHandler",commandProperties = {
@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds",value = "3000")
})
public String paymentInfo_TimeOut(Integer id) {
int a = 10 / 0;
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "線程池:" + Thread.currentThread().getName() + "paymentInfo_TimeOut - ID:" + id + "耗時3秒";
}
//處理時發現線程為Hystrix的線程
public String paymentInfo_TimeOutHandler(Integer id) {
return "線程池:" + Thread.currentThread().getName() + "paymentInfo_TimeOutHandler - ID:" + id + "/(ㄒoㄒ)/~~";
}
}
一旦調用服務方法失敗并抛出了錯誤資訊後,會自動調用@HystrixCommand标注好的兜底方法來進行處理
我們在代碼中分别制造兩個異常
- int a = 10/0 計算異常
- 我們能接受三秒鐘,它運作五秒,逾時異常
目前服務不可用了,做服務降級,兜底的方案都是paymentInfo_TimeOutHandler
主啟動類激活:添加新的注解@EnableCircuitBreaker
80FallBack
80訂單微服務,也可以更好地保護自己,自己也照葫蘆畫瓢進行用戶端降級保護
主啟動類:@EnableHystrix
注:@EnableHystrix其實就是繼承了@EnableCircuitBreaker
@EnableCircuitBreaker
public @interface EnableHystrix {
}
ribbon: #設定Ribbon逾時時間5s
ReadTimeout: 5000 #指的是建立連接配接後從伺服器讀取到可用資源的時間
ConnectTimeout: 5000 #指的是建立連接配接所用的時間,适用于網絡狀态正常的情況下,兩端連接配接所用的時間
80消費者
@RestController
public class OrderHystrixController {
@Resource
private PaymentHystrixService paymentHystrixService;
@GetMapping("/consumer/payment/hystrix/ok/{id}")
public String paymentInfo_OK(@PathVariable("id") Integer id) {
return paymentHystrixService.paymentInfo_OK(id);
}
@GetMapping("/consumer/payment/hystrix/timeout/{id}")
@HystrixCommand(fallbackMethod = "paymentInfo_TimeOutHandler",commandProperties = {
@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds",value = "1500")
})
public String paymentInfo_TimeOut(@PathVariable("id") Integer id) {
return paymentHystrixService.paymentInfo_TimeOut(id);
}
public String paymentInfo_TimeOutHandler(Integer id) {
return "消費者80,忙死了/(ㄒoㄒ)/~~";
}
}
消費者80這裡的逾時時間設定為1.5秒,服務提供者8001可以通過(8001端設定允許5秒但是耗時3秒),但消費者80隻等待1.5秒就轉去執行兜底方案
目前問題
- 每一個業務方法對應一個兜底的方法,代碼膨脹
- 業務邏輯方法和處理異常服務降級的方法混在一塊,耦合度高
解決問題
每個方法配置一個???膨脹
@DefaultProperties:标注在類上,表示沒有配過fallbackMethod的就找這個default全局通用的,配置過的就找自己配置的精确打擊的
@DefaultProperties(defaultFallBack = “”)
1:1 每個方法配置一個服務降級方法,技術上可以,實際上很傻
1:N除了個别重要的任務有專屬兜底方案,其他普通的都可以通過它統一跳轉到統一處理結果頁面
通用的和獨享的各自分開,避免了代碼膨脹,合理的減少了代碼量
@RestController
@DefaultProperties(defaultFallback = "paymentGlobalFallbackMethod")
public class OrderHystrixController {
@Resource
private PaymentHystrixService paymentHystrixService;
@GetMapping("/consumer/payment/hystrix/ok/{id}")
public String paymentInfo_OK(@PathVariable("id") Integer id) {
return paymentHystrixService.paymentInfo_OK(id);
}
@GetMapping("/consumer/payment/hystrix/timeout/{id}")
// @HystrixCommand(fallbackMethod = "paymentInfo_TimeOutHandler",commandProperties = {
// @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds",value = "1500")
// })
@HystrixCommand
public String paymentInfo_TimeOut(@PathVariable("id") Integer id) {
return paymentHystrixService.paymentInfo_TimeOut(id);
}
public String paymentInfo_TimeOutHandler() {
return "消費者80,忙死了/(ㄒoㄒ)/~~";
}
public String paymentGlobalFallbackMethod() {
return "全局異常處理資訊,請稍後再試";
}
}
和業務邏輯混在一起???混亂
服務降級,用戶端去調用伺服器端,遇到服務端當機或關閉
修改用戶端消費者80的程式,建立PaymentFallbackService實作Feign用戶端接口
@Component
public class PaymentFallbackService implements PaymentHystrixService {
@Override
public String paymentInfo_OK(Integer id) {
return "---PaymentFallbackService: paymentInfo_OK fallback ";
}
@Override
public String paymentInfo_TimeOut(Integer id) {
return "---PaymentFallbackService: paymentInfo_TimeOut fallback ";
}
}
并在Feign用戶端注解上新加一個fallback屬性,指出用來兜底的服務降級類
@FeignClient(value = “CLOUD-PROVIDER-HYSTRIX-PAYMENT”,fallback = PaymentFallbackService.class)
最後在application.yml中開啟Feign對于Hystrix支援,OpenFeign用戶端将被Hystrix斷路器包裹
feign:
hystrix:
enabled: true
關閉服務端8001,通路測試http://localhost/consumer/payment/hystrix/ok/22,發現此時的服務端8001雖然已經down了,但是我們做了服務降級處理,讓用戶端80在服務端不可用的情況下也會獲得提示資訊而不會挂起耗死伺服器
服務熔斷
熔斷是什麼?
熔斷機制概述
熔斷機制是應對雪崩效應的一種微服務鍊路保護機制,當扇對外連結路的某個微服務出錯不可用或者響應時間太長時,會進行服務的降級,進而熔斷該結點微服務的調用,快速傳回錯誤的響應資訊
當檢測到該節點微服務調用響應正常時,
恢複調用鍊路
在Spring Cloud架構裡,熔斷機制通過Hystrix實作,Hystrix會監控微服務間的調用情況
當失敗的調用到了一定門檻值,預設是5秒内20次調用失敗,就會啟動熔斷機制,熔斷機制的注解是@HystrixCommand
附上martinfowler大神論文https://martinfowler.com/bliki/CircuitBreaker.html
了解
- 調用失敗會出發降級,而
降級會調用fallback方法
- 但無論如何降級,流程一定是先調用正常方法再調用fallback方法
- 假如機關時間内調用失敗次數過多,也就是降級次數過多,則會觸發熔斷
-
熔斷以後就會跳過正常方法直接調用fallback方法
- 所謂 “ 熔斷服務不可用 ” 就是因為跳過了正常方法直接執行fallback
當失敗的次數(降級次數)過多時,即達到了門檻值滿足了上圖中threshold reached條件,觸發服務熔斷,斷路器打開,假設我隻能承受100的并發量每秒鐘,而現在給我幹到了500,是以我死掉了(跳閘),而其中存在第三種狀态-半開-表示我已準備好進行再次試驗,以檢視問題是否已解決。比如過了一會沒有那麼多并發量了,我覺得我現在慢慢的能夠承接的住了,比如現在每秒鐘70的并發,我的上限還是100,那麼我可以試試放過去一個試試能不能成功,慢慢的放着放着發現能夠适應了,就把閘道合上,這就是從半開Half Open到Closed的過程
實操
修改支付微服務提供者8001的service與controller
//service新加兩個測試方法
@HystrixCommand(fallbackMethod = "paymentCircuitBreaker_fallback",commandProperties = {
@HystrixProperty(name = "circuitBreaker.enabled",value = "true"), //是否開啟斷路器
@HystrixProperty(name = "circuitBreaker.requestVolumeThreshold",value = "10"), //熔斷觸發的最少個數/10s内
@HystrixProperty(name = "circuitBreaker.sleepWindowInMilliseconds",value = "10000"), //熔斷多少秒後去嘗試請求
@HystrixProperty(name = "circuitBreaker.errorThresholdPercentage",value = "60"), //失敗率達到多少後熔斷
})
public String paymentCircuitBreaker(Integer id){
if (id < 0){
throw new RuntimeException("*****id 不能負數");
}
String serialNumber = UUID.randomUUID().toString();
return Thread.currentThread().getName()+"\t"+"調用成功,流水号:"+serialNumber;
}
public String paymentCircuitBreaker_fallback(Integer id){
return "id 不能負數,請稍候再試,(┬_┬)/~~ id: " +id;
}
//controller
@GetMapping("/payment/circuit/{id}")
public String paymentCircuitBreaker(@PathVariable("id") Integer id){
return paymentService.paymentCircuitBreaker(id);
}
熔斷打開和閉合的精确方式如下:
- 假設電路上的音量達到某個門檻值(
)HystrixCommandProperties.circuitBreakerRequestVolumeThreshold()
- 并假設誤差百分比超過門檻值誤差百分比(
)HystrixCommandProperties.circuitBreakerErrorThresholdPercentage()
- 然後,斷路器從轉換
為CLOSED
OPEN
- 當它斷開時,它會使針對該斷路器的所有請求短路
- 經過一段時間(
)後,下一個單個請求被允許通過(這是HystrixCommandProperties.circuitBreakerSleepWindowInMilliseconds()
狀态)。如果請求失敗,則斷路器将HALF-OPEN
在睡眠視窗持續時間内傳回到該狀态。如果請求成功,斷路器将切換到,OPEN
并且1中的邏輯将再次接管CLOSED
測試:測試http://localhost:8001/payment/circuit/-1,多次錯誤以後,慢慢正确,會發現剛開始的正确不滿足條件,就算正确的通路位址也直接走降級,需要慢慢的恢複鍊路
總結
熔斷類型
- 熔斷關閉:熔斷關閉不會對服務進行熔斷
- 熔斷打開:請求不再進行調用目前服務,當打開時常到所設時常則進入半熔斷狀态
- 熔斷半開:部分請求根據規則調用目前服務,如果請求成功且符合規則則認為服務恢複正常,關閉熔斷
斷路器開啟或關閉的條件
- 當滿足一定門檻值的時候(預設10秒内超過20的請求次數)
- 當失敗率達到一定的時候(預設10秒内超過50%的請求失敗)
- 到達以上門檻值,斷路器将會開啟
- 當開啟的時候,所有的請求都直接降級
- 一段時間以後(預設5S),斷路器為半開狀态,會讓其中一個請求嘗試,成功則斷路器關閉,失敗繼續重複
斷路器打開之後
- 再有請求調用的時候,将不會調用主邏輯,而是直接調用降級fallback。通過斷路器,實作自動地發現錯誤并将邏輯切換,減少響應延遲的效果
- 當斷路器打開,對主邏輯進行熔斷以後,Hystrix會啟動一個休眠時間窗,在時間窗内,降級邏輯是臨時的成為主邏輯,當休眠時間窗到期,斷路器将進入半開狀态,釋放一次請求到原來的主邏輯上,如果此時請求正常傳回,那麼斷路器将繼續閉合,主邏輯恢複,如果這次請求依然有問題,斷路器繼續進入打開狀态,休眠時間闖關重新計時
服務熔斷一般放在服務端,而服務降級一般放在消費端
服務限流
為什麼需要限流
- 複雜分布式系統通常有很多依賴,如果一個應用不能對來自依賴故障進行隔離,那麼應用本身就處在被拖垮的風險中。在一個高流量的網站中,某個單一後端一旦發生延遲,将會在數秒内導緻 所有應用資源被耗盡
- 如秒殺、搶購、雙十一等場景,在某一時間點會有爆發式的網絡流量湧入,如果沒有好的網絡流量限制,任由流量壓到背景服務執行個體,很有可能造成資源耗盡,服務無法響應,甚至嚴重的導緻應用崩潰
以後在學習Alibaba的sentinel的時候在做說明
服務監控HystrixDashboard
概述
除了隔離依賴服務的調用以外,Hystrix還提供了準實時的調用監控(Hystrix Dashboard),Hystrix會持續的記錄所有通過Hystrix發起的請求的執行資訊,并以統計報表和圖形的形式展現給使用者,包括每秒執行多少請求,多少成功多少失敗Netflix通過hystrix-metrics-event-stream項目實作了對以上名額的監控。Spring Cloud也提供了HystrixDashboard的整合,對監控内容轉化為可視化界面
儀表盤9001
建立9001監控項目引入依賴
<dependencies>
<!--新增hystrix dashboard-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix-dashboard</artifactId>
</dependency>
<!--注意所有的Provider微服務提供者(8001,8002 ...)都需要依賴監控配置-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
YML
server:
port: 9001
主啟動類新增**@EnableHystrixDashboard**
@SpringBootApplication
@EnableHystrixDashboard
public class HystrixDashboard9001 {
public static void main(String[] args) {
SpringApplication.run(HystrixDashboard9001.class,args);
}
}
啟動項目通路 http://localhost:9001/hystrix
斷路器示範
修改8001
/**
* 此配置是為了服務監控而配置,SpringCloud更新以後的坑
* ServletRegistrationBean因為springboot的預設路徑不是 /hystrix.stream
* 隻要在自己項目裡配置下面的servlet就可以了
* @return
*/
@Bean
public ServletRegistrationBean<Servlet> getServlet(){
HystrixMetricsStreamServlet streamServlet = new HystrixMetricsStreamServlet();
ServletRegistrationBean<Servlet> registrationBean = new ServletRegistrationBean<Servlet>(streamServlet);
registrationBean.setLoadOnStartup(1);
registrationBean.addUrlMappings("/hystrix.stream");
registrationBean.setName("HystrixMetricsStreamServlet");
return registrationBean;
}
啟動一個7001Eureka服務端注冊中心,再啟動我們的8001與9001
測試位址:
http://localhost:8001/payment/circuit/7
http://localhost:8001/payment/circuit/-7
會看到我們的監控頁面發生動态響應,是否觸發熔斷等
先通路正确位址,再通路錯誤位址,再通路正确位址,會發現圖示的斷路器都是慢慢放開的
如何看?