背景
目前的网关是基于 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 不同, 这里的拦截器同时支持异步和同步两种模式, 对于一些比较快的操作, 可以直接使用同步拦截器.
异步拦截器示例:
- 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 的异步
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();
- }
- @PostMapping
- public Callable<String> processUpload(final MultipartFile file) {
- return new Callable<String>() {
- public String call() throws Exception {
- // ...
- return "someView";
- }
- };
- }
虽然说 Servlet3.1 提供了对异步的支持, 但是其编程模型本质上还是同步的: Filter , Servlet , 或者有一些方法仍然是阻塞的, 比如 getParameter , getPart 等, 解析请求体, 写会响应本质上还是同步的, 但一般来说性能损耗也不算大, 网关的耗时基本上都在业务方的 IO 调用上.
Spring 5 Reactive
对于异步编程模型的选择, Spring5 中引入了两种方式, 一种是构建于 Servlet 3.1 之上的 SpringMVC , 另一种是构建于 Netty 之上的 Spring webFlux . Spring 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 Cloud DiscoveryClient
- @SpringBootApplication
- public 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://wanshi.iteye.com/blog/2410210
来源: http://www.tuicool.com/articles/beMba2y