【Spring Cloud】Eureka、Ribbon、Hystrix

0. 前言

  1. 使用RestTemplate发送请求

  2. 了解SpringCloud的作用

  3. 搭建Eureka注册中心

  4. 了解Robbin负载均衡

  5. 了解Hystrix熔断器

1. 系统架构

graph LR;
1[集中式架构] --> 2[垂直拆分]
2 --> 3[分布式服务]
3 --> 4[SOA面向服务架构]
4 --> 5[微服务架构]

前面在介绍dubbo的时候提过微服务的概念了https://www.jianshu.com/p/310509f4790c

spring cloud则是spring framework在微服务上的设计

微服务的特点

单一职责:微服务中每一个服务都对应唯一的业务能力,做到单一职责

微:微服务的服务拆分粒度很小,例如一个用户管理就可以作为一个服务。每个服务虽小,但“五脏俱全”。

面向服务:面向服务是说每个服务都要对外暴露Rest风格服务接口API。并不关心服务的技术实现,做到与平台

和语言无关,也不限定用什么技术实现,只要提供Rest的接口即可。

自治:自治是说服务间互相独立,互不干扰

  • 团队独立:每个服务都是一个独立的开发团队,人数不能过多。

  • 技术独立:因为是面向服务,提供Rest接口,使用什么技术没有别人干涉

  • 前后端分离:采用前后端分离开发,提供统一Rest接口,后端不用再为PC、移动端开发不同接口

  • 数据库分离:每个服务都使用自己的数据源

  • 部署独立,服务间虽然有调用,但要做到服务重启不影响其它服务。有利于持续集成和持续交付。每个服务都是独立的组件,可复用,可替换,降低耦合,易维护

微服务架构与SOA都是对系统进行拆分;微服务架构基于SOA思想,可以把微服务当做去除了ESB的SOA。ESB是SOA架构中的中心总线,设计图形应该是星形的,而微服务是去中心化的分布式软件架构。两者比较类似,但其实也有一些差别:

功能 SOA 微服务
组件大小 大块业务逻辑 单独任务或小块业务逻辑
耦合 通常松耦合 总是松耦合
管理 着重中央管理 着重分散管理
目标 确保应用能够交互操作 易维护、易扩展、更轻量级的交互

2. 远程调用

无论是微服务还是SOA,都面临着服务间的远程调用。那么服务间的远程调用方式有哪些呢?

常见的远程调用方式有以下2种:

  • RPC:Remote Produce Call远程过程调用,RPC****基于****Socket****,工作在会话层。自定义数据格式,速度快,效率高。早期的webservice,现在热门的dubbo,都是RPC的典型代表

  • Http:http其实是一种网络传输协议,基于****TCP****,工作在应用层,规定了数据传输的格式。现在客户端浏览器与服务端通信基本都是采用Http协议,也可以用来进行远程服务调用。缺点是消息封装臃肿,优势是对服务的提供和调用方没有任何技术限定,自由灵活,更符合微服务理念。现在热门的Rest风格,就可以通过http协议来实现。

区别:RPC的机制是根据语言的API(language API)来定义的,而不是根据基于网络的应用来定义的。

如果你们公司全部采用Java技术栈,那么使用Dubbo作为微服务架构是一个不错的选择。

相反,如果公司的技术栈多样化,而且你更青睐Spring家族,那么Spring Cloud搭建微服务是不二之选。在我们的项目中,会选择Spring Cloud套件,因此会使用Http方式来实现服务间调用。

关于RPC的概念稍微更陌生,可以参考下

谁能用通俗的语言解释一下什么是 RPC 框架?

rpc与rest区别于联系

我个人理解为,rpc多用在系统之间的组件的交互,例如分布式的组件之间的交互。

如何理解RPC和REST

基于RPC的API更加适用行为(也就是命令和过程),基于REST的API更加适用于构建模型(也就是资源和实体),处理CRUD。

  • REST使用HTTP的方法,例如:GET,POST,PUT,DELETE,OPTIONS还有比较不常用的PATCH方法。
  • RPC通常只会使用GET和POST方法,GET方法通常用来获取信息,POST方法可以用来进行所有的行为。

3. Spring的RestTemplate

既然微服务选择了Http,那么我们就需要考虑自己来实现对请求和响应的处理。不过开源世界已经有很多的http客户

端工具,能够帮助我们做这些事情,例如:

  • HttpClient

  • OKHttp

  • URLConnection

不过这些不同的客户端,API各不相同。

而Spring也有对http的客户端进行封装,提供了工具类叫RestTemplate。Spring提供了一个RestTemplate模板工具类,对基于Http的客户端进行了封装,并且实现了对象与json的序列化和反序列化,非常方便。RestTemplate并没有限定Http的客户端类型,而是进行了抽象,目前常用的3种都有支持,其中默认的为JDK原生的URLConnection。

4. Spring Cloud简介

4.1 简介

Spring Cloud是Spring旗下的项目之一,官网地址:http://projects.spring.io/spring-cloud/

Spring最擅长的就是集成,把世界上最好的框架拿过来,集成到自己的项目中。

Spring Cloud也是一样,它将现在非常流行的一些技术整合到一起,实现了诸如:配置管理,服务发现,智能路由,负载均衡,熔断器,控制总线,集群状态等功能;协调分布式环境中各个系统,为各类服务提供模板性配置。其主要

涉及的组件包括:

  • Eureka:注册中心

  • Zuul、Gateway:服务网关

  • Ribbon:负载均衡

  • Feign:服务调用

  • Hystrix或Resilience4j:熔断器

以上只是其中一部分,架构图:

4.2 版本

Spring Cloud不是一个组件,而是许多组件的集合;

SpringCloud则是通过希腊英文字母的方式,在发布的版本时是以伦敦地铁站作为版本命名,并按地铁站名称的首字母A-Z依次命名。

  • 第一代版本: Angle;
  • 第二代版本: Brixton;
  • 第三代版本: Camden;
  • 第四代版本: Dalston;
  • 第五代版本: Edgware;
  • 第六代版本: Finchley;
  • 第七代版本: GreenWich;
  • 第八代版本: Hoxton;

项目中使用新的Hoxton版本

5. 模拟微服务场景

5.1 创建父工程

微服务中需要同时创建多个项目,但为了学习,先创建一个父工程,然后后续的工程都以这个工程为父,实现maven的聚合。这样可以在一个窗口看到所有工程,方便学习。在实际开发中,每个微服务可独立一个工程。

创建一个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启动器 -->
            <dependency>
                <groupId>tk.mybatis</groupId>
                <artifactId>mapper-spring-boot-starter</artifactId>
                <version>${mapper.starter.version}</version>
            </dependency>
            <!-- mysql驱动 -->
            <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 创建user-service工程

在父工程下右键new一个子模块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启动器 -->
        <dependency>
            <groupId>tk.mybatis</groupId>
            <artifactId>mapper-spring-boot-starter</artifactId>
        </dependency>
        <!-- mysql驱动 -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
    </dependencies>

然后我们复用之前的代码并删掉一些不必要的文件和参数https://www.jianshu.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

#激活配置文件;需要制定其他的配置文件名称
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
#日志记录级别
logging:
  level:
    org.example: debug
    org.springframework: info
#tomcat端口
server:
  port: 9091
# mybatis配置
mybatis:
  # 实体类别名包路径
  type-aliases-package: org.example.pojo
  # 映射文件路径
  # mapper-locations: classpath:mappers/*.xml
  configuration:
    # 控制台输出执行sql
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl


自动生成后我想运行启动类,发现运行不了,出现以下错误

java: 程序包org.springframework.boot不存在

以为是没设置source root和resource root和test root,设置完后也没有用。
发现右边栏也没有maven工具栏,问题出在这个项目并没有被idea识别为maven项目,右键我们的pom.xml,点击 mark as maven project,然后就能识别为maven项目了

5.3 创建服务调用者consumer-demo

同样的在parent下创建,修改pom.xml,添加依赖

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
</dependencies>

因为是消费端,不需要直接和数据库打交道,就不用mybatis和通用mapper了

修改启动类,注册一个RestTemplate

@SpringBootApplication
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
    @Bean
    public RestTemplate restTemplate(){
        return new RestTemplate();
    }
}

