7.1 微服务-SpringCloud

目录

7.1.1 微服务概述

7.1.1.1 系统架构演变

7.1.1.1.1 单体架构

7.1.1.1.2 垂直拆分

7.1.1.1.3 分布式服务

7.1.1.1.4 SOA

7.1.1.1.5 微服务

7.1.1.2 服务调用方式

7.1.1.2.1 RPC和HTTP

7.1.1.2.2 区别

7.1.1.3 Http客户端工具

7.1.1.4 RestTemplate

7.1.2 SpringCloud概述

7.1.2.1 什么是SpringCloud

7.1.2.2 SpringCloudAlibaba

7.1.3 Eureka注册中心

7.1.3.1 原理

7.1.3.2 模拟微服务场景

7.1.3.2.1 项目搭建

7.1.3.3 Eureka详解

7.1.3.3.1 基础架构

7.1.3.3.2 高可用的Eureka Server

7.1.3.3.3 服务提供者

7.1.3.3.4 服务消费者

7.1.3.3.5 失效剔除和自我保护

7.1.4  Ribbon—负载均衡

7.1.4.1 什么是Ribbon

 7.1.4.2 开启负载均衡

 7.1.4.2.1 启动两个服务实例

7.1.4.3 负载均衡源码分析

7.1.4.4 负载均衡策略


7.1.1 微服务概述

7.1.1.1 系统架构演变

Java架构的演变_想了好久才取了这的博客-CSDN博客

7.1.1.1.1 单体架构

只需一个应用,将所有功能都部署在一起,以减少部署节点和成本。此时,用于简化增删改查工作量的数据访问框架(ORM)是影响项目开发的关键。

 存在的问题

  • 代码耦合,开发维护困难
  • 无法针对不同模块进行针对性优化
  • 无法水平扩展
  • 单点容错率低,并发能力差

7.1.1.1.2 垂直拆分

当访问量逐渐增大,单一应用无法满足需求,此时为了应对更高的并发和业务需求,我们根据业务功能对系统进行拆分

优点

  • 系统拆分实现了流量分担,解决了并发问题
  • 可以针对不同模块进行优化
  • 方便水平扩展,负载均衡,容错率提高

缺点

  • 系统间相互独立,会有很多重复开发工作,影响开发效率

7.1.1.1.3 分布式服务

当垂直应用越来越多,应用之间交互不可避免,将核心业务抽取出来,作为独立的服务,逐渐形成稳定的服务中心,使前端应用能更快速的响应多变的市场需求。此时,用于提高业务复用及整合的分布式调用是关键。

 优点:

  • 将基础服务进行了抽取,系统间相互调用,提高了代码复用和开发效率

缺点:

  • 系统间耦合度变高,调用关系错综复杂,难以维护

7.1.1.1.4 SOA

SOA :面向服务架构  

服务越来越多,需要管理每个服务的地址;调用关系错综复杂,难以理清依赖关系;服务状态难以管理,无法根据服务情况动态管理等等问题逐渐显现。

此时需增加一个调度中心基于访问压力实时管理集群容量,提高集群利用率。

因此用于提高机器利用率的资源调度和治理中心(SOA)是关键。

服务注册中心,实现服务自动注册和发现,无需人为记录服务地址

服务自动订阅,服务列表自动推送,服务调用透明化,无需关心依赖关系

动态监控服务状态监控报告,人为控制服务状态

缺点:

  • 服务间会有依赖关系,一旦某个环节出错会影响较大

  • 服务关系复杂,运维、测试部署困难,不符合DevOps思想

7.1.1.1.5 微服务

微服务的特点:

  • 单一职责:微服务中每一个服务都对应唯一的业务能力,做到单一职责
  • 微:微服务的服务拆分粒度很小,例如一个用户管理就可以作为一个服务。每个服务虽小,但“五脏俱全”。
  • 面向服务:面向服务是说每个服务都要对外暴露Rest风格服务接口API。并不关心服务的技术实现,做到与平台和语言无关,也不限定用什么技术实现,只要提供Rest的接口即可。
  • 自治:自治是说服务间互相独立,互不干扰
    • 团队独立:每个服务都是一个独立的开发团队,人数不能过多。
    • 技术独立:因为是面向服务,提供Rest接口,使用什么技术没有别人干涉
    • 前后端分离:采用前后端分离开发,提供统一Rest接口,后端不用再为PC、移动段开发不同接口
    • 数据库分离:每个服务都使用自己的数据源
    • 部署独立,服务间虽然有调用,但要做到服务重启不影响其它服务。有利于持续集成和持续交付。每个服务都是独立的组件,可复用,可替换,降低耦合,易维护

