小米开源分布式KV存储系统Pegasus( 四 )


那么这种问题该怎么处理呢?就我们这边的经验来看,是要把代码的组织方式从“对临界区的争抢”到“无锁串行化的排队” 。我先用一张图来说明一下我们的这种代码架构:

小米开源分布式KV存储系统Pegasus

文章插图
具体来看,就是以“一致性状态”为核心,把所有涉及到状态更改的事件串到一个队列中,通过单独的线程取队列事件执行 。所谓执行,就是更改一致性状态 。如果执行过程中触发了 IO,就用纯异步的方式,IO 完成后的响应事件又会串到队列中来 。如下图所示:
另外,为了避免创建过多的线程,我们采用了线程池的做法 。一个“一致性状态”的数据结构只会被一个线程进行修改,但多个结构可能共享一个线程; 和线程的,是多对一的关系 。
总的来说,通过这种事件驱动和纯异步的方式,我们得以在访问一致性状态时避免细粒度的锁同步 。CPU 没有陷入 IO 等待中,以及线程池的使用,性能方面也是有保障的 。此外,这种实现方式由于把一个写请求清晰的分成了不同的阶段,是非常方便我们对读写流程进行监控的,这对项目的性能分析是非常有好处的 。
测试
接下里我们重点讲一下测试是怎么做的 。
分布式系统稳定的问题
当我们把系统做下来之后,发现真正长期困扰着我们的其实是如何将系统做稳定 。
这个难题主要体现在哪几个方面呢?总结之后有以下三点:
难以测试,没有较有效的方法测试系统的问题 。现在的经验便是将它当成一个黑盒,读写的同时杀任务,想办法使它的某一个模块出现问题,再去查看全局是否出现问题 。然而这样的测试方法只能去撞 bug,因为问题是概率性的出现的 。
难以复现 。因为 bug 是概率性出现的,就算通过测试发现了问题,这个问题也不太容易复现,从而进一步对调试带来困扰 。
难以回归 。假如通过看 log、观察现象、分析代码找到了问题的症结 。你的修复方法是不是有效也没有说服力 。这样修是不是能解决问题?会不会引发新的问题?因为没有稳定复现的方法,这两个问题是很难回答的 。
根源:不确定性
那么造成以上那些难点的根源在哪里?总结一下,我们认为是程序自身的不确定性 。该不确定性体现在两个方面:
用一个公式概括一下:
小概率的 IO 错误 + 随机执行路径 = 不容易复现的异常状况
那么对于这个问题,我们应该怎么解决?
既然在线上很难复现问题,那么能不能构造一种模拟的场景:在这个模拟场景里边,我们可以模拟 IO 错误的概率,也可以控制程序的执行顺序 。再在这样的模拟场景里运行代码,如果逻辑真的出现了问题,我们就可以按照相同的执行顺序把问题复现出来 。逻辑修改后,还可以进一步做成单元测试,这样难以回归的问题也就解决了 。
当然这样描述这个问题,还是非常抽象 。这里我举个简单的例子来说明下:
假设有这样的一个账户系统,里面有 Alice 和 Bob 两个人,两人账户的余额各为 100 元,分别存储在两台机器上,两人均可以向对方发起转账的交易 。现在 Alice 要向 Bob 转账 5 元,最简单的实现是 Alice 把自己账号上的余额扣减 5 元,然后向 Bob 所在的机器发起一个增加 5 元的请求 。等 Bob 的账号增加 5 元后,就可以通知 Alice 说转账成功了 。
如果机器不会宕机、磁盘不会出故障、网络也不会出问题,那么这种简单的实现方式并无不可 。但这样的假设绝对不会成立,所以为了应对这些方面的问题,我们可能加入一系列的手段来让账号系统变得可靠起来,诸如增加交易日志,把交易和账户信息备份到多个机器上,引入一些分布式事务的技术 。这些手段的引入,会使得我们的系统变得复杂起来 。