面试官常问的“一致性哈希”,都在这 18 张图里

2020/11/20 13:01
阅读数 79

大家好,好久不见啦。最近快年底了,公司、部门事情太多:冲刺 KPI、做部门预算……所以忙东忙西的,写文章就被耽搁了。再加上这篇文章比较硬,我想给大家讲得通俗易懂,着实花了很多时间琢磨怎么写。

话不多说,小故事开始。

前言

当架构师大刘看到实习生小李提交的记账流水乱序的问题的时候,他知道没错了:这一次,大刘又要用一致性哈希这个老伙计来解决这个问题了。

嗯,一致性哈希,分布式架构师必备良药,让我们一起来尝尝它。

1. 满眼都是自己二十年前的样子,让我们从哈希开始

在 N 年前,互联网的分布式架构方兴未艾。大刘所在的公司由于业务需要,引入了一套由 IBM 团队设计的业务架构。

这套架构采用了分布式的思想,通过 RabbitMQ 的消息中间件来通信。这套架构,在当时的年代里,算是思想超前,技术少见的黑科技架构了。

但是,由于当年分布式技术落地并不广泛,有很多尚不成熟的地方。所以,这套架构在经年日久的使用中,一些问题逐渐突出。其中,最典型的问题有两个:

  1. RabbitMQ 是个单点,它一坏掉,整个系统就会全部瘫痪。
  2. 收、发消息的业务系统也是单点。任何一点出现问题,对应队列的消息要么无从消费,要么海量消息堆积。

无论哪种问题,最终是整套分布式系统都无法使用,后续处理非常麻烦。

对于 RabbitMQ 的单点问题,由于当时 RabbitMQ 的集群功能非常弱,普通模式有 queue 本身的单点问题,所以,最终使用了 Keepalived 配合了两台无关系的 RabbitMQ 搞出了高可用。

而对于业务系统单点问题,从一开始着手解决的时候就出现了波折。一般来说,我们要解决单点问题,方法就是堆机器,堆应用。收发是单点,我们直接多部署几个应用就可以了。如果仅仅从技术上看,无非就是多个收发消息的应用大家一起竞争往 MQ 中放消息拿消息而已。

但是,恰恰就是在把收发消息的应用集群化后,系统出现了问题。

本身这套系统架构会被应用到公司的多类业务上,有些业务对消息的顺序有着苛刻的要求。

比如,公司内部的 IM 应用,不管是点对点的聊天还是群聊消息,都需要对话消息严格有序。而当我们把生产消息和消费消息的应用集群化后,问题出现了:

聊天记录出现了乱序

A 和 B 对话,会出现某些消息没有严格按照 A 发出的先后顺序被 B 接收,于是整个聊天顺序乱成了一锅粥。

经过排查,发现问题的根源就在于应用集群上。由于没有对应用集群收发消息做特殊的处理,当 A 发出一条聊天信息给B时,发送到 RabbitMQ 中的信息会被在 B 处的消费端所争抢。如果 A 在短时间内发出了几条信息,那么就可能会被集群中的不同应用抢走。

这时候,乱序的问题就出现了。虽然应用业务逻辑是相同的,但是这些集群中的应用依然可能在处理信息速度上出现差异,最终导致用户看到的聊天信息错乱。

问题找到了,解决办法是什么?

上面我们说过了,消息顺序错乱是因为集群中不同应用抢消息然后处理速度不一样导致的。如果我们能保证 A 和 B 会话,从开始之后到会话结束之前,永远只会被 B 所在的消费消息集群应用中的同一个应用消费,那么我们就能保证消息有序。这样一来,我们就可以在消费消息的那个应用中,对抢到的消息进行排队,然后依次处理。

那么,这种保证怎么实现呢?

首先,我们在 RabbitMQ 中会建立有相同前缀的队列,后面跟着队列编号。然后,集群中的不同应用会分别监听这两个有着不同编号的队列。当在 A 发送信息时,我们会对信息做一次简单的哈希:

m = hash(id) mod n

这里,id 是用户的标识。n 是集群中 B 所在业务系统部署的数量。最终的 m 是我们需要发送到的目的队列编号。

假设,hash(id) 的结果为 2000,n 为 2,经过计算 m = 0。此时,A 就会把他和 B 的对话信息都发送到 chat00 的队列里。B 收到消息后,就会依次显示给终端用户。这样,聊天乱序的问题就解决了。

那么,事情到此就结束了吗?这个解决方案是完美的吗?

2. 看来,我们需要增加应用数量了

