异步化

异步化

携程异步化改造

当前结构

Tirp.com 消息推送平台也是基于同步+阻塞 IO 的 servlet 模型架构。当客户端发起网络请求的时候,请求首先会由 Tomcat 容器的 Acceptor 线程进行处理,将 Channel 放到待处理请求队列然后通过 Poller 线程进行 IO 多路复用的监听,当 Poller 监听到 Channel 的可读事件后,请求体将会从缓冲区被读入内存,然后交由 Tomcat 容器的 Worker 线程池进行消费。由于需要使用阻塞 IO 调用下游的第三方发送接口,所以 Worker 线程池需要启动大量的线程进行并发操作。

Trip.com 消息推送平台使用 AWS 的 SES 服务进行邮件发送,在发送 Email 时将会调用 AWS 的同步 SDK:

// 底层基于 Apache HttpClient 的 BIO 调用
SendEmailResult sendEmail(SendEmailRequest sendEmailRequest);

存在问题

而随着业务量上涨带来上游消息负载增加,原有的阻塞 IO 模型在高并发下,会有大量线程处于阻塞状态,导致应用需要囤积大量的线程以应对峰值压力。过多的线程将会造成大量的内存占用和频繁线程上下文切换的开销,所以原有的 servlet 线程模型具有 CPU 利用率低、内存占用大、对异常请求不具备弹性等缺点。该平台在压力峰值时需要部署大量机器,它主要具有以下性能上的问题:

  • 线程上下文切换开销

一次请求的 IO 时间平均在 1200ms,最高能达到 50000ms,而计算时间只有 1~2ms,根据最佳线程公式理论上 1C 需要 600~2500 个线程。囤积如此多的线程将会造成大量的上下文切换开销和上 GB 的内存占用。但若是使用少量的线程,将可能由于线程数量的限制,导致请求量过高时拿不到处理线程,最终请求超时,不具备低延迟等特性。

  • 部署成本高

若是采用主流的 2C4G 容器配置,理论上将需要 1000+ 的线程用于处理请求,这将占用大概 1GB 的内存空间。同时若并发请求数>线程数时,需要采用水平扩容增加服务的吞吐量,所以服务需要按照峰值并发进行预估部署,造成空闲时间大量资源的浪费。

  • 超时风险

一次 IO 最高能达到 50s,当有异常请求导致响应时间突增时,因为会阻塞线程,导致线程池中的线程大部分都被阻塞,从而无法响应新的请求。在这种情况下,少量的异常请求将会导致上游大量的超时报错,因此服务不具有弹性。

Reactive

Reactive 构建的程序代表的是异步非阻塞、函数式编程、事件驱动的思想。

反应式系统的瓶颈不在于线程模型,在不同的工作负载下,使用 EventLoop 线程模型将始终提供 CPU 资源允许的计算能力,当达到计算能力瓶颈时可以横向拓展 CPU 计算资源。反应式系统通过 EventLoop+NIO 模型,避免线程的上下文开销,同时也避免线程池资源的大小成为系统的瓶颈

结果

通过与原应用的压力测试结果对比,我们发现使用 EventLoop+NIO 的新应用,在相同硬件资源下,QPS 能够提升 2~3 倍RT 缩短近 50%,同时在内存占用上也得到了一定的优化。