目录
1 微服务网关概述

在学习完前面的知识后,微服务架构已经初具雏形。但还有一些问题:不同的微服务一般会有不同的网络地址,客户端在访问这些微服务时必须记住几十甚至几百个地址,这对于客户端方来说太复杂也难以维护。如下图:

如果让客户端直接与各个微服务通讯,可能会有很多问题:
- 客户端会请求多个不同的服务,需要维护不同的请求地址,增加开发难度
- 在某些场景下存在跨域请求的问题
- 加大身份认证的难度,每个微服务需要独立认证
因此,我们需要一个微服务网关,介于客户端与服务器之间的中间层,所有的外部请求都会先经过微服务网关。客户端只需要与网关交互,只知道一个网关地址即可,这样简化了开发还有以下优点:
- 易于监控
- 易于认证
- 减少了客户端与各个微服务之间的交互次数
1.1 服务网关的概念
1.1.1 什么是微服务网关
API 网关是一个服务器,是系统对外的唯一入口。 API 网关封装了系统内部架构,为每个客户端提供一个定制的API 。 API 网关方式的核心要点是,所有的客户端和消费端都通过统一的网关接入微服务,在网关层处理所有的非业务功能。通常,网关也是提供REST/HTTP 的访问 API 。服务端通过 API-GW 注册和管理服务。
1.1.2 作用和应用场景
网关具有的职责,如身份验证、监控、负载均衡、缓存、请求分片与管理、静态响应处理。当然,最主要的职责还是与“ 外界联系 ” 。
1.2 常见的API网关实现方式
Kong
基于 Nginx+Lua 开发,性能高,稳定,有多个可用的插件 ( 限流、鉴权等等 ) 可以开箱即用。问题:只支持Http 协议;二次开发,自由扩展困难;提供管理 API ,缺乏更易用的管控、配置方式。
Zuul
Netflflix 开源,功能丰富,使用 JAVA 开发,易于二次开发;需要运行在 web 容器中,如 Tomcat 。问题:缺乏管控,无法动态配置;依赖组件较多;处理Http 请求依赖的是 Web 容器,性能不如Nginx;
Spring Cloud Gateway
SpringCloud 提供的网关服务
Nginx+lua实现
使用 Nginx 的反向代理和负载均衡可实现对 api 服务器的负载均衡及高可用。问题:自注册的问题和网关本身的扩展性
1.3 基于Nginx的网关实现
1.3.1 Nginx介绍
1.3.2 正向 / 反向代理
( 1 )正向代理
正向代理, " 它代理的是客户端,代客户端发出请求 " ,是一个位于客户端和原始服务器 (origin server) 之间的服务器,为了从原始服务器取得内容,客户端向代理发送一个请求并指定目标( 原始服务器 ) ,然后代理向原始服务器转交请求并将获得的内容返回给客户端。客户端必须要进行一些特别的设置才能使用正向代理。
( 2 )反向代理
多个客户端给服务器发送的请求, Nginx 服务器接收到之后,按照一定的规则分发给了后端的业务处理服务器进行处理了。此时~ 请求的来源也就是客户端是明确的,但是请求具体由哪台服务器处理的并不明确了,Nginx 扮演的就是一个反向代理角色。客户端是无感知代理的存在的,反向代理对外都是透明的,访问者并不知道自己访问的是一个代理。因为客户端不需要任何配置就可以访问。反向代理," 它代理的是服务端,代服务端接收求 " ,主要用于服务器集群分布式部署的情况下,反向代理隐藏了服务器的信息如果只是单纯的需要一个最基础的具备转发功能的网关,那么使用 Ngnix 是一个不错的选择。
1.3.3 准备工作
- 启动 shop_service_order 微服务,单独请求地址:http://127.0.0.1:9001/
- 启动 shop_service_product 微服务,单独请求地址:http://127.0.0.1:9002/
- 安装资料中提供的ngnix。找到ngnix.exe双击运行即可
1.3.4 配置Nginx的请求转发
在\nginx-1.8.0\conf\nginx.conf中配置
location /api-order {
proxy_pass http://127.0.0.1:9001/;
}
location /api-product {
proxy_pass http://127.0.0.1:9002/;
}
1.3.5 访问nginx
localhost:80
2 微服务网关Zuul
2.1 Zuul简介
ZUUL 是 Netflflix 开源的微服务网关,它可以和 Eureka 、 Ribbon 、 Hystrix 等组件配合使用, Zuul 组件的核心是一系列的过滤器,这些过滤器可以完成以下功能:
- 动态路由:动态将请求路由到不同后端集群
- 压力测试:逐渐增加指向集群的流量,以了解性能
- 负载分配:为每一种负载类型分配对应容量,并弃用超出限定值的请求
- 静态响应处理:边缘位置进行响应,避免转发到内部集群
- 身份认证和安全: 识别每一个资源的验证要求,并拒绝那些不符的请求。Spring Cloud对Zuul进行了整合和增强。
Spring Cloud 对 Zuul 进行了整合和增强
2.2 搭建Zuul网关服务器
( 1 )创建工程导入依赖
在 IDEA 中创建 ZUUL 网关工程 shop_zuul_server ,并添加响应依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-zuul</artifactId>
</dependency>
( 2 )编写启动类
创建启动类 ZuulServerApplication。@EnableZuulProxy :通过 @EnableZuulProxy 注解开启 Zuul 网管功能
@SpringBootApplication
//开启zuul网关功能
@EnableZuulProxy
//eureka的服务发现
@EnableDiscoveryClient
public class ZuulServerApplication {
public static void main(String[] args) {
SpringApplication.run(ZuulServerApplication.class,args);
}
}
( 3 )编写配置
创建配置文件 application.yml ,并添加相应配置
server:
port: 8080 #端口
spring:
application:
name: api-zuul-server #服务名称
2.3 Zuul中的路由转发
最直观的理解:“路由 ” 是指根据请求 URL ,将请求分配到对应的处理程序。在微服务体系中, Zuul 负责接收所有的请求。根据不同的URL 匹配规则,将不同的请求转发到不同的微服务处理。
只需要在application.yml 文件中配置路由规则即可:
zuul:
routes:
#已商品微服务
product-service: #路由id,随便写
path: /product-service/** #映射路径 #localhost:8080/product-service/sxxssds
url: http://127.0.0.1:9001 #映射路径对应的实际微服务url地址
- product-service:配置路由id,可以随意取名
- url:映射路径对应的实际url地址
- path:配置映射路径,这里将所有请求前缀为/product-service/的请求,转发到http://127.0.0.1:9002处理
配置好 Zuul 路由之后启动服务,在浏览器中输入 http://localhost:8080/product -service/product/1 ,即可访问到订单微服务。
2.3.1 面向服务的路由
微服务一般是由几十、上百个服务组成,对于一个 URL 请求,最终会确认一个服务实例进行处理。如果对每个服务实例手动指定一个唯一访问地址,然后根据URL 去手动实现请求匹配,这样做显然就不合理。
Zuul 支持与 Eureka 整合开发,根据 ServiceID 自动的从注册中心中获取服务地址并转发请求,这样做的好处不仅可以通过单个端点来访问应用的所有服务,而且在添加或移除服务实例的时候不用修改Zuul 的路由配置。
( 1 )添加 Eureka 客户端依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
( 2 )开启 Eureka 客户端发现功能
@SpringBootApplication
//开启zuul网关功能
@EnableZuulProxy
//eureka的服务发现
@EnableDiscoveryClient
public class ZuulServerApplication {
public static void main(String[] args) {
SpringApplication.run(ZuulServerApplication.class,args);
}
}
( 3 )添加 Eureka 配置,获取服务信息
#配置Eureka
eureka:
client:
service-url:
defaultZone: http://localhost:9000/eureka/
instance:
prefer-ip-address: true #使用ip地址注册
( 4 )修改映射配置,通过服务名称获取
zuul:
routes:
#已商品微服务
product-service: #路由id,随便写
path: /product-service/** #映射路径 #localhost:8080/product-service/sxxssds
serviceId: service-product #配置转发的微服务的服务名称
因为已经有了 Eureka 客户端,我们可以从 Eureka 获取服务的地址信息,因此映射时无需指定 IP 地址,而是通过服务名称来访问,而且Zuul 已经集成了 Ribbon 的负载均衡功能。
2.3.2 简化的路由配置
在刚才的配置中,我们的规则是这样的:
- zuul.routes.<route>.path=/xxx/** : 来指定映射路径。 <route> 是自定义的路由名
- zuul.routes.<route>.serviceId=/product-service :来指定服务名。
而大多数情况下,我们的 <route> 路由名称往往和服务名会写成一样的。因此 Zuul 就提供了一种简化的配置语法: zuul.routes.<serviceId>=<path>
上面的配置可以简化为一条:
zuul:
routes:
#已商品微服务
#如果路由id 和 对应的微服务的serviceId一致的话
service-product: /product-service/**
2.3.3 默认的路由规则
在使用 Zuul 的过程中,上面讲述的规则已经大大的简化了配置项。但是当服务较多时,配置也是比较繁琐的。因此Zuul 就指定了默认的路由规则:
- 默认情况下,一切服务的映射路径就是服务名本身。
例如服务名为: shop - service - product ,则默认的映射路径就是: /shop - service - product/**
zuul:
routes:
#已商品微服务
#如果当前的微服务名称 service-product , 默认的请求映射路径 /service-product/**
# /service-order/
2.3.4 Zuul加入后的架构
2.4 Zuul中的过滤器
通过之前的学习,我们得知 Zuul 它包含了两个核心功能:对请求的 路由 和 过滤 。其中路由功能负责将外部请求转发到具体的微服务实例上,是实现外部访问统一入口的基础;而过滤器功能则负责对请求的处理过程进行干预,是实现请求校验、服务聚合等功能的基础。其实,路由功能在真正运行时,它的路由映射和请求转发同样也由几个不同的过滤器完成的。所以,过滤器可以说是Zuul 实现 API 网关功能最为核心的部件,每一个进入Zuul 的 HTTP 请求都会经过一系列的过滤器处理链得到请求响应并返回给客户端。
2.4.1 ZuulFilter简介
Zuul 中的过滤器跟我们之前使用的 javax.servlet.Filter 不一样, javax.servlet.Filter 只有一种类型,可以通过配置 urlPatterns 来拦截对应的请求。而 Zuul 中的过滤器总共有 4 种类型,且每种类型都有对应的使用场景。
1. PRE :这种过滤器在请求被路由之前调用。我们可利用这种过滤器实现身份验证、在集群中选择请求的微服务、记录调试信息等。
2. ROUTING :这种过滤器将请求路由到微服务。这种过滤器用于构建发送给微服务的请求,并使用Apache HttpClient或 Netfifilx Ribbon 请求微服务。
3. POST :这种过滤器在路由到微服务以后执行。这种过滤器可用来为响应添加标准的 HTTPHeader、收集统计信息和指标、将响应从微服务发送给客户端等。
4. ERROR :在其他阶段发生错误时执行该过滤器。
Zuul 提供了自定义过滤器的功能实现起来也十分简单,只需要编写一个类去实现 zuul 提供的接口
/**
* 自定义的zuul过滤器
* 继承抽象父类
*/
@Component
public class LoginFilter extends ZuulFilter {
/**
* 定义过滤器类型
* pre
* routing
* post
* error
*/
public String filterType() {
return "pre";
}
/**
* 指定过滤器的执行顺序
* 返回值越小,执行顺序越高
*/
public int filterOrder() {
return 1;
}
/**
* 当前过滤器是否生效
* true : 使用此过滤器
* flase : 不使用此过滤器
*/
public boolean shouldFilter() {
return true;
}
/**
* 指定过滤器中的业务逻辑
*/
public Object run() throws ZuulException {
//System.out.println("执行了过滤器");
}
}
ZuulFilter 是过滤器的顶级父类。在这里我们看一下其中定义的 4 个最重要的方法:
- shouldFilter :返回一个 Boolean 值,判断该过滤器是否需要执行。返回true执行,返回false不执行。
- run :过滤器的具体业务逻辑。
- filterOrder :通过返回的int值来定义过滤器的执行顺序,数字越小优先级越高。
- filterType :返回字符串,代表过滤器的类型。包含以下4种:
- pre :请求在被路由之前执行
- routing :在路由请求时调用
- post :在routing和errror过滤器之后调用
- error :处理请求时发生错误调用
2.4.2 生命周期

