今天解读的是来自Uber的Jeff分享的Uber开源库Ringpop,它也是Uber整个底层架构所使用的基础设施,欢迎大家回看我们前一期的文章Uber实时架构从0到1,有助于了解本文。

 

为了了解Uber的底层架构,我们首先要想一下Uber最基础的服务——派遣服务。什么是派遣服务呢?用户发送地址,然后寻找司机来满足他的需求。在Uber的底层架构里,司机每4秒要更新位置信息,这个信息会发到Uber的后台,基于Uber的用户量,这样的写操作每秒会发生1,000,000次,这是Uber希望实现的QPS目标。另外乘客会查找身边的车辆,这个请求估算大概是每秒10,000次。

 

为了满足这个需求,我们看一看它是如何在Uber 内部实现的?

 

首先一个用户在中间发送了自己的请求,他会走到紫色的圆圈中,然后在Uber里面通过Google S2的架构,能够把地图分成很多零星的小块,我们就看哪些零星小块覆盖紫色的圈,这样就能通过查找零星块里面的汽车信息,来找到距离用户最近的汽车信息。

 

那这个架构里的挑战是什么呢?

  • 单机无法处理那么多的请求
  • 有中心的架构存在单点失败问题
  • 如何实现无中心的架构

 

其实系统也是可以有中心的,只是要尽量减少中心服务的事情,这样才能够极大简化系统。很多时候不是做什么事情都非常绝对,大家应该学会做平衡。

 

如何让服务器连到一块去呢?

先看一个机器启动的情况,假设A是一个节点,它启动了。那它首先要去读一个初始列表,这个列表有一群地址信息来获得所有基本的服务。它可以把自己注册为其中一个位置,比如192.168.1.1/3000;之后如果服务B启动,也会读这个初始列表,接着又能随机的向初始列表去连接,直到找到第一行就能连到A了,所以可以互相连上。简单来说,是用初始列表来互相连接。当然这个初始列表也可以是一个外在服务,告诉B里面还有哪些服务,这样也可以。

 

A和B加入了,如何加入更多的节点呢?

另外的点也会读取初始列表,随机访问这些初始列表并且连接他们,这样就能连接上A了,所以还是初始列表。但有趣的是,当它连上以后,他们需要传递信息,需要互相通知,所以这时候就需要通过随机Ping的方式来传递消息,在Ping的过程中,A就会把C存在告诉B,A也会把B存在告诉C,B也跟C互联上,他们都会互相通知。

 

这里有个问题,如何同步消息呢?

 

同步消息的话我们需要知道两件事,第一件事是哪些消息需要同步?如果每次都需要通过发送所有消息来验证就太麻烦,所以在RingPop里面有个有趣的架构,叫做CheckSum。它对自己本地的所有信息做了一个校验和,在Ping的时候可以把自己的校验和发给对方,看一看是否一致,不一致的话说明信息需要同步,同步一下达到校验和一致,这样就能让系统实现最终的一致性。

再进一步来看,这个节点都连起来了,但如果服务器挂了怎么办?

这也很简单,他们会经常互相Ping,这是随机的Ping,当Ping过来之后,比如A挂掉,B就会发现这个连接连不通,A可能是挂了,所以就会发现问题。可这个时候还有一定的误判率,比如就算C发现A挂掉,或者出现问题,也很可能只是C和A之间的网络出现了问题,或者C自己的网络出现了问题。

 

那怎么办呢?C就需要向身边的小伙伴询问,比如会对B说,你也看看A怎么样,B也发现A挂了,这时候就基本可以确定A是挂掉了。所以总结就是C需要向身边的小伙伴询问来得到情况,查看是否是自己的问题。

还有一个问题是短暂的网络异常,很可能B和C去Ping A都挂了,但A可能只是简单的阻塞了1-2秒,或者A在这时候出现网络异常,出现了半分钟的问题。我们这时候就把A移掉吗?A再回来怎么办?这个代价很高,完全不需要干这种极端的事情,我们做什么呢?可以先将A标记为“嫌疑人”,就是可能挂掉了,之后再等待一段时间,比如1分钟,如果A还是不行,再标记为“死亡”,所以这是我们可以设置的一个值。

 

虽然讲了这么多,这里还会有个不稳定节点,什么意思呢?C访问A可能挂了,结果过了10秒之后又好了,再过10秒又坏了,也就是A已经阻塞,或者服务性能特别差,它只是随机的出现问题,这时候它只是偶尔出现问题,并不是经常出问题,怎么才能规避这种问题呢?

 

答案是:混乱度(熵-Entropy)。我们每个节点都会对其他节点评论混乱度,比如C看A,一会挂一会好,每次挂的时候C就会认为A的混乱度增加,接着C如果发现A恢复正常,随着时间递减,这个混乱度会逐渐降低。直到某个时刻,混乱度大于某个阈值,就会认为A已经彻底混乱。这时候再问身边的伙伴,因为很可能还是C自己的网络问题。于是身边的伙伴都会看A情况,如果大家都认为A挂了,就把A驱除一段时间,因为很可能A过程中就好了,可能是因为压力太大,驱除出去之后就恢复了,这样是很好的方法。

如何保证数据不丢?

 

我们在系统中,很可能A,B,C都挂掉,数据可能会丢掉,那如何才能保证数据不丢呢?

 

要保证不丢就需要复制,就是我们每个数据都复制几份,一般是复制三份,那要怎么做呢?比如我们在Data后面加个后缀

 

  • Hash(Data+0)
  • Hash(Data+1)
  • Hash(Data+2)

 

这样就能复制到不同节点上。实际上复制到不同节点是个trick的问题,因为只用Hash1,即使你用0,1,2也可能把他们hash在同样的节点上。所以另外一种方法,有人说那我们可以这样,每次找到一个数据存上去以后,再往后数三个节点,保证在不同的节点上,而且这样不会Hash到相同的地方上。这是一个很好的方法,可以选择去运用。但实际上我们会考虑真的需要Hash三份吗?其实很多时候不需要,有时候只需要Hash两份就够了,大家可以想下背后的道理。

 

如何处理消息

 

因为在系统中大家互相会传递各种消息,消息多种多样,有的是自己能处理,有的自己不能处理,那怎么办呢?

 

答案就是每个节点有个收件箱,相当于Messagebox,Mailox,把收到的所有消息都按序存放在里面,之后有个线程,每次从里面取出一个消息。如果消息属于自己,就处理掉,如果不属于自己就转发给别人,这个非常简单的处理逻辑,就是大名鼎鼎的Actor模型,也是微服务里面常用的架构。

右图是具体信息,大家可以参考文献看一看,比如每个都有节点,他们之间有个Mailbox,Mailbox里面有方法有状态,是数据和程序,就是运行的function,之后进程在里面不断的执行。

 

总结:

 

  • 初始列表连成环,信息摘要四处窜

 

通过读取初始列表能够互相认识,然后通过互相随机的Ping信息摘要就能发现数据的不一致,就能进行恢复和同步,从而保证对系统的看法是一致的。

 

 

  • 确定混乱和失败,不要忘记问伙伴

 

系统中出现各种失败和混乱的时候,一定要向伙伴问一下,很可能是自己出现问题。

 

 

  • 数据不丢多哈希,演员模型最耐看

 

为了保证数据不丢,就可以用多个Hash的方式来保存在多个节点上,然后演员模型里面通过Mailbox和一个单线程的处理模型,让我们非常简单的实现了整个系统的分布式架构微服务模型。

 

参考资料:《Uber’s Ringpop and the Fight for Flap Dampening》