随着公司的发展,公司的人数也急剧上升,公司内部的 IM 使用人数也跟着多了起来,新问题又随之出现了。

最主要的问题是,人们收到聊天信息的速度变慢了。原因也很简单,收取聊天信息的集群机器不够用了。解决办法可以简单直接点,再加台机器就好了。

不过,由于收消息的集群中新加入了一台机器,这时候,我们还需要额外多作一些事情:

  1. 我们需要为新加入的这台机器上的应用额外再多增加一个队列 chat02。

  2. 我们还需要修改下我们的分配消息的规则,把原来的 hash(id) mod 2 修改为 hash(id) mod 3。

  3. 重新启动发送消息的项目,以便修改的规则生效。

  4. 把收消息的应用部署到新机器上。

到这时,一切还都在可控范围。开发人员只需要在需要的时候,新增加个队列,然后把我们的分配规则小小的修改下即可。

但是,他们不知道的是,暴风雨就要来了。

3. 新的问题来了,也许这就是人生吧

由于公司内部很多人在使用这个 IM 工具。有些时候,为了方便,公司的客户还有一些合作方也用起了这个 IM。这让事情变得复杂了起来。起初,开发人员还是像往常一样,每当人们抱怨说收消息过慢的时候,他们就会加一台机器。

最糟糕的是,公司的客户也会抱怨,他们发现 IM 有时候彻底不可用。这可不是小事情。公司内部人员的问题还可以内部沟通解决。但是公司客户的问题,大意不得,因为这关系到公司产品的名誉。

那么,这到底是怎么一回事呢?

原来,根本原因还在于每次修改完配置规则后的重启服务。每次修改完配置规则,就需要规划好一个恰当的停机时间,去重新对项目做个上线。

但是,这种方法在公司的客户也使用这个 IM 后就行不通了。因为公司的客户有不少是在国外的。也就是说,不管白天还是深夜,很可能总是有人在使用这个 IM。

这就迫使开发人员们,在增加机器时,还需要去和多方协调沟通出一个上线时间,然后发布公告,再去上线。这种反复沟通,再上线,再反复沟通,再上线直接把开发人员们折腾了个半死。

往往沟通完,上线时间直接被放到了半个月以后。而在这半个月里,开发人员还要承受无数内部 IM 使用人的口水。费心竭力的沟通,声嘶力竭的解释,缺眠少觉的上线,这一切的一切推动着开发人员们必须对眼前这套技术方案作出改变了。

4. 思路转起来,队列环起来

新的技术方案的需求本质就是:

无论是分配消息规则变化还是集群机器添加都不能停机停服务

对于这种情况,一个很好的解决方案就是如果我们对项目配置文件进行动态的定时检测,当发现变动时,刷新配置规则即可。

一切看上去很美好,采用了动态的定时检测后,每当我们需要新增集群中的机器时,我们只需要如下三个步骤了:

  1. 增加一个队列
  2. 修改分配消息的规则
  3. 部署新的机器

客户毫无感知,开发人员们也不需要和用户们协调沟通出专门的上线安排。可是,这个方案也存在一些问题:

  1. 随着我们的系统部署越来越多,我们需要手工修改规则的系统也越来越多。
  2. 如果消费机器宕机了,我们需要删除队列,同时还需要去删除修改分配消息的规则,等到机器恢复了,我们还要再把分配消息的规则改回去。

这个分配消息的规则真讨厌啊,每次有变动,就要去关心这个分配消息的规则。有没有什么办法能把这个分配变得更自动化一些呢?

如果我们假设在 MQ 中有 100 个收发聊天信息的队列(100:这是对我们的IM不可能达到的一个数字),我们只需要在配置规则中配置成:

hash(id) mod 100

然后,我们的发送消息的应用启动后,去动态的探测出真实的所有收发聊天信息的队列信息。

当我们通过哈希算出的编号发现没有真实对应的队列存在时,就根据一定的规则,去找到一个真实存在的队列,这个队列,就是我们要发消息的队列。

如果我们做到这样,那么以后,每次队列有变化,无论增多还是减少,我们都不需要再去考虑分配规则的事情了,只需要移除有问题的队列或者增加有对应消费者的队列即可。

这个思想,就是一致性哈希的思想。

具体怎么做呢?

第一步,我们假设有个 100 个收发聊天信息的队列,并且.........

展开阅读全文
加载中
点击引领话题📣 发布并加入讨论🔥
打赏
0 评论
0 收藏
0
分享
返回顶部
顶部