解决吞吐性能问题时的思路

什么是Throughput

Throughput指的是应用处理任务的速率,它所描述的是应用在单位时间内能够处理多大数量的任务

如下,如果应用能够在1s中处理3个task,我们可以说它的throughput是3tps

1

值得注意的是,throughput这个指标所代表的是速率,它并不代表同时性(Concurrency),比如图一中的3tps的应用,我们能说它可以在1s中处理3个task,但是并不意味着3个task是同时被处理的,而可能是顺序、线性地被处理

如果应用可以支持同时处理多个任务,比如应用(系统)中有2个worker,每个worker都可以并行地在1s中内处理3个task,它的throughput则是6tps

2

如何提高throughput呢?显然可以想到:

  1. 缩短每个任务处理的耗时
  2. 让更多的任务可以被同时处理 - 增加并行能力

下图中的应用(系统)可以支持同时处理3个任务,并且每个任务的处理耗时缩短到一半,其throughput是18tps

3

##并行中的共享资源和锁

如果对任务的处理需要访问/修改共享资源呢?比如在扣减库存的任务中,每个任务都需要去访问(校验)当前库存余量,并且要修改(扣减)它

对共享资源的并发访问和修改会产生冲突和一致性问题,比如有两个扣减库存的任务正在同时进行,此时库存余量为1,两个任务都从存储中拿到了当前的库存余量,当其中一个任务完成后,库存余量被扣减为0,此时另一个任务已经完成了校验过程,再去扣减库存的时候,库存余量就被更新成了-1

我们通常使用锁来解决对共享资源的争用所导致的并发冲突与一致性问题,使用资源锁来隔离对资源的操作,保证数据的一致性(正确性)

4

如图,在试图使用某个资源之前,先获取锁从而占用住这个资源,隔离掉其它任务对此资源的访问/修改,从而在占用时间里保证资源的一致性,再使用结束后则释放锁,使资源可以被其它任务访问

为了避免并发冲突以及获取一致性,并非一定要通过锁,也会有其它的方法(比如原子操作),但是总体上还是在对资源的访问/修改制造隔离

隔离的后果是什么呢?

锁(共享资源)的争用和等待

5

争用会导致wait time

也因为我们对资源的访问/修改进行了隔离,导致了多个任务的处理无法同时使用共享资源,每个任务都要等待其它任务对资源的占用结束后才可以继续进行处理,这个等待就会产生wait time

这些wait time意味着:

  1. 任务的处理时间被延长 - 体现为latency
  2. 并行的任务越多,wait time越长 - 破坏了并行处理任务的能力

降低锁的成本

降低使用锁的费用

锁的创建、获取、释放和销毁都是有代价的,降低使用锁本身的费用,比如把数据库锁换成redis锁,甚至换成本地内存锁

以减库存为例: 提前把库存放到redis里,从redis中扣减

降低锁的占用时间

在获取到资源锁之后,应该尽快地释放它,尽量不要在占用锁的期间里做比较花费时间的事情,比如:1)发送HTTP请求 2)执行昂贵的SQL语句 3)超时等待 等等

但是它有局限,如果我们尝试增加任务的并行数,wait time就会继续随之增长

6 6-1

使用更细粒度的锁(共享资源)

通过尽量使用更细粒度的锁(共享资源),可以使锁争用(碰撞)的概率更低,出现等待的情况也就更少

7

以减库存为例: 把库存分布到多个篮子里,100=10*10,随机或者按策略去某个篮子里扣减,这样原本是所有任务都使用单一的库存余量,现在变成分散地使用10个库存余量,出现等待的概率就会变少

无论是降低锁成本还是降低锁粒度,其目的都是减少争用的发生,减少任务的wait time,从而可以提高对多任务的并行处理能力

缓冲请求,合并任务,批量处理(Buffer-Merge-Process)

8

以减库存为例:

  1. 把减库存请求丢进队列中
  2. 每次从队列中取出多个减库存请求,合并成一个减库存任务
  3. 处理合并后的减库存任务

这种方式会导致单个任务的耗时增加 - 因为任务不会立即被处理,但是可以增加总体的throughput,某种程度上是用延迟交换了吞吐,需要考虑这个交换是否值得

总体思路

1.首先定位争用

2.减少争用,减少Wait Time

3.最后才尝试Buffer-Merge-Process

通常这三个方法都可以尝试,都有着一些成本和副作用,也经常需要结合使用,但是在考虑解决方案时,优先考虑解决锁争用

共享资源争用是个很糟糕的质量信号,即使在当前看起来它没有产生很严重的后果,但是实际上它有着非常大的隐患

它会导致应用在性能上变得脆弱:我们可以通过减少锁的费用和占用时间来减少争用从而提高性能,相反的,当锁的费用上升以及占用时间增加时,很容易大量争用导致性能急剧下降,而这时想要解决性能问题很可能要付出非常大的成本 。比如网络环境变化导致使用锁时的延迟增高,又或者一个业务需求或者bugfix需要你在占用锁时执行一个昂贵的sql语句或者http请求,甚至可能只是一个轻微的网络波动,都可能导致应用的吞吐剧烈下降

资源争用下的Scalability问题

当我们在开发应用的时候,对于应用的吞吐性能可以有三种要求:

  1. 够用就行,只要能满足当前需求即可
  2. 吞吐性能不仅要够用,还要出色,比如当前业务只需要我们的应用有30tps,但是我们在设计和开发时,要以1000tps的性能质量来要求它
  3. 当前的吞吐性能需要满足当前的业务需求,不要求应用具备过高的吞吐性能,但是要求在将来它可以通过较低的成本来提升到更高的吞吐性能

第三种要求实际上就是对于应用的scalability的要求,它不要求过高的吞吐性能,但是它需要应用能够快速地响应业务需求对于吞吐性能要求的提升

即当外部环境变化时 - 比如业务规模的增长,比如一个重要的feature带来了性能的降低,比如云迁移导致了应用运行环境发生了变化,这时我们需要能够通过调配资源能够简单快速的提升应用的吞吐性能来适应新的需求,先快速地做到"Doing more",然后再去"With less"