正常流程:
- 请求到达首先会经过pre类型过滤器,而后到达routing类型,进行路由,请求就到达真正的服务提供者,执行请求,返回结果后,会到达post过滤器。而后返回响应。
异常流程:
- 整个过程中,pre或者routing过滤器出现异常,都会直接进入error过滤器,再error处理完毕后,会将请求交给POST过滤器,最后返回给用户。
- 如果是error过滤器自己出现异常,最终也会进入POST过滤器,而后返回。
- 如果是POST过滤器出现异常,会跳转到error过滤器,但是与pre和routing不同的时,请求不会再到达POST过滤器了。
不同过滤器的场景:
- 请求鉴权:一般放在pre类型,如果发现没有访问权限,直接就拦截了
- 异常处理:一般会在error类型和post类型过滤器中结合来处理。
- 服务调用时长统计:pre和post结合使用。
2.4.3 自定义过滤器
接下来我们来自定义一个过滤器,模拟一个登录的校验。基本逻辑:如果请求中有 access-token 参数,则认为请求有效,放行。

代码实现:
/**
* 自定义的zuul过滤器
* 继承抽象父类
*/
@Component
public class LoginFilter extends ZuulFilter {
/**
* 定义过滤器类型
* pre
* routing
* post
* error
*/
public String filterType() {
return "pre";
}
/**
* 指定过滤器的执行顺序
* 返回值越小,执行顺序越高
*/
public int filterOrder() {
return 1;
}
/**
* 当前过滤器是否生效
* true : 使用此过滤器
* flase : 不使用此过滤器
*/
public boolean shouldFilter() {
return true;
}
/**
* 指定过滤器中的业务逻辑
* 身份认证:
* 1.所有的请求需要携带一个参数 : access-token
* 2.获取request请求
* 3.通过request获取参数access-token
* 4.判断token是否为空
* 4.1 token==null : 身份验证失败
* 4.2 token!=null : 执行后续操作
* 在zuul网关中,通过RequestContext的上下问对象,可以获取对象request对象
*/
public Object run() throws ZuulException {
//System.out.println("执行了过滤器");
//1.获取zuul提供的上下文对象RequestContext
RequestContext ctx = RequestContext.getCurrentContext();
//2.从RequestContext中获取request
HttpServletRequest request = ctx.getRequest();
//3.获取请求参数access-token
String token = request.getParameter("access-token");
//4.判断
if (token ==null) {
//4.1 如果token==null ,拦截请求,返回认证失败
ctx.setSendZuulResponse(false); // 拦截请求
ctx.setResponseStatusCode(HttpStatus.UNAUTHORIZED.value());
}
//4.2 如果token!=null ,继续后续操作
return null;
}
}
RequestContext :用于在过滤器之间传递消息。它的数据保存在每个请求的 ThreadLocal 中。它用于存储请求路由到哪里、错误、HttpServletRequest 、 HttpServletResponse 都存储在RequestContext中。 RequestContext 扩展了 ConcurrentHashMap ,所以,任何数据都可以存储
在上下文中。
2.5 服务网关Zuul的核心源码解析
2.6 Zuul网关存在的问题
在实际使用中我们会发现直接使用 Zuul 会存在诸多问题,包括:
- 性能问题 :Zuul1x版本本质上就是一个同步Servlet,采用多线程阻塞模型进行请求转发。简单讲,每来一个请求,Servlet容器要为该请求分配一个线程专门负责处理这个请求,直到响应返回客户端这个线程才会被释放返回容器线程池。如果后台服务调用比较耗时,那么这个线程就会被阻塞,阻塞期间线程资源被占用,不能干其它事情。我们知道Servlet容器线程池的大小是有限制的,当前端请求量大,而后台慢服务比较多时,很容易耗尽容器线程池内的线程,造成容器无法接受新的请求。
- 不支持任何长连接,如websocket
3 微服务网关GateWay
Zuul 1.x 是一个基于阻塞 IO 的 API Gateway 以及 Servlet ;直到 2018 年 5 月, Zuul 2.x (基于Netty,也是非阻塞的,支持长连接)才发布,但 Spring Cloud 暂时还没有整合计划。 Spring Cloud Gateway 比 Zuul 1.x 系列的性能和功能整体要好。
3.1 Gateway简介
3.1.1 简介
Spring Cloud Gateway 是 Spring 官方基于 Spring 5.0 , Spring Boot 2.0 和 Project Reactor 等技术开发的网关,旨在为微服务架构提供一种简单而有效的统一的 API 路由管理方式,统一访问接口。 Spring Cloud Gateway 作为 Spring Cloud 生态系中的网关,目标是替代 Netflflix ZUUL ,其不仅提供统一的路由方式,并且基于 Filter 链的方式提供了网关基本的功能,例如:安全,监控 / 埋点,和限流等。它是基于Nttey 的响应式开发模式。
3.1.2 核心概念
- 路由(route):路由是网关最基础的部分,路由信息由一个ID、一个目的URL、一组断言工厂和一组Filter组成。如果断言为真,则说明请求URL和配置的路由匹配。
- 断言(predicates) :Java8中的断言函数,Spring Cloud Gateway中的断言函数输入类型是Spring5.0框架中的ServerWebExchange。Spring Cloud Gateway中的断言函数允许开发者去定义匹配来自Http Request中的任何信息,比如请求头和参数等。
- 过滤器(fifilter):一个标准的Spring webFilter,Spring Cloud Gateway中的Filter分为两种类型,分别是Gateway Filter和Global Filter。过滤器Filter可以对请求和响应进行处理。
3.2 入门案例
3.2.1 入门案例
( 1 ) 创建工程导入依赖
在项目中添加新的模块 shop_gateway_server ,并导入依赖。
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
注意SpringCloud Gateway 使用的 web 框架为 webflflux ,和 SpringMVC 不兼容。引入的限流组件是hystrix。 redis 底层不再使用 jedis ,而是 lettuce 。
( 2 ) 配置启动类
@SpringBootApplication
public class GatewayServerApplication {
public static void main(String[] args) {
SpringApplication.run(GatewayServerApplication.class,args);
}
}
( 3 ) 编写配置文件
cloud: #配置SpringCloudGateway的路由
gateway:
routes:
- id: product-service
uri: lb://service-product
predicates:
- Path=/product/**
创建 application.yml 配置文件:
- id:我们自定义的路由 ID,保持唯一
- uri:目标服务地址
- predicates:路由条件,Predicate 接受一个输入参数,返回一个布尔值结果。该接口包含多种默认方法来将 Predicate 组合成其他复杂的逻辑(比如:与,或,非)。
- fifilters:过滤规则,暂时没用。
上面这段配置的意思是,配置了一个 id 为 product-service 的路由规则,当访问网关请求地址以product 开头时,会自动转发到地址: http://127.0.0.1:9002/ 。配置完成启动项目即可在浏览器访问进行测试,当我们访问地址 http://localhost:8080/product/1 时会展示页面展示如下:
3.2.2 路由规则
Spring Cloud Gateway 的功能很强大,前面我们只是使用了 predicates 进行了简单的条件匹配,其实Spring Cloud Gataway 帮我们内置了很多 Predicates 功能。在 Spring Cloud Gateway 中 Spring 利用Predicate 的特性实现了各种路由匹配规则,有通过 Header 、请求参数等不同的条件来进行作为条件匹配到对应的路由。

实例:
3.2.3 动态路由
和 zuul 网关类似,在 SpringCloud GateWay 中也支持动态路由:即自动的从注册中心中获取服务列表并访问。
( 1 )添加注册中心依赖
在工程的 pom 文件中添加注册中心的客户端依赖(这里以 Eureka 为例)
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
( 2 )配置动态路由
修改 application.yml 配置文件,添加 eureka 注册中心的相关配置,并修改访问映射的 URL 为服务名称。
#eureka注册中心
eureka:
client:
service-url:
defaultZone: http://localhost:9000/eureka/
instance:
prefer-ip-address: true #使用ip地址注册
uri : uri 以 lb: // 开头( lb 代表从注册中心获取服务),后面接的就是你需要转发到的服务名称
cloud: #配置SpringCloudGateway的路由
gateway:
routes:
- id: product-service
uri: lb://service-product
predicates:
- Path=/product/**
3.2.4 重写转发路径
在 SpringCloud Gateway 中,路由转发是直接将匹配的路由 path 直接拼接到映射路径( URI )之后,那么在微服务开发中往往没有那么便利。这里就可以通过RewritePath 机制来进行路径重写。
( 1 ) 案例改造
修改 application.yml ,将匹配路径改为 /product - service/**
重新启动网关,我们在浏览器访问 http://127.0.0.1:8080/product-service/product/1 ,会抛出 404 。这是由于路由转发规则默认转发到商品微服务( http://127.0.0.1:9002/product -service/product/1 )路径上,而商品微服务又没有 product - service 对应的映射配置。
( 2 ) 添加 RewritePath 重写转发路径
修改 application.yml ,添加重写规则。
cloud: #配置SpringCloudGateway的路由
gateway:
routes:
- id: order-service
uri: lb://service-order
predicates:
- Path=/order-service/**
filters:
- RewritePath=/order-service/(?<segment>.*), /$\{segment}
通过 RewritePath 配置重写转发的 url ,将 /product-service/(?.*) ,重写为 {segment} ,然后转发到订单微服务。比如在网页上请求 http://localhost:8080/product-service/product ,此时会将请求转发到 http://127.0.0.1:9002/product/1 ( 值得注意的是在 yml 文档中 $ 要写成 $\ )
3.3 过滤器
Spring Cloud Gateway 除了具备请求路由功能之外,也支持对请求的过滤。通过 Zuul 网关类似,也是通过过滤器的形式来实现的。那么接下来我们一起来研究一下Gateway 中的过滤器。
3.3.1 过滤器基础
(1)过滤器的生命周期
Spring Cloud Gateway 的 Filter 的生命周期不像 Zuul 的那么丰富,它只有两个:“pre” 和 “post” 。
- PRE: 这种过滤器在请求被路由之前调用。我们可利用这种过滤器实现身份验证、在集群中选择请求的微服务、记录调试信息等。
- POST:这种过滤器在路由到微服务以后执行。这种过滤器可用来为响应添加标准的 HTTPHeader、收集统计信息和指标、将响应从微服务发送给客户端等。
过滤器类型
Spring Cloud Gateway 的 Filter 从作用范围可分为另外两种 GatewayFilter 与 GlobalFilter 。
- GatewayFilter:应用到单个路由或者一个分组的路由上。
- GlobalFilter:应用到所有的路由上。
3.3.2 局部过滤器
局部过滤器( GatewayFilter ),是针对单个路由的过滤器。可以对访问的 URL 过滤,进行切面处理。在Spring Cloud Gateway中通过 GatewayFilter 的形式内置了很多不同类型的局部过滤器。这里简单将Spring Cloud Gateway内置的所有过滤器工厂整理成了一张表格,虽然不是很详细,但能作为速览使 用。如下:
每个过滤器工厂都对应一个实现类,并且这些类的名称必须以 GatewayFilterFactory 结尾,这是Spring Cloud Gateway的一个约定,例如 AddRequestHeader 对应的实现类为 AddRequestHeaderGatewayFilterFactory 。对于这些过滤器的使用方式可以参考官方文档
3.3.3 全局过滤器
全局过滤器( GlobalFilter )作用于所有路由, Spring Cloud Gateway 定义了 Global Filter 接口,用户可以自定义实现自己的Global Filter 。通过全局过滤器可以实现对权限的统一校验,安全性验证等功能,并且全局过滤器也是程序员使用比较多的过滤器。
Spring Cloud Gateway 内部也是通过一系列的内置全局过滤器对整个路由转发进行处理如下:

3.4 统一鉴权
内置的过滤器已经可以完成大部分的功能,但是对于企业开发的一些业务功能处理,还是需要我们自己编写过滤器来实现的,那么我们一起通过代码的形式自定义一个过滤器,去完成统一的权限校验。
3.4.1 鉴权逻辑
开发中的鉴权逻辑:
- 当客户端第一次请求服务时,服务端对用户进行信息认证(登录)
- 认证通过,将用户信息进行加密形成token,返回给客户端,作为登录凭证
- 以后每次请求,客户端都携带认证的token
- 服务端对token进行解密,判断是否有效。
如上图,对于验证用户是否已经登录鉴权的过程可以在网关层统一检验。检验的标准就是请求中是否携带token 凭证以及 token 的正确性。
3.4.2 代码实现
下面的我们自定义一个 GlobalFilter ,去校验所有请求的请求参数中是否包含 “token” ,如何不包含请求参数“token” 则不转发路由,否则执行正常的逻辑。
/**
* 自定义一个全局过滤器
* 实现 globalfilter , ordered接口
*/
//@Component
public class LoginFilter implements GlobalFilter,Ordered {
/**
* 执行过滤器中的业务逻辑
* 对请求参数中的access-token进行判断
* 如果存在此参数:代表已经认证成功
* 如果不存在此参数 : 认证失败.
* ServerWebExchange : 相当于请求和响应的上下文(zuul中的RequestContext)
*/
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
System.out.println("执行了自定义的全局过滤器");
//1.获取请求参数access-token
String token = exchange.getRequest().getQueryParams().getFirst("access-token");
//2.判断是否存在
if(token == null) {
//3.如果不存在 : 认证失败
System.out.println("没有登录");
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
return exchange.getResponse().setComplete(); //请求结束
}
//4.如果存在,继续执行
return chain.filter(exchange); //继续向下执行
}
/**
* 指定过滤器的执行顺序 , 返回值越小,执行优先级越高
*/
public int getOrder() {
return 0;
}
}
- 自定义全局过滤器需要实现GlobalFilter和Ordered接口。
- 在fifilter方法中完成过滤器的逻辑判断处理
- 在getOrder方法指定此过滤器的优先级,返回值越大级别越低
- ServerWebExchange 就相当于当前请求和响应的上下文,存放着重要的请求-响应属性、请求实
- 例和响应实例等等。一个请求中的request,response都可以通过 ServerWebExchange 获取
- 调用 chain.filter 继续向下游执行
3.5 网关限流
3.5.1 常见的限流算法
( 1 ) 计数器
计数器限流算法是最简单的一种限流实现方式。其本质是通过维护一个单位时间内的计数器,每次请求计数器加1 ,当单位时间内计数器累加到大于设定的阈值,则之后的请求都被拒绝,直到单位时间已经过去,再将计数器重置为零。

