006.Spring Cloud Netflix - Hoxton.RELEASE

特别说明: 本人平时混迹于 B 站,不咋回复这里的评论,有问题可以到 B 站视频评论区留言找我
视频地址: https://space.bilibili.com/31137138/favlist?fid=326428938
课件说明: 本次提供的课件是 Spring Cloud Netflix 版微服务架构指南,如果有兴趣想要学习 Spring Cloud Alibaba 版,可以前往 http://www.qfdmy.com 查看相关课程资源
案例代码: https://github.com/topsale/hello-spring-cloud-netflix

概述

Spring Cloud 为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智能路由,微代理,控制总线)。分布式系统的协调导致了样板模式, 使用 Spring Cloud 开发人员可以快速地支持实现这些模式的服务和应用程序。他们将在任何分布式环境中运行良好,包括开发人员自己的笔记本电脑,裸机数据中心,以及 Cloud Foundry 等托管平台

项目进入维护期

【官方新闻】Spring Cloud Greenwich.RC1 available now

2018 年 12 月 12 日,Netflix 宣布 Spring Cloud Netflix 系列技术栈进入维护模式(不再添加新特性)

最近,Netflix 宣布 Hystrix 正在进入维护模式。自 2016 年以来,Ribbon 已处于类似状态。虽然 Hystrix 和 Ribbon 现已处于维护模式,但它们仍然在 Netflix 大规模部署。

Hystrix DashboardTurbine 已被 Atlas 取代。这些项目的最后一次提交分别是 2 年前和 4 年前。Zuul1Archaius1 都被后来不兼容的版本所取代。

以下 Spring Cloud Netflix 模块和相应的 Starter 将进入维护模式:

  • spring-cloud-netflix-archaius
  • spring-cloud-netflix-hystrix-contract
  • spring-cloud-netflix-hystrix-dashboard
  • spring-cloud-netflix-hystrix-stream
  • spring-cloud-netflix-hystrix
  • spring-cloud-netflix-ribbon
  • spring-cloud-netflix-turbine-stream
  • spring-cloud-netflix-turbine
  • spring-cloud-netflix-zuul

什么是维护模式

将模块置于维护模式,意味着 Spring Cloud 团队将不会再向模块添加新功能。我们将修复 block 级别的 bug 以及安全问题,我们也会考虑并审查社区的小型 pull request

替代品

我们建议对这些模块提供的功能进行以下替换

CURRENT REPLACEMENT
Hystrix Resilience4j
Hystrix Dashboard / Turbine Micrometer + Monitoring System
Ribbon Spring Cloud Loadbalancer
Zuul 1 Spring Cloud Gateway
Archaius 1 Spring Boot external config + Spring Cloud Config

创建项目工程

Spring Cloud 项目都是基于 Spring Boot 进行开发,并且都是使用 Maven 做项目管理工具。在实际开发中,我们一般都会创建一个依赖管理项目作为 Maven 的 Parent 项目使用,这样做可以极大的方便我们对 Jar 包版本的统一管理

POM

