Spring Cloud第十四篇 | Api网关Zuul
本文是Spring Cloud专栏的第十四篇文章,了解前十三篇文章内容有助于更好的理解本文:
一、网关分类
开放Api
开放api(openApi) 企业需要将自身数据、能力等作为开发平台向外开放,通常会以rest的方式向外提供,最好的例子就是淘宝开放平台、腾讯公司的QQ开发平台、微信开放平台。 Open API开放平台必然涉及到客户应用的接入、API权限的管理、调用次数管理等,必然会有一个统一的入口进行管理,这正是API网关可以发挥作用的时候。
微服务网关
微服务的概念最早在2012年提出,在Martin Fowler的大力推广下,微服务在2014年后得到了大力发展。 在微服务架构中,有一个组件可以说是必不可少的,那就是微服务网关,微服务网关处理了负载均衡,缓存,路由,访问控制,服务代理,监控,日志等。API网关在微服务架构中正是以微服务网关的身份存在。
API服务管理平台
上述的微服务架构对企业来说有可能实施上是困难的,企业有很多遗留系统,要全部抽取为微服务器改动太大,对企业来说成本太高。但是由于不同系统间存在大量的API服务互相调用,因此需要对系统间服务调用进行管理,清晰地看到各系统调用关系,对系统间调用进行监控等。 API网关可以解决这些问题,我们可以认为如果没有大规模的实施微服务架构,那么对企业来说微服务网关就是企业的API服务管理平台。
二、网关设计
开放API接口
1、对于OpenAPI使用的API网关来说,一般合作伙伴要以应用的形式接入到OpenAPI平台,合作伙伴需要到 OpenAPI平台申请应用。因此在OpenAPI网关之外,需要有一个面向合作伙伴的使用的平台用于合作伙伴,这就要求OpenAPI网关需要提供API给这个用户平台进行访问。如下架构:
当然如果是在简单的场景下,可能并不需要提供一个面向合作伙伴的门户,只需要由公司的运营人员直接添加合作伙伴应用id/密钥等,这种情况下也就不需要合作伙伴门户子系统。
内网API接口
2、对于内网的API网关,在起到的作用上来说可以认为是微服务网关,也可以认为是内网的API服务治理平台。当企业将所有的应用使用微服务的架构管理起来,那么API网关就起到了微服务网关的作用。而当企业只是将系统与系统之间的调用使用rest api的方式进行访问时使用API网关对调用进行管理,那么API网关起到的就是API服务治理的作用。架构参考如下:
3、对于公司内部公网应用(如APP、公司的网站),如果管理上比较细致,在架构上是可能由独立的API网关来处理这部分内部公网应用,如果想比较简单的处理,也可以是使用面向合作伙伴的API网关。如果使用独立的API网关,有以下的好处:
面向合作伙伴和面向公司主体业务的优先级不一样,不同的API网关可以做到业务影响的隔离。
内部API使用的管理流程和面向合作伙伴的管理流程可能不一样。
内部的API在功能扩展等方面的需求一般会大于OpenAPI对于功能的要求。
基于以上的分析,如果公司有能力,那么还是建议分开使用合作伙伴OPEN API网关和内部公网应用网关。
三、网关框架
-
Tyk:Tyk是一个开放源码的API网关,它是快速、可扩展和现代的。Tyk提供了一个API管理平台,其中包括API网关、API分析、开发人员门户和API管理面板。Try 是一个基于Go实现的网关服务。https://tyk.io
-
Kong:Kong是一个可扩展的开放源码API Layer(也称为API网关或API中间件)。Kong 在任何RESTful API的前面运行,通过插件扩展,它提供了超越核心平台的额外功能和服务,是基于Nginx+Lua进行二次开发的方案。https://konghq.com
-
Orange:Orange和Kong类似也是基于OpenResty的一个API网关程序,是由国人开发的。 http://orange.sumory.com
-
Netflix Zuul:Zuul是Netflix公司的开源项目,提供动态路由、监视、弹性、安全性等功能的边缘服务。Zuul是Netflix出品的一个基于JVM路由和服务端的负载均衡器,Spring Cloud在Netflix项目中也已经集成了Zuul。https://github.com/Netflix/zuul
-
GateWay:GateWay是Spring Cloud的一个子项目,构建于Spring5+,基于Spring Boot 2.x 响应式的、非阻塞式的 API。https://spring.io/projects/spring-cloud-gateway
四、网关作用
网关的作用,可以实现负载均衡、路由转发、日志、权限控制、监控等。
五、网关与过滤器区别
网关是拦截所有服务器请求进行控制
过滤器拦截某单个服务器请求进行控制
六、Nginx与Zuul区别
Nginx是采用服务器负载均衡进行转发
Zuul依赖Ribbon和Eureka实现本地负载均衡转发
相对来说Nginx功能比Zuul功能更加强大,能够整合其他语言比如Lua脚本实现强大的功能,同时Nginx可以更好的抗高并发,Zuul网关适用于请求过滤和拦截等。
七、网关
Zuul是Spring Cloud推荐的一个组件:https://github.com/Netflix/zuul
1、使用Zuul实现反向代理
1-1、在springcloud-zuul模块中添加依赖
<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>
1-2、application.yml配置文件内容如下:
spring:
application:
name: springcloud-zuul
server:
port: 9999
eureka:
client:
service-url:
defaultZone: http://localhost:8700/eureka
#客户端每隔30秒从Eureka服务上更新一次服务信息
registry-fetch-interval-seconds: 30
#需要将我的服务注册到eureka上
register-with-eureka: true
#需要检索服务
fetch-registry: true
#心跳检测检测与续约时间
instance:
#告诉服务端,如果我10s之内没有给你发心跳,就代表我故障了,将我剔除掉,默认90s
#Eureka服务端在收到最后一次心跳之后等待的时间上限,单位为秒,超过则剔除(客户端告诉服务端按照此规则等待自己)
lease-expiration-duration-in-seconds: 10
#每隔2s向服务端发送一次心跳,证明自已依然活着,默认30s
#Eureka客户端向服务端发送心跳的时间间隔,单位为秒(客户端告诉服务端自己会按照该规则)
lease-renewal-interval-in-seconds: 2
#以/api-a/ 开头的请求都转发给springcloud-service-consumer服务
#以/api-b/开头的请求都转发给springcloud-service-feign服务
zuul:
routes:
api-a:
path: /api-a/**
serviceId: springcloud-service-consumer
api-b:
path: /api-b/**
serviceId: springcloud-service-feign
1-3、在主类上添加注解
@EnableZuulProxy //开启Zuul的支持 @EnableEurekaClient //开启Eureka客户端支持
完成上面的操作之后,我们可以启动相应的服务,启动相应服务如下:
然后访问springcloud-service-consumer服务中的接口:http://localhost:9999/api-a/consumer/hello,同理springcloud-service-feign的服务中接口也是这样 http://localhost:9999/api-b/feign/hello路径中的api-a,和api-b分别被路由到响应的服务上去,你也可以配置忽略api-a,api-b等等其他配置
2、Zuul对Ribbon和Hystrix的支持
从依赖上可以看出来,Zuul自身会依赖Ribbon和Hystrix的依赖,所以Zuul本身就拥有线程隔离和断路器的自我保护功能,以及对服务调用的客户端负载均衡功能,但是仅限于我们path和serviceId的组合使用
zuul.routes.<route>.path
zuul.routes.<route>.serviceId
不支持path和url的组合使用
zuul.routes.<route>.path
zuul.routes.<route>.url
3、使用Zuul过滤器
微服务数量多的情况下,我们为每个服务都加上安全校验和权限控制,是非常麻烦的,这样的做法并不可取,它会增加后期系统的维护难度,因为每一个系统中的各种校验逻辑很多情况下大致相同或者类似,然后这些非业务的逻辑代码分散到各个服务中,产生的冗余代码是我们不想看到的,所以通常的做法是通过网关服务来完成这些非业务性质的校验。
3-1、Filter的生命周期
Filter的生命周期有4个,分别是 “PRE”、“ROUTING”、“POST” 和“ERROR”,整个生命周期可以用下图来表示
Zuul大部分功能都是通过过滤器来实现的,这些过滤器类型对应于请求的典型生命周期。
-
PRE:这种过滤器在请求被路由之前调用。我们可利用这种过滤器实现身份验证、在集群中选择请求的微服务、记录调试信息等。
-
ROUTING:这种过滤器将请求路由到微服务。这种过滤器用于构建发送给微服务的请求,并使用 Apache HttpClient 或 Netfilx Ribbon 请求微服务。
-
POST:这种过滤器在路由到微服务以后执行。这种过滤器可用来为响应添加标准的 HTTP Header、收集统计信息和指标、将响应从微服务发送给客户端等。
-
ERROR:在其他阶段发生错误时执行该过滤器。
在Zuul网关中,我们需要自定义一个类来继承ZuulFilter抽象类并实现4个相应的抽象方法即可。
简单实例,验证请求有没有userToken参数:
@Component public class TokenFilter extends ZuulFilter { // 过滤器类型 pre 表示在 请求之前进行拦截 @Override public String filterType() { return "pre"; } // 过滤器的执行顺序。当请求在一个阶段的时候存在多个多个过滤器时,需要根据该方法的返回值依次执行 @Override public int filterOrder() { return 0; } // 判断过滤器是否生效 @Override public boolean shouldFilter() { return true; } @Override public Object run() throws ZuulException { // 获取上下文 RequestContext currentContext = RequestContext.getCurrentContext(); HttpServletRequest request = currentContext.getRequest(); String userToken = request.getParameter("userToken"); if (StringUtils.isEmpty(userToken)) { //setSendZuulResponse(false)令zuul过滤该请求,不进行路由 currentContext.setSendZuulResponse(false); //设置返回的错误码 currentContext.setResponseStatusCode(401); currentContext.setResponseBody("userToken is null"); return null; } // 否则正常执行业务逻辑..... return null; } }
3-2、重启springcloud-zuul服务,访问:
http://localhost:9999/api-b/feign/hello返回:userToken is null
http://localhost:9999/api-b/feign/hello?userToken=""返回:spring cloud provider-01 hello world
在上面实现的过滤器代码中,我们通过继承ZuulFilter抽象类并重写了下面的四个方法来实现自定义的过滤器。这四个方法分别定义了:
-
filterType():过滤器的类型,它决定过滤器在请求的哪个生命周期中执行。这里定义为pre,代表会在请求被路由之前执行。
-
filterOrder():过滤器的执行顺序。当请求在一个阶段中存在多个过滤器时,需要根据该方法返回的值来依次执行。通过数字指定,数字越大,优先级越低。
-
shouldFilter():判断该过滤器是否需要被执行。这里我们直接返回了true,因此该过滤器对所有请求都会生效。实际运用中我们可以利用该函数来指定过滤器的有效范围。
-
run():过滤器的具体逻辑。这里我们通过currentContext.setSendZuulResponse(false)令 Zuul 过滤该请求,不对其进行路由,然后通过currentContext.setResponseStatusCode(401)设置了其返回的错误码,当然我们也可以进一步优化我们的返回,比如,通过currentContext.setResponseBody(body)对返回 body 内容进行编辑等。
4、Zuul路由规则
4-1、默认路由规则
由于Zuul引入Eureka网关的时候,会为每一个服务创键一个默认的路由规则,默认情况下实例名作为请求的前缀,这样不对外开放的服务也会被外界访问到,我们可以控制一下为哪些服务创键路由规则。
zuul.ignored-services: *
*表示不为所有的服务创建默认的路由规则,则需要我们自己配置路由规则。
4-2、自定义路由规则
为了兼容客户端不同版本,有时候需要我们为一组互相配合的微服务定义一个版本标识来方便管理,它们的版本关系,根据这个标识我们很容易的知道这些服务需要一起启动并配合使用,比如我们的服务都采用以版本这样的命名方式,例如:consumer-v1,consumer-v2分版本访问服务的话,我们可以使用自定义路由规则,注入PatternServiceRouteMapper对象即可自动的构建类似 /v1/consumer/** 的路由规则
@Bean public PatternServiceRouteMapper patternServiceRouteMapper(){ return new PatternServiceRouteMapper("(?<name>^.+)-(?<version>v.+$)", "${version}/${name}"); }
PatternServiceRouteMapper对象可以通过正则表达式来自定义服务与路由映射的生成关系。其中构造函数的
第一个参数: 用来匹配服务名称是否符合该自定义规则的正则表达式,
第二个参数: 用来定义根据服务名中定义的内容转换出的路径表达式规则。
当开发者在API网关中定义了PatternServiceRouteMapper实现之后,只要符合第一个参数定义规则的服务名,都会优先使用该实现构建出的路径表达式,如果没有匹配上的服务则还是会使用默认的路由映射规则,即采用完整服务名作为前缀的路径表达式。
5、路径匹配
在上面案例上我们看到了使用通配符作为匹配路径,一共有三种通配符,如下:
通配符 | 通配符含义 |
---|---|
? | 匹配单个字符,如:/consumer/a,/consumer/b |
* | 匹配任意数量的字符,如:/consumer/abc,/consumer/def |
** | 匹配任意数量的字符,支持多级路径,如:/consumer/abc/def,/consumer/ghi/gkl |
我们在使用的时候,比如我们的consumer服务路由路径为:/consumer/**,由于发展还需要再次拆分出另外一个consumer-ext服务,路由规则为/consumer/ext/**,由于**匹配多级目录这时候我们需要区别这些服务路径,properties配置无法保证配置的加载顺序,但在YML配置文件中我们可以使用/consumer/ext/**配置在/consumer/**前面则可以保证consumer-ext服务的正常路由
6、忽略表达式
zuul.ignored-patterns: /**/hello/**
该配置表示忽略路由路径包含hello的路径
7、路由前缀
zuul.prefix: /api zuul.strip-prefix: true
prefix:前缀,当请求匹配前缀时会进行代理
strip-prefix:代理前缀默认(true)会从请求路径中移除,可以设置为false关闭移除代理前缀动作,也可以通过zuul.routes.<route>.strip-prefix=false来对指定路由关闭移除代理前缀动作。
但是在《Spring Cloud微服务实战》中指出Brixton.SR7和Camden.SR3中有Bug,该案例版本为Finchley.SR4未发现Bug
8、Zuul安全与Header
敏感的Header设置,一般来说同一个系统中的服务之间共享Header,不过Zuul防止一些敏感的Header外泄,防止它们被传递到下游服务器,如果我们需要传递Cookie,Set-Cookie,Authorization 这些信息,我们可以这样做
做法一全局设置:将 zuul.sensitive-headers 的值设置为空
做法二指定路由设置: zuul.routes.<route>.sensitiveHeaders: '' zuul.routes.<route>.custom-sensitive-headers: true
9、忽略 Header
可用 zuul.ignoredHeaders 属性丢弃一些 Header,这样设置后 Cookie 将不会传播到其它微服务中
zuul.ignored-headers: Cookie
10、禁用指定的 Filter
zuul.<SimpleClassName>.<filterType>.disable=true
具体详细配置参考Spring官网:https://cloud.spring.io/spring-cloud-static/Finchley.SR4/single/spring-cloud.html#_router_and_filter_zuul
八、Zuul的动态路由
Zuul作为服务的统一入口,传统方式将路由规则配置在配置文件中,如果路由规则发生了改变,需要重启服务器,这就会对外界停止服务。这时候我们结合SpringCloud Config分布式配置中心《Spring Cloud第十篇 | 分布式配置中心Config》实现动态路由规则,此处不再演示消息总线Bus《Spring Cloud第十二篇 | 消息总线Bus》的使用。
1、配置中心服务端
1-1、为了保证以前配置中心服务端模块(springcloud-config-server)的整洁性,此处新建一个配置中心服务端模块命名为(springcloud-zuul-config-server)
1-2、添加依赖
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-config-server</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency>
1-3、配置application.yml文件
spring: application: name: springcloud-zuul-config-server cloud: config: server: git: #配置git仓库地址 uri: https://gitee.com/coding-farmer/config-center #配置仓库路径 search-paths: "{profile}" #访问git仓库的用户名 username: #访问git仓库的密码 password: #配置中心通过git从远程git库,有时本地的拷贝被污染, #这时配置中心无法从远程库更新本地配置,设置force-pull=true,则强制从远程库中更新本地库 force-pull: true #默认从git仓库克隆下载的在C:/Users/<当前用户>/AppData/Local/Temp #basedir: server: port: 8888 eureka: client: service-url: defaultZone: http://localhost:8700/eureka #客户端每隔30秒从Eureka服务上更新一次服务信息 registry-fetch-interval-seconds: 30 #需要将我的服务注册到eureka上 register-with-eureka: true #需要检索服务 fetch-registry: true #心跳检测检测与续约时间 instance: #告诉服务端,如果我10s之内没有给你发心跳,就代表我故障了,将我剔除掉,默认90s #Eureka服务端在收到最后一次心跳之后等待的时间上限,单位为秒,超过则剔除(客户端告诉服务端按照此规则等待自己) lease-expiration-duration-in-seconds: 10 #每隔2s向服务端发送一次心跳,证明自已依然活着,默认30s #Eureka客户端向服务端发送心跳的时间间隔,单位为秒(客户端告诉服务端自己会按照该规则) lease-renewal-interval-in-seconds: 2 # 启用ip配置 这样在注册中心列表中看见的是以ip+端口呈现的 prefer-ip-address: true # 实例名称 最后呈现地址:ip:2002 instance-id: ${spring.cloud.client.ip-address}:${server.port}
1-4、在启动类上添加注解
@EnableConfigServer
@EnableEurekaClient
到此配置中心服务端搭建完成
2、修改zuul服务模块
2-1、添加配置中心客户端依赖
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-config-client</artifactId> </dependency>
2-2、将application.yml文件改名为application-bak.yml(为了application.yml文件),配置bootstrap.yml文件
server:
port: 9999
spring:
application:
name: springcloud-zuul
cloud:
config:
#uri则表示配置中心的地址
#uri: http://localhost:8888
#注:config 客户端在没有 spring.cloud.config.name属性的时候,服务端{application} 获取的是客户端
#spring.application.name的值,否则,获取的是 spring.cloud.config.name的值。
#1)、当没有spring.cloud.config.name时,客户端获取的是spring.application.name 所对应的git库中的文件,并且只能
#获取一个文件,
#2)、当一个项目中有需求要获取多个文件时,就需要用到spring.cloud.config.name这个属性,以逗号分割
name: configzuul
profile: dev
#label对应了label部分
label: master
# username:
# password:
discovery:
#表示开启通过服务名来访问config-server
enabled: true
#则表示config-server的服务名
service-id: springcloud-zuul-config-server
#失败快速响应
fail-fast: true
retry:
#配置重试次数,默认为6
max-attempts: 6
#初始重试间隔时间,默认1000ms
initial-interval: 1000
#间隔乘数,默认1.1
multiplier: 1.1
#最大间隔时间,默认2000ms
max-interval: 2000
eureka:
client:
service-url:
defaultZone: http://localhost:8700/eureka
#客户端每隔30秒从Eureka服务上更新一次服务信息
registry-fetch-interval-seconds: 30
#需要将我的服务注册到eureka上
register-with-eureka: true
#需要检索服务
fetch-registry: true
#心跳检测检测与续约时间
instance:
#告诉服务端,如果我10s之内没有给你发心跳,就代表我故障了,将我剔除掉,默认90s
#Eureka服务端在收到最后一次心跳之后等待的时间上限,单位为秒,超过则剔除(客户端告诉服务端按照此规则等待自己)
lease-expiration-duration-in-seconds: 10
#每隔2s向服务端发送一次心跳,证明自已依然活着,默认30s
#Eureka客户端向服务端发送心跳的时间间隔,单位为秒(客户端告诉服务端自己会按照该规则)
lease-renewal-interval-in-seconds: 2
# 启用ip配置 这样在注册中心列表中看见的是以ip+端口呈现的
prefer-ip-address: true
# 实例名称 最后呈现地址:ip:2002
instance-id: ${spring.cloud.client.ip-address}:${server.port}
management:
endpoints:
web:
exposure:
include: ["info","health","refresh"]
2-3、配置刷新类
@Configuration public class Config { @RefreshScope @ConfigurationProperties("zuul") public ZuulProperties zuulProperties() { return new ZuulProperties(); } }
3、在代码仓库添加配configzuul-dev.yml置文件
4、启动相关服务
Eureka服务(springcloud-eureka-server),zuul的配置中心服务(springcloud-zuul-config-server)、提供者服务(springcloud-service-provider)、消费者服务(springcloud-service-consumer)、zuul服务(springcloud-zuul)
访问消费者服务接口:http://localhost:9999/api-a/consumer/hello?userToken=""
访问配置中心服务端:http://localhost:8888/configzuul-dev.yml,查看配置结果如图:
修改仓库config-center的configzuul-dev.yml配置为api-c接着在访问配置中心仓库配置结果,http://localhost:8888/configzuul-dev.yml,注意api-a的key不要修改
然后发送post请求zuul的refresh端点进行配置刷新http://localhost:9999/actuator/refresh
然后你会发现http://localhost:9999/api-a/consumer/hello?userToken=’‘路径访问不通了,访问http://localhost:9999/api-c/consumer/hello?userToken=’'结果如图,显示页面为
到此zuul的动态刷新完成,此处动态刷新就是使用了配置中心的功能,不了解的可以参考《Spring Cloud第十篇 | 分布式配置中心Config》
详细参考案例源码:https://gitee.com/coding-farmer/springcloud-learn