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

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

话很少说,小故事开始。面试

前言

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

嗯,一致性哈希,分布式架构师必备良药,让咱们一块儿来尝尝它。算法

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 个收发聊天信息的队列,而且这些队列处于一个环上。

第二步,咱们获取到真实的收发聊天信息的队列数量,假设有 5 个。

第三步,咱们把真实的队列映射到咱们第一步假设的环中。

第四步,咱们经过分配规则 hash(id) mod 100 计算出对应的队列编号。

若是 hash(id) 的结果为 2000,那么算出的队列编号 m = 0。这时候,咱们一查,发现对应编号 0 的 chat00 队列确实存在,那么就直接发送消息到 chat00 中。

若是咱们的 hash(id) 的结果为 1999,那么算出的队列编号 m = 99。此时,咱们去查队列映射关系,发现 99 编号并无对应的真实队列。这时候怎么办?很简单,咱们顺时针继续往下找,找到谁了呢?0 对应的 chat00 队列,这是真实存在的,这时候,咱们就将消息发送到 chat00 队列中。

上面四步就是一个基本的一致性哈希算法了。

那么,这套一致性哈希算法知足咱们不想老是更新消息分配规则的需求吗?让咱们验证一下:

  1. 假设咱们须要在消费信息端集群增长一台机器
    咱们若是要增长一台机器,那么同时咱们也须要在 MQ 中增长一个队列。这时候,咱们的分配规则是 hash(id) mod 100,增长了队列后,真实的队列数假设为 6。此时,若是 hash(id) mod 100 的结果小于 6,那么分配的规则和没有增长机器的时候规则同样,之前分配到哪一个队列,如今仍是分配到哪一个队列。可是对于结果等于 6 的状况,则发生了变化。信息会被自动分配给 chat05。当分配给 chat05 后,新的消费者就会自动开始进入正常工做了,咱们不须要作任何人工干预,也不须要考虑分配规则的变化。

    增长机器之前:

    增长机器以后:

  2. 假设消费信息端集群一台机器宕机了
    模拟宕机,此时咱们会去减小一个队列。减小后的真实队列数量为 5,则正好和增长队列相反,m = 5 时,那么行为不会有任何变化,之前分到哪一个队列,仍是分到哪一个队列。若是 m = 6,因为已经不存在真实的队列了,就会作顺时针查找,结果找到 chat00,之前会分到 chat05 的就会被分到 chat00。而此时,chat00 因为正好有消费者,因此,系统的用户是毫无感知的,咱们也专心修复咱们机器便可。当机器恢复后,就会和新增机器同样,计算结果为 6 的信息会被从新分配回 chat05。

目前,咱们能够看到,当咱们引入一致性哈希后,咱们无论新增机器仍是集群机器宕机,我只须要跟随着机器的状态,作一个操做便可:增长或者减小 MQ 中的队列。一切简单化了。

那么,这个方案是否依然还有问题呢?

5. 失衡的圆环,压垮骆驼的可能只是一根稻草

假设咱们目前有 5 个队列存在,咱们的分配规则是 m = hash(id) mod 100。那么,此时,问题就出来了。

若是 m 的值大于 5,因为没有对应的真实队列存在,系统就会顺时针顺着咱们构造出来的哈希环找,最终会找到 chat00 这个队列上。

而后,你会发现,只要是 m 值大于 5 的 id 对应用户发的信息,最终都会落入到 chat00 队列中。

在极端状况下,若是大量的信息涌入到 chat00 队列里,因为对应 chat00 的消费者处理不过来,极可能会致使这个消费者的崩溃。

而后,去除队列后,根据规则,又会有大量的信息涌入到 chat00 后续的队列 chat01 里,这些信息又会致使 chat01 对应应用的崩溃,最终引起整个集群的崩溃,这就是雪崩效应。

咱们须要一种更巧妙的办法来解决这个问题。

6. 从实变虚,也许咱们应该更敢想一些

