微服务深入浅出(7)-- 网关路由Zuul
Zuul用于构建边界服务,致力于动态路由,过滤,监控,弹性伸缩和安全等方向。
1、Zuul+Ribbon+Eureka结合,可以实现智能路由和负载均衡
2、网关将所有服务的API接口统一聚合统一暴露
3、网关统一爆率接口后,可以做身份和权限认证
4、实现监控功能,实时日志输出
5、流量监控,实现降级和限流
6、方便测试
1、网关存在的必要性
不同的微服务有不同的请求地址,如果一个客户端需要访问多个接口才能完成一个业务需求的话,可能存在以下问题:
# 客户端会多次请求不同微服务,增加客户端的复杂性
# 存在跨域请求,在一定场景下处理相对复杂
# 认证复杂,每一个服务都需要独立认证
# 难以重构,随着项目的迭代,可能需要重新划分微服务,如果客户端直接和微服务通信,那么重构会难以实施
# 某些微服务可能使用了其他协议,直接访问有一定困难
而微服务网关可以很好的解决这个问题:
这样客户端只需要和网关交互,而无需直接调用特定微服务的接口,而且方便监控,易于认证,减少客户端和各个微服务之间的交互次数。
2、主流解决方案
# Spring Cloud Gateway
# Zuul
Zuul基于 servlet 2.5(使用3.x),使用阻塞API。 它不支持任何 长连接 ,如 web sockets。而Gateway建立在Spring Framework 5,Project Reactor和Spring Boot 2之上,使用非阻塞API。 Websockets得到支持,并且由于它与Spring紧密集成,所以将会是一个更好的开发体验。
参考:https://juejin.im/post/5aa4eacbf265da237a4ca36f
3、模拟场景
客户端请求后端服务,网关提供后端服务的统一入口。后端的服务都注册到Zookeeper、Consul或者Eureka (服务发现、配置管理中心服务)。网关通过负载均衡。转发到具体的后端服务。
4、Zuul
Zuul 提供了四种过滤器的 API,动态读取、编译和运行这些过滤器。过滤器之间不能相互通讯,只能通过RequestContext对象共享数据。
# 前置(Pre)鉴权、请求转发、增加请求参数等行为
一般来说整个服务的鉴权逻辑可以很复杂。
- 客户端:App、Web、Backend
- 权限组:用户、后台人员、其他开发者
- 实现:OAuth、JWT
- 使用方式:Token、Cookie、SSO
而对于后端应用来说,它们其实只需要知道请求属于谁,而不需要知道为什么,所以 Gateway 可以友善的帮助后端应用完成鉴权这个行为,并将用户的唯一标示透传到后端,而不需要、甚至不应该将身份信息也传递给后端,防止某些应用利用这些敏感信息做错误的事情。Zuul 默认情况下在处理后会删除请求的 Authorization
头和 Set-Cookie
头,也算是贯彻了这个原则。
# 后置(Post)统计返回值和调用时间、记录日志、增加跨域头等行为
使用 Gateway 做跨域相比应用本身或是 Nginx 的好处是规则可以配置的更加灵活。例如一个常见的规则。
-
对于任意的 AJAX 请求,返回
Access-Control-Allow-Origin
为*
,且Access-Control-Allow-Credentials
为true
,这是一个常用的允许任意源跨域的配置,但是不允许请求携带任何 Cookie -
如果一个被信任的请求者需要携带 Cookie,那么将它的
Origin
增加到白名单中。对于白名单中的请求,返回Access-Control-Allow-Origin
为该域名,且Access-Control-Allow-Credentials
为true
,这样请求者可以正常的请求接口,同时可以在请求接口时携带 Cookie -
对于 302 的请求,即使在白名单内也必须要设置
Access-Control-Allow-Origin
为*
,否则重定向后的请求携带的Origin
会为null
,有可能会导致 iOS 低版本的某些兼容问题
Gateway 可以统一收集所有应用请求的记录,并写入日志文件或是发到监控系统,相比 Nginx 的 access log,好处主要也是二次开发比较方便,比如可以关注一些业务相关的 HTTP 头,或是将请求参数和返回值都保存为日志打入消息队列中,便于线上故障调试。也可以收集一些性能指标发送到类似 Statsd 这样的监控平台。
# 路由(Route)一般只需要选择 Zuul 中内置的即可
#错误(Error)一般只需要一个,这样可以在 Gateway 遇到错误逻辑时直接抛出异常中断流程,并直接统一处理返回结果
错误过滤器的主要用法就像是 Jersey 中的 ExceptionMapper
或是 Spring MVC 中的 @ExceptionHandler
一样,在处理流程中认为有问题时,直接抛出统一的异常,错误过滤器捕获到这个异常后,就可以统一的进行返回值的封装,并直接结束该请求。
总结关键特性:
1、Type,规定类型
2、Execution Order,规定执行顺序,Order值越小越优先
3、Criteria,规定执行所需要的条件
4、Action,如果符合条件,则执行Action
一个请求会先按顺序通过所有的前置过滤器,之后在路由过滤器中转发给后端应用,得到响应后又会通过所有的后置过滤器,最后响应给客户端。在整个流程中如果发生了异常则会跳转到错误过滤器中。
5、注解配置
/**
* 这个接口需要鉴权,鉴权方式是 OAuth
*/
@Authorization(OAuth)
@RequestMapping(value = "/users/{id}", method = RequestMethod.DELETE)
public void del(@PathVariable int id) {
//...
}
/**
* 这个接口可以缓存,并且每个 IP/User 每秒最多请求 10 次
*/
@Cacheable
@RateLimiting(limit = "10/1s", scope = {IP, USER})
@RequestMapping(value = "/users/{id}", method = RequestMethod.GET)
public void info(@PathVariable int id) {
//...
}
6、稳定性
# 隔离机制
在 Zuul 中,每一个后端应用都称为一个 Route,为了避免一个 Route 抢占了太多资源影响到其他 Route 的情况出现,Zuul 使用 Hystrix 对每一个 Route 都做了隔离和限流。
Hystrix 的隔离策略有两种,基于线程或是基于信号量。Zuul 默认的是基于线程的隔离机制,这意味着每一个 Route 的请求都会在一个固定大小且独立的线程池中执行,这样即使其中一个 Route 出现了问题,也只会是某一个线程池发生了阻塞,其他 Route 不会受到影响。一般使用 Hystrix 时,只有调用量巨大会受到线程开销影响时才会使用信号量进行隔离策略,对于 Zuul 这种网络请求的用途使用线程隔离更加稳妥。
# 重试机制
Zuul 的路由主要有 Eureka 和 Ribbon 两种方式,简单介绍下 Ribbon 支持哪些容错配置。
重试的场景分为三种:
-
okToRetryOnConnectErrors
:只重试网络错误 -
okToRetryOnAllErrors
:重试所有错误 -
OkToRetryOnAllOperations
:重试所有操作(这里不太理解,猜测是 GET/POST 等请求都会重试)
重试的次数有两种:
-
MaxAutoRetries
:每个节点的最大重试次数 -
MaxAutoRetriesNextServer
:更换节点重试的最大次数
一般来说我们希望只在网络连接失败时进行重试、或是对 5XX 的 GET 请求进行重试(不推荐对 POST 请求进行重试,无法保证幂等性会造成数据不一致)。单台的重试次数可以尽量小一些,重试的节点数尽量多一些,整体效果会更好。
如果有更加复杂的重试场景,例如需要对特定的某些 API、特定的返回值进行重试,那么也可以通过实现 RequestSpecificRetryHandler
定制逻辑(不建议直接使用 RetryHandler
,因为这个子类可以使用很多已有的功能)。
7、Tomcat
Tomcat的最大并发数是可以配置的,实际运用中,最大并发数与硬件性能和CPU数量都有很大关系的。更好的硬件,更多的处理器都会使Tomcat支持更多的并发。
Tomcat 默认的HTTP实现是采用阻塞式的Socket通信,每个请求都需要创建一个线程处理,当一个进程有500个线程在跑的话,那性能已经是很低很低了。Tomcat默认配置的最大请求数是150,也就是说同时支持150个并发。具体能承载多少并发,需要看硬件的配置,CPU越多性能越高,分配给JVM的内存越多性能也就越高,但也会加重GC的负担。当某个应用拥有 250个以上并发的时候,应考虑应用服务器的集群。操作系统对于进程中的线程数有一定的限制:
Windows 每个进程中的线程数不允许超过 2000
Linux 每个进程中的线程数不允许超过 1000
在Java中每开启一个线程需要耗用1MB的JVM内存空间用于作为线程栈之用,此处也应考虑。
8、实际应用
引入依赖
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-zuul</artifactId> </dependency>
启动类开启zuul代理:
@SpringBootApplication @EnableEurekaClient @EnableZuulProxy public class DemoApplication { public static void main(String[] args) { SpringApplication.run(DemoApplication.class, args); } }
配置文件配置路由信息:
server: port: 9009 spring: application: name: zuul-client eureka: client: service-url: defaultZone: http://localhost:9001/eureka/ zuul: routes: hiapi: path: /hiapi/** serviceId: hi-service
访问:http://localhost:9009/hiapi/hi,如果hi-service部署了多个实例,那么zuul在路由转发就做了负载均衡。
当然也可以使用url属性代替serviceId属性,通过指定ip+port的方式的url地址来直接访问(当然这种情况很少出现)
如果想自己维护负载均衡的服务列表,可以使用如下方式:
zuul:
routes:
hiapi:
path: /hiapi/**
serviceId: hiapi-v1
ribbon:
eureka:
enabled: false
hiapi-v1:
ribbon:
listOfServers: http://localhost:9007,http://localhost:9008,http://localhost:9009/hiapi/hi
配置API接口的版本号:
zuul: routes: hiapi: path: /hiapi/** serviceId: hi-service prefix: v1
那么访问路径将变为:http://localhost:9009/v1/hiapi/hi
集成Hystrix实现熔断器:
@Component public class MyFallbackProvider implements FallbackProvider { @Override public String getRoute() { return "hi-service"; // 应用名称或者serviceId,或者是正则表达式,如* } @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() { HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); return headers; } }; } }
在Zuul中使用过滤器:
@Component public class MyFilter extends ZuulFilter { @Override public String filterType() { return FilterConstants.PRE_TYPE; // 前置过滤器
} @Override public int filterOrder() { return 0; // 优先级为0,数字越大,优先级越低 } @Override public boolean shouldFilter() { return true; // 是否执行该过滤器,此处为true,说明需要过滤 } @Override public Object run() throws ZuulException { RequestContext ctx = RequestContext.getCurrentContext(); HttpServletRequest request = ctx.getRequest(); String token = request.getParameter("token"); if (StringUtils.isBlank(token)) { ctx.setSendZuulResponse(false); ctx.setResponseStatusCode(401); try { ctx.getResponse().getWriter().write("token is empty"); } catch (IOException e) { } } return null; } }