0 异步的优势
太多的线程会造成频繁的cpu上下文切换,你可以想象一下,假设你的小公司只有8台电脑,你雇8个程序员一直不停的工作显然是效率最高的。考虑到程序员要休息不可能连轴转,雇佣24个人,每天三班倒,效率也还行。
但是,你要雇佣10000个人,他们还是只能用这8台电脑,大部分时间不都浪费在换人、交接工作上啦。
异步编程是通过分工的方式,是为了减少了cpu因线程等待的可能,让CPU一直处于工作状态。换句话说,如果我们能想办法减少CPU空闲时间,我们的计算机就可以支持更多的线程。
其实线程是一个抽象概念,我们从物理层面理解,就是单位时间内把每毫核分配处理不同的任务,从而提高单位时间内CPU的利用率。
线程就是为了能自动分配CPU时间片而生的。
异步实现里面还是要用线程池限制一下线程数吧,否则没有达到减少线程的效果。
异步模式设计的程序可以显著减少线程等待,从而在高吞吐量的场景中,极大提升系统的整体性能,显著降低时延。
因此,像MQ这种需要超高吞吐量和超低时延的中间件系统,在其核心流程中,会大量采用异步设计。
1 案例引入
一个转账的微服务Transfer( accountFrom, accountTo, amount),该服务有三参数
- 转出账户
- 转入账户
- 转账金额
要从账户A中转账100到账户B:
- 先从A的账户中减去100元
- 再给B的账户加上100元,转账完成。
- 对应时序图:
调用另外一个微服务Add(account, amount),给账户account增加金额amount,当amount为负值时,就是扣减相应金额。
为简化,省略了错误处理和事务相关代码
2 同步的性能瓶颈
同步实现,对应的伪代码:
Transfer(accountFrom, accountTo, amount) {
// 先从accountFrom的账户中减去相应的钱数
Add(accountFrom, -1 * amount)
// 再把减去的钱数加到accountTo的账户中
Add(accountTo, amount)
return OK
}
先从accountFrom的账户中减去相应的钱数,再把减去的钱数加到accountTo的账户中,同步实现简单直接。
性能如何?
假设微服务Add的平均响应时延是50ms,微服务Transfer的平均响应时延大约等于执行2次Add的时延,即100ms。随调用Transfer服务的请求越来越多,会出现什么情况呢?
该实现,每处理一个请求耗时100ms,这100ms过程要独占个线程。
可得:每个线程每秒最多可处理10个请求。假设服务器同时打开的线程数量上限10,000,可以计算出这台服务器每秒钟可以处理的请求上限是: 10,000 (个线程)* 10(次请求每秒) = 100,000 次每秒。
若请求速度超过这个值,那么请求就不能被马上处理,只能阻塞或者排队,这时候Transfer服务的响应时延由100ms延长到了:排队的等待时延 + 处理时延(100ms)。即大量请求时,我们的微服务的平均响应时延变长了。
这是不是已达服务器极限?远没有!
若监测服务器指标,会发现无论是CPU、内存,还是网卡流量或者是磁盘的IO都空闲的很,那我们Transfer服务中的那10,000个线程在作甚?
没错!绝大部分线程都在等待Add服务返回结果。
即采用同步,整个服务器的所有线程大部分时间都没在工作,而在等待!
若能减少或避免这种无意义等待,即可大幅提升服务吞吐能力,提升性能。
异步实现方案
异步实现同样的业务。
TransferAsync(accountFrom, accountTo, amount, OnComplete