MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Redis rehash 对系统稳定性的影响

2022-11-263.6k 阅读

Redis 中的哈希表

在深入探讨 Redis rehash 对系统稳定性的影响之前,我们先来了解一下 Redis 中的哈希表结构。Redis 使用哈希表来存储键值对,哈希表是一种非常高效的数据结构,能够在平均情况下以 O(1) 的时间复杂度进行插入、查找和删除操作。

Redis 中哈希表的实现由两个主要的数据结构组成:哈希表(dict)和哈希节点(dictEntry)。

哈希表(dict

哈希表的结构体定义如下(简化版,实际 Redis 代码中有更多字段用于管理和优化):

typedef struct dict {
    dictType *type;
    void *privdata;
    dictht ht[2];
    int rehashidx; /* rehashing not in progress if rehashidx == -1 */
    int iterators; /* number of iterators currently running */
} dict;

这里的 ht 是一个包含两个哈希表的数组,ht[0] 用于正常情况下存储数据,而 ht[1] 主要在 rehash 过程中使用。rehashidx 字段用于记录 rehash 的进度,当 rehashidx == -1 时,表示没有正在进行的 rehash 操作。

哈希节点(dictEntry

哈希节点结构体定义如下:

typedef struct dictEntry {
    void *key;
    union {
        void *val;
        uint64_t u64;
        int64_t s64;
        double d;
    } v;
    struct dictEntry *next;
} dictEntry;

每个 dictEntry 代表一个键值对,key 是键,v 是值,next 指针用于解决哈希冲突,通过链地址法将冲突的节点链接在一起。

哈希表的初始创建

当 Redis 创建一个新的哈希表时,它会初始化 ht[0],并将 ht[1] 初始化为 NULL。例如,当我们在 Redis 中执行 HSET myhash field1 value1 这样的命令来创建一个哈希表时,Redis 底层会为这个哈希表分配内存并初始化相关的数据结构。

什么是 Redis rehash

随着数据的不断插入和删除,哈希表可能会出现两种情况:负载因子过高或过低。为了保证哈希表的性能,Redis 需要调整哈希表的大小,这个过程就叫做 rehash。

负载因子的计算

负载因子(load factor)的计算公式为: [ \text{load factor} = \frac{\text{哈希表中已有的节点数量}}{\text{哈希表的大小}} ]

当负载因子过高(默认超过 1)时,说明哈希表中的节点过于密集,哈希冲突的概率增加,此时需要对哈希表进行扩展(扩大哈希表的大小)。当负载因子过低(默认小于 0.1)时,说明哈希表中的节点过于稀疏,占用了过多的内存,此时需要对哈希表进行收缩(缩小哈希表的大小)。

rehash 的过程

  1. 分配空间
    • 如果是扩展,会为 ht[1] 分配一个大小为 ht[0] 两倍的空间(如果 ht[0] 的大小小于 64,则直接分配 64 个槽位)。
    • 如果是收缩,会为 ht[1] 分配一个合适大小的空间,使得收缩后负载因子在合理范围内。
  2. 迁移数据
    • Redis 会逐步将 ht[0] 中的数据迁移到 ht[1] 中。这个迁移过程不是一次性完成的,而是分多次进行,每次迁移一部分数据。具体来说,每次执行哈希表相关的操作(如插入、查找、删除)时,会顺带迁移 ht[0] 中的一个或多个节点到 ht[1]
    • 迁移过程中,Redis 会根据 rehashidx 记录当前迁移到 ht[0] 的哪个槽位。当 ht[0] 中的所有数据都迁移到 ht[1] 后,ht[0] 被释放,ht[1] 成为新的 ht[0],同时 rehashidx 被设置为 -1,表示 rehash 完成。
  3. 更新指针: 完成迁移后,将 ht[1] 赋值给 ht[0],并将 ht[1] 重新初始化为 NULL

Redis rehash 对系统稳定性的影响

性能抖动

  1. 迁移过程中的额外开销: 在 rehash 的数据迁移阶段,每次哈希表操作除了原本的操作逻辑外,还需要额外执行数据迁移的工作。例如,在执行 HGET 命令获取哈希表中的一个值时,原本只需要根据哈希值查找对应的槽位和链表即可,但在 rehash 过程中,还需要迁移一个或多个 ht[0] 中的节点到 ht[1]。这就导致了每次操作的时间开销增加,可能会引起系统性能的抖动。

假设我们有一个简单的 Redis 哈希表操作的 Python 代码示例:

import redis

r = redis.Redis(host='localhost', port=6379, db=0)

# 填充一些数据
for i in range(10000):
    r.hset('myhash', f'field{i}', f'value{i}')

# 获取一个值
value = r.hget('myhash', 'field1')
print(value)

在正常情况下,上述代码执行 hget 操作的时间非常短。但如果此时 Redis 正在进行 rehash,由于每次 hget 操作可能会触发数据迁移,导致操作时间变长,从而影响系统的整体性能。

  1. 对高并发场景的影响: 在高并发环境下,大量的哈希表操作同时进行,rehash 带来的性能抖动问题会更加严重。每个操作都可能因为数据迁移而增加执行时间,这可能导致系统响应时间变长,甚至出现短暂的卡顿现象。例如,在一个高并发的 Web 应用中,多个请求同时访问 Redis 哈希表获取数据,如果此时 Redis 正在进行 rehash,这些请求的响应时间可能会明显增加,影响用户体验。

内存使用的变化

  1. 扩展时的内存增长: 在哈希表扩展时,Redis 会先为 ht[1] 分配一个比 ht[0] 更大的空间。在数据迁移完成之前,系统中会同时存在两个哈希表(ht[0]ht[1]),这就导致内存使用量瞬间增加。如果系统对内存使用比较敏感,这种突然的内存增长可能会导致系统内存不足,甚至引发系统崩溃。

例如,假设我们的系统原本内存使用接近上限,而 Redis 哈希表由于负载因子过高触发了扩展 rehash。在扩展过程中,ht[1] 分配了大量额外的内存,使得系统内存使用超过了限制,可能会导致操作系统开始进行内存交换(swap),严重影响系统性能。

  1. 收缩时的内存释放延迟: 当哈希表进行收缩 rehash 时,虽然最终会释放 ht[0] 的内存,但在整个迁移过程中,ht[0] 的内存并不会立即释放。只有当所有数据都迁移到 ht[1] 后,ht[0] 才会被释放。这就意味着在收缩 rehash 过程中,内存的释放是延迟的。如果系统需要及时回收内存以满足其他需求,这种延迟可能会导致系统资源分配不均衡。

数据一致性问题

  1. 迁移过程中的读写不一致: 在 rehash 的数据迁移过程中,由于数据逐步从 ht[0] 迁移到 ht[1],可能会出现读写不一致的情况。例如,在某个时刻,一个写操作将新数据写入了 ht[0],但紧接着的读操作可能从 ht[1] 中读取数据,而此时该数据还未迁移到 ht[1],就会导致读取到的数据不一致。

为了避免这种情况,Redis 在 rehash 过程中,所有的写操作都会同时写入 ht[0]ht[1],以保证数据的一致性。但这种策略也增加了写操作的复杂度和时间开销。

  1. 部分失败情况下的数据状态: 如果在 rehash 过程中出现系统故障(如 Redis 进程崩溃),可能会导致哈希表处于一个不一致的状态。例如,部分数据已经迁移到 ht[1],而部分还在 ht[0] 中。当 Redis 重启后,需要有相应的机制来恢复哈希表到一个一致的状态。Redis 通过记录 rehashidx 的值,在重启后可以继续从上次中断的地方进行 rehash,以保证数据的完整性和一致性。

如何应对 Redis rehash 对系统稳定性的影响

监控负载因子

  1. 定期检查负载因子: 通过 Redis 的 INFO 命令可以获取到哈希表的负载因子信息。例如,可以使用以下命令查看哈希表的相关信息:
redis-cli INFO memory | grep hash

其中会包含类似 used_memory_human:98.54K(内存使用情况)和 hash_table_size:1024(哈希表大小)等信息,通过计算可以得到负载因子。我们可以定期(如每隔几分钟)执行这个命令,监控负载因子的变化情况。

  1. 设置合理的阈值并报警: 根据系统的实际情况,设置合理的负载因子阈值。例如,如果系统对性能比较敏感,可以将扩展阈值设置为 0.75,收缩阈值设置为 0.2。当负载因子接近这些阈值时,通过监控系统(如 Prometheus + Grafana)发送报警信息,通知运维人员采取相应措施。

调整 rehash 策略

  1. 手动触发 rehash: 在一些情况下,我们可以手动触发 Redis 的 rehash 操作。例如,在系统负载较低的时间段,通过发送 BGREWRITEAOFBGSAVE 命令(这些命令会间接触发 rehash),让 Redis 在后台进行 rehash,以减少对正常业务的影响。

  2. 调整 rehash 速度: Redis 中 rehash 的速度是通过每次操作迁移的节点数量来控制的。虽然 Redis 目前没有直接提供调整这个参数的方法,但我们可以通过修改 Redis 源码并重新编译的方式来调整每次迁移的节点数量,以控制 rehash 的速度。不过这种方法需要谨慎使用,因为修改源码可能会带来兼容性等问题。

优化数据结构设计

  1. 避免过度使用哈希表: 如果系统中的数据结构可以用其他更简单的数据结构表示,尽量避免过度使用哈希表。例如,如果数据量较小且不需要频繁的查找操作,可以使用列表(list)或集合(set)等数据结构,以减少哈希表 rehash 带来的影响。

  2. 合理预估数据量: 在设计 Redis 数据结构时,尽量合理预估数据量的增长情况。如果能够提前知道数据量会在某个范围内增长,可以在创建哈希表时设置一个合适的初始大小,减少后续 rehash 的频率。例如,如果预计数据量不会超过 1000 个键值对,可以在创建哈希表时设置初始大小为 1024,这样在数据增长过程中可以减少扩展 rehash 的次数。

总结 Redis rehash 相关要点

Redis rehash 是维护哈希表性能的重要机制,但它对系统稳定性也会产生多方面的影响,包括性能抖动、内存使用变化和数据一致性问题等。通过监控负载因子、调整 rehash 策略和优化数据结构设计等方法,可以有效降低 rehash 对系统稳定性的影响,确保 Redis 在高并发、大数据量的场景下稳定运行。

在实际应用中,我们需要根据系统的特点和需求,灵活运用这些方法,保障 Redis 服务的高效和稳定,为上层应用提供可靠的数据存储和访问支持。同时,持续关注 Redis 的发展和优化,及时采用新的特性和策略,也是提升系统性能和稳定性的重要途径。

通过对 Redis rehash 的深入理解和合理应对,我们能够更好地利用 Redis 的强大功能,构建出高性能、高可用的数据存储和处理系统。无论是小型的 Web 应用,还是大规模的分布式系统,对 Redis rehash 的妥善处理都是确保系统稳定运行的关键因素之一。