在微服务架构中,后端服务每每不直接开放给调用端,而是经过一个API网关根据请求的url,路由到相应的服务。当添加API网关后,在第三方调用端和服务提供方之间就建立了一面墙,这面墙直接与调用方通讯进行权限控制,后将请求均衡分发给后台服务端。前端
在微服务架构模式下后端服务的实例数通常是动态的,对于客户端而言很难发现动态改变的服务实例的访问地址信息。所以在基于微服务的项目中为了简化前端的调用逻辑,一般会引入 API Gateway 做为轻量级网关,同时 API Gateway 中也会实现相关的认证逻辑从而简化内部服务之间相互调用的复杂度。spring
一般而言不一样的客户端对于显示时对于数据的需求是不一致的,好比手机端或者 Web 端又或者在低延迟的网络环境或者高延迟的网络环境。后端
所以为了优化客户端的使用体验,API Gateway 能够对通用性的响应数据进行裁剪以适应不一样客户端的使用需求。同时还能够将多个 API 调用逻辑进行聚合,从而减小客户端的请求数,优化客户端用户体验.api
固然咱们还能够针对不一样的渠道和客户端提供不一样的API Gateway,对于该模式的使用由另一个你们熟知的方式叫Backend for front-end, 在Backend for front-end模式当中,咱们能够针对不一样的客户端分别建立其BFF,进一步了解BFF能够参考这篇文章:Pattern: Backends For Frontends安全
对于系统而言进行微服务改造一般是因为原有的系统存在或多或少的问题,好比技术债务,代码质量,可维护性,可扩展性等等。API Gateway的模式一样适用于这一类遗留系统的改造,经过微服务化的改造逐步实现对原有系统中的问题的修复,从而提高对于原有业务响应力的提高。经过引入抽象层,逐步使用新的实现替换旧的实现。网络
在Spring Cloud体系中, Spring Cloud Zuul就是提供负载均衡、反向代理、权限认证的一个API gateway。架构
Spring Cloud Zuul路由是微服务架构的不可或缺的一部分,提供动态路由,监控,弹性,安全等的边缘服务。Zuul是Netflix出品的一个基于JVM路由和服务端的负载均衡器。app
使用代码示例进行说明,这里直接将gateway服务化.负载均衡
引入依赖:ide
<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>
配置文件:
spring.application.name=zuul-server server.port=8888 eureka.client.service-url.defaultZone=http://localhost:8761/eureka/
启动类添加注解:
@SpringBootApplication @EnableZuulProxy public class ZuulServerDemoApplication { public static void main(String[] args) { SpringApplication.run(ZuulServerDemoApplication.class, args); } }
这样zuul就搭建成功了
和前面的项目进行结合,启动前面的注册中心和eureka的两个提供者,再将zuul-server启动,这样就能够进行测试了.访问localhost:8888/eureka-service-producter/hello?name=wangzhi,这样就是经过zuul来访问提供者,而且多刷新几回,能够看到实现了负载均衡,均匀出现. 感兴趣的能够将消费者也启动一下,而后访问消费者看看会出现什么状况.额外说一下前面路径中的eureka-service-producter其实就是访问项目的spring.applicaiton.name,也就是注册中心能够看到的application,这个看清楚就没问题.
到这里,zuul网关的使用和自动转发机制就完成了,下面说说zuul相对的高级知识.
Filter是Zuul的核心,用来实现对外服务的控制。Filter的生命周期有4个,分别是“PRE”、“ROUTING”、“POST”、“ERROR”,整个生命周期能够用下图来表示。
Zuul大部分功能都是经过过滤器来实现的,这些过滤器类型对应于请求的典型生命周期。
类型 | 顺序 | 过滤器 | 功能 |
pre | -3 | ServletDetectionFilter | 标记助理server的类型 |
pre | -2 | Servlet30WrapperFilter | 包装HttpServletRequest请求 |
pre | -1 | FormBodyWrapperFilter | 包装请求体 |
route | 1 | DebugFilter | 标记调试标志 |
route | 5 | PreDecorationFilter | 处理请求上下文供后续使用 |
route | 10 | RibbonRoutingFilter | serviceId请求转发 |
route | 100 | SimpleHostRoutingFilter | url请求转发 |
route | 500 | SendForwardFilter | forward请求转发 |
post | 0 | SendErrorFilter | 处理有错误的请求响应 |
post | 1000 | SendResponseFilter | 处理正常的请求响应 |
zuul.FormBodyWrapperFilter.pre.disable=true
其实也很简单,继承ZuulFilter就能够了,重写四个方法,可是要了解个四个方法的返回值分别表示的是什么意思?
public class MyFilter extends ZuulFilter { /** * 定义filter的类型,有pre、route、post、error四种 * @return */ @Override public String filterType() { return "pre"; } /** * 定义filter的顺序,数字越小表示顺序越高,越先执行 * @return */ @Override public int filterOrder() { return 10; } /** * 表示是否须要执行该filter,true表示执行,false表示不执行 * @return */ @Override public boolean shouldFilter() { return true; } /** * filter须要执行的具体操做,好比说权限认证,过滤等等 * @return * @throws ZuulException */ @Override public Object run() throws ZuulException { return null; } }
举个例子:
public class TokenFilter extends ZuulFilter { private final Logger logger = LoggerFactory.getLogger(TokenFilter.class); @Override public String filterType() { return "pre"; } @Override public int filterOrder() { return 0; } @Override public boolean shouldFilter() { return true; } @Override public Object run() throws ZuulException { RequestContext context = RequestContext.getCurrentContext(); HttpServletRequest request = context.getRequest(); logger.info("--->>> TokenFilter {},{}", request.getMethod(), request.getRequestURL().toString()); // 获取请求参数信息 String token = request.getParameter("token"); if (StringUtils.isNotBlank(token)) { //对请求进行路由 context.setSendZuulResponse(true); context.setResponseStatusCode(200); context.set("isSuccess", true); return null; } else { //不对其进行路由 context.setSendZuulResponse(false); context.setResponseStatusCode(400); context.setResponseBody("token is empty"); context.set("isSuccess", false); return null; } } }
要想在过滤器中起做用,还须要额外配置一个,在启动类中添加过滤器的Bean
@Bean public TokenFilter tokenFilter(){ return new TokenFilter(); }
启动注册中心,提供者和修改后的zuul,此次访问的时候就须要在路径后面加上token=xx,不然请求会被拦截.“PRE”类型的Filter作不少的验证工做,在实际使用中咱们能够结合shiro、oauth2.0等技术去作鉴权、验证。
咱们的后端服务出现异常的时候,咱们不但愿将异常抛出给最外层,指望服务能够自动进行一降级。Zuul给咱们提供了这样的支持。当某个服务出现异常时,直接返回咱们预设的信息。
咱们经过自定义的fallback方法,而且将其指定给某个route来实现该route访问出问题的熔断处理。主要继承ZuulFallbackProvider接口来实现,ZuulFallbackProvider默认有两个方法,一个用来指明熔断拦截哪一个服务,一个定制返回内容。
public interface ZuulFallbackProvider { /** * The route this fallback will be used for. * @return The route the fallback will be used for. */ public String getRoute(); /** * Provides a fallback response. * @return The fallback response. */ public ClientHttpResponse fallbackResponse(); }
实现类经过实现getRoute方法,告诉Zuul它是负责哪一个route定义的熔断。而fallbackResponse方法则是告诉 Zuul 断路出现时,它会提供一个什么返回值来处理请求。后来Spring又扩展了此类,丰富了返回方式,在返回的内容中添加了异常信息,所以最新版本建议直接继承类FallbackProvider。
案例:修改zuul-server-demo
建立HelloFallback
@Component public class HelloFallback implements FallbackProvider { private final Logger logger = LoggerFactory.getLogger(FallbackProvider.class); /** * 返回指定要处理的 service。 * @return */ @Override public String getRoute() { return "eureka-service-producter"; } @Override public ClientHttpResponse fallbackResponse(String route, Throwable cause) { if (cause != null && cause.getCause() != null) { String reason = cause.getCause().getMessage(); logger.info("Excption {}",reason); } return fallbackResponse(); } 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("The service is unavailable.".getBytes()); } @Override public HttpHeaders getHeaders() { HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); return headers; } }; } }
进行测试,启动注册中心,两个提供者,zuul. 输入http://localhost:8888/eureka-service-producter/hello?name=wangzhi&token=wangzhi进行访问,多刷新几回能够看到没有问题,这个时候将一个提供者关闭,再次刷新能够看到问题就出来了,返回The service is unavailable. 这个就是zuul的路由熔断. 可是注意的是: ** Zuul 目前只支持服务级别的熔断,不支持具体到某个URL进行熔断。**
有时候由于网络或者其它缘由,服务可能会暂时的不可用,这个时候咱们但愿能够再次对服务进行重试,Zuul也帮咱们实现了此功能,须要结合Spring Retry 一块儿来实现。下面咱们以上面的项目为例作演示。
添加依赖: <dependency> <groupId>org.springframework.retry</groupId> <artifactId>spring-retry</artifactId> </dependency> 添加配置: #是否开启重试功能 zuul.retryable=true #对当前服务的重试次数 ribbon.MaxAutoRetries=2 #切换相同Server的次数 ribbon.MaxAutoRetriesNextServer=0
修改controller方法就好,只修改一个项目的就好 private Logger logger = Logger.getLogger(HelloController.class); @GetMapping("hello") public String hello(@RequestParam String name){ logger.info("request two name is "+name); try{ Thread.sleep(1000000); }catch ( Exception e){ logger.error(" hello two error",e); } return "hello," + name + "222222"; }
此次访问,虽然还会返回The service is unavailable.可是能够在改动的提供者项目的控制台看到日志打印了3遍,说明重试了两次.
开启重试在某些状况下是有问题的,好比当压力过大,一个实例中止响应时,路由将流量转到另外一个实例,颇有可能致使最终全部的实例全被压垮。说到底,断路器的其中一个做用就是防止故障或者压力扩散。用了retry,断路器就只有在该服务的全部实例都没法运做的状况下才能起做用。这种时候,断路器的形式更像是提供一种友好的错误信息,或者伪装服务正常运行的假象给使用者。
不用retry,仅使用负载均衡和熔断,就必须考虑到是否可以接受单个服务实例关闭和eureka刷新服务列表之间带来的短期的熔断。若是能够接受,就无需使用retry。
对于微服务来讲,zuul也是很重要的,因此在必要状况下应该实现高可用,也就是搭建集群,和前面搭建集群同样道理没有什么区别.为了保证Zuul的高可用性,前端能够同时启动多个Zuul实例进行负载,在Zuul的前端使用Nginx或者F5进行负载转发以达到高可用性。
上面就是zuul的所有内容的,之后有须要再进行补充.