( 2 ) 漏桶算法
漏桶算法可以很好地限制容量池的大小,从而防止流量暴增。漏桶可以看作是一个带有常量服务时间的单服务器队列,如果漏桶(包缓存)溢出,那么数据包会被丢弃。 在网络中,漏桶算法可以控制端口的流量输出速率,平滑网络上的突发流量,实现流量整形,从而为网络提供一个稳定的流量。

为了更好的控制流量,漏桶算法需要通过两个变量进行控制:一个是桶的大小,支持流量突发增多时可以存多少的水(burst ),另一个是水桶漏洞的大小( rate )。

( 3 ) 令牌桶算法
令牌桶算法是对漏桶算法的一种改进,桶算法能够限制请求调用的速率,而令牌桶算法能够在限制调用的平均速率的同时还允许一定程度的突发调用。在令牌桶算法中,存在一个桶,用来存放固定数量的令牌。算法中存在一种机制,以一定的速率往桶中放令牌。每次请求调用需要先获取令牌,只有拿到令牌,才有机会继续执行,否则选择选择等待可用的令牌、或者直接拒绝。放令牌这个动作是持续不断的进行,如果桶中令牌数达到上限,就丢弃令牌,所以就存在这种情况,桶中一直有大量的可用令牌,这时进来的请求就可以直接拿到令牌执行,比如设置qps 为 100 ,那么限流器初始化完成一秒后,桶中就已经有100 个令牌了,这时服务还没完全启动好,等启动完成对外提供服务时,该限流器可以抵挡瞬时的100 个请求。所以,只有桶中没有令牌时,请求才会进行等待,最后相当于以一定的速率执行。


