对开发者来说,异步是一种程序设计的思想,使用异步模式设计的程序可显著减少线程等待,从而在高吞吐的场景中,极大提升系统的整体性能,显著降低延时。
因此,像消息队列这种需要超高吞吐量和超低时延的中间件系统,在其核心流程中,一定会大量采用异步的设计思想。
接下来,我们一起来通过一个非常简单的例子学习,如何使用异步设计,提升系统性能。
异步设计如何提升系统性能?
假设我们要实现一个转账的微服务,Transfer(accountFrom, accountTo, amount),这个服务有三个参数:分别是转出,转入,金额。
实现过程也比较简单,要从账户A转账100元到账户B中:
1. 先从 A 的账户减 100 元
2. 再给 B 的账户加 100 元,转账完成
对应的时序图是这样:
client transfer_service account_service
|-------Transfer(A, B, 100)------> | ------------------ Add(A,-100)-----------------> |
| | <--------------------- OK ------------------------- |
| | ------------------ Add(B , 100)----------------> |
| <--------------OK ------------------- | <--------------------- OK ------------------------- |
client transfer_service account_service
在这个例子的实现构成中,我们调用了另外的微服务 Add,功能是给账户增减金额。
特别说明,这段时序未做错误和事务处理,实际开发勿学。仅用于专注学习性能优化。
1. 同步实现的性能瓶颈
首先来看下同步实现。
首先从from扣钱,再从to加钱。分析一下代码性能,很容易计算出实现的微服务的transfer的平均响应时延大约等于两次 Add 的时延,也就是 100ms。随着调用 Transfer 服务的请求越来越多,每个请求100ms独占一个线程。一秒每个线程最多处理10个请求。每个机器上的线程资源并非无限,假使我们的服务器能同时打开的线程数量上线是1万。可计算出单服务器每秒的QPS为 10万。
如果请求超出,只能阻塞或者排队,此时transfer服务的响应时延由100ms延长到了:排队等待+处理时延。在大量请求下,微服务的平均响应时延变长。
但其实,此时机器CPU、内存、网卡流量、IO都空闲的很,因为1万个线程基本都在等待下游Add服务返回结果。
也就是说,采用同步实现,整个服务大部分的线程没在工作,都在等待。
若能减少或避免无意义等待,就可大幅提升服务的吞吐能力,从而提升服务总体性能。
2. 采用异步实现解决等待问题
接下来我们看一下,如何用异步思想来解决这个问题,实现同样的业务逻辑
先定义两个回调方法
- OnDebit:扣减账户from后的回调方法
- OnAllDone:转入账户 to 完成后的回调方法
改造后的整体异步语义是:
1. 异步从from的账户减去前述钱数,然后调用OnDebit
2. 在OnDebit中,异步把减去的钱加到to,然后执行 OnAllDone
3. 在OnAllDone中,调用OnComplete
client transfer_service account_service
|-------TransferAsync -----------> | ------------------ AddAsync ----------------> |
| | <----------------- OnDebit --------------------- |
| | ------------------ AddAsync -----------------> |
| <--------- OnComplete ---------- | <--------------------- OnAllDone --------------- |
client transfer_service account_service
会发现,异步化实现后,整个流程的时序和同步实现是完全一样的,区别只是在线程模型上由同步顺序调用改为了异步调用和回调的机制。
接下来分析一下异步实现的性能,由于流程和同步一致,低请求的时延仍然是100ms。在超高请求数量场景下,异步实现不再需要线程等待执行结果,只需要个位数量的线程,即可实现同步场景大量线程一样的吞吐量。
因为没有线程数量的限制,总体吞吐会超过同步,在CPU,内存,网络带宽,I/O的资源达到极限之前,响应时延不会随请求量增大而增加,几乎可以一直维持100ms。
简单实用的java的异步框架 CompletableFuture
略
小结
简单的说,异步思想就是,当我们要执行一项比较耗时的操作,不要等操作结束,而是给这个操作一个命令,当操作完成后接下来执行什么。
使用异步变成模型,虽然不能加快程序本身的速度,但可以减少或避免线程等待,只用很少的线程就可以达到超高的吞吐。
同时我们也需注意异步模型的问题,相比于同步实现,异步实现的复杂度要大很多,代码的可读性和可维护性都会显著下降。虽然使用异步编程框架简化了异步开发,但并不能解决模型高复杂度。
异步性能虽好,但不要滥用,只有类似消息队列这种业务逻辑简单并且要高吞吐的场景下,后者必须长时间等待资源的地方,才考虑使用一步模型。
若业务逻辑复杂,性能足够满足业务需求情况下,采用符合人类自然的思路且易于开发和维护的同步模型是更加明智的选择。
思考
第一个思考题:我们实现转账的时候,并没有考虑失败的处理。
如果调用账户服务失败,如何通知客户端。(callback中通知)
在两次调用账户服务失败时,如果某次失败,如何保证账户数据是平的。(首次失败后一步无需执行,第二次失败考虑重试,不能重试进行补偿,undo首次的转账。另一种想法是改造成单次调用的,下游进行单机事务操作。)
第二个思考题:
异步实现中,回调方法OnComplete是在什么线程中运行的。(OnAllDone)
是否能控制回调方法的执行线程数,该如何做?(原理是使用异步线程池控制回调方法的线程数。绑定回调到指定线程池执行即可)