创建实体类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;

    // 创建时间
    private Date created;

    // 更新时间
    private Date updated;

    // 备注
    private String note;

}

增加一个controller,直接调用RestTemplate,远程访问user-service的服务接口

@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);
    }
}

此时,consumer端没有配置端口,默认就是8080

浏览器输入http://localhost:8080/consumer/1 得到查询结果

5.4 思考问题

简单回顾一下,刚才我们写了什么:我们没有使用spring cloud 是吧,只是单纯的用spring boot把消费端与生产端又做了一遍,这个我们之前都谢过了。

user-service:对外提供了查询用户的接口

consumer-demo:通过RestTemplate访问 http://locahost:9091/user/{id} 接口,查询用户数据

存在什么问题?

  • 在consumer中,我们把url地址硬编码到了代码中,不方便后期维护

  • consumer需要记忆user-service的地址,如果出现变更,可能得不到通知,地址将失效

  • consumer不清楚user-service的状态,服务宕机也不知道

  • user-service只有1台服务,不具备高可用性

  • 即便user-service形成集群,consumer还需自己实现负载均衡

其实上面说的问题,概括一下就是分布式服务必然要面临的问题:

  • 服务管理

    ​ - 如何自动注册和发现

    ​ - 如何实现状态监管

    ​ - 如何实现动态路由

  • 服务如何实现负载均衡

  • 服务如何解决容灾问题

  • 服务如何实现统一配置

以上的问题,都将在SpringCloud中得到答案。

6. Eureka注册中心

首先我们来解决第一问题,服务的管理。

6.1 问题分析

在刚才的案例中,user-service对外提供服务,需要对外暴露自己的地址。而consumer-demo(调用者)需要记录

服务提供者的地址。将来地址出现变更,还需要及时更新。这在服务较少的时候并不觉得有什么,但是在现在日益复

杂的互联网环境,一个项目可能会拆分出十几,甚至几十个微服务。此时如果还人为管理地址,不仅开发困难,将来

测试、发布上线都会非常麻烦,这与DevOps的思想是背道而驰的。

DevOps的思想是系统可以通过一组过程、方法或系统;提高应用发布和运维的效率,降低管理成本。

网约车

这就好比是 网约车出现以前,人们出门叫车只能叫出租车。一些私家车想做出租却没有资格,被称为黑车。而很多

人想要约车,但是无奈出租车太少,不方便。私家车很多却不敢拦,而且满大街的车,谁知道哪个才是愿意载人的。

一个想要,一个愿意给,就是缺少引子,缺乏管理啊。

此时滴滴这样的网约车平台出现了,所有想载客的私家车全部到滴滴注册,记录你的车型(服务类型),身份信息

(联系方式)。这样提供服务的私家车,在滴滴那里都能找到,一目了然。

此时要叫车的人,只需要打开APP,输入你的目的地,选择车型(服务类型),滴滴自动安排一个符合需求的车到你

面前,为你服务,完美!

Eureka做什么?

Eureka就好比是滴滴,负责管理、记录服务提供者的信息。服务调用者无需自己寻找服务,而是把自己的需求告诉

Eureka,然后Eureka会把符合你需求的服务告诉你。

同时,服务提供方与Eureka之间通过 “心跳” 机制进行监控,当某个服务提供方出现问题,Eureka自然会把它从服务

列表中剔除。

这就实现了服务的自动注册、发现、状态监控。

6.2 原理

基本架构

Eureka:就是服务注册中心(可以是一个集群),对外暴露自己的地址

提供者:启动后向Eureka注册自己信息(地址,提供什么服务)

消费者:向Eureka订阅服务,Eureka会将对应服务的所有提供者地址列表发送给消费者,并且定期更新

心跳(续约):提供者定期通过http方式向Eureka刷新自己的状态

6.3 搭建eureka-server工程

创建工程

创建eureka-server,依旧是父工程下,添加依赖

<dependencies>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
    </dependency>
</dependencies>

编写启动类

//声明当前应用为eureka服务
@EnableEurekaServer
@SpringBootApplication
public class EurelaServerApplication {
    public static void main(String[] args) {
        SpringApplication.run(EurelaServerApplication.class);
    }
}