3.5.2 基于Filter的限流
SpringCloudGateway 官方就提供了基于令牌桶的限流支持。基于其内置的过滤器工厂RequestRateLimiterGatewayFilterFactory 实现。在过滤器工厂中是通过 Redis 和 lua 脚本结合的方式进行流量控制。
(0)启动redis
解压redis压缩包,先启动redis-server.exe在启动redis.li.exe服务端,最后在服务端输入monitor监控服务端数据。
( 1 ) 环境搭建
导入 redis 的依赖
首先在工程的 pom 文件中引入 gateway 的起步依赖和 redis 的 reactive 依赖,代码如下:
<!--监控依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!--redis的依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis-reactive</artifactId>
</dependency>
准备 redis
( 2 ) 修改 application.yml 配置文件
在 application.yml 配置文件中加入限流的配置,代码如下:
server:
port: 8080 #端口
spring:
application:
name: api-gateway-server #服务名称
redis:
host: localhost
pool: 6379
database: 0
cloud: #配置SpringCloudGateway的路由
gateway:
routes:
- id: product-service
uri: lb://service-product
predicates:
- Path=/product-service/**
filters:
# - name: RequestRateLimiter
# args:
# # 使用SpEL从容器中获取对象 即限流规则
# key-resolver: '#{@userKeyResolver}'
# # 令牌桶每秒填充平均速率
# redis-rate-limiter.replenishRate: 1
# # 令牌桶的上限
# redis-rate-limiter.burstCapacity: 3
- RewritePath=/product-service/(?<segment>.*), /$\{segment}
在 application.yml 中添加了 redis 的信息,并配置了 RequestRateLimiter 的限流过滤器:
- burstCapacity,令牌桶总容量。
- replenishRate,令牌桶每秒填充平均速率。
- key-resolver,用于限流的键的解析器的 Bean 对象的名字。它使用 SpEL 表达式根据#{@beanName}从 Spring 容器中获取 Bean 对象。
( 3 ) 配置 KeyResolver
为了达到不同的限流效果和规则,可以通过实现 KeyResolver 接口,定义不同请求类型的限流键。
@Configuration
public class KeyResolverConfiguration {
/**
* 编写基于请求路径的限流规则
* //abc
* //基于请求ip 127.0.0.1
* //基于参数
*/
//@Bean
public KeyResolver pathKeyResolver() {
//自定义的KeyResolver
return new KeyResolver() {
/**
* ServerWebExchange :
* 上下文参数
*/
public Mono<String> resolve(ServerWebExchange exchange) {
return Mono.just( exchange.getRequest().getPath().toString());
}
};
}
/**
* 基于请求参数的限流
*
* 请求 abc ? userId=1
*/
@Bean
public KeyResolver userKeyResolver() {
return exchange -> Mono.just(
exchange.getRequest().getQueryParams().getFirst("userId")
//exchange.getRequest().getHeaders().getFirst("X-Forwarded-For") 基于请求ip的限流
);
}
}
通过 reids 的 MONITOR 可以监听 redis 的执行过程。这时候 Redis 中会有对应的数据:
大括号中就是我们的限流 Key, 这边是 IP ,本地的就是 localhost
- timestamp:存储的是当前时间的秒数,也就是System.currentTimeMillis() / 1000或者Instant.now().getEpochSecond()
- tokens:存储的是当前这秒钟的对应的可用的令牌数量
Spring Cloud Gateway 目前提供的限流还是相对比较简单的,在实际中我们的限流策略会有很多种情况,比如:
- 对不同接口的限流
- 被限流后的友好提示
3.5.3 基于Sentinel的限流
Sentinel 支持对 Spring Cloud Gateway 、 Zuul 等主流的 API Gateway 进行限流。 从 1.6.0 版本开始, Sentinel 提供了 Spring Cloud Gateway 的适配模块,可以提供两种资源维度的限流:
- route 维度:即在 Spring 配置文件中配置的路由条目,资源名为对应的 routeId
- 自定义 API 维度:用户可以利用 Sentinel 提供的 API 来自定义一些 API 分组
Sentinel 1.6.0 引入了 Sentinel API Gateway Adapter Common 模块,此模块中包含网关限流的规则和自定义 API 的实体和管理逻辑:
- GatewayFlowRule :网关限流规则,针对 API Gateway 的场景定制的限流规则,可以针对不同route 或自定义的 API 分组进行限流,支持针对请求中的参数、Header、来源 IP 等进行定制化的限流。
- ApiDefinition :用户自定义的 API 定义分组,可以看做是一些 URL 匹配的组合。比如我们可以定义一个 API 叫 my_api ,请求 path 模式为 /foo/** 和 /baz/** 的都归到 my_api 这个 API分组下面。限流的时候可以针对这个自定义的 API 分组维度进行限流。
( 1 )环境搭建
导入 Sentinel 的响应依赖
<!--sentinel限流-->
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-spring-cloud-gateway-adapter</artifactId>
<version>1.6.3</version>
</dependency>
( 2 ) 编写配置类
/**
* sentinel限流的配置
*/
//@Configuration
public class GatewayConfiguration {
private final List<ViewResolver> viewResolvers;
private final ServerCodecConfigurer serverCodecConfigurer;
public GatewayConfiguration(ObjectProvider<List<ViewResolver>> viewResolversProvider,
ServerCodecConfigurer serverCodecConfigurer) {
this.viewResolvers = viewResolversProvider.getIfAvailable(Collections::emptyList);
this.serverCodecConfigurer = serverCodecConfigurer;
}
/**
* 配置限流的异常处理器:SentinelGatewayBlockExceptionHandler
*/
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public SentinelGatewayBlockExceptionHandler sentinelGatewayBlockExceptionHandler() {
return new SentinelGatewayBlockExceptionHandler(viewResolvers, serverCodecConfigurer);
}
/**
* 配置限流过滤器
*/
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public GlobalFilter sentinelGatewayFilter() {
return new SentinelGatewayFilter();
}
/**
* 配置初始化的限流参数
* 用于指定资源的限流规则.
* 1.资源名称 (路由id)
* 2.配置统计时间
* 3.配置限流阈值
*/
@PostConstruct
public void initGatewayRules() {
Set<GatewayFlowRule> rules = new HashSet<>();
// rules.add(new GatewayFlowRule("product-service")
// .setCount(1)
// .setIntervalSec(1)
// );
rules.add(new GatewayFlowRule("product_api")
.setCount(1).setIntervalSec(1)
);
GatewayRuleManager.loadRules(rules);
}
}
- 基于Sentinel 的Gateway限流是通过其提供的Filter来完成的,使用时只需注入对应的SentinelGatewayFilter 实例以及SentinelGatewayBlockExceptionHandler 实例即可。
- @PostConstruct定义初始化的加载方法,用于指定资源的限流规则。这里资源的名称为 order-service ,统计时间是1秒内,限流阈值是1。表示每秒只能访问一个请求。
( 3 )网关配置
( 4 )自定义异常提示
当触发限流后页面显示的是 Blocked by Sentinel: FlowException 。为了展示更加友好的限流提示,Sentinel支持自定义异常处理。您可以在 GatewayCallbackManager 注册回调进行定制:setBlockHandler :注册函数用于实现自定义的逻辑处理被限流的请求,对应接口为
BlockRequestHandler 。默认实现为 DefaultBlockRequestHandler ,当被限流时会返回类似于下面的错误信息: Blocked by Sentinel: FlowException 。
/**
* 自定义限流处理器
*/
@PostConstruct
public void initBlockHandlers() {
BlockRequestHandler blockHandler = new BlockRequestHandler() {
public Mono<ServerResponse> handleRequest(ServerWebExchange serverWebExchange, Throwable throwable) {
Map map = new HashMap();
map.put("code",001);
map.put("message","不好意思,限流啦");
return ServerResponse.status(HttpStatus.OK)
.contentType(MediaType.APPLICATION_JSON_UTF8)
.body(BodyInserters.fromObject(map));
}
};
GatewayCallbackManager.setBlockHandler(blockHandler);
}
( 5 ) 参数限流
上面的配置是针对整个路由来限流的,如果我们只想对某个路由的参数做限流,那么可以使用参数限流方式:
* 配置初始化的限流参数
* 用于指定资源的限流规则.
* 1.资源名称 (路由id)
* 2.配置统计时间
* 3.配置限流阈值
*/
@PostConstruct
public void initGatewayRules() {
Set<GatewayFlowRule> rules = new HashSet<>();
rules.add(new GatewayFlowRule("product-service")
.setCount(1)
.setIntervalSec(1)
);
rules.add(new GatewayFlowRule("product_api")
.setCount(1).setIntervalSec(1)
);
GatewayRuleManager.loadRules(rules);
}
通过指定 PARAM_PARSE_STRATEGY_URL_PARAM 表示从 url 中获取参数, setFieldName 指定参数名称
( 6 ) 自定义 API 分组
/**
* 自定义API限流分组
* 1.定义分组
* 2.对小组配置限流规则
*/
@PostConstruct
private void initCustomizedApis() {
Set<ApiDefinition> definitions = new HashSet<>();
ApiDefinition api1 = new ApiDefinition("product_api")
.setPredicateItems(new HashSet<ApiPredicateItem>() {{
add(new ApiPathPredicateItem().setPattern("/product-service/product/**"). //已/product-service/product/开都的所有url
setMatchStrategy(SentinelGatewayConstants.URL_MATCH_STRATEGY_PREFIX));
}});
ApiDefinition api2 = new ApiDefinition("order_api")
.setPredicateItems(new HashSet<ApiPredicateItem>() {{
add(new ApiPathPredicateItem().setPattern("/order-service/order")); //完全匹配/order-service/order 的url
}});
definitions.add(api1);
definitions.add(api2);
GatewayApiDefinitionManager.loadApiDefinitions(definitions);
}
3.6 网关高可用
高可用 HA ( High Availability )是分布式系统架构设计中必须考虑的因素之一,它通常是指,通过设计减少系统不能提供服务的时间。我们都知道,单点是系统高可用的大敌,单点往往是系统高可用最大的风险和敌人,应该尽量在系统设计的过程中避免单点。方法论上,高可用保证的原则是“ 集化 ” ,或者叫“ 冗余 ” :只有一个单点,挂了服务会受影响;如果有冗余备份,挂了还有其他 backup 能够顶上。

