SpringCloud 微服务实战4 - Gateway服务网关、Config配置中、Bus服务总线

一、Gateway 新一代网关

1.概述

1.1Gateway是什么

zuul1 官网:Home · Netflix/zuul Wiki · GitHub

GateWay官网:Spring Cloud Gateway

Cloud 全家桶中有个很重要的组件就是网关,在 1.x版本中都是采用 Zuul 网关;但在2.x版本中,Zuul的升级一直跳票,SpringCloud最后自己研发了一个网关替代Zuul,那就是 SpringCloud Gateway。一句话:Gateway是原Zuul1.x版的替代。

Gateway 是在Spring生态系统之上构建的 API 网关服务,基于 Spring5,SpringBoot和Project Reactor 等技术。Gateway旨在提供一种简单而有效的方式来对 API 进行路由,以及提供一些强大的过滤器功能,例如:熔断、限流、重试等。

SpringCloud Gateway 作为 SpringCloud 生态系统中的网关,目标是替代Zuul,在SpringCloud 2.0以上版本中,没有对新版本的Zuul2.0以上最新高性能版本进行集成,仍然是使用 Zuul1.x 非 Reactor 模式的老版本程序。而为了提高网关的性能,SpringCloud Gateway 是基于 WebFlux 框架实现的,而 WebFlux 框架底层则使用了高效能的 Reactor 模式通讯框架 Netty。

SpringCloud Gateway 的目标是提供统一的方式且基于 Filter 链的方式提供了网关基本的功能,例如:安全、监控/指标和限流。

1.2 Gateway能干吗

  • 反向代理
  • 鉴权
  • 流量控制
  • 熔断
  • 日志监控
  • ......

1.3微服务架构中网关在哪里

1.4有了Zuul怎么又出来了Gateway

我们为什么选择 Gateway?

一方面因为 Zuul1.0 已经进入了维护阶段,而且 Gateway 是 SpringCloud 团队研发的,是亲儿子产品,值得信赖。

Gateway 是基于异步非阻塞模式上进行开发的,性能方面不需要担心。虽然 Netflix 早就发布了最新的 Zuul 2.x,但是 SpringCloud 貌似没有整合计划。而且 Netflix 相关组件都宣布进入维护期,不知前景如何。

多方面综合考虑,Gateway 是很理想的网关选择。

SpringCloud Gateway 具有如下特性:

  • 基于Spring Framework 5,Project Reactor 和 SpringBoo 2.0 进行构建
  • 动态路由:能够匹配任何请求属性
  • 可以对路由指定 Predicate(断言)和 Fliter(过滤器)
  • 集成Hystrix的断路器功能
  • 集成SpringCloud服务发现功能
  • 易于编写 Predicate(断言)和 Fliter(过滤器)
  • 请求限流功能
  • 支持路径重写

在 SpringCloud Finchley 正式版之前,SpringCloud推荐的网关是 Netflix 提供的 Zuul;

SpringCloud Gateway 和 Zuul的区别:

  • Zuul 1.x 是一个基于阻塞 I/O 模型的 API Gateway
  • Zuul 1.x 基于 Servlet2.5 使用阻塞框架,它不支持任何长连接(如WebSocket)。Zuul的设计模式和 Nginx 较像,每次 I/O 操作都是从工作线程中选择一个执行,请求线程被阻塞到工作线程完成,但是差别是 Nginx 用 C++ 实现,Zuul 用 Java 实现,而 JVM 本身会有第一次加载较慢的情况,使得 Zuul 的性能相对较差。
  • Zuul 2.x 理念更先进,向基于 Netty 非阻塞和支持长连接,但 SpringCloud 目前还没整合。
  • SpringCloud Gateway 基于Spring Framework 5,Project Reactor 和 SpringBoo 2.0 进行构建,使用非阻塞API。
  • SpringCloud Gateway 还支持 WebSocket,并与 Spring 紧密集成拥有更好的开发体验

Zuul1.x模型 

SpringCloud中所集成的Zuul版本,采用的是Tomcat容器,使用传统的 Servlet IO 模型。

Servlet 由 Servlet Container 进行声明周期管理

  • Container 启动时构造 Servlet 对象并调用 Servlet init() 进行初始化。
  • Container 运行时接收请求,并未每个请求分配一个线程(一般从线程池中获取空闲线程)然后调用 service()。
  • Container 关闭时调用 Servlet destroy() 销毁 Servlet。