编写配置文件application.yml

server:
  port: 10086
spring:
  application:
    name: eureka-server #应用名称,会在Eureka中作为服务而ID标识(serverId)
eureka:
  client:
    service-url: #EurekaServer的地址,现在是自己的地址,如果是集群,需要写其它Server地址
      defaultZone: http:127.0.0.1:10086/eureka
    register-with-eureka: false # 不注册自己
    fetch-registry: false #不拉取

启动服务

启动 eureka-server 访问:http://127.0.0.1:10086

6.4 服务注册

注册服务,就是在服务上添加Eureka的客户端依赖,客户端代码会自动把服务注册到EurekaServer中。

添加依赖

我们在user-service中添加Eureka客户端依赖:

<!-- Eureka客户端 -->              
                <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>

修改启动类

在启动类上开启Eureka客户端功能,通过添加 @EnableDiscoveryClient 来开启Eureka客户端功能

//声明当前应用为eureka服务
@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
#日志记录级别
logging:
  level:
    org.example: debug
    org.springframework: info
#tomcat端口
server:
  port: 9091
# mybatis配置
mybatis:
  # 实体类别名包路径
  type-aliases-package: org.example.pojo
  # 映射文件路径
  # mapper-locations: classpath:mappers/*.xml
  configuration:
    # 控制台输出执行sql
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

eureka:
  client:
    service-url:
      defaultZone: http://localhost:10086/eureka

注意:

这里我们添加了spring.application.name属性来指定应用名称,将来会作为服务的id使用。

测试

重启 user-service 项目,访问Eureka监控页面,可以看到user-service已经注册成功了

6.5 服务发现

接下来我们修改 consumer-demo ,尝试从EurekaServer获取服务。

方法与服务提供者类似,只需要在项目中添加EurekaClient依赖,就可以通过服务名称来获取信息了!

添加依赖

修改consumer-demo的pom.xml

<!-- Eureka客户端 -->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>

修改启动类

@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 #应用名称
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中注册的user-service实例列表
        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跟踪运行

重启 consumer-demo 项目;然后再浏览器中再次访问 http://localhost:8080/consumer/1 ;在代码中debug跟进查

看最终拼接要访问的URL:

6.6 Eureka详解

基础架构

Eureka架构中的三个核心角色:

服务注册中心

Eureka的服务端应用,提供服务注册和发现功能,就是刚刚我们建立的eureka-server

服务提供者

提供服务的应用,可以是SpringBoot应用,也可以是其它任意技术实现,只要对外提供的是Rest风格服务即可。本例中就是我们实现的user-service

服务消费者

消费应用从注册中心获取服务列表,从而得知每个服务方的信息,知道去哪里调用服务方。本例中就是我们实现的consumer-demo

高可用的Eureka Server

Eureka Server即服务的注册中心,在刚才的案例中,我们只有一个EurekaServer,事实上EurekaServer也可以是一个集群,形成高可用的Eureka中心。

服务同步

多个Eureka Server之间也会互相注册为服务,当服务提供者注册到Eureka Server集群中的某个节点时,该节点会把服务的信息同步给集群中的每个节点,从而实现数据同步。因此,无论客户端访问到Eureka Server集群中的任意一个节点,都可以获取到完整的服务列表信息。

而作为客户端,需要把信息注册到每个Eureka中:

如果有三个Eureka,则每一个EurekaServer都需要注册到其它几个Eureka服务中,例如:有三个分别为10086、10087、10088,则:

10086要注册到10087和10088上

10087要注册到10086和10088上

10088要注册到10086和10087上

搭建单机Eureka Server集群

在单机配置两个Eureka Server,端口分别为 10086 10010

1)修改原来Eureka配置文件

server:
  port: ${port:10086}
spring:
  application:
    name: eureka-server #应用名称,会在Eureka中作为服务而ID标识(serverId)
eureka:
  client:
    service-url:
      # eureka 服务地址,如果是集群的话;需要指定其它集群eureka地址
      defaultZone: http://127.0.0.1:10086/eureka

所谓的高可用注册中心,其实就是把EurekaServer自己也作为一个服务,注册到其它EurekaServer上,这样多个