我们实际使用 Spring Cloud Gateway 的方式如上图,不同的客户端使用不同的负载将请求分发到后端的 Gateway , Gateway 再通过 HTTP 调用后端服务,最后对外输出。因此为了保证 Gateway 的高可用性,前端可以同时启动多个 Gateway 实例进行负载,在 Gateway 的前端使用 Nginx 或者 F5 进行负载转发以达到高可用性。
( 1 )
准备多个 GateWay 工程
修改 shop_gateway_server 的 application.yml 。添加如下配置
通过不同的 profifiles 配置启动两个网关服务,请求端口分别为 8080 和 8081 。浏览器验证发现效果是一致
的。
( 2 ) 配置 ngnix
找到 ngnix 添加负载均衡配置

在浏览器上通过访问 http://localhost/order-service/order/buy/1 请求的效果和之前是一样的。这次关闭一台网关服务器,还是可以支持部分请求的访问。
3.7 执行流程分析
Spring Cloud Gateway 核心处理流程如上图所示, Gateway 的客户端向 Spring Cloud Gateway 发送请求,请求首先被 HttpWebHandlerAdapter 进行提取组装成网关上下文,然后网关的上下文会传递到 DispatcherHandler 。 DispatcherHandler 是所有请求的分发处理器, DispatcherHandler 主要
负责分发请求对应的处理器。比如请求分发到对应的 RoutePredicateHandlerMapping (路由断言处理映射器)。路由断言处理映射器主要作用用于路由查找,以及找到路由后返回对应的FilterWebHandler 。 FilterWebHandler 主要负责组装 Filter 链并调用 Filter 执行一系列的 Filter 处理,
然后再把请求转到后端对应的代理服务处理,处理完毕之后将 Response 返回到 Gateway 客户端。
4 微服务的链路追踪概述
4.1 微服务架构下的问题
在大型系统的微服务化构建中,一个系统会被拆分成许多模块。这些模块负责不同的功能,组合成系统,最终可以提供丰富的功能。在这种架构中,一次请求往往需要涉及到多个服务。互联网应用构建在不同的软件模块集上,这些软件模块,有可能是由不同的团队开发、可能使用不同的编程语言来实现、有可能布在了几千台服务器,横跨多个不同的数据中心,也就意味着这种架构形式也会存在一些问题:
- 如何快速发现问题?
- 如何判断故障影响范围?
- 如何梳理服务依赖以及依赖的合理性?
- 如何分析链路性能问题以及实时容量规划?
分布式链路追踪( Distributed Tracing ),就是将一次分布式请求还原成调用链路,进行日志记录,性能监控并将 一次分布式请求的调用情况集中展示。比如各个服务节点上的耗时、请求具体到达哪台机器上、每个服务节点的请求状态等等。
目前业界比较流行的链路追踪系统如: Twitter 的 Zipkin ,阿里的 鹰眼 ,美团的 Mtrace ,大众点评的 cat 等,大部分都是基于 google 发表的 Dapper 。 Dapper 阐述了分布式系统,特别是微服务架构中链路追踪的概念、数据表示、埋点、传递、收集、存储与展示等技术细节。
4.2 Sleuth概述
4.2.1 简介
Spring Cloud Sleuth 主要功能就是在分布式系统中提供追踪解决方案,并且兼容支持了 zipkin ,你只需要在pom 文件中引入相应的依赖即可。
4.2.2 相关概念
Spring Cloud Sleuth 为 Spring Cloud 提供了分布式根据的解决方案。它大量借用了 Google Dapper 的设计。先来了解一下Sleuth 中的术语和相关概念。Spring Cloud Sleuth采用的是 Google 的开源项目 Dapper 的专业术语。
- Span:基本工作单元,例如,在一个新建的span中发送一个RPC等同于发送一个回应请求给RPC,span通过一个64位ID唯一标识,trace以另一个64位ID表示,span还有其他数据信息,比如摘要、时间戳事件、关键值注释(tags)、span的ID、以及进度ID(通常是IP地址)span在不断的启动和停止,同时记录了时间信息,当你创建了一个span,你必须在未来的某个时刻停止它。
- Trace:一系列spans组成的一个树状结构,例如,如果你正在跑一个分布式大数据工程,你可能 需要创建一个trace。
- Annotation:用来及时记录一个事件的存在,一些核心annotations用来定义一个请求的开始和结束
- cs - Client Sent -客户端发起一个请求,这个annotion描述了这个span的开始
- sr - Server Received -服务端获得请求并准备开始处理它,如果将其sr减去cs时间戳便可得到网络延迟
- ss - Server Sent -注解表明请求处理的完成(当请求返回客户端),如果ss减去sr时间戳便可得到服务端需要的处理请求时间
- cr - Client Received -表明span的结束,客户端成功接收到服务端的回复,如果cr减去cs时间戳便可得到客户端从服务端获取回复的所有所需时间
4.3 链路追踪Sleuth入门
接下来通过之前的项目案例整合 Sleuth ,完成入门案例的编写
( 1 )配置依赖
修改微服务工程引入 Sleuth 依赖
<!--sleuth链路追踪-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-sleuth</artifactId>
</dependency>
( 2 )修改配置文件
修改 application.yml 添加日志级别
logging:
level:
root: info
org.springframework.web.servlet.DispatcherServlet: DEBUG
org.springframework.cloud.sleuth: DEBUG
每个微服务都需要添加如上的配置。启动微服务,调用之后,我们可以在控制台观察到 sleuth 的日志输出。 其中 ff8ff8b803a3b558 是 TraceId ,后面跟着的是 SpanId ,依次调用有一个全局的 TraceId ,将调用链路串起来。仔细分析每个微服务的日志,不难看出请求的具体过程。
查看日志文件并不是一个很好的方法,当微服务越来越多日志文件也会越来越多,通过 Zipkin 可以将日志聚合,并进行可视化展示和全文检索。
4.4 Zipkin的概述
Zipkin 是 Twitter 的一个开源项目,它基于 Google Dapper 实现,它致力于收集服务的定时数据,以解决微服务架构中的延迟问题,包括数据的收集、存储、查找和展现。 我们可以使用它来收集各个服务器上请求链路的跟踪数据,并通过它提供的 REST API 接口来辅助我们查询跟踪数据以实现对分布式系统的监控程序,从而及时地发现系统中出现的延迟升高问题并找出系统性能瓶颈的根源。除了面向开发的 API 接口之外,它也提供了方便的 UI 组件来帮助我们直观的搜索跟踪信息和分析请求链路明细,比如:可以查询某段时间内各用户请求的处理时间等。 Zipkin 提供了可插拔数据存储方式: In
Memory 、 MySql 、 Cassandra 以及 Elasticsearch 。

