重分片对 Redis 集群的性能影响分析¶
关于 Redis 集群,
存在着一种非常常见的说法,
那就是:
因为 MOVED
转向和 ASK
转向的存在,
一个客户端可能需要访问不止一个节点才能获取到它想要的键,
因此 Redis 集群在进行重分片的时候会有性能损耗,
并且 Redis 集群单个节点的访问性能也比不上 Redis 单机服务器的性能。
这种说法初看上去似乎有一定道理, 但实际上并不准确。 本文将对 Redis 集群的转向机制进行详细的介绍, 并说明为什么转向不会给集群的性能带来影响。
转向¶
Redis 集群是通过分片(shard)方式, 将一个数据库划分为多个部分, 并将不同部分交给集群中的不同服务器来处理, 从而达到扩展性能的目的。
其中, 数据库的每个部分就是一个槽(slot), 一个槽可以包含任意多个键(key); 而集群中的每个服务器则是一个节点(node)。
随着数据量的增多或者访问量的加大, 集群中的一个或多个节点可能会无法继续处理之前分配给它们的槽, 这时用户就需要对集群进行重分片(reshard), 也即是, 给集群增加更多节点, 并把之前由已有节点处理的槽分配一部分给新节点负责。
Redis 集群在重分片操作执行的过程中, 就会产生 ASK 转向:
对于槽 s 包含的键 k 来说, 它在进行重分片的过程中, 既可能存在于原节点里面, 也有可能存在于新节点里面。
当客户端想要获取键 k 时, 它首先会访问原节点, 如果找到就直接返回键 k 的值。
如果客户端没有在原节点找到键 k , 那么原节点就会向客户端返回一个 ASK 转向, 让它尝试到新节点里面进行寻找。
因此, 如果一个客户端在访问时遇到了 ASK 转向, 那么它可能需要访问两个节点才能找到它想要找的键。
另一方面, 当 Redis 集群的重分片操作执行完毕之后, 集群中就会产生 MOVED 转向:
在重分片之后, 原来位于槽 s 的键 k 已经从原节点迁移到了新节点。
如果一个没有察觉到集群槽分配情况已经发生了变化的客户端继续尝试向原节点获取槽 s 的键 k , 那么原节点就会向客户端返回一个 MOVED 转向, 告知客户端, 槽 s 已经被转移到了新节点, 而得到这一信息的客户端则会重新向新节点发送一个命令请求, 尝试获取键 k 。
因此, 如果一个客户端在访问时遇到了 MOVED 转向, 那么它也可能需要访问两个节点才能找到它想要的键。
总的来说, 当 ASK 转向和 MOVED 转向出现时, 客户端可能需要两次访问才能获取到一个键, 那么这两个转向是否会对客户端访问 Redis 集群的性能带来影响呢?
MOVED 转向会影响性能吗?¶
在一般情况下, Redis 集群的客户端都会在内部缓存集群的槽分配情况, 比如这样:
槽 |
负责的节点 |
---|---|
0~5461 |
node_a |
5462~10922 |
node_b |
10923~16383 |
node_c |
然后, 每当用户想要访问键 k 时, 客户端都会通过以下公式, 计算出键 k 所在的槽:
slot = CRC16(k) % 16384
然后客户端就可以根据 slot
的值以及槽分配的表格,
直接对槽所在的节点进行访问,
在这种情况下,
客户端只需要一次访问就可以获取到键 k 的值。
另一方面,
当集群完成了重分片操作之后,
它的槽分配情况就会发生变化,
这时客户端的访问就可能会遇到 MOVED
转向。
在第一次遇到 MOVED
转向之后,
客户端就可以通过发送 CLUSTER SLOTS 来获取并更新自己内部缓存的槽分配情况,
就像这样:
槽 |
负责的节点 |
---|---|
0~5461 |
node_a |
5462~10922 |
node_b |
10923~13000 |
node_c |
13001~16383 |
node_d |
在此之后, 客户端就可以跟之前一样, 通过一次访问来获取指定的键了。
因为 MOVED
转向对于每个客户端最多只会带来一次额外的访问,
所以这个转向毫无疑问是不会对性能产生影响的。
ASK 转向会影响性能吗?¶
在集群进行重分片的过程中, 获取一个键的值可能需要访问两个节点, 所以在理论上来说, 在重分片的过程中, 获取一个键所需的时间会比平时多出一倍。 但是如果我们实际地考察 ASK 转向出现的条件, 就会发现 ASK 转向在实际中对集群的影响是很小的:
重分片并不是一个常见的操作, 这个操作通常需要间隔一段时间才执行一次, 至少是一个月, 甚至更长的时间。 如果你需要频繁地对集群进行重分片, 那么说明你没有仔细地规划各个节点的槽分配情况。
跟集群的上线时间相比, 集群进行重分片所需的时间通常很短暂, 也即是, ASK 转向在集群里面出现的时间是很短的。
举个例子, 假设一个 Redis 集群, 它在运行一个月之后进行了一次时长为 15 分钟的重分片操作, 那么 ASK 转向出现的时长在整个集群的运行时长中所占的比例可以通过以下计算得出:
整个集群的运行时长为: 30天 * 24小时 * 60分钟 = 43200 分钟
重分片时长在集群运行时长中所占的比例为: 15 / 43200 = 0.00034722222222222224
这也就是说,集群进行重分片的时长只占整个集群运行时长的约 0.035%
最后, 根据概率, 即使在重分片进行的过程中, 客户端无需经过 ASK 转向就能成功取得数据的几率为 50% , 而经过 ASK 转向才能取得数据的几率也同样为 50% , 所以 ASK 转向真正对集群产生影响的时间, 实际上就是 15 分钟的一半, 也即是 7.5 分钟, 这也就是说:
在集群进行重分片的过程中, ASK 转向真正对集群产生影响的时长只有 7.5 分钟, 这段时间只占整个集群运行时长比例的 0.00017361111111111112 , 也即是 0.017% 。
如果用户进行重分片的间隔更长一些, 比如几个月才重分片一次, 又或者重分片的执行时间更短一些, 那么这个值将会变得更小, 甚至可以忽略不计。
因为 ASK 转向对客户端可能造成的最坏影响就是需要两次访问才能够获取到数据, 但 Redis 服务器的访问速度是如此之快, 并且 ASK 转向在集群运行时间中所占的比例又是如此之小, 所以我们可以断言, ASK 转向并不会对集群的性能产生影响。
重分片真正的性能问题所在¶
既然 MOVED 转向和 ASK 转向都不会对性能造成影响, 那么是否可以在线对集群进行重分片呢? 答案是不行的。
因为 Redis 集群目前在进行重分片的时候, 会使用 MIGRATE 命令, 将被迁移的槽包含的每个键从原节点移动到新节点, 就像这样:
for key in all_keys_in_target_slot:
使用 MIGRATE 将键 key 从当前节点(原节点)移动到新节点
并且在每个 MIGRATE
命令执行的过程中,
原节点和新节点都会被阻塞,
直到命令执行完毕为止。
因此如果你直接对生产环境中的集群执行重分片操作,
而涉及该操作的两个节点正好又是被频繁访问的节点的话,
那么访问这两个节点的其他客户端就很可能会出现大量的超时错误。
换句话来说,
重分片的确会对 Redis 集群的性能带来影响,
最起码会对正在进行重分片的两个节点带来影响,
但这种性能影响不是因为转向而引起的,
而是因为 MIGRATE
命令引起的。
另外,
这里再说一个也许很多人都没有注意到的问题 ——
如果你认真地去看 redis-trib.rb
里面的重分片操作实现,
就会发现这个操作是有一个竞争条件在里面的:
如果客户端在重分片的过程中,
仍然对原节点正在迁移的槽进行写入,
那么就可能会导致重分片操作因为出错而失败,
这时你就只能自己写脚本去把这个节点未完成的迁移工作做完。
因此,
即使你不在乎 MIGRATE
命令带来的性能问题,
但如果你不想给自己找麻烦的话,
也请不要对正在被频繁访问的 Redis 集群进行重分片。
总的来说, 在 Redis 重分片的相关命令和操作尚未改善到足够好的地步之前, 重分片操作最好还是在集群离线的情况下进行, 也即是, 在没有外部客户端访问集群的情况下进行。
软件从来都不是一个黑箱¶
抽象也许是软件开发中最有用也最重要的一项技术, 无论是多么复杂的软件, 我们总可以把它们抽象成一个简单的个体, 然后通过图形界面或者文字界面去操作它们。 在理想的世界中, 一个软件就像一个黑箱, 我们无需了解这个箱子的任何内部实现, 只要通过文档提供的 API 来操作它们即可。
但是在现实世界中, 软件从来都不是一个黑箱, 即使是最为简单的软件, 在执行最为简单的操作时, 都有可能会出现一个令你意想不到的错误。 这时如果你对软件的内部构造有足够的了解, 那么要定位和解决这个错误就不会是一件难事。 但如果你对于这个软件的内部构造一窍不通, 换句话说, 如果你真的把软件当成了一个黑箱来使用, 那么软件产生的所有超出预期的行为, 都会给你带来巨大的麻烦。
以本文的例子来说,
如果你对 Redis 集群的重分片机制的了解不够,
那么当你看到集群性能在重分片时出现了下降,
你可能就会认为这是转向带来的问题,
但是却没有意识到罪魁祸首实际上是 MIGRATE
命令。
又或者说,
如果你对 redis-trib.rb
中的重分片操作的实现不够了解,
那么你就可能会一边对集群进行重分片,
一边对集群进行写入,
然后一个偶然的机会,
嘭的一下,
redis-trib.rb
就因为错误而退出了,
只留下一个灰溜溜的、重分片了一半的集群给你。
真实世界中的软件从来都不是一个黑箱, 无论是哪个软件, 如果你想要真正地了解它, 那么就一定要熟读它的文档, 如果有可能的话, 还要深入地了解它的源码。 这样的话, 即使这个软件出现任何问题, 你也会有足够的能力去解决它。