EurekaServer之间就能互相发现对方,从而形成集群。因此我们做了以下修改:

注意把register-with-eureka和fetch-registry修改为true或者注释掉

在上述配置文件中的${}表示在jvm启动时候若能找到对应port或者defaultZone参数则使用,若无则使用后面

的默认值

把service-url的值改成了另外一台EurekaServer的地址,而不是自己

2)另外一台启动的时候指定端口port和defaultZone配置

点击Edit Configurations...

修改原来的启动配置;在如下界面中的 VM options 中设置 -DdefaultZone=http://127.0.0.1:10010/eureka

将它复制一份命名为10010,再进行配置-Dport=10010 -DdefaultZone=http://127.0.0.1:10086/eureka

3)客户端注册服务到集群

因为EurekaServer不止一个,因此 user-service 项目注册服务或者consumer-demo获取服务的时候,service-url参数需要修改为如下:

eureka:
  client:
    service-url:
      defaultZone: http://localhost:10086/eureka, http://localhost:10010/eureka

4)查看集群

启动两台server查看集群

为了方便后面的演示,在将集群改为单个服务

Erueka客户端

服务提供者要向EurekaServer注册服务,并且完成服务续约等工作。

服务注册

服务提供者在启动时,会检测配置属性中的: eureka.client.register-with-erueka=true 参数是否正确,事实上

默认就是true。如果值确实为true,则会向EurekaServer发起一个Rest请求,并携带自己的元数据信息,Eureka

Server会把这些信息保存到一个双层Map结构中。

  • 第一层Map的Key就是服务id,一般是配置中的 spring.application.name 属性

  • 第二层Map的key是服务的实例id。一般host+ serviceId + port,例如: localhost:user-service:8081

  • 值则是服务的实例对象,也就是说一个服务,可以同时启动多个不同实例,形成集群。

默认注册时使用的是主机名或者localhost,如果想用ip进行注册,可以在 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 ;在调用服务的时候就已经变成ip地址;需要注意的是:不是在

eureka中的控制台服务实例状态显示。

服务续约

在注册服务完成以后,服务提供者会维持一个心跳(定时向EurekaServer发起Rest请求),告诉EurekaServer:“我

还活着”。这个我们称为服务的续约(renew);

