【Spring Cloud】Eureka街图、Ribbon域携、Hystrix

0. 前言

  1. 使用RestTemplate發(fā)送請(qǐng)求

  2. 了解SpringCloud的作用

  3. 搭建Eureka注冊(cè)中心

  4. 了解Robbin負(fù)載均衡

  5. 了解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 框架涵叮?

rpc與rest區(qū)別于聯(lián)系

我個(gè)人理解為惭蹂,rpc多用在系統(tǒng)之間的組件的交互伞插,例如分布式的組件之間的交互。

如何理解RPC和REST

基于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)中热监。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末捺弦,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子狼纬,更是在濱河造成了極大的恐慌,老刑警劉巖骂际,帶你破解...
    沈念sama閱讀 218,941評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件疗琉,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡歉铝,警方通過(guò)查閱死者的電腦和手機(jī)盈简,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,397評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人柠贤,你說(shuō)我怎么就攤上這事香浩。” “怎么了臼勉?”我有些...
    開(kāi)封第一講書(shū)人閱讀 165,345評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵邻吭,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我宴霸,道長(zhǎng)囱晴,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,851評(píng)論 1 295
  • 正文 為了忘掉前任瓢谢,我火速辦了婚禮畸写,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘氓扛。我一直安慰自己枯芬,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,868評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布采郎。 她就那樣靜靜地躺著千所,像睡著了一般。 火紅的嫁衣襯著肌膚如雪尉剩。 梳的紋絲不亂的頭發(fā)上真慢,一...
    開(kāi)封第一講書(shū)人閱讀 51,688評(píng)論 1 305
  • 那天,我揣著相機(jī)與錄音理茎,去河邊找鬼黑界。 笑死,一個(gè)胖子當(dāng)著我的面吹牛皂林,可吹牛的內(nèi)容都是我干的朗鸠。 我是一名探鬼主播,決...
    沈念sama閱讀 40,414評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼础倍,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼烛占!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起沟启,我...
    開(kāi)封第一講書(shū)人閱讀 39,319評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤忆家,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后德迹,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體芽卿,經(jīng)...
    沈念sama閱讀 45,775評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,945評(píng)論 3 336
  • 正文 我和宋清朗相戀三年胳搞,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了卸例。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片称杨。...
    茶點(diǎn)故事閱讀 40,096評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖筷转,靈堂內(nèi)的尸體忽然破棺而出姑原,到底是詐尸還是另有隱情,我是刑警寧澤呜舒,帶...
    沈念sama閱讀 35,789評(píng)論 5 346
  • 正文 年R本政府宣布锭汛,位于F島的核電站,受9級(jí)特大地震影響阴绢,放射性物質(zhì)發(fā)生泄漏店乐。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,437評(píng)論 3 331
  • 文/蒙蒙 一呻袭、第九天 我趴在偏房一處隱蔽的房頂上張望眨八。 院中可真熱鬧左电,春花似錦连舍、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,993評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)丽已。三九已至丑婿,卻和暖如春诀拭,著一層夾襖步出監(jiān)牢的瞬間迁筛,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,107評(píng)論 1 271
  • 我被黑心中介騙來(lái)泰國(guó)打工耕挨, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留细卧,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,308評(píng)論 3 372
  • 正文 我出身青樓俗孝,卻偏偏與公主長(zhǎng)得像酒甸,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子赋铝,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,037評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容