API网关异步化改造技术选型

Posted 架构艺术

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了API网关异步化改造技术选型相关的知识,希望对你有一定的参考价值。

背景

目前的网关是基于Spring Boot 1.5.x 和Tomcat 8.5.x构建,采用多线程阻塞模型,也就是说每个请求都会占用一个独立的线程资源,而线程在JVM中是一个相对比较重的资源。当应用是CPU密集型的或者说依赖的远程服务都正常工作时,这种模型能够很好的满足需求,但一旦后端服务出现了延迟,比如慢查询、FullGC、依赖的第三方接口出问题等情况,线程池很容易被打满,使得整个集群服务出现问题。典型的IO密集型的应用也会有类似的问题,比如网关有很多HTTP请求、RPC远程调用等,当并发量比较大的时候,线程都阻塞在IO等待上,造成线程资源的浪费。

这种模型的优势比较明显:

  • 编程模型简单

  • 易于开发、调试、运维等。本地调试问题支持直接打断点、通过ThreadLocal变量实现监控、通过thread dump即可获取当前请求的处理流程等

但劣势也很明显:

  • 连接数限制。容器的最大线程数一般是固定的,tomcat默认是200,因此当发生网络延迟、FullGC、第三方服务慢等情况造成上游服务延迟时,线程池很容易会被打满,造成新的请求被拒绝,但这个时候其实线程都阻塞在IO上,系统的资源被没有得到充分的利用。

tomcat默认可以接收10000个连接,worker线程默认为200,当线程池被打满后,poller线程会继续接收新的连接请求,并放到epoll队列中,当超过最大连接数后,则会拒绝响应,虽然Tomcat采用了NIO模型,但由于业务线程是同步处理的的,因此当并发比较高时,很容易造成线程池被打满。

  • 容易受网络、磁盘IO等延迟影响。需要谨慎设置超时时间,如果设置不当,且接口之前的隔离做的不是很完善,则服务很容易被一个延迟的接口拖垮。

而异步化的方式则完全不同,通常情况下一个CPU核启动一个线程即可处理所有的请求、响应。一个请求的生命周期不再固定于一个线程,而是会分成不同的阶段交由不同的线程池处理,系统的资源能够得到更充分的利用。而且因为线程不再被某一个连接独占,一个连接所占用的系统资源也会低得多,只是一个文件描述符加上几个监听器,而在阻塞模型中,每条连接都会独占一个线程,是一个非常重的资源。对于上游服务的延迟情况,能够得到很大的缓解,因为在阻塞模型中,慢请求会独占一个线程资源,而异步化之后,因为单条连接诶所占用的资源变的非常低,因此系统可以同时处理大量的请求。

因此考虑对网关进行异步化改造,解决当前遇到的超时、延迟等问题。

技术选型

Zuul 2

Zuul 2基于Netty和RxJava实现,采用了异步非阻塞模型,本质上其实就是队列+事件驱动。在zuul 1中一个请求的完整生命周期都是在一个线程中完成的,但在zuul 2中,请求首先会经过netty server,接着会运行前置拦截器,然后通过netty客户端将请求转发给后端的服务,最后运行后置拦截器并返回响应。但是和zuul 1不同,这里的拦截器同时支持异步和同步两种模式,对于一些比较快的操作,可以直接使用同步拦截器。

API网关异步化改造技术选型

异步拦截器示例:

class SampleServiceFilter extends HttpInboundFilter { private static final Logger log = LoggerFactory.getLogger(SampleServiceFilter.class)
private final SampleService sampleService
@Inject SampleServiceFilter(SampleService sampleService) { this.sampleService = sampleService }
@Override int filterOrder() { return 500 }

@Override boolean shouldFilter(HttpRequestMessage msg) { return sampleService.isHealthy() }
@Override Observable<HttpRequestMessage> applyAsync(HttpRequestMessage request) { //模拟慢请求 return sampleService.makeSlowRequest().map({ response -> log.info("Fetched sample service result: {}", response)
return request }) }}

这里返回的是一个Observable,这是RxJava中的概念,和Java8的CompletableFuture有点像,对于方法调用者来说拿到的都是一个Observable,而内部的实现方式可以是同步,也可以是异步,但是调用者不用关心这个东西,无论实现怎么改,方法的签名是不用变的,始终返回的都是一个Observable

关于响应式的概念这里就不多做介绍了,我觉得上手还是有点难度,个人更倾向于coroutine的方案。

String[] names = ...;
Observable.from(names)
.subscribe(new Action1<String>() {
@Override
public void call(String name) {
Log.d(tag, name);
}
});

