抱歉,您的浏览器无法访问本站
本页面需要浏览器支持(启用)JavaScript
了解详情 >

Spring Cloud Zuul 服务网关

架构分析

​ 通过之前几篇Spring Cloud中几个核心组件的介绍,我们已经可以构建一个简略的(不够完善)微服务架构了。比如下图所示:

​ 我们使用Spring Cloud Netflix中的Eureka实现了服务注册中心以及服务注册与发现;而服务间通过Ribbon或Feign实现服务的消费以及均衡负载;通过Spring Cloud Config实现了应用多环境的外部化配置以及版本管理。为了使得服务集群更为健壮,使用Hystrix的融断机制来避免在微服务架构中个别服务出现异常时引起的故障蔓延。

​ 在该架构中,我们的服务集群包含:内部服务Service A和Service B,他们都会注册与订阅服务至Eureka Server,而Open Service是一个对外的服务,通过均衡负载公开至服务调用方。本文我们把焦点聚集在对外服务这块,这样的实现是否合理,或者是否有更好的实现方式呢?

不足之处

先来说说这样架构需要做的一些事儿以及存在的不足:

  • 首先,破坏了服务无状态特点。为了保证对外服务的安全性,我们需要实现对服务访问的权限控制,而开放服务的权限控制机制将会贯穿并污染整个开放服务的业务逻辑,这会带来的最直接问题是,破坏了服务集群中REST API无状态的特点。从具体开发和测试的角度来说,在工作中除了要考虑实际的业务逻辑之外,还需要额外可续对接口访问的控制处理。
  • 其次,无法直接复用既有接口。当我们需要对一个即有的集群内访问接口,实现外部服务访问时,我们不得不通过在原有接口上增加校验逻辑,或增加一个代理调用来实现权限控制,无法直接复用原有的接口。
架构存在的问题

让客户端直接与各个微服务通讯,会有以下的问题:

  • 客户端会多次请求不同的微服务,增加了客户端的复杂性。
  • 存在跨域请求,在一定场景下处理相对复杂。
  • 认证复杂,每个服务都需要独立认证。
  • 难以重构,随着项目的迭代,可能需要重新划分微服务。例如,可能将多个服务合并成一个或者将一个服务拆分成多个。如果客户端直接与微服务通讯,那么重构将会很难实施。
  • 某些微服务可能使用了防火墙/浏览器不友好的协议,直接访问会有一定困难。

面对类似上面的问题,我们要如何解决呢?下面进入本文的正题:服务网关!

使用网关的优点

使用网关优点:

  • 易于监控。可在微服务网关收集监控数据并将其推送到外部系统进行分析。

  • 易于认证。可在微服务网关上进行认证。然后再将请求转发到后端的微服务,而无须在每个微服务中进行认证。

  • 减少了客户端与各个微服务之间的交互次数。

​ 为了解决上面这些问题,我们需要将权限控制这样的东西从我们的服务单元中抽离出去,而最适合这些逻辑的地方就是处于对外访问最前端的地方,我们需要一个更强大一些的均衡负载器,它就是本文将来介绍的:服务网关。

​ 服务网关是微服务架构中一个不可或缺的部分。通过服务网关统一向外系统提供REST API的过程中,除了具备服务路由、均衡负载功能之外,它还具备了权限控制等功能。Spring Cloud Netflix中的Zuul就担任了这样的一个角色,为微服务架构提供了前门保护的作用,同时将权限控制这些较重的非业务逻辑内容迁移到服务路由层面,使得服务集群主体能够具备更高的可复用性和可测试性

网关概念

​ 网关是系统的唯一对外的入口,介于客户端和服务器端之间的中间层,处理非业务功能 提供路由请求、鉴权、监控、缓存、限流等功能。它将”1对N”问题转换成了”1对1”问题。

​ 通过服务路由的功能,可以在对外提供服务时,只暴露 网关中配置的调用地址,而调用方就不需要了解后端具体的微服务主机。

为什么要使用微服务网关