上图展示了 Zipkin 的基础架构,它主要由 4 个核心组件构成:
- Collector:收集器组件,它主要用于处理从外部系统发送过来的跟踪信息,将这些信息转换为Zipkin 内部处理的 Span 格式,以支持后续的存储、分析、展示等功能。
- Storage:存储组件,它主要对处理收集器接收到的跟踪信息,默认会将这些信息存储在内存中,我们也可以修改此存储策略,通过使用其他存储组件将跟踪信息存储到数据库中。
- RESTful API:API 组件,它主要用来提供外部访问接口。比如给客户端展示跟踪信息,或是外接系统访问以实现监控等。
- Web UI:UI 组件,基于 API 组件实现的上层应用。通过 UI 组件用户可以方便而有直观地查询和分析跟踪信息。
Zipkin 分为两端,一个是 Zipkin 服务端,一个是 Zipkin 客户端,客户端也就是微服务的应用。客户端会配置服务端的 URL 地址,一旦发生服务间的调用的时候,会被配置在微服务里面的 Sleuth 的监听器监听,并生成相应的 Trace 和 Span 信息发送给服务端。发送的方式主要有两种,一种是 HTTP 报文的方式,还有一种是消息总线的方式如 RabbitMQ 。
不论哪种方式,我们都需要:
- 一个 Eureka 服务注册中心,这里我们就用之前的 eureka 项目来当注册中心。
- 一个 Zipkin 服务端。
- 多个微服务,这些微服务中配置Zipkin 客户端。
4.5 Zipkin Server的部署和配置
( 1 ) Zipkin Server 下载
从 spring boot 2.0 开始,官方就不再支持使用自建 Zipkin Server 的方式进行服务链路追踪,而是直接提供了编译好的 jar 包来给我们使用。可以从官方网站下载 先下载 Zipkin 的 web UI ,我们这里下载的是zipkin- server - 2.12.9 - exec.jar
( 2 ) 启动
在命令行输入 java - jar zipkin - server - 2.12.9 - exec.jar 启动 Zipkin Server
- 默认Zipkin Server的请求端口为 9411
- Zipkin Server的启动参数可以通过官方提供的yml配置文件查找
- 在浏览器输入 http://127.0.0.1:9411即可进入到Zipkin Server的管理后台
4.6 客户端Zipkin+Sleuth整合
通过查看日志分析微服务的调用链路并不是一个很直观的方案,结合 zipkin 可以很直观地显示微服务之
间的调用关系。
( 1 )客户端添加依赖
客户端指的是需要被追踪的微服务
<!--zipkin依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zipkin</artifactId>
</dependency>
( 2 )修改客户端配置文件
#修改zipkin使用rabbitmq采集数据
zipkin:
#base-url: http://127.0.0.1:9411/ #server的请求地址
sender:
#type: web #数据的传输方式 , 已http的形式向server端发送数据
type: rabbit #向rabbitmq中发送消息
sleuth:
sampler:
probability: 1 #采样比
指定了 zipkin server 的地址,下面制定需采样的百分比,默认为 0.1 ,即 10% ,这里配置 1 ,是记录全部的sleuth 信息,是为了收集到更多的数据(仅供测试用)。在分布式系统中,过于频繁的采样会影响系统性能,所以这里配置需要采用一个合适的值。
( 3 )测试
以此启动每个微服务,启动 Zipkin Service 。通过浏览器发送一次微服务请求。打开 Zipkin Service控制台,我们可以根据条件追踪每次请求调用过程
单击该 trace 可以看到请求的细节
4.7 基于消息中间件收集数据
在默认情况下, Zipkin 客户端和 Server 之间是使用 HTTP 请求的方式进行通信(即同步的请求方式),在网络波动,Server 端异常等情况下可能存在信息收集不及时的问题。 Zipkin 支持与 rabbitMQ 整合完成异步消息传输。
加了 MQ 之后,通信过程如下图所示:
4.7.1 RabbitMQ的安装与启动
略
4.7.2 服务端启动
- RABBIT_ADDRESSES : 指定RabbitMQ地址
- RABBIT_USER: 用户名(默认guest)
- RABBIT_PASSWORD : 密码(默认guest)
启动 Zipkin Server 之后,我们打开 RabbitMQ 的控制台可以看到多了一个 Queue

