09 切片集群
Redis响应慢问题排查:使用INFO命令查看latest_fork_usec(最近一次fork)耗时
原因:fork 在执行时会阻塞主线程。数据量越大,fork 操作造成的主线程阻塞的时间越长。
数据存储方案
Redis 应对数据量增多的两种方案:纵向扩展(scale up)和横向扩展(scale out)。
- 纵向扩展:升级单个 Redis 实例的资源配置,包括增加内存、磁盘、CPU。
- 横向扩展:增加当前Redis实例数。
分布式管理
- 数据切片后,在多个实例之间如何分布?
- 客户端怎么确定想要访问的数据在哪个实例上?
切片和实例对应关系
Redis 3.0 开始,官方提供了一个名为 Redis Cluster 的方案,用于实现切片集群。
Redis Cluster 方案采用哈希槽(Hash Slot,接下来我会直接称之为 Slot),来处理数据和实例之间的映射关系。
在 Redis Cluster 方案中,一个切片集群共有 16384 个哈希槽,这些哈希槽类似于数据分区,每个键值对都会根据它的 key,被映射到一个哈希槽中。
具体过程:
- 根据键值对的 key,按照CRC16算法计算一个 16 bit 的值;
- 用这个 16bit 值对 16384 取模,得到 0~16383 范围内的模数,每个模数代表一个相应编号的哈希槽。
分配方案:
- 部署 Redis Cluster 方案时,可以使用 cluster create 命令创建集群,此时,Redis 会自动把这些槽平均分布在集群实例上。例如,如果集群中有 N 个实例,那么,每个实例上的槽个数为 16384/N 个。
也可以使用 cluster meet 命令手动建立实例间的连接,形成集群,再使用
cluster addslots
命令,指定每个实例上的哈希槽个数。redis-cli -h 172.16.19.3 –p 6379 cluster addslots 0,1 redis-cli -h 172.16.19.4 –p 6379 cluster addslots 2,3 redis-cli -h 172.16.19.5 –p 6379 cluster addslots 4
注意:手动分配哈希槽时,需要把 16384 个槽都分配完,否则 Redis 集群无法正常工作。
客户端定位数据
客户端和集群实例建立连接后,实例就会把哈希槽的分配信息发给客户端。
- 每个实例只知道自己被分配了哪些哈希槽,但是会把自己的哈希槽信息发给和它相连接的其它实例。当实例之间相互连接后,每个实例就有所有哈希槽的映射关系了。
- 客户端收到哈希槽信息后,会把哈希槽信息缓存在本地。当客户端请求键值对时,会先计算键所对应的哈希槽,然后就可以给相应的实例发送请求了
实例哈希槽变化情况:
- 在集群中,实例有新增或删除,Redis 需要重新分配哈希槽;
- 为了负载均衡,Redis 需要把哈希槽在所有实例上重新分布一遍。
重定向机制
当客户端把一个键值对的操作请求发给一个实例时,如果这个实例上并没有这个键值对映射的哈希槽,那么,这个实例就会给客户端返回下面的 MOVED 命令响应结果,这个结果中就包含了新实例的访问地址。
GET hello:key
(error) MOVED 13320 172.16.19.5:6379
ASK命令:当实例部分迁移时,会返回ASK
GET hello:key
(error) ASK 13320 172.16.19.5:6379
示例:Slot 2 正在从实例 2 往实例 3 迁移,key1 和 key2 已经迁移过去,key3 和 key4 还在实例 2。客户端向实例 2 请求 key2 后,就会收到实例 2 返回的 ASK 命令。
ASK 命令表示两层含义:
第一,表明 Slot 数据还在迁移中
第二,ASK 命令把客户端所请求数据的最新实例地址返回给客户端,此时,客户端需要给实例 3 发送 ASKING 命令,然后再发送操作命令。
和 MOVED 命令不同,ASK 命令并不会更新客户端缓存的哈希槽分配信息。
小结
集群的实例增减,或者是为了实现负载均衡而进行的数据重新分布,会导致哈希槽和实例的映射关系发生变化,客户端发送请求时,会收到命令执行报错信息。
问题:键值对的 key 先做 CRC 计算,再和哈希槽做映射的好处?
1、key的数量非常多时,直接记录每个key对应的实例映射关系,占用空间。
2、Redis Cluster采用无中心化的模式(无proxy,客户端与服务端直连),客户端在某个节点访问一个key,如果这个key不在这个节点上,这个节点需要有纠正客户端路由到正确节点的能力(MOVED响应),这就需要节点之间互相交换路由表,每个节点拥有整个集群完整的路由关系。如果存储的都是key与实例的对应关系,节点之间交换信息也会变得非常庞大,消耗过多的网络资源,而且就算交换完成,相当于每个节点都需要额外存储其他节点的路由表,内存占用过大造成资源浪费。
3、当集群在扩容、缩容、数据均衡时,节点之间会发生数据迁移,迁移时需要修改每个key的映射关系,维护成本高。
4、而在中间增加一层哈希槽,可以把数据和节点解耦,key通过Hash计算,只需要关心映射到了哪个哈希槽,然后再通过哈希槽和节点的映射表找到节点,相当于消耗了很少的CPU资源,不但让数据分布更均匀,还可以让这个映射表变得很小,利于客户端和服务端保存,节点之间交换信息时也变得轻量。 5、当集群在扩容、缩容、数据均衡时,节点之间的操作例如数据迁移,都以哈希槽为基本单位进行操作,简化了节点扩容、缩容的难度,便于集群的维护和管理。
10 问题解答
课后问题
整数数组和压缩列表作为底层数据结构的优势是什么?
节省空间。
整数数组和压缩列表都是在内存中分配一块地址连续的空间,然后把集合中的元素一个接一个地放在这块空间内,非常紧凑。
Redis 之所以采用不同的数据结构,其实是在性能和内存使用效率之间进行的平衡。
Redis 基本 IO 模型中还有哪些潜在的性能瓶颈?
在 Redis 基本 IO 模型中,主要是主线程在执行操作,任何耗时的操作,例如 bigkey、全量返回等操作,都是潜在的性能瓶颈。
AOF 重写过程中有没有其他潜在的阻塞风险?
1.fork创建 bgrewriteaof 子进程时,会阻塞主线程。子进程要拷贝父进程的页表,页表越大,执行时间越长。
2.bgrewriteaof 子进程和主线程共享内存。主线程新写或修改时会申请新的内存,操作系统在分配内存空间时,有查找和锁的开销,这就会导致阻塞。
AOF 重写为什么不共享使用 AOF 本身的日志?
如果都用 AOF 日志的话,主线程要写,bgrewriteaof 子进程也要写,这两者会竞争文件系统的锁,这就会对 Redis 主线程的性能造成影响。
为什么主从库间的复制不使用 AOF?
1.RDB 文件是二进制文件,无论是要把 RDB 写入磁盘,还是要通过网络传输 RDB,IO 效率都比记录和传输 AOF 的高。
2.在从库端进行恢复时,用 RDB 的恢复效率要高于用 AOF。
在主从切换过程中,客户端能否正常地进行请求操作呢?
主从集群一般是采用读写分离模式,当主库故障后,客户端仍然可以把读请求发送给从库,让从库服务。但是,对于写请求操作,客户端就无法执行了。
如果想要应用程序不感知服务的中断,还需要哨兵或客户端再做些什么吗?
- 客户端需要能缓存应用发送的写请求。只要不是同步写操作(Redis 应用场景一般也没有同步写),写请求通常不会在应用程序的关键路径上,所以,客户端缓存写请求后,给应用程序返回一个确认就行。
- 主从切换完成后,客户端要能和新主库重新建立连接,哨兵需要提供订阅频道,让客户端能够订阅到新主库的信息。同时,客户端也需要能主动和哨兵通信,询问新主库的信息。
哨兵实例是不是越多越好呢?如果同时调大 down-after-milliseconds 值,对减少误判是不是也有好处?
哨兵实例越多,误判率会越低;
但是在判定主库下线和选举 Leader 时,实例需要拿到的赞成票数也越多,等待所有哨兵投完票的时间可能也会相应增加,主从库切换的时间也会变长,客户端容易堆积较多的请求操作,可能会导致客户端请求溢出,从而造成请求丢失。如果业务层对 Redis 的操作有响应时间要求,就可能会因为新主库一直没有选定,新操作无法执行而发生超时报警。
调大 down-after-milliseconds 后,可能会导致这样的情况:主库实际已经发生故障了,但是哨兵过了很长时间才判断出来,这就会影响到 Redis 对业务的可用性。
为什么 Redis 不直接用一个表,把键值对和实例的对应关系记录下来?
1.如果使用表记录键值对和实例的对应关系,一旦键值对和实例的对应关系发生了变化(例如实例有增减或者数据重新分布),就要修改表。
2.如果数据量非常大,使用表记录键值对和实例的对应关系,需要的额外存储空间也会增加。
典型问题
1.什么时候做rehash?
- Redis 会使用装载因子(load factor)来判断是否需要做 rehash。
装载因子的计算方式是,哈希表中所有 entry 的个数除以哈希表的哈希桶个数。
- 装载因子≥1,同时,哈希表被允许进行 rehash(没有进行APF和RDB);
- 装载因子≥5。
注意:在进行 RDB 生成和 AOF 重写时,哈希表的 rehash 是被禁止的,这是为了避免对 RDB 和 AOF 重写造成影响。
2.Redis 会执行定时任务,定时任务中就包含了 rehash 操作。
2.主线程、子进程和后台线程的关系
- 主进程:一个进程启动后,没有再创建额外的线程,那么,这样的进程一般称为主进程或主线程。
子进程:在主线程中,使用 fork 创建子进程
- 创建 RDB 的后台子进程,同时由它负责在主从同步时传输 RDB 给从库;
- 通过无盘复制方式传输 RDB 的子进程;
- bgrewriteaof 子进程。
- 使用 pthread_create 创建后台线程,自行执行一些任务,例如执行异步删除任务。
3. 写时复制的底层实现机制
Redis 在使用 RDB 方式进行持久化时,主线程 fork 出 bgsave 子进程后,bgsave 子进程实际是复制了主线程的页表。
此时主线程接收到了新写或修改操作,那么,主线程会使用写时复制机制。
写时复制就是指,主线程在有写操作时,才会把这个新写或修改后的数据写入到一个新的物理地址中,并修改自己的页表映射。
4. replication buffer 和 repl_backlog_buffer 的区别
- replication buffer 是主从库在进行全量复制时,记录主库在全量复制期间的写操作的buffer;
replication buffer 不是共享的,而是每个从库都有一个对应的客户端。 - repl_backlog_buffer 是为了支持从库增量复制,主库上用于持续保存写操作的一块专用 buffer;
是所有从库共享的。