线程就是为了能自动分配CPU时间片而生。异步模式设计可显著减少线程等待,在高吞吐量场景中,极大提升系统整体性能,降低时延。因此,像MQ这种需要超高吞吐量和超低时延中间件系统,其核心流程大量采用异步。
异步的本质是为了不占用过多的线程对象。比如一个响应时间是1秒的http1.1请求,并且不考虑http pipeline:
即QPS 5000时,同步需5000个connection和5000个线程,而异步可以省下5000个线程的内存以及操作系统对这些线程的管理能耗。
某转账微服务Transfer有如下参数
调用另外一个微服务Add(account, amount),给账户account增加金额amount,当amount为负值时,就是扣减相应金额。现在要从账户A转账100到账户B:
假设Add平均响应时延60ms,Transfer平均响应时延就是120ms。Transfer每处理一个请求耗时120ms,这过程要独占1个线程。每个线程每s最多可处理约10个请求。假设服务器同时打开线程数量上限为10,000,可计算出这台服务器每s可处理请求上限: 10,000 (个线程)* 10(次请求每秒) = 100,000 次每秒。
若请求速度超过该值,请求就不能被马上处理,只能阻塞或排队,这时Transfer服务响应时延由120ms延长到:排队等待时延 + 处理时延(120ms)。即大量请求时,微服务平均响应时延变长!这就到了服务器极限吗?远没有!若监测服务器指标,会发现无论CPU、内存or网卡流量、磁盘I/O都闲的很,那Transfer服务那10,000个线程在作甚?绝大部分线程都在等待Add服务返回结果!所以采用同步,整个服务器的所有线程大部分时间都没在工作,而是在等待!若能减少或避免这种无意义等待,就能大幅提升服务吞吐量,提升性能。
TransferAsync只是比Transfer多个参数,一个回调方法OnComplete(Java可传个回调类的实例来实现): 请帮我执行转账,当转账完成后,请调用OnComplete。调用TransferAsync的线程无需等待转账完成,即可立即返回。待转账结束,TransferService自然会调用OnComplete()方法执行转账后续工作。
异步实现相比同步实现,先要定义如下回调方法:
异步实现的语义:
异步的时序流程和同步实现完全一样,只是线程模型由同步调用改为异步和回调。
时序和同步实现一样,在少量请求场景下,平均响应时延一样是120ms。在高请求数量场景下,异步不再需线程等待执行结果,只需个位数量的线程,即可实现同步场景需要大量线程同样的吞吐量。
由于无线程数量限制,总体吞吐上限>>同步实现,且在服务器CPU、网络带宽资源达到极限前,响应时延不会随请求数量增加而显著升高,几乎可一直保持约120ms平均响应时延。
Java开发常用异步框架:
Java 8中新增的CompletableFuture几乎涵盖异步程序所需的大部分功能,易写出优雅且易维护的异步代码。
接下来用CompletableFuture改造转账服务。
微服务接口:
转账服务:
客户端使用CompletableFuture既可同步调用,也可异步:
调用异步方法获得返回的CompletableFuture对象后:
CompletableFuture默认在ForkjoinPool commonpool里执行,也可指定一个Executor线程池执行,借鉴guava的ListenableFuture的时间,回调可以指定线程池执行,这样就能控制这个线程池的线程数。
异步实现中,回调方法 OnComplete()在执行OnAllDone()回调方法的那个线程,可通过一个异步线程池控制回调方法的线程数,如Spring中的async就是通过结合线程池来实现异步。
CompletableFuture不完全同于ForkJoin,可简单理解为:
但并非所有场景下,CompletableFuture都要用get()结束,有时无需调用阻塞的get()方法。而且CompletableFuture默认使用 ForkJoinPool,但也支持给它提供一个自定义执行器。
第一个问题,转入转出这两个操作不需要串行,是可以并行的。甚至执行顺序都没什么要求。我们唯一要保证的是这两个操作在一个事务中执行, “要么都成功,要么都失败”,就可以了。
你这个场景是在调用方(转账服务)异步,而服务提供方(账户服务)还是同步服务的情况下,才会出现。
你仔细看一下我们的异步设计,服务提供方提供的也是异步服务,那调用账户服务也是一瞬间就完成了,这样就不会出现你说的“几万个请求对象在CompletableFuture内部线程池内部还是排队”的情况了。
异步思想就是,当要执行很耗时的操作时,不去等待操作结束,而是给该操作一个命令:“当ooo操作完成后,然后执行xxx”
使用异步编程,本身并不能加快程序本身的速度,但能减少或避免线程等待,只用很少线程就得到高吞吐。
异步性能虽好,切勿滥用,只有类似MQ这种业务逻辑简单且需超高吞吐量场景,或须长时等待资源,才考虑使用异步模型。 若业务逻辑复杂,在性能足够满足业务需求情况下,采用易于开发维护的同步模型更适合。