注意:本文的前提是基于zuul的1.3.X版本来解析的,2.0版本采用了netty作为底层框架重新设计了整个zuul的架构,将在后面进行分析。
zuul是什么
zuul是Netflix设计用来为所有面向设备、web网站提供服务的所有应用的门面,zuul可以提供动态路由、监控、弹性扩展、安全认证等服务,他还可以根据需求将请求路由到多个应用中。
zuul是用来解决什么问题的
在使用网关之前,动态的路由是通过Nginx的配置来做的,但是一旦发生改变,比如IP地址发生改变,加入其它路由,就要重新配置Nginx,重启Nginx。安全认证是放在每一个应用中,应用中包含了非业务强相关的内容,看起来也是不够优雅。
在目前的应用中,zuul主要用来做如下几件事情:
-
动态路由:APP、web网站通过zuul来访问不同的服务提供方,且与ribbon结合,还可以负载均衡的路由到同一个应用不同的实例中。
-
安全认证:zuul作为互联网服务架构中的网关,可以用来校验非法访问、授予token、校验token等。
-
限流:zuul通过记录每种请求的类型来达到限制访问过多导致服务down掉的目的。
-
静态响应处理:直接在zuul就处理一些请求,返回响应内容,不转发到微服务内部。
-
区域弹性:主要是针对AWS上的应用做一些弹性扩展。
zuul的最佳实践是怎样的
Netflix是如何使用zuul的?
可以看到,在Netflix的架构中,亚马逊的弹性负载均衡作为第一层,zuul作为第二层,为所有应用的网关,请求经过AWS的负载均衡先发送到zuul,zuul再将流量转发到各个API、website等等,然后各个API、website应用再调用各个微服务。
当然在Netflix的使用中,zuul也结合了Netflix的其他微服务组件一起使用。
-
Hystrix:用来服务降级及熔断
-
Ribbon:用来作为软件的负载均衡,微服务之间相互调用是通过ribbon作为软件负载均衡使用负载到微服务集群内的不同的实例
-
Turbin:监控服务的运行状况
-
Feign:用作微服务之间发送rest请求的组件,可以将rest调用类似spring的其他bean一样直接注入使用功能
-
Eureka:服务注册中心
入门示例
引入zuul
dependencies {
//spring-cloud-starter-netflix-zuul模块
implementation('org.springframework.cloud:spring-cloud-starter-netflix-zuul')
//test模块
testImplementation('org.springframework.boot:spring-boot-starter-test')
//引入eureka-client模块
implementation('org.springframework.cloud:spring-cloud-starter-netflix-eureka-client')
}
这里引入eureka-client是为了将zuul也注册到eureka中,作为微服务中的一个应用,那么zuul就能将eureka中注册的所有微服务应用的注册信息都拿到,从而做到动态路由。
启动类
@EnableDiscoveryClient
@EnableZuulProxy
@SpringBootApplication
public class ZuulApplication {
public static void main(String[] args) {
SpringApplication.run(ZuulApplication.class, args);
}
}
zuul的路由配置
zuul的配置类为ZuulProperties,配置前缀为zuul。
下面为路由的一些示例配置(未结合eureka):
zuul:
routes:
product: http://localhost:8082/product/**
cart: http://localhost:8081/cart/**
zuul服务引用了eureka-client后配置可以改成如下:
zuul:
routes:
product: /product/**
cart: /cart/**
或者更细致的配置:
zuul:
routes:
cart:
path: /cart/**
serviceId: cart
原访问地址:http://localhost:8081/cart/1
经过zuul后的访问地址:http://localhost:8080/cart/cart/1
访问效果:
注意:zuul只有结合了eureka,才会有ribbon作为软件负载均衡,直接配置逻辑URL,不会起到负载均衡的效果,也不会有hystrix作为熔断器使用。
如下:
zuul:
routes:
users:
path: /product/**
url: http://localhost:8082/product
如果想指定多个服务的列表且需要通过ribbon实现负载均衡,配置可参考如下:
zuul:
routes:
users:
path: /product/**
serviceId: product
ribbon:
eureka:
enabled: false
product:
ribbon:
listOfServers: example.com,google.com
当整合eureka时,配置简单,服务太多,不想一个个配置的时候,可以通过定义PatternServiceRouteMapper来全局定义匹配规则。
个人推荐这种方式,简单。
示例如下:
@Configuration
public class ZuuPatternServiceRouteMapperConfiguration {
//注意!!!这两者保留一个即可。
/**
* 没有版本号的路由匹配规则bean
* @retu没rn 路由匹配规则bean
*/
@Bean
public PatternServiceRouteMapper patternServiceRouteMapper(){
return new PatternServiceRouteMapper("(?<version>v.*$)","${name}");
}
/**
* 有版本号的路由匹配规则bean
* @return 路由匹配规则bean
*/
@Bean
public PatternServiceRouteMapper patternServiceRouteMapperWithVersion(){
return new PatternServiceRouteMapper("(?<name>.*)-(?<version>v.*$)","${version}/${name}");
}
}
全局添加映射前缀
通过zuul.prefix可以指定全局映射前缀,可以避免服务URL直接暴露出去,如前缀为/api(注意,这个”/“要加上,否则可能会出现404),默认情况下,代理前缀会在请求转发前从请求中删除前缀,可以通过zuul.stripPrefix来配置,默认是true。
zuul:
prefix: /api
strip-prefix: true
这样原来直接访问比如说cart服务的地址为http://localhost:8081/cart/1,通过zuul网关,且添加了全局映射前缀/api后的访问路径变为http://localhost:8080/api/cart/cart/1。
如果在局部路由想关闭这个删除前缀映射,可以通过以下配置指定:
zuul:
routes:
cart:
path: /cart/**
stripPrefix: false
不走路由的服务或者映射配置
ZuulProperties配置中,有两个属性ignoredServices,ignoredPatterns,属性类型均为LinkedHashSet。
ignoredServices:指定忽略某些服务的路由
ignoredPatterns:配置不走路由的某些路由匹配规则
zuul的filter
1.X版本的zuul实现是依赖于servlet的,所以对过滤器支持较好。zuul本身提供了较多的filter,如SendForwardFilter、DebugFilter、SendResponseFilter、SendErrorFilter等。
zuul本身也提供了抽象类ZuulFilter,供自定义filter。
- pre:在zuul按照规则路由到下游服务之前执行,如果需要对请求进行预处理,比如鉴权、限流等,应在此类型的过滤器实现。
- route:这类filter是zuul路由的具体执行者,是HTTPClient、OKhttp、或ribbon发送原始http请求的地方。
- post:这类filter是下游服务返回响应信息后执行的地方,如果需要对response做一些处理,可以考虑在此类过滤器实现。
- error:在整个生命周期类如果出现异常,则会进入此类过滤器,可作为全局异常处理。
自定义ZuulFilter
自定义ZuulFilter,需要实现几个方法,下面为前置过滤器的简单示例。
public class AuthenticationFilter extends ZuulFilter {
@Override
public String filterType() {
//fiterType,有pre、route、post、error四种类型,分别代表路由前、路由时
//路由后以及异常时的过滤器
return FilterConstants.PRE_TYPE;
}
@Override
public int filterOrder() {
//排序,指定相同filterType时的执行顺序,越小越优先执行,这里指定顺序为路由过滤器的顺序-1,即在路由前执行
return return FilterConstants.PRE_DECORATION_FILTER_ORDER - 1;
}
@Override
public boolean shouldFilter() {
//是否应该调用run()方法
return false;
}
@Override
public Object run() throws ZuulException {
HttpServletRequest request = RequestContext.getCurrentContext().getRequest();
String token = request.getHeader("X-Authentication");
if (StringUtils.isBlank(token)) {
throw new TokenNotValidException("token不存在!");
}
//校验token正确性
return null;
}
}
多个过滤器之间可能想要传输内容,那么可以通过zuul提供的RequestContext来完成,RequestContext本身也是依赖于ThreadLocal来完成的。RequestContext本身也携带了HttpServletRequest和HttpServletResponse,可以获取请求或响应中的内容。
zuul的过滤器也可以配置为不启用
通过zuul...disable=false来关闭指定过滤器。
例如:
zuul.SendResponseFilter.post.disable=true
@EnableZuulServer与@EnableZuulProxy的区别
在zuul的@EnableZuulProxy注解的源代码中,是这么说的,@EnableZuulProxy提供了一些基本的反向代理过滤器,@EnableZuulServer只是将zuul指定为一个zuul的server,并不提供任何反向代理的过滤器。一般我们推荐使用@EnableZuulProxy,如果不想用zuul自带的过滤器,可以通过上面的方式关闭指定的过滤器。
@EnableZuulProxy自带的filter如下:
-
pre类型:
-
ServletDetectionFilter
-
FormBodyWrapperFilter:解析请求form表单并在转发到下游请求前重新编码
-
DebugFilter:会打印执行每个过滤器的执行日志,但仅仅是打印”Debug.addRoutingDebug("Filter " + filter.filterType() + " " + filter.filterOrder() + " " + filterName);“
-
SendForwardFilter:将请求转发(路由)到下游服务
-
-
post类型:
- SendResponseFilter:将下游服务对请求的响应写入当前response中。
-
error类型:
- SendErrorFilter:处理异常情况的过滤器
zuul的http客户端
zuul实现路由,是通过http方式转发到其他服务的,那么就需要http客户端。默认情况下是通过Apache HTTP Client来发送http请求的,如果想使用restClient或者OKhttp可以通过配置指定。
ribbon:
okhttp:
enabled: true
或者
ribbon:
restclient:
enabled: true
当然可以自定义Apache HTTP Client和OkHttpClient并声明为Bean。
敏感header内容
zuul支持将一些敏感的header内容不转发到下游的其他服务中,通过zuul.sensitiveHeaders将这些header内容对下游服务屏蔽。默认情况下,header中的Cookie、Set-Cookie、Authorization将不被传递到下游服务器中,可以通过zuul.sensitiveHeaders指定全局的敏感header内容。所以在下游服务器想要获取到cookie内容时,这里需要重新配置下。个人建议,cookie、Authorization内容应该在zuul内将其转换成下游服务需要的userId、user等,再传递到下游服务器。
超时配置
zuul支持配置超时时间,如果使用的是eureka作为服务注册中心,那么只要指定ribbon的超时时间即可。即ribbon.ReadTimeout和ribbon.SocketTimeout。如果使用的是具体的URL路由,那么通过zuul.host.connect-timeout-millis和zuul.host.socket-timeout-millis指定。zuulProperties内部类Host中还有一些诸如maxTotalConnections最大连接数等的一些配置。
CORS配置
默认情况下,zuul是允许所有cors请求的,如果要自定义cors,可以通过自定义WebMvcConfigurer来完成。
如:
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/*")
.allowedOrigins("http://baidu.com");
}
}
其他配置
重试
zuul.retryable可以配置zuul是否使用ribbon的重试机制。
代理请求header
zuul.addProxyHeaders可以配置是否将X-Forwarded-Host放入转发的请求中
zuul的实现原理及设计是怎样的
了解了zuul的使用方式,我们要开始了解下zuul的源码以及zuul的整个架构设计。
先从启动类ZuulApplication入手。
@EnableDiscoveryClient
@EnableZuulProxy
@SpringBootApplication
public class ZuulApplication {
public static void main(String[] args) {
SpringApplication.run(ZuulApplication.class, args);
}
}
可以看到启动类上加了注解@EnableZuulProxy,我们看下这个注解的源码。
@EnableCircuitBreaker
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Import(ZuulPr