服务网关和Zuul
为什么要有服务网关:
我们都知道在微服务架构中,系统会被拆分为很多个微服务。那么作为客户端要如何去调用这么多的微服务呢?难道要一个个的去调用吗?很显然这是不太实际的,我们需要有一个统一的接口与这些微服务打交道,这就是我们需要服务网关的原因。
我们已经知道,在微服务架构中,不同的微服务可以有不同的网络地址,各个微服务之间通过互相调用完成用户请求,客户端可能通过调用N个微服务的接口完成一个用户请求。比如:用户查看一个商品的信息,它可能包含商品基本信息、价格信息、评论信息、折扣信息、库存信息等等,而这些信息获取则来源于不同的微服务,诸如产品系统、价格系统、评论系统、促销系统、库存系统等等,那么要完成用户信息查看则需要调用多个微服务,这样会带来几个问题:
- 客户端多次请求不同的微服务,增加客户端代码或配置编写的复杂性
- 认证繁杂,访问每个服务都要进行一次认证
- 每个服务都通过http访问,导致http请求增加,效率不高拖慢系统性能
- 多个服务存在跨域请求问题,处理起来比较复杂
如下图所示:
我们该如何解决这些问题呢?我们可以尝试想一下,不要让前端直接知道后台诸多微服务的存在,我们的系统本身就是从业务领域的层次上进行划分,形成多个微服务,这是后台的处理方式。对于前台而言,后台应该仍然类似于单体应用一样,一次请求即可,于是我们可以在客户端和服务端之间增加一个API网关,所有的外部请求先通过这个微服务网关。它只需跟网关进行交互,而由网关进行各个微服务的调用。
这样的话,我们就可以解决上面提到的问题,同时开发就可以得到相应的简化,还可以有如下优点:
- 减少客户端与微服务之间的调用次数,提高效率
- 便于监控,可在网关中监控数据,可以做统一切面任务处理
- 便于认证,只需要在网关进行认证即可,无需每个微服务都进行认证
- 降低客户端调用服务端的复杂度
这里可以联想到一个概念,面向对象设计中的门面模式,即对客户端隐藏细节,API网关也是类似的东西,只不过叫法不同而已。它是系统的入口,封装了应用程序的内部结构,为客户端提供统一服务,一些与业务本身功能无关的公共逻辑可以在这里实现,诸如认证、鉴权、监控、缓存、负载均衡、流量管控、路由转发等等。示意图:
总结一下,服务网关大概就是四个功能:统一接入、流量管控、协议适配、安全维护。而在目前的网关解决方案里,有Nginx+ Lua、Kong、Tyk以及Spring Cloud Zuul等等。这里以Zuul为例进行说明,它是Netflix公司开源的一个API网关组件,Spring Cloud对其进行二次封装做到开箱即用。同时,Zuul还可以与Spring Cloud中的Eureka、Ribbon、Hystrix等组件配合使用。
可以说,Zuul实现了两个功能,路由转发和过滤器:
- 路由转发:接受请求,转发到后端服务
- 过滤器:提供一系列过滤器完成权限、日志、限流等切面任务。
- 可以说路由+过滤器=Zuul
服务网关的要素:
- 网关作为唯一的入口,所以稳定性和高可用是跑不了了
- 以及具备良好的并发性能
- 安全性,确保服务不被恶意访问
- 扩展性,网关容易成为吞吐量的瓶颈,所以需要便于扩展
Zuul的四种过滤器API:
- 前置(Pre)
- 路由(Route)
- 后置(Post)
- 错误(Error)
zuul前后置过滤器的典型应用场景:
- 前置(Pre)
- 限流
- 鉴权
- 参数校验调整
- 后置(Post)
- 统计
- 日志
Zuul的核心是一系列过滤器,开发者通过实现过滤器接口,可以做大量切面任务,即AOP思想的应用。Zuul的过滤器之间没有直接的相互通信,而是通过本地ThreadLocal变量进行数据传递的。Zuul架构图:
在Zuul里,一个请求的生命周期:
Zuul:路由转发,排除和自定义
本小节我们来学习如何使用服务网关,也就是Spring Cloud Zuul这个组件,首先新建一个项目,选择如下模块:
pom.xml配置的依赖如下:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.4.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
<spring-cloud.version>Finchley.SR1</spring-cloud.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-config</artifactId>
</dependency>
<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-zuul</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<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>
项目创建好后,将application.properties改为bootstrap.yml,编辑内容如下:
spring:
application:
name: api-gateway
cloud:
config:
discovery:
enabled: true
service-id: CONFIG
profile: dev
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka/
注:我这里使用了配置中心,若对此不熟悉的话,可以参考我另一篇文章:Spring Cloud Config - 统一配置中心
在启动类中,加上@EnableZuulProxy
注解,代码如下:
package org.zero.springcloud.apigateway;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.cloud.netflix.zuul.EnableZuulProxy;
@SpringBootApplication
@EnableZuulProxy
public class ApiGatewayApplication {
public static void main(String[] args) {
SpringApplication.run(ApiGatewayApplication.class, args);
}
}
完成以上配置后启动这个项目,我这里项目启动是正常的。然后我们来通过这个网关访问一下商品服务中获取商品列表的接口。如下:
访问地址说明:
- 该zuul项目跑在8951端口上
- 第一个
/product
是需要访问的服务的名称 - 后面跟的
/buyer/product/list
是商品服务中获取商品列表的接口地址
只要是在eureka上注册的服务都能够通过zuul进行转发,例如我通过zuul来访问config的配置文件:
如上,可以看到,报错了,网关超时。这是因为默认情况下,zuul的熔断机制超时时间是2秒,当一个服务响应的时间较长就会报网关超时错误。
我们在配置文件中,加上如下超时时间的配置即可:
ribbon.ReadTimeout, ribbon.SocketTimeout这两个就是ribbon超时时间设置,当在yml写时,应该是没有提示的,给人的感觉好像是不是这么配的一样,其实不用管它,直接配上就生效了。
还有zuul.host.connect-timeout-millis, zuul.host.socket-timeout-millis这两个配置,这两个和上面的ribbon都是配超时的。区别在于,如果路由方式是serviceId的方式,那么ribbon的生效,如果是url的方式,则zuul.host开头的生效。(此处重要!使用serviceId路由和url路由是不一样的超时策略)
如果你在zuul配置了熔断fallback的话,熔断超时也要配置,即hystrix那段配置。不然如果你配置的ribbon超时时间大于熔断的超时,那么会先走熔断,相当于你配的ribbon超时就不生效了。
现在重启项目,再次访问之前的地址,就不会出现网关超时的错误了:
之前我们访问的都是GET类型的接口,我们来看看POST类型的是否能够正常访问。如下:
每次请求某个服务的接口,都需要带上这个服务的名称。有没有办法可以自定义这个规则呢?答案是有的,在配置文件中,增加路由的自定义配置:
zuul:
routes:
myProduct:
path: /myProduct/**
serviceId: product
说明:
- myProduct 自定义的前缀
- path 匹配的地址
- product 路由到哪个服务
重启项目,测试如下:
在项目启动的时候,我们也可以在控制台中查看到zuul所有的路由规则:
如果我们有些服务的接口不希望对外暴露,只希望在服务间调用,那么就可以在配置文件中,增加路由排除的配置。例如我不希望listForOrder
被外部访问,则在配置文件中,增加如下配置即可:
zuul:
...
ignored-patterns:
- /myProduct/buyer/product/listForOrder
- /product/buyer/product/listForOrder
重启项目,这时访问就会报404了。如下:
还可以使用通配符进行匹配。如下示例:
zuul:
...
ignored-patterns:
- /**/buyer/product/listForOrder
Zuul:Cookie和动态路由
我们在web开发中,经常会利用到cookie来保存用户的登录标识。但我们使用了zuul组件后,默认情况下,cookie是无法直接传递给服务的,因为cookie默认被列为敏感的headers。所以我们需要在配置文件中,将sensitiveHeaders的值置空。如下:
zuul:
...
routes:
myProduct:
path: /myProduct/**
serviceId: product
sensitiveHeaders: # 置空该属性的值即可
我们每次配置路由信息都需要重启项目,显得很麻烦,在线上环境也不能这样随便重启项目。所以我们得实现动态路由的功能,实现动态路由其实就利用一下我们之前实现的动态刷新配置文件的功能即可。首先把Zuul路由相关的配置剪切到git上,如下:
注:我这里使用了配置中心,若对此不熟悉的话,可以参考我另一篇文章:Spring Cloud Config - 统一配置中心
在pom.xml文件中,增加如下依赖项:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bus-amqp</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-config-client</artifactId>
</dependency>
然后在bootstrap.yml中,增加rabbitmq的配置。如下:
spring:
application:
name: api-gateway
cloud:
config:
discovery:
enabled: true
service-id: CONFIG
profile: dev
rabbitmq:
host: 127.0.0.1
port: 5672
username: admin
password: admin
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka/
最后在项目中创建一个config包,在该包中创建一个ZuulConfig配置类,用于加载配置文件中的配置。代码如下:
package org.zero.springcloud.apigateway.config;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.cloud.netflix.zuul.filters.ZuulProperties;
import org.springframework.stereotype.Component;
/**
* @program: api-gateway
* @description: 网关路由配置类
* @author: 01
* @create: 2018-08-25 15:51
**/
@Component
public class ZuulConfig {
@RefreshScope
@ConfigurationProperties("zuul")
public ZuulProperties zuulProperties(){
return new ZuulProperties();
}
}
完成以上配置后,重启项目,即可实现动态路由了,例如我现在把myProduct改成yourProduct,如下:
此时无需重启项目,访问新的地址即可。如下:
Zuul的高可用
- 因为Zuul也属于一个微服务,所以我们将多个Zuul节点注册到Eureka Server即可实现Zuul的高可用性
- 将Nginx和Zuul “混搭”,利用nginx做负载均衡,转发到多个Zuul上
原文地址:http://blog.51cto.com/zero01/2173327