​ 微服务场景下,每一个微服务对外暴露了一组细粒度的服务。客户端的请求可能会涉及到一串的服务调用,如果将这些微服务都暴露给客户端,那么客户端需要多次请求不同的微服务才能完成一次业务处理,增加客户端的代码复杂度。另外,对于微服务我们可能还需要服务调用进行统一的认证和校验等等。微服务架构虽然可以将我们的开发单元拆分的更细,降低了开发难度,但是如果不能够有效的处理上面提到的问题,可能会造成微服务架构实施的失败。

问题

​ 不同的微服务一般会有不同的网络地址,而客户端可能需要调用多个服务接口才能完成一个业务需求,若让客户端直接与各个微服务通信,会有以下问题:

  1. 客户端会多次请求不同微服务,增加了客户端复杂性

  2. 存在跨域请求,处理相对复杂

  3. 认证复杂,每个服务都需要独立认证

  4. 难以重构,多个服务可能将会合并成一个或拆分成多个

简化客户端调用复杂度

​ 在微服务架构模式下后端服务的实例数一般是动态的,对于客户端而言很难发现动态改变的服务实例的访问地址信息。因此在基于微服务的项目中为了简化前端的调用逻辑,通常会引入API Gateway作为轻量级网关,同时API Gateway中也会实现相关的认证逻辑从而简化内部服务之间相互调用的复杂度。

数据裁剪以及聚合

​ 通常而言不同的客户端对于显示时对于数据的需求是不一致的,比如手机端或者Web端又或者在低延迟的网络环境或者高延迟的网络环境。

​ 因此为了优化客户端的使用体验,API Gateway可以对通用性的响应数据进行裁剪以适应不同客户端的使用需求。同时还可以将多个API调用逻辑进行聚合,从而减少客户端的请求数,优化客户端用户体验

多渠道支持

​ 当然我们还可以针对不同的渠道和客户端提供不同的API Gateway,对于该模式的使用由另外一个大家熟知的方式叫Backend for front-end, 在Backend for front-end模式当中,我们可以针对不同的客户端分别创建其BFF,进一步了解BFF可以参考这篇文章:Pattern: Backends For Frontends

遗留系统的微服务化改造

​ 对于系统而言进行微服务改造通常是由于原有的系统存在或多或少的问题,比如技术债务,代码质量,可维护性,可扩展性等等。API Gateway的模式同样适用于这一类遗留系统的改造,通过微服务化的改造逐步实现对原有系统中的问题的修复,从而提升对于原有业务响应力的提升。通过引入抽象层,逐步使用新的实现替换旧的实现。

网关的优点

​ 封装了应用程序的内部结构。客户端只需要同网关交互,而不必调用特定的服务。API 网关为每一类客户端提供了特定的 API ,从而减少客户端与应用程序间的交互次数,简化客户端代码的处理。

​ 微服务网关介于服务端与客户端的中间层,所有外部服务请求都会先经过微服务网关客户只能跟微服务网关进行交互,无需调用特定微服务接口,使得开发得到简化

总的理解网关优点

服务网关 = 路由转发 + 过滤器

  1. 路由转发:接收一切外界请求,转发到后端的微服务上去。

  2. 过滤器:在服务网关中可以完成一系列的横切功能,例如权限校验、限流以及监控等,这些都可以通过过滤器完成(其实路由转发也是通过过滤器实现的)。

网关的缺点

​ 增加了一个必须开发、部署和维护的高可用组件。还有一个风险是 API 网关变成了开发瓶颈。为了暴露每个微服务,开发人员必须更新 API 网关。API 网关的更新过程要尽可能地简单,否则为了更新网关,开发人员将不得不排队等待。不过,虽然有这些不足,但对于大多数现实世界的应用程序而言使用 API 网关是合理的。

引入网关的注意点

  • 增加了网关,多了一层转发(原本用户请求直接访问open-service即可),性能会下降一些(但是下降不大,通常,网关机器性能会很好,而且网关与open-service的访问通常是内网访问,速度很快);
  • 网关的单点问题:在整个网络调用过程中,一定会有一个单点,可能是网关、nginx、dns服务器等。防止网关单点,可以在网关层前边再挂一台nginx,nginx的性能极高,基本不会挂,这样之后,网关服务就可以不断的添加机器。但是这样一个请求就转发了两次,所以最好的方式是网关单点服务部署在一台牛逼的机器上(通过压测来估算机器的配置),而且nginx与zuul的性能比较,根据国外的一个哥们儿做的实验来看,其实相差不大,zuul是netflix开源的一个用来做网关的开源框架;
  • 网关要尽量轻。