上述模式的缺点:Servlet是一个简单的网络IO 模型,当请求 进入Servlet Container 时,Servlet Container 就会为其绑定一个线程,在并发不高的场景下这种模型是使用的。但是一旦高并发(比如jmeter压力测试),线程数量就会上涨,而线程资源代价是昂贵的(上下文切换,内存消耗大)严重影响请求的处理时间。在一些简单业务场景下,不希望为每个 request 分配一个线程,只需要1个或几个线程就能应对极大并发的请求,这种业务场景下 Servlet 模型没有优势。

所以 Zuul1.x 是基于 Servlet 之上的一个阻塞式处理模型,即Spring实现了处理所有请求的一个Servlet(DispatcherServlet),并由该 Servlet 阻塞式处理。

Gateway模型

传统的 Web 框架,比如说:struts2、SpringMVC 等都是基于 Servlet API 与 Servlet 容器基础之上运行的。

但是在 Servlet 3.1 之后有了异步非阻塞的支持。而 WebFlux 是一个典型非阻塞异步的框架,它的核心是基于 Reactor 的相关 API 实现的。相对于传统的 Web 框架来说,它可以运行在诸如 Netty、Undertow 及支持 Servlet 3.1 的容器上。

Spring WebFlux 是 Spring5.0 引入的新的响应式框架,区别与 SpringMVC,它不需要依赖 ServletAPI ,它是完全异步非阻塞的,并基于 Reactor 来实现响应式流规范。

2.三大核心

Route(路由):路由是构建网关的基本模块,它由ID、目标URL、一些列断言和过滤器组成,如果断言为true则匹配该路由。

Predicate(断言):参考的是 Java8 的 java.util.Predicate,开发人员可以匹配 HTTP 请求中的所有内容(例如请求头或请求参数),如果请求与断言相匹配则进行路由

Filter(过滤):指的是 Spring 框架中 GatewayFilter 的实例,使用过滤器,可以在请求被路由前或之后对请求进行修改。

总体:Web请求,通过一些匹配条件,定位到真正的服务节点。并在这个转发过程的前后,进行一些精细化控制。Predicate就是我们的匹配条件;filter,就可以理解为一个无所不能的拦截器。有了这两个元素,再加上目标uri,就可以实现一个具体的路由了。

3.Gateway 工作流

 客户端向 Spring Cloud Gateway 发出请求,然后 Gateway Handler Mapping 中找到与请求相匹配的路由,将其发送到 Gateway Web Handler。

Handler 再通过指定的过滤器链来将请求发送到我们实际的服务执行业务逻辑,然后返回。过滤器之间用虚线分开是因为过滤器可能会在发送代理请求之前(pre)或之后(post)执行业务逻辑。

Filter在 pre 类型的过滤器可以做参数校验、权限校验、流量监控、日志输出、协议转换等。在 post 类型的过滤器中可以做响应内容、响应头的修改,日志的输出,流量监控等有着非常重要的作用。

核心逻辑:路由转发+执行过滤器链

4.入门配置

新建模块 cloud-gateway-gateway9527

注意:因为SpringCloudGateway的内部是通过netty + webflux实现的,webflux是和springmvc有冲突的,需要移除掉 spring-boot-starter-web 依赖