有两个重要参数可以修改服务续约的行为;可以在 user-service 中添加如下配置项:

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
    # 续约间隔,默认30秒
    lease-renewal-interval-in-seconds: 5
    # 服务失效时间,默认90秒
    lease-expiration-duration-in-seconds: 5
  • ``lease-renewal-interval-in-second`:服务续约(renew)的间隔,默认为30秒

  • lease-expiration-duration-in-seconds:服务失效时间,默认值90秒

也就是说,默认情况下每隔30秒服务会向注册中心发送一次心跳,证明自己还活着。如果超过90秒没有发送心跳,

EurekaServer就会认为该服务宕机,会定时(eureka.server.eviction-interval-timer-in-ms设定的时间)从服务列表

中移除,这两个值在生产环境不要修改,默认即可。

获取服务列表

当服务消费者启动时,会检测eureka.client.fetch-registry=true 参数的值,如果为true,则会从Eureka Server服务的列表拉取只读备份,然后缓存在本地。并且 每隔30秒 会重新拉取并更新数据。可以在 consumer-demo项目中通过下面的参数来修改:

eureka:
  client:
    service-url:
      defaultZone: http://127.0.0.1:10086/eureka
    registry-fetch-interval-seconds: 30

失效剔除和自我保护

如下的配置都是在Eureka Server服务端进行:

服务下线

当服务进行正常关闭操作时,它会触发一个服务下线的REST请求给Eureka Server,告诉服务注册中心:“我要下线

了”。服务中心接受到请求之后,将该服务置为下线状态。

失效剔除

有时我们的服务可能由于内存溢出或网络故障等原因使得服务不能正常的工作,而服务注册中心并未收到“服务下

线”的请求。相对于服务提供者的“服务续约”操作,服务注册中心在启动时会创建一个定时任务,默认每隔一段时间

(默认为60秒)将当前清单中超时(默认为90秒)没有续约的服务剔除,这个操作被称为失效剔除。

可以通过 eureka.server.eviction-interval-timer-in-ms 参数对其进行修改,单位是毫秒。

自我保护

我们关停一个服务,很可能会在Eureka面板看到一条警告:

这是触发了Eureka的自我保护机制。当服务未按时进行心跳续约时,Eureka会统计服务实例最近15分钟心跳续约的比例是否低于了85%。

在生产环境下,因为网络延迟等原因,心跳失败实例的比例很有可能超标,但是此时就把服务剔除列表并不妥当,因为服务可能没有宕机。Eureka在这段时间内不会剔除任何服务实例,直到网络恢复正常。

生产环境下这很有效,保证了大多数服务依然可用,不过也有可能获取到失败的服务实例,因此服务调用者必须做好服务的失败容错。

可以通过下面的配置来关停自我保护:

eureka:
  client:
    service-url:
      # eureka 服务地址,如果是集群的话;需要指定其它集群eureka地址
      defaultZone: ${defaultZone:http://127.0.0.1:10010/eureka}
  server:
    enable-self-preservation: false #关闭自我保护模式,默认为打开

7. 负载均衡Ribbon

7.1 什么是ribbon

在刚才的案例中,我们启动了一个 user-service ,然后通过DiscoveryClient来获取服务实例信息,然后获取ip和端口来访问。

但是实际环境中,往往会开启很多个 user-service 的集群。此时获取的服务列表中就会有多个,到底该访问哪一个

呢?

一般这种情况下就需要编写负载均衡算法,在多个实例列表中进行选择。不过Eureka中已经集成了负载均衡组件:Ribbon,简单修改代码即可使用。

什么是Ribbon:

Ribbon 是一个基于 HTTP 和 TCP 的 客服端负载均衡工具,它是基于 Netflix Ribbon 实现的。

它不像 Spring Cloud 服务注册中心、配置中心、API 网关那样独立部署,但是它几乎存在于每个 Spring Cloud 微服务中。包括 Feign 提供的声明式服务调用也是基于该 Ribbon 实现的。

Ribbon 默认提供很多种负载均衡算法,例如轮询、随机等等。甚至包含自定义的负载均衡算法。

7.2 启动两个user-service实例

我们和上面一样,修改两个user-service的端口,一个为9091,一个为9092

7.3 开启负载均衡

因为Eureka中已经集成了Ribbon,所以我们无需引入新的依赖。修改consumer的启动类。

给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调用方式,不再手动获取ip和端口,而是直接通过服务名称调用;

  @GetMapping("{id}")
    public User queryById(@PathVariable Long id) {
        String url = "http://user-service/user/" + id;

        // 获取eureka中注册的user-service实例列表
//        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);
    }

访问http://localhost:8080/consumer/1

查看userService 9091和9092的控制台,只有9091的端口打印了日志信息

Ribbon默认的负载均衡策略是轮询。SpringBoot也帮提供了修改负载均衡规则的配置入口在consumer�

demo的配置文件中添加如下,就变成随机的了:

在配置文件中添加如下

user-service:
  ribbon:
    NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule

格式是: {服务名称}.ribbon.NFLoadBalancerRuleClassName

反复请求测试一下,发现9091和9092的user-service都能收到信息打印日志

7.3 源码跟踪

为什么只输入了service名称就可以访问了呢?之前还要获取ip和端口。

显然是有组件根据service名称,获取到了服务实例的ip和端口。因为 consumer-demo 使用的是RestTemplate

spring的负载均衡自动配置类LoadBalancerAutoConfiguration.LoadBalancerInterceptorConfig会自动配置

负载均衡拦截器(在spring-cloud-commons-**.jar包中的spring.factories中定义的自动配置类), 它就是

LoadBalancerInterceptor ,这个类会在对RestTemplate的请求进行拦截,然后从Eureka根据服务id获取服务列

表,随后利用负载均衡算法得到真实的服务地址信息,替换服务id。

我们进行源码跟踪,查找LoadBalancerInterceptor并在serviceName上打上断点

跟进excute方法

可以看到获取了9092端口的服务

贼跟进下一次,发现后去的是9091

多次访问 consumer-demo 的请求地址;然后跟进代码,点左边栏的小箭头(resume program)可以快速调试,发现其果然实现了负载均衡。

8. 熔断器Hystrix

8.1 简介

Hystrix是Netflflix开源的一个延迟和容错库,用于隔离访问远程服务、第三方库,防止出现级联失败。Hystrix 在英文里面的意思是 豪猪,它的logo 的图是一头豪猪,它在微服务系统中是一款提供保护机制的组件,和eureka一样也是由netflflix公司开发。

8.2 雪崩

微服务中,服务间调用关系错综复杂,一个请求,可能需要调用多个微服务接口才能实现,会形成非常复杂的调用链路:

如图,一次业务请求,需要调用A、P、H、I四个服务,这四个服务又可能调用其它服务。

如果此时,某个服务出现异常:

例如: 微服务I 发生异常,请求阻塞,用户请求就不会得到响应,则tomcat的这个线程不会释放,于是越来越多的

用户请求到来,越来越多的线程会阻塞:

服务器支持的线程和并发数有限,请求一直阻塞,会导致服务器资源耗尽,从而导致所有其它服务都不可用,形成雪崩效应。

这就好比,一个汽车生产线,生产不同的汽车,需要使用不同的零件,如果某个零件因为种种原因无法使用,那么就会造成整台车无法装配,陷入等待零件的状态,直到零件到位,才能继续组装。 此时如果有很多个车型都需要这个零件,那么整个工厂都将陷入等待的状态,导致所有生产都陷入瘫痪。一个零件的波及范围不断扩大。

Hystrix解决雪崩问题的手段主要是服务降级,包括:

  • 线程隔离

  • 服务熔断

8.3 线程隔离和服务降级

原理

线程隔离示意图

  • Hystrix为每个依赖服务调用分配一个小的线程池,如果线程池已满调用将被立即拒绝,默认不采用排队,加速失败判定时间

  • 用户的请求将不再直接访问服务,而是通过线程池中的空闲线程来访问服务,如果线程池已满,或者请求超时,则会进行降级处理,什么是服务降级?

服务降级:优先保证核心服务,而非核心服务不可用或弱可用。

用户的请求故障时,不会被阻塞,更不会无休止的等待或者看到系统崩溃,至少可以看到一个执行结果(例如返回友好的提示信息) 。

服务降级虽然会导致请求失败,但是不会导致阻塞,而且最多会影响这个依赖服务对应的线程池中的资源,对其它服务没有响应。

触发Hystrix服务降级的情况:

  • 线程池已满

  • 请求超时

实现线程隔离

1)引入依赖

在 consumer-demo 消费端系统的pom.xml文件添加如下依赖:

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>

2)开启熔断

在启动类 ConsumerApplication 上添加注解:@EnableCircuitBreaker

@SpringBootApplication
@EnableDiscoveryClient
@EnableCircuitBreaker //线程隔离
public class Application {
 ... ...
}

可以看到,我们类上的注解越来越多,在微服务中,经常会引入上面的三个注解,于是Spring就提供了一个组合注解:@SpringCloudApplication,来看看他的实现

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootApplication
@EnableDiscoveryClient
@EnableCircuitBreaker
public @interface SpringCloudApplication {

}

正好包括了上面三个,因此我们用组合注解代替上面是三个,

@SpringCloudApplication
public class Application {
 ... ...
}

3)编写降级逻辑

当目标服务的调用出现故障,我们希望快速失败,给用户一个友好提示。因此需要提前编写好失败时的降级处理逻

辑,要使用HystrixCommand来完成。

改造 ConsumerController.java处理器类

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函数
    public String queryByIdFallback(Long id){
        log.error("查询用户信息失败。id:{}", id);
        return "对不起,网络太拥挤了!";
    }
}

要注意;因为熔断的降级逻辑方法必须跟正常逻辑方法保证:相同的参数列表和返回值声明

失败逻辑中返回User对象没有太大意义,一般会返回友好提示。所以把queryById的方法改造为返回String,

反正也是Json数据。这样失败逻辑中返回一个错误说明,会比较方便。

@HystrixCommand(fallbackMethod = "queryByIdFallBack"):用来声明一个降级逻辑的方法

测试:当 user-service 正常提供服务时,访问与以前一致。但是当将 user-service 停机时,会发现页面返回了降级处理信息:

4)默认的fallback

刚才把fallback写在了某个业务方法上,如果这样的方法很多,那岂不是要写很多。所以可以把Fallback配置加在类上,实现默认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") //设置默认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函数
    public String queryByIdFallback(Long id) {
        log.error("查询用户信息失败。id:{}", id);
        return "对不起,网络太拥挤了!";
    }
    // 默认fallback
    public String defaultFallback() {
        return "默认提示:对不起,网络太拥挤了!";
    }

}

@DefaultProperties(defaultFallback = "defaultFallBack"):在类上指明统一的失败降级方法;该类中所有方法返回类型要与处理失败的方法的返回类型一致。

5)超时设置

在之前的案例中,请求在超过1秒后都会返回错误信息,这是因为Hystrix的默认超时时长为1,我们可以通过配置修改这个值;consumer-demo的application.yml 添加如下配置:

hystrix:
  command:
    default:
      execution:
        isolation:
          thread:
            timeoutInMilliseconds: 2000

这个配置会作用于全局所有方法。

为了触发超时,可以在 user-service的UserService.java 的方法中让其休眠2秒;

        //根据id查询
    public User queryById(Long id) {
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return userMapper.selectByPrimaryKey(id);
    }

测试一下:

可以发现,请求的时长已经到了2s+,证明配置生效了。如果把修改时间修改到2秒以下,又可以正常访问

8.4 服务熔断

原理

在服务熔断中,使用的熔断器,也叫断路器,其英文单词为:Circuit Breaker

熔断机制与家里使用的电路熔断原理类似;当如果电路发生短路的时候能立刻熔断电路,避免发生灾难。在分布式系统中应用服务熔断后;服务调用方可以自己进行判断哪些服务反应慢或存在大量超时,可以针对这些服务进行主动熔断,防止整个系统被拖垮。

Hystrix的服务熔断机制,可以实现弹性容错;当服务请求情况好转之后,可以自动重连。通过断路的方式,将后续请求直接拒绝,一段时间(默认5秒)之后允许部分请求通过,如果调用成功则回到断路器关闭状态,否则继续打开,拒绝请求的服务。

Hystrix的熔断状态机模型:

状态机有3个状态:

  • Closed:关闭状态(断路器关闭),所有请求都正常访问。

  • Open:打开状态(断路器打开),所有请求都会被降级。Hystrix会对请求情况计数,当一定时间内失败请求百分比达到阈值,则触发熔断,断路器会完全打开。默认失败比例的阈值是50%,请求次数最少不低于20次。

  • Half Open:半开状态,不是永久的,断路器打开后会进入休眠时间(默认是5S)。随后断路器会自动进入半开状态。此时会释放部分请求通过,若这些请求都是健康的,则会关闭断路器,否则继续保持打开,再次进行休眠计时

实现熔断

为了能够精确控制请求的成功或失败,在 consumer-demo 的处理器业务方法中加入一段逻辑;

修改 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);
    }

这样如果参数是id为1,一定失败,其它情况都成功。(不要忘了清空user-service中的休眠逻辑)

我们准备两个请求窗口:

一个请求:http://localhost:8080/consumer/1,注定失败

一个请求:http://localhost:8080/consumer/2,肯定成功

当我们疯狂访问id为1的请求时(超过20次),就会触发熔断。断路器会打开,一切请求都会被降级处理。

此时你访问id为2的请求,会发现返回的也是失败,而且失败时间很短,只有20毫秒左右;因进入半开状态之后2是可

以的。

不过,默认的熔断触发要求较高,休眠时间窗较短,为了测试方便,我们可以通过配置修改熔断策略:

上述的配置项可以参考 HystrixCommandProperties 类中。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,425评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,058评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,186评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,848评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,249评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,554评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,830评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,536评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,239评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,505评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,004评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,346评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,999评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,060评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,821评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,574评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,480评论 2 267

推荐阅读更多精彩内容