7.1.1.2 服务调用方式

7.1.1.2.1 RPC和HTTP

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

  • RPC:Remote Produce Call远程过程调用,类似的还有RMI。自定义数据格式,基于原生TCP通信,速度快,效率高。早期的webservice,现在热门的dubbo,都是RPC的典型代表

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

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

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

由于HTTP比较灵活更符合微服务理念,所以前端用的较多

而由于RPC速度快,效率高,所以后端使用的较多

7.1.1.2.2 区别

Rpc和Http的区别 - 知乎

7.1.1.3 Http客户端工具

很多的http客户端工具可以帮助实现对请求和响应的处理:

  • HttpClient
  • OKHttp
  • URLConnection

但是,不同的客户端,API各不相同

因此出现了Spring的RestTemplate,将这些API都封装进去

7.1.1.4 RestTemplate

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

  • HttpClient
  • OkHttp
  • JDK原生的URLConnection(默认的)

7.1.2 SpringCloud概述

7.1.2.1 什么是SpringCloud

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

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

SpringCloud也是一样,它将现在非常流行的一些技术整合到一起,实现了诸如:配置管理,服务发现,智能路由,负载均衡,熔断器,控制总线,集群状态等等功能。其主要涉及的组件包括:

  • Eureka:服务治理组件,包含服务注册中心,服务注册与发现机制的实现。(服务治理,服务注册/发现)
  • Zuul:网关组件,提供智能路由,访问过滤功能
  • Ribbon:客户端负载均衡的服务调用组件(客户端负载)
  • Feign:服务调用,给予Ribbon和Hystrix的声明式服务调用组件 (声明式服务调用)
  • Hystrix:容错管理组件,实现断路器模式,帮助服务依赖中出现的延迟和为故障提供强大的容错能力。(熔断、断路器,容错)

版本问题

7.1.2.2 SpringCloudAlibaba

7.1.3 Eureka注册中心

7.1.3.1 原理

7.1.3.2 模拟微服务场景

模拟一个服务调用的场景,搭建两个工程:service-provider(服务提供方)和service-consumer(服务调用方)。方便后面学习微服务架构

服务提供方:使用mybatis操作数据库,实现对数据的增删改查;并对外提供rest接口服务。

服务消费方:使用restTemplate远程调用服务提供方的rest接口服务,获取数据。

7.1.3.2.1 项目搭建

 Eureka模块项目结构

 首先处理pom.xmlw

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.0.6.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

    <groupId>com.bl</groupId>
    <artifactId>service_provider</artifactId>
    <version>1.0-SNAPSHOT</version>
  
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>1.3.2</version>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <!-- 需要手动引入通用mapper的启动器,spring没有收录该依赖 -->
        <dependency>
            <groupId>tk.mybatis</groupId>
            <artifactId>mapper-spring-boot-starter</artifactId>
            <version>2.0.2</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
    </dependencies>
</project>

分别向pom.xml文件中

<properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>1.8</java.version>
        <!--springcloud版本对应springboot版本-->
        <spring-cloud.version>Finchley.SR2</spring-cloud.version>
</properties>
  <!-- SpringCloud依赖管理  不是真正加依赖,而是指定版本管理 方便后面的引入相应版本的依赖-->
    <!-- SpringCloud依赖管理 -->
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring-cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>
 <!--eureka服务端
        1.上报自己的信息到eureka注册中心
        2. 从eureka中拉取服务列表-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
            <version>2.0.2.RELEASE</version>
        </dependency>

修改application.yml

server:
  port: 10086

spring:
  datasource:
    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql://localhost:3306/mydb?useUnicode=yes&characterEncoding=UTF-8&useSSL=false&serverTimezone=UTC
    username: root
    password: 123456
  application:
    name: service_provider # 应用名称,会在Eureka中显示

mybatis:
  type-aliases-package: com.bl.model

eureka:
  client:
    service-url:  # EurekaServer的地址,现在是自己的地址,如果是集群,需要加上其它Server的地址。
      defaultZone: http://localhost:10086/eureka/   

