接上篇文章:
http://www.itdecent.cn/p/dac81a7bde30
負(fù)載均衡Ribbon
什么是Ribbon
Ribbon是Netflix發(fā)布的負(fù)載均衡器,它有助于控制HTTP和TCP客戶端的行為,為Ribbon配置服務(wù)提供者地址列表后,Ribbon就可基于某種負(fù)載均衡算法、自動(dòng)地幫助服務(wù)消費(fèi)者去請(qǐng)求。Ribbon默認(rèn)為我們提供了很多的負(fù)載均衡算法,例如輪詢、隨機(jī)等。我們也可以自定義Ribbon的負(fù)載均衡算法
實(shí)現(xiàn)Ribbon負(fù)載均衡
我們啟動(dòng)兩個(gè)user-service實(shí)例,一個(gè)8081 一個(gè)8082
和Eureka的高可用配置方式一致,復(fù)制啟動(dòng)參數(shù),修改啟動(dòng)端口
-Dserver.port=8082

EureKa監(jiān)控面板

開啟負(fù)載均衡
Eureka中已經(jīng)集成了Ribbon,所以我們無需引入新的依賴,直接修改代碼
在RestTemplate的配置方法上添加@LoadBalanced注解
@SpringBootApplication
@EnableDiscoveryClient //開啟Eureka客戶端
public class ConsumerApplication {
@Bean
@LoadBalanced
public RestTemplate restTemplate() {
return new RestTemplate();
}
public static void main(String[] args) {
SpringApplication.run(ConsumerApplication.class, args);
}
}
修改調(diào)用方式,不再手動(dòng)獲取ip和端口,直接通過服務(wù)名稱調(diào)用
@GetMapping("{id}")
public User queyById(@PathVariable("id") Long id) {
//根據(jù)服務(wù)id獲取服務(wù)實(shí)例列表
String url = "http://user-service/user/" + id;
User user = restTemplate.getForObject(url, User.class);
return user;
}
源碼分析
為什么在這里我們輸入服務(wù)名稱就可以調(diào)用了呢?之前還需要獲取ip和端口
顯然有人幫我們根據(jù)service名稱,獲取到了服務(wù)實(shí)例和ip端口。它就是LoadBalanceInterceptor,這個(gè)類會(huì)在對(duì)RestTemplate的請(qǐng)求進(jìn)行攔截,然后從Eureka根據(jù)服務(wù)id獲取服務(wù)列表,隨后利用負(fù)載均衡算法得到真實(shí)的服務(wù)地址信息,替換服務(wù)id
我們來看一下源碼:



修改負(fù)載均衡規(guī)則的配置入口:consumer的微服務(wù)配置
user-service:
ribbon:
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule
格式:{服務(wù)名稱}.ribbon.NFLoadBalancerRuleClassName 值就是IRule的實(shí)現(xiàn)類
負(fù)載均衡策略
Ribbon默認(rèn)是采用懶加載,即第一次訪問時(shí)才會(huì)去創(chuàng)建負(fù)載均衡客戶端,往往會(huì)出現(xiàn)超時(shí),如果需要采用饑餓加載,隨項(xiàng)目啟動(dòng)創(chuàng)建,可以這樣配置
ribbon:
eager-load:
clients: user-service
enabled: true
負(fù)載均衡的源碼流程

通過RestTemplate進(jìn)行請(qǐng)求時(shí),我們?cè)L問的地址是http://user-service/user/1,RibbonLoadBalancerInterceptor會(huì)攔截RestTemplate的請(qǐng)求,并獲取請(qǐng)求的host,然后根據(jù)服務(wù)id獲取服務(wù)列表,然后會(huì)根據(jù)負(fù)載均衡規(guī)則選中某個(gè)服務(wù),選中服務(wù)后會(huì)重寫URL,這樣就相當(dāng)于通過ip和端口調(diào)用某個(gè)服務(wù)
Hystrix
簡(jiǎn)介
Hystrix是Netflix公司的一款組件,是一個(gè)開源的延遲和容錯(cuò)庫(kù),用于隔離訪問遠(yuǎn)程服務(wù)、第三方庫(kù),防止出現(xiàn)級(jí)聯(lián)失敗。
雪崩問題
在微服務(wù)中,服務(wù)間調(diào)用關(guān)系錯(cuò)綜復(fù)雜,一個(gè)請(qǐng)求,可能需要調(diào)用多個(gè)微服務(wù)接口與才能實(shí)現(xiàn),會(huì)形成非常復(fù)雜的調(diào)用鏈路:

在一次業(yè)務(wù)請(qǐng)求中,需要調(diào)用A、P、H、I四個(gè)服務(wù),這四個(gè)服務(wù)又可能又調(diào)用其他服務(wù),這個(gè)時(shí)候,如果某個(gè)服務(wù)出現(xiàn)異常

例如微服務(wù)I發(fā)生異常,請(qǐng)求阻塞,用戶不會(huì)得到響應(yīng),則tomcat的這個(gè)線程不會(huì)釋放,于是越來越多的請(qǐng)求進(jìn)來后,就會(huì)造成越來越多的阻塞

服務(wù)器支持的線程和并發(fā)數(shù)有限,請(qǐng)求一直阻塞,會(huì)導(dǎo)致服務(wù)器資源耗盡,從而導(dǎo)致所有其它服務(wù)都不可用,形成雪崩效應(yīng)。
Hystrix解決雪崩問題的手段主要是服務(wù)降級(jí)
- 線程隔離
- 服務(wù)熔斷
線程隔離,服務(wù)降級(jí)
原理
線程隔離示意圖

解讀:
Hystrix為每個(gè)依賴服務(wù)調(diào)用分配一個(gè)小的線程池,如果線程池已滿調(diào)用將被立即拒絕,默認(rèn)不采用排隊(duì),加速失敗判定時(shí)間
用戶的請(qǐng)求將不再直接訪問服務(wù),而是通過線程池中的空閑線程來訪問服務(wù),如果線程池已滿,或者請(qǐng)求超時(shí),則會(huì)進(jìn)行降級(jí)處理
服務(wù)降級(jí)
優(yōu)先保證核心服務(wù),而非核心服務(wù)不可用或弱可用
用戶的請(qǐng)求故障時(shí),不會(huì)被阻塞,更不會(huì)無休止的等待或者看到系統(tǒng)奔潰,至少可以看到一個(gè)執(zhí)行結(jié)果。
服務(wù)降級(jí)雖然會(huì)導(dǎo)致請(qǐng)求失敗,但是不會(huì)導(dǎo)致阻塞。而且最多會(huì)影響這個(gè)依賴服務(wù)對(duì)應(yīng)的線程池中的資源,對(duì)其它服務(wù)沒有影響。
觸發(fā)Hystrix服務(wù)降級(jí)的情況
- 線程池已滿
- 請(qǐng)求超時(shí)
實(shí)戰(zhàn)
引入依賴
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
開啟熔斷
在啟動(dòng)類上添加注解 @EnableCircuitBreaker
@SpringBootApplication
@EnableDiscoveryClient //開啟Eureka客戶端
@EnableCircuitBreaker //開啟熔斷
public class ConsumerApplication {
@Bean
@LoadBalanced
public RestTemplate restTemplate() {
return new RestTemplate();
}
public static void main(String[] args) {
SpringApplication.run(ConsumerApplication.class, args);
}
}
我們?cè)陬惿系淖⒔庠絹碓蕉?,其?@SpringBootApplication @EnableDiscoveryClient @EnableCircuitBreaker 可以合并為一個(gè)注解
@SpringCloudApplication
編寫降級(jí)邏輯
當(dāng)目標(biāo)服務(wù)的調(diào)用出現(xiàn)故障,我們希望快速失敗,給用戶一個(gè)友好提示。這樣我們就需要提前編寫好失敗的降級(jí)處理邏輯。使用HystrixCommond來完成
@GetMapping("{id}")
@HystrixCommand(fallbackMethod = "queryByIdFallBack")
public User queyById(@PathVariable("id") Long id) {
//根據(jù)服務(wù)id獲取服務(wù)實(shí)例列表
String url = "http://user-service/user/" + id;
System.out.println("url=========" + url);
User user = restTemplate.getForObject(url, User.class);
return user;
}
public String queryByIdFallBack(Long id) {
log.error("查詢用戶信息失敗id:{}", id);
return "網(wǎng)絡(luò)錯(cuò)誤";
}
- @HystrixCommand(fallbackMethod="queryByIdFallBack"):用來聲明一個(gè)降級(jí)邏輯的方法
需要注意的是,熔斷的降級(jí)邏輯方法必須跟正常邏輯方法保證:相同的參數(shù)列表和返回值聲明。
測(cè)試:
當(dāng)user-service正常提供服務(wù)時(shí),訪問服務(wù)與之前一致
image.png
我們將user-service停掉后,發(fā)現(xiàn)頁面返回了降級(jí)處理信息
image.png
默認(rèn)的Fallback
我們剛才是指定了Fallback,那如果需要服務(wù)降級(jí)的方法很多,我們豈不是要寫很多的fallback,我們可以把Fallback配置加載類上,實(shí)現(xiàn)默認(rèn)fallback
@DefaultProperties(defaultFallback="defaultFallBack")
@RestController
@RequestMapping("consumer")
@Slf4j
@DefaultProperties(defaultFallback = "queryByIdFallBack")
public class ConsumerController {
@Autowired
private RestTemplate restTemplate;
@Autowired
private DiscoveryClient discoveryClient;
@GetMapping("{id}")
@HystrixCommand
public String queyById(@PathVariable("id") Long id) {
//根據(jù)服務(wù)id獲取服務(wù)實(shí)例列表
String url = "http://user-service/user/" + id;
System.out.println("url=========" + url);
String user = restTemplate.getForObject(url, String.class);
return user;
}
public String queryByIdFallBack(Long id) {
log.error("查詢用戶信息失敗id:{}", id);
return "網(wǎng)絡(luò)錯(cuò)誤";
}
}
超時(shí)設(shè)置
在之前的案例中,請(qǐng)求在超過1秒后會(huì)返回錯(cuò)誤信息,這是因?yàn)镠ystrix的默認(rèn)超時(shí)時(shí)長(zhǎng)為1秒,我們可以在配置中修改這個(gè)值
hystrix:
command:
default:
execution:
isolation:
thread:
timeoutInMilliseconds: 5000
這個(gè)配置會(huì)作用于全局所有方法
服務(wù)熔斷
熔斷原理
熔斷器,也叫斷路器。Circuit Breaker