其中 zipkin 就是为我们自动创建的 Queue 队列
4.7.3 客户端配置
( 1 )
配置依赖
导入 spring - rabbit 依赖,是 Spring 提供的对 rabbit 的封装,客户端会根据配置自动的生产消息并发送到目标队列中
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-sleuth-zipkin</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.amqp</groupId>
<artifactId>spring-rabbit</artifactId>
</dependency>
( 2 ) 配置消息中间件 rabbit mq 地址等信息
zipkin:
sender:
#数据的传输方式 , 已http的形式向server端发送数据
type: rabbit #向rabbitmq中发送消息
sleuth:
sampler:
probability: 1 #采样比
rabbitmq:
host: localhost
port: 5672
username: guest
password: guest
listener: # 这里配置了重试策略
direct:
retry:
enabled: true
simple:
retry:
enabled: true
- 修改消息的投递方式,改为rabbit即可。
- 添加rabbitmq的相关配置
( 3 ) 测试
关闭 Zipkin Server ,并随意请求连接。打开 rabbitmq 管理后台可以看到,消息已经推送到 rabbitmq 。当Zipkin Server 启动时,会自动的从 rabbitmq 获取消息并消费,展示追踪数据
可以看到如下效果:
- 请求的耗时时间不会出现突然耗时特长的情况
- 当ZipkinServer不可用时(比如关闭、网络不通等),追踪信息不会丢失,因为这些信息会保存在Rabbitmq服务器上,直到Zipkin服务器可用时,再从Rabbitmq中取出这段时间的信息
4.8 存储跟踪数据
Zipkin Server 默认时间追踪数据信息保存到内存,这种方式不适合生产环境。因为一旦 Service 关闭重启或者服务崩溃,就会导致历史数据消失。Zipkin 支持将追踪数据持久化到 mysql 数据库或者存储到elasticsearch中。这里已 mysql 为例。
4.8.1 准备数据库
可以从官网找到 Zipkin Server 持久 mysql 的数据库脚本。
/*
SQLyog Ultimate v11.33 (64 bit)
MySQL - 5.5.58 : Database - zipkin
*********************************************************************
*/
/*!40101 SET NAMES utf8 */;
/*!40101 SET SQL_MODE=''*/;
/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;
/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;
CREATE DATABASE /*!32312 IF NOT EXISTS*/`zipkin` /*!40100 DEFAULT CHARACTER SET utf8 */;
USE `zipkin`;
/*Table structure for table `zipkin_annotations` */
DROP TABLE IF EXISTS `zipkin_annotations`;
CREATE TABLE `zipkin_annotations` (
`trace_id_high` bigint(20) NOT NULL DEFAULT '0' COMMENT 'If non zero, this means the trace uses 128 bit traceIds instead of 64 bit',
`trace_id` bigint(20) NOT NULL COMMENT 'coincides with zipkin_spans.trace_id',
`span_id` bigint(20) NOT NULL COMMENT 'coincides with zipkin_spans.id',
`a_key` varchar(255) NOT NULL COMMENT 'BinaryAnnotation.key or Annotation.value if type == -1',
`a_value` blob COMMENT 'BinaryAnnotation.value(), which must be smaller than 64KB',
`a_type` int(11) NOT NULL COMMENT 'BinaryAnnotation.type() or -1 if Annotation',
`a_timestamp` bigint(20) DEFAULT NULL COMMENT 'Used to implement TTL; Annotation.timestamp or zipkin_spans.timestamp',
`endpoint_ipv4` int(11) DEFAULT NULL COMMENT 'Null when Binary/Annotation.endpoint is null',
`endpoint_ipv6` binary(16) DEFAULT NULL COMMENT 'Null when Binary/Annotation.endpoint is null, or no IPv6 address',
`endpoint_port` smallint(6) DEFAULT NULL COMMENT 'Null when Binary/Annotation.endpoint is null',
`endpoint_service_name` varchar(255) DEFAULT NULL COMMENT 'Null when Binary/Annotation.endpoint is null',
UNIQUE KEY `trace_id_high` (`trace_id_high`,`trace_id`,`span_id`,`a_key`,`a_timestamp`) COMMENT 'Ignore insert on duplicate',
KEY `trace_id_high_2` (`trace_id_high`,`trace_id`,`span_id`) COMMENT 'for joining with zipkin_spans',
KEY `trace_id_high_3` (`trace_id_high`,`trace_id`) COMMENT 'for getTraces/ByIds',
KEY `endpoint_service_name` (`endpoint_service_name`) COMMENT 'for getTraces and getServiceNames',
KEY `a_type` (`a_type`) COMMENT 'for getTraces',
KEY `a_key` (`a_key`) COMMENT 'for getTraces',
KEY `trace_id` (`trace_id`,`span_id`,`a_key`) COMMENT 'for dependencies job'
) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=COMPRESSED;
/*Data for the table `zipkin_annotations` */
/*Table structure for table `zipkin_dependencies` */
DROP TABLE IF EXISTS `zipkin_dependencies`;
CREATE TABLE `zipkin_dependencies` (
`day` date NOT NULL,
`parent` varchar(255) NOT NULL,
`child` varchar(255) NOT NULL,
`call_count` bigint(20) DEFAULT NULL,
UNIQUE KEY `day` (`day`,`parent`,`child`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=COMPRESSED;
/*Data for the table `zipkin_dependencies` */
/*Table structure for table `zipkin_spans` */
DROP TABLE IF EXISTS `zipkin_spans`;
CREATE TABLE `zipkin_spans` (
`trace_id_high` bigint(20) NOT NULL DEFAULT '0' COMMENT 'If non zero, this means the trace uses 128 bit traceIds instead of 64 bit',
`trace_id` bigint(20) NOT NULL,
`id` bigint(20) NOT NULL,
`name` varchar(255) NOT NULL,
`parent_id` bigint(20) DEFAULT NULL,
`debug` bit(1) DEFAULT NULL,
`start_ts` bigint(20) DEFAULT NULL COMMENT 'Span.timestamp(): epoch micros used for endTs query and to implement TTL',
`duration` bigint(20) DEFAULT NULL COMMENT 'Span.duration(): micros used for minDuration and maxDuration query',
UNIQUE KEY `trace_id_high` (`trace_id_high`,`trace_id`,`id`) COMMENT 'ignore insert on duplicate',
KEY `trace_id_high_2` (`trace_id_high`,`trace_id`,`id`) COMMENT 'for joining with zipkin_annotations',
KEY `trace_id_high_3` (`trace_id_high`,`trace_id`) COMMENT 'for getTracesByIds',
KEY `name` (`name`) COMMENT 'for getTraces and getSpanNames',
KEY `start_ts` (`start_ts`) COMMENT 'for getTraces ordering and range'
) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=COMPRESSED;
/*Data for the table `zipkin_spans` */
/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;
/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;
4.8.2 配置启动服务端
java -jar zipkin-server-2.12.9-exec.jar --STORAGE_TYPE=mysql --MYSQL_HOST=127.0.0.1 --MYSQL_TCP_PORT=3306 --MYSQL_DB=zipkin --MYSQL_USER=root --MYSQL_PASS=root
STORAGE_TYPE : 存储类型
MYSQL_HOST : mysql 主机地址
MYSQL_TCP_PORT : mysql 端口
MYSQL_DB : mysql 数据库名称
MYSQL_USER : mysql 用户名
MYSQL_PASS : mysql 密码
配置好服务端之后,可以在浏览器请求几次。回到数据库查看会发现数据已经持久化到 mysql 中。
本文含有隐藏内容,请 开通VIP 后查看