完整的Eureka服务端的pom.xml文件如下

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <!--springboot父类工程-->
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.0.6.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.bl</groupId>
    <artifactId>Eureka</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>1.8</java.version>
        <!--springcloud版本对应springboot版本-->
        <spring-cloud.version>Finchley.SR2</spring-cloud.version>
    </properties>
    <!-- SpringCloud依赖管理  不是真正加依赖,而是指定版本管理 方便后面的引入相应版本的依赖-->
    <!-- SpringCloud依赖管理 -->
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring-cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

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

修改启动类,在类上添加@EnableEurekaServer注解 

EurekaApplication 启动类 

@SpringBootApplication
@EnableEurekaServer
//通过添加`@EnableDiscoveryClient`来开启Eureka客户端功能
public class EurekaApplication {
    public static void main(String[] args) {
        SpringApplication.run(EurekaApplication.class, args);
    }
}

service_provider模块项目结构 

pom.xml处理同上,但是要注意此时的依赖为客户端,而非服务器

spring-cloud-starter-netflix-eureka-client

    <!--eureka客户端
        1.上报自己的信息到eureka注册中心
        2. 从eureka中拉取服务列表-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
            <version>2.0.2.RELEASE</version>
        </dependency>

完整pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.0.6.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.bl</groupId>
    <artifactId>service_provider</artifactId>
    <version>1.0-SNAPSHOT</version>


    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>1.8</java.version>
        <!--springcloud版本对应springboot版本-->
        <spring-cloud.version>Finchley.SR2</spring-cloud.version>
    </properties>

    <!-- SpringCloud依赖管理  不是真正加依赖,而是指定版本管理 方便后面的引入相应版本的依赖-->
    <!-- SpringCloud依赖管理 -->
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring-cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>1.3.2</version>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <!-- 需要手动引入通用mapper的启动器,spring没有收录该依赖 -->
        <dependency>
            <groupId>tk.mybatis</groupId>
            <artifactId>mapper-spring-boot-starter</artifactId>
            <version>2.0.2</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
        <!--eureka客户端
        1.上报自己的信息到eureka注册中心
        2. 从eureka中拉取服务列表-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
            <version>2.0.2.RELEASE</version>
        </dependency>
    </dependencies>
</project>

  application.yml

server:
  port: 8080

spring:
  datasource:
    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql://localhost:3306/mydb?useUnicode=yes&characterEncoding=UTF-8&useSSL=false&serverTimezone=UTC
    username: root
    password: 123456
  application:
    name: service_provider # 应用名称,会在Eureka中显示

mybatis:
  type-aliases-package: com.bl.model

eureka:
  client:
    service-url:  # EurekaServer的地址,现在是自己的地址,如果是集群,需要加上其它Server的地址。
      defaultZone: http://localhost:10086/eureka/

通用mapper 

@Mapper
public interface UserMapper extends tk.mybatis.mapper.common.Mapper<User>{

}

model层user

@Table(name="user")
@Data
public class User implements Serializable {
    @Id
    private Integer id;
    private String name;
    private String password;
}

UserserviceImpl  

@Service
public class UserserviceImpl implements Userservice {
    @Autowired
    private UserMapper userMapper;
    @Override
    public User findById(int id) {
        System.out.println(userMapper.selectAll());
        return userMapper.selectByPrimaryKey(id);
    }
}

UserController 

@Controller
@RequestMapping("provider")
public class UserController {

    @Autowired
    private UserserviceImpl userServiceImpl;

    @RequestMapping("findbyid/{id}")
    @ResponseBody
    public User findById(@PathVariable("id") int id) {
        System.out.println(userServiceImpl.findById(id));
        return userServiceImpl.findById(id);
    }
}

service_consumer模块项目结构

完整pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.0.6.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>org.example</groupId>
    <artifactId>service_consumer</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>1.8</java.version>
        <!--springcloud版本对应springboot版本-->
        <spring-cloud.version>Finchley.SR2</spring-cloud.version>
    </properties>

    <!-- SpringCloud依赖管理  不是真正加依赖,而是指定版本管理 方便后面的引入相应版本的依赖-->
    <!-- SpringCloud依赖管理 -->
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring-cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>


    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>1.3.2</version>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <!-- 需要手动引入通用mapper的启动器,spring没有收录该依赖 -->
        <dependency>
            <groupId>tk.mybatis</groupId>
            <artifactId>mapper-spring-boot-starter</artifactId>
            <version>2.0.2</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
        <!--eureka客户端
       1.上报自己的信息到eureka注册中心
       2. 从eureka中拉取服务列表-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
            <version>2.0.2.RELEASE</version>
        </dependency>
    </dependencies>
</project>

application.yml

server:
  port: 8081

