4.8 Router and Filter: Zuul
=== 4.8 路由及过滤器:Zuul
路由功能是微服务框架中一个不可或缺的部分。例如:
/
可以映射到一个web应用中,/api/users
会被映射到用户服务;/api/shop
会被映射到商品服务……
是Netflix的一个基于JVM路由以及服务端负载均衡的框架。
提供一下功能:
- 身份验证
- 推断
- 压力测试
- 金丝雀测试(canary testing)
- 动态路由
- 服务迁移
- 反压减负
- 安全
- 静态资源处理
- 主主通讯管理
Zuul的规则引擎支持任何基于JVM的语言来编写规则以及过滤器,例如:Java,Groovy。
注意: 原有的zuul.max.host.connections
属性,已经被zuul.host.maxTotalConnections
和zuul.host.maxPerRouteConnections
两个新的配置项替代。默认值分别为:200,20。
注意: 默认的Hystrix隔离模式(ExecutionIsolationStrategy
)对于所有的路由都是SEMAPHORE
。可以通过配置zuul.ribbonIsolationStrategy
来自定义,如:THREAD
。
4.8.1 How to Include Zuul 如何引入Zuul
一如既往,在工程中引入相应的启动器就行:group:
org.springframework.cloud
;artifact id:spring-cloud-starter-zuul
。
4.8.2 Embedded Zuul Reverse Proxy 内嵌Zuul反向代理
Spring Cloud 会创建一个内嵌的Zuul代理,来简化前端UI应用调用后端服务的开发。对于前端接口调用这个功能会比较有用,避免手工处理复杂的跨域(CORS)以及交叉身份验证等后端一系列问题。
如果需要开启此功能,只需要在Spring Boot 的主类中加上
@EnableZuulProxy
注解就行。按约定,假设一个ID为“users”的服务,会收到代理服务的/users
的请求。代理服务会使用Ribbon的负载来转发请求,所有的请求也将由一个Hystrix命令来执行,这样也就自带降级,断路器等功能。
注意: Zuul的启动器没有包括服务发现的客户端,因此,需要手工引入服务发现框架(例如:Eureka)
如果需要不想自动添加服务,可以设置
zuul.ignored-services
来指定一组服务id来匹配。如果不希望通过匹配来发现服务,也可以直接进行路由映射配置,例如:
application.yml
zuul: ignoredServices: '*' routes: users: /myusers/**
上例中,除了“users”服务,其他的服务都会被忽略。
可以添加额外的路由配置,例如:
application.yml
zuul: routes: users: /myusers/**
这个示例,配置了前端通过
/myusers
的http访问,将会被后端“users”服务处理(例如:/myusers/101
将会转发的/101
)
如果需要更精细的控制路由,如指定路径、服务id,可以这样:
application.yml
zuul: routes: users: path: /myusers/** serviceId: users_service
这个示例,配置了前端
/myusers
请求,会被转发到后端“users_service”服务。path
可以指定一个正则表达式来匹配路径,因此,/myusers/*
只能匹配一级路径,但是通过/myusers/**
可以匹配所有以/myusers/
开头的路径。
后端服务除了可以通过
serviceId
(基于服务发现)来指定外,还可以通过url
(物理路径/直连)来指定。例如:
application.yml
zuul: routes: users: path: /myusers/** url: http://example.com/users_service
这个简单的url路由不会由
HystrixCommand
来执行,自然,也就得不到Ribbon的负载均衡,降级,断路器等功能。如果需要这些,可以指定一个服务路由并且为这个serviceId配置一个Ribbon客户端。例如:
application.yml
zuul: routes: users: path: /myusers/** serviceId: usersribbon: eureka: enabled: falseusers: ribbon: listOfServers: example.com,google.com
可以一个转换器,让serviceId和路由之间使用正则表达式来自动匹配。例如:
ApplicationConfiguration.java
@Beanpublic PatternServiceRouteMapper serviceRouteMapper() { return new PatternServiceRouteMapper( "(?^.+)-(? v.+$)", "${version}/${name}");}
这样,一个serviceId为“myusers-v1”的服务,就会被映射到路由为“/v1/myusers/”的路径上。任何正则表达式都可以,但是所有的命名组必须包括
servicePattern
和routePattern
两部分。如果servicePattern
没有匹配一个serviceId,那就会使用默认的。在上例中,一个serviceId为“myusers”的服务,将会被映射到路由“/myusers/”中(不带版本信息)。这个特性默认是关闭的,而且只适用于已经发现的服务。
可以通过
zuul.prefix
为所有的映射增加统一的前缀。如:/api
。默认情况下,代理会在转发前自动剥离这个前缀。如果需要转发时带上前缀,可以配置:zuul.stripPrefix=false
来关闭这个默认行为。例如:
application.yml
zuul: routes: users: path: /myusers/** stripPrefix: false
注意: zuul.stripPrefix
只会对zuul.prefix
的前缀起作用。对于path
指定的前缀不会起作用。
上例中,请求
/myusers/101
,将会被转发到/myusers/101
的“users”服务。
zuul.routes
实体实际上会绑定一个ZuulProperties
对象。如果你看看这个对象,你会发现它也有一个“retryable”属性。如果设置为“true”,那Ribbon客户端会自动失败重试。(如果需要,可以修改这个属性)
默认情况下,转发的请求,头信息会添加
X-Forwarded-Host
信息。可以通过配置:zuul.addProxyHeaders = false
来关闭这个特性。前缀默认会被剥离,但是后端可以通过X-Forwarded-Prefix
拿到前缀。
如果配置了一个默认路由:
/
,那么当应用带有@EnableZuulProxy
注解时,可以得到一个独立的服务。例如:zuul.route.home: /
,那所有的请求(“/**”)都会转发到“home”服务。
如果还需要更进一步的控制忽略规则,可以配置忽略的路径匹配规则。要注意的是匹配是从前缀开始的,并且会匹配所有符合条件的服务。例如:
application.yml
zuul: ignoredPatterns: /**/admin/** routes: users: /myusers/**
这个示例中,“/myusers/101”将会转发到“/101”到“users”服务。但是如果包含“/admin”则不会转发。
警告: 如果想按找配置的顺序进行路由规则控制,则需要使用YAML,如果是使用propeties文件,则会丢失顺序。例如:
application.yml
zuul: routes: users: path: /myusers/** legacy: path: /**
如果使用properties文件进行配置,则
legacy
就可能会先生效,这样users
就没效果了。
4.8.3 Zuul Http Client
Zuul默认会使用Apache HTTP客户端,而不是使用Ribbon的
RestClient
。如果想要使用RestClient
或者okhttp3.OkHttpClient
,可以配置:ribbon.restclient.enabled=true
或则ribbon.okhttp.enabled=true
4.8.3.1 Cookies and Sensitive Headers
同一个系统中各个服务之间通过Headers来共享信息是没啥问题的,但是如果不想Headers中的一些敏感信息随着HTTP转发泄露出去话,需要在路由配置中指定一个忽略Header的清单。
如同在浏览器中一样,Cookies也是一个敏感信息源,所以也需要一个特殊的规则来进行配置。 如果消费端是一个浏览器,那么Cookies也会对下游服务造成一定的影响(在下游服务看来这些cookies信息都来自同一个地方)
如果精心设计了个服务,只有一个下游服务会设置cookies,那么这些信息会随着后端返回给所有的调用者。同样,如果由代理来设置cookies,那后端服务自然的就会共享这些信息。
另外,下游服务对cookies的读写,可能并不会对调用者有啥实际作用,因此建议大家,在路由中至少设置"Set-Cookie"、"Cookie"这两个Header。即便路由到本地服务,这样意味着允许代理,服务之间传递指定cookies信息。
配置的
sensitiveHeaders
可以用逗号分割。例如:
application.yml
zuul: routes: users: path: /myusers/** sensitiveHeaders: Cookie,Set-Cookie,Authorization url: https://downstream
也可以为
sensitiveHeaders
指定一个全局的配置:zuul.sensitiveHeaders
。这样,当路由中没有配置sensitiveHeaders
那么就会采用全局配置。
注意: 全局配置是Spring Cloud Netflix 1.1中的新特性。
4.8.3.2 Ignored Headers
如果每一个路由配置额外的敏感Header,那你可以设置一个全局的
zuul.ignoredHeaders
来统一的组织下游服务随意的共享Header信息。
当然默认是没有这个配置的,如果引入Spring Security,那么会自动加上“security”Header信息。
下游服务也可以添加他们自己的Header。这种情况就需要保留这些Header信息,所以需要设置
zuul.ignoreSecurityHeaders
为false
。特别是在引入Spring Security时。
4.8.4 The Routes Endpoint 路由接口
如果在Spring Boot中使用了
@EnableZuulProxy
,那么默认就会开启一个额外的HTTP接口:/routes
。GET请求这个接口会返回路由映射的清单。POST请求可以强制刷新某个存在的路由。
注意: 服务信息变更后,路由映射会自动更新。通过这个接口这是强制立即刷新。
4.8.5 Strangulation Patterns and Local Forwards 匹配范围及本地跳转
迁移系统,或者和旧系统整合时的常用套路就是逐步替代,灰色过度。在这一点上Zuul代理也比较方便,可以让客户端老接口全部走代理,通过路由转发到新的系统接口上。 例如:
application.yml
zuul: routes: first: path: /first/** url: http://first.example.com second: path: /second/** url: forward:/second third: path: /third/** url: forward:/3rd legacy: path: /** url: http://legacy.example.com
在上例中,
legacy
的匹配范围实际上被限制死了。注意:url中的forward
的用法类似 Spring中的@RequestMapping
。
4.8.6 Uploading Files through Zuul 通过Zuul进行文件上传
如果已经使用
@EnableZuulProxy
,那么就已经可以通过代理进行文件上传操作了,当然文件不能过大。大文件最好直接通过Spring MVC的DispatcherServlet
来处理/zuul/*
请求。例如: 假设配置了zuul.routes.customers=/customers/**
,那就可以POST请求/zuul/customers/*
来处理大文件的上传操作。Servelt路径可以通过zuul.servletPath
来指定。
如果文件真的比较大,那么还会导致上传过程中处理超时。所以,还需要配置一下超时设置。例如:
application.yml
hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds: 60000ribbon: ConnectTimeout: 3000 ReadTimeout: 60000
注意:大文件流的请求,需要使用chunked编码(一些浏览器默认就是chunked编码)。例如: 在命令行中可以这样:
$ curl -v -H "Transfer-Encoding: chunked" \ -F "file=@mylarge.iso" localhost:9999/zuul/simple/file
4.8.7 Plain Embedded Zuul
如果使用
@EnableZuulServer
,可以抛开代理,直接使用Zuul服务,或者只让部分服务走代理。应用中,可以额外添加带有@EnableZuulProxy
的ZuulFilter
类型的Bean,这些Bean就会组成一个过滤器链。这样就是,通过过滤器来处理请求而不是走代理了。
这种情况下,Zuul服务的路由仍然可以通过
zuul.routes.*
进行配置,不过当然就不会有服务发现以及代理等功能了。因此,serviceId
和url
就可以被忽略。例如:
application.yml
zuul: routes: api: /api/**
上例就是直接映射“/api/**”到Zuul的过滤器链。
4.8.8 Disable Zuul Filters 禁用Zuul过滤器
默认情况下,无论是代理模式还是服务模式,Spring Cloud都为Zuul配备了一组
ZuulFilter
。 具体开启了哪些过滤器,可以参见
如果想禁用这些过滤器,可以配置:
zuul.<SimpleClassName>.<filterType>.disable=true
。 按照惯例,这个过滤器都放置在包**.filters
下。所以,如果想要禁用org.springframework.cloud.netflix.zuul.filters.post.SendResponseFilter
过滤器,则可以配置:org.springframework.cloud.netflix.zuul.filters.post.SendResponseFilter
4.8.9 Providing Hystrix Fallbacks For Routes 为路由提供降级功能
可以创建一个Bean:
ZuulFallbackProvider
来为Zuul调用链路提供一个降级服务。在这个Bean中需要制定一个路由的ID,当触发降级时,由这个路由来返回ClientHttpResponse
。下面展示了一个简单的ZuulFallbackProvider
实例:
class MyFallbackProvider implements ZuulFallbackProvider { @Override public String getRoute() { return "customers"; } @Override public ClientHttpResponse fallbackResponse() { return new ClientHttpResponse() { @Override public HttpStatus getStatusCode() throws IOException { return HttpStatus.OK; } @Override public int getRawStatusCode() throws IOException { return 200; } @Override public String getStatusText() throws IOException { return "OK"; } @Override public void close() { } @Override public InputStream getBody() throws IOException { return new ByteArrayInputStream("fallback".getBytes()); } @Override public HttpHeaders getHeaders() { HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); return headers; } }; }}
相应的配置如下:
zuul: routes: customers: /customers/**
4.8.10 Polyglot support with Sidecar 跨语言支持
是否想在非JVM平台语言中使用Eureka、Ribbon及Config服务吗? 开发了一套:Spring Cloud Netflix Sidecar。 它包含了一套简单的HTTP API(例如:主机名,端口),用于暴露各个服务端口。 你也可以通过Zuul内嵌的代理服务路由到Eureka服务。 Spring Cloud Config Server可以通过主机名直接访问,也可以通过Zuul代理访问。Sidecar中的非JVM应用可以实现一个健康检查机制,这样就可以在应用上下线时得到反馈。
在工程中引入Sidecar需要加入相关依赖:group:
org.springframework.cloud
,artifact id:spring-cloud-netflix-sidecar
。
开启Sidecar,只需要在Boot应用中加上
@EnableSidecar
注解。这个注解相当于:@EnableCircuitBreaker
,@EnableDiscoveryClient
以及@EnableZuulProxy
三个注解的合集。 这样,运行后就可以被非JVM应用访问。
可以在
application.yml
中,对Sidecar进行配置:sidecar.port
,sidecar.health-uri
。sidecar.port
指定一个端口,用于非JVM应用监听。这样Sidecar就可以将应用注册到Eureka。sidecar.health-uri
指定一个URI,非JVM应用通过这个URI来模拟Spring Boot的健康检测。这个应该返回一个JSON格式。例如:
health-uri-document
{ "status":"UP"}
Sidecar应用的
application.yml
可以这样配置:
application.yml
server: port: 5678spring: application: name: sidecarsidecar: port: 8000 health-uri: http://localhost:8000/health.json
/hosts/{serviceId}
就相当于DiscoveryClient.getInstances()
方法。 下面的例子,展示一个/hosts/customers
返回两个不同主机上的实例。http://localhost:5678/hosts/{serviceId}
这个接口也可以被非JVM应用访问到(假设Sidecar的监听端口为5678)。
/hosts/customers
[ { "host": "myhost", "port": 9000, "uri": "http://myhost:9000", "serviceId": "CUSTOMERS", "secure": false }, { "host": "myhost2", "port": 9000, "uri": "http://myhost2:9000", "serviceId": "CUSTOMERS", "secure": false }]
Zuul代理会为Eureka服务,按照
/<serviceId>
,自动添加路由。相应的,消费端会增加到/customers
。 非JVM应用可以访问这个接口来获取消费端信息:http://localhost:5678/customers
(假设Sidecar的监听端口为5678)
如果Config Server已经注册到Eureka了,那非JVM应用也可以通过Zuul代理进行访问。如果ConfigServer的serviceId是
configserver
,Sidecar端口为5678,那就可以通过http://localhost:5678/configserver
对Config Server进行访问。
非JVM应用可以通过Config Server获得YAML格式的配置信息。例如:调用
http://sidecar.local.spring.io:5678/configserver/default-master.yml
可以返回下列信息:
eureka: client: serviceUrl: defaultZone: http://localhost:8761/eureka/ password: passwordinfo: description: Spring Cloud Samples url: https://github.com/spring-cloud-samples