- 《深入理解 Spring Cloud 与微服务构建》第十章 路由网关 Spring Cloud Zuul
- 一、Zuul 简介
- 二、Zuul 的工作原理
- 三、案例实战
- 1.搭建 Zuul 服务
- 2.在 Zuul 上配置 API 接口的版本号
- 3.在 Zuul 上配置熔断器
- 4.在 Zuul 中使用过滤器
- 5.Zuul 的常见使用方式
Zuul 作为微服务系统的网管组件,用于构建边界服务(Edge Service),致力于动态路由、过滤、监控、d性伸缩和安全。Zuul 在微服务架构中的重要作用主要体现在以下 6 个方面:
- Zuul、Ribbon 以及 Eureka 相结合,可以实现智能路由和负载均衡的功能,Zuul 能够将请求流量按某种策略分发到集群状态的多个服务实例
- 网关将所有服务的 API 接口统一聚合,并统一对外暴露。外界系统调用 API 接口时,都是由网关对外暴露的 API 接口,外界系统不需要知道微服务系统中各服务相互调用的复杂性。微服务系统也保护了其内部微服务单元的 API 接口,防止其被外界直接调用,导致服务的敏感信息对外暴露
- 网关服务可以做到用户身份认证和权限认证,防止非法请求 *** 作 API 接口,对服务器起到保护作用
- 网关可以实现监控功能,实时日志输出,对请求进行记录
- 网关可以用来实现流量监控,在高流量的情况下,对服务进行降级
- API 接口从内部服务分离出来,方便做测试
Zuul 是通过 Servlet 来实现的,Zuul 通过自定义的 ZuulServlet(类似于 Spring MVC 的 DispatchServlet)来对请求进行控制。Zuul 的核心是一系列过滤器,可以在 HTTP 请求的发起和响应期间执行一系列的过滤器。Zuul 包括以下 4 种过滤器:
- PRE 过滤器:它是在请求路由到具体的服务之前执行的,这种类型的过滤器可以做安全验证,例如身份验证、参数验证等
- ROUTING 过滤器:它用于将请求路由到具体的微服务实例。在默认情况下,它使用 Http Client 进行网络请求
- POST 过滤器:它是在请求已被路由到微服务后执行的。一般情况下,用作收集统计信息、指标,以及将响应传输到客户端
- ERROR 过滤器:它是在其它过滤器发生错误时执行的
Zuul 采取了动态读取、编译和运行这些过滤器。过滤器之间不能直接相互通信,而是通过 RequestContext 对象来共享数据,每个请求都会创建一个 RequestContext 对象。Zuul 过滤器具有以下关键特性:
- Type(类型):Zuul 过滤器的类型,这个类型决定了过滤器在请求的那个阶段起作用,例如 Pre、Post 阶段等
- Execution Order(执行顺序):规定了过滤器的执行顺序,Order 的值越小,越先执行
- Criteria(标准):过滤器执行所需的条件
- Action(行动):如果服务执行条件,则执行 Action(即逻辑代码)
Zuul 请求的生命周期如图所示:
当一个客户端 Request 请求进入 Zuul 网关服务时,网关先进入 “pre filter”,进行一系列的验证、 *** 作或者判断。然后交给 “routing filter” 进行路由转发,转发到具体的服务实例进行逻辑处理、返回数据。当具体的服务处理完后,最后由 “post filter” 进行处理,该类型的处理器处理完之后,将 Response 信息返回给客户端
ZuulServlet 是 Zuul 的核心 Servlet。ZuulServlet 的作用是初始化 ZuulFilter,并编排这些 ZuulFilter 的执行顺序。该类中有一个 service() 方法,执行了过滤器执行的逻辑
public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException { try { this.init((HttpServletRequest)servletRequest, (HttpServletResponse)servletResponse); RequestContext context = RequestContext.getCurrentContext(); context.setZuulEngineRan(); 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(); } }
从上面的代码可知,首先执行 preRoute() 方法,这个方法执行的是 PRE 类型的过滤器的逻辑。如果执行这个方法时出错了,那么会执行 error(e) 和 postRoute()。然后执行 route() 方法,该方法是执行 ROUTING 类型过滤器的逻辑。最后执行 postRoute(),该方法执行了 POST 类型过滤器的逻辑
三、案例实战 1.搭建 Zuul 服务本案例基于上一章案例的基础上进行搭建,新建一个 Spring Boot 工程,取名为 eureka-zuul-client,在 pom 文件中引入相关依赖,包括继承了主 Maven 工程的 pom 文件,引入 Eureka Client 的起步依赖 spring-cloud-starter-netflix-eureka-client、Zuul 的起步依赖 spring-cloud-starter-netflix-zuul、Web 功能的起步依赖 spring-boot-starter-web,以及 Spring Boot 测试的起步依赖 spring-boot-starter-test。代码如下:
Eureka org.sisyphus 1.0-SNAPSHOT 4.0.0 eureka-zuul-clientorg.springframework.cloud spring-cloud-starter-netflix-eureka-clientorg.springframework.cloud spring-cloud-starter-netflix-zuulorg.springframework.boot spring-boot-starter-weborg.springframework.boot spring-boot-starter-test
在程序的启动类 EurekaZuulClientApplication 加上 @EnableEurekaClient 注解,开启 EurekaClient 的功能;加上 @SpringBootApplication 注解,表明自己是一个 Spring Boot 工程;加上 @EnableZuulProxy 注解,开启 Zuul 的功能。代码如下:
package com.sisyphus; 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; @EnableZuulProxy @EnableEurekaClient @SpringBootApplication public class EurekaZuulClientApplication { public static void main(String[] args) { SpringApplication.run(EurekaZuulClientApplication.class, args); } }
在工程的配置文件 application.yml 中做相关的配置,包括配置服务注册中心的地址为 http://localhost:8761/eureka,程序的端口号为 5000,程序名为 service-zuul
在本案例中,zuul.routes.hiapi.path 为 “/hiapi/**”,zuul.routes.hiapi.serviceId 为 “eureka-client”,这两个配置就可以将以 “/hiapi” 开头的 Url 路由到 eureka-client 服务。其中,zuul.routes.hiapi 中的 “hiapi” 是自己定义的,需要指定它的 path 和 serviceId,两者配合使用,就可以将指定类型的请求 Url 路由到指定的 ServiceId。同理,满足以 “/ribbonapi” 开头的请求 Url 都会被分发到 eureka-ribbon-client,满足以 “/feignapi” 开头的请求 Url 都会被分发到 eureka-feign-client 服务。如果某服务存在多个实例,Zuul 结合 Ribbon 会做负载均衡,将请求均分的部分路由到不同的服务实例
eureka: client: serviceUrl: defaultZone: http://localhost:8761/eureka/ server: port: 5000 spring: application: name: service-zuul zuul: routes: hiapi: path: /hiapi/** serviceId: eureka-ribbon-client ribbonapi: path: /ribbonapi/** serviceId: eureka-ribbon-client feignapi: path: /feignapi/** serviceId: eureka-feign-client
依次启动工程 eureka-server、eureka-client、eureka-ribbon-client、eureka-feign-client 和 eureka-zuul-client,其中 eureka-client 启动两个实例,端口为 8762 和 8763。在浏览器上多次访问 http://localhost:5000/hiapi/hi?name=sisyphus,浏览器会交替显示以下内容:
hi sisyphus,i am from port:8762 hi sisyphus,i am from port:8763
可见 Zuul 在路由转发做了负载均衡。同理,多次访问 http://localhost:5000/feignapi/hi?name=sisyphus 和 http://localhost:5000/ribbonapi/hi?name=sisyphus,也可以看到相似的内容
如果不需要用 Ribbon 做负载均衡,可以指定服务实例的 Url,用 zuul.routes.hiapi.url 配置指定,这时就不需要配置 zuul.routes.hiapi.serviceId 了。一旦指定了 Url,Zuul 就不能做负载均衡了,而是直接访问指定的 Url,在实际的开发中这种做法是不可取的。修改配置的代码如下:
zuul: routes: hiapi: path: /hiapi/** url: http://localhost:8762
重新启动 eureka-zuul-service 服务,请求 http://localhost:5000/hiapi/hi?name=sisyphus,浏览器只会显示以下内容:
hi sisyphus,i am from port:8762
如果你想指定 Url,并且想做负载均衡,那么就需要自己维护负载均衡的服务注册列表。首先,将 ribbon.eureka.enable 改为 false,即 Ribbon 负载均衡客户端不向 Eureka Client 获取服务注册列表信息。然后需要自己维护一份注册列表,该注册列表对应的服务名为 hiapi-v1(这个名字可自定义),通过配置 hiapi-v1.ribbon.listOfServers 来配置多个负载均衡的 Url。代码如下:
zuul: routes: hiapi: path: /hiapi/** serviceId: hiapi-v1 ribbon: eureka: enabled: false hiapi-v1: ribbon: listOfServers: http://localhost:8762,http://localhost:8763
重新启动 eureka-zuul-service 服务,在浏览器上访问 http://localhost:5000/hiapi/hi?name=sisyphus,浏览器会交替显示如下内容:
hi sisyphus,i am from port:8762 hi sisyphus,i am from port:87632.在 Zuul 上配置 API 接口的版本号
如果想给每一个服务的 API 接口加前缀,例如 http://localhost:5000/v1/hiapi/hi?name=sisyphus/,即在所有的 API 接口上加一个 v1 作为版本号。这时需要用到 zuul.prefix 的配置,配置示例代码如下:
zuul: routes: hiapi: path: /hiapi/** serviceId: hiapi-v1 ribbonapi: path: /ribbonapi/** serviceId: eureka-ribbon-client feignapi: path: /feignapi/** serviceId: eureka-feign-client prefix: /v1
重新启动 eureka-zuul-service 服务,在浏览器上访问 http://localhost:5000/v1/hiapi/hi?name=sisyphus,浏览器会交替显示:
hi sisyphus,i am from port:8762 hi sisyphus,i am from port:87633.在 Zuul 上配置熔断器
Zuul 作为 Netflix 组件,可以与 Ribbon、Eureka 和 Hystrix 等组件相结合,实现负载均衡、熔断器的功能。在默认情况下,Zuul 和 Ribbon 相结合,实现了负载均衡的功能。下面来讲解如何在 Zuul 上实现熔断功能
在 Zuul 中实现熔断功能需要实现 FallbackProvider 的接口。实现该接口有两个方法,一个是 getRoute() 方法,用于指定熔断功能应用于哪些路由的服务;另一个方法 fallbackResponse() 为进入熔断功能时执行的逻辑。ZuulFallbackProvider 的源码如下:
public interface FallbackProvider { String getRoute(); ClientHttpResponse fallbackResponse(String route, Throwable cause); }
实现一个针对 eureka-client 服务的熔断器,当 eureka-client 的服务出现故障时,进入熔断逻辑,向浏览器输入一句错误提示
package com.fallbackProvider; 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 MyFallbackProvider implements FallbackProvider { @Override public String getRoute() { return "eureka-client"; } @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("oooops!error!i am the fallback.".getBytes()); } @Override public HttpHeaders getHeaders() { HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); return headers; } }; } }
重新启动 eureka-zuul-client 工程,并且关闭 eureka-client 的所有实例,在浏览器上访问 http://localhost:5000/hiapi/hi?name=sisyphus,浏览器会显示:
oooops!error!i am the fallback.
如果需要所有的路由服务都加熔断功能,只需要在 getRoute() 方法上返回 “*” 的匹配符,代码如下:
@Override public String getRoute() { return "*"; }4.在 Zuul 中使用过滤器
在前面的章节讲述了过滤器的作用和种类,下面来讲解如何实现一个自定义的过滤器。实现过滤器很简单,只需要继承 ZuulFilter,并实现 ZuulFilter 中的抽象方法,包括 filterType() 和 filterOrder(),以及 IZuulFilter 的 shouldFilter,并实现 ZuulFilter 中的抽象方法,包括 filterType() 和 filterOrder(),以及 ZuulFilter 的 shouldFilter() 和 Object run() 的两个方法。其中,filterType() 即过滤器的类型,有 4 中类型,分别是 “pre”、“post”、“routing” 和 “error”。filterOrder() 是过滤顺序,它是一个 Int 类型的值,值越小,越早执行该过滤器。shouldFilter() 表示该过滤器是否过滤逻辑,如果为 true,则执行 run() 方法;如果为 false,则不执行 run() 方法。run() 方法写具体的过滤的逻辑。在本例中,检查请求的参数中是否穿了 token 这个参数,如果没有传,则请求不被路由到具体的服务实例,直接返回响应,状态码为 401.代码如下:
package com.sisyphus.filter; import com.netflix.zuul.ZuulFilter; import com.netflix.zuul.context.RequestContext; import com.netflix.zuul.exception.ZuulException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Component; import javax.servlet.http.HttpServletRequest; import static org.springframework.cloud.netflix.zuul.filters.support.FilterConstants.PRE_TYPE; @Component public class MyFilter extends ZuulFilter { private static Logger log = LoggerFactory.getLogger(MyFilter.class); @Override public String filterType() { return PRE_TYPE; } @Override public int filterOrder() { return 0; } @Override public boolean shouldFilter() { return true; } @Override public Object run() throws ZuulException { RequestContext ctx = RequestContext.getCurrentContext(); HttpServletRequest request = ctx.getRequest(); Object accessToken = request.getParameter("token"); if(accessToken == null){ log.warn("token is empty"); ctx.setSendZuulResponse(false); ctx.setResponseStatusCode(401); try{ ctx.getResponse().getWriter().write("token is empty"); }catch (Exception e){ return null; } } log.info("ok"); return null; } }
重新启动服务,打开浏览器,访问 http://localhost:5000/hiapi/hi?name=sisyphus,浏览器显示:
token is empty
再次在浏览器上输入 http://localhost:5000/hiapi/hi?name=sisyphus&token=token,即加上了 token 这个请求参数,浏览器显示:
hi sisyphus,i am from port:8762
可见,MyFilter 这个 Bean 注入 IoC 容器之后,对请求进行了过滤,并在请求路由转发之前进行了逻辑判断。在实际开发中,可以用此过滤器进行安全验证
5.Zuul 的常见使用方式Zuul 是采用了类似于 Spring MVC 的 DispatchServlet 来实现的,采用的是异步阻塞模型,所以性能比 Nginx 差。由于 Zuul 和其它 Netflix 组件可以相互配合、无缝集成,Zuul 很容易就能实现负载均衡、智能路由和熔断器等功能。在大多数情况下,Zuul 都是以集群的形式存在的。由于 Zuul 的横向扩展能力非常好,所以当负载过高时,可以通过添加实例来解决性能瓶颈
一种常见的使用方式是对不同的渠道使用不同的 Zuul 来进行路由,例如移动端用一个 Zuul 网关实例,WEB 端用一个 Zuul 网关实例,其它的客户端用一个 Zuul 实例进行路由
另一种常见的集群是通过 Nginx 和 Zuul 相互结合来做负载均衡。暴露在最外面的是 Nginx 主从双热备进行 Keepalive,Nginx 经过某种路由策略,将请求路由转发到 Zuul 集群上,Zuul 最终将请求分发到具体的服务上
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)