如何使用异步设计提升系统性能?
异步设计如何提升系统性能?
- 假设我们要实现一个转账的微服务 Transfer(accountFrom, accountTo, amount),这个服务有三个参数:分别是转出账户、转入账户和转账金额。
- 这个例子的实现过程中,我们调用了另外一个微服务 Add(account, amount),它的功能是给账户 account 增加金额 amount,当 amount 为负值的时候,就是扣减响应的金额。
- 在这段代码中,为了使问题简化以便我们能专注于异步和性能优化,省略了错误处理和事务相关的代码,在实际的开发中不要这样做。
同步实现的性能瓶颈
- 首先我们来看一下同步实现,对应的伪代码如下:
Transfer(accountFrom, accountTo, amount) {// 先从accountFrom的账户中减去相应的钱数Add(accountFrom, -1 * amount)// 再把减去的钱数加到accountTo的账户中Add(accountTo, amount)return OK }
- 假设微服务 Add 的平均响应时延是 50ms,那么很容易计算出我们实现的微服务 Transfer 的平均响应时延大约等于执行 2 次 Add 的时延,也就是 100ms。
- 在这种实现中,每处理一个请求需要耗时 100ms,并在这 100ms 过程中是需要独占一个线程的,那么可以得出这样一个结论:每个线程每秒钟最多可以处理 10 个请求。
- 我们知道,每台计算机上的线程资源并不是无限的,假设我们使用的服务器同时打开的线程数量上限是 10,000,可以计算出这台服务器每秒钟可以处理的请求上限是 10,000(个线程)* 10(次请求每秒)= 100,000 次每秒。
- 如果请求速度超过这个值,那么请求就不能被马上处理,只能阻塞或者排队,这时候 Transfer 服务的响应时延由 100ms 延长到了:排队的等待时延 + 处理时延 (100ms)。
- 也就是说,在大量请求的情况下,我们的微服务的平均响应时延变长了。
- 采用同步实现的方式,整个服务器的所有线程大部分时间都没有在工作,而是都在等待。
采用异步实现解决等待问题
-
接下来我们看一下,如何用异步的思想来解决这个问题,实现同样的业务逻辑。
TransferAsync(accountFrom, accountTo, amount, OnComplete()) {// 异步从 accountFrom 的账户中减去相应的钱数,然后调⽤OnDebit⽅法。AddAsync(accountFrom, -1 * amount, OnDebit(accountTo, amount, OnAllDone(OnComplete())) } // 扣减账户 accountFrom 完成后调⽤ OnDebit(accountTo, amount, OnAllDone(OnComplete())) {// 再异步把减去的钱数加到 accountTo 的账户中,然后执⾏ OnAllDone ⽅法AddAsync(accountTo, amount, OnAllDone(OnComplete())) } // 转⼊账户 accountTo 完成后调⽤ OnAllDone(OnComplete()) {OnComplete() }
-
异步的实现过程相对于同步来说,稍微有些复杂。
- 我们先定义 2 个回调方法:
- OnDebit():扣减账户 accountFrom 完成后调用的回调方法;
- 异步从 accountFrom 的账户中减去相应的钱数,然后调用 OnDebit 方法;
- 在 OnDebit 方法中,异步把减去的钱数加到 accountTo 的账户中,然后执行 OnAllDone 方法;
- OnAllDone():转入账户 accountTo 完成后调用的回调方法。
- 在 OnAllDone 方法中,调用 OnComplete 方法。
- OnDebit():扣减账户 accountFrom 完成后调用的回调方法;
- 异步化实现后,整个流程的时序和同步实现是完全一样的,区别只是在线程模型上由同步顺序调用改为了异步调用和回调的机制。
- 由于流程的时序和同步实现是一样的,在低请求数量的场景下,平均响应时延一样是 100ms。
- 在超高请求数量场景下,异步的实现不再需要线程等待执行结果,只需要个位数量的线程,即可实现同步场景大量线程一样的吞吐量。
- 由于没有了线程的数量的限制,总体吞吐量上限会大大超过同步实现,并且在服务器 CPU、网络带宽资源达到极限之前,响应时延不会随着请求数量增加而显著升高,几乎可以一直保持约 100ms 的平均响应时延。
- 我们先定义 2 个回调方法:
简单实用的异步框架: CompletableFuture
- Java 中比较常用的异步框架有 Java 内置的 CompletableFuture 和 ReactiveX 的 RxJava。
- 接下来,我们来看下,如何用 CompletableFuture 实现的转账服务。
- 首先,我们用 CompletableFuture 定义 2 个微服务的接口:
/*** 账户服务*/ public interface AccountService {/*** 变更账户⾦额* @param account 账户 ID* @param amount 增加的⾦额,负值为减少*/CompletableFuture<Void> add(int account, int amount); }
/*** 转账服务*/ public interface TransferService {/*** 异步转账服务* @param fromAccount 转出账户* @param toAccount 转⼊账户* @param amount 转账⾦额,单位分*/CompletableFuture<Void> transfer(int fromAccount, int toAccount, int amount); }
- 然后我们来实现转账服务:
/*** 转账服务的实现*/public class TransferServiceImpl implements TransferService {@Injectprivate AccountService accountService; // 使⽤依赖注⼊获取账户服务的实例@Overridepublic CompletableFuture<Void> transfer(int fromAccount, int toAccount, int amount) {// 异步调⽤ add ⽅法从 fromAccount 扣减相应⾦额// 然后调⽤ add ⽅法给 toAccount 增加相应⾦额return accountService.add(fromAccount, -1 * amount).thenCompose(v -> accountService.add(toAccount, amount));} }
- 首先,我们用 CompletableFuture 定义 2 个微服务的接口: