微服务全链路异步化实践

本文来自OPPO互联网基础技术团队,转载请注名作者。同时欢迎关注我们的公众号:OPPO_tech,与你分享OPPO前沿互联网技术及活动。

1. 背景

随着公司业务的发展,核心服务流量越来越大,使用到的资源也越来越多。在微服务架构体系中,大部分的业务是基于Java 语言实现的,受限于Java 的线程实现,一个Java 线程映射到一个kernel 线程,造成了高并发场景下线程资源的极大浪费,线程成为提高系统并发和吞吐量的瓶颈。

在微服务架构下,使用同步编程模式时不仅造成了资源的极大浪费,并且在流量发生激增波动的时候,受制于系统资源而无法快速的扩容。本文将探索服务异步化在并发、吞吐量方面对系统带来的提升。

2. 如何快速提高服务吞吐量

首先,以微服务架构中的RPC 服务调用举例,测试和探索在微服务架构中,异步架构如何提高服务的吞吐量和并发。ESA Stack 是OPPO 自研的基础框架技术栈,ESA RPC 是自研的RPC 框架。本节测试服务我们使用ESA RPC 搭建。

关于ESA RPC的详情,可以参考我们之前发布的文章《Dubbo协议解析及ESA RPC实践》。

2.1 服务架构

下图所示为测试环境架构。其中Service A 既是服务端也是客户端,它模拟了生产环境中大部分服务的角色。我们对Service A 分别采用同步模型和纯异步模型进行压测,其中纯异步模型包含了客户端、服务端逻辑的异步处理。Service B 模拟一个耗时为N ms 的下游服务,为Service A 的调用提供固定的延时响应。

测试服务器的配置为8 核16G,千兆网卡。

2.2 同步异步模型对比

测试场景1:

并发压测客户端200~8000,服务耗时50ms,分别对同步和异步架构进行压测,对比TPS、服务耗时、CPU 上下文切换;同步模式下,线程数和并发客户端相同;异步模式下,使用框架默认的200 线程。测试数据如下。

测试场景2:

并发压测客户端8000,服务耗时50~500ms,分别对同步和异步模式进行压测,对比TPS、服务耗时、CPU 上下文切换;同步模式下服务端8000 线程;异步模式下,使用框架默认的200 线程。

2.3 服务扩展性对比

并发指服务瞬时同时处理的任务数(包含处于IO 等待状态的任务)。服务端设置业务处理线程200,那么同步模式下能提供的并发为200;纯异步模式下服务并发不受线程限制,IO密集型服务尤其收益。在系统流量突增的情景下,异步模式具有更强的可扩展性(Scalability)。

2.4 结论

根据上面的测试数据可以做出以下对比:

  • 同步模式,线程数与并发成正比,并发越高对线程的消耗越多
  • 异步模式,提高并发不需要线程增加
  • 同步模式,系统Context Switch 次数随并发提高而快速增加
  • 异步模式,系统Context Switch 次数明显小于同步模式
  • 同步模式,并发超过某个临界点后,服务耗时快速上升,系统吞吐量急剧下降
  • 异步模式,吞吐量随着并发增加,服务耗时上升速度明显低于同步模式

从而得出以下结论:

  • 可以通过异步化微服务架构,提高相同资源配置下的服务吞吐量
  • 随着下游平均耗时的增加,异步化带来的吞吐和耗时的提升作用减小
  • 线程资源有限(内核、内存),不能无限增加来提高并发能力,异步化能极大提高系统瞬时并发能力(Scalability)

结论分析:

  • 高并发下同步模型大量线程在内核态度/用户态、不同CPU 核之间进行切换,Context Switch 增加,系统性能下降
  • 下游平均耗时增加时,系统CPU 繁忙程度降低,Context Switch 对性能系统影响下降

3. 异步模型探索

3.1 阻塞与非阻塞

在操作系统中,线程是CPU 调度的基本单位;阻塞调用是指发起调用后,线程进入阻
塞状态(让出CPU),直到获得结果或异常返回;非阻塞调用是指不等待结果,调用不阻塞线程直接返回。

3.2 同步与异步

同步和异步关注的是消息通信机制;同步就是在发起调用后就得到返回结果(未必是完整结果),也就是由调用者主动等待结果;异步则是调用在发出之后直接返回,通过信号通知、回调函数处理来通知结果。

3.3 四种IO 模型

非IO 系统调用层面, 阻塞/非阻塞和同步/异步基本是同义词;在IO 系统调用层面,同步/异步和阻塞/非阻塞有以下组合:

  • 同步阻塞调用,线程同步等待阻塞调用结果
  • 同步非阻塞调用,线程通过轮训获取非阻塞调用结果
  • 异步阻塞调用,IO 事件阻塞,IO 操作不阻塞
  • 异步非阻塞调用,调用立即返回,信号/回调处理结果

我们通过一个简单的客户端来介绍四种IO 模型的代码写法:


同步阻塞IO

非同步阻塞IO

多路复用IO

Asynchnorous IO

对四中IO 模型,有以下的对比:

  • 同步阻塞式IO 模型,编程简单但线程阻塞,资源利用率低;
  • 同步非阻塞式IO 模型,需要轮训CPU,浪费资源;
  • 异步非阻塞AIO 模型,不阻塞线程,使用回调方式处理数据,但是编程难度高;
  • 多路复用IO 模型,能够实现异步非阻塞IO,且编程简单,方便实现同步和异步调用,因此成为RPC 框架的首选。

4. 全链路异步编程指南

4.1 全链路组成及现状

微服务架构下的全链路包含了网关层、WEB 服务、RPC 服务、数据层等。目前公司的网关层已经实现了纯异步架构,Web 框架和RPC 框架支持纯异步编程,数据存储层目前异步方案还不成熟。

4.2 网关异步化

网关层由于其特殊性,不需要访问业务数据库只做协议转换和流量转发,目前已经使用了纯异步的架构;其IO 密集型的特点,特别适合纯异步的架构,可以极大的节省资源。

4.3 Web 服务异步化

Web 服务作为微服务体系内的重要组成,服务节点众多,传统的Web 服务框架SpringMVC 不支持纯异步化编程,OPPO 自研Web 框架Restlight 支持纯异步编程,且性能远超SpringMVC。下面是性能对比及Restlight 异步实践。

Restlight 框架异步编程实践:通过Controller 方法返回值区分同步和异步调用,且支持三种异步调用方式,CompletableFuture、ListenableFuture(Guava)、Future(Netty)。

4.4 RPC 调用异步化

RPC 调用等待下游response 返回时,线程不应处于block 状态;作为微服务架构中数据流量最大的一部分,RPC 调用异步化的收益巨大;目前ESA RPC 已经具备了纯异步化的能力,提供RPC 调用的服务一般既是客户端也是服务端,因此包含了客户端异步调用能力和服务端异步处理能力;为了兼容存量接口,ESA RPC 既支持CompletableFuture 也支持普通返回值的接口。

客户端异步化实践:底层使用异步非阻塞IO 收发网路数据包,使用CompletableFUture传递IO 事件以实现响应式编程,客户端不被RPC 调用阻塞,可继续调用其他服务。

接口返回CompletableFuture 来实现异步调用:


普通接口使用ESARpcContext::asyncCall 实现异步调用:


服务端异步化实践:通过服务端异步功能返回CompletableFuture 给框架以释放Biz 线程,自定义线程池或者IO 线程池收到下游response 后,完成返回给框架的Future。

接口定义返回CompletableFuture 来实现异步调用:


普通接口通过ESARpcContext::startAsync 开启服务端异步:


4.5 存储层异步化

数据操作是每个请求调用链的终点,纯异步的架构必须使用异步存储层客户端,目前OPPO 没有自研的存储层异步客户端,但业界开源方案欣欣向荣:

  • 数据库: Vert.x JDBC 客户端
  • Redis:Redisson、Lettuce
  • Queue:基本都支持异步调用

4.6 纯异步与伪异步

异步调用目的在于防止当前业务线程被阻塞。伪异步将任务包装为Runnable 放入另一个线程执行并等待,当前Biz 线程不阻塞;纯异步为响应式编程模型,通过IO 实践驱动任务完成。他们的区别不在于是否将请求放入另一个线程池执行,而在于是否有线程阻塞等待Response。

5. 异步化未来发展

5.1 异步化带来的问题

相比于同步模型,异步模型存在以下问题:

  • 代码可读性和可维护性较差,可能出现Callback Hell
  • 框架SDK 变得复杂,使用门槛增加
  • 业务可能不清楚代码逻辑执行线程
  • 大量的ThreadLocal 需要手动export/import

简单来说,异步编程就是以编程的简单性(simplity)来交换性能(performance)。

5.2 使用协程实现异步非阻塞

目前在其他语言中,Erlang、Go、Kotlin 等都支持了协程,使用携程的好处是在语言层面支持了异步调用,业务代码可以使用同步的写法达到异步的效果,线程不被阻塞,避免大量的CPU 上下文切换,提升系统的性能。

目前Java 对协程的支持也在进行中, Project Loom 就是Java 的协程项目:http://openjdk.java.net/proje...

主要有以下几个概念:

  • Fiber,轻量级线程(用户态线程),基于Continuation 实现
  • Continuation,指令执行单元, 阻塞时调用Continuation::yield , 恢复时调用Continuation::run
  • Scheduler,用户态Fiber 调度器(ForkJoinPool),使用有限Workers 线程执行任意数量Fibers

开发者可以使用Fiber 来执行业务代码块,当遇到LockSupport::park、socket io 等阻塞调用时,Fiber 中的代码单元执行会被阻塞,但是底层的线程并不会被阻塞。由此达到了开发同步模式代码,运行时达到异步执行的目的。

未来,ESAStack服务框架会支持协程。目前 Restlight框架已经支持协程并在内部开始试用,ESARPC也有支持协程的计划。框架提供的服务线程使用 Fiber 执行业务逻辑,业务实现中数据库请求、下游服务调用均在 Fiber 之中执行, 其包含的 IO 等阻塞调用只挂起 Fiber 而不阻塞所在线程,从而避免了过多的上下文切换提升 了吞吐量,达到了和异步模式一样的效果。

阅读 566

推荐阅读