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


下表给出了我们对两种方案的对比:
两者相比,一个理论上更优美,一个实践上对业务更友好 。最后我们还是从业务出发,我们放弃了理论更优美也更容易实现的方案,而选择了对业务友好的方案 。
Hash的负载均衡
说完 hash,接下来就是负载均衡的问题 。下面列出了一般分布式 KV 存储在负载均衡上的目标:
负载均衡 - 目标
具体来看,负载均衡的目标有两点:
A. 和不能共享
B. 对于每个表,和都能在不同的上平均分配
上图是对目标 B 的一个简单说明:假如一张表有四个,而一共有三个,我们希望 12 个的分布情况是 (1, 3), (2, 2), (1, 3) 。
负载均衡 - 算法
在实现我们的负载均衡算法时,有一个很重要的注意点是:角色切换要优于数据的拷贝,因为角色切换的成本非常低 。
对于这点的说明,可以参考上图中的两种情况:
在左图里,4 个分布在和上,而上没有任何。这时候,通过把上的A 和上的A 做角色对调,就可以满足的均衡 。
在右图中,4 个在四个上的分布是 (2, 1, 1, 0) 。如果想要做的均衡,需要把上的一个迁移到上,但直接迁移需要拷贝数据 。此时,我们如果引入中间节点,先把的A 和的A 角色互换,再把的D 和的D 互换,就能实现的均衡 。
为了处理这些情况,我们对集群中可能的流向建立了一个有向图,然后利用了 Ford- 的方法进行的迁移调换 。具体的算法不再展开,可以参见我们的开源项目代码 。
在真实的负载均衡中,还有很多情况需要考虑:
在当前的开源项目上,有部分情况还没有做考虑 。后面在负载均衡上我们会持续做优化,大家可以持续保持关注 。
一致性和可用性
前面介绍了扩展性,接下来是一致性和可用性 。我们在设计上的考虑前面已经介绍过了:
当我们按照这些设计目的将系统实现出来,并准备找业务小试牛刀时,业务一句话就将我们顶了回来:“你们有双机房热备么?”
为什么要单独说这一点?因为对于一个强一致性的分布式存储系统而言,跨机房的容错是一件比较麻烦的事情:
通过对这个问题进行反思,我们认为我们其实钻了一个“要做完美系统”的牛角尖 。就业务的角度来看,作为一个完善的存储系统,的确要对各种各样的异常情况都需要做好处理 。但随着异常情况发生概率的降低,业务对一致性的要求其实也是逐步放宽的:
在了解了业务的需求后,我们为设计了多级的冗余策略来应对不同的风险:
对上述冗余策略的几点说明:
延时保证
最后介绍一下保证延时性能方面的问题 。对于这个问题,有两点需要强调一下:
在实现语言上,我们选择了 C++ 。原因前面也说过,为了性能保障,我们必须采用没有运行时 GC 的语言 。另一个选择可能是 Rust 。在为什么选 C++ 而不选 Rust 上,我们的考虑如下:
当然这个选择是偏保守型的,在语言层面的探讨也到此为止 。重点还是说下我们一致性协议实现的问题 。
就实践来看,想要把一致性协议实现的正确、高效,并且还要保证代码是易维护、可测试的,是一件比较难的事情,主要原因就在于一个完整的请求会涉及很多个阶段,阶段之间会产生 IO,再加上并发上的要求,往往需要对代码进行很细粒度的加锁 。
下图给出了一个完整写请求的流程简图:
从上图可以看出,当客户端发起一个写请求后,这条请求会先后产生写本地磁盘、以及发送网络 RPC 等多个事件,无论事件成功还是失败,都需要对公共的一致性状态进行访问或修改 。这样的逻辑,会使得我们要对一致性状态进行非常繁琐的线程同步,是非常容易产生 bug 的 。