文章目录
一、网关产生的背景
大型综合性网站往往由成百上千乃至上万个微服务组成,而每个微服务往往会依赖多个不同的微服务,不同微服务一般会有不同的网络地址。如一个放款微服务,可能需要调用多个微服务接口才能完成一次放款流程。一次放款的业务流程如下:
若让放款微服务直接与各个微服务系统通信,会有以下弊端:
- 调用服务多次请求多个不同的微服务,增加了调用方的复杂性
- 存在跨域请求,一定场景下处理起来相对复杂
- 调用认证复杂,每个服务都需要独立认证
- 难以重构,随着需求变动,项目迭代,可能需要重新划分微服务,或者拆分或者合并。若调用服务与各个微服务直接通信,调用服务也需要随之变动。
- 每个微服务使用协议可能会不一样,直接访问困难
鉴于以上存在的问题,微服务网关便应运而生。微服务网关是介于客户端与服务端直接的中间层,所有来自客户端对不同服务端的请求都先经过网关。
微服务网关封装了各个微服务的内部调用结构,调用方只须跟网关交互,而无须直接调用特定微服务接口。这样开发就可以得到简化。不仅如此,而且还有以下优点:
- 易于监控。微服务网关搜集监控数据并将其推送到外部系统进行分析。
- 易于认证。可在微服务网关进行认证,然后将请求转发到后端微服务,无须每个请求服务都认证
- 减少了调用方与各个微服务的交互次数
二、Zuul简介
Zuul亦是Netflix开源的微服务网关,它和Eureka、Ribbon、Hystrix等组件配合使用。Zuul的核心是一系列的过滤器,这些过滤器大致有以下功能:
- 身份认证与安全: 识别每个资源的验证要求,并不拒绝那些与要求不符的请求。
- 审查与监控: 在边缘位置追踪有意义的的数据和统计结果,从而产生精确的数据视图。
- 动态路由: 动态地将不同的请求路由到不同的后端集群。
- 压力测试: 逐渐增加指向集群的流量,以了解性能。
- 负载分配: 为每一种负载策略分配对应的容量,并启用超出限定值的请求。
- 静态响应处理: 在边缘位置直接建立部分响应,从而避免其转发到内部集群;
- 多区域弹性: 跨越AWS Region进行请求路由,旨在实现ELB(Elastic Load Balancing)使用的多样化,以及让系统边缘更贴近系统的使用者。
SpringCloud对Zuul进行了整合与增强。目前,Zuul使用的默认的HTTP客户端是Apache HTTPClient,也可以使用RedisClient或okhttp3.OkHttpClient。若想要使用它们,设置ribbon.restclient.enabled=true 或者 ribbon.okhttp.enabled=true即可
三、Zuul实现API网关
1、引入maven pom.xml依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-zuul</artifactId>
</dependency>
2、在启动类上添加@EnableZuulProxy注解,声明一个Zuul代理。该代理用使用Ribbon来定位注册在Eureka Server中的微服务;同时,代理还整合了Hystrix以实现容错,所有经过Zuul的请求都会在Hystrix命令中执行。
@SpringCloudApplication
@EnableZuulProxy
public class UwsGateWayApplication {
public static void main(String[] args) {
SpringApplication.run(UwsGateWayApplication.class);
}
}
3、编写配置application.yml
server:
port: 8088
spring:
application:
name: uws-data-zuul
eureka:
instance:
prefer-ip-address: true
client:
service-url:
defaultZone: http://localhost:8761/eureka/
四、 路由配置详解
默认情况下,Zuul网关代理会代理所有注册到Eureka Server的微服务。有时候我们需要对代理部分微服务,或者对url较为精确的控制,Zuul也提供了这种能力。
1、忽略所有微服务,只路由指定微服务
zuul:
ignored-services: '*'
routes:
users: /myusers/**
2、忽略指定微服务,多个用逗号隔开
zuul:
ignored-services: users,consumers
3、自定义指定微服务的访问路径
zuul:
routes:
users-service: /myusers/**
4、同时指定微服务的serviceId和对应路径
zuul:
routes:
users:
path: /myusers/**
serviceId: users_service
5、同时指定url和path
zuul:
routes:
users:
path: /myusers/**
url: http://example.com/users_service
需要说明的是此种方式的配置,路由不会作为HystrixCommand执行,也不会使用Ribbon负载多个url,即Hystrix容错和Ribbon的负载均衡在这种配置下无效。下边5的配置可解决此问题
6、同时指定path和url,并且不破坏Zuul的Hystrix和Ribbon特性
zuul:
routes:
echo:
path: /myusers/**
service-Id: myusers-service
strip-prefix: true
hystrix:
command:
myusers-service:
execution:
isolation:
thread:
timeoutInMilliseconds: ...
myusers-service:
ribbon:
NIWSServerListClassName: com.netflix.loadbalancer.ConfigurationBasedServerList
listOfServers: http://example1.com,http://example2.com
ConnectTimeout: 1000
ReadTimeout: 3000
MaxTotalHttpConnections: 500
MaxConnectionsPerHost: 100
另一种方法是指定服务路由并为serviceId配置Ribbon客户端,但是需要禁用Ribbon支持Eureka
zuul:
routes:
users:
path: /myusers/**
service-Id: users
ribbon:
eureka:
enabled: false
users:
ribbon:
listOfServers: example.com,google.com
7、使用正则表达式指定Zuul的匹配规则
借助PatternServiceRouteMapper实现微服务到映射路由的正则匹配
@Bean
public PatternServiceRouteMapper serviceRouteMapper() {
//PatternServiceRouteMapper(String servicePattern,String routePattern)
//servicePattern 指定微服务的正则
//routePattern 指定路由正则
return new PatternServiceRouteMapper(
"(?<name>^.+)-(?<version>v.+$)",
"${version}/${name}");
}
可以把myusers-v1这个服务映射到路由到/v1/myusers/**这个路径
8、路由前缀
默认情况下前缀在Zuul转发前将去除,将zuul.stripPrefix=fasle,可以关闭此行为。
(1)访问Zuul的/api/myusers/101路径被转发到users微服务的/api/myusers/101路径
zuul:
prefix: /api
stripPrefix: false
routes:
users: /myusers/**
(2)访问Zuul的/myusers/101路径被转发到users微服务的/myusers/101路径
zuul:
routes:
users:
path: /myusers/**
stripPrefix: false
9、忽略某些路径
有时候,我们想让Zuul代理某个微服务,但又想保护某些敏感路径不被代理,可以使用ignoredPatterns,指定忽略的正则
zuul:
ignoredPatterns: /**/admin/**
routes:
users: /myusers/**
将users微服务映射到 /myusers/**路径,并且忽略包含/admin/的路径。
10、若你想要保持路由顺序,需要使用yml配置,使用property文件顺序会丢失
zuul:
routes:
users:
path: /myusers/**
legacy:
path: /**
五、Zuul的容错与回退
SpringCloud中,Zuul默认整合了Hystrix。当断路器打开时,通过实现FallbackProvider来响应回退,指定路由ID,提供ClientHttpResponse作为回退响应。
(1)application.yml配置回退服务
zuul:
routes:
customers: /customers/**
(2)回退类如下:
class MyFallbackProvider implements FallbackProvider {
@Override
public String getRoute() {//指定回退服务
return "customers";
}
@Override
public ClientHttpResponse fallbackResponse(String route, final Throwable cause) {
if (cause instanceof HystrixTimeoutException) {
return response(HttpStatus.GATEWAY_TIMEOUT);
} else {
return response(HttpStatus.INTERNAL_SERVER_ERROR);
}
}
private ClientHttpResponse response(final HttpStatus status) {
return new ClientHttpResponse() {
@Override
public HttpStatus getStatusCode() throws IOException {
return status;
}
@Override
public int getRawStatusCode() throws IOException {//数字类型的状态码
return status.value();
}
@Override
public String getStatusText() throws IOException {//状态文本
return status.getReasonPhrase();
}
@Override
public void close() {
}
@Override
public InputStream getBody() throws IOException {//响应体
return new ByteArrayInputStream("fallback".getBytes());
}
@Override
public HttpHeaders getHeaders() {//headers设定
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
return headers;
}
};
}
}
若服务无法正常响应,就会返回响应体中的内容“fallback”
(3)如果要为所有路由提供默认回退,可以创建类型为FallbackProvider的bean,并让getRoute方法返回*或空,如下例所示:
class MyFallbackProvider implements FallbackProvider {
@Override
public String getRoute() {
return "*";
}
@Override
public ClientHttpResponse fallbackResponse(String route, Throwable throwable) {
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的安全与Header
一般来说,同一个系统中的服务之间可以共享Header。不过应该尽量防止让一些敏感的Header外泄。因此在很多场景下,需要通过路由指定一些敏感头列表。
1、敏感头设置
(1)为每个服务单独指定敏感头。
如下,为users微服务单独指定敏感Header。
zuul:
routes:
users:
path: /myusers/**
sensitiveHeaders: Cookie,Set-Cookie,Authorization
url: https://downstream
Cookie,Set-Cookie,Authorization这也是sensitiveHeaders的默认值。若没有需要,无须再去设置它
(2)发送全部Header
sensitiveHeaders好比是一个黑名单,默认值不为空。要使zuul发送所有头(忽略的头除外),必须将其显式设置为空列表。如果要将cookie或authorization headers传递到后端,则必须这样做。如下
zuul:
routes:
users:
path: /myusers/**
sensitiveHeaders:
url: https://downstream
(3)设置全局头
通过设置zuul.sensitiveHeaders来设置敏感头,会覆盖全部敏感头的设置
zuul:
sensitiveHeaders:Cookie,Set-Cookie,Authorization
2、忽略Header
可使用zuul.ignoredHeaders,丢弃一些头,不被传播到其它微服务中
zuul:
ignoredHeaders: Header1,Header2
默认情况下,zuul.ignoredHeaders的值时空的。但是如果Spring Security在项目的classpath中,那么其默认是就是。所以当Spring Security在项目的classpath中时候,同时又要使用下游微服务的Spring Security的Header时,可以将zuul.ignoreSecurityHeaders设置为false。
七、Zuul的过滤器
1、@EnableZuulProxy vs. @EnableZuulServer
过滤器是Zuul的核心组件,Zuul的大部分功能都是通过过滤器实现的。Spring Cloud Netflix包含了许多过滤器,这取决于使用哪个过滤器启用Zuul。@EnableZuulProxy 是 @EnableZuulServer的超集,换句话说,@EnableZuulProxy包含了@EnableZuulServer安装的所有的过滤器。“代理”中的附加过滤器设计目的是路由功能,若你想要空白的Zuul,你该使用@EnableZuulServer注解。
2、@EnableZuulServer包含的过滤器
@EnablezuulServer创建一个SimpleRouteLocator,它从SpringBoot配置文件加载路由定义。
启用该注解包含了以下4中过滤器类型
- Pre filters:这种过滤器在请求被路由之前调用。
- ServletDetectionFilter:通过设置布尔型的过滤器常量IS_DISPATCHER_SERVLET_REQUEST_KEY来检测请求是否通过Spring调度器。
- FormBodyWrapperFilter:解析表单数据并为下游请求重新编码
- DebugFilter:如果设置了debug请求参数,则将RequestContext.setDebugRouting() 和RequestContext.setDebugRequest()设置为true
- Route filters:这种过滤将请求路由到微服务。
- SendForwardFilter:使用servlet requestDispatcher转发请求。转发位置存储在RequestContext属性FilterConstants.FORWARD_TO_KEY键中。这对于转发到当前应用程序中的endpoints 很有用
- Post filters:在路由到微服务以后执行。
- SendResponseFilter:将代理请求的响应写入当前响应
- Error filters:在发生错误时执行该过滤器
- SendErrorFilter:如果requestContext.getthrowable()不为空,则转发到/error(默认情况下)。通过设置error.path属性,可以更改默认转发路径(/error)
3、@EnableZuulProxy包含的过滤器
启用 @EnableZuulProxy注解时,会创建一个 DiscoveryClientRouteLocator ,加载DiscoveryClient(如Eureka)路由定义和属性,从DiscoveryClient为每个serviceId一个路由,添加新服务时,路由也会被刷新。
除了前面2中的过滤器外,还安装了以下过滤器
- Pre filters:
- PreDecorationFilter:根据RouteLocator确定路由的位置和方式,它还为下游请求设置各种与代理相关的头
- Route filters:
- RibbonRoutingFilter:使用Ribbon、Hystrix和可插拔的HTTP Client客户端发送请求,在RequestContext 属性FilterConstants.SERVICE_ID_KEY中可以得到serviceId。这过滤器包含了不同的HTTP客户端:
- Apache HttpClient:也是默认的HTTP客户端
- Squareup OkHttpClient v3: 通过在类路径上设置com.squareup.okhttp3:okhttp库并设置Ribbonokhttp.enabled=true来启用。
- Netflix Ribbon HTTP client: 通过设置ribbon.restclient.enabled=true启用。但此客户端有一些限制,包括它不支持“PATCH ”方法,但是它也有内置的重试。
- SimpleHostRoutingFilter:通过Apache HttpClient向预定的URL发送请求,RequestContext.getRouteHost()包含发送的URLS
- RibbonRoutingFilter:使用Ribbon、Hystrix和可插拔的HTTP Client客户端发送请求,在RequestContext 属性FilterConstants.SERVICE_ID_KEY中可以得到serviceId。这过滤器包含了不同的HTTP客户端:
八、Zuul的高可用
Zuul做为微服务的网关,把外部请求路由到各个服务的,扮演着至关重要的作用。故在生产环境中高可用部署是必不可少的。
1、Zuul注册到了Eureka Server
Zuul注册到Eureka Server时,高可用就很简单了。只须将多个Zuul节点注册到Eureka Server上,就可以实习Zuul的高可用了。这种情况Zuul的高可用和一般的微服务高可用没有什么区别。
2、Zuul未注册Eureka Server
此种场景也更为常见,不可能让所有的客户端都注册到Eureka Server上。这时候,需要借助负载均衡器转发到每个Zuul节点来实现高可用,例如nginx、haproxy、F5等
参考:《Spring Cloud与Docker微服务架构实战》周立
参考: https://cloud.spring.io/spring-cloud-static/Finchley.SR2/single/spring-cloud.html#_router_and_filter_zuul