Hystrix的熔斷狀態(tài)機(jī)模型

狀態(tài)機(jī)有3個(gè)狀態(tài)
- Closed:關(guān)閉狀態(tài),所有請(qǐng)求都正常訪問
- Open:打開狀態(tài),所有請(qǐng)求都會(huì)被降級(jí)。Hystrix會(huì)對(duì)請(qǐng)求情況計(jì)數(shù),當(dāng)一定時(shí)間內(nèi)失敗請(qǐng)求百分比達(dá)到閥值,則觸發(fā)熔斷,斷路器會(huì)完全關(guān)閉。默認(rèn)失敗比例的閥值是50%,請(qǐng)求次數(shù)最少不低于20次
- Half Open 半開狀態(tài)。open狀態(tài)不是永久的,打開后會(huì)進(jìn)入休眠時(shí)間,隨后斷路器會(huì)自動(dòng)進(jìn)入半開狀態(tài),此時(shí)會(huì)釋放1次請(qǐng)求通過,若這個(gè)請(qǐng)求是健康的,則會(huì)關(guān)閉斷路器,否則繼續(xù)保持打開,再次進(jìn)行5秒休眠
實(shí)戰(zhàn)
為了能夠精確控制請(qǐng)求的成功或失敗,我們?cè)赾onsumer的調(diào)用業(yè)務(wù)中加入一些邏輯
@GetMapping("{id}")
@HystrixCommand
public String queyById(@PathVariable("id") Long id) {
if (id == 1) {
throw new RuntimeException("busy");
}
//根據(jù)服務(wù)id獲取服務(wù)實(shí)例列表
String url = "http://user-service/user/" + id;
System.out.println("url=========" + url);
String user = restTemplate.getForObject(url, String.class);
return user;
}
在這個(gè)邏輯中,如果參數(shù)id為1,則一定會(huì)失敗,其它情況都成功
我們準(zhǔn)備兩個(gè)請(qǐng)求窗口
- http://localhost:8080/consumer/1 注定失敗
-
http://localhost:8080/consumer/2 注定成功
修改熔斷器默認(rèn)參數(shù):
circuitBreaker:
requestVolumeThreshold: 10
sleepWindowInMilliseconds: 100
errorThresholdPercentage: 50
- requestVolumeThreshold:觸發(fā)熔斷的最小請(qǐng)求次數(shù),默認(rèn)20
- sleepWindowInMilliseconds:休眠時(shí)長(zhǎng),默認(rèn)5000毫秒
- errorThresholdPercentage:觸發(fā)熔斷的失敗請(qǐng)求最小占比 默認(rèn)50%
我們的操作是:瘋狂的對(duì)id=1進(jìn)行請(qǐng)求,就會(huì)觸發(fā)熔斷,斷路器會(huì)斷開,一切請(qǐng)求都會(huì)被降級(jí)處理,此時(shí)訪問id=2,發(fā)現(xiàn)返回的也是失敗,而且失敗時(shí)間只有20毫秒左右

