Redis 分片

Redis Sharding | Redis 分片

Partitioning(分片)

分片(partitioning)就是将你的数据拆分到多个 Redis 实例的过程,这样每个实例将只包含所有键的子集。Redis 的分片承担着两个主要目标:

  • 允许使用很多电脑的内存总和来支持更大的数据库。没有分片,你就被局限于单机能支持的内存容量。
  • 允许伸缩计算能力到多核或多服务器,伸缩网络带宽到多服务器或多网络适配器。

分片的缺点(Disadvantages)

Redis 的一些特性与分片在一起时玩转的不是很好:

涉及多个键的操作通常不支持。例如,你不能对映射在两个不同 Redis 实例上的键执行交集(事实上有办法做到,但不是直接这么干)。

涉及多个键的事务不能使用。

分片的粒度(granularity)是键,所以不能使用一个很大的键来分片数据集,例如一个很大的有序集合。

当使用了分片,数据处理变得更复杂,例如,你需要处理多个 RDB/AOF 文件,备份数据时你需要聚合多个实例和主机的持久化文件。

添加和删除容量也很复杂。例如,Redis 集群具有运行时动态添加和删除节点的能力来支持透明地再均衡数据,但是其他方式,像客户端分片和代理都不支持这个特性。但是,有一种称为预分片(Presharding)的技术在这一点上能帮上忙。

分片基础(Basic)

有很多不同的分片标准(criteria)。假想我们有 4 个 Redis 实例 R0,R1,R2,R3,还有很多表示用户的键,像 user:1,user:2,…等等,我们能找到不同的方式来选择一个指定的键存储在哪个实例中。换句话说,有许多不同的办法来映射一个键到一个指定的 Redis 服务器。

Range Partitioning(范围分片)

最简单的执行分片的方式之一是范围分片(range partitioning),通过映射对象的范围到指定的 Redis 实例来完成分片。例如,我可以假设用户从 ID 0 到 ID 10000 进入实例 R0,用户从 ID 10001 到 ID 20000 进入实例 R1,等等。这套办法行得通,并且事实上在实践中被采用,然而,这有一个缺点,就是需要一个映射范围到实例的表格。这张表需要管理,不同类型的对象都需要一个表,所以范围分片在 Redis 中常常并不可取,因为这要比替他分片可选方案低效得多。

Hash Partitioning(哈希分片)

一种范围分片的替代方案是哈希分片(hash partitioning)。这种模式适用于任何键,不需要键像 object_name:这样的饿形式,就像这样简单:

  • 使用一个哈希函数(例如,crc32 哈希函数)将键名转换为一个数字。例如,如果键是 foobar,crc32(foobar)将会输出类似于 93024922 的东西。
  • 对这个数据进行取模运算,以将其转换为一个 0 到 3 之间的数字,这样这个数字就可以映射到我的 4 台 Redis 实例之一。93024922 模 4 等于 2,所以我知道我的键 foobar 应当存储到 R2 实例。注意:取模操作返回除法操作的余数,在许多编程语言总实现为%操作符。

分片实现

Client Side Partitioning

客户端分片(Client side partitioning)意味着,客户端直接选择正确的节点来写入和读取指定键,把分片的逻辑放在 Redis 客户端实现,通过 Redis 客户端预先定义好的路由规则,把对 Key 的访问转发到不同的 Redis 实例中,最后把返回结果汇集。

客户端分片的优势在于所有的逻辑都是可控的,不依赖于第三方分布式中间件。开发人员清楚怎么实现分片、路由的规则,不用担心踩坑。而缺点在于:

  • 这是一种静态的分片方案,需要增加或者减少 Redis 实例的数量,需要手工调整分片的程序。
  • 可运维性差,集群的数据出了任何问题都需要运维人员和开发人员一起合作,减缓了解决问题的速度,增加了跨部门沟通的成本。
  • 在不同的客户端程序中,维护相同的分片逻辑成本巨大。例如,系统中有两套业务系统共用一套 Redis 集群,一套业务系统用 Java 实现,另一套业务系统用 PHP 实现。为了保证分片逻辑的一致性,在 Java 客户端中实现的分片逻辑也需要在 PHP 客户端实现一次。相同的逻辑在不同的系统中分别实现,这种设计本来就非常糟糕,而且需要耗费巨大的开发成本保证两套业务系统分片逻辑的一致性。

Proxy assisted partitioning:代理协助分片

我们的客户端发送请求到一个可以理解 Redis 协议的代理上,而不是直接发送请求到 Redis 实例上。代理会根据配置好的分片模式,来保证转发我们的请求到正确的 Redis 实例,并返回响应给客户端。Redis 和 Memcached 的代理 Twemproxy 实现了代理协助的分片。

Query routing:查询路由方式

查询路由意味着,你可以发送你的查询到一个随机实例,这个实例会保证转发你的查询到正确的节点。Redis 集群在客户端的帮助下,实现了查询路由的一种混合形式(请求不是直接从 Redis 实例转发到另一个,而是客户端收到重定向到正确的节点)。 Redis 3.0 集群采用了 P2P 的模式,完全去中心化。Redis 把所有的 Key 分成了 16384 个 slot,每个 Redis 实例负责其中一部分 slot。集群中的所有信息(节点、端口、slot 等),都通过节点之间定期的数据交换而更新。

Redis 客户端在任意一个 Redis 实例发出请求,如果所需数据不在该实例中,通过重定向命令引导客户端访问所需的实例。

Redis 3.0 集群的工作流程如图 4 所示。

img

图 4Redis 3.0 集群的工作流程图

如图 4 所示 Redis 集群内的机器定期交换数据,工作流程如下。

(1) Redis 客户端在 Redis2 实例上访问某个数据。

(2) 在 Redis2 内发现这个数据是在 Redis3 这个实例中,给 Redis 客户端发送一个重定向的命令。

(3) Redis 客户端收到重定向命令后,访问 Redis3 实例获取所需的数据。

Redis 3.0 的集群方案有以下两个问题。

  • 一个 Redis 实例具备了“数据存储”和“路由重定向”,完全去中心化的设计。这带来的好处是部署非常简单,直接部署 Redis 就行,不像 Codis 有那么多的组件和依赖。但带来的问题是很难对业务进行无痛的升级,如果哪天 Redis 集群出了什么严重的 Bug,就只能回滚整个 Redis 集群。
  • 对协议进行了较大的修改,对应的 Redis 客户端也需要升级。升级 Redis 客户端后谁能确保没有 Bug?而且对于线上已经大规模运行的业务,升级代码中的 Redis 客户端也是一个很麻烦的事情。

综合上面所述的两个问题,Redis 3.0 集群在业界并没有被大规模使用。

High Available Cluster | 高可用集群

避免集群倾斜