Zuul简介

​ Zuul参考GOF设计模式中的Facade模式,将细粒度的服务组合起来提供一个粗粒度的服务,所有请求都导入一个统一的入口,那么整个服务只需要暴露一个api,对外屏蔽了服务端的实现细节,也减少了客户端与服务器的网络调用次数。这就是API服务网关(API Gateway)服务。我们可以把API服务网关理解为介于客户端和服务器端的中间层,所有的外部请求都会先经过API服务网关。因此,API服务网关几乎成为实施微服务架构时必须选择的一环。

​ Spring Cloud Netflix的Zuul组件可以做反向代理的功能,通过路由寻址将请求转发到后端的粗粒度服务上,并做一些通用的逻辑处理。

Zuul功能

Zuul包含了对请求的路由和过滤两个最主要的功能:

  其中路由功能负责将外部请求转发到具体的微服务实例上,是实现外部访问统一入口的基础,类似于保安的职能,而过滤器功能则负责对请求的处理过程进行干预,是实现请求校验,服务聚合等功能的基础,Zuul和Eureka进行整合,将Zuul自身注册为Eureka服务治理下的应用,同时从Eureka中获取其他的微服务消息,也即以后访问微服务是通过Zuul跳转后获得,最终Zuul服务还是会注册进Eureka。提供 服务代理 ,路由,过滤三大功能。

  从以上这张架构图中,我们可以看到所有的请求都必须通过API GateWay服务才能到达后面的服务,这就是Zuul所需要承担起来的责任。可见他的存在是很重要的

通过Zuul我们可以完成以下功能:

  • 动态路由

  • 监控与审查

  • 身份认证与安全

  • 压力测试: 逐渐增加某一个服务集群的流量,以了解服务性能;

  • 金丝雀测试

  • 服务迁移

  • 负载剪裁: 为每一个负载类型分配对应的容量,对超过限定值的请求弃用;

  • 静态应答处理

开始使用

添加POM依赖

创建项目并添加POM依赖

1
2
3
4
5
6
7
8
9
10
11
12
<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-config</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-zuul</artifactId>
</dependency>

配置文件配置

bootstrap配置

在bootstrap.properties 加入一下配置启动配置中心

1
2
3
4
5
6
7
8
9
10
11
#注册中心地址
spring.cloud.config.uri=http://localhost:8899
#配置文件分支
spring.cloud.config.label=master
# 配置文件名称
spring.cloud.config.name=application
# 配置文件环境
spring.cloud.config.profile=dev
#如果连接不上获取配置有问题,快速响应失败
spring.cloud.config.fail-fast=true

application配置

在application.properties 加入配置

1
2
3
4
5
6
7
8
9
server.port=9999
spring.application.name=zuul-server
eureka.client.serviceUrl.defaultZone=${eureka.client.serviceUrl.defaultZone}
# 续约更新时间间隔,一般设置比续约到期时间少,该配置表示,每隔30秒就向服务端发送心跳。
eureka.lease-renewal-interval-in-seconds=${eureka.lease-renewal-interval-in-seconds}
# 续约到期时间,可以单独给每个服务设置,如果在90秒(默认)内没有给服务发送心跳,则剔除该服务。
eureka.instance.lease-expiration-duration-in-seconds=${eureka.lease-renewal-interval-in-seconds}
# 每隔30秒就去注册中心拉取注册表信息。
eureka.client.registry-fetch-interval-seconds=${eureka.lease-renewal-interval-in-seconds}

创建启动类

1
2
3
4
5
6
7
8
9
10
@SpringBootApplication
@EnableEurekaClient
//启动ZUUL
@EnableZuulProxy
public class ZuulServerApplication {

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

这里使用@EnableZuulProxy表示开启zuul网关。

@EnableDiscoveryClient,可以发现@EnableEurekaClient注解实现包含了@EnableDiscoveryClient,这里只用来调用eureka服务的话,两个都可以使用,如果要使用其他的,比如consul,那就只能用@EnableDiscoveryClient了。

启动测试

启动各服务

请按照下面的顺序启动各服务器:

  • eureka-server
  • config-server
  • order-server
  • user-server
  • zuul-server

Ok, 服务启动后我们可以在Eureka服务器看到如下界面:

访问测试

访问 http://localhost:9999/user-server/order/version zuul会将请求转发到user-server的/order/version接口

可见,Zuul-Server已经帮我们路由到相应的微服务。

负载均衡测试

前置工作

在user-server 中增加一个接口

1
2
3
4
5
@GetMapping("invokInfo")
public String getInvokInfo() {
String message = "访问端口:" + serverPort + ",版本号:" + orderVersion;
return message;
}
访问测试

接下来我们测试一下负载均衡是否可以正常工作。前面我们已经启动了两个user-server微服务,端口分别为:8081和8082,我们多次在浏览器中输入以下地址: http://localhost:9999/user-server/invokInfo进行请求,我们将会看到以下信息会在屏幕中交替输出:

1
2
访问端口:8082,版本号:1.8.0
访问端口:8081,版本号:1.8.0

可见,负载均衡也是正常工作的。

Hystrix容错与监控测试

之前我们是在hystrix-dashboard项目中集成Hystrix的监控,那么我们启动该服务。然后在Hystrix Dashboard中输入: http://192.168.136.128:9099/hystrix,添加加user-server的监控。

然后进行监控,那么我们将看到如下界面:

这说明,Zuul已经整合了Hystrix。

spring-cloud-starter-zuul本身已经集成了hystrix和ribbon,所以Zuul天生就拥有线程隔离和断路器的自我保护能力,以及对服务调用的客户端负载均衡功能。但是,我们需要注意,当使用path与url的映射关系来配置路由规则时,对于路由转发的请求则不会采用HystrixCommand来包装,所以这类路由请求就没有线程隔离和断路器保护功能,并且也不会有负载均衡的能力。因此,我们在使用Zuul的时候尽量使用path和serviceId的组合进行配置,这样不仅可以保证API网关的健壮和稳定,也能用到Ribbon的客户端负载均衡功能。

路由规则动态更新

Spring Cloud Zuul 作为微服务的网关,请求经过zuul路由到内部的各个service,由于存在着新增/修改/删除服务的路由规则的需求,zuul的路由规则的动态变更功能提供了无须重启zuul网关,即可实时更新

动态刷新

  1. spring boot 集成了spring actuator 提供的 refresh功能后,在congfig-server的git配置仓库中新增一个zuul的路由规则,

  2. post方式刷新refresh端点 curl -X POST http://localhost:9999/actuator/refresh

  3. 再次访问zuul发现路由规则中存在新增的规则

更新流程

从触发refresh操作开始 -> ZuulPropeties中route更新 整个流程如下:

​ Zuul网关的路由规则加载核心类 DiscoveryClientRouteLocatorSimpleRouteLocator,详细可参考 Spring Cloud Zuul源码。这里不做分析。

​ 路由规则的加载机制主要是通过SimpleRouteLocator来加载ZuulPropetties中的路由规则。上图说明了整个从refresh到属性注入ZuulProperties的整个流程

缺点

​ 由于refresh后 首先加载的配置中心的全部zuul的最新K/V数据,然后根据加载的属性K/V注入规则到ZuulPropeties( Bean)中,而ZuulPropeties在refresh之前就存在Bean容器中,

​ 所以新增或者修改Zuul路由规则,refresh后会新增或者覆盖ZuulPropeties中的属性值,而删除操作ZuulPropeties中的路由规则依旧存在,所以删除无效

优点

使用简单,基于配置仓库对路由规则进行版本管理,只需向外暴露refresh端点即可。

代码实现

扩展ZuulPropeties并使用RefreshScope注解,修改配置中心的路由规则后,触发refresh操作路由规则即会发生变更

1
2
3
4
5
6
7
8
9
10
11
@Configuration
public class ZuulConfig {

@Bean
@RefreshScope
@ConfigurationProperties("zuul")
@Primary
public ZuulProperties zuulProperties() {
return new ZuulProperties();
}
}

评论