(git上的源码:https://gitee.com/rain7564/spring_microservices_study/tree/master/fifth-spring-cloud-zuul)
对于服务网关是什么、有什么用?使用API Gateway这篇文章已经讲得很清楚了,这里就不再赘述。当然这只是翻译版,原版在这里:Building Microservices: Using an API Gateway
Spring Cloud和Netflix Zuul
Using an API Gateway一文中提到Netflix API Gateway其实就是Netflix Zuul。Zuul是一个服务网关,Spring Cloud融入Zuul后,使用Spring Cloud提供的注解很容易就能搭建一个API Gateway。Zuul提供了几个功能,包括:
- 只用一个URL就能映射应用中所有服务的路由。但Zuul并不局限于单个URL,也可以定义多个路由入口,做到细粒度的路由映射。
- 自定义过滤器对经过网关的所有请求进行过滤。服务网关的过滤器,可以实现对所有请求进行过滤,而不用在各个服务实现过滤器。
下面进入正题,服务网关的实现。
创建gateway-zuul服务
创建一个Zuul服务端,首先要创建一个Spring Boot项目,然后引入Zuul相关的启动依赖。
创建Spring boot项目并修改pom文件
创建一个空Spring Boot项目后,pom文件修改如下:
4.0.0
cn.study.microservice
gateway-zuul
0.0.1-SNAPSHOT
jar
gateway-zuul
API Gateway
cn.study.microservice
fifth-spring-cloud-zuul
0.0.1-SNAPSHOT
org.springframework.cloud
spring-cloud-starter-zuul
org.springframework.cloud
spring-cloud-starter-eureka
上面的xml文件,只引入了两个启动依赖,第一个是Zuul的启动依赖,该启动依赖除了包含Zuul的核心jar包外,还包括Hystrix、Ribbon、actuator等。第二个是后文介绍将Zuul服务托管在Eureka时会用到。
修改启动类
pom文件修改好之后,需要修改gateway-zuul服务的启动类,如下:
@SpringBootApplication
@EnableZuulProxy
public class GatewayZuulApplication {
public static void main(String[] args) {
SpringApplication.run(GatewayZuulApplication.class, args);
}
}
上面的代码实际上只加了一个注解—@EnableZuulProxy。
若在添加注解@EnableZuulProxy时是手动输入,且IDE开启自动补全功能,那么应该会看到另一个注解——@EnableZuulServer。如果使用该注解,虽然也会创建一个Zuul服务端,但不加载任何反向代理过滤器,不使用Eureka的服务发现来发现其他服务。@EnableZuulServer只在搭建自己的路由服务并不使用Zuul的预构建功能时使用。比如需要使用Zuul来配合其它不是Eureka的服务发现引擎,如Consul。
与Eureka结合使用
Zuul proxy server本来就是被设计用在Spring Cloud项目中。正因为如此,Zuul自动使用Eureka来作为服务发现的依赖,可以通过服务发现其它服务,然后在Zuul内部使用Ribbon实现客户端负载均衡。
添加application.yml文件,然后在该文件中加入如下配置:
server:
port: 5555
eureka:
instance:
preferIpAddress: true
client:
serviceUrl:
defaultZone: http://localhost:8761/eureka/
配置路由规则
Zuul的核心是反向代理。反向代理实际上是一个中间服务器,处于想要访问某个资源的客户端与资源中间。反向代理会捕捉客户端的请求并代表客户端想远程资源发起请求。
在微服务架构中,Zuul(反向代理)从客户端“得到”一个调用,然后转发给下游服务。所以,从客户端服务角度来看,与客户端交互的实际上是Zuul。而Zuul需要与下游的服务进行交互,所以必须知道客户端的请求想要路由到哪个服务。Zuul可以通过几种途径来达到这一目标,包括:
- 通过服务发现自动映射路由
- 使用服务发现手动映射路由
- 使用静态URLs手动映射路由
通过服务发现自动映射路由
Zuul的所有路由映射都可以在zuul服务的application.yml文件中定义。然而,Zuul还可以在零配置的情况下,根据请求url携带的serviceId将请求正确路由到目标服务的某个实例。如果没有指定特定的路由(手动配置,下文会介绍),Zuul默认会使用被调用服务的Eureka service ID并将其映射到其中一个目标服务实例。举个简单的例子,如果你想访问organization-service服务的一个接口,该接口是根据orgId获取对应的organization详细信息,而且希望使用Zuul的自动路由,那么你可以让客户端直接访问Zuul服务的实例,然后使用如下URL:
http://localhost:5555/organizationservice/v1/organizations/e254f8c-c442-4ebe-a82a-e2fc1d1ff78a
其中http://localhost:5555,是Zuul服务实例的访问地址;而organizationservice是organization服务的service ID;剩下的部分则是希望调用的接口。看下面的图可能更容易理解,如下:
Eureka配合Zuul使用的优美之处在于,不仅可以通过单个端点来访问应用的所有服务,而且,在添加或移除服务实例的时候不用修改Zuul的路由配置。另外,也可以添加一个新的服务到Eureka,而Zuul会对访问新添加的服务自动路由,因为Zuul是通过与Eureka通信然后从Eureka获取微服务实例真正物理地址,只要服务托管在Eureka中。
如果想要查看Zuul服务器管理的路由,可以访问Zuul暴露的/routes端点。该端点会返回所有服务的映射列表。启动所有服务,然后访问http://localhost:5555/routes,若跟着本教程走,应该可以看到类似如下图的返回:
如果出现下面的情况:
则需要在application.yml或bootstrap.yml文件中加入如下配置:
management:
security:
enabled: false
观察正常访问http://localhost:5555/routes后的返回json对象,类似"/config-server/**"的key,是Zuul基于Eureka service ID自动为服务创建的服务路由,请求匹配到的服务路由就可以映射得到Eureka service ID,即json对象的value,最后就可以根据这个ID定位具体的服务实例。
使用服务发现手动映射路由
Zuul允许配置更细粒度的路由映射规则,可以明确定义路由映射而不是单纯依赖使用服务的Eureka service ID自动创建。假设想要缩短organizationservice来简化路由,而不是使用Zuul默认提供的/organizationservice/**,那么可以通过在Zuul服务的配置文件中手动定义路由映射关系,例如:
zuul:
routes:
organizationservice: /organization/**
在Zuul服务的application.yml加上上面的配置后,就可以使用类似/organization/v1/organizations/{organization-id}的路由来访问organization服务了。此时,若重启Zuul然后再次访问http://localhost:5555/routes,可以出现如下返回结果:
观察上图,可以看到Zuul为organization服务提供了“入口”,第一个是刚刚手动配置上去的,而第二个则是Zuul默认提供的。
注意:如果使用Zuul基于Eureka service ID自动创建的路由映射,那么当某个服务没有任何一个实例处于运行状态,那么Zuul将不会为该服务创建路由映射。然而,如果手动将路由映射到Eureka service ID,那么,即使没有实例注册到Eureka,Zuul依旧会暴露出手动配置的。当然,若尝试使用不存在任何服务实例的路由,Zuul将直接返回500错误。
忽略某些服务
如果想要将Zuul自动创建的路由映射从路由列表中移除,只留下手动配置的,那么可以在application.yml文件中在加一个额外的Zuul参数——ignored-services即可。示例如下:
zuul:
ignored-services: 'organizationservice'
routes:
organizationservice: /organization/**
这样,就能将Zuul根据Eureka Service ID自动创建的"/organizationservice/**": "organizationservice"从路由映射列表中移除。添加上面的配置然后重启,访问/routes端点,可以看到如下返回:
上图中,Zuul默认给organization服务创建的路由映射已经被忽略了,只留下手动配置的。如果希望Zuul忽略所有自动配置的路由映射,可以使用:
zuul:
ignored-services: '*'
routes:
organizationservice: /organization/**
加上如上配置后重启,然后访问端点/routes,返回如下:
手动配置多个服务
云应用肯定会包含许多微服务,所以一般都有为多个服务手动配置路由映射的需求,这样的需求实现起来也比较简单,如下是对organization服务和license服务的路由映射做手动配置:
zuul:
ignored-services: '*'
routes:
organizationservice: /organization/**
licenseservice: /license/**
访问/routes结果是:
不同API路由共用一样的模型
在不同服务路由的开头附加一个的前缀是很常见的。比如希望在不同服务的路由的开头都加上一个/api的前缀,Zuul也是支持的。可以使用如下配置来实现这一功能:
zuul:
ignored-services: '*'
prefix: /api
routes:
organizationservice: /organization/**
licenseservice: /license/**
可见,zuul.prefix属性可以用来定制所有服务路由的统一前缀。再次访问/routes端点,返回如下:
现在若需要访问organization服务的/v1/organizations/{organization-id}端口,则需要使用:
http://localhost:5555/api/organization/v1/organizations/{organization-id}
使用静态URLs手动映射路由
Zuul也可以用来转发没有注册到Eureka的服务的请求。在某些情况下,需要设置Zuul将部分请求直接路由到定义的静态URL。比如,假设organization服务是使用Python编写的,然后也想让Zuul来做反向代理,那么可以使用如下的配置实现:
zuul:
prefix: /api
routes:
organizationstatic:
path: /organizationstatic/**
url: http://localhost:11000
关闭其他服务,只启动zuul服务和organization服务,启动过程中会报错,这是因为eureka没启动,服务没办法注册到eureka,可以不管它,只要成功启动就行。然后访问zuul的/routes端点,可以看到只有刚刚配置的静态URL,如图:
然后你会发现,访问
http://localhost:5555/api/organizationstatic/v1/organizations/e254f8c-c442-4ebe-a82a-e2fc1d1ff78a/时,可以成功访问,结果如下:
下面来分析一下,之前的配置是什么含义,如下图:
但是,问题又来了,因为这样的配置会绕过eureka,这就导致请求都会指向单个路由,那么当organization服务有多个怎么办?怎么利用Ribbon来实现服务均衡?有两种做法:
第一种:
zuul:
prefix: /api
routes:
organizationstatic:
path: /organizationstatic/**
serviceId: organizationstatic
ribbon:
eureka:
enabled: false
organizationstatic:
ribbon:
listOfServers: http://localhost:11000,http://localhost:11001
第二种:
zuul:
prefix: /api
routes:
organizationstatic:
path: /organizationstatic/**
serviceId: organizationstatic
organizationstatic:
ribbon:
NIWSServerListClassName: com.netflix.loadbalancer.ConfigurationBasedServerList
listOfServers: http://localhost:11000,http://localhost:11001
第一种实现方法需要Ribbon禁用eureka,感觉不太友好。官方文档是这样说的:
我的理解是:因为如果不禁用的话,zuul会以为organizationstatic这个service ID是来自Eureka的,所以就去eureka那边查找对应的服务实例,然而,organizationstatic根本就没注册到eureka,所以就直接报500了。
而第二种实现方法,是告诉zuul在使用Ribbon做负载均衡时,直接在提供的server列表中获取服务实例。观察第二种方法的配置,可以看到添加了.ribbon.NIWSServerListClassName属性。官方文档是这样说的:
官方文档地址:https://github.com/spring-cloud/spring-cloud-netflix/blob/master/docs/src/main/asciidoc/spring-cloud-netflix.adoc
使用上面的配置后,只启动gateway-zuul服务和两个organization服务实例,两个organization服务的端口分别是11000和11001。然后访问http://localhost:5555/api/organizationstatic/v1/organizations/e254f8c-c442-4ebe-a82a-e2fc1d1ff78a/,会发现负载均衡器起作用了。至于如何启动多个实例,请参考另一篇教程服务注册与发现——Netflix Eureka,这里面有提及。
非java服务的处理
之前说到静态映射路由负载均衡的第一种实现,需要zuul服务器的Ribbon禁用eureka,禁用后会有一个问题,就是其他注册到eureka的服务无法通过网关访问,需要手动自己配置.listOfServers属性。
所以对于其它语言编写的服务也想要通过zuul统一管理,有两个方案:第一,使用一个独立的zuul服务专门转发那些其它语言的服务;第二,创建Spring Cloud Sidecar实例(推荐使用这种),Spring Cloud sidecar允许注册其它语言实现的服务到eureka,然后就可以通过zuul代理了。Spring Cloud sidecar这里就不细讲了,实现也比较简单,可参考Polyglot support with Sidecar。
zuul的超时时间
zuul使用Hystrix和Ribbon库来帮助避免一个耗时较长的调用影响到整个网关的性能。默认情况下,当一个调用经过1s后还为处理完成并返回,zuul会终止该调用并统一返回500错误(这个1s的超时时间,其实是Hystrix的默认超时时间)。但是,我们可以在zuul的配置文件中对这个超时时间进行修改。比如,将其修改成7s,可以这样设置:
...
hystrix:
command:
default:
execution:
isolation:
thread:
timeoutInMilliseconds: 7000
假如需要修改特定的服务的超时时间,比如organization服务,则需要将上面的hystrix.command.defualt.换成hystrix.command.organizationservice,即将default换成对应的服务名。
最后,需要修改另一个超时时间属性。当我们覆盖hystrix默认超时时间且新超时时间大于5s,那么当一个调用超过5s时,Ribbon也会认为该调用超时。也就是说,当配置hystrix的超时时间大于5s,那么还需要配置Ribbon的超时时间,配置如下:
ribbon:
ReadTimeout: 7000
如果需要指定某个服务的Ribbon超时时间,则要用(clientname为服务名):
clientname:
ribbon:
ReadTimeout: 7000
动态重新加载路由配置
zuul动态加载路由配置,需要具备Spring Cloud Config基础,若不了解Spring Cloud Config,可参考另一篇教程分布式配置——Spring Cloud Configuration。
动态加载路由在实际应用中是极其有用的,因为可以在变更路由配置后,不用重新编译、重新部署zuul服务。在分布式配置——Spring Cloud Configuration中已经讲过如何将配置文件迁移到config server中,我们也可以将zuul的配置交由config server管理。(注意:本教程不使用git作为config server的配置文件存储库,而是直接使用config server的classpath,这样比较简单,只是每次改完配置文件需要重新启动config server)。
在config server的classpath:config目录下,创建目录gateway-zuul,然后创建gateway-zuul.yml文件,内容如下:
server:
port: 5555
eureka:
instance:
preferIpAddress: true
client:
serviceUrl:
defaultZone: http://localhost:8761/eureka/
接着修改config server服务的application.yml,添加config server扫描路径,如下:
...
spring:
profiles:
active: native
cloud:
config:
server:
native:
search-locations: classpath:config/,classpath:config/licenseservice, classpath:config/gateway-zuul
然后修改zuul服务的pom.xml文件和bootstrap.yml文件,分别为:
...
org.springframework.cloud
spring-cloud-config-client
...
spring:
application:
name: gateway-zuul
profiles:
active: default
cloud:
config:
uri: http://localhost:8888
management:
security:
enabled: false
然后重启config-server服务和gateway-zuul服务,当看到zuul服务的控制台有类似如下输出,才证明成功从config server加载配置文件(不然就要找找是哪里出错了):
访问zuul服务的/routes端点,返回结果如下:
修改config-server服务管理的zuul服务的配置文件,添加如下配置:
zuul:
ignored-services: '*'
prefix: /api
routes:
organizationservice: /organization/**
licenseservice: /license/**
然后使用POST方式访问http://localhost:555/refresh(记得要先重启config-server服务),可以看到返回如下:
证明zuul服务的配置信息中的上面4个配置的值已变更。再次访问zuul服务的/routes,返回结果如下:
证明zuul已经动态加载配置成功了。
有关Spring Cloud Zuul的入门教程就介绍到这里,后续会在进阶教程中继续介绍zuul真正强大的功能——filter。
完!