POM

    <dependencies>
        <!-- gateway -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-gateway</artifactId>
        </dependency>
        <!-- eureka-client -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
        <!-- 引入自己的 cloud-api-commons 模块-->
        <dependency>
            <groupId>com.cloud.study</groupId>
            <artifactId>cloud-api-commons</artifactId>
            <version>${project.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        <!-- lombok -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <!-- test -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

YAML

server:
  port: 9527

spring:
  application:
    name: cloud-gateway

eureka:
  client:
    register-with-eureka: true #表示是否将自己注册进EurekaServer,默认为true
    fetch-registry: true #是否从 EurekaServer 抓取自己有的注册信息,默认为true。单节点无所谓,集群必须设置为true才能配合ribbon使用负载均衡
    service-url:
      #设置与 Eureka Server 交互的地址查询服务和注册服务都需要依赖这个地址
      defaultZone: http://eureka7001.com:7001/eureka/,http://eureka7002.com:7002/eureka/ #集群 Eureka 配置
  instance:
    instance-id: ${spring.application.name}
    prefer-ip-address: true #显示IP

主启动类

@SpringBootApplication
@EnableEurekaClient
public class GatewayMain9527 {
    public static void main(String[] args) {
        SpringApplication.run(GatewayMain9527.class, args);
    }
}

9527网关如何做路由映射?

cloud-provider-payment8001 看看 controller的访问地址(get、lb)

我们目前不想暴露8001端口,希望在 8001 外侧嵌套一层9527

YAML新增网关配置

spring:
  application:
    name: cloud-gateway
  cloud:
    gateway:
      routes:
        - id: payment_routh           #路由的ID,没有固定规则,但要求唯一,建议配合服务名
          uri: http://localhost:8001  #匹配后提供服务的路由地址
          predicates:
            - Path=/payment/get/**     #断言,路径相匹配的进行路由

        - id: payment_routh2
          uri: http://localhost:8001
          predicates:
            - Path=/payment/lb/**

注意 Path 要大写 

测试

  • 启动7001/7002
  • 启动8001
  • 启动9527

访问说明:

YAML配置说明

Gateway 网关路由有两种配置方式:

  • 在配置文件 yaml 中配置,详情见前面的配置
  • 代码中注入 RoutLocatoe 的 Bean

代码中注入 RoutLocatoe 的 Bean

我们用一个案例,通过9527的网关访问到外网百度国内新闻网址 百度新闻——海量中文资讯平台

在项目中添加如下配置类

@Configuration
public class GatewayConfig {
    /**
     * 配置了一个id为 path_route_study、path_route_study2 的路由规则
     * 当访问地址为 http://localhost:9527/guonei 时会自动转发到地址 https://news.baidu.com/guonei
     */
    @Bean
    public RouteLocator routes(RouteLocatorBuilder routeLocatorBuilder){
        RouteLocatorBuilder.Builder routes = routeLocatorBuilder.routes();
        routes.route("path_route_study", r -> r.path("/guonei").uri("https://news.baidu.com/guonei"));
        routes.route("path_route_study2", r -> r.path("/guoji").uri("https://news.baidu.com/guoji"));
        return routes.build();
    }
}

访问 http://localhost:9527/guonei 和  http://localhost:9527/guoji 时能正常调整到页面,访问别的页面时就会报错。

5.通过微服务名实现动态路由

默认情况下 Gateway 会根据注册中心注册的服务列表,已注册中心上微服务名为路径,创建动态路由进行转发,从而实现动态路由的功能

修改 cloud-gateway-gateway9527 的 yaml 配置文件

