0.學(xué)習(xí)目標(biāo)
- 會(huì)配置Hystix熔斷
- 會(huì)使用Feign進(jìn)行遠(yuǎn)程調(diào)用
- 能獨(dú)立搭建Zuul網(wǎng)關(guān)
- 能編寫(xiě)Zuul的過(guò)濾器
1.Hystrix
1.1.簡(jiǎn)介
Hystrix,英文意思是豪豬敛劝,全身是刺宴抚,看起來(lái)就不好惹替蛉,是一種保護(hù)機(jī)制斜棚。
Hystrix也是Netflix公司的一款組件。
主頁(yè):https://github.com/Netflix/Hystrix/
那么Hystix的作用是什么呢啄育?具體要保護(hù)什么呢?
Hystix是Netflix開(kāi)源的一個(gè)延遲和容錯(cuò)庫(kù)拌消,用于隔離訪(fǎng)問(wèn)遠(yuǎn)程服務(wù)挑豌、第三方庫(kù),防止出現(xiàn)級(jí)聯(lián)失敗墩崩。
1.2.雪崩問(wè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ù)。
如果此時(shí)虚青,某個(gè)服務(wù)出現(xiàn)異常:
例如微服務(wù)I發(fā)生異常它呀,請(qǐng)求阻塞,用戶(hù)不會(huì)得到響應(yīng)棒厘,則tomcat的這個(gè)線(xiàn)程不會(huì)釋放纵穿,于是越來(lái)越多的用戶(hù)請(qǐng)求到來(lái),越來(lái)越多的線(xiàn)程會(huì)阻塞:
服務(wù)器支持的線(xiàn)程和并發(fā)數(shù)有限奢人,請(qǐng)求一直阻塞谓媒,會(huì)導(dǎo)致服務(wù)器資源耗盡,從而導(dǎo)致所有其它服務(wù)都不可用达传,形成雪崩效應(yīng)篙耗。
這就好比迫筑,一個(gè)汽車(chē)生產(chǎn)線(xiàn),生產(chǎn)不同的汽車(chē)宗弯,需要使用不同的零件脯燃,如果某個(gè)零件因?yàn)榉N種原因無(wú)法使用,那么就會(huì)造成整臺(tái)車(chē)無(wú)法裝配蒙保,陷入等待零件的狀態(tài)辕棚,直到零件到位,才能繼續(xù)組裝邓厕。 此時(shí)如果有很多個(gè)車(chē)型都需要這個(gè)零件逝嚎,那么整個(gè)工廠都將陷入等待的狀態(tài),導(dǎo)致所有生產(chǎn)都陷入癱瘓详恼。一個(gè)零件的波及范圍不斷擴(kuò)大补君。
Hystix解決雪崩問(wèn)題的手段有兩個(gè):
- 線(xiàn)程隔離
- 服務(wù)熔斷
1.3.線(xiàn)程隔離,服務(wù)降級(jí)
1.3.1.原理
線(xiàn)程隔離示意圖:
解讀:
Hystrix為每個(gè)依賴(lài)服務(wù)調(diào)用分配一個(gè)小的線(xiàn)程池昧互,如果線(xiàn)程池已滿(mǎn)調(diào)用將被立即拒絕挽铁,默認(rèn)不采用排隊(duì).加速失敗判定時(shí)間。
用戶(hù)的請(qǐng)求將不再直接訪(fǎng)問(wèn)服務(wù)敞掘,而是通過(guò)線(xiàn)程池中的空閑線(xiàn)程來(lái)訪(fǎng)問(wèn)服務(wù)叽掘,如果線(xiàn)程池已滿(mǎn),或者請(qǐng)求超時(shí)玖雁,則會(huì)進(jìn)行降級(jí)處理更扁,什么是服務(wù)降級(jí)?
服務(wù)降級(jí):優(yōu)先保證核心服務(wù)赫冬,而非核心服務(wù)不可用或弱可用浓镜。
用戶(hù)的請(qǐng)求故障時(shí),不會(huì)被阻塞面殖,更不會(huì)無(wú)休止的等待或者看到系統(tǒng)崩潰竖哩,至少可以看到一個(gè)執(zhí)行結(jié)果(例如返回友好的提示信息) 。
服務(wù)降級(jí)雖然會(huì)導(dǎo)致請(qǐng)求失敗脊僚,但是不會(huì)導(dǎo)致阻塞相叁,而且最多會(huì)影響這個(gè)依賴(lài)服務(wù)對(duì)應(yīng)的線(xiàn)程池中的資源,對(duì)其它服務(wù)沒(méi)有響應(yīng)辽幌。
觸發(fā)Hystix服務(wù)降級(jí)的情況:
- 線(xiàn)程池已滿(mǎn)
- 請(qǐng)求超時(shí)
1.3.2.動(dòng)手實(shí)踐
1.3.2.1.引入依賴(lài)
首先在itcast-service-consumer的pom.xml中引入Hystrix依賴(lài):
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
1.3.2.2.開(kāi)啟熔斷
可以看到增淹,我們類(lèi)上的注解越來(lái)越多,在微服務(wù)中乌企,經(jīng)常會(huì)引入上面的三個(gè)注解虑润,于是Spring就提供了一個(gè)組合注解:@SpringCloudApplication
因此,我們可以使用這個(gè)組合注解來(lái)代替之前的3個(gè)注解加酵。
@SpringCloudApplication
public class ItcastServiceConsumerApplication {
@Bean
@LoadBalanced
public RestTemplate restTemplate(){
return new RestTemplate();
}
public static void main(String[] args) {
SpringApplication.run(ItcastServiceConsumerApplication.class, args);
}
}
1.3.2.3.編寫(xiě)降級(jí)邏輯
我們改造itcast-service-consumer拳喻,當(dāng)目標(biāo)服務(wù)的調(diào)用出現(xiàn)故障哭当,我們希望快速失敗,給用戶(hù)一個(gè)友好提示冗澈。因此需要提前編寫(xiě)好失敗時(shí)的降級(jí)處理邏輯钦勘,要使用HystixCommond來(lái)完成:
@Controller
@RequestMapping("consumer/user")
public class UserController {
@Autowired
private RestTemplate restTemplate;
@GetMapping
@ResponseBody
@HystrixCommand(fallbackMethod = "queryUserByIdFallBack")
public String queryUserById(@RequestParam("id") Long id) {
String user = this.restTemplate.getForObject("http://service-provider/user/" + id, String.class);
return user;
}
public String queryUserByIdFallBack(Long id){
return "請(qǐng)求繁忙,請(qǐng)稍后再試亚亲!";
}
}
要注意彻采,因?yàn)槿蹟嗟慕导?jí)邏輯方法必須跟正常邏輯方法保證:相同的參數(shù)列表和返回值聲明。失敗邏輯中返回User對(duì)象沒(méi)有太大意義捌归,一般會(huì)返回友好提示肛响。所以我們把queryById的方法改造為返回String,反正也是Json數(shù)據(jù)惜索。這樣失敗邏輯中返回一個(gè)錯(cuò)誤說(shuō)明特笋,會(huì)比較方便。
說(shuō)明:
- @HystrixCommand(fallbackMethod = "queryByIdFallBack"):用來(lái)聲明一個(gè)降級(jí)邏輯的方法
測(cè)試:
當(dāng)itcast-service-provder正常提供服務(wù)時(shí)巾兆,訪(fǎng)問(wèn)與以前一致雹有。但是當(dāng)我們將itcast-service-provider停機(jī)時(shí),會(huì)發(fā)現(xiàn)頁(yè)面返回了降級(jí)處理信息:
1.3.2.4.默認(rèn)FallBack
我們剛才把fallback寫(xiě)在了某個(gè)業(yè)務(wù)方法上臼寄,如果這樣的方法很多,那豈不是要寫(xiě)很多溜宽。所以我們可以把Fallback配置加在類(lèi)上吉拳,實(shí)現(xiàn)默認(rèn)fallback:
@Controller
@RequestMapping("consumer/user")
@DefaultProperties(defaultFallback = "fallBackMethod") // 指定一個(gè)類(lèi)的全局熔斷方法
public class UserController {
@Autowired
private RestTemplate restTemplate;
@GetMapping
@ResponseBody
@HystrixCommand // 標(biāo)記該方法需要熔斷
public String queryUserById(@RequestParam("id") Long id) {
String user = this.restTemplate.getForObject("http://service-provider/user/" + id, String.class);
return user;
}
/**
* 熔斷方法
* 返回值要和被熔斷的方法的返回值一致
* 熔斷方法不需要參數(shù)
* @return
*/
public String fallBackMethod(){
return "請(qǐng)求繁忙,請(qǐng)稍后再試适揉!";
}
}
- @DefaultProperties(defaultFallback = "defaultFallBack"):在類(lèi)上指明統(tǒng)一的失敗降級(jí)方法
- @HystrixCommand:在方法上直接使用該注解留攒,使用默認(rèn)的剪輯方法。
- defaultFallback:默認(rèn)降級(jí)方法嫉嘀,不用任何參數(shù)炼邀,以匹配更多方法,但是返回值一定一致
1.3.2.5.設(shè)置超時(shí)
在之前的案例中剪侮,請(qǐng)求在超過(guò)1秒后都會(huì)返回錯(cuò)誤信息拭宁,這是因?yàn)镠ystix的默認(rèn)超時(shí)時(shí)長(zhǎng)為1,我們可以通過(guò)配置修改這個(gè)值:
我們可以通過(guò)hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds
來(lái)設(shè)置Hystrix超時(shí)時(shí)間瓣俯。該配置沒(méi)有提示杰标。
hystrix:
command:
default:
execution:
isolation:
thread:
timeoutInMilliseconds: 6000 # 設(shè)置hystrix的超時(shí)時(shí)間為6000ms
改造服務(wù)提供者
改造服務(wù)提供者的UserController接口,隨機(jī)休眠一段時(shí)間彩匕,以觸發(fā)熔斷:
@GetMapping("{id}")
public User queryUserById(@PathVariable("id") Long id) {
try {
Thread.sleep(6000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return this.userService.queryUserById(id);
}
1.4.服務(wù)熔斷
1.4.1.熔斷原理
熔斷器腔剂,也叫斷路器,其英文單詞為:Circuit Breaker
熔斷狀態(tài)機(jī)3個(gè)狀態(tài):
- Closed:關(guān)閉狀態(tài)驼仪,所有請(qǐng)求都正常訪(fǎng)問(wèn)掸犬。
- Open:打開(kāi)狀態(tài)袜漩,所有請(qǐng)求都會(huì)被降級(jí)。Hystix會(huì)對(duì)請(qǐng)求情況計(jì)數(shù)湾碎,當(dāng)一定時(shí)間內(nèi)失敗請(qǐng)求百分比達(dá)到閾值宙攻,則觸發(fā)熔斷,斷路器會(huì)完全打開(kāi)胜茧。默認(rèn)失敗比例的閾值是50%粘优,請(qǐng)求次數(shù)最少不低于20次。
- Half Open:半開(kāi)狀態(tài)呻顽,open狀態(tài)不是永久的雹顺,打開(kāi)后會(huì)進(jìn)入休眠時(shí)間(默認(rèn)是5S)。隨后斷路器會(huì)自動(dòng)進(jìn)入半開(kāi)狀態(tài)廊遍。此時(shí)會(huì)釋放部分請(qǐng)求通過(guò)嬉愧,若這些請(qǐng)求都是健康的,則會(huì)完全關(guān)閉斷路器喉前,否則繼續(xù)保持打開(kāi)没酣,再次進(jìn)行休眠計(jì)時(shí)
1.4.2.動(dòng)手實(shí)踐
為了能夠精確控制請(qǐng)求的成功或失敗,我們?cè)赾onsumer的調(diào)用業(yè)務(wù)中加入一段邏輯:
@GetMapping("{id}")
@HystrixCommand
public String queryUserById(@PathVariable("id") Long id){
if(id == 1){
throw new RuntimeException("太忙了");
}
String user = this.restTemplate.getForObject("http://service-provider/user/" + id, String.class);
return user;
}
這樣如果參數(shù)是id為1卵迂,一定失敗裕便,其它情況都成功。(不要忘了清空service-provider中的休眠邏輯)
我們準(zhǔn)備兩個(gè)請(qǐng)求窗口:
- 一個(gè)請(qǐng)求:http://localhost/consumer/user/1见咒,注定失敗
- 一個(gè)請(qǐng)求:http://localhost/consumer/user/2旗芬,肯定成功
當(dāng)我們瘋狂訪(fǎng)問(wèn)id為1的請(qǐng)求時(shí)(超過(guò)20次)昌罩,就會(huì)觸發(fā)熔斷。斷路器會(huì)斷開(kāi),一切請(qǐng)求都會(huì)被降級(jí)處理切心。
此時(shí)你訪(fǎng)問(wèn)id為2的請(qǐng)求滥嘴,會(huì)發(fā)現(xiàn)返回的也是失敗售碳,而且失敗時(shí)間很短匈勋,只有幾毫秒左右:
不過(guò),默認(rèn)的熔斷觸發(fā)要求較高庆揩,休眠時(shí)間窗較短俐东,為了測(cè)試方便,我們可以通過(guò)配置修改熔斷策略:
circuitBreaker.requestVolumeThreshold=10
circuitBreaker.sleepWindowInMilliseconds=10000
circuitBreaker.errorThresholdPercentage=50
解讀:
- requestVolumeThreshold:觸發(fā)熔斷的最小請(qǐng)求次數(shù)订晌,默認(rèn)20
- errorThresholdPercentage:觸發(fā)熔斷的失敗請(qǐng)求最小占比犬性,默認(rèn)50%
- sleepWindowInMilliseconds:休眠時(shí)長(zhǎng),默認(rèn)是5000毫秒
2.Feign
在前面的學(xué)習(xí)中腾仅,我們使用了Ribbon的負(fù)載均衡功能乒裆,大大簡(jiǎn)化了遠(yuǎn)程調(diào)用時(shí)的代碼:
String user = this.restTemplate.getForObject("http://service-provider/user/" + id, String.class);
如果就學(xué)到這里,你可能以后需要編寫(xiě)類(lèi)似的大量重復(fù)代碼推励,格式基本相同鹤耍,無(wú)非參數(shù)不一樣肉迫。有沒(méi)有更優(yōu)雅的方式,來(lái)對(duì)這些代碼再次優(yōu)化呢稿黄?
這就是我們接下來(lái)要學(xué)的Feign的功能了喊衫。
2.1.簡(jiǎn)介
有道詞典的英文解釋?zhuān)?/p>
為什么叫偽裝?
Feign可以把Rest的請(qǐng)求進(jìn)行隱藏杆怕,偽裝成類(lèi)似SpringMVC的Controller一樣族购。你不用再自己拼接url,拼接參數(shù)等等操作陵珍,一切都交給Feign去做寝杖。
項(xiàng)目主頁(yè):https://github.com/OpenFeign/feign
2.2.快速入門(mén)
改造itcast-service-consumer工程
2.2.1.導(dǎo)入依賴(lài)
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
2.2.2.開(kāi)啟Feign功能
我們?cè)趩?dòng)類(lèi)上,添加注解互纯,開(kāi)啟Feign功能
@SpringCloudApplication
@EnableFeignClients // 開(kāi)啟feign客戶(hù)端
public class ItcastServiceConsumerApplication {
public static void main(String[] args) {
SpringApplication.run(ItcastServiceConsumerApplication.class, args);
}
}
刪除RestTemplate:feign已經(jīng)自動(dòng)集成了Ribbon負(fù)載均衡的RestTemplate瑟幕。所以,此處不需要再注冊(cè)RestTemplate留潦。
2.2.3.Feign的客戶(hù)端
在itcast-service-consumer工程中只盹,添加UserClient接口:
內(nèi)容:
@FeignClient(value = "service-provider") // 標(biāo)注該類(lèi)是一個(gè)feign接口
public interface UserClient {
@GetMapping("user/{id}")
User queryById(@PathVariable("id") Long id);
}
- 首先這是一個(gè)接口,F(xiàn)eign會(huì)通過(guò)動(dòng)態(tài)代理兔院,幫我們生成實(shí)現(xiàn)類(lèi)殖卑。這點(diǎn)跟mybatis的mapper很像
-
@FeignClient
,聲明這是一個(gè)Feign客戶(hù)端坊萝,類(lèi)似@Mapper
注解懦鼠。同時(shí)通過(guò)value
屬性指定服務(wù)名稱(chēng) - 接口中的定義方法,完全采用SpringMVC的注解屹堰,F(xiàn)eign會(huì)根據(jù)注解幫我們生成URL,并訪(fǎng)問(wèn)獲取結(jié)果
改造原來(lái)的調(diào)用邏輯街氢,調(diào)用UserClient接口:
@Controller
@RequestMapping("consumer/user")
public class UserController {
@Autowired
private UserClient userClient;
@GetMapping
@ResponseBody
public User queryUserById(@RequestParam("id") Long id){
User user = this.userClient.queryUserById(id);
return user;
}
}
2.2.4.啟動(dòng)測(cè)試
訪(fǎng)問(wèn)接口:
正常獲取到了結(jié)果扯键。
2.3.負(fù)載均衡
Feign中本身已經(jīng)集成了Ribbon依賴(lài)和自動(dòng)配置:
因此我們不需要額外引入依賴(lài),也不需要再注冊(cè)RestTemplate
對(duì)象珊肃。
2.4.Hystrix支持
Feign默認(rèn)也有對(duì)Hystrix的集成:
只不過(guò)荣刑,默認(rèn)情況下是關(guān)閉的。我們需要通過(guò)下面的參數(shù)來(lái)開(kāi)啟:(在itcast-service-consumer工程添加配置內(nèi)容)
feign:
hystrix:
enabled: true # 開(kāi)啟Feign的熔斷功能
但是伦乔,F(xiàn)eign中的Fallback配置不像hystrix中那樣簡(jiǎn)單了厉亏。
1)首先,我們要定義一個(gè)類(lèi)UserClientFallback烈和,實(shí)現(xiàn)剛才編寫(xiě)的UserClient爱只,作為fallback的處理類(lèi)
@Component
public class UserClientFallback implements UserClient {
@Override
public User queryById(Long id) {
User user = new User();
user.setUserName("服務(wù)器繁忙,請(qǐng)稍后再試招刹!");
return user;
}
}
2)然后在UserFeignClient中恬试,指定剛才編寫(xiě)的實(shí)現(xiàn)類(lèi)
@FeignClient(value = "service-provider", fallback = UserClientFallback.class) // 標(biāo)注該類(lèi)是一個(gè)feign接口
public interface UserClient {
@GetMapping("user/{id}")
User queryUserById(@PathVariable("id") Long id);
}
3)重啟測(cè)試:
2.5.請(qǐng)求壓縮(了解)
Spring Cloud Feign 支持對(duì)請(qǐng)求和響應(yīng)進(jìn)行GZIP壓縮窝趣,以減少通信過(guò)程中的性能損耗。通過(guò)下面的參數(shù)即可開(kāi)啟請(qǐng)求與響應(yīng)的壓縮功能:
feign:
compression:
request:
enabled: true # 開(kāi)啟請(qǐng)求壓縮
response:
enabled: true # 開(kāi)啟響應(yīng)壓縮
同時(shí)训柴,我們也可以對(duì)請(qǐng)求的數(shù)據(jù)類(lèi)型哑舒,以及觸發(fā)壓縮的大小下限進(jìn)行設(shè)置:
feign:
compression:
request:
enabled: true # 開(kāi)啟請(qǐng)求壓縮
mime-types: text/html,application/xml,application/json # 設(shè)置壓縮的數(shù)據(jù)類(lèi)型
min-request-size: 2048 # 設(shè)置觸發(fā)壓縮的大小下限
注:上面的數(shù)據(jù)類(lèi)型、壓縮大小下限均為默認(rèn)值幻馁。
2.6.日志級(jí)別(了解)
前面講過(guò)洗鸵,通過(guò)logging.level.xx=debug
來(lái)設(shè)置日志級(jí)別。然而這個(gè)對(duì)Fegin客戶(hù)端而言不會(huì)產(chǎn)生效果仗嗦。因?yàn)?code>@FeignClient注解修改的客戶(hù)端在被代理時(shí)膘滨,都會(huì)創(chuàng)建一個(gè)新的Fegin.Logger實(shí)例。我們需要額外指定這個(gè)日志的級(jí)別才可以儒将。
1)設(shè)置com.leyou包下的日志級(jí)別都為debug
logging:
level:
cn.itcast: debug
2)編寫(xiě)配置類(lèi)吏祸,定義日志級(jí)別
內(nèi)容:
@Configuration
public class FeignLogConfiguration {
@Bean
Logger.Level feignLoggerLevel(){
return Logger.Level.FULL;
}
}
這里指定的Level級(jí)別是FULL,F(xiàn)eign支持4種級(jí)別:
- NONE:不記錄任何日志信息钩蚊,這是默認(rèn)值贡翘。
- BASIC:僅記錄請(qǐng)求的方法,URL以及響應(yīng)狀態(tài)碼和執(zhí)行時(shí)間
- HEADERS:在BASIC的基礎(chǔ)上砰逻,額外記錄了請(qǐng)求和響應(yīng)的頭信息
- FULL:記錄所有請(qǐng)求和響應(yīng)的明細(xì)鸣驱,包括頭信息、請(qǐng)求體蝠咆、元數(shù)據(jù)踊东。
3)在FeignClient中指定配置類(lèi):
@FeignClient(value = "service-privider", fallback = UserFeignClientFallback.class, configuration = FeignConfig.class)
public interface UserFeignClient {
@GetMapping("/user/{id}")
User queryUserById(@PathVariable("id") Long id);
}
4)重啟項(xiàng)目,即可看到每次訪(fǎng)問(wèn)的日志:
3.Zuul網(wǎng)關(guān)
通過(guò)前面的學(xué)習(xí)刚操,使用Spring Cloud實(shí)現(xiàn)微服務(wù)的架構(gòu)基本成型闸翅,大致是這樣的:
我們使用Spring Cloud Netflix中的Eureka實(shí)現(xiàn)了服務(wù)注冊(cè)中心以及服務(wù)注冊(cè)與發(fā)現(xiàn);而服務(wù)間通過(guò)Ribbon或Feign實(shí)現(xiàn)服務(wù)的消費(fèi)以及均衡負(fù)載菊霜。為了使得服務(wù)集群更為健壯坚冀,使用Hystrix的融斷機(jī)制來(lái)避免在微服務(wù)架構(gòu)中個(gè)別服務(wù)出現(xiàn)異常時(shí)引起的故障蔓延。
在該架構(gòu)中鉴逞,我們的服務(wù)集群包含:內(nèi)部服務(wù)Service A和Service B记某,他們都會(huì)注冊(cè)與訂閱服務(wù)至Eureka Server,而Open Service是一個(gè)對(duì)外的服務(wù)构捡,通過(guò)均衡負(fù)載公開(kāi)至服務(wù)調(diào)用方液南。我們把焦點(diǎn)聚集在對(duì)外服務(wù)這塊,直接暴露我們的服務(wù)地址勾徽,這樣的實(shí)現(xiàn)是否合理滑凉,或者是否有更好的實(shí)現(xiàn)方式呢?
先來(lái)說(shuō)說(shuō)這樣架構(gòu)需要做的一些事兒以及存在的不足:
先來(lái)說(shuō)說(shuō)這樣架構(gòu)需要做的一些事兒以及存在的不足:
-
破壞了服務(wù)無(wú)狀態(tài)特點(diǎn)。
為了保證對(duì)外服務(wù)的安全性譬涡,我們需要實(shí)現(xiàn)對(duì)服務(wù)訪(fǎng)問(wèn)的權(quán)限控制闪幽,而開(kāi)放服務(wù)的權(quán)限控制機(jī)制將會(huì)貫穿并污染整個(gè)開(kāi)放服務(wù)的業(yè)務(wù)邏輯,這會(huì)帶來(lái)的最直接問(wèn)題是涡匀,破壞了服務(wù)集群中REST API無(wú)狀態(tài)的特點(diǎn)盯腌。
從具體開(kāi)發(fā)和測(cè)試的角度來(lái)說(shuō),在工作中除了要考慮實(shí)際的業(yè)務(wù)邏輯之外陨瘩,還需要額外考慮對(duì)接口訪(fǎng)問(wèn)的控制處理腕够。
-
無(wú)法直接復(fù)用既有接口。
當(dāng)我們需要對(duì)一個(gè)即有的集群內(nèi)訪(fǎng)問(wèn)接口舌劳,實(shí)現(xiàn)外部服務(wù)訪(fǎng)問(wèn)時(shí)帚湘,我們不得不通過(guò)在原有接口上增加校驗(yàn)邏輯,或增加一個(gè)代理調(diào)用來(lái)實(shí)現(xiàn)權(quán)限控制甚淡,無(wú)法直接復(fù)用原有的接口大诸。
面對(duì)類(lèi)似上面的問(wèn)題,我們要如何解決呢贯卦?答案是:服務(wù)網(wǎng)關(guān)资柔!
為了解決上面這些問(wèn)題,我們需要將權(quán)限控制這樣的東西從我們的服務(wù)單元中抽離出去撵割,而最適合這些邏輯的地方就是處于對(duì)外訪(fǎng)問(wèn)最前端的地方贿堰,我們需要一個(gè)更強(qiáng)大一些的均衡負(fù)載器的 服務(wù)網(wǎng)關(guān)。
服務(wù)網(wǎng)關(guān)是微服務(wù)架構(gòu)中一個(gè)不可或缺的部分啡彬。通過(guò)服務(wù)網(wǎng)關(guān)統(tǒng)一向外系統(tǒng)提供REST API的過(guò)程中羹与,除了具備服務(wù)路由
、均衡負(fù)載
功能之外庶灿,它還具備了權(quán)限控制
等功能纵搁。Spring Cloud Netflix中的Zuul就擔(dān)任了這樣的一個(gè)角色,為微服務(wù)架構(gòu)提供了前門(mén)保護(hù)的作用往踢,同時(shí)將權(quán)限控制這些較重的非業(yè)務(wù)邏輯內(nèi)容遷移到服務(wù)路由層面腾誉,使得服務(wù)集群主體能夠具備更高的可復(fù)用性和可測(cè)試性。
3.1.簡(jiǎn)介
官網(wǎng):https://github.com/Netflix/zuul
Zuul:維基百科
電影《捉鬼敢死隊(duì)》中的怪獸菲语,Zuul,在紐約引發(fā)了巨大騷亂惑灵。
事實(shí)上山上,在微服務(wù)架構(gòu)中,Zuul就是守門(mén)的大Boss英支!一夫當(dāng)關(guān)佩憾,萬(wàn)夫莫開(kāi)!
3.2.Zuul加入后的架構(gòu)
不管是來(lái)自于客戶(hù)端(PC或移動(dòng)端)的請(qǐng)求,還是服務(wù)內(nèi)部調(diào)用妄帘。一切對(duì)服務(wù)的請(qǐng)求都會(huì)經(jīng)過(guò)Zuul這個(gè)網(wǎng)關(guān)楞黄,然后再由網(wǎng)關(guān)來(lái)實(shí)現(xiàn) 鑒權(quán)、動(dòng)態(tài)路由等等操作抡驼。Zuul就是我們服務(wù)的統(tǒng)一入口鬼廓。
3.3.快速入門(mén)
3.3.1.新建工程
填寫(xiě)基本信息:
添加Zuul依賴(lài):
3.3.2.編寫(xiě)配置
server:
port: 10010 #服務(wù)端口
spring:
application:
name: api-gateway #指定服務(wù)名
3.3.3.編寫(xiě)引導(dǎo)類(lèi)
通過(guò)@EnableZuulProxy
注解開(kāi)啟Zuul的功能:
@SpringBootApplication
@EnableZuulProxy // 開(kāi)啟網(wǎng)關(guān)功能
public class ItcastZuulApplication {
public static void main(String[] args) {
SpringApplication.run(ItcastZuulApplication.class, args);
}
}
3.3.4.編寫(xiě)路由規(guī)則
我們需要用Zuul來(lái)代理service-provider服務(wù),先看一下控制面板中的服務(wù)狀態(tài):
- ip為:127.0.0.1
- 端口為:8081
映射規(guī)則:
server:
port: 10010 #服務(wù)端口
spring:
application:
name: api-gateway #指定服務(wù)名
zuul:
routes:
service-provider: # 這里是路由id致盟,隨意寫(xiě)
path: /service-provider/** # 這里是映射路徑
url: http://127.0.0.1:8081 # 映射路徑對(duì)應(yīng)的實(shí)際url地址
我們將符合path
規(guī)則的一切請(qǐng)求碎税,都代理到 url
參數(shù)指定的地址
本例中,我們將 /service-provider/**
開(kāi)頭的請(qǐng)求馏锡,代理到http://127.0.0.1:8081
3.3.5.啟動(dòng)測(cè)試
訪(fǎng)問(wèn)的路徑中需要加上配置規(guī)則的映射路徑雷蹂,我們?cè)L問(wèn):http://127.0.0.1:10010/service-provider/user/1
3.4.面向服務(wù)的路由
在剛才的路由規(guī)則中,我們把路徑對(duì)應(yīng)的服務(wù)地址寫(xiě)死了杯道!如果同一服務(wù)有多個(gè)實(shí)例的話(huà)匪煌,這樣做顯然就不合理了。我們應(yīng)該根據(jù)服務(wù)的名稱(chēng)党巾,去Eureka注冊(cè)中心查找 服務(wù)對(duì)應(yīng)的所有實(shí)例列表萎庭,然后進(jìn)行動(dòng)態(tài)路由才對(duì)!
對(duì)itcast-zuul工程修改優(yōu)化:
3.4.1.添加Eureka客戶(hù)端依賴(lài)
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
3.4.2.添加Eureka配置昧港,獲取服務(wù)信息
eureka:
client:
registry-fetch-interval-seconds: 5 # 獲取服務(wù)列表的周期:5s
service-url:
defaultZone: http://127.0.0.1:10086/eureka
3.4.3.開(kāi)啟Eureka客戶(hù)端發(fā)現(xiàn)功能
@SpringBootApplication
@EnableZuulProxy // 開(kāi)啟Zuul的網(wǎng)關(guān)功能
@EnableDiscoveryClient
public class ZuulDemoApplication {
public static void main(String[] args) {
SpringApplication.run(ZuulDemoApplication.class, args);
}
}
3.4.4.修改映射配置擎椰,通過(guò)服務(wù)名稱(chēng)獲取
因?yàn)橐呀?jīng)有了Eureka客戶(hù)端,我們可以從Eureka獲取服務(wù)的地址信息创肥,因此映射時(shí)無(wú)需指定IP地址达舒,而是通過(guò)服務(wù)名稱(chēng)來(lái)訪(fǎng)問(wèn),而且Zuul已經(jīng)集成了Ribbon的負(fù)載均衡功能叹侄。
zuul:
routes:
service-provider: # 這里是路由id巩搏,隨意寫(xiě)
path: /service-provider/** # 這里是映射路徑
serviceId: service-provider # 指定服務(wù)名稱(chēng)
3.4.5.啟動(dòng)測(cè)試
再次啟動(dòng),這次Zuul進(jìn)行代理時(shí)趾代,會(huì)利用Ribbon進(jìn)行負(fù)載均衡訪(fǎng)問(wèn):
3.5.簡(jiǎn)化的路由配置
在剛才的配置中贯底,我們的規(guī)則是這樣的:
-
zuul.routes.<route>.path=/xxx/**
: 來(lái)指定映射路徑。<route>
是自定義的路由名 -
zuul.routes.<route>.serviceId=service-provider
:來(lái)指定服務(wù)名撒强。
而大多數(shù)情況下禽捆,我們的<route>
路由名稱(chēng)往往和服務(wù)名會(huì)寫(xiě)成一樣的。因此Zuul就提供了一種簡(jiǎn)化的配置語(yǔ)法:zuul.routes.<serviceId>=<path>
比方說(shuō)上面我們關(guān)于service-provider的配置可以簡(jiǎn)化為一條:
zuul:
routes:
service-provider: /service-provider/** # 這里是映射路徑
省去了對(duì)服務(wù)名稱(chēng)的配置飘哨。
3.6.默認(rèn)的路由規(guī)則
在使用Zuul的過(guò)程中胚想,上面講述的規(guī)則已經(jīng)大大的簡(jiǎn)化了配置項(xiàng)。但是當(dāng)服務(wù)較多時(shí)芽隆,配置也是比較繁瑣的浊服。因此Zuul就指定了默認(rèn)的路由規(guī)則:
- 默認(rèn)情況下统屈,一切服務(wù)的映射路徑就是服務(wù)名本身。例如服務(wù)名為:
service-provider
牙躺,則默認(rèn)的映射路徑就 是:/service-provider/**
也就是說(shuō)愁憔,剛才的映射規(guī)則我們完全不配置也是OK的,不信就試試看孽拷。
3.7.路由前綴
配置示例:
zuul:
routes:
service-provider: /service-provider/**
service-consumer: /service-consumer/**
prefix: /api # 添加路由前綴
我們通過(guò)zuul.prefix=/api
來(lái)指定了路由的前綴吨掌,這樣在發(fā)起請(qǐng)求時(shí),路徑就要以/api開(kāi)頭乓搬。
3.8.過(guò)濾器
Zuul作為網(wǎng)關(guān)的其中一個(gè)重要功能思犁,就是實(shí)現(xiàn)請(qǐng)求的鑒權(quán)。而這個(gè)動(dòng)作我們往往是通過(guò)Zuul提供的過(guò)濾器來(lái)實(shí)現(xiàn)的进肯。
3.8.1.ZuulFilter
ZuulFilter是過(guò)濾器的頂級(jí)父類(lèi)激蹲。在這里我們看一下其中定義的4個(gè)最重要的方法:
public abstract ZuulFilter implements IZuulFilter{
abstract public String filterType();
abstract public int filterOrder();
boolean shouldFilter();// 來(lái)自IZuulFilter
Object run() throws ZuulException;// IZuulFilter
}
-
shouldFilter
:返回一個(gè)Boolean
值,判斷該過(guò)濾器是否需要執(zhí)行江掩。返回true執(zhí)行学辱,返回false不執(zhí)行。 -
run
:過(guò)濾器的具體業(yè)務(wù)邏輯环形。 -
filterType
:返回字符串策泣,代表過(guò)濾器的類(lèi)型。包含以下4種:-
pre
:請(qǐng)求在被路由之前執(zhí)行 -
route
:在路由請(qǐng)求時(shí)調(diào)用 -
post
:在route和errror過(guò)濾器之后調(diào)用 -
error
:處理請(qǐng)求時(shí)發(fā)生錯(cuò)誤調(diào)用
-
-
filterOrder
:通過(guò)返回的int值來(lái)定義過(guò)濾器的執(zhí)行順序抬吟,數(shù)字越小優(yōu)先級(jí)越高萨咕。
3.8.2.過(guò)濾器執(zhí)行生命周期
這張是Zuul官網(wǎng)提供的請(qǐng)求生命周期圖,清晰的表現(xiàn)了一個(gè)請(qǐng)求在各個(gè)過(guò)濾器的執(zhí)行順序火本。
正常流程:
- 請(qǐng)求到達(dá)首先會(huì)經(jīng)過(guò)pre類(lèi)型過(guò)濾器危队,而后到達(dá)route類(lèi)型,進(jìn)行路由钙畔,請(qǐng)求就到達(dá)真正的服務(wù)提供者茫陆,執(zhí)行請(qǐng)求,返回結(jié)果后擎析,會(huì)到達(dá)post過(guò)濾器簿盅。而后返回響應(yīng)。
異常流程:
- 整個(gè)過(guò)程中揍魂,pre或者route過(guò)濾器出現(xiàn)異常桨醋,都會(huì)直接進(jìn)入error過(guò)濾器,在error處理完畢后现斋,會(huì)將請(qǐng)求交給POST過(guò)濾器喜最,最后返回給用戶(hù)。
- 如果是error過(guò)濾器自己出現(xiàn)異常步责,最終也會(huì)進(jìn)入POST過(guò)濾器返顺,將最終結(jié)果返回給請(qǐng)求客戶(hù)端。
- 如果是POST過(guò)濾器出現(xiàn)異常蔓肯,會(huì)跳轉(zhuǎn)到error過(guò)濾器遂鹊,但是與pre和route不同的是,請(qǐng)求不會(huì)再到達(dá)POST過(guò)濾器了蔗包。
所有內(nèi)置過(guò)濾器列表:
3.8.3.使用場(chǎng)景
場(chǎng)景非常多:
- 請(qǐng)求鑒權(quán):一般放在pre類(lèi)型秉扑,如果發(fā)現(xiàn)沒(méi)有訪(fǎng)問(wèn)權(quán)限,直接就攔截了
- 異常處理:一般會(huì)在error類(lèi)型和post類(lèi)型過(guò)濾器中結(jié)合來(lái)處理调限。
- 服務(wù)調(diào)用時(shí)長(zhǎng)統(tǒng)計(jì):pre和post結(jié)合使用舟陆。
3.9.自定義過(guò)濾器
接下來(lái)我們來(lái)自定義一個(gè)過(guò)濾器,模擬一個(gè)登錄的校驗(yàn)耻矮∏厍基本邏輯:如果請(qǐng)求中有access-token參數(shù),則認(rèn)為請(qǐng)求有效裆装,放行踱承。
3.9.1.定義過(guò)濾器類(lèi)
內(nèi)容:
@Component
public class LoginFilter extends ZuulFilter {
/**
* 過(guò)濾器類(lèi)型,前置過(guò)濾器
* @return
*/
@Override
public String filterType() {
return "pre";
}
/**
* 過(guò)濾器的執(zhí)行順序
* @return
*/
@Override
public int filterOrder() {
return 1;
}
/**
* 該過(guò)濾器是否生效
* @return
*/
@Override
public boolean shouldFilter() {
return true;
}
/**
* 登陸校驗(yàn)邏輯
* @return
* @throws ZuulException
*/
@Override
public Object run() throws ZuulException {
// 獲取zuul提供的上下文對(duì)象
RequestContext context = RequestContext.getCurrentContext();
// 從上下文對(duì)象中獲取請(qǐng)求對(duì)象
HttpServletRequest request = context.getRequest();
// 獲取token信息
String token = request.getParameter("access-token");
// 判斷
if (StringUtils.isBlank(token)) {
// 過(guò)濾該請(qǐng)求哨免,不對(duì)其進(jìn)行路由
context.setSendZuulResponse(false);
// 設(shè)置響應(yīng)狀態(tài)碼茎活,401
context.setResponseStatusCode(HttpStatus.SC_UNAUTHORIZED);
// 設(shè)置響應(yīng)信息
context.setResponseBody("{\"status\":\"401\", \"text\":\"request error!\"}");
}
// 校驗(yàn)通過(guò),把登陸信息放入上下文信息琢唾,繼續(xù)向后執(zhí)行
context.set("token", token);
return null;
}
}
3.9.2.測(cè)試
沒(méi)有token參數(shù)時(shí)载荔,訪(fǎng)問(wèn)失敗:
添加token參數(shù)后:
3.10.負(fù)載均衡和熔斷
Zuul中默認(rèn)就已經(jīng)集成了Ribbon負(fù)載均衡和Hystix熔斷機(jī)制采桃。但是所有的超時(shí)策略都是走的默認(rèn)值懒熙,比如熔斷超時(shí)時(shí)間只有1S,很容易就觸發(fā)了芍碧。因此建議我們手動(dòng)進(jìn)行配置:
hystrix:
command:
default:
execution:
isolation:
thread:
timeoutInMilliseconds: 2000 # 設(shè)置hystrix的超時(shí)時(shí)間為6000ms