0. 前言
使用RestTemplate發(fā)送請(qǐng)求
了解SpringCloud的作用
搭建Eureka注冊(cè)中心
了解Robbin負(fù)載均衡
了解Hystrix熔斷器
1. 系統(tǒng)架構(gòu)
graph LR;
1[集中式架構(gòu)] --> 2[垂直拆分]
2 --> 3[分布式服務(wù)]
3 --> 4[SOA面向服務(wù)架構(gòu)]
4 --> 5[微服務(wù)架構(gòu)]
前面在介紹dubbo的時(shí)候提過(guò)微服務(wù)的概念了http://www.reibang.com/p/310509f4790c
spring cloud則是spring framework在微服務(wù)上的設(shè)計(jì)
微服務(wù)的特點(diǎn):
單一職責(zé):微服務(wù)中每一個(gè)服務(wù)都對(duì)應(yīng)唯一的業(yè)務(wù)能力,做到單一職責(zé)
微:微服務(wù)的服務(wù)拆分粒度很小,例如一個(gè)用戶管理就可以作為一個(gè)服務(wù)策幼。每個(gè)服務(wù)雖小,但“五臟俱全”奴紧。
面向服務(wù):面向服務(wù)是說(shuō)每個(gè)服務(wù)都要對(duì)外暴露Rest風(fēng)格服務(wù)接口API特姐。并不關(guān)心服務(wù)的技術(shù)實(shí)現(xiàn),做到與平臺(tái)
和語(yǔ)言無(wú)關(guān)黍氮,也不限定用什么技術(shù)實(shí)現(xiàn)唐含,只要提供Rest的接口即可。
自治:自治是說(shuō)服務(wù)間互相獨(dú)立沫浆,互不干擾
團(tuán)隊(duì)獨(dú)立:每個(gè)服務(wù)都是一個(gè)獨(dú)立的開(kāi)發(fā)團(tuán)隊(duì)捷枯,人數(shù)不能過(guò)多。
技術(shù)獨(dú)立:因?yàn)槭敲嫦蚍?wù)件缸,提供Rest接口铜靶,使用什么技術(shù)沒(méi)有別人干涉
前后端分離:采用前后端分離開(kāi)發(fā),提供統(tǒng)一Rest接口他炊,后端不用再為PC、移動(dòng)端開(kāi)發(fā)不同接口
數(shù)據(jù)庫(kù)分離:每個(gè)服務(wù)都使用自己的數(shù)據(jù)源
部署獨(dú)立已艰,服務(wù)間雖然有調(diào)用痊末,但要做到服務(wù)重啟不影響其它服務(wù)。有利于持續(xù)集成和持續(xù)交付哩掺。每個(gè)服務(wù)都是獨(dú)立的組件凿叠,可復(fù)用,可替換嚼吞,降低耦合盒件,易維護(hù)
微服務(wù)架構(gòu)與SOA都是對(duì)系統(tǒng)進(jìn)行拆分;微服務(wù)架構(gòu)基于SOA思想舱禽,可以把微服務(wù)當(dāng)做去除了ESB的SOA炒刁。ESB是SOA架構(gòu)中的中心總線,設(shè)計(jì)圖形應(yīng)該是星形的誊稚,而微服務(wù)是去中心化的分布式軟件架構(gòu)翔始。兩者比較類(lèi)似罗心,但其實(shí)也有一些差別:
功能 | SOA | 微服務(wù) |
---|---|---|
組件大小 | 大塊業(yè)務(wù)邏輯 | 單獨(dú)任務(wù)或小塊業(yè)務(wù)邏輯 |
耦合 | 通常松耦合 | 總是松耦合 |
管理 | 著重中央管理 | 著重分散管理 |
目標(biāo) | 確保應(yīng)用能夠交互操作 | 易維護(hù)、易擴(kuò)展城瞎、更輕量級(jí)的交互 |
2. 遠(yuǎn)程調(diào)用
無(wú)論是微服務(wù)還是SOA渤闷,都面臨著服務(wù)間的遠(yuǎn)程調(diào)用。那么服務(wù)間的遠(yuǎn)程調(diào)用方式有哪些呢脖镀?
常見(jiàn)的遠(yuǎn)程調(diào)用方式有以下2種:
RPC:Remote Produce Call遠(yuǎn)程過(guò)程調(diào)用飒箭,RPC****基于****Socket****,工作在會(huì)話層蜒灰。自定義數(shù)據(jù)格式弦蹂,速度快,效率高卷员。早期的webservice盈匾,現(xiàn)在熱門(mén)的dubbo,都是RPC的典型代表
Http:http其實(shí)是一種網(wǎng)絡(luò)傳輸協(xié)議毕骡,基于****TCP****削饵,工作在應(yīng)用層,規(guī)定了數(shù)據(jù)傳輸?shù)母袷?/strong>∥次祝現(xiàn)在客戶端瀏覽器與服務(wù)端通信基本都是采用Http協(xié)議窿撬,也可以用來(lái)進(jìn)行遠(yuǎn)程服務(wù)調(diào)用。缺點(diǎn)是消息封裝臃腫叙凡,優(yōu)勢(shì)是對(duì)服務(wù)的提供和調(diào)用方?jīng)]有任何技術(shù)限定劈伴,自由靈活,更符合微服務(wù)理念∥找現(xiàn)在熱門(mén)的Rest風(fēng)格跛璧,就可以通過(guò)http協(xié)議來(lái)實(shí)現(xiàn)。
區(qū)別:RPC的機(jī)制是根據(jù)語(yǔ)言的API(language API)來(lái)定義的新啼,而不是根據(jù)基于網(wǎng)絡(luò)的應(yīng)用來(lái)定義的追城。
如果你們公司全部采用Java技術(shù)棧,那么使用Dubbo作為微服務(wù)架構(gòu)是一個(gè)不錯(cuò)的選擇燥撞。
相反座柱,如果公司的技術(shù)棧多樣化,而且你更青睞Spring家族物舒,那么Spring Cloud搭建微服務(wù)是不二之選色洞。在我們的項(xiàng)目中,會(huì)選擇Spring Cloud套件冠胯,因此會(huì)使用Http方式來(lái)實(shí)現(xiàn)服務(wù)間調(diào)用火诸。
關(guān)于RPC的概念稍微更陌生,可以參考下
誰(shuí)能用通俗的語(yǔ)言解釋一下什么是 RPC 框架涵叮?
我個(gè)人理解為惭蹂,rpc多用在系統(tǒng)之間的組件的交互伞插,例如分布式的組件之間的交互。
基于RPC的API更加適用行為(也就是命令和過(guò)程)盾碗,基于REST的API更加適用于構(gòu)建模型(也就是資源和實(shí)體)媚污,處理CRUD。
- REST使用HTTP的方法廷雅,例如:GET,POST,PUT,DELETE,OPTIONS還有比較不常用的PATCH方法耗美。
- RPC通常只會(huì)使用GET和POST方法,GET方法通常用來(lái)獲取信息航缀,POST方法可以用來(lái)進(jìn)行所有的行為商架。
3. Spring的RestTemplate
既然微服務(wù)選擇了Http,那么我們就需要考慮自己來(lái)實(shí)現(xiàn)對(duì)請(qǐng)求和響應(yīng)的處理芥玉。不過(guò)開(kāi)源世界已經(jīng)有很多的http客戶
端工具蛇摸,能夠幫助我們做這些事情,例如:
HttpClient
OKHttp
URLConnection
不過(guò)這些不同的客戶端灿巧,API各不相同赶袄。
而Spring也有對(duì)http的客戶端進(jìn)行封裝,提供了工具類(lèi)叫RestTemplate抠藕。Spring提供了一個(gè)RestTemplate模板工具類(lèi)饿肺,對(duì)基于Http的客戶端進(jìn)行了封裝,并且實(shí)現(xiàn)了對(duì)象與json的序列化和反序列化盾似,非常方便敬辣。RestTemplate并沒(méi)有限定Http的客戶端類(lèi)型,而是進(jìn)行了抽象零院,目前常用的3種都有支持溉跃,其中默認(rèn)的為JDK原生的URLConnection。
4. Spring Cloud簡(jiǎn)介
4.1 簡(jiǎn)介
Spring Cloud是Spring旗下的項(xiàng)目之一告抄,官網(wǎng)地址:http://projects.spring.io/spring-cloud/
Spring最擅長(zhǎng)的就是集成喊积,把世界上最好的框架拿過(guò)來(lái),集成到自己的項(xiàng)目中玄妈。
Spring Cloud也是一樣,它將現(xiàn)在非常流行的一些技術(shù)整合到一起髓梅,實(shí)現(xiàn)了諸如:配置管理,服務(wù)發(fā)現(xiàn)枯饿,智能路由酝锅,負(fù)載均衡,熔斷器奢方,控制總線搔扁,集群狀態(tài)等功能爸舒;協(xié)調(diào)分布式環(huán)境中各個(gè)系統(tǒng),為各類(lèi)服務(wù)提供模板性配置稿蹲。其主要
涉及的組件包括:
Eureka:注冊(cè)中心
Zuul扭勉、Gateway:服務(wù)網(wǎng)關(guān)
Ribbon:負(fù)載均衡
Feign:服務(wù)調(diào)用
Hystrix或Resilience4j:熔斷器
以上只是其中一部分,架構(gòu)圖:
4.2 版本
Spring Cloud不是一個(gè)組件苛聘,而是許多組件的集合涂炎;
SpringCloud則是通過(guò)希臘英文字母的方式,在發(fā)布的版本時(shí)是以倫敦地鐵站作為版本命名设哗,并按地鐵站名稱(chēng)的首字母A-Z依次命名唱捣。
- 第一代版本: Angle;
- 第二代版本: Brixton网梢;
- 第三代版本: Camden震缭;
- 第四代版本: Dalston;
- 第五代版本: Edgware战虏;
- 第六代版本: Finchley拣宰;
- 第七代版本: GreenWich;
- 第八代版本: Hoxton活烙;
項(xiàng)目中使用新的Hoxton版本
5. 模擬微服務(wù)場(chǎng)景
5.1 創(chuàng)建父工程
微服務(wù)中需要同時(shí)創(chuàng)建多個(gè)項(xiàng)目徐裸,但為了學(xué)習(xí),先創(chuàng)建一個(gè)父工程啸盏,然后后續(xù)的工程都以這個(gè)工程為父重贺,實(shí)現(xiàn)maven的聚合。這樣可以在一個(gè)窗口看到所有工程回懦,方便學(xué)習(xí)气笙。在實(shí)際開(kāi)發(fā)中,每個(gè)微服務(wù)可獨(dú)立一個(gè)工程怯晕。
創(chuàng)建一個(gè)spring-cloud-parent的module并修改pom.xml潜圃,這里指定了子工程的spring cloud、通用mapper舟茶、和mysql connector版本
<groupId>org.example</groupId>
<artifactId>spring-cloud-parent</artifactId>
<version>1.0-SNAPSHOT</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.5.RELEASE</version>
<relativePath/>
</parent>
<properties>
<java.version>1.8</java.version>
<spring-cloud.version>Hoxton.SR9</spring-cloud.version>
<mapper.starter.version>2.1.5</mapper.starter.version>
<mysql.version>5.1.46</mysql.version>
</properties>
<dependencyManagement>
<dependencies>
<!-- springCloud -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!-- 通用Mapper啟動(dòng)器 -->
<dependency>
<groupId>tk.mybatis</groupId>
<artifactId>mapper-spring-boot-starter</artifactId>
<version>${mapper.starter.version}</version>
</dependency>
<!-- mysql驅(qū)動(dòng) -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>${mysql.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-config</artifactId>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
5.2 創(chuàng)建user-service工程
在父工程下右鍵new一個(gè)子模塊user-service谭期,并添加依賴
<parent>
<artifactId>spring-cloud-parent</artifactId>
<groupId>org.example</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>user-service</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 通用Mapper啟動(dòng)器 -->
<dependency>
<groupId>tk.mybatis</groupId>
<artifactId>mapper-spring-boot-starter</artifactId>
</dependency>
<!-- mysql驅(qū)動(dòng) -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
</dependencies>
然后我們復(fù)用之前的代碼并刪掉一些不必要的文件和參數(shù)http://www.reibang.com/p/d8dd982ba754
application.yml,綁定localhost的9091端口
jdbc:
driverClassName: com.mysql.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/test
username: root
password: root
#激活配置文件吧凉;需要制定其他的配置文件名稱(chēng)
spring:
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/test
username: root
password: root
redis:
host: localhost
port: 6379
#日志記錄級(jí)別
logging:
level:
org.example: debug
org.springframework: info
#tomcat端口
server:
port: 9091
# mybatis配置
mybatis:
# 實(shí)體類(lèi)別名包路徑
type-aliases-package: org.example.pojo
# 映射文件路徑
# mapper-locations: classpath:mappers/*.xml
configuration:
# 控制臺(tái)輸出執(zhí)行sql
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
自動(dòng)生成后我想運(yùn)行啟動(dòng)類(lèi)隧出,發(fā)現(xiàn)運(yùn)行不了,出現(xiàn)以下錯(cuò)誤
java: 程序包org.springframework.boot不存在
以為是沒(méi)設(shè)置source root和resource root和test root,設(shè)置完后也沒(méi)有用阀捅。
發(fā)現(xiàn)右邊欄也沒(méi)有maven工具欄胀瞪,問(wèn)題出在這個(gè)項(xiàng)目并沒(méi)有被idea識(shí)別為maven項(xiàng)目,右鍵我們的pom.xml饲鄙,點(diǎn)擊 mark as maven project凄诞,然后就能識(shí)別為maven項(xiàng)目了
5.3 創(chuàng)建服務(wù)調(diào)用者consumer-demo
同樣的在parent下創(chuàng)建圆雁,修改pom.xml,添加依賴
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
因?yàn)槭窍M(fèi)端帆谍,不需要直接和數(shù)據(jù)庫(kù)打交道伪朽,就不用mybatis和通用mapper了
修改啟動(dòng)類(lèi),注冊(cè)一個(gè)RestTemplate
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
@Bean
public RestTemplate restTemplate(){
return new RestTemplate();
}
}
創(chuàng)建實(shí)體類(lèi)pojo.User.java
package org.example.pojo;
import lombok.Data;
import java.util.Date;
/**
* @ClassName User
* @Description TODO
* @Author Patrick Star
* @Date 2020/12/16 2:22 下午
*/
@Data
public class User {
private Long id;
private String userName;
// 密碼
private String password;
// 姓名
private String name;
// 年齡
private Integer age;
// 性別既忆,1男性驱负,2女性
private Integer sex;
// 出生日期
private Date birthday;
// 創(chuàng)建時(shí)間
private Date created;
// 更新時(shí)間
private Date updated;
// 備注
private String note;
}
增加一個(gè)controller,直接調(diào)用RestTemplate,遠(yuǎn)程訪問(wèn)user-service的服務(wù)接口
@RestController
@RequestMapping("/consumer")
public class ConsumerController {
@Autowired
public RestTemplate restTemplate;
@GetMapping("{id}")
public User queryById(@PathVariable Long id){
String url = "localhost:9091/user/"+id;
return restTemplate.getForObject(url,User.class);
}
}
此時(shí)患雇,consumer端沒(méi)有配置端口跃脊,默認(rèn)就是8080
瀏覽器輸入http://localhost:8080/consumer/1 得到查詢結(jié)果
5.4 思考問(wèn)題
簡(jiǎn)單回顧一下,剛才我們寫(xiě)了什么:我們沒(méi)有使用spring cloud 是吧苛吱,只是單純的用spring boot把消費(fèi)端與生產(chǎn)端又做了一遍酪术,這個(gè)我們之前都謝過(guò)了。
user-service:對(duì)外提供了查詢用戶的接口
consumer-demo:通過(guò)RestTemplate訪問(wèn) http://locahost:9091/user/{id} 接口翠储,查詢用戶數(shù)據(jù)
存在什么問(wèn)題绘雁?
在consumer中,我們把url地址硬編碼到了代碼中援所,不方便后期維護(hù)
consumer需要記憶user-service的地址庐舟,如果出現(xiàn)變更,可能得不到通知住拭,地址將失效
consumer不清楚user-service的狀態(tài)挪略,服務(wù)宕機(jī)也不知道
user-service只有1臺(tái)服務(wù),不具備高可用性
即便user-service形成集群滔岳,consumer還需自己實(shí)現(xiàn)負(fù)載均衡
其實(shí)上面說(shuō)的問(wèn)題杠娱,概括一下就是分布式服務(wù)必然要面臨的問(wèn)題:
-
服務(wù)管理
? - 如何自動(dòng)注冊(cè)和發(fā)現(xiàn)
? - 如何實(shí)現(xiàn)狀態(tài)監(jiān)管
? - 如何實(shí)現(xiàn)動(dòng)態(tài)路由
服務(wù)如何實(shí)現(xiàn)負(fù)載均衡
服務(wù)如何解決容災(zāi)問(wèn)題
服務(wù)如何實(shí)現(xiàn)統(tǒng)一配置
以上的問(wèn)題,都將在SpringCloud中得到答案谱煤。
6. Eureka注冊(cè)中心
首先我們來(lái)解決第一問(wèn)題摊求,服務(wù)的管理。
6.1 問(wèn)題分析
在剛才的案例中刘离,user-service對(duì)外提供服務(wù)室叉,需要對(duì)外暴露自己的地址。而consumer-demo(調(diào)用者)需要記錄
服務(wù)提供者的地址硫惕。將來(lái)地址出現(xiàn)變更太惠,還需要及時(shí)更新。這在服務(wù)較少的時(shí)候并不覺(jué)得有什么疲憋,但是在現(xiàn)在日益復(fù)
雜的互聯(lián)網(wǎng)環(huán)境,一個(gè)項(xiàng)目可能會(huì)拆分出十幾梁只,甚至幾十個(gè)微服務(wù)缚柳。此時(shí)如果還人為管理地址埃脏,不僅開(kāi)發(fā)困難,將來(lái)
測(cè)試秋忙、發(fā)布上線都會(huì)非常麻煩彩掐,這與DevOps的思想是背道而馳的。
DevOps的思想是系統(tǒng)可以通過(guò)一組過(guò)程灰追、方法或系統(tǒng)堵幽;提高應(yīng)用發(fā)布和運(yùn)維的效率,降低管理成本。
網(wǎng)約車(chē)
這就好比是 網(wǎng)約車(chē)出現(xiàn)以前弹澎,人們出門(mén)叫車(chē)只能叫出租車(chē)朴下。一些私家車(chē)想做出租卻沒(méi)有資格,被稱(chēng)為黑車(chē)苦蒿。而很多
人想要約車(chē)殴胧,但是無(wú)奈出租車(chē)太少,不方便佩迟。私家車(chē)很多卻不敢攔团滥,而且滿大街的車(chē),誰(shuí)知道哪個(gè)才是愿意載人的报强。
一個(gè)想要灸姊,一個(gè)愿意給,就是缺少引子秉溉,缺乏管理啊力惯。
此時(shí)滴滴這樣的網(wǎng)約車(chē)平臺(tái)出現(xiàn)了,所有想載客的私家車(chē)全部到滴滴注冊(cè)坚嗜,記錄你的車(chē)型(服務(wù)類(lèi)型)夯膀,身份信息
(聯(lián)系方式)。這樣提供服務(wù)的私家車(chē)苍蔬,在滴滴那里都能找到诱建,一目了然。
此時(shí)要叫車(chē)的人碟绑,只需要打開(kāi)APP俺猿,輸入你的目的地,選擇車(chē)型(服務(wù)類(lèi)型)格仲,滴滴自動(dòng)安排一個(gè)符合需求的車(chē)到你
面前押袍,為你服務(wù),完美凯肋!
Eureka做什么谊惭?
Eureka就好比是滴滴,負(fù)責(zé)管理、記錄服務(wù)提供者的信息圈盔。服務(wù)調(diào)用者無(wú)需自己尋找服務(wù)豹芯,而是把自己的需求告訴
Eureka,然后Eureka會(huì)把符合你需求的服務(wù)告訴你驱敲。
同時(shí)铁蹈,服務(wù)提供方與Eureka之間通過(guò) “心跳” 機(jī)制進(jìn)行監(jiān)控,當(dāng)某個(gè)服務(wù)提供方出現(xiàn)問(wèn)題众眨,Eureka自然會(huì)把它從服務(wù)
列表中剔除握牧。
這就實(shí)現(xiàn)了服務(wù)的自動(dòng)注冊(cè)、發(fā)現(xiàn)娩梨、狀態(tài)監(jiān)控沿腰。
6.2 原理
基本架構(gòu)
Eureka:就是服務(wù)注冊(cè)中心(可以是一個(gè)集群),對(duì)外暴露自己的地址
提供者:?jiǎn)?dòng)后向Eureka注冊(cè)自己信息(地址姚建,提供什么服務(wù))
消費(fèi)者:向Eureka訂閱服務(wù)矫俺,Eureka會(huì)將對(duì)應(yīng)服務(wù)的所有提供者地址列表發(fā)送給消費(fèi)者,并且定期更新
心跳(續(xù)約):提供者定期通過(guò)http方式向Eureka刷新自己的狀態(tài)
6.3 搭建eureka-server工程
創(chuàng)建工程
創(chuàng)建eureka-server,依舊是父工程下掸冤,添加依賴
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
</dependencies>
編寫(xiě)啟動(dòng)類(lèi)
//聲明當(dāng)前應(yīng)用為eureka服務(wù)
@EnableEurekaServer
@SpringBootApplication
public class EurelaServerApplication {
public static void main(String[] args) {
SpringApplication.run(EurelaServerApplication.class);
}
}
編寫(xiě)配置文件application.yml
server:
port: 10086
spring:
application:
name: eureka-server #應(yīng)用名稱(chēng)厘托,會(huì)在Eureka中作為服務(wù)而ID標(biāo)識(shí)(serverId)
eureka:
client:
service-url: #EurekaServer的地址,現(xiàn)在是自己的地址稿湿,如果是集群铅匹,需要寫(xiě)其它Server地址
defaultZone: http:127.0.0.1:10086/eureka
register-with-eureka: false # 不注冊(cè)自己
fetch-registry: false #不拉取
啟動(dòng)服務(wù)
啟動(dòng) eureka-server 訪問(wèn):http://127.0.0.1:10086
6.4 服務(wù)注冊(cè)
注冊(cè)服務(wù),就是在服務(wù)上添加Eureka的客戶端依賴饺藤,客戶端代碼會(huì)自動(dòng)把服務(wù)注冊(cè)到EurekaServer中包斑。
添加依賴
我們?cè)趗ser-service中添加Eureka客戶端依賴:
<!-- Eureka客戶端 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
修改啟動(dòng)類(lèi)
在啟動(dòng)類(lèi)上開(kāi)啟Eureka客戶端功能,通過(guò)添加 @EnableDiscoveryClient 來(lái)開(kāi)啟Eureka客戶端功能
//聲明當(dāng)前應(yīng)用為eureka服務(wù)
@EnableEurekaServer
@EnableEurekaClient
@SpringBootApplication
public class EurelaServerApplication {
public static void main(String[] args) {
SpringApplication.run(EurelaServerApplication.class);
}
}
修改user-service配置文件
spring:
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/test
username: root
password: root
redis:
host: localhost
port: 6379
application:
name: user-service
#日志記錄級(jí)別
logging:
level:
org.example: debug
org.springframework: info
#tomcat端口
server:
port: 9091
# mybatis配置
mybatis:
# 實(shí)體類(lèi)別名包路徑
type-aliases-package: org.example.pojo
# 映射文件路徑
# mapper-locations: classpath:mappers/*.xml
configuration:
# 控制臺(tái)輸出執(zhí)行sql
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
eureka:
client:
service-url:
defaultZone: http://localhost:10086/eureka
注意:
這里我們添加了spring.application.name屬性來(lái)指定應(yīng)用名稱(chēng)涕俗,將來(lái)會(huì)作為服務(wù)的id使用罗丰。
測(cè)試
重啟 user-service 項(xiàng)目,訪問(wèn)Eureka監(jiān)控頁(yè)面,可以看到user-service已經(jīng)注冊(cè)成功了
6.5 服務(wù)發(fā)現(xiàn)
接下來(lái)我們修改 consumer-demo 再姑,嘗試從EurekaServer獲取服務(wù)萌抵。
方法與服務(wù)提供者類(lèi)似,只需要在項(xiàng)目中添加EurekaClient依賴元镀,就可以通過(guò)服務(wù)名稱(chēng)來(lái)獲取信息了绍填!
添加依賴
修改consumer-demo的pom.xml
<!-- Eureka客戶端 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
修改啟動(dòng)類(lèi)
@SpringBootApplication
@EnableDiscoveryClient
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
@Bean
public RestTemplate restTemplate(){
return new RestTemplate();
}
}
新增配置文件
server:
port: 8080
spring:
application:
name: connsumer-demo #應(yīng)用名稱(chēng)
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:10086/eureka
修改controller
package org.example.controller;
import org.example.pojo.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
import java.util.List;
/**
* @ClassName ConsumerController
* @Description TODO
* @Author Patrick Star
* @Date 2020/12/16 2:24 下午
*/
@RestController
@RequestMapping("/consumer")
public class ConsumerController {
@Autowired
public RestTemplate restTemplate;
// 新增discoveryClient,注意包名
// import org.springframework.cloud.client.discovery.DiscoveryClient;
@Autowired
private DiscoveryClient discoveryClient;
@GetMapping("{id}")
public User queryById(@PathVariable Long id) {
String url = "http://localhost:9091/user/" + id;
// 獲取eureka中注冊(cè)的user-service實(shí)例列表
List<ServiceInstance> serviceInstanceList = discoveryClient.getInstances("user-service");
ServiceInstance serviceInstance = serviceInstanceList.get(0);
url = "http://"+serviceInstance.getHost()+":"+serviceInstance.getPort()+"/user/"+id;
return restTemplate.getForObject(url, User.class);
}
}
Debug跟蹤運(yùn)行
重啟 consumer-demo 項(xiàng)目栖疑;然后再瀏覽器中再次訪問(wèn) http://localhost:8080/consumer/1 讨永;在代碼中debug跟進(jìn)查
看最終拼接要訪問(wèn)的URL:
6.6 Eureka詳解
基礎(chǔ)架構(gòu)
Eureka架構(gòu)中的三個(gè)核心角色:
服務(wù)注冊(cè)中心
Eureka的服務(wù)端應(yīng)用,提供服務(wù)注冊(cè)和發(fā)現(xiàn)功能遇革,就是剛剛我們建立的eureka-server
服務(wù)提供者
提供服務(wù)的應(yīng)用卿闹,可以是SpringBoot應(yīng)用揭糕,也可以是其它任意技術(shù)實(shí)現(xiàn),只要對(duì)外提供的是Rest風(fēng)格服務(wù)即可比原。本例中就是我們實(shí)現(xiàn)的user-service
服務(wù)消費(fèi)者
消費(fèi)應(yīng)用從注冊(cè)中心獲取服務(wù)列表插佛,從而得知每個(gè)服務(wù)方的信息,知道去哪里調(diào)用服務(wù)方量窘。本例中就是我們實(shí)現(xiàn)的consumer-demo
高可用的Eureka Server
Eureka Server即服務(wù)的注冊(cè)中心,在剛才的案例中氢拥,我們只有一個(gè)EurekaServer蚌铜,事實(shí)上EurekaServer也可以是一個(gè)集群,形成高可用的Eureka中心嫩海。
服務(wù)同步
多個(gè)Eureka Server之間也會(huì)互相注冊(cè)為服務(wù)冬殃,當(dāng)服務(wù)提供者注冊(cè)到Eureka Server集群中的某個(gè)節(jié)點(diǎn)時(shí),該節(jié)點(diǎn)會(huì)把服務(wù)的信息同步給集群中的每個(gè)節(jié)點(diǎn)叁怪,從而實(shí)現(xiàn)數(shù)據(jù)同步审葬。因此,無(wú)論客戶端訪問(wèn)到Eureka Server集群中的任意一個(gè)節(jié)點(diǎn)奕谭,都可以獲取到完整的服務(wù)列表信息涣觉。
而作為客戶端,需要把信息注冊(cè)到每個(gè)Eureka中:
如果有三個(gè)Eureka血柳,則每一個(gè)EurekaServer都需要注冊(cè)到其它幾個(gè)Eureka服務(wù)中官册,例如:有三個(gè)分別為10086、10087难捌、10088膝宁,則:
10086要注冊(cè)到10087和10088上
10087要注冊(cè)到10086和10088上
10088要注冊(cè)到10086和10087上
搭建單機(jī)Eureka Server集群
在單機(jī)配置兩個(gè)Eureka Server,端口分別為 10086 10010
1)修改原來(lái)Eureka配置文件
server:
port: ${port:10086}
spring:
application:
name: eureka-server #應(yīng)用名稱(chēng)根吁,會(huì)在Eureka中作為服務(wù)而ID標(biāo)識(shí)(serverId)
eureka:
client:
service-url:
# eureka 服務(wù)地址员淫,如果是集群的話;需要指定其它集群eureka地址
defaultZone: http://127.0.0.1:10086/eureka
所謂的高可用注冊(cè)中心击敌,其實(shí)就是把EurekaServer自己也作為一個(gè)服務(wù)介返,注冊(cè)到其它EurekaServer上,這樣多個(gè)
EurekaServer之間就能互相發(fā)現(xiàn)對(duì)方愚争,從而形成集群映皆。因此我們做了以下修改:
注意把register-with-eureka和fetch-registry修改為true或者注釋掉
在上述配置文件中的${}表示在jvm啟動(dòng)時(shí)候若能找到對(duì)應(yīng)port或者defaultZone參數(shù)則使用,若無(wú)則使用后面
的默認(rèn)值
把service-url的值改成了另外一臺(tái)EurekaServer的地址轰枝,而不是自己
2)另外一臺(tái)啟動(dòng)的時(shí)候指定端口port和defaultZone配置
點(diǎn)擊Edit Configurations...
修改原來(lái)的啟動(dòng)配置捅彻;在如下界面中的 VM options 中設(shè)置 -DdefaultZone=http://127.0.0.1:10010/eureka
將它復(fù)制一份命名為10010,再進(jìn)行配置-Dport=10010 -DdefaultZone=http://127.0.0.1:10086/eureka
3)客戶端注冊(cè)服務(wù)到集群
因?yàn)镋urekaServer不止一個(gè),因此 user-service
項(xiàng)目注冊(cè)服務(wù)或者consumer-demo
獲取服務(wù)的時(shí)候鞍陨,service-url
參數(shù)需要修改為如下:
eureka:
client:
service-url:
defaultZone: http://localhost:10086/eureka, http://localhost:10010/eureka
4)查看集群
啟動(dòng)兩臺(tái)server查看集群
為了方便后面的演示步淹,在將集群改為單個(gè)服務(wù)
Erueka客戶端
服務(wù)提供者要向EurekaServer注冊(cè)服務(wù)从隆,并且完成服務(wù)續(xù)約等工作。
服務(wù)注冊(cè)
服務(wù)提供者在啟動(dòng)時(shí)缭裆,會(huì)檢測(cè)配置屬性中的: eureka.client.register-with-erueka=true 參數(shù)是否正確键闺,事實(shí)上
默認(rèn)就是true。如果值確實(shí)為true澈驼,則會(huì)向EurekaServer發(fā)起一個(gè)Rest請(qǐng)求辛燥,并攜帶自己的元數(shù)據(jù)信息,Eureka
Server會(huì)把這些信息保存到一個(gè)雙層Map結(jié)構(gòu)中缝其。
第一層Map的Key就是服務(wù)id挎塌,一般是配置中的 spring.application.name 屬性
第二層Map的key是服務(wù)的實(shí)例id。一般host+ serviceId + port内边,例如: localhost:user-service:8081
值則是服務(wù)的實(shí)例對(duì)象榴都,也就是說(shuō)一個(gè)服務(wù)棠耕,可以同時(shí)啟動(dòng)多個(gè)不同實(shí)例佑淀,形成集群。
默認(rèn)注冊(cè)時(shí)使用的是主機(jī)名或者localhost娘摔,如果想用ip進(jìn)行注冊(cè)和屎,可以在 user-service 中添加配置如下:
eureka:
client:
service-url:
defaultZone: http://localhost:10086/eureka, http://localhost:10010/eureka
instance:
ip-address: 127.0.0.1 # ip地址
prefer-ip-address: true # 更傾向于使用ip拴驮,而不是host名
修改完后先后重啟 user-service 和 consumer-demo ;在調(diào)用服務(wù)的時(shí)候就已經(jīng)變成ip地址眶俩;需要注意的是:不是在
eureka中的控制臺(tái)服務(wù)實(shí)例狀態(tài)顯示莹汤。
服務(wù)續(xù)約
在注冊(cè)服務(wù)完成以后,服務(wù)提供者會(huì)維持一個(gè)心跳(定時(shí)向EurekaServer發(fā)起Rest請(qǐng)求)颠印,告訴EurekaServer:“我
還活著”纲岭。這個(gè)我們稱(chēng)為服務(wù)的續(xù)約(renew);
有兩個(gè)重要參數(shù)可以修改服務(wù)續(xù)約的行為线罕;可以在 user-service 中添加如下配置項(xiàng):
eureka:
client:
service-url:
defaultZone: http://localhost:10086/eureka, http://localhost:10010/eureka
instance:
# 更傾向使用ip地址止潮,而不是host名
prefer-ip-address: true
# ip地址
ip-address: 127.0.0.1
# 續(xù)約間隔,默認(rèn)30秒
lease-renewal-interval-in-seconds: 5
# 服務(wù)失效時(shí)間钞楼,默認(rèn)90秒
lease-expiration-duration-in-seconds: 5
``lease-renewal-interval-in-second`:服務(wù)續(xù)約(renew)的間隔喇闸,默認(rèn)為30秒
lease-expiration-duration-in-seconds
:服務(wù)失效時(shí)間,默認(rèn)值90秒
也就是說(shuō)询件,默認(rèn)情況下每隔30秒服務(wù)會(huì)向注冊(cè)中心發(fā)送一次心跳燃乍,證明自己還活著。如果超過(guò)90秒沒(méi)有發(fā)送心跳宛琅,
EurekaServer就會(huì)認(rèn)為該服務(wù)宕機(jī)刻蟹,會(huì)定時(shí)(eureka.server.eviction-interval-timer-in-ms
設(shè)定的時(shí)間)從服務(wù)列表
中移除,這兩個(gè)值在生產(chǎn)環(huán)境不要修改嘿辟,默認(rèn)即可舆瘪。
獲取服務(wù)列表
當(dāng)服務(wù)消費(fèi)者啟動(dòng)時(shí)片效,會(huì)檢測(cè)eureka.client.fetch-registry=true
參數(shù)的值,如果為true
英古,則會(huì)從Eureka Server服務(wù)的列表拉取只讀備份淀衣,然后緩存在本地。并且 每隔30秒 會(huì)重新拉取并更新數(shù)據(jù)召调∨蚯牛可以在 consumer-demo項(xiàng)目中通過(guò)下面的參數(shù)來(lái)修改:
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:10086/eureka
registry-fetch-interval-seconds: 30
失效剔除和自我保護(hù)
如下的配置都是在Eureka Server服務(wù)端進(jìn)行:
服務(wù)下線
當(dāng)服務(wù)進(jìn)行正常關(guān)閉操作時(shí),它會(huì)觸發(fā)一個(gè)服務(wù)下線的REST請(qǐng)求給Eureka Server唠叛,告訴服務(wù)注冊(cè)中心:“我要下線
了”国撵。服務(wù)中心接受到請(qǐng)求之后,將該服務(wù)置為下線狀態(tài)玻墅。
失效剔除
有時(shí)我們的服務(wù)可能由于內(nèi)存溢出或網(wǎng)絡(luò)故障等原因使得服務(wù)不能正常的工作,而服務(wù)注冊(cè)中心并未收到“服務(wù)下
線”的請(qǐng)求壮虫。相對(duì)于服務(wù)提供者的“服務(wù)續(xù)約”操作澳厢,服務(wù)注冊(cè)中心在啟動(dòng)時(shí)會(huì)創(chuàng)建一個(gè)定時(shí)任務(wù),默認(rèn)每隔一段時(shí)間
(默認(rèn)為60秒)將當(dāng)前清單中超時(shí)(默認(rèn)為90秒)沒(méi)有續(xù)約的服務(wù)剔除囚似,這個(gè)操作被稱(chēng)為失效剔除剩拢。
可以通過(guò) eureka.server.eviction-interval-timer-in-ms 參數(shù)對(duì)其進(jìn)行修改,單位是毫秒饶唤。
自我保護(hù)
我們關(guān)停一個(gè)服務(wù)徐伐,很可能會(huì)在Eureka面板看到一條警告:
這是觸發(fā)了Eureka的自我保護(hù)機(jī)制。當(dāng)服務(wù)未按時(shí)進(jìn)行心跳續(xù)約時(shí)募狂,Eureka會(huì)統(tǒng)計(jì)服務(wù)實(shí)例最近15分鐘心跳續(xù)約的比例是否低于了85%办素。
在生產(chǎn)環(huán)境下,因?yàn)榫W(wǎng)絡(luò)延遲等原因祸穷,心跳失敗實(shí)例的比例很有可能超標(biāo)性穿,但是此時(shí)就把服務(wù)剔除列表并不妥當(dāng),因?yàn)榉?wù)可能沒(méi)有宕機(jī)雷滚。Eureka在這段時(shí)間內(nèi)不會(huì)剔除任何服務(wù)實(shí)例需曾,直到網(wǎng)絡(luò)恢復(fù)正常。
生產(chǎn)環(huán)境下這很有效祈远,保證了大多數(shù)服務(wù)依然可用呆万,不過(guò)也有可能獲取到失敗的服務(wù)實(shí)例,因此服務(wù)調(diào)用者必須做好服務(wù)的失敗容錯(cuò)车份。
可以通過(guò)下面的配置來(lái)關(guān)停自我保護(hù):
eureka:
client:
service-url:
# eureka 服務(wù)地址谋减,如果是集群的話;需要指定其它集群eureka地址
defaultZone: ${defaultZone:http://127.0.0.1:10010/eureka}
server:
enable-self-preservation: false #關(guān)閉自我保護(hù)模式躬充,默認(rèn)為打開(kāi)
7. 負(fù)載均衡Ribbon
7.1 什么是ribbon
在剛才的案例中逃顶,我們啟動(dòng)了一個(gè) user-service 讨便,然后通過(guò)DiscoveryClient來(lái)獲取服務(wù)實(shí)例信息,然后獲取ip和端口來(lái)訪問(wèn)以政。
但是實(shí)際環(huán)境中霸褒,往往會(huì)開(kāi)啟很多個(gè) user-service 的集群。此時(shí)獲取的服務(wù)列表中就會(huì)有多個(gè)盈蛮,到底該訪問(wèn)哪一個(gè)
呢废菱?
一般這種情況下就需要編寫(xiě)負(fù)載均衡算法,在多個(gè)實(shí)例列表中進(jìn)行選擇抖誉。不過(guò)Eureka中已經(jīng)集成了負(fù)載均衡組件:Ribbon殊轴,簡(jiǎn)單修改代碼即可使用。
什么是Ribbon:
Ribbon 是一個(gè)基于 HTTP 和 TCP 的 客服端負(fù)載均衡工具袒炉,它是基于 Netflix Ribbon 實(shí)現(xiàn)的旁理。
它不像 Spring Cloud 服務(wù)注冊(cè)中心、配置中心我磁、API 網(wǎng)關(guān)那樣獨(dú)立部署孽文,但是它幾乎存在于每個(gè) Spring Cloud 微服務(wù)中。包括 Feign 提供的聲明式服務(wù)調(diào)用也是基于該 Ribbon 實(shí)現(xiàn)的夺艰。
Ribbon 默認(rèn)提供很多種負(fù)載均衡算法芋哭,例如輪詢、隨機(jī)等等郁副。甚至包含自定義的負(fù)載均衡算法减牺。
7.2 啟動(dòng)兩個(gè)user-service實(shí)例
我們和上面一樣,修改兩個(gè)user-service的端口存谎,一個(gè)為9091拔疚,一個(gè)為9092
7.3 開(kāi)啟負(fù)載均衡
因?yàn)镋ureka中已經(jīng)集成了Ribbon,所以我們無(wú)需引入新的依賴愕贡。修改consumer的啟動(dòng)類(lèi)草雕。
給RestTemplate添加@LoadBalanced
注解:
@SpringBootApplication
@EnableDiscoveryClient
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
@LoadBalanced
@Bean
public RestTemplate restTemplate(){
return new RestTemplate();
}
}
修改ConsumerController調(diào)用方式,不再手動(dòng)獲取ip和端口固以,而是直接通過(guò)服務(wù)名稱(chēng)調(diào)用墩虹;
@GetMapping("{id}")
public User queryById(@PathVariable Long id) {
String url = "http://user-service/user/" + id;
// 獲取eureka中注冊(cè)的user-service實(shí)例列表
// List<ServiceInstance> serviceInstanceList = discoveryClient.getInstances("user-service");
// ServiceInstance serviceInstance = serviceInstanceList.get(0);
//
// url = "http://"+serviceInstance.getHost()+":"+serviceInstance.getPort()+"/user/"+id;
return restTemplate.getForObject(url, User.class);
}
訪問(wèn)http://localhost:8080/consumer/1
查看userService 9091和9092的控制臺(tái),只有9091的端口打印了日志信息
Ribbon默認(rèn)的負(fù)載均衡策略是輪詢憨琳。SpringBoot也幫提供了修改負(fù)載均衡規(guī)則的配置入口在consumer?
demo的配置文件中添加如下诫钓,就變成隨機(jī)的了:
在配置文件中添加如下
user-service:
ribbon:
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule
格式是: {服務(wù)名稱(chēng)}.ribbon.NFLoadBalancerRuleClassName
反復(fù)請(qǐng)求測(cè)試一下,發(fā)現(xiàn)9091和9092的user-service都能收到信息打印日志
7.3 源碼跟蹤
為什么只輸入了service名稱(chēng)就可以訪問(wèn)了呢篙螟?之前還要獲取ip和端口菌湃。
顯然是有組件根據(jù)service名稱(chēng),獲取到了服務(wù)實(shí)例的ip和端口遍略。因?yàn)?consumer-demo
使用的是RestTemplate
惧所,
spring的負(fù)載均衡自動(dòng)配置類(lèi)LoadBalancerAutoConfiguration.LoadBalancerInterceptorConfig
會(huì)自動(dòng)配置
負(fù)載均衡攔截器(在spring-cloud-commons-**.jar
包中的spring.factories
中定義的自動(dòng)配置類(lèi))骤坐, 它就是
LoadBalancerInterceptor
,這個(gè)類(lèi)會(huì)在對(duì)RestTemplate的請(qǐng)求進(jìn)行攔截下愈,然后從Eureka根據(jù)服務(wù)id獲取服務(wù)列
表纽绍,隨后利用負(fù)載均衡算法得到真實(shí)的服務(wù)地址信息,替換服務(wù)id势似。
我們進(jìn)行源碼跟蹤,查找LoadBalancerInterceptor
并在serviceName上打上斷點(diǎn)
跟進(jìn)excute方法
可以看到獲取了9092端口的服務(wù)
賊跟進(jìn)下一次拌夏,發(fā)現(xiàn)后去的是9091
多次訪問(wèn) consumer-demo 的請(qǐng)求地址;然后跟進(jìn)代碼履因,點(diǎn)左邊欄的小箭頭(resume program)可以快速調(diào)試障簿,發(fā)現(xiàn)其果然實(shí)現(xiàn)了負(fù)載均衡。
8. 熔斷器Hystrix
8.1 簡(jiǎn)介
Hystrix是Netflflix開(kāi)源的一個(gè)延遲和容錯(cuò)庫(kù)栅迄,用于隔離訪問(wèn)遠(yuǎn)程服務(wù)站故、第三方庫(kù),防止出現(xiàn)級(jí)聯(lián)失敗毅舆。Hystrix 在英文里面的意思是 豪豬世蔗,它的logo 的圖是一頭豪豬,它在微服務(wù)系統(tǒng)中是一款提供保護(hù)機(jī)制的組件朗兵,和eureka一樣也是由netflflix公司開(kāi)發(fā)。
8.2 雪崩
微服務(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)求阻塞属韧,用戶請(qǐng)求就不會(huì)得到響應(yīng),則tomcat的這個(gè)線程不會(huì)釋放蛤吓,于是越來(lái)越多的
用戶請(qǐng)求到來(lái)宵喂,越來(lái)越多的線程會(huì)阻塞:
服務(wù)器支持的線程和并發(fā)數(shù)有限,請(qǐng)求一直阻塞会傲,會(huì)導(dǎo)致服務(wù)器資源耗盡锅棕,從而導(dǎo)致所有其它服務(wù)都不可用拙泽,形成雪崩效應(yīng)。
這就好比裸燎,一個(gè)汽車(chē)生產(chǎ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ò)大几蜻。
Hystrix解決雪崩問(wèn)題的手段主要是服務(wù)降級(jí)喇潘,包括:
線程隔離
服務(wù)熔斷
8.3 線程隔離和服務(wù)降級(jí)
原理
線程隔離示意圖
Hystrix為每個(gè)依賴服務(wù)調(diào)用分配一個(gè)小的線程池,如果線程池已滿調(diào)用將被立即拒絕梭稚,默認(rèn)不采用排隊(duì)颖低,加速失敗判定時(shí)間。
用戶的請(qǐng)求將不再直接訪問(wèn)服務(wù)弧烤,而是通過(guò)線程池中的空閑線程來(lái)訪問(wèn)服務(wù)忱屑,如果線程池已滿,或者請(qǐng)求超時(shí)暇昂,則會(huì)進(jìn)行降級(jí)處理莺戒,什么是服務(wù)降級(jí)?
服務(wù)降級(jí):優(yōu)先保證核心服務(wù)急波,而非核心服務(wù)不可用或弱可用从铲。
用戶的請(qǐng)求故障時(shí),不會(huì)被阻塞澄暮,更不會(huì)無(wú)休止的等待或者看到系統(tǒng)崩潰名段,至少可以看到一個(gè)執(zhí)行結(jié)果(例如返回友好的提示信息) 。
服務(wù)降級(jí)雖然會(huì)導(dǎo)致請(qǐng)求失敗泣懊,但是不會(huì)導(dǎo)致阻塞吉嫩,而且最多會(huì)影響這個(gè)依賴服務(wù)對(duì)應(yīng)的線程池中的資源,對(duì)其它服務(wù)沒(méi)有響應(yīng)嗅定。
觸發(fā)Hystrix服務(wù)降級(jí)的情況:
線程池已滿
請(qǐng)求超時(shí)
實(shí)現(xiàn)線程隔離
1)引入依賴
在 consumer-demo 消費(fèi)端系統(tǒng)的pom.xml文件添加如下依賴:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
2)開(kāi)啟熔斷
在啟動(dòng)類(lèi) ConsumerApplication 上添加注解:@EnableCircuitBreaker
@SpringBootApplication
@EnableDiscoveryClient
@EnableCircuitBreaker //線程隔離
public class Application {
... ...
}
可以看到自娩,我們類(lèi)上的注解越來(lái)越多,在微服務(wù)中,經(jīng)常會(huì)引入上面的三個(gè)注解忙迁,于是Spring就提供了一個(gè)組合注解:@SpringCloudApplication
脐彩,來(lái)看看他的實(shí)現(xiàn)
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootApplication
@EnableDiscoveryClient
@EnableCircuitBreaker
public @interface SpringCloudApplication {
}
正好包括了上面三個(gè),因此我們用組合注解代替上面是三個(gè)姊扔,
@SpringCloudApplication
public class Application {
... ...
}
3)編寫(xiě)降級(jí)邏輯
當(dāng)目標(biāo)服務(wù)的調(diào)用出現(xiàn)故障惠奸,我們希望快速失敗,給用戶一個(gè)友好提示恰梢。因此需要提前編寫(xiě)好失敗時(shí)的降級(jí)處理邏
輯佛南,要使用HystrixCommand來(lái)完成。
改造 ConsumerController.java
處理器類(lèi)
package org.example.controller;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
import lombok.extern.slf4j.Slf4j;
import org.example.pojo.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
import java.util.List;
/**
* @ClassName ConsumerController
* @Description TODO
* @Author Patrick Star
* @Date 2020/12/16 2:24 下午
*/
@RestController
@RequestMapping("/consumer")
@Slf4j
public class ConsumerController {
@Autowired
public RestTemplate restTemplate;
// 新增discoveryClient嵌言,注意包名
// import org.springframework.cloud.client.discovery.DiscoveryClient;
@Autowired
private DiscoveryClient discoveryClient;
@GetMapping("{id}")
@HystrixCommand(fallbackMethod = "queryByIdFallback")
public String queryById(@PathVariable Long id) {// 返回值為String
String url = "http://user-service/user/" + id;
return restTemplate.getForObject(url, String.class);// User.class改為String.class
}
// 添加fallback函數(shù)
public String queryByIdFallback(Long id){
log.error("查詢用戶信息失敗嗅回。id:{}", id);
return "對(duì)不起,網(wǎng)絡(luò)太擁擠了摧茴!";
}
}
要注意绵载;因?yàn)槿蹟嗟慕导?jí)邏輯方法必須跟正常邏輯方法保證:相同的參數(shù)列表和返回值聲明。
失敗邏輯中返回User對(duì)象沒(méi)有太大意義苛白,一般會(huì)返回友好提示娃豹。所以把queryById的方法改造為返回String,
反正也是Json數(shù)據(jù)购裙。這樣失敗邏輯中返回一個(gè)錯(cuò)誤說(shuō)明懂版,會(huì)比較方便。
@HystrixCommand(fallbackMethod = "queryByIdFallBack"):用來(lái)聲明一個(gè)降級(jí)邏輯的方法
測(cè)試:當(dāng) user-service 正常提供服務(wù)時(shí)躏率,訪問(wèn)與以前一致定续。但是當(dāng)將 user-service 停機(jī)時(shí),會(huì)發(fā)現(xiàn)頁(yè)面返回了降級(jí)處理信息:
4)默認(rèn)的fallback
剛才把fallback寫(xiě)在了某個(gè)業(yè)務(wù)方法上禾锤,如果這樣的方法很多,那豈不是要寫(xiě)很多摹察。所以可以把Fallback配置加在類(lèi)上恩掷,實(shí)現(xiàn)默認(rèn)fallback;
再次改造 ConsumerController.java
package org.example.controller;
import com.netflix.hystrix.contrib.javanica.annotation.DefaultProperties;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
import lombok.extern.slf4j.Slf4j;
import org.example.pojo.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
import java.util.List;
/**
* @ClassName ConsumerController
* @Description TODO
* @Author Patrick Star
* @Date 2020/12/16 2:24 下午
*/
@RestController
@RequestMapping("/consumer")
@Slf4j
@DefaultProperties(defaultFallback = "defaultFallback") //設(shè)置默認(rèn)fallback
public class ConsumerController {
@Autowired
public RestTemplate restTemplate;
// 新增discoveryClient供嚎,注意包名
// import org.springframework.cloud.client.discovery.DiscoveryClient;
@Autowired
private DiscoveryClient discoveryClient;
@GetMapping("{id}")
// @HystrixCommand(fallbackMethod = "queryByIdFallback")
@HystrixCommand
public String queryById(@PathVariable Long id) {
String url = "http://user-service/user/" + id;
return restTemplate.getForObject(url, String.class);
}
// 添加fallback函數(shù)
public String queryByIdFallback(Long id) {
log.error("查詢用戶信息失敗黄娘。id:{}", id);
return "對(duì)不起,網(wǎng)絡(luò)太擁擠了克滴!";
}
// 默認(rèn)fallback
public String defaultFallback() {
return "默認(rèn)提示:對(duì)不起逼争,網(wǎng)絡(luò)太擁擠了!";
}
}
@DefaultProperties(defaultFallback = "defaultFallBack")
:在類(lèi)上指明統(tǒng)一的失敗降級(jí)方法劝赔;該類(lèi)中所有方法返回類(lèi)型要與處理失敗的方法的返回類(lèi)型一致誓焦。
5)超時(shí)設(shè)置
在之前的案例中,請(qǐng)求在超過(guò)1秒后都會(huì)返回錯(cuò)誤信息着帽,這是因?yàn)镠ystrix的默認(rèn)超時(shí)時(shí)長(zhǎng)為1杂伟,我們可以通過(guò)配置修改這個(gè)值移层;consumer-demo的application.yml 添加如下配置:
hystrix:
command:
default:
execution:
isolation:
thread:
timeoutInMilliseconds: 2000
這個(gè)配置會(huì)作用于全局所有方法。
為了觸發(fā)超時(shí)赫粥,可以在 user-service的UserService.java 的方法中讓其休眠2秒观话;
//根據(jù)id查詢
public User queryById(Long id) {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return userMapper.selectByPrimaryKey(id);
}
測(cè)試一下:
可以發(fā)現(xiàn),請(qǐng)求的時(shí)長(zhǎng)已經(jīng)到了2s+越平,證明配置生效了频蛔。如果把修改時(shí)間修改到2秒以下,又可以正常訪問(wèn)
8.4 服務(wù)熔斷
原理
在服務(wù)熔斷中秦叛,使用的熔斷器晦溪,也叫斷路器,其英文單詞為:Circuit Breaker
熔斷機(jī)制與家里使用的電路熔斷原理類(lèi)似书闸;當(dāng)如果電路發(fā)生短路的時(shí)候能立刻熔斷電路尼变,避免發(fā)生災(zāi)難。在分布式系統(tǒng)中應(yīng)用服務(wù)熔斷后浆劲;服務(wù)調(diào)用方可以自己進(jìn)行判斷哪些服務(wù)反應(yīng)慢或存在大量超時(shí)嫌术,可以針對(duì)這些服務(wù)進(jìn)行主動(dòng)熔斷,防止整個(gè)系統(tǒng)被拖垮牌借。
Hystrix的服務(wù)熔斷機(jī)制度气,可以實(shí)現(xiàn)彈性容錯(cuò);當(dāng)服務(wù)請(qǐng)求情況好轉(zhuǎn)之后膨报,可以自動(dòng)重連磷籍。通過(guò)斷路的方式,將后續(xù)請(qǐng)求直接拒絕现柠,一段時(shí)間(默認(rèn)5秒)之后允許部分請(qǐng)求通過(guò)院领,如果調(diào)用成功則回到斷路器關(guān)閉狀態(tài),否則繼續(xù)打開(kāi)够吩,拒絕請(qǐng)求的服務(wù)比然。
Hystrix的熔斷狀態(tài)機(jī)模型:
狀態(tài)機(jī)有3個(gè)狀態(tài):
Closed:關(guān)閉狀態(tài)(斷路器關(guān)閉),所有請(qǐng)求都正常訪問(wèn)周循。
Open:打開(kāi)狀態(tài)(斷路器打開(kāi))强法,所有請(qǐng)求都會(huì)被降級(jí)。Hystrix會(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),不是永久的置媳,斷路器打開(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í)
實(shí)現(xiàn)熔斷
為了能夠精確控制請(qǐng)求的成功或失敗路捧,在 consumer-demo 的處理器業(yè)務(wù)方法中加入一段邏輯;
修改 consumer-demo的ConsumerController.java
@GetMapping("{id}")
@HystrixCommand
public String queryById(@PathVariable Long id) {
if(id == 1){ throw new RuntimeException("太忙了"); }
String url = "http://user-service/user/" + id;
return restTemplate.getForObject(url, String.class);
}
這樣如果參數(shù)是id為1传黄,一定失敗杰扫,其它情況都成功。(不要忘了清空user-service中的休眠邏輯)
我們準(zhǔn)備兩個(gè)請(qǐng)求窗口:
一個(gè)請(qǐng)求:http://localhost:8080/consumer/1膘掰,注定失敗
一個(gè)請(qǐng)求:http://localhost:8080/consumer/2章姓,肯定成功
當(dāng)我們瘋狂訪問(wèn)id為1的請(qǐng)求時(shí)(超過(guò)20次),就會(huì)觸發(fā)熔斷识埋。斷路器會(huì)打開(kāi)凡伊,一切請(qǐng)求都會(huì)被降級(jí)處理。
此時(shí)你訪問(wèn)id為2的請(qǐng)求窒舟,會(huì)發(fā)現(xiàn)返回的也是失敗系忙,而且失敗時(shí)間很短,只有20毫秒左右惠豺;因進(jìn)入半開(kāi)狀態(tài)之后2是可
以的银还。
不過(guò),默認(rèn)的熔斷觸發(fā)要求較高洁墙,休眠時(shí)間窗較短蛹疯,為了測(cè)試方便,我們可以通過(guò)配置修改熔斷策略:
上述的配置項(xiàng)可以參考 HystrixCommandProperties 類(lèi)中热监。