通过上面的论述,咱们发现,咱们在分配队列时,之因此失衡,是由于咱们的队列在圆环上的分配失衡。

咱们全部的真实队列都是按照顺时针依次排布在圆环上的。在上面的场景里,咱们只有 5 个队列。此时,咱们假设会有 100 个队列。那么,m = hash(id) mod 100 这个公式里:

m 大于 5 的几率为 95%

因为咱们的 5 个队列是按照编号顺序依次排列的。那就说明全部 m 大于 5 的信息就都会映射到一个不存在的队列上,最终,根据规则,顺时针滑到了 0 对应的 chat00 队列中。

若是,咱们可让真实存在的队列均匀分布到环上,那么,这种严重失衡的现象还会再出现吗?

从上面的图咱们能够看出,若是咱们能让真实的队列均匀的在圆环上分布,那么这种严重失衡的现象就会获得极大的缓解。

那么如何让这些队列能均匀的分布在这个圆环中呢?还记得咱们在苦恼分配信息规则的不断修改时,咱们大胆的假设了一个咱们的 IM 系统永远也不可能达到的队列数字吗?

咱们假设了 MQ 中有 100 个队列,而后,咱们去判断这些队列是否真实存在。不存在,咱们就顺时针滑动一直找到真实存在的队列为止。

若是咱们再大胆一点,偷偷的把咱们的假设进一步优化,把一些原本须要判断为不存在的队列去映射到真正已经存在的队列上,那么咱们是否是就等于把这些真正存在的队列均匀分布到这个圆环上了?

像上图这种,把已经存在的少许队列去映射到多个假设队列的方法,就是一致性哈希的虚拟节点办法。

而对于怎么让少许的队列映射到多个假设队列,是有多种实现算法存在的。

好比,咱们能够把真实存在的队列名加上一些编号去分别哈希一下, 像hash(chat00) mod 100,hash(chat00#1) mod 100,而后根据获得的余数,去把 chat00 这个真实队列和对应余数的环中的位置映射上。

若是 hash(chat00) mod 100 = 31,那么 31 号的位置就对应于 chat00,之后全部 m = hash(id) mod 100 中 m = 31的所对应的消息就会直接被发送到 chat00 队列。

而 hash(00#1) mod 100 = 56,则 m = 56对应的消息一样也会直接发送到 chat00 队列。

这样,咱们就间接的把 MQ 中的真实存在的队列作了均匀化分布,从而大大减小了信息失衡的现象。

7. 理解算法的思想胜于算法的实现

好了,经过实际场景来对于一致性哈希的思想就暂时剖析到这里了。

一致性哈希做为一种很是经典的算法思想,被普遍的用于各大分布式项目当中,用于解决各类分片问题,任务分发问题。

可是,在这里,我要纠正一个观点:不少人都在网上说 redis 使用了一致性哈希。这是错的,redis 只是使用了一致性哈希的思想。好比一致性哈希中的环分布,再好比虚拟节点对应真实节点的思想。

可是 redis 并无使用任何哈希算法去计算分布,若是有兴趣的读者,能够仔细去看下有关内容。从 redis 的例子上来讲,咱们能够看到,只有理解了算法的思想,咱们才能更容易更灵活地因地制宜的分解、修正、改进算法,让算法能更切合实际的融入到咱们的项目之中。

经过这篇文章咱们从哈希开始,一直到用到一致性哈希的虚拟节点分布,怎么样,您以为一致性哈希这道良药味道如何呢?

第一次写图解的文章,你们包容一下直男的审美!

第一次写图解的文章,画图真是累吐血了!求你们看完点个

我准备了一些纯手打的高质量PDF:
深刻浅出Java多线程、HTTP超全汇总、Java基础核心总结、程序员必知的硬核知识大全、简历面试谈薪的超全干货。

别看数量很少,但篇篇都是干货,看完的都说很肝。

领取方式:扫码关注后,在公众号后台回复:666