<?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 https://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.2.1.RELEASE</version>
        <relativePath/>
    </parent>

    <groupId>com.funtl</groupId>
    <artifactId>hello-spring-cloud-netflix</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>pom</packaging>

    <modules>
        <module>dependencies</module>
    </modules>

    <properties>
        <java.version>1.8</java.version>
        <maven.compiler.source>${java.version}</maven.compiler.source>
        <maven.compiler.target>${java.version}</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
    </properties>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>com.funtl</groupId>
                <artifactId>dependencies</artifactId>
                <version>${project.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <profiles>
        <profile>
            <id>default</id>
            <activation>
                <activeByDefault>true</activeByDefault>
            </activation>
            <properties>
                <spring-javaformat.version>0.0.15</spring-javaformat.version>
            </properties>
            <build>
                <plugins>
                    <plugin>
                        <groupId>io.spring.javaformat</groupId>
                        <artifactId>spring-javaformat-maven-plugin</artifactId>
                        <version>${spring-javaformat.version}</version>
                    </plugin>
                    <plugin>
                        <groupId>org.apache.maven.plugins</groupId>
                        <artifactId>maven-surefire-plugin</artifactId>
                        <configuration>
                            <includes>
                                <include>**/*Tests.java</include>
                            </includes>
                            <excludes>
                                <exclude>**/Abstract*.java</exclude>
                            </excludes>
                            <systemPropertyVariables>
                                <java.security.egd>file:/dev/./urandom</java.security.egd>
                                <java.awt.headless>true</java.awt.headless>
                            </systemPropertyVariables>
                        </configuration>
                    </plugin>
                    <plugin>
                        <groupId>org.apache.maven.plugins</groupId>
                        <artifactId>maven-enforcer-plugin</artifactId>
                        <executions>
                            <execution>
                                <id>enforce-rules</id>
                                <goals>
                                    <goal>enforce</goal>
                                </goals>
                                <configuration>
                                    <rules>
                                        <bannedDependencies>
                                            <excludes>
                                                <exclude>commons-logging:*:*</exclude>
                                            </excludes>
                                            <searchTransitive>true</searchTransitive>
                                        </bannedDependencies>
                                    </rules>
                                    <fail>true</fail>
                                </configuration>
                            </execution>
                        </executions>
                    </plugin>
                    <plugin>
                        <groupId>org.apache.maven.plugins</groupId>
                        <artifactId>maven-install-plugin</artifactId>
                        <configuration>
                            <skip>true</skip>
                        </configuration>
                    </plugin>
                    <plugin>
                        <groupId>org.apache.maven.plugins</groupId>
                        <artifactId>maven-javadoc-plugin</artifactId>
                        <configuration>
                            <skip>true</skip>
                        </configuration>
                        <inherited>true</inherited>
                    </plugin>
                </plugins>
            </build>
        </profile>
    </profiles>

    <repositories>
        <repository>
            <id>spring-milestone</id>
            <name>Spring Milestone</name>
            <url>https://repo.spring.io/milestone</url>
            <snapshots>
                <enabled>false</enabled>
            </snapshots>
        </repository>
        <repository>
            <id>spring-snapshot</id>
            <name>Spring Snapshot</name>
            <url>https://repo.spring.io/snapshot</url>
            <snapshots>
                <enabled>true</enabled>
            </snapshots>
        </repository>
    </repositories>

    <pluginRepositories>
        <pluginRepository>
            <id>spring-milestone</id>
            <name>Spring Milestone</name>
            <url>https://repo.spring.io/milestone</url>
            <snapshots>
                <enabled>false</enabled>
            </snapshots>
        </pluginRepository>
        <pluginRepository>
            <id>spring-snapshot</id>
            <name>Spring Snapshot</name>
            <url>https://repo.spring.io/snapshot</url>
            <snapshots>
                <enabled>true</enabled>
            </snapshots>
        </pluginRepository>
    </pluginRepositories>

</project>

创建统一的依赖管理

POM

<?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>

    <groupId>com.funtl</groupId>
    <artifactId>dependencies</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>pom</packaging>

    <properties>
        <spring-cloud.version>Hoxton.RELEASE</spring-cloud.version>
    </properties>

    <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>

    <repositories>
        <repository>
            <id>spring-milestone</id>
            <name>Spring Milestone</name>
            <url>https://repo.spring.io/milestone</url>
            <snapshots>
                <enabled>false</enabled>
            </snapshots>
        </repository>
        <repository>
            <id>spring-snapshot</id>
            <name>Spring Snapshot</name>
            <url>https://repo.spring.io/snapshot</url>
            <snapshots>
                <enabled>true</enabled>
            </snapshots>
        </repository>
    </repositories>

    <pluginRepositories>
        <pluginRepository>
            <id>spring-milestone</id>
            <name>Spring Milestone</name>
            <url>https://repo.spring.io/milestone</url>
            <snapshots>
                <enabled>false</enabled>
            </snapshots>
        </pluginRepository>
        <pluginRepository>
            <id>spring-snapshot</id>
            <name>Spring Snapshot</name>
            <url>https://repo.spring.io/snapshot</url>
            <snapshots>
                <enabled>true</enabled>
            </snapshots>
        </pluginRepository>
    </pluginRepositories>

</project>

服务注册与发现(Eureka)

POM

  • 创建一个名为 discovery 的项目
<?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>com.funtl</groupId>
        <artifactId>hello-spring-cloud-netflix</artifactId>
        <version>0.0.1-SNAPSHOT</version>
    </parent>

    <artifactId>discovery</artifactId>
    <packaging>pom</packaging>

    <modules>
        <module>discovery-eureka</module>
    </modules>
</project>
  • 创建一个名为 discovery-eureka 的项目,它是 discovery 的子项目
<?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>com.funtl</groupId>
        <artifactId>discovery</artifactId>
        <version>0.0.1-SNAPSHOT</version>
    </parent>

    <artifactId>discovery-eureka</artifactId>
    <packaging>jar</packaging>

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

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <mainClass>com.funtl.hello.spring.cloud.eureka.DiscoveryEurekaApplication</mainClass>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

Application

启动一个服务注册中心,只需要一个注解 @EnableEurekaServer

package com.funtl.hello.spring.cloud.eureka;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;

@SpringBootApplication
@EnableEurekaServer
public class DiscoveryEurekaApplication {

    public static void main(String[] args) {
        SpringApplication.run(DiscoveryEurekaApplication.class, args);
    }

}

application.yml

Eureka 是一个高可用的组件,它没有后端缓存,每一个实例注册之后需要向注册中心发送心跳(因此可以在内存中完成),在默认情况下 Erureka Server 也是一个 Eureka Client , 必须要指定一个 Server

spring:
  application:
    name: discovery-eureka

server:
  port: 8761

eureka:
  instance:
    hostname: localhost
  client:
    registerWithEureka: false
    fetchRegistry: false
    serviceUrl:
      defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/

通过 eureka.client.registerWithEureka:falsefetchRegistry:false 来表明自己是一个 Eureka Server

通过浏览器访问

Eureka Server 是有界面的,启动工程,打开浏览器访问:http://localhost:8761

Lusifer_20191203023353.png

创建服务提供者

当 Client 向 Server 注册时,它会提供一些元数据,例如主机和端口,URL,主页等。Eureka Server 从每个 Client 实例接收心跳消息。 如果心跳超时,则通常将该实例从注册 Server 中删除

POM

  • 创建一个名为 provider 的项目
<?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>com.funtl</groupId>
        <artifactId>hello-spring-cloud-netflix</artifactId>
        <version>0.0.1-SNAPSHOT</version>
    </parent>

    <artifactId>provider</artifactId>
    <packaging>pom</packaging>

    <modules>
        <module>provider-admin-service</module>
    </modules>
</project>
  • 创建一个名为 provider-admin-service 的项目,它是 provider 的子项目
<?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>com.funtl</groupId>
        <artifactId>provider</artifactId>
        <version>0.0.1-SNAPSHOT</version>
    </parent>

    <artifactId>provider-admin-service</artifactId>
    <packaging>jar</packaging>

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

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <mainClass>com.funtl.hello.spring.cloud.provider.ProviderAdminApplication</mainClass>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

Application

通过注解 @EnableEurekaClient 表明自己是一个 Eureka Client

package com.funtl.hello.spring.cloud.provider;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;

@SpringBootApplication
@EnableEurekaClient
public class ProviderAdminApplication {

    public static void main(String[] args) {
        SpringApplication.run(ProviderAdminApplication.class, args);
    }

}

application.yml

spring:
  main:
    allow-bean-definition-overriding: true
  application:
    name: provider-admin

server:
  port: 11000

eureka:
  instance:
    hostname: localhost
  client:
    serviceUrl:
      defaultZone: http://${eureka.instance.hostname}:8761/eureka/

注意: 需要指明 spring.application.name,这个很重要,这在以后的服务与服务之间相互调用一般都是根据这个 name

Controller

package com.funtl.hello.spring.cloud.provider.controller;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class ProviderAdminController {

    @Value("${server.port}")
    private String port;

    @GetMapping(value = "hi")
    public String sayHi() {
        return "Hello Eureka, i am from port: " + port;
    }

}

通过浏览器访问

启动工程,打开 http://localhost:8761 ,即 Eureka Server 的网址:

Lusifer_20191203051305.png

你会发现一个服务已经注册在服务中了,服务名为 PROVIDER-ADMIN , 端口为 11000,这时打开 http://localhost:11000/hi ,你会在浏览器上看到 :

Hello Eureka, i am from port: 11000

创建服务消费者(Feign)

关于 Ribbon

Ribbon 是一个负载均衡客户端,可以很好的控制 HTTP 和 TCP 的一些行为。在微服务架构中,业务都会被拆分成一个独立的服务,服务与服务的通讯是基于 HTTP RESTful 的。Spring Cloud 有两种服务调用方式,一种是 Ribbon + RestTemplate,另一种是 Feign

什么是 Feign

Feign 是一个声明式的伪 HTTP 客户端,它使得写 HTTP 客户端变得更简单。使用 Feign,只需要创建一个接口并注解。它具有可插拔的注解特性,可使用 Feign 注解和 JAX-RS 注解。Feign 支持可插拔的编码器和解码器。Feign 默认集成了 Ribbon,并和 Eureka 结合,默认实现了负载均衡的效果

  • Feign 采用的是基于接口的注解
  • Feign 整合了 Ribbon

POM

  • 创建一个名为 business 的项目
<?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>com.funtl</groupId>
        <artifactId>hello-spring-cloud-netflix</artifactId>
        <version>0.0.1-SNAPSHOT</version>
    </parent>

    <artifactId>business</artifactId>
    <packaging>pom</packaging>

    <modules>
        <module>business-admin-feign</module>
        <module>business-admin-service</module>
    </modules>
</project>
  • 创建一个名为 business-admin-feign 的项目,它是 business 的子项目
<?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>com.funtl</groupId>
        <artifactId>business</artifactId>
        <version>0.0.1-SNAPSHOT</version>
    </parent>

    <artifactId>business-admin-feign</artifactId>
    <packaging>jar</packaging>

    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>
    </dependencies>
</project>
  • 创建一个名为 business-admin-service 的项目,它是 business 的子项目
<?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>com.funtl</groupId>
        <artifactId>business</artifactId>
        <version>0.0.1-SNAPSHOT</version>
    </parent>

    <artifactId>business-admin-service</artifactId>
    <packaging>jar</packaging>

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

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>

        <!-- Projects -->
        <dependency>
            <groupId>com.funtl</groupId>
            <artifactId>business-admin-feign</artifactId>
            <version>${project.parent.version}</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <mainClass>com.funtl.hello.spring.cloud.business.BusinessAdminApplication</mainClass>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

注意: 以上方式是为了利用 Maven 模块化功能提高 Feign 的复用性

Feign

通过 @FeignClient("服务名") 注解来指定调用哪个服务

package com.funtl.hello.spring.cloud.business.feign;

import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;

@FeignClient(value = "provider-admin")
public interface BusinessAdminFeign {

    @GetMapping(value = "hi")
    String sayHi();

}

Application

通过 @EnableFeignClients 注解开启 Feign 功能

package com.funtl.hello.spring.cloud.business;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;

@EnableFeignClients
@EnableDiscoveryClient
@SpringBootApplication
public class BusinessAdminApplication {

    public static void main(String[] args) {
        SpringApplication.run(BusinessAdminApplication.class, args);
    }

}

application.yml

spring:
  main:
    allow-bean-definition-overriding: true
  application:
    name: business-admin

server:
  port: 12000

eureka:
  instance:
    hostname: localhost
  client:
    serviceUrl:
      defaultZone: http://${eureka.instance.hostname}:8761/eureka/

Controller

package com.funtl.hello.spring.cloud.business.controller;

import com.funtl.hello.spring.cloud.business.feign.BusinessAdminFeign;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class BusinessAdminController {

    @Autowired
    private BusinessAdminFeign adminFeign;

    @GetMapping(value = "hi")
    public String sayHi() {
        return adminFeign.sayHi();
    }
}

测试负载均衡

修改 provider-admin-service 项目的 application.yml 中的 server.port,以启动多个实例,通过浏览器请求:http://localhost:12000/hi ,此时浏览器交替显示

Hello Eureka, i am from port: 11000
Hello Eureka, i am from port: 11001

增加服务熔断功能(Hystrix)

在微服务架构中,根据业务来拆分成一个个的服务,服务与服务之间可以通过 RPC 相互调用,在 Spring Cloud 中可以用 RestTemplate + RibbonFeign 来调用。为了保证其高可用,单个服务通常会集群部署。由于网络原因或者自身的原因,服务并不能保证 100% 可用,如果单个服务出现问题,调用这个服务就会出现线程阻塞,此时若有大量的请求涌入,Servlet 容器的线程资源会被消耗完毕,导致服务瘫痪。服务与服务之间的依赖性,故障会传播,会对整个微服务系统造成灾难性的严重后果,这就是服务故障的 “雪崩” 效应

为了解决这个问题,业界提出了熔断器模型。Netflix 开源了 Hystrix 组件,实现了熔断器模式,Spring Cloud 对这一组件进行了整合。在微服务架构中,一个请求需要调用多个服务是非常常见的

Lusifer201805292246007.png

较底层的服务如果出现故障,会导致连锁故障。当对特定的服务的调用的不可用达到一个阀值(Hystrix 是 5 秒 20 次) 熔断器将会被打开

Lusifer201805292246008.png

熔断器打开后,为了避免连锁故障,通过 fallback 方法可以直接返回一个固定值

开启熔断器

Feign 是自带熔断器的,但默认是关闭的。需要在配置文件中配置打开它,在配置文件增加以下代码

feign:
  hystrix:
    enabled: true

创建熔断类

business-admin-feign 项目中增加熔断类,如果请求失败或超时或异常则会触发熔断并返回一个固定结果

package com.funtl.hello.spring.cloud.business.feign.fallback;

import com.funtl.hello.spring.cloud.business.feign.BusinessAdminFeign;
import org.springframework.stereotype.Component;

@Component
public class BusinessAdminFallback implements BusinessAdminFeign {
    @Override
    public String sayHi() {
        return "请求失败了,请重试...";
    }
}

声明熔断类

@FeignClient 注解中增加 fallback 属性声明熔断类

package com.funtl.hello.spring.cloud.business.feign;

import com.funtl.hello.spring.cloud.business.feign.fallback.BusinessAdminFallback;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;

@FeignClient(value = "provider-admin", fallback = BusinessAdminFallback.class)
public interface BusinessAdminFeign {

    @GetMapping(value = "hi")
    String sayHi();

}

测试熔断器

此时我们关闭服务提供者,再次请求 http://localhost:12000/hi 浏览器会显示

请求失败了,请重试...

增加集群熔断监控(Turbine)

使用场景

在复杂的分布式系统中,相同服务的结点经常需要部署上百甚至上千个,很多时候,运维人员希望能够把相同服务的节点状态以一个整体集群的形式展现出来,这样可以更好的把握整个系统的状态。 为此,Netflix 又提供了一个开源项目 Turbine 来提供把多个 hystrix.stream 的内容聚合为一个数据源供 Dashboard 展示

什么是 Turbine

Turbine 是聚合服务器发送事件流数据的一个工具,Hystrix 的监控中,只能监控单个节点,实际生产中都为集群,因此可以通过 Turbine 来监控集群下 Hystrix 的 Metrics 情况,Turbine 的 GitHub 地址:https://github.com/Netflix/Turbine

创建熔断监控中心

  • 创建一个名为 addons 的项目,该项目用于统一管理附加组件,我们将熔断器作为附加组件集成进项目中,增加可复用性
<?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>com.funtl</groupId>
        <artifactId>hello-spring-cloud-netflix</artifactId>
        <version>0.0.1-SNAPSHOT</version>
    </parent>

    <artifactId>addons</artifactId>
    <packaging>pom</packaging>

    <modules>
        <module>addons-hystrix-dashboard</module>
        <module>addons-hystrix-turbine</module>
    </modules>
</project>
  • 创建一个名为 addons-hystrix-dashboard 的项目,该项目的主要作用是配置 HystrixMetricsStreamServlet 用以收集熔断信息
<?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>com.funtl</groupId>
        <artifactId>addons</artifactId>
        <version>0.0.1-SNAPSHOT</version>
    </parent>

    <artifactId>addons-hystrix-dashboard</artifactId>
    <packaging>jar</packaging>

    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-hystrix-dashboard</artifactId>
        </dependency>
    </dependencies>
</project>
  • addons-hystrix-dashboard 项目中创建熔断器配置类
package com.funtl.hello.spring.cloud.addons.configuration;

import com.netflix.hystrix.contrib.metrics.eventstream.HystrixMetricsStreamServlet;
import org.springframework.boot.web.servlet.ServletRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class HystrixDashboardConfiguration {

    @Bean
    public ServletRegistrationBean getServlet() {
        HystrixMetricsStreamServlet streamServlet = new HystrixMetricsStreamServlet();
        ServletRegistrationBean registrationBean = new ServletRegistrationBean(streamServlet);
        registrationBean.setLoadOnStartup(1);
        // 配置收集端点路径
        registrationBean.addUrlMappings("/hystrix.stream");
        registrationBean.setName("HystrixMetricsStreamServlet");
        return registrationBean;
    }

}
  • 创建一个名为 addons-hystrix-turbine 的项目,该项目用于统一收集集群中的熔断信息
<?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>com.funtl</groupId>
        <artifactId>addons</artifactId>
        <version>0.0.1-SNAPSHOT</version>
    </parent>

    <artifactId>addons-hystrix-turbine</artifactId>
    <packaging>jar</packaging>

    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-turbine</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-hystrix-dashboard</artifactId>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <mainClass>com.funtl.hello.spring.cloud.addons.HystrixTurbineApplication</mainClass>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>
  • addons-hystrix-turbine 项目的 Application,通过注解 @EnableTurbine@EnableHystrixDashboard 开启 Hystrix Dashboard 和 Turbine 收集功能
package com.funtl.hello.spring.cloud.addons;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.cloud.netflix.hystrix.dashboard.EnableHystrixDashboard;
import org.springframework.cloud.netflix.turbine.EnableTurbine;

@EnableTurbine
@EnableHystrixDashboard
@EnableEurekaClient
@SpringBootApplication
public class HystrixTurbineApplication {

    public static void main(String[] args) {
        SpringApplication.run(HystrixTurbineApplication.class, args);
    }

}
  • addons-hystrix-turbine 项目的 application.yml
spring:
  main:
    allow-bean-definition-overriding: true
  application:
    name: addons-hystrix-turbine

server:
  port: 8847

eureka:
  instance:
    hostname: localhost
  client:
    serviceUrl:
      defaultZone: http://${eureka.instance.hostname}:8761/eureka/

turbine:
  # 可以让同一主机上的服务通过主机名与端口号的组合来进行区分
  # 默认情况下会以 HOST 来区分不同的服务,这会使得在本机调试的时候,本机上的不同服务聚合成一个服务来统计
  combine-host-port: true
  # 配置监控服务的列表,表明监控哪些服务多个使用 "," 分割
  app-config: business-admin
  # 用于指定集群名称,当服务数量非常多的时候,可以启动多个
  cluster-name-expression: metadata['cluster']
  aggregator:
    # 指定聚合哪些集群,多个使用 "," 分割,默认为 default
    cluster-config: business
  # 用于替换源码 org.springframework.cloud.netflix.turbine.SpringClusterMonitor 中的收集端点
  # 我们配置的 Servlet 指向了 /hystrix.stream,Turbine 默认收集端点为 /actuator/hystrix.stream
  instanceUrlSuffix: /hystrix.stream

消费者开启熔断监控

  • 修改 POM,增加刚才创建的 com.funtl:addons-hystrix-dashboard 项目依赖
<dependency>
    <groupId>com.funtl</groupId>
    <artifactId>addons-hystrix-dashboard</artifactId>
    <version>${project.parent.version}</version>
</dependency>
  • 修改 Application 中的 @SpringBootApplication 注解里包扫描路径,让 Spring 可以扫描到 addons-hystrix-dashboard 项目中的 Java 配置
package com.funtl.hello.spring.cloud.business;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.netflix.hystrix.EnableHystrix;
import org.springframework.cloud.openfeign.EnableFeignClients;

@EnableHystrix
@EnableFeignClients
@EnableDiscoveryClient
@SpringBootApplication(scanBasePackages = "com.funtl.hello.spring.cloud")
public class BusinessAdminApplication {

    public static void main(String[] args) {
        SpringApplication.run(BusinessAdminApplication.class, args);
    }

}
  • 修改 application.yml 配置
eureka:
  instance:
    hostname: localhost
    # 增加用于集群的配置,集群名为 business,与 Turbine 的配置匹配
    metadata-map:
      cluster: business
  client:
    serviceUrl:
      defaultZone: http://${eureka.instance.hostname}:8761/eureka/

测试熔断收集功能

: ping
data: {"reportingHostsLast10Seconds":1,"name":"meta","type":"meta","timestamp":1575400020449}

: ping
data: {"reportingHostsLast10Seconds":1,"name":"meta","type":"meta","timestamp":1575400023450}

: ping
data: {"reportingHostsLast10Seconds":1,"name":"meta","type":"meta","timestamp":1575400026452}

: ping
data: {"reportingHostsLast10Seconds":1,"name":"meta","type":"meta","timestamp":1575400029453}

注意: 在没有访问消费者接口的情况下只会打印 PING 信息

  • 调用消费者接口(多调用几次,还需要等待一小会),观察监控流信息变化,可以发现已经收集到了集群信息
: ping
data: {"rollingCountFallbackSuccess":0,"rollingCountFallbackFailure":0,"propertyValue_circuitBreakerRequestVolumeThreshold":20,"propertyValue_circuitBreakerForceOpen":false,"propertyValue_metricsRollingStatisticalWindowInMilliseconds":10000,"latencyTotal_mean":0,"rollingMaxConcurrentExecutionCount":0,"type":"HystrixCommand","rollingCountResponsesFromCache":0,"rollingCountBadRequests":0,"rollingCountTimeout":0,"propertyValue_executionIsolationStrategy":"THREAD","rollingCountFailure":0,"rollingCountExceptionsThrown":0,"rollingCountFallbackMissing":0,"threadPool":"provider-admin","latencyExecute_mean":0,"isCircuitBreakerOpen":false,"errorCount":0,"rollingCountSemaphoreRejected":0,"group":"provider-admin","latencyTotal":{"0":0,"99":0,"100":0,"25":0,"90":0,"50":0,"95":0,"99.5":0,"75":0},"requestCount":0,"rollingCountCollapsedRequests":0,"rollingCountShortCircuited":0,"propertyValue_circuitBreakerSleepWindowInMilliseconds":5000,"latencyExecute":{"0":0,"99":0,"100":0,"25":0,"90":0,"50":0,"95":0,"99.5":0,"75":0},"rollingCountEmit":0,"currentConcurrentExecutionCount":0,"propertyValue_executionIsolationSemaphoreMaxConcurrentRequests":10,"errorPercentage":0,"rollingCountThreadPoolRejected":0,"propertyValue_circuitBreakerEnabled":true,"propertyValue_executionIsolationThreadInterruptOnTimeout":true,"propertyValue_requestCacheEnabled":true,"rollingCountFallbackRejection":0,"propertyValue_requestLogEnabled":true,"rollingCountFallbackEmit":0,"rollingCountSuccess":0,"propertyValue_fallbackIsolationSemaphoreMaxConcurrentRequests":10,"propertyValue_circuitBreakerErrorThresholdPercentage":50,"propertyValue_circuitBreakerForceClosed":false,"name":"BusinessAdminFeign#sayHi()","reportingHosts":1,"propertyValue_executionIsolationThreadPoolKeyOverride":"null","propertyValue_executionIsolationThreadTimeoutInMilliseconds":1000,"propertyValue_executionTimeoutInMilliseconds":1000}
Lusifer_20191204031220.png
  • 停止服务提供者,触发消费者熔断,观察控制台,此时可以看到控制台监控到了集群状态
Lusifer_20191204031437.png

Hystrix 相关说明

什么情况下会触发熔断

类型 描述 触发 fallback
EMIT 值传递 NO
SUCCESS 执行完成,没有错误 NO
FAILURE 执行抛出异常 YES
TIMEOUT 执行开始,但没有在允许的时间内完成 YES
BAD_REQUEST 执行抛出 HystrixBadRequestException NO
SHORT_CIRCUITED 断路器打开,不尝试执行 YES
THREAD_POOL_REJECTED 线程池拒绝,不尝试执行 YES
SEMAPHORE_REJECTED 信号量拒绝,不尝试执行 YES

什么情况下会抛出异常

类型 描述 抛异常
FALLBACK_EMIT Fallback 值传递 NO
FALLBACK_SUCCESS Fallback 执行完成,没有错误 NO
FALLBACK_FAILURE Fallback 执行抛出出错 YES
FALLBACK_REJECTED Fallback 信号量拒绝,不尝试执行 YES
FALLBACK_MISSING 没有 Fallback 实例 YES

监控界面指标说明

Lusifer_20191205011456.png
20171123110838020.png

常用配置说明

使用方法为在 Feign 接口的调用方法上增加注解,案例代码如下

@HystrixCommand(commandProperties = {
     @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds",value = "3000")
})
  • 超时时间(默认 1000ms,单位:ms)

    • execution.isolation.thread.timeoutInMilliseconds:在调用方配置,被该调用方的所有方法的超时时间都是该值,优先级低于下边的指定配置
    • execution.isolation.thread.timeoutInMilliseconds:在调用方配置,被该调用方的指定方法(HystrixCommandKey 方法名)的超时时间是该值
  • 线程池核心线程数

    • coreSize:默认为 10
  • Queue

    • maxQueueSize:最大排队长度。默认 -1,使用 SynchronousQueue。其他值则使用 LinkedBlockingQueue。如果要从 -1 换成其他值则需重启,即该值不能动态调整,若要动态调整,需要使用到下边这个配置
    • queueSizeRejectionThreshold:排队线程数量阈值,默认为 5,达到时拒绝,如果配置了该选项,队列的大小是该队列(注意:如果 maxQueueSize=-1 的话,则该选项不起作用
  • 断路器

    • circuitBreaker.requestVolumeThreshold:当在配置时间窗口内达到此数量的失败后,进行短路。默认 20 个(10s 内请求失败数量达到 20 个,断路器开)
    • circuitBreaker.sleepWindowInMilliseconds:短路多久以后开始尝试是否恢复,默认 5s
    • circuitBreaker.errorThresholdPercentage:出错百分比阈值,当达到此阈值后,开始短路。默认 50%
  • fallback

    • fallback.isolation.semaphore.maxConcurrentRequests:调用线程允许请求 HystrixCommand.GetFallback() 的最大数量,默认 10。超出时将会有异常抛出,注意:该项配置对于 THREAD 隔离模式也起作用
  • 详细参数说明: https://github.com/Netflix/Hystrix/wiki/Configuration

创建路由网关(Zuul)

在微服务架构中,需要几个基础的服务治理组件,包括服务注册与发现、服务消费、负载均衡、熔断器、智能路由、配置管理等,由这几个基础组件相互协作,共同组建了一个简单的微服务系统。一个简单的微服务系统如下图:

Lusifer201805292246011.png

在 Spring Cloud 微服务系统中,一种常见的负载均衡方式是,客户端的请求首先经过负载均衡(Zuul、Ngnix),再到达服务网关(Zuul 集群),然后再到具体的服。服务统一注册到高可用的服务注册中心集群,服务的所有的配置文件由配置服务管理,配置服务的配置文件放在 GIT 仓库,方便开发人员随时改配置

Zuul 的主要功能是路由转发和过滤器。路由功能是微服务的一部分,比如 /api/user 转发到到 User 服务,/api/shop 转发到到 Shop 服务。Zuul 默认和 Ribbon 结合实现了负载均衡的功能

POM

  • 创建一个名为 gateway 的项目,网关可以有多种选型(zuulspring cloud gateway)也可以针对不同的场景(对内的网关,对外的网关,针对某种业务类型的网关,开发平台的网关等)
<?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>com.funtl</groupId>
        <artifactId>hello-spring-cloud-netflix</artifactId>
        <version>0.0.1-SNAPSHOT</version>
    </parent>

    <artifactId>gateway</artifactId>
    <packaging>pom</packaging>

    <modules>
        <module>gateway-zuul-business</module>
    </modules>
</project>
  • 创建一个名为 gateway-zuul-business 的项目,这是使用 zuul 实现的针对业务的网关
<?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>com.funtl</groupId>
        <artifactId>gateway</artifactId>
        <version>0.0.1-SNAPSHOT</version>
    </parent>

    <artifactId>gateway-zuul-business</artifactId>
    <packaging>jar</packaging>

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

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <mainClass>com.funtl.hello.spring.cloud.gateway.ZuulBusinessApplication</mainClass>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

Application

增加 @EnableZuulProxy 注解开启 Zuul 功能

package com.funtl.hello.spring.cloud.gateway;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.netflix.zuul.EnableZuulProxy;

@EnableZuulProxy
@EnableDiscoveryClient
@SpringBootApplication
public class ZuulBusinessApplication {

    public static void main(String[] args) {
        SpringApplication.run(ZuulBusinessApplication.class, args);
    }

}

application.yml

spring:
  main:
    allow-bean-definition-overriding: true
  application:
    name: gateway-zuul-business

server:
  port: 8080

eureka:
  instance:
    hostname: localhost
  client:
    serviceUrl:
      defaultZone: http://${eureka.instance.hostname}:8761/eureka/

zuul:
  routes:
    api-business-admin:
      # 以 /api/business/admin 开头的请求都转发给 business-admin 服务
      path: /api/business/admin/**
      serviceId: business-admin

路由失败时回调

路由的服务无法请求时也需要触发熔断功能,通过实现 FallbackProvider 接口可以直接返回固定结果给客户端

package com.funtl.hello.spring.cloud.gateway.fallback;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.cloud.netflix.zuul.filters.route.FallbackProvider;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.stereotype.Component;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.Map;

/**
 * 路由 business-admin 失败时的回调
 * <p>
 * Description:
 * </p>
 *
 * @author Lusifer
 * @version v1.0.0
 * @date 2019-12-04 16:09:44
 * @see com.funtl.hello.spring.cloud.gateway.fallback
 *
 */
@Component
public class BusinessAdminFallbackProvider implements FallbackProvider {
    @Override
    public String getRoute() {
        // ServiceId,如果需要所有调用都支持回退,则 return "*" 或 return null
        return "business-admin";
    }

    /**
     * 如果请求服务失败,则返回指定的信息给调用者
     *
     * @param route
     * @param cause
     * @return
     */
    @Override
    public ClientHttpResponse fallbackResponse(String route, Throwable cause) {
        return new ClientHttpResponse() {
            /**
             * 网关向 api 服务请求失败了,但是消费者客户端向网关发起的请求是成功的,
             * 不应该把 api 的 404,500 等问题抛给客户端
             * 网关和 api 服务集群对于客户端来说是黑盒
             * @return
             * @throws IOException
             */
            @Override
            public HttpStatus getStatusCode() throws IOException {
                return HttpStatus.OK;
            }

            @Override
            public int getRawStatusCode() throws IOException {
                return HttpStatus.OK.value();
            }

            @Override
            public String getStatusText() throws IOException {
                return HttpStatus.OK.getReasonPhrase();
            }

            @Override
            public void close() {

            }

            @Override
            public InputStream getBody() throws IOException {
                ObjectMapper objectMapper = new ObjectMapper();
                Map<String, Object> map = new HashMap<>();
                map.put("status", 200);
                map.put("message", "无法连接,请检查您的网络");
                return new ByteArrayInputStream(objectMapper.writeValueAsString(map).getBytes("UTF-8"));
            }

            @Override
            public HttpHeaders getHeaders() {
                HttpHeaders headers = new HttpHeaders();
                // 和 getBody 中的内容编码一致
                headers.setContentType(MediaType.APPLICATION_JSON);
                return headers;
            }
        };
    }
}

测试路由网关

通过浏览器访问:http://localhost:8080/api/business/admin/hi ,浏览器输出如下

Hello Eureka, i am from port: 11000

服务链路追踪(ZipKin)

ZipKin 是一个开放源代码的分布式跟踪系统,由 Twitter 公司开源,它致力于收集服务的定时数据,以解决微服务架构中的延迟问题,包括数据的收集、存储、查找和展现。它的理论模型来自于 Google Dapper 论文

每个服务向 ZipKin 报告计时数据,ZipKin 会根据调用关系通过 ZipKin UI 生成依赖关系图,显示了多少跟踪请求通过每个服务,该系统让开发者可通过一个 Web 前端轻松的收集和分析数据,例如用户每次请求服务的处理时间等,可方便的监测系统中存在的瓶颈

为什么需要链路追踪

微服务架构是通过业务来划分服务的,使用 REST 调用。对外暴露的一个接口,可能需要很多个服务协同才能完成这个接口功能,如果链路上任何一个服务出现问题或者网络超时,都会形成导致接口调用失败。随着业务的不断扩张,服务之间互相调用会越来越复杂

2279594-dd72907e82f89fd6.png

随着服务的越来越多,对调用链的分析会越来越复杂。它们之间的调用关系也许如下

2279594-4b7d1b6abe595390.png

术语解释

  • Span:基本工作单元,例如,在一个新建的 Span 中发送一个 RPC 等同于发送一个回应请求给 RPC,Span 通过一个 64 位 ID 唯一标识,Trace 以另一个 64 位 ID 表示。
  • Trace:一系列 Spans 组成的一个树状结构,例如,如果你正在运行一个分布式大数据工程,你可能需要创建一个 Trace。
  • Annotation:用来即使记录一个事件的存在,一些核心 Annotations 用来定义一个请求的开始和结束
    • cs:Client Sent,客户端发起一个请求,这个 Annotation 描述了这个 Span 的开始
    • sr:Server Received,服务端获得请求并准备开始处理它,如果将其 sr 减去 cs 时间戳便可得到网络延迟
    • ss:Server Sent 表明请求处理的完成(当请求返回客户端),如果 ss 减去 sr 时间戳便可得到服务端需要的处理请求时间
    • cr:Client Received 表明 Span 的结束,客户端成功接收到服务端的回复,如果 cr 减去 cs 时间戳便可得到客户端从服务端获取回复的所有所需时间

将 Span 和 Trace 在一个系统中使用 Zipkin 注解的过程图形化:

2279594-4b865f2a2c271def.png

部署链路追踪

我们可以使用 Docker 快速部署 ZipKin 服务器,docker-compose.yml 配置文件如下

version: '3.1'
services:
  zipkin:
    image: openzipkin/zipkin
    container_name: zipkin
    ports:
      - 9411:9411

通过浏览器访问:http://your_host:9411/zipkin/

Lusifer_20191204180309.png

配置链路追踪

  • 在所有需要被追踪的项目(包括 Eureka Server)中增加 spring-cloud-starter-zipkin 依赖
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-zipkin</artifactId>
</dependency>
  • 在这些项目的 application.yml 配置文件中增加 Zipkin Server 的地址即可
spring:
  zipkin:
    base-url: http://localhost:9411
Lusifer_20191204180348.png

外部化配置中心(Config)

在分布式系统中,由于服务数量巨多,为了方便服务配置文件统一管理,实时更新,所以需要分布式配置中心组件。在 Spring Cloud 中,有分布式配置中心组件 Spring Cloud Config ,它支持配置服务放在配置服务的内存中(即本地),也支持放在远程 Git 仓库中。在 Spring Cloud Config 组件中,分两个角色,一是 Config Server,二是 Config Client

配置中心服务端

  • addons 项目中增加子项目 addons-cloud-config,POM 配置文件如下
<?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>com.funtl</groupId>
        <artifactId>addons</artifactId>
        <version>0.0.1-SNAPSHOT</version>
    </parent>

    <artifactId>addons-cloud-config</artifactId>
    <packaging>jar</packaging>

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

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <mainClass>com.funtl.hello.spring.cloud.addons.CloudConfigApplication</mainClass>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>
  • 创建 Application,通过 @EnableConfigServer 注解,开启配置服务器功能
package com.funtl.hello.spring.cloud.addons;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.config.server.EnableConfigServer;

@EnableConfigServer
@EnableDiscoveryClient
@SpringBootApplication
public class CloudConfigApplication {

    public static void main(String[] args) {
        SpringApplication.run(CloudConfigApplication.class, args);
    }

}
  • application.yml 配置文件中增加相关配置,此处我们使用 GitLab 仓库存放我们的配置文件,其中 Spring Cloud Config 默认端口为 8888
spring:
  main:
    allow-bean-definition-overriding: true
  application:
    name: addons-cloud-config
  cloud:
    config:
      label: master
      server:
        git:
          uri: http://10.3.50.121:8080/root/hello-spring-cloud-netflix.git
          search-paths: respo
          username: root
          password: 12345678

server:
  port: 8888

eureka:
  instance:
    hostname: localhost
  client:
    serviceUrl:
      defaultZone: http://${eureka.instance.hostname}:8761/eureka/

注意: 如果使用 GitLab 作为仓库的话,git.uri 需要在结尾加上 .git,GitHub 则不用

部署测试用配置

在项目中创建一个名为 respo 的目录用于存放配置文件,创建一个名为 config-client-dev.yml 的配置文件用于测试配置服务器是否生效

foo: foo version 1
demo:
  message: Hello Spring Cloud Config

测试配置中心

浏览器端访问:http://localhost:8888/config-client/dev/master 显示如下

<Environment>
    <name>config-client</name>
    <profiles>
        <profiles>dev</profiles>
    </profiles>
    <label>master</label>
    <version>fcaca1fd6b3f95cb577f55d075a24f79d5e6a4a7</version>
    <state/>
    <propertySources>
        <propertySources>
            <name>
                http://10.3.50.121:8080/root/hello-spring-cloud-netflix.git/respo/config-client-dev.yml
            </name>
            <source>
                <foo>foo version 1</foo>
                <demo.message>Hello Spring Cloud Config</demo.message>
            </source>
        </propertySources>
    </propertySources>
</Environment>

能够成功读取出刚才上传的配置则说明配置中心部署成功

配置中心相关说明

  • 配置文件

    • spring.cloud.config.label:配置仓库的分支
    • spring.cloud.config.server.git.uri:配置 Git 仓库地址(GitHub、GitLab、码云 ...)
    • spring.cloud.config.server.git.search-paths:配置仓库路径(存放配置文件的目录)
    • spring.cloud.config.server.git.username:访问 Git 仓库的账号
    • spring.cloud.config.server.git.password:访问 Git 仓库的密码
  • HTTP 请求地址和资源文件映射

    • http://ip:port/{application}/{profile}[/{label}]
    • http://ip:port/{application}-{profile}.yml
    • http://ip:port/{label}/{application}-{profile}.yml
    • http://ip:port/{application}-{profile}.properties
    • http://ip:port/{label}/{application}-{profile}.properties

配置中心客户端

在所有需要使用配置中心的服务按以下流程操作

  • 增加依赖
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-config</artifactId>
</dependency>
  • 增加配置
spring:
  cloud:
    config:
      # 配置服务中心的地址
      uri: http://localhost:8888
      # 配置文件名称的前缀
      name: config-client
      # 配置仓库的分支
      label: master
      # 配置文件的环境标识(dev,test,prod)
      profile: dev

# 增加健康检查配置
# 这里的目的是开启 actuator/refresh 接口用于刷新配置
management:
  endpoint:
    shutdown:
      enabled: false
  endpoints:
    web:
      exposure:
        include: "*"
  • 在 Controller 中获取动态配置,通过 @RefreshScope 注解开启刷新配置功能,可以使用 DynamicPropertyFactory 动态获取配置内容
package com.funtl.hello.spring.cloud.business.controller;

import com.funtl.hello.spring.cloud.business.feign.BusinessAdminFeign;
import com.netflix.config.DynamicPropertyFactory;
import com.netflix.config.DynamicStringProperty;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;

@RefreshScope
@RestController
public class BusinessAdminController {
    @GetMapping(value = "hello")
    public String sayHello() {
        DynamicStringProperty property = DynamicPropertyFactory.getInstance().getStringProperty("demo.message", "Hello World");
        return property.getValue();
    }
}
  • 刷新配置,修改配置文件后并不会自动刷新配置,需要手动 POST 请求服务的 actuator/refresh 接口才可以使新配置生效,我们可以使用 Postman 测试
Lusifer_20191205001354.png
  • 通过浏览器访问对应接口查看效果即可

Spring Boot Profile

我们在做项目开发的时候,生产环境和测试环境的一些配置可能会不一样,有时候一些功能也可能会不一样,所以我们可能会在上线的时候手工修改这些配置信息。但是 Spring 中为我们提供了 Profile 这个功能。我们只需要在启动的时候添加一个虚拟机参数,激活自己环境所要用的 Profile 就可以了。

操作起来很简单,只需要为不同的环境编写专门的配置文件,如:application-dev.ymlapplication-prod.yml, 启动项目时只需要增加一个命令参数 --spring.profiles.active=环境配置 即可,启动命令如下

java -jar app.jar --spring.profiles.active=prod

特别说明: 本人平时混迹于 B 站,不咋回复这里的评论,有问题可以到 B 站视频评论区留言找我
视频地址: https://space.bilibili.com/31137138/favlist?fid=326428938
课件说明: 本次提供的课件是 Spring Cloud Netflix 版微服务架构指南,如果有兴趣想要学习 Spring Cloud Alibaba 版,可以前往 http://www.qfdmy.com 查看相关课程资源
案例代码: https://github.com/topsale/hello-spring-cloud-netflix