spring:
  datasource:
    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql://localhost:3306/mydb?useUnicode=yes&characterEncoding=UTF-8&useSSL=false&serverTimezone=UTC
    username: root
    password: 123456
  application:
    name: service_consumer # 应用名称,会在Eureka中显示

mybatis:
  type-aliases-package: com.bl.model

eureka:
  client:
    service-url:  # EurekaServer的地址,现在是自己的地址,如果是集群,需要加上其它Server的地址。
      defaultZone: http://localhost:10086/eureka/

ConsumerApplication 启动类 

@SpringBootApplication
@EnableDiscoveryClient //springcloud提供的
public class ConsumerApplication {
        public static void main(String[] args) {
            SpringApplication.run(ConsumerApplication.class,args);
        }
//由于@SpringBootApplication中包含@configuration,所以可以充当配置类
//开启客户端后需要手动配置RestTemplate
    @Bean
    RestTemplate restTemplate(){
        return new RestTemplate();
    }

}

 UserController 

@RestController
@RequestMapping("/consumer")
public class UserController {
//需要在配置类中手动配置RestTemplate !!!
    @Resource
    RestTemplate restTemplate;
    @Autowired
    DiscoveryClient discoveryClient;

    @RequestMapping("/findbyid/{id}")
    public User findById(@PathVariable Integer id){
        // 使用RestTemplate远程调用提供方
        // User user = restTemplate.getForObject("http://localhost:8081/provider/findById/" + id, User.class);

        // 根据服务名称获取实例列表
        List<ServiceInstance> instances = discoveryClient.getInstances("service-provider");
//由于只有一个eureka存在,所以get(0)获取到的第一个实例就是存在的eureka
        ServiceInstance instance = instances.get(0);
        // 服务地址
        String url = "http://" + instance.getHost() + ":" + instance.getPort() + "/provider/findById/" + id;
        // 调用
        User user = restTemplate.getForObject(url, User.class);
        System.out.println(user);
        return user;
    }
}

 运行三个模块,打开浏览器访问localhost:10086,进入Eureka默认的图像化管理页面

7.1.3.3 Eureka详解

7.1.3.3.1 基础架构

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

  • 服务注册中心
  • Eureka的服务端应用,提供服务注册和发现功能,就是刚刚我们建立的eureka。
  • 服务提供者
  • 提供服务的应用,可以是SpringBoot应用,也可以是其它任意技术实现,只要对外提供的是Rest风格服务即可。本例中就是我们实现的service-provider。
  • 服务消费者
  • 消费应用从注册中心获取服务列表,从而得知每个服务方的信息,知道去哪里调用服务方。本例中就是我们实现的service-consumer。

7.1.3.3.2 高可用的Eureka Server

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

服务同步

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

动手搭建高可用的EurekaServer

我们假设要运行两个EurekaServer的集群,端口分别为:10086和10087。只需要把eureka启动两次即可。

注意:idea中一个应用不能启动两次,我们需要重新配置一个启动器

按如下方式创建两个eureka启动器

修改eureka的application.yml

所谓的高可用注册中心,其实就是把Eureka自己也作为一个服务进行注册,这样多个Eureka之间就能互相发现对方,从而形成集群。因此我们做了以下修改:

把service-url的值改成了另外一台Eureka的地址,而非自己

server:
  port: 10086 # 端口
spring:
  application:
    name: eureka_server # 应用名称,会在Eureka中显示
eureka:
  client:
    service-url: # EurekaServer的地址,现在是自己的地址,如果是集群,需要加上其它Server的地址。
     defaultZone: http://localhost:10087/eureka/

启动第一个 EurekaApplication

启动报错,正常现象,因为10087服务没有启动:

再次修改eureka的配置

server:
  port: 10087 # 端口
spring:
  application:
    name: eureka-server # 应用名称,会在Eureka中显示
eureka:
  client:
    service-url: # 配置其他Eureka服务的地址,而不是自己,比如10087
      defaultZone: http://127.0.0.1:10086/eureka

 启动第二个EurekaApplication

 访问localhost:10087

客户端注册服务到集群

因为EurekaServer不止一个,因此注册服务的时候,service-url参数需要变化:

eureka:
  client:
    service-url: # EurekaServer地址,多个地址以','隔开
      defaultZone: http://127.0.0.1:10086/eureka,http://127.0.0.1:10087/eureka

7.1.3.3.3 服务提供者

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

服务注册