Zuul 2是一个不错的选择,但是spring官方已经不打算集成zuul 2了,加上Netflix也打算把技术栈尽可能的迁移到Spring,hystrix和Eureka也都进入维护状态,不再开发新特性,zuul未来也有可能是同样的命运。

Moving forward, we plan to leverage the strong abstractions within Spring to further modularize and evolve the Netflix infrastructure. Where there is existing strong community direction — such as the upcoming Spring Cloud Load Balancer — we intend to leverage these to replace aging Netflix software. Where there is new innovation to bring — such as the new Netflix Adaptive Concurrency Limiters — we want to help contribute these back to the community.

基于Servlet3.1的异步

API网关异步化改造技术选型

Servlet3.1引入了非阻塞式编程模型,支持请求的异步处理。

public void doGet(request, response) { ServletOutputStream out = response.getOutputStream(); AsyncContext ctx = request.startAsync(); //异步写入 out.setWriteListener(new WriteListener() { void onWritePossible() { while (out.isReady()) { byte[] buffer = readFromSomeSource(); if (buffer != null) out.write(buffer); ---> Async Write! else{ ctx.complete(); break; } } } }); }

Spring 4.x+也增加了对非阻塞式IO的支持,例如下面的代码示例(SpringMVC5 + Tomcat 8.5+):

 @GetMapping(value = "/asyncNonBlockingRequestProcessing") public CompletableFuture<String> asyncNonBlockingRequestProcessing(){ ListenableFuture<String> listenableFuture = getRequest.execute(new AsyncCompletionHandler<String>() { @Override public String onCompleted(Response response) throws Exception { logger.debug("Async Non Blocking Request processing completed"); return "Async Non blocking..."; } }); return listenableFuture.toCompletableFuture(); } @PostMappingpublic Callable<String> processUpload(final MultipartFile file) {
return new Callable<String>() { public String call() throws Exception { // ... return "someView"; } };
}

虽然说Servlet3.1提供了对异步的支持,但是其编程模型本质上还是同步的:FilterServlet, 或者有一些方法仍然是阻塞的,比如getParametergetPart等,解析请求体、写会响应本质上还是同步的,但一般来说性能损耗也不算大,网关的耗时基本上都在业务方的IO调用上。

Spring 5 Reactive

对于异步编程模型的选择,Spring5中引入了两种方式,一种是构建于Servlet 3.1之上的SpringMVC,另一种是构建于Netty之上的Spring WebFluxSpring WebFlux不同于Spring MVC,是一个专门为异步设计的响应式框架,完全非阻塞,支持响应式编程模型,可以运行在 Netty, Undertow, 和 Servlet 3.1+容器中。

不同于SpringMVC,WebFlux的请求体、响应都支持响应式类型,可以异步的接受、写入响应,是一个完全异步化的框架。

@PostMapping("/accounts")public void handle(@RequestBody Mono<Account> account) { // ...}
@PostMapping("/owners/{ownerId}/pets/{petId}/edit")public Mono<String> processSubmit(@Valid @ModelAttribute("pet") Mono<Pet> petMono) { return petMono .flatMap(pet -> { // ... }) .onErrorResume(ex -> { // ... });}

另外Spring WebFlux也提供了一个响应式、非阻塞的HTTP客户端:WebClient. 其内部支持多种实现,默认是Reactor Netty,也支持Jetty reactive HttpClient,当然也可以自己通过ClientHttpConnector扩展。

Mono<Void> result = client.post() .uri("/persons/{id}", id) .contentType(MediaType.APPLICATION_JSON) .body(personMono, Person.class) .retrieve() .bodyToMono(Void.class);

Spring Cloud Gateway

Spring Cloud Gateway是由spring官方基于Spring5.0、Spring Boot2.0、Project Reactor等技术开发的网关,目的是代替原先版本中的Spring Cloud Netfilx Zuul,目前Netfilx已经开源了Zuul2.0,但Spring没有考虑集成,而是推出了自己开发的Spring Cloud GateWay。该项目提供了一个构建在Spring生态系统之上的API网关。
特性:

  • 基于Spring Framework 5, Project Reactor 和 Spring Boot 2.0

  • 能够根据请求的任何属性匹配路由

  • 支持Hystrix

  • 支持Spring Cloud DiscoveryClient

  • 限流

  • 路径重写

  • 过滤器

@SpringBootApplicationpublic class DemogatewayApplication { @Bean public RouteLocator customRouteLocator(RouteLocatorBuilder builder) { return builder.routes() .route("path_route", r -> r.path("/get") .uri("http://httpbin.org")) .route("host_route", r -> r.host("*.myhost.org") .uri("http://httpbin.org")) .route("rewrite_route", r -> r.host("*.rewrite.org") .filters(f -> f.rewritePath("/foo/(?<segment>.*)", "/${segment}")) .uri("http://httpbin.org")) .route("hystrix_route", r -> r.host("*.hystrix.org") .filters(f -> f.hystrix(c -> c.setName("slowcmd"))) .uri("http://httpbin.org")) .route("hystrix_fallback_route", r -> r.host("*.hystrixfallback.org") .filters(f -> f.hystrix(c -> c.setName("slowcmd").setFallbackUri("forward:/hystrixfallback"))) .uri("http://httpbin.org")) .route("limit_route", r -> r .host("*.limited.org").and().path("/anything/**") .filters(f -> f.requestRateLimiter(c -> c.setRateLimiter(redisRateLimiter()))) .uri("http://httpbin.org")) .build(); }}

自研

另外也可以参考Zuul2、Spring Cloud Gateway等,基于Netty、Vertx或者spring4.x提供的基于Servlet 3.1的异步机制自研,但自研成本会很高,需要从零开始开发。

对比

选型 优势 劣势
Zuul 2 特性完善。重试、并发保护等 Spring官方不打算集成,需要自己搞。后期项目的活跃度,Netflix开源的eureka、hystrix都进入了维护模式
Spring Boot 1.x + Spring 4.x Servlet 3.1 部分支持异步 如果目前是基于传统spring mvc的方式,相对改造成本比较小
Spring Boot 2 + Spring MVC 部分支持异步 需要升级Spring Boot 2
Spring Boot 2 + Spring Web Flux 完全异步化、异步Http客户端的WebClient 需要升级Spring Boot 2
自研 能够更好的和业务结合 成本太高

问题

需要特别注意的一些问题:

  • 异步化之后,整个流程都是基于事件驱动,请求处理的流程随时可能被切换断开,需要通过trace_id等机制才能把整个执行流再串联起来,给开发、调试、运维等引入了很多复杂性,比如想在IDE里面通过打断点排查问题就不是很方便了。

  • 整个流程都是基于事件驱动,代码相对而言会变得更复杂,想梳理清楚整个工作流程会更麻烦,同步的方式只要跟着IDE一步一步点进去就可以。

  • ThreadLocal机制在异步化之后就不能很好的工作了。Netflix也遇到了很多ThreadLocal的问题,比如监控、traceId的传递、业务参数的传递等,这个需要特别注意。

  • 异步的编程模式,采用回调、future还是响应式? 更激进一点可以考虑下kotlin的coroutine

总结

网关的异步化改造相对还是比较必要的,作为所有流量的入口,性能、稳定性是非常重要的一环,另外由于网关接入了内部所有的API,因此在大促时需要进行比较完善的压测,评估网关的容量,并进行扩容,但如果内部的业务比较复杂,网关接入了非常多的API,这种中心化的方案就会导致很难对网关进行比较准确的容量评估,后面可以考虑基于Service Mesh的思想,对网关进行去中心化改造,将网关的核心逻辑,比如鉴权、限流、协议转换、计费、监控、告警等都抽到sidecar中。

参考链接

  • https://tech.youzan.com/api-gateway-in-practice/

  • https://blog.wangqi.love/articles/Java/API%E7%BD%91%E5%85%B3%E6%8A%80%E6%9C%AF%E6%80%BB%E7%BB%93.html

  • https://blogs.oracle.com/arungupta/non-blocking-io-using-servlet-31:-scalable-applications-using-java-ee-7-totd-188

  • https://medium.com/@the.raj.saxena/springboot-2-performance-servlet-stack-vs-webflux-reactive-stack-528ad5e9dadc

  • https://blogs.oracle.com/arungupta/whats-new-in-servlet-31-java-ee-7-moving-forward

  • http://blog.didispace.com/api-gateway-Zuul-1-zuul-2-how-to-choose/

  • https://wanshi.iteye.com/blog/2410210


作者:aCoder2013

原文:https://github.com/aCoder2013/blog/issues/34



-------END-------



架构艺术云计算 | 微服务 | 互联网 | 软件 | App | Web

往期文章:

以上是关于API网关异步化改造技术选型的主要内容,如果未能解决你的问题,请参考以下文章

7个点5 种 API 网关技术选型,yyds

(原创)大话微服务架构之api网关-apiGateway-技术选型

K8s 网关选型初判:Nginx 还是 Envoy?

微服务网关的选型

微服务网关的选型

微服务网关的选型