spring:
  application:
    name: cloud-gateway
  cloud:
    gateway:
      discovery:
        locator:
          enabled: true     #开启从注册中心动态创建路由的功能,利用微服务名进行路由
      routes:
        - id: payment_routh           #路由的ID,没有固定规则,但要求唯一,建议配合服务名
          #uri: http://localhost:8001  #匹配后提供服务的路由地址
          uri: lb://cloud-payment-service #匹配后提供服务的路由地址
          predicates:
            - Path=/payment/get/**     #断言,路径相匹配的进行路由

        - id: payment_routh2
          #uri: http://localhost:8001
          uri: lb://cloud-payment-service #匹配后提供服务的路由地址
          predicates:
            - Path=/payment/lb/**

需要注意的是 uri 的协议是为 lb,表示启用 Gateway 的负载均衡功能

lb://serviceName 是SpringCloud Gateway 在微服务中自动为我们创建的负载均衡 uri 

6.Predicate 的使用

Gateway 项目运行的时候会输出下面的log

6.1 Route Predicate Factories 是什么

SpringCloud Gateway 将路由匹配作为 Spring WebFlux HandlerMapping 基础架构的一部分。SpringCloud Gateway 包含许多内置的 Route Predicate 工厂。所有的这些 Predicate 都与HTTP请求的不同属性匹配。多个 Route Predicate 工厂可以进行组合。

SpringCloud Gateway 创建 Route 对象时,使用 RoutePredicateFactories 创建 Predicate 对象,Predicate 对象可以赋值给 Route。

6.3 常用的 Route Predicate

Route Predicate 是在 yaml 文件下的 predicates 配置

常用的 Route Predicate 有:

After Route Predicate

在xx时间之后才能访问的路由 

spring:
  cloud:
    gateway:
      routes:
        - id: payment_routh2
          #uri: http://localhost:8001
          uri: lb://cloud-payment-service #匹配后提供服务的路由地址
          predicates:
            - Path=/payment/lb/**
            - After=2021-09-16T15:19:39.381+08:00[Asia/Shanghai] 

这个时间如何得出,可以运行以下测试程序:

public class T1 {
    public static void main(String[] args) {
        ZonedDateTime zbj = ZonedDateTime.now(); //默认时区
        System.out.println(zbj); //输出 2021-09-16T15:19:39.381+08:00[Asia/Shanghai]
    }
}

Before Route Predicate

在xx时间之前才能访问的路由 

spring:
  cloud:
    gateway:
      routes:
        - id: payment_routh2
          #uri: http://localhost:8001
          uri: lb://cloud-payment-service #匹配后提供服务的路由地址
          predicates:
            - Path=/payment/lb/**
            - Before=2021-09-16T15:19:39.381+08:00[Asia/Shanghai]

Between Route Predicate

在xx时间与xx时间之间才能访问的路由,两个时间应英文逗号隔开

spring:
  cloud:
    gateway:
      routes:
        - id: payment_routh2
          #uri: http://localhost:8001
          uri: lb://cloud-payment-service #匹配后提供服务的路由地址
          predicates:
            - Path=/payment/lb/**
            - Between=2021-09-16T15:19:39.381+08:00[Asia/Shanghai],2021-10-16T15:19:39.381+08:00[Asia/Shanghai]

Cookie Route Predicate

Cookie Route Predicate 需要两个参数,一个是 Cookie name,一个是正则表达式。路由规则会通过获取对应的 Cookie 值和正则表达式去匹配,如果匹配上就会执行路由,如果没有匹配上则不执行。

spring:
  cloud:
    gateway:
      routes:
        - id: payment_routh2
          #uri: http://localhost:8001
          uri: lb://cloud-payment-service #匹配后提供服务的路由地址
          predicates:
            - Path=/payment/lb/**
            - Cookie=username,chenj

测试 使用 crul 工具进行测试

  • 不带 Cookie 的访问  curl http://localhost:9527/payment/lb
  • 带 Cookie 的访问(username=111) curl http://localhost:9527/payment/lb --cookie "username=111"
  • 带 Cookie 的访问(username=chenj) curl http://localhost:9527/payment/lb --cookie "username=chenj"

Header Route Predicate

两个参数:一个是属性名,一个是正则表达式,这个属性值和正则表达式匹配则执行。

spring:
  cloud:
    gateway:
      routes:
        - id: payment_routh2
          #uri: http://localhost:8001
          uri: lb://cloud-payment-service #匹配后提供服务的路由地址
          predicates:
            - Path=/payment/lb/**
            - Header=X-Request-Id, \d+ #请求头要有 X-Request-Id 属性且值为整数

 curl http://localhost:9527/payment/lb -H "X-Request-Id:123"

Host Route Predicate

Host Route Predicate 接收一组参数,一组匹配的域名列表,这个模板是一个 ant 分隔的模板,用.号作为分隔符。它通过参数中的主机地址作为匹配规则。

spring:
  cloud:
    gateway:
      routes:
        - id: payment_routh2
          #uri: http://localhost:8001
          uri: lb://cloud-payment-service #匹配后提供服务的路由地址
          predicates:
            - Path=/payment/lb/**
            - Host=**.com 

 curl http://localhost:9527/payment/lb -H "Host:www.aa.com"

Method Route Predicate

可以通过是 POST、GET、PUT、DELETE 等不同的请求方式来进行路由

spring:
  cloud:
    gateway:
      routes:
        - id: payment_routh2
          #uri: http://localhost:8001
          uri: lb://cloud-payment-service #匹配后提供服务的路由地址
          predicates:
            - Path=/payment/lb/**
            - Method=GET
  • 以Get方式访问 curl http://localhost:9527/payment/lb
  • 已POST 方式访问 curl -X post http://localhost:9527/payment/lb 失败

Path Route Predicate

Path Route Predicate 接收一个匹配路径的参数来判断是否走路由

spring:
  cloud:
    gateway:
      routes:
        - id: payment_routh2
          #uri: http://localhost:8001
          uri: lb://cloud-payment-service #匹配后提供服务的路由地址
          predicates:
            - Path=/payment/lb/**

curl http://localhost:9527/payment/lb 

Query Route Predicate

Query Route Predicate 支持传入两个参数,一个是属性名一个为属性值,属性值可以是正则表达式。

spring:
  cloud:
    gateway:
      routes:
        - id: payment_routh2
          #uri: http://localhost:8001
          uri: lb://cloud-payment-service #匹配后提供服务的路由地址
          predicates:
            - Path=/payment/lb/**
            - Query=smile
  •  curl http://localhost:9527/payment/lb?smile=1  成功
  • curl http://localhost:9527/payment/lb 失败

7.Filter的使用

7.1 Filter 是什么?

路由过滤器可用于修改进入的 HTTP 请求和返回的 HTTP 响应,路由过滤器只能指定路由进行使用。

SpringCloud Gateway 内置了多种路由过滤器,他们都由 GatewayFilter 的工厂类来产生。

7.2 Spring Cloud Gateway 的 Filter

生命周期:pre、post

GatewayFilter

官网:Spring Cloud Gateway

有30多种,自己可以照着官网,敲一两个例子即可。

我们现在测试 AddRequestHeader、AddResponseHeader

spring:
  cloud:
    gateway:
      routes:
        - id: payment_routh2
          #uri: http://localhost:8001
          uri: lb://cloud-payment-service #匹配后提供服务的路由地址
          predicates:
            - Path=/payment/lb/**
          filters:
            - AddRequestHeader=X-Request-red, blue   #请求头添加 X-Request-red:red
            - AddResponseHeader=X-Response-Red, Blue #响应头添加 X-Response-Red:Blue
            - AddRequestParameter=color, blue        #请求url自动添加参数

此时修改8001 的PaymentController,输出 Headers 和 参数

    @GetMapping("/payment/lb")
    public String paymentLb(HttpServletRequest request) {
        Enumeration<String> headers = request.getHeaderNames();
        System.out.println("-----------header----------------");
        while (headers.hasMoreElements()) {
            String header = headers.nextElement();
            System.out.println(header+ ":" +request.getHeader(header));
        }
        System.out.println("-----------param----------------");
        Enumeration<String> params = request.getParameterNames();
        while (params.hasMoreElements()) {
            String param = params.nextElement();
            System.out.println("param: " +param+ "=" +request.getParameter(param));
        }
        return serverPort;
    }

可以发现8001的请求头中有  x-request-red:blue,参数中有 color=blue

响应头由我们甚至的值

  

GlobalFilter

官网:Spring Cloud Gateway

将在下一章节降到自定义过滤器,介绍 GlobalFilter 的使用。

7.3 自定义过滤器(GlobalFilter )

主要由两个接口 GlobalFilter、Ordered。

自定义过滤器能干嘛:

  • 全局日志记录
  • 统一网关鉴权
  • ......

测试代码

@Component
@Slf4j
public class MyLogGatewayFilter implements GlobalFilter, Ordered {
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        log.info("********** come in MyLogGatewayFilter:    "+ new Date());
        String uname = exchange.getRequest().getQueryParams().getFirst("uname");
        if (uname == null) {
            log.info("******* 用户名为null,非法用户********");
            exchange.getResponse().setStatusCode(HttpStatus.NOT_ACCEPTABLE);
            return exchange.getResponse().setComplete();
        }
        return chain.filter(exchange);
    }

    @Override
    public int getOrder() {
        return 0; //数字越小优先级越高
    }
}

测试

二、SpringCloud Config 分布式配置

1、概述

官网地址:Spring Cloud Config

1.1 分布式系统面临的问题——配置问题

微服务意味着要将单体应用中的业务拆分成一个个子服务,每个服务的粒度相对较小,因此系统中会出现大量的服务。由于每个服务都需要必要的配置信息才能运行,所以一套集中式的、动态的配置管理设施是必不可少的。

SpringCloud 提供了 ConfigServer 来解决这个问题,我们每一个微服务自己带着一个 application.yaml,上百个配置文件的管理...

1.2 Config 是什么?

SpringCloud Config 为微服务架构中的微服务提供集中化的外部配置支持,配置服务器为各个不同微服务应用的所有环境提供了一个中心化的外部配置。 

SpringCloud Config 分为 服务端和客户端两部分

服务端也称为分布式配置中心,它是一个独立的微服务应用,用来连接配置服务器并未客户端提供获取配置信息,加密/解密信息等访问接口。

客户端则是通过指定的配置中心来管理应用资源,以及与业务相关的配置内容,并在启动的时候从配置中心获取和加载配置信息。配置服务器默认采用git来存储配置信息,这样就有助于对环境配置进行版本管理,并且可以通过git客户端工具来方便的访问配置内容。

1.3 Config 能干嘛?

集中管理配置文件。

不同环境不同配置,动态化的配置更新,分环境部署比如 dev/test/prod/beta/release。

运行期间动态调整配置,不再需要在每个服务部署的机器上编写配置文件,服务会向配置中心统一拉取自己的信息。

当配置发生改变时,服务不需要重启即可感知到配置的变化并应用新的配置。

将配置信息以 REST 接口的形式暴露。

1.4 与GitHub整合配置

由于 SpringCloud Config默认使用 Git来存储配置文件(也有其他方式,比如支持 SVN 和本地文件),但最推荐的还是 Git,而且使用的是 http/https 访问形式。

2、Config服务端配置与测试

用你自己的账号在 码云(gitee)上新建一个名为 springcloud-config的新 Repository。

由上一步获得刚新建的 gitee 地址 https://gitee.com/canima_895/springcloud-config.git

本地硬盘目录上新建git仓库并 clone :

此时在本地 F 盘下 F:\config\SpringCloud2021\springcloud-config

  • 保存文件的格式必须为 UTF-8
  • 如果需要修改,修改完成后可以通过 git add 、git comm -m "xx" 、git push origin master 提交到gitee后台
  • config-dev.yaml 内容如下(config-test.yaml、config-prod.yaml,内容与之类似)
config:
  info: "master branch,springcloud-config/config-dev.yaml version=1"

新建module模块 cloud-config-center3344,它即为Cloud的配置中心模块 cloudConfig Center。

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">
    <parent>
        <artifactId>cloud-study</artifactId>
        <groupId>com.cloud.study</groupId>
        <version>1.0</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>cloud-config-center-3344</artifactId>

    <dependencies>
        <!-- config -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-config-server</artifactId>
        </dependency>
        <!-- Eureka client -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
        <!-- 引入自己的 cloud-api-commons 模块-->
        <dependency>
            <groupId>com.cloud.study</groupId>
            <artifactId>cloud-api-commons</artifactId>
            <version>${project.version}</version>
        </dependency>

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

        <!--开启热部署-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
        <!-- lombok -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <!-- test -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>
</project>

yaml文件。

server:
  port: 3344

spring:
  application:
    name: cloud-config-server #注册进 Eureka服务器的微服务名
  cloud:
    config:
      server:
        git:
          #username: #GIT用户名
          #password: #GIT密码
          uri: https://gitee.com/canima_895/springcloud-config.git #gitee上面的 git仓库名字
          #搜索目录 (配置文件远程仓库名字)
          search-paths:
            - springcloud-config
          #读取分支
          label: master

eureka:
  client:
    register-with-eureka: true #表示是否将自己注册进EurekaServer,默认为true
    fetch-registry: true #是否从 EurekaServer 抓取自己有的注册信息,默认为true。单节点无所谓,集群必须设置为true才能配合ribbon使用负载均衡
    service-url:
      #设置与 Eureka Server 交互的地址查询服务和注册服务都需要依赖这个地址
      defaultZone: http://eureka7001.com:7001/eureka/,http://eureka7002.com:7002/eureka/ #集群 Eureka 配置

主启动类。

@SpringBootApplication
@EnableEurekaClient
@EnableConfigServer
public class ConfigMain3344 {
    public static void main(String[] args) {
        SpringApplication.run(ConfigMain3344.class, args);
    }
}

windows 下修改 hosts 文件,增加映射。

127.0.0.1 config-3344.com

测试通过 Config 微服务是否可以从 GitHub 上获取配置内容。

配置读取规则有以下几种,个人习惯还是使用第一种

/{label}/{application}-{profile}.yaml

/{application}-{profile}.yaml :默认会从master分支查找

/{application}/{profile}/{label}.yaml

{
    "name":"config",
    "profiles":[
        "test"
    ],
    "label":"master",
    "version":"82729ce5eef7b9c135649d0bf669cdd5d6335809",
    "state":null,
    "propertySources":[
        {
            "name":"https://gitee.com/canima_895/springcloud-config.git/config-test.yaml",
            "source":{
                "config.info":"master branch,springcloud-config/config-test.yaml version=1"
            }
        }
    ]
}

3、Config客户端配置与测试

新建 cloud-config-client-3355

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">
    <parent>
        <artifactId>cloud-study</artifactId>
        <groupId>com.cloud.study</groupId>
        <version>1.0</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>cloud-config-client-3355</artifactId>

    <dependencies>
        <!-- config -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-config</artifactId>
        </dependency>
        <!-- Eureka client -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
        <!-- 引入自己的 cloud-api-commons 模块-->
        <dependency>
            <groupId>com.cloud.study</groupId>
            <artifactId>cloud-api-commons</artifactId>
            <version>${project.version}</version>
        </dependency>

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

        <!--开启热部署-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
        <!-- lombok -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <!-- test -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>
</project>

yaml文件 - bootstrap.yaml

application.yaml 是用户级的资源配置项,bootstrap.yaml 是系统的,优先级更高。

SpringCloud会创建一个“Bootstrap Context”,作为Spring应用的“Application Context”的父上下文。初始化的时候,“Bootstrap Context”负责从外部源加载配置属性并解析配置。这两个上下文共享一个从外部获取的“Environment”。

“Bootstrap ”属性有高优先级,默认情况下,他们不会被本地配置覆盖。“Bootstrap Context”和“Application Context”有着不同的约定,所以新增了一个“bootstrap.yaml”文件,保证“Bootstrap Context”和“Application Context”配置的分离。

要将Client模块下的 application.yaml文件改为“Bootstrap Context”和“Application Context”,这是很关键的,因为 

server:
  port: 3355
spring:
  application:
    name: config-client
  cloud:
    # config 客户端信息
    config:
      label: master #分支名称
      name: config  #配置文件名称
      profile: dev  #读取后缀名
      uri: http://config-3344.com:3344 #配置中心地址

eureka:
  client:
    register-with-eureka: true #表示是否将自己注册进EurekaServer,默认为true
    fetch-registry: true #是否从 EurekaServer 抓取自己有的注册信息,默认为true。单节点无所谓,集群必须设置为true才能配合ribbon使用负载均衡
    service-url:
      #设置与 Eureka Server 交互的地址查询服务和注册服务都需要依赖这个地址
      defaultZone: http://eureka7001.com:7001/eureka/,http://eureka7002.com:7002/eureka/ #集群 Eureka 配置

主启动

@SpringBootApplication
@EnableEurekaClient
public class ConfigClientMain3355 {
    public static void main(String[] args) {
        SpringApplication.run(ConfigClientMain3355.class, args);
    }
}

业务类

@RestController
@Slf4j
public class ConfigClientController {
    @Value("${config.info}")
    private String configInfo;

    @GetMapping("/configInfo")
    public String getConfigInfo() {
        return configInfo;
    }
}

测试

问题

  • 修改 Git 上的配置文件内容( config-dev.yaml )。
  • 刷新 3344 ,发现 配置中心 立刻响应。
  • 刷新 3355 ,发现 客户端 输出的配置仍然是原来的内容。
  • 3355 没有变化,除非自己重启或者重新加载。

4、Config客户端之动态刷新

目的:避免每次更新配置都要重启客户端微服务 3355。

4.1 动态刷新步骤 - 修改 3355 模块

pom 引入Actuator 监控,我们在之前已经引入了

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

修改 yaml,暴露监控端口

#暴露监控端点
management:
  endpoints:
    web:
      exposure:
        include:  "*"

@RefreshScope 业务类修改 

@RestController
@Slf4j
@RefreshScope
public class ConfigClientController {
    @Value("${config.info}")
    private String configInfo;

    @GetMapping("/configInfo")
    public String getConfigInfo() {
        return configInfo;
    }
}

自动刷新只能刷新 @RefreshScope 注解下的配置,一些特殊配置,如数据库等,需要同样先设置数据库链接ConfigServer类,然后通过加 @RefreshScope 注解方式。

测试:

  • 启动 7001/7002
  • 启动 3344
  • 启动 3355
  • 此时修改 git 上的值 ,发现3355的值还是没有改变

运维人员,使用 crul 发送POS请求:

curl -X POST "http://localhost:3355/actuator/refresh"

总结:运维人员修改完配置文件后,需要发送一个 POST 请求,这样客户端才会刷新最新的配置文件。 

再次访问 http://localhost:3355/configInfo ,发现值已经刷新。

4.2 还有什么问题?

由4.1节可知,修改了配置文件后,每个客户端都要发一个 POST 请求,如果有100个微服务那么就要发送100次POST请求。所以在下一章节消息总线将会解决此问题。

三、SpringCloud Bus 消息总线

1、概述

1.1 SpringCloud Bus 是什么?

SpringCloud Bus 配合 SpringCloud Config 使用可以实现配置的动态刷新

SpringCloud Bus 是用来将分布式系统的节点与轻量级消息系统链接起来的框架,它整合了 Java 的事件处理机制和消息中间件的功能

Bug 支持两种消息代理:RabbitMQ 和 Kafka。

1.2 SpringCloud Bus 能干吗?

SpringCloud Bus 能管理和传播分布式系统间的消息,就像一个分布式执行器,可用于广播状态更改、事件推送等,也可以当做微服务间的通信通道。

 1.3 为什么被称为总线

 在微服务架构的系统中,通常会使用轻量级的消息代理来构建一个公用的消息主题,并让系统中所有微服务实例都连接上来。由于该主题中产生的消息会被所有实例监听和消费,所以称它为消息总线。在总线的各个实例上,都可以方便地广播一些需要让其他连接在该主题上的实例都知道的消息。

基本原理:ConfigClient 实例都监听 MQ 中同一个 topic(默认是 SpringCloudBus)。当一个服务刷新数据的时候,它会把这个消息放入 Topic 中,这样其他监听同一个 Topic 的服务就能得到通知,然后去更新自生的配置。

2、RabbitMQ 环境配置

Windows下RabbitMQ安装及注意事项_Harry的博客-CSDN博客

安装 Erlang,下载地址:Erlang Programming Language

安装 RabbitMQ,下载地址:Downloading and Installing RabbitMQ — RabbitMQ

进入 RabbitMQ 安装目录下的 sbin 目录:D:\Program Files\RabbitMQ Server\rabbitmq_server-3.9.5\sbin

输入以下命令启动管理功能:rabbitmq-plugins enable rabbitmq_management

安装完成后,启动我们的服务(点击 RabbitMQ Service start 图标)

访问地址查看是否安装成功:http://127.0.0.1:15672

输入账号密码并登录  guest-guest

3、SpringCloud Bus 动态刷新全局广播

必须先具备良好的 RabbitMQ 环境。

为演示广播效果,再以3355为模板再制作一个 3366模块,在3355、3366 的controller增加server.port的输出

@RestController
@Slf4j
@RefreshScope
public class ConfigClientController {
    @Value("${config.info}")
    private String configInfo;
    @Value("${server.port}")
    private String serverPort;

    @GetMapping("/configInfo")
    public String getConfigInfo() {
        return "serverPort: " + serverPort + "\tconfigInfo: " + configInfo;
    }
}

3.1 设计思想

a.利用消息总线触发一个客户端/bus/refresh,而刷新所有客户端的配置。

b.利用消息总线触发一个服务端ConfigServer的/bus/refresh 端点,而刷新所有客户端的配置。

显然 b 的架构更加适合,a 不适合的原因如下:

  • 打破了微服务的职责单一性,因为微服务本身是业务模块,它本不应该承担配置刷新的职责。
  • 破坏了微服务各节点的对等性。
  • 有一定的局限性。例如,微服务在迁移时,它的网络地址经常变化,此时如果想要做到自动刷新,那就会增加更多的修改。

3.2 给 cloud-config-center-3344 配置中心服务端添加消息总线支持

pom文件添加 RabbitMQ 支持

        <!-- 添加消息总线RabbitMQ支持 -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-bus-amqp</artifactId>
        </dependency>

application.yaml 文件添加 rabbitmq 配置和 暴露监控端点配置

#暴露监控端点
management:
  endpoints:
    web:
      exposure:
        include:  "bus-refresh"

#rabbitmq相关配置
rabbitmq:
  host: localhost
  port: 5672
  username: guest
  password: guest

3.3 给 cloud-config-center-3355、cloud-config-center-3366 客户端添加消息总线支持

 pom文件添加 RabbitMQ 支持

        <!-- 添加消息总线RabbitMQ支持 -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-bus-amqp</artifactId>
        </dependency>

bootstrap.yaml 添加rabbitmq配置

#rabbitmq相关配置
rabbitmq:
  host: localhost
  port: 5672
  username: guest
  password: guest

3.6 测试

启动7001/7002

启动3344

启动3355、3366

去Git上增加 config-dev.yaml 中的版本号

发送 POST 请求 ,(切记,往配置中心3344 发送

curl -X POST "http://localhost:3344/actuator/bus-refresh"

刷新页面  http://localhost:3355/configInfo 、http://localhost:3366/configInfo 发现配置均已更新

4、SpringCloud Bus 动态刷新定点通知

不想全部通知,只想定点通知。

公式: http://localhost:3344/actuator/bus-refresh/{destination} 。

/bus/refresh 请求不再发送到具体的服务实例上,而是发给 config server 并通过 destination 参数类指定需要更新的服务或实例。destination=微服务名+:+端口号。

我们已刷新运行在 3355 端口上的 config-client 为例

curl -X POST "http://localhost:3344/actuator/bus-refresh/config-client:3355"
  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值