服务提供者在启动时,会检测配置属性中的:eureka.client.register-with-eureka=true参数是否正确,事实上默认就是true。如果值确实为true,则会向EurekaServer发起一个Rest请求,并携带自己的元数据信息,Eureka Server会把这些信息保存到一个双层Map结构中。

Map<serviceId,Map<服务实例id,实例对象instance>>

  • 第一层Map的Key就是服务id,一般是配置中的spring.application.name属性
  • 第二层Map的key是服务的实例id。一般host+ serviceId + port,例如:locahost:service-provider:8081
  • 值则是服务的实例对象,也就是说一个服务,可以同时启动多个不同实例,形成集群。

服务续约

在注册服务完成以后,服务提供者会维持一个心跳(定时向EurekaServer发起Rest请求),告诉EurekaServer:“我还活着”。这个我们称为服务的续约(renew);

有两个重要参数可以修改服务续约的行为:

eureka:
  instance:
    lease-expiration-duration-in-seconds: 90
    lease-renewal-interval-in-seconds: 30
  • lease-renewal-interval-in-seconds:服务续约(renew)的间隔,默认为30秒
  • lease-expiration-duration-in-seconds:服务失效时间,默认值90秒

也就是说,默认情况下每个30秒服务会向注册中心发送一次心跳,证明自己还活着。如果超过90秒没有发送心跳,EurekaServer就会认为该服务宕机,会从服务列表中移除,这两个值在生产环境不要修改,默认即可。

但是在开发时,这个值有点太长了,经常我们关掉一个服务,会发现Eureka依然认为服务在活着。所以我们在开发阶段可以适当调小。

eureka:
  instance:
    lease-expiration-duration-in-seconds: 10 # 10秒即过期
    lease-renewal-interval-in-seconds: 5 # 5秒一次心跳

7.1.3.3.4 服务消费者

获取服务列表

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

eureka:
  client:
    registry-fetch-interval-seconds: 5

生产环境中,我们不需要修改这个值。

但是为了开发环境下,能够快速得到服务的最新状态,我们可以将其设置小一点。

7.1.3.3.5 失效剔除和自我保护

服务下线

当服务进行正常关闭操作时,它会触发一个服务下线的REST请求给Eureka Server,告诉服务注册中心:“我要下线了”。服务中心接受到请求之后,将该服务置为下线状态。

失效剔除

有些时候,我们的服务提供方并不一定会正常下线,可能因为内存溢出、网络故障等原因导致服务无法正常工作。Eureka Server需要将这样的服务剔除出服务列表。因此它会开启一个定时任务,每隔60秒对所有失效的服务(超过90秒未响应)进行剔除。

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

这个会对我们开发带来极大的不变,你对服务重启,隔了60秒Eureka才反应过来。开发阶段可以适当调整,比如:10秒

自我保护

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

 这是触发了Eureka的自我保护机制。当一个服务未按时进行心跳续约时,Eureka会统计最近15分钟心跳失败的服务实例的比例是否超过了85%。在生产环境下,因为网络延迟等原因,心跳失败实例的比例很有可能超标,但是此时就把服务剔除列表并不妥当,因为服务可能没有宕机。Eureka就会把当前实例的注册信息保护起来,不予剔除。生产环境下这很有效,保证了大多数服务依然可用。

但是这给我们的开发带来了麻烦, 因此开发阶段我们都会(在eureka模块中)关闭自我保护模式:

eureka:
  server:
    enable-self-preservation: false # 关闭自我保护模式(缺省为打开)
    eviction-interval-timer-in-ms: 1000 # 扫描失效服务的间隔时间(缺省为60*1000ms)

7.1.4  Ribbon—负载均衡

7.1.4.1 什么是Ribbon

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

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

一般这种情况下我们就需要编写负载均衡算法,在多个实例列表中进行选择。

不过Eureka中已经帮我们集成了负载均衡组件:Ribbon,简单修改代码即可使用。

什么是Ribbon:

 7.1.4.2 开启负载均衡

仍按照前面的项目进行演示

 7.1.4.2.1 启动两个服务实例

因为Eureka中已经集成了Ribbon,所以我们无需引入新的依赖,直接修改代码。

修改service_consumer的引导类,在RestTemplate的配置方法上添加@LoadBalanced注解:

@SpringBootApplication
@EnableDiscoveryClient //springcloud提供的
public class ConsumerApplication {
        public static void main(String[] args) {
            SpringApplication.run(ConsumerApplication.class,args);
        }
    @Bean
    @LoadBalanced
    RestTemplate restTemplate(){
        return new RestTemplate();
    }

}

