Spring Cloud Zuul
通过前几章的介绍,我们对于Spring Cloud Netflix下的核心组件已经了解了一大半。这些组件基本涵盖了微服务架构中最基础的几个核心设施,利用这些组件我们已经可以构建起一个简单的微服务架构系统,比如,通过使用Spring Cloud Eureka实现高可用的服务注册中心以及实现微服务的注册与发现;通过Spring Cloud Eureka或Feign实现服务间负载均衡的接口调用;同时,为了使分布式系统更为健壮,对于依赖的服务调用使用Spring Cloud Hystrix来进行包装,实现线程隔离并加入熔断机制,以避免在威武架构中因个别服务出现异常而引起级联故障蔓延。通过上述思路,我们可以设计出类似下图点基础系统架构。
在该架构中,我们的服务集群包含内部服务Service A和Service B,它们都会向Eureka Server集群进行注册与订阅服务,而Open Service是一个对外的RESTful API服务,它通过F5、Nginx等网络设备或工具软件实现对各个微服务等路由与负载均衡,并公开給外部的客户端调用。
在本章中,我们将把视线聚焦在对外服务这块内容,通常也称为边缘服务。首先需要肯定的是,上面等架构实现系统功能是完全没有问题等,但是我们还是可以进一步思考一下,这样等架构是否还有不足的地方会使运维人员或开发人员感到痛苦。
首先,我们从运维人员的角度来看看,他们平时都需要做一些什么工作来支持这样等架构。当客户端应用单击某个功能的路由和负载均衡分配后,被转发到各个不同的服务实例上。而为了让这些设施能够正确路由和分发请求,运维人员需要手工维护这些路由规则与服务实例列表,当有实例增减或是IP地址变动等情况发生的时候,也需要手工地去同步修改这些信息以保持实例信息与中间件配置内容的一致性。再系统规模不大的时候,维护这些信息的工作还不会太过复杂,但是如果当系统规模不断增大,那么这些看似简单的维护任务会变得越来越难,并且出现配置错误的概率也会逐渐增加,那么这些看似简单的维护任务会变得越来越难,并且出现配置错误的概率也会逐渐增加。很显然,这样的做法并不可取,所以我们需要一套机制来有效降低维护路由规则与服务实例列表的难度。
其次,我们再从开发人员的角度来看看,再这样的架构下,会产生一些怎么样的问题呢?大多数情况下,为了保证对外服务的安全性,我们再服务端实现的微服务接口,往往都会有一定的权限校验机制,比如对用户登录状态的校验等;同时为了防止客户端在发起请求时被篡改等安全方面的考虑,还会有一些签名校验等机制存在。这时候,由于使用了微服务架构的理念,我们将原本处于一个应用中等多个模块拆分了多个应用,但是这些应用提供等接口都需要这些校验逻辑,我们不得不在这些应用中都实现这样一套校验逻辑。随着微服务规模的扩大,这些校验逻辑的冗余变得越来越多,突然有一天我们发现这套校验逻辑有个BUG需要修复,或者需要对其做一些扩展和优化,此时我们就不得不去每个应用里修改这些逻辑,而这样等修改不仅会引起开发人员的抱怨,更会加重测试人员的负担。所以,我们也需要一套机制能够很好地解决微服务架构中,对于微服务接口访问时各前置校验的冗余问题。
为了解决上面这些常见的架构问题,API网关的概念应运而生。API网关是一个更为智能的应用服务器,它等定义类似于面向对象设计模式中的Facade模式,它的存在就像是整个微服务架构系统的门面一样,所有的外部客户端访问都需要经过它来进行调度和过滤。它除了要实现请求路由、负载均衡、校验过滤等功能之外,还需要更多能力,比如与服务治理框架的结合、请求转发时的熔断机制、服务的聚合等一系列高级功能。
既然API网关对于微服务架构这么重要,那么在Spring Cloud中是否有相应的解决方案呢?答案是很肯定的,Spring Cloud提供了基于Netflix Zuul实现的API网关组件---Spring Cloud Zuul。那么,它是如何解决上面这两个普通问题的呢?
首先,对于路由规则与服务实例的维护问题。Spring Cloud Zuul通过与Spring Cloud Eureka进行整合,将自身注册为Eureka服务治理下的应用,同时从Eureka中获得了所有其他微服务的实例信息。这样的设计非常巧妙地将服务治理体系中维护的实例信息利用起来,使得将维护服务实例的工作交给了服务治理框架自动完成,不再需要人工介入。而对大部分情况下,这样的默认设置已经可以实现我们大部分的路由需求,除了一些特殊情况(比如兼容一些老的URL)还需要做一些特别的配置。但是相比于之前架构下的运维工作量,通过引入Spring Cloud Zuul实现API网关后,已经能够大大减少了。
其次,对于类似签名校验、登录校验在微服务架构中的冗余问题。理论上来说,这些校验逻辑在本质上与微服务应用自身的业务并没有多大的关系,所以它们完全可以独立成一个单独的服务存在,只是它们被剥离和独立出来之后,并不是給各个微服务调用,而是在API网关服务上进行统一调用来对微服务接口前置过滤,以实现对微服务接口的拦截和校验。Spring Cloud Zuul提供了一套过滤机制,它可以很好地支持这样的任务。开发者可以通过使用Zuul来创建各种校验过滤器,然后指定哪些规则的请求需要执行校验逻辑,只有通过校验的才会被路由到具体的微服务接口,不然就返回错误提示。通过这样的改造,各个业务层的微服务应用就不用需要业务性质的校验逻辑了,这使得我们的微服务应用可以更专注于业务逻辑的开发,同时微服务的自动化测试也变得更容易实现。
微服务架构虽然可以将我们的开发单元拆分得更为细致,有效降低了开发难度,但是它所引出的各种问题如果处理不当会成为实施过程中的不稳定因素,甚至掩盖原本实施微服务带来的优势。所以,在微服务架构的实施方案中,API网关服务的使用几乎成为了必然的选择。
下面我们将详细介绍Spring Cloud Zuul的使用方法、配置属性以及一些不足之处和需要进行的思考。
快速入门
介绍了这么多关于API网关服务的概念和作用,在这一节中,我们不妨用实际多示例来直观地体验一下Spring Cloud Zuul中封装的API网关是如何使用和运作,并应用到微服务架构中去的。
构建网关
首先,在实现各种API网关服务的高级功能之前,我们需要做一些准备工作,比如,构建起最基本的API网关服务,并且搭建几个用于路由和过滤使用的微服务应用等。对于微服务应用,我们可以直接使用之前章节实现的hello-service-provider和kyle-feign-service。虽然之前我们一直将kyle-feign-service视为消费者,但是在Eureka的服务注册与发现体系中,每个服务既是提供者也是消费者,所以kyle-feign-service实质上也是一个服务提供者。之前我们访问http://localhost:8868/client/getHost?name=kyle等一系列接口就是它提供的服务。读者也可以使用自己实现的微服务应用,因为这部分不是本章的重点,任何微服务应用都可以被用来进行后续的试验。这里,我们详细介绍一下API网关服务的构建过程。
▪️创建一个基础的Spring Boot工程,命名为api-gateway-zuul,并在pom.xml中引入spring-cloud-starter-zuul依赖,具体如下:
org.springframework.boot
spring-boot-starter-parent
1.4.5.RELEASE
org.springframework.cloud
spring-cloud-starter-eureka
org.springframework.cloud
spring-cloud-starter-zuul
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-starter-tomcat
org.springframework.boot
spring-boot-starter-undertow
provided
org.springframework.boot
spring-boot-starter-test
test
org.springframework.cloud
spring-cloud-dependencies
Camden.SR7
pom
import
对于spring-cloud-starter-zuul依赖,可以通过查看它的依赖内容了解到:该模块中不仅包含了Netflix Zuul的核心依赖zuul-core,它还包含了下面这些网关服务需要到重要依赖。
- pring-cloud-starter-hystrix:该依赖用来在网关服务中实现对微服务转发时候到保护机制,通过线程隔离和断路器,防止微服务到故障引发API网关资源无法释放,从而影响其他应用到对外服务。
- spring-cloud-starter-ribbon:该依赖用来实现在网关进行路由转发到时候的客户端负载均衡以及请求重试。
- spring-cloud-starter-actuator:该依赖用来提供常规的微服务管理端点。另外,在Spring Cloud Zuul中还特别提供了/routes端点来返回当前到所有路由规则。
▪️创建应用主类,使用@EnableZuulProxy注解开启Zuul的API网关服务功能。
/**
* 反向代理,,eureka客户端 api网关zuul
*
* @version
* @author kyle 2018年8月6日上午11:06:42
* @since 1.8
*/
@EnableEurekaClient
@EnableZuulProxy
@SpringBootApplication
@RibbonClient(value = "hello-service-provider")
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
▪️在application.properties中配置Zuul应用的基础信息,如应用名、服务端口号,具体内容如下:
#spring.application.name=api-gateway-zuul
eureka.instance.appname=api-gateway-zuul
eureka.instance.virtualHostName=api-gateway-zuul
eureka.instance.secureVirtualHostName=api-gateway-zuul
server.port=8869
#将自己注册到eureka注册中心,单节点关闭
eureka.client.registerWithEureka=true
#从注册中心获取注册信息,单节点关闭
eureka.client.fetchRegistry=true
#从注册中心获取注册信息的时间间隔
eureka.client.registryFetchIntervalSeconds=10
#非安全通信端口
#eureka.instance.nonSecurePort=80
#是否启用非安全端口接受请求
#eureka.instance.nonSecurePortEnabled=true
#安全通信端口
#eureka.instance.securePort=443
#是否启用安全端口接受请求
#eureka.instance.securePortEnabled=true
#是否优先使用IP地址作为主机名的标识,默认false
#eureka.instance.preferIpAddress=false
#eureka节点定时续约时间,默认30
eureka.instance.leaseRenewalIntervalInSeconds=15
#eureka节点剔除时间,默认90
eureka.instance.leaseExpirationDurationInSeconds=45
#从注册中心获取注册信息的时间间隔
eureka.client.registryFetchIntervalSeconds=5
eureka.client.eureka-server-connect-timeout-seconds=15
eureka.client.eureka-server-read-timeout-seconds=10
eureka.instance.instance-id=${spring.cloud.client.ipAddress}:ribbon-service-provider-peer:${server.port}
#注册到另外两个节点,实现集群
eureka.client.serviceUrl.defaultZone=http://localhost:8887/eureka/,http://localhost:8888/eureka/,http://localhost:8889/eureka/
logging.level.com.kyle.client.feign.inter.HelloServiceFeign=DEBUG
# 设置IO线程数, 它主要执行非阻塞的任务,它们会负责多个连接, 默认设置每个CPU核心一个线程
server.undertow.io-threads=19
# 阻塞任务线程池, 当执行类似servlet请求阻塞操作, undertow会从这个线程池中取得线程,它的值设置取决于系统的负载
server.undertow.worker-threads=20
# 以下的配置会影响buffer,这些buffer会用于服务器连接的IO操作,有点类似netty的池化内存管理
server.undertow.buffer-size=1024
# 每个区分配的buffer数量 , 所以pool的大小是buffer-size * buffers-per-region
server.undertow.buffers-per-region=2048
# 是否分配的直接内存
server.undertow.direct-buffers=true
#最大分区数量
server.undertow.max-regions=10
#socket-binding="http",保持长连接
server.undertow.always-set-keep-alive=true
#开启hystrix容错,默认是不开启的,目前应用还未加入容错机制
#feign.hystrix.enabled=true
hystrix.command.default.execution.timeout.enabled=true
#设置hystrix超时时间
hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds=60000
#熔断配置
#熔断窗口时间的标本数量
#hystrix.command.default.circuitBreaker.requestVolumeThreshold=20
#熔断时间
#hystrix.command.default.circuitBreaker.sleepWindowInMilliseconds=5000
#容器窗口的错误占比,百分制
#hystrix.command.default.circuitBreaker.errorThresholdPercentage=50
#熔断窗口时间
#hystrix.command.default.metrics.rollingStats.timeInMilliseconds=10000
#hystrix窗口期内监控上报的并发上限
#hystrix.command.default.metrics.rollingPercentile.bucketSize=100
#重试机制开启为true,关闭为false
spring.cloud.loadbalancer.retry.enabled=true
ribbon.NFLoadBalancerRuleClassName=com.netflix.loadbalancer.ZoneAvoidanceRule
#以下配置全局有效
ribbon.eureka.enabled=true
#建立连接超时时间,原1000
ribbon.ConnectTimeout=60000
#请求处理的超时时间,5分钟
ribbon.ReadTimeout=60000
#所有操作都重试
ribbon.OkToRetryOnAllOperations=true
#重试发生,更换节点数最大值
ribbon.MaxAutoRetriesNextServer=10
#单个节点重试最大值
ribbon.MaxAutoRetries=1
#zuul.okhttp.enabled=true
zuul.semaphore.max-semaphores=500
#zuul路由最大连接数
zuul.host.maxTotalConnections=200
#每个路由最大线程数
zuul.host.maxPerRouteConnections=1000
zuul.host.max-per-route-connections=5
zuul.host.socket-timeout-millis=60000
zuul.host.connect-timeout-millis=60000
完成上面的工作后,通过Zuul实现的API网关服务就构建完毕了。
面向服务的路由
很显然,传统路由的配置方式对于我们来说并不友好,它同样需要运维人员花费时间来维护各个路由path与url的关系。配置如下:
zuul.routes.api-a.path=/api-a/**
zuul.routes.api-a.url=http://localhost:8080/
为了解决这个问题,Spring Cloud Zuul实现了与Spring Cloud Eureka的无缝整合,我们可以让路由的path不是映射具体的url,而是让它映射到某个具体的服务,具体的url则交给Eureka的服务发现机制去自动维护,我们称这类路由为面向服务的路由。在Zuul中使用服务路由也同样简单,只需做下面这些配置。
- 为了与Eureka整合,我们需要在pom.xml中引入spring-cloud-starter-eureka依赖,具体如下:
org.springframework.cloud
spring-cloud-starter-eureka
- 在api-gateway-zuul的application.properties配置文件中指定Eureka注册中心的位置,并且配置服务路由。具体如下:
zuul.routes.api-a.path=/api-a/**
zuul.routes.api-a.serviceId=hello-service-provider
zuul.routes.api-b.path=/api-b/**
zuul.routes.api-b.serviceId=feign-service-provider
eureka.client.serviceUrl.defaultZone=http://localhost:8887/eureka/,http://localhost:8888/eureka/,http://localhost:8889/eureka/
针对我们之前准备的两个微服务应用hello-service-provider和feign-service-provider,在上面的配置中分别定义了两个名为api-a和api-b的路由来映射它们。另外,通过指定Eureka Server服务注册中心的位置,除了将自己注册成服务之外,同时也让Zuul能够获取ello-service-provider和feign-service-provider服务的实例清单,以实现path的映射服务,再从服务中挑选实例来进行请求转发单完整路由机制。
在完成了上面单服务路由配置之后,我们可以将eureka-server、hello-service-provider和feign-service-provider以及这里用Spring Cloud Zuul构建的api-gateway-zuul都启动起来。启动完毕,在eureka-server的信息面板中,我们也可以看到,多了一个网关服务API-GATEWAY-ZUUL。
通过上面的搭建工作,我们已经可以通过服务网关来访问hello-service-provider和feign-service-provider这两个服务了。根据配置单映射关系,分别向网关发起下面这些请求。
- http://localhost:8869/api-a/demo/getHost?name=chandler:该url符合/api-a/**规则,由api-a路由请求转发,该路由映射单serviceId为hello-service-provider,所以最终hello请求会被转发到hello-service-provider服务单某个实例上去。
- http://localhost:8869/api-b/feign/getHost?name=kyle:该url符合/api-b/**规则,由api-b路由负责转发,该路由映射单serviceId为feign-service-provider,所以最终/feign/getHost?name=kyle请求会被发送到feign-service-provider服务单某个实例上去。
通过面向服务单路由配置方式,我们不需要再为各个路由维护微服务应用的具体实例的位置,而是通过简单的path与serviceId的映射组合,使得维护工作变得非常简单。这完全归功于Spring Cloud Eureka的服务发现机制,它使得API网关可以自动化完成服务实例清单的维护,完美地解决了路由映射实例单维护问题。
测试结果补充
#api-gateway-zuul控台输出
2018-08-06 14:24:05.250 INFO 2587 --- [ XNIO-2 task-1] o.s.c.n.zuul.web.ZuulHandlerMapping : Mapped URL path [/api-b/**] onto handler of type [class org.springframework.cloud.netflix.zuul.web.ZuulController]
2018-08-06 14:24:05.250 INFO 2587 --- [ XNIO-2 task-1] o.s.c.n.zuul.web.ZuulHandlerMapping : Mapped URL path [/api-a/**] onto handler of type [class org.springframework.cloud.netflix.zuul.web.ZuulController]
2018-08-06 14:24:05.250 INFO 2587 --- [ XNIO-2 task-1] o.s.c.n.zuul.web.ZuulHandlerMapping : Mapped URL path [/hello-service-provider/**] onto handler of type [class org.springframework.cloud.netflix.zuul.web.ZuulController]
2018-08-06 14:24:05.250 INFO 2587 --- [ XNIO-2 task-1] o.s.c.n.zuul.web.ZuulHandlerMapping : Mapped URL path [/eureka-server/**] onto handler of type [class org.springframework.cloud.netflix.zuul.web.ZuulController]
2018-08-06 14:24:05.250 INFO 2587 --- [ XNIO-2 task-1] o.s.c.n.zuul.web.ZuulHandlerMapping : Mapped URL path [/feign-service-provider/**] onto handler of type [class org.springframework.cloud.netflix.zuul.web.ZuulController]
2018-08-06 14:24:05.300 INFO 2587 --- [ XNIO-2 task-1] s.c.a.AnnotationConfigApplicationContext : Refreshing org.springframework.context.annotation.AnnotationConfigApplicationContext@4d7493a3: startup date [Mon Aug 06 14:24:05 CST 2018]; parent: org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext@5629510
2018-08-06 14:24:05.346 INFO 2587 --- [ XNIO-2 task-1] f.a.AutowiredAnnotationBeanPostProcessor : JSR-330 'javax.inject.Inject' annotation found and supported for autowiring
2018-08-06 14:24:05.493 INFO 2587 --- [ XNIO-2 task-1] c.netflix.config.ChainedDynamicProperty : Flipping property: hello-service-provider.ribbon.ActiveConnectionsLimit to use NEXT property: niws.loadbalancer.availabilityFilteringRule.activeConnectionsLimit = 2147483647
2018-08-06 14:24:05.510 INFO 2587 --- [ XNIO-2 task-1] c.n.u.concurrent.ShutdownEnabledTimer : Shutdown hook installed for: NFLoadBalancer-PingTimer-hello-service-provider
2018-08-06 14:24:05.536 INFO 2587 --- [ XNIO-2 task-1] c.netflix.loadbalancer.BaseLoadBalancer : Client:hello-service-provider instantiated a LoadBalancer:DynamicServerListLoadBalancer:{NFLoadBalancer:name=hello-service-provider,current list of Servers=[],Load balancer stats=Zone stats: {},Server stats: []}ServerList:null
2018-08-06 14:24:05.540 INFO 2587 --- [ XNIO-2 task-1] c.n.l.DynamicServerListLoadBalancer : Using serverListUpdater PollingServerListUpdater
2018-08-06 14:24:05.564 INFO 2587 --- [ XNIO-2 task-1] c.netflix.config.ChainedDynamicProperty : Flipping property: hello-service-provider.ribbon.ActiveConnectionsLimit to use NEXT property: niws.loadbalancer.availabilityFilteringRule.activeConnectionsLimit = 2147483647
2018-08-06 14:24:05.566 INFO 2587 --- [ XNIO-2 task-1] c.n.l.DynamicServerListLoadBalancer : DynamicServerListLoadBalancer for client hello-service-provider initialized: DynamicServerListLoadBalancer:{NFLoadBalancer:name=hello-service-provider,current list of Servers=[10.166.37.142:8877],Load balancer stats=Zone stats: {defaultzone=[Zone:defaultzone; Instance count:1; Active connections count: 0; Circuit breaker tripped count: 0; Active connections per server: 0.0;]
},Server stats: [[Server:10.166.37.142:8877; Zone:defaultZone; Total Requests:0; Successive connection failure:0; Total blackout seconds:0; Last connection made:Thu Jan 01 08:00:00 CST 1970; First connection made: Thu Jan 01 08:00:00 CST 1970; Active Connections:0; total failure count in last (1000) msecs:0; average resp time:0.0; 90 percentile resp time:0.0; 95 percentile resp time:0.0; min resp time:0.0; max resp time:0.0; stddev resp time:0.0]
]}ServerList:org.springframework.cloud.netflix.ribbon.eureka.DomainExtractingServerList@3f516e95
2018-08-06 14:24:06.544 INFO 2587 --- [erListUpdater-0] c.netflix.config.ChainedDynamicProperty : Flipping property: hello-service-provider.ribbon.ActiveConnectionsLimit to use NEXT property: niws.loadbalancer.availabilityFilteringRule.activeConnectionsLimit = 2147483647
2018-08-06 14:24:33.652 INFO 2587 --- [ XNIO-2 task-2] s.c.a.AnnotationConfigApplicationContext : Refreshing org.springframework.context.annotation.AnnotationConfigApplicationContext@5e817cb7: startup date [Mon Aug 06 14:24:33 CST 2018]; parent: org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext@5629510
2018-08-06 14:24:33.686 INFO 2587 --- [ XNIO-2 task-2] f.a.AutowiredAnnotationBeanPostProcessor : JSR-330 'javax.inject.Inject' annotation found and supported for autowiring
2018-08-06 14:24:33.741 INFO 2587 --- [ XNIO-2 task-2] c.netflix.config.ChainedDynamicProperty : Flipping property: feign-service-provider.ribbon.ActiveConnectionsLimit to use NEXT property: niws.loadbalancer.availabilityFilteringRule.activeConnectionsLimit = 2147483647
2018-08-06 14:24:33.746 INFO 2587 --- [ XNIO-2 task-2] c.n.u.concurrent.ShutdownEnabledTimer : Shutdown hook installed for: NFLoadBalancer-PingTimer-feign-service-provider
2018-08-06 14:24:33.747 INFO 2587 --- [ XNIO-2 task-2] c.netflix.loadbalancer.BaseLoadBalancer : Client:feign-service-provider instantiated a LoadBalancer:DynamicServerListLoadBalancer:{NFLoadBalancer:name=feign-service-provider,current list of Servers=[],Load balancer stats=Zone stats: {},Server stats: []}ServerList:null
2018-08-06 14:24:33.748 INFO 2587 --- [ XNIO-2 task-2] c.n.l.DynamicServerListLoadBalancer : Using serverListUpdater PollingServerListUpdater
2018-08-06 14:24:33.749 INFO 2587 --- [ XNIO-2 task-2] c.netflix.config.ChainedDynamicProperty : Flipping property: feign-service-provider.ribbon.ActiveConnectionsLimit to use NEXT property: niws.loadbalancer.availabilityFilteringRule.activeConnectionsLimit = 2147483647
2018-08-06 14:24:33.750 INFO 2587 --- [ XNIO-2 task-2] c.n.l.DynamicServerListLoadBalancer : DynamicServerListLoadBalancer for client feign-service-provider initialized: DynamicServerListLoadBalancer:{NFLoadBalancer:name=feign-service-provider,current list of Servers=[10.166.37.142:8868],Load balancer stats=Zone stats: {defaultzone=[Zone:defaultzone; Instance count:1; Active connections count: 0; Circuit breaker tripped count: 0; Active connections per server: 0.0;]
},Server stats: [[Server:10.166.37.142:8868; Zone:defaultZone; Total Requests:0; Successive connection failure:0; Total blackout seconds:0; Last connection made:Thu Jan 01 08:00:00 CST 1970; First connection made: Thu Jan 01 08:00:00 CST 1970; Active Connections:0; total failure count in last (1000) msecs:0; average resp time:0.0; 90 percentile resp time:0.0; 95 percentile resp time:0.0; min resp time:0.0; max resp time:0.0; stddev resp time:0.0]
]}ServerList:org.springframework.cloud.netflix.ribbon.eureka.DomainExtractingServerList@65451b8c
2018-08-06 14:24:34.751 INFO 2587 --- [erListUpdater-0] c.netflix.config.ChainedDynamicProperty : Flipping property: feign-service-provider.ribbon.ActiveConnectionsLimit to use NEXT property: niws.loadbalancer.availabilityFilteringRule.activeConnectionsLimit = 2147483647
请求过滤
在实现了请求路由功能之后,我们的微服务应用提供的接口就可以通过统一的API网关入口被客户端访问到了。但是,每个客户端用户请求微服务的应用提供的接口时,它们的访问权限往往都有一定的限制,系统并不会将所有的微服务接口都对它们开发。然而,目前的服务路由并没有限制权限这样的功能,所有请求都会毫无保留地转发到具体的应用并返回结果,为了实现对客户端请求的安全校验和权限控制,最简单和粗暴的方法就是为每个微服务应用都实现一套用于校验签名和鉴别权限的过滤器或拦截器。不过,这样的做法并不可取,它会增加日后系统的维护难度,因为同一个系统的各种校验逻辑很多情况下都是大致相同或类似的,这样的实现方式使得相似的校验逻辑代码被分散到了各个微服务中去,冗余代码的出现是我们不希望看到的。所以,比较好的做法是将这些校验逻辑剥离出去,构建出一个独立的鉴权服务。在完成剥离之后,有不少开发者会直接再微服务应用中通过调用鉴权服务来实现校验,但是这样的做法仅仅只是解决了权限逻辑的分离,并没有在本质上将这部分不属于冗余的逻辑从原有的微服务应用中拆分出,冗余的拦截器或过滤器依然会存在。
对于这样的问题,更好的做法是通过前置的网关服务来完成这些非业务性质的校验。由于网关服务的假如,外部客户端访问我们的系统已经有了统一入口,既然这些校验与具体业务无关,那何不在请求到达的时候就完成校验和过滤,而不是转发后再过滤而导致更长的请求延迟。同时,通过在网关中完成校验和过滤,微服务应用端就可以去除各种复杂的过滤器和拦截器了,这使得微服务应用接口的开发和测试复杂度得到了相应降低。
为了再API网关中实现对客户端请求的校验,我们将继续介绍Spring Cloud Zuul的另一个核心功能:请求过滤。Zuul允许开发者在API网关上通过定义过滤器来实现对请求的拦截与过滤,实现的方法非常简单,我们之需要继承ZuulFilter抽象类并实现它定义的4个抽象函数就可以完成对请求的拦截和过滤了。
下面的代码定义了一个简单的Zuul过滤器,它实现了在请求被路由之前检查HttpServletRequest中是否有accessToken参数,若有就进行路由,若没有就拒绝访问,返回401 Unauthorized错误。
/**
* 拦截器
*
* @version
* @author kyle 2018年8月6日下午3:23:53
* @since 1.8
*/
public class AccessFilter extends ZuulFilter {
private static Logger logger = LoggerFactory.getLogger(AccessFilter.class);
@Override
public boolean shouldFilter() {
return true;
}
@Override
public Object run() {
RequestContext context = RequestContext.getCurrentContext();
HttpServletRequest request = context.getRequest();
logger.info("send {} request to {}", request.getMethod(), request.getRequestURL().toString());
Object accessToken = request.getParameter("accessToken");
if (accessToken == null) {
logger.warn("access token is empty");
context.setSendZuulResponse(false);
context.setResponseStatusCode(401);
return null;
}
logger.info("access token ok");
return null;
}
@Override
public String filterType() {
return "pre";
}
@Override
public int filterOrder() {
return 0;
}
}
在上面实现都过滤器代码中,我们通过继承ZuulFilter抽象类并重写下面4个方法来实现自定义都过滤器。这4个方法分别定义了如下内容。
- filterType:过滤器都类型,它决定过滤器在请求都哪一个生命周期中执行。这里定义为pre,代表会在请求被路由之前执行。
- filterOrder:过滤器的执行顺序。当请求在一个阶段中存在多个过滤器时,需要根据该方法返回都值来依次执行。
- shouldFilter:判断该过滤器是否需要被执行。这里我们直接返回了true,因此过滤器对所有请求都会生效。实际运用中我们可以利用该函数来指定过滤器的有效范围。
- run:过滤器的具体逻辑。这里我们通过context.setSendZuulResponse(false)令zuul过滤该请求,不对其进行路由,然后也可以进一步优化我们的返回,比如,通过context.setResponseBody(body)对返回的body内容进行编辑等。
在实现了自定义过滤器之后,它并不会直接生效,我们还需要为其创建具体的Bean才能启动该过滤器,比如,在应用启动类中增加如下内容:
/**
* 反向代理,,eureka客户端 api网关zuul
*
* @version
* @author kyle 2018年8月6日上午11:06:42
* @since 1.8
*/
@EnableEurekaClient
@EnableZuulProxy
@SpringBootApplication
@RibbonClient(value = "hello-service-provider")
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
@Bean
public AccessFilter accessFilter() {
return new AccessFilter();
}
}
当然你也可以在AccessFilter使用@Component扫描加载成组件。在对api-gateway-zuul服务完成了上面的改造之后,我们可以重新启动它,并发起下面的请求,对上面定义的过滤器做一个验证。
- http://localhost:8869/api-a/demo/getHost?name=chandler:返回401错误。
- http://localhost:8869/api-a/demo/getHost?name=chandler&accessToken-token:正确路由到hello-service-provider的/demo/getHost接口,并返回结果。
补充测试结果
#api-gateway-zuul控台输出
2018-08-06 16:03:43.618 INFO 3786 --- [ XNIO-2 task-3] com.kyle.api.zuul.filter.AccessFilter : send GET request to http://localhost:8869/api-a/demo/getHost
2018-08-06 16:03:43.619 WARN 3786 --- [ XNIO-2 task-3] com.kyle.api.zuul.filter.AccessFilter : access token is empty
2018-08-06 16:04:05.270 INFO 3786 --- [ XNIO-2 task-4] com.kyle.api.zuul.filter.AccessFilter : send GET request to http://localhost:8869/api-a/demo/getHost
2018-08-06 16:04:05.270 INFO 3786 --- [ XNIO-2 task-4] com.kyle.api.zuul.filter.AccessFilter : access token ok
2018-08-06 16:04:05.271 INFO 3786 --- [ XNIO-2 task-4] s.c.a.AnnotationConfigApplicationContext : Refreshing org.springframework.context.annotation.AnnotationConfigApplicationContext@4f8fd98f: startup date [Mon Aug 06 16:04:05 CST 2018]; parent: org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext@737a135b
2018-08-06 16:04:05.294 INFO 3786 --- [ XNIO-2 task-4] f.a.AutowiredAnnotationBeanPostProcessor : JSR-330 'javax.inject.Inject' annotation found and supported for autowiring
2018-08-06 16:04:05.352 INFO 3786 --- [ XNIO-2 task-4] c.netflix.config.ChainedDynamicProperty : Flipping property: hello-service-provider.ribbon.ActiveConnectionsLimit to use NEXT property: niws.loadbalancer.availabilityFilteringRule.activeConnectionsLimit = 2147483647
2018-08-06 16:04:05.356 INFO 3786 --- [ XNIO-2 task-4] c.n.u.concurrent.ShutdownEnabledTimer : Shutdown hook installed for: NFLoadBalancer-PingTimer-hello-service-provider
2018-08-06 16:04:05.357 INFO 3786 --- [ XNIO-2 task-4] c.netflix.loadbalancer.BaseLoadBalancer : Client:hello-service-provider instantiated a LoadBalancer:DynamicServerListLoadBalancer:{NFLoadBalancer:name=hello-service-provider,current list of Servers=[],Load balancer stats=Zone stats: {},Server stats: []}ServerList:null
2018-08-06 16:04:05.358 INFO 3786 --- [ XNIO-2 task-4] c.n.l.DynamicServerListLoadBalancer : Using serverListUpdater PollingServerListUpdater
2018-08-06 16:04:05.372 INFO 3786 --- [ XNIO-2 task-4] c.netflix.config.ChainedDynamicProperty : Flipping property: hello-service-provider.ribbon.ActiveConnectionsLimit to use NEXT property: niws.loadbalancer.availabilityFilteringRule.activeConnectionsLimit = 2147483647
2018-08-06 16:04:05.373 INFO 3786 --- [ XNIO-2 task-4] c.n.l.DynamicServerListLoadBalancer : DynamicServerListLoadBalancer for client hello-service-provider initialized: DynamicServerListLoadBalancer:{NFLoadBalancer:name=hello-service-provider,current list of Servers=[10.166.37.142:8877],Load balancer stats=Zone stats: {defaultzone=[Zone:defaultzone; Instance count:1; Active connections count: 0; Circuit breaker tripped count: 0; Active connections per server: 0.0;]
},Server stats: [[Server:10.166.37.142:8877; Zone:defaultZone; Total Requests:0; Successive connection failure:0; Total blackout seconds:0; Last connection made:Thu Jan 01 08:00:00 CST 1970; First connection made: Thu Jan 01 08:00:00 CST 1970; Active Connections:0; total failure count in last (1000) msecs:0; average resp time:0.0; 90 percentile resp time:0.0; 95 percentile resp time:0.0; min resp time:0.0; max resp time:0.0; stddev resp time:0.0]
]}ServerList:org.springframework.cloud.netflix.ribbon.eureka.DomainExtractingServerList@18210fda
2018-08-06 16:04:06.364 INFO 3786 --- [erListUpdater-0] c.netflix.config.ChainedDynamicProperty : Flipping property: hello-service-provider.ribbon.ActiveConnectionsLimit to use NEXT property: niws.loadbalancer.availabilityFilteringRule.activeConnectionsLimit = 2147483647
到这里对于API网关服务的快速入门示例就完成了。通过对Spring Cloud Zuul两个核心功能的介绍,相信读者已经能够体会到API网关服务对微服务架构到重要性了,就目前掌握到API网关知识,我们可以将具体原因总结如下:
- 它作为系统到统一入口,屏蔽了系统内部各个微服务的细节。
- 它可以与服务治理框架结合,实现自动化的服务实例维护以及负载均衡陆游转发。
- 它可以实现接口权限校验与微服务业务逻辑到解耦。
- 通过服务网关中到过滤器,在各生命周期中去校验请求到内容,将原本在对外服务层做到校验前移,保证了微服务的无状态性,同时降低了微服务到测试难度,让服务本身集中关注业务逻辑的处理。
实际上,基于Spring Cloud Zuul实现到API网关服务除了上面所示的优点之外,它还有一些更加强大到功能,我们将在后面对其进行更深入的介绍。通过本节的内容,我们只是希望以一个简单到例子带领大家先来简单认识一下API网关服务提供的基础功能以及它在微服务架构中的重要地位。下一篇博客,我将详解网关zuul的技术细节。
如果需要給我修改意见的发送邮箱:[email protected]
本博客的代码示例已上传GitHub:Spring Cloud Netflix组件入门
资料参考:《Spring Cloud 微服务实战》
转发博客,请注明,谢谢。