一、为什么需要Zuul?
Zuul 作为微服务系统的网关组件,用于构建边界服务( Edge Service ),致力于动态路由、过滤、监控、弹性伸缩和安全。Zuul 作为路由网关组件,在微服务架构中有着非常重要的作用,主要体现在以下6个方面。
1)Zuul Ribbon 以及 Eureka 相结合,可以实现智能路由和负载均衡的功能, Zuul 能够将请求流量按某种策略分发到集群状态的多个服务实例。
2)网关将所有服务的 API 接口统一聚合,并统一对外暴露。外界系统调用 API 接口时,都是由网关对外暴露的 API 接口,外界系统不需要知道微服务系统中各服务相互调用的复杂性。微服务系统 保护了其内部微服务单元的 API 接口 防止其被外界直接调用,导致服务的敏感信息对外暴露。
3)网关服务可以做用户身份认证和权限认证,防止非法请求操作 API 接口,对服务器起到保护作用。
4)网关可以实现监控功能,实时日志输出,对请求进行记录。
5)网关可以用来实现流量监控。在高流量的情况下,对服务进行降级。
6)API 接口从内部服务分离出来 方便做测试。
二、Zuul的工作原理
Zuul 是通过 Servlet 来实现的, Zuul 通过自定义的 Zuul Servlet (类似于 Spring MVC的DispatcServlet 〕来对请求进行控制。 Zuul 的核心是一系列过滤器,可以在 Http 请求的发起和响应返回期间执行一系列的过滤器。 Zuul 包括以下4种过滤器。
1)PRE 过滤器:它是在请求路由到具体的服务之前执行的,这种类型的过滤器可以做安全验证,例如身份验证、 参数验证等。
2)ROUTING 过滤器:它用于将请求路由到具体的微服务 。在默认情况下,它使用Http Client 进行网络请求。
3)POST 过滤器:它是在请求已被路由到微服务后执行的一般情况下,用作收集统计信息、指标,以及将响应传输到客户端。
4)ERROR 过滤器:它是在其他过滤器发生错误时执行的。
1)Type (类型):Zuul 过滤器的类型,这个类型决定了过滤器在请求的哪个阶段起作用,例如 Pre Post 阶段等。
2)Execution Order (执行顺序) :规定了过滤器的执行顺序, Order 的值越小,越先执行
3)Criteria (标准):Filter 行所需的条件。
4)Action (行动):如果符合执行条件,则执行 Action (即逻辑代码)。
当一个客户端 Request 请求进入 Zuul网关服务时,网关先进入“pre filter ,进行一系列的验证、操作或者判断。然后交给“routing filter ”进行路由转发,转发到具体的服务实例进行逻辑处理、返回数据。当具体的服务处理完后,最后由“post filter进行处理,该类型的处理器处理完之后,将 Response 信息返回给客户端。
ZuulServlet是Zuul的核心Servlet,ZuulServlet的作用是初始化ZuulFilter。并编排这些顺序,具体的逻辑在service()方法中
public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException { try { ... try { this.preRoute(); } catch (ZuulException var12) { this.error(var12); this.postRoute(); return; } try { this.route(); } catch (ZuulException var13) { this.error(var13); this.postRoute(); return; } try { this.postRoute(); } catch (ZuulException var11) { this.error(var11); } } catch (Throwable var14) { this.error(new ZuulException(var14, 500, "UNHANDLED_EXCEPTION_" + var14.getClass().getName())); } finally { RequestContext.getCurrentContext().unset(); } }
三、搭建具体的Zuul应用服务。
1)加入依赖:
<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> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency>
2)编写启动程序加上@EnableZuulProxy注解。
package com.cetc; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.netflix.eureka.EnableEurekaClient; import org.springframework.cloud.netflix.zuul.EnableZuulProxy; @SpringBootApplication @EnableEurekaClient @EnableZuulProxy public class ZuulApplication { public static void main(String[] args) { SpringApplication.run(ZuulApplication.class, args); } }
3)加入配置(服务端口为8682):
server: port: 8682 spring: application: name: zuul eureka: client: service-url: defaultZone: http://127.0.0.1:8670/eureka/ # 实际开发中建议使用域名的方式 zuul: routes: test: path: /test/** url: http://127.0.0.1:8673/ ribbon: path: /ribbon/** serviceId: rest feign: path: /feign/** serviceId: feign
说明:上面的为基本配置:
zuul.routes.test/ribbon/feign:中的test/ribbon/feign为自定义配置。
path:为Zuul中代理的路径。
serviceId:为服务的名称。
url:为具体的地址,不存在负载均衡的问题。(主要用于外部服务加入)
4)测试:启动项目包含Eureka-Server, 2个Eureka-Client,Ribbon中的Rest,Feign和Zuul。端口分别为:8670、8673/8674、8675、8676、8682。
效果如下:
测试接口为:
http://127.0.0.1:8682/test/api/test/getPort
http://127.0.0.1:8682/ribbon/api/ribbon/getPort
http://127.0.0.1:8682/feign/api/feign/getPort
可见这里的访问:使用url的方式默认没有负载均衡的,所以不建议使用,使用ribbon和feign两种方式都是可以进行负载均衡的。
5)如果想使用url的方式来做负载均衡那么就要自己维护访问列表。配置如下:
zuul: routes: test-r: path: /test-r/** serviceId: test-r test-r: ribbon: listOfServers: http://127.0.0.1:8673/, http://127.0.0.1:8674/ ribbon: eureka: enabled: false
说明:这里禁用ribbon不影响前面配置好的服务。但是会影响Zuul调用ribbon的rest或者feign服务,如果这两个服务存在负载均衡,那么调用的时候就存在负载均衡问题。所以一般建议不要这样使用。
测试:
6)如果想加入具体的版本号,可以加入如下配置:
zuul: prefix: /v1
访问方式为:http://127.0.0.1:8682/v1/test/api/test/getPort。在链接中加入版本号。
四、Zuul加入熔断器:
默认实现FallbackProvider接口,加入容器即可
package org.springframework.cloud.netflix.zuul.filters.route; import org.springframework.http.client.ClientHttpResponse; public interface FallbackProvider { String getRoute(); ClientHttpResponse fallbackResponse(String var1, Throwable var2); }
实现为:
package com.cetc.config; import org.springframework.cloud.netflix.zuul.filters.route.FallbackProvider; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.client.ClientHttpResponse; import org.springframework.stereotype.Component; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; @Component public class ZuulFallbackProvider implements FallbackProvider { @Override public String getRoute() { return "*"; } @Override public ClientHttpResponse fallbackResponse(String route, Throwable cause) { 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("error".getBytes()); } @Override public HttpHeaders getHeaders() { HttpHeaders httpHeaders = new HttpHeaders(); httpHeaders.setContentType(MediaType.APPLICATION_JSON_UTF8); return httpHeaders; } }; } }
测试:关闭feign测试接口为:
五、在Zuul中自定义filter:
package com.cetc.filter; import com.netflix.zuul.ZuulFilter; import com.netflix.zuul.context.RequestContext; import com.netflix.zuul.exception.ZuulException; import org.springframework.cloud.netflix.zuul.filters.support.FilterConstants;import org.springframework.http.HttpStatus; import org.springframework.stereotype.Component; import org.springframework.util.StringUtils; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; @Component public class CustomZuulFilter extends ZuulFilter { @Override public String filterType() { return FilterConstants.PRE_TYPE; } @Override public int filterOrder() { return 0; } @Override public boolean shouldFilter() { //是否开启过滤逻辑,开启后运行run()方法 return true; } @Override public Object run() throws ZuulException { RequestContext requestContext = RequestContext.getCurrentContext(); HttpServletRequest request = requestContext.getRequest(); String token = request.getParameter("token"); if (StringUtils.isEmpty(token)) { HttpServletResponse response = requestContext.getResponse(); requestContext.setSendZuulResponse(false); requestContext.setResponseStatusCode(HttpStatus.UNAUTHORIZED.value()); try { response.getWriter().write("token is empty"); } catch (IOException e) { e.printStackTrace(); } } return null; } }
测试:
是不是感觉这类有点验证的味道了。通过自定义的过滤就能达到请求过滤的目的。实现过程如下:
六、Zuul的常见使用方式。
Zuul 是采用了类似于 Spring MVC DispatchServlet 来实现的,采用的是异步阻塞模型,所以性能比 Ngnix 差。由于 Zuul和其他 Netflix 组件可以相互配合、无缝集成 Zuul 很容易就能实现负载均衡、智能路由和熔断器等功能。在大多数情况下 Zuul 都是以集群的形式在的。由于Zuul的横向扩展能力非常好,所以当负载过高时,可以通过添加实例来解决性能瓶颈。
1)一种常见的使用方式是对不同的渠道使用不同的 Zuul 来进行路由,例如移动端共用Zuul一个网关实例。Web 端用另一个Zuul 网关实例,其他的客户端用另外一个Zuul 实例进行路由。
2)另外一种常见的集群是通过 Ngnix和Zuul 相互结合来做负载均衡。暴露在最外面的是Ngnix 主从双热备进行 Keepalive, Ngnix 经过某种路由策略,将请求路由转发到 Zuul 集群上,Zuul 最终将请求分发到具体的服务上。