修改调用方式,不再手动获取ip和端口,而是直接通过服务名称调用:


@RestController
@RequestMapping("/consumer")
public class UserController {
    @Resource
    RestTemplate restTemplate;
    @Autowired
    DiscoveryClient discoveryClient;

    @RequestMapping("/findbyid/{id}")
    public User findById(@PathVariable Integer id){
   /*
        // 使用RestTemplate远程调用提供方
        // User user = restTemplate.getForObject("http://localhost:8081/provider/findById/" + id, User.class);

        // 根据服务名称获取实例列表
        List<ServiceInstance> instances = discoveryClient.getInstances("service-provider");
        ServiceInstance instance = instances.get(0);
        // 服务地址
        String url = "http://" + instance.getHost() + ":" + instance.getPort() + "/provider/findById/" + id;
        // 调用
        User user = restTemplate.getForObject(url, User.class);
        System.out.println(user);
        return user;

    */
        // 调用
        User user = restTemplate.getForObject("http://service_provider/provider/findbyid/" + id, User.class);
        /*
        或者
         String baseUrl=http://service_provider/provider/findById/" + id;
         User user = this.restTemplate.getForObject(baseUrl, User.class);
         */
        System.out.println(user);
        return user;
    }
}

 启动

如果报以下错误

 原因是:服务端的名称不能包含下划线

原配置信息

spring:
  application:
    name: eureka_server # 应用名称,会在Eureka中显示

修改

spring:
  application:
    name: eureka-server # 应用名称,会在Eureka中显示

7.1.4.3 负载均衡源码分析

首先简单了解Debug调试

在service-consumer中的UserController加入断点

step Into 进入

public <T> T getForObject(String url, Class<T> responseType, Object... uriVariables) throws RestClientException {
        RequestCallback requestCallback = this.acceptHeaderRequestCallback(responseType);
        HttpMessageConverterExtractor<T> responseExtractor = new HttpMessageConverterExtractor(responseType, this.getMessageConverters(), this.logger);
        return this.execute(url, HttpMethod.GET, requestCallback, responseExtractor, (Object[])uriVariables);
    }

但是url还没有解析

主要执行了execute这个方法,进去看看这个方法执行细节

    public <T> T execute(String url, HttpMethod method, @Nullable RequestCallback requestCallback, @Nullable ResponseExtractor<T> responseExtractor, Object... uriVariables) throws RestClientException {
        URI expanded = this.getUriTemplateHandler().expand(url, uriVariables);
        return this.doExecute(expanded, method, requestCallback, responseExtractor);
    }

 url地址仍没有解析

主要执行了doExecute这个方法,进去看看这个方法执行细节

主要是request.execute,得到一个response,进去看看这个方法执行细节

内部执行方法, 传入一个头信息,进入这个方法中

 又是一个内部执行,进去看看

 一个执行器,进去看看

进去

是个与拦截器相关的方法

思考:

根据IP和端口号才能调用某个服务器,因为IP和端口号是这个服务器的明确地址

拦截器作用:过滤/拦截某个请求

传入的是service-provider,应该根据service-provider得到一个具体的IP和端口号

通过拦截器,拦截请求,然后再把service-provider解析成具体的IP和端口号

继续进入

    public <T> T execute(String serviceId, LoadBalancerRequest<T> request) throws IOException {
//取出serviceId,传入getLoadBalancer从而得到一个负载均衡器
        ILoadBalancer loadBalancer = this.getLoadBalancer(serviceId);
//传入负载均衡器的到一个Server 对象
        Server server = this.getServer(loadBalancer);
        if (server == null) {
            throw new IllegalStateException("No instances available for " + serviceId);
        } else {
            RibbonLoadBalancerClient.RibbonServer ribbonServer = new RibbonLoadBalancerClient.RibbonServer(serviceId, server, this.isSecure(server, serviceId), this.serverIntrospector(serviceId).getMetadata(server));
            return this.execute(serviceId, ribbonServer, request);
        }
    }

当得到Server 对象时,就已经解析出IP和端口号

 在这里打个断点,并放过本次断点,继续运行

没有断点了,程序运行完毕

之后我们重新请求服务,看看端口是不是会变换(是否有负载均衡)

遇到第一个断点,放行它 

到达第二个断点

发现端口号改为8082

7.1.4.4 负载均衡策略

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

老李头喽

高级内容,进一步深入JA领域

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值