Redis成本优化-版本升级-3.hashtable优化
本文是Redis成本优化系列文章的第3篇,讲述了hashtable的相关优化(系列文章相关目录见文末)
一、几个实验
hash相关配置
before 7.0
hash-max-ziplist-entries:512
hash-max-ziplist-value:64
after 7.0
hash-max-listpack-entries 512
hash-max-listpack-value 64如下三个场景,hash的内部实现都是hashtable,而不是ziplist:
1.写入100万条hash键值对:key为37字节(hash-32字节uuid)、2对field(f0、f1)-value(100字节)
版本  | 2.8.24  | 3.0.7  | 3.2.13  | 4.0.14  | 5.0.14  | 6.0.20  | 6.2.15  | 7.0.12  | 
容量MB  | 603.15  | 603.15  | 549.75  | 488.77  | 488.77  | 488.77  | 488.77  | 450.71  | 
2. 写入1万条hash键值对:key为37字节(hash-32字节uuid)、514对field(f0..f513)-value(5字节)
版本  | 2.8.24  | 3.0.7  | 3.2.13  | 4.0.14  | 5.0.14  | 6.0.15  | 6.2.15  | 7.0.12  | 
容量MB  | 589.94  | 589.94  | 511.29  | 315.27  | 315.27  | 315.27  | 315.27  | 275.90  | 
3. 写入1万条hash键值对:key为37字节(hash-32字节uuid)、514对field(f0..f513)-value(100字节)
版本  | 2.8.24  | 3.0.7  | 3.2.13  | 4.0.14  | 5.0.14  | 6.0.15  | 6.2.15  | 7.0.12  | 
容量MB  | 1060.52  | 1060.52  | 981.87  | 825.07  | 825.07  | 825.07  | 825.07  | 824.76  | 
初步结论:
- 2.8到3.0:hashtable容量无变化
 - 3.0到3.2:优化来源于sdshdr拆分,详见Redis成本优化-版本升级-1.SDS优化历史
 - 3.2到4.0:优化来源于复杂数据架构中的元素去掉robj,详见Redis成本优化-版本升级-2.复杂数据结构robj优化
 - 4.0->5.0->6.0->6.2:无容量变化
 - 7.0:在实验1和实验场景2下容量有一定程度缩减(8%、14%)
 
二、7.0已知hash优化:dict结构简化
dict是Redis的基础数据结构,Redis的全部键值、过期键值、hash|set|zset数据结构均用到了dict,Redis 7做了重要优化:
来源:Significant memory savings in case of many hash or zset keys (#9228)1 优化前的dict结构
7.0之前:dict内部包含了两个dictht

相关代码:
typedef struct dictht {
    dictEntry **table;
    unsigned long size;
    unsigned long sizemask;
    unsigned long used;
} dictht;
typedef struct dict {
    dictType *type;
    void *privdata;
    dictht ht[2];
    long rehashidx;
    int16_t pauserehash;
} dict;2 Redis 7.0的相关优化
- 去掉privdata
 - 去掉dictht,相关元数据放到了dict中。
 

相关代码变为:
struct dict {
    dictType *type;
    dictEntry **ht_table[2];
    unsigned long ht_used[2];
    long rehashidx;
    int16_t pauserehash;
    signed char ht_size_exp[2]; 
};- dictht ht[2]用dictEntry **ht_table[2]代替
 - 原来两个dictht的used用unsigned long ht_used[2]代替
 - 原来两个dictht的size用signed char ht_size_exp[2]代替,且由8个字节变为1个字节,计算方法如下:
 
#define DICTHT_SIZE(exp) ((exp) == -1 ? 0 : (unsigned long)1<<(exp))
#define DICTHT_SIZE_MASK(exp) ((exp) == -1 ? 0 : (DICTHT_SIZE(exp))-1)3. github上的相关效果:
通过数据结构的优化(96->56字节),必然会在hash|set|zset key较多且为小value时,效果更为明显。由于要对单一功能进行测试,所以这里直接使用PR中的测试结果(https://github.com/redis/redis/pull/9228),
(1) 内存优化:提升较为明显
original dict  | original dict  | optimized dict  | 
1000000 65 byte one field hashes  | 290.38M  | 252.23M  | 
1000 hashes with 1000 20 byte fields  | 62.22M  | 62.1M  | 
1000000 sets with 1 1 byte entry  | 214.84M  | 176.69M  | 
dict struct size (theoretical improvement)  | 96b  | 56b  | 
(2) 性能变化:除了get random keys外,其他整体有提升。
dict benchmark 10000000  | Inserting  | Linear access of existing  | 2nd round  | Random access of existing  | Accessing random keys  | Accessing missing  | Removing and adding  | 
original  | 5371  | 2531  | 2507  | 4135  | 1447  | 2893  | 4882  | 
optimized  | 5253  | 2488  | 2481  | 4076  | 1600  | 2841  | 4801  | 
improvement  | 2.20%  | 1.70%  | 1.04%  | 1.43%  | -10.57%  | 1.80%  | 1.66%  | 
三、Redis 7.0 hash其他优化?
1. 问题分析

按照上一节分析,我们可以计算下相关实验
- 实验1:1000000(key) * (96-56) = 38.14MB (基本符合预期)
 - 实验2、3:10000(key) * (96-56) = 0.38MB ,完全不符合预期!
 
翻遍了Redis 7 release notes好像没发现其他hash优化,并且对测试程序多次测试,数值上没什么异常!
2. 问题定位
我们对实验2中的hash key进行debug可以发现
(1) 针对每个hash key, Redis 6.2.5会比Redis 7.0.12的table size大512个,也就是512*8字节(指针) = 4KB,针对上述实验二正好吻合。
10000个hash * 4KB 约等于 40MB- Redis 6.2.15正在进行rehash,table size = 512 + 1024
 
Redis:6.2.15> debug HTSTATS-KEY hash-c25f6e05-8cd7-4c00-9776-d1d53e3e3b00
Hash table 0 stats (main hash table):
 table size: 512
 number of elements: 509
 different slots: 325
 max chain length: 6
 avg chain length (counted): 1.57
 avg chain length (computed): 1.57
 Chain length distribution:
   0: 187 (36.52%)
   1: 200 (39.06%)
   2: 83 (16.21%)
   3: 30 (5.86%)
   4: 8 (1.56%)
   5: 3 (0.59%)
   6: 1 (0.20%)
Hash table 1 stats (rehashing target):
 table size: 1024
 number of elements: 5
 different slots: 4
 max chain length: 2
 avg chain length (counted): 1.25
 avg chain length (computed): 1.25
 Chain length distribution:
   0: 1020 (99.61%)
   1: 3 (0.29%)
   2: 1 (0.10%)- Redis 7.0.12只用了一个dictht, table size=1024
 
Hash table 0 stats (main hash table):
 table size: 1024
 number of elements: 514
 different slots: 395
 max chain length: 4
 avg chain length (counted): 1.30
 avg chain length (computed): 1.30
 Chain length distribution:
   0: 629 (61.43%)
   1: 296 (28.91%)
   2: 81 (7.91%)
   3: 16 (1.56%)
   4: 2 (0.20%)(2) 相关优化:https://github.com/redis/redis/pull/8943

每次对hash类型(当前是ziplist实现)进行添加元素时,会进行元素个数的检测,当ziplist的长度 > hash-max-ziplist-entries时,会将其转成一个hashtable
t_hash(before 7.0):具体方法是创建一个空的dict然后逐个遍历ziplist执行dictAdd,在这个过程中会触发到rehash(512这个元素这个点,有关rehash可以参考Redis开发规范解析(三)--一个Redis最好存多少key),所以hash的内部实现有两个dictht。
void hashTypeConvertZiplist(robj *o, int enc) {
    serverAssert(o->encoding == OBJ_ENCODING_ZIPLIST);
   if (enc == OBJ_ENCODING_HT) {
        hashTypeIterator *hi;
        dict *dict;
        int ret;
        hi = hashTypeInitIterator(o);
        dict = dictCreate(&hashDictType, NULL);
        while (hashTypeNext(hi) != C_ERR) {
            sds key, value;
            key = hashTypeCurrentObjectNewSds(hi,OBJ_HASH_KEY);
            value = hashTypeCurrentObjectNewSds(hi,OBJ_HASH_VALUE);
            ret = dictAdd(dict, key, value);
            if (ret != DICT_OK) {
                //忽略内容
            }
        }
        hashTypeReleaseIterator(hi);
        zfree(o->ptr);
        o->encoding = OBJ_ENCODING_HT;
        o->ptr = dict;
    }
}t_hash(after 7.0):提前进行扩容,所以只有一个dictht

3. 问题验证
(1) 如果514对换成1023对呢?可以看到7.0相比6.2并无优化
3. 写入1万条hash键值对:key为37字节、1023对field(f0..f1022)-value(5字节)
2.8.24  | 3.0.7  | 3.2.13  | 4.0.14  | 5.0.14  | 6.0.15  | 6.2.15  | 7.0.12  | |
容量MB  | 1016.85  | 1016.85  | 862.32  | 470.38  | 470.38  | 470.38  | 470.38  | 470.04  | 
Redis:6.2.15> debug htstats-key hash-aa51a2cd-21d9-4ce1-9440-77b4ef5863c6
Hash table 0 stats (main hash table):
 table size: 1024
 number of elements: 1023
 different slots: 654
 max chain length: 5
 avg chain length (counted): 1.56
 avg chain length (computed): 1.56
 Chain length distribution:
   0: 370 (36.13%)
   1: 397 (38.77%)
   2: 166 (16.21%)
   3: 73 (7.13%)
   4: 15 (1.46%)
   5: 3 (0.29%)
Redis:7.0.12> debug htstats-key hash-ed298442-e907-4736-bf0b-57fb0578b12e
Hash table 0 stats (main hash table):
 table size: 1024
 number of elements: 1023
 different slots: 639
 max chain length: 5
 avg chain length (counted): 1.60
 avg chain length (computed): 1.60
 Chain length distribution:
   0: 385 (37.60%)
   1: 364 (35.55%)
   2: 191 (18.65%)
   3: 60 (5.86%)
   4: 23 (2.25%)
   5: 1 (0.10%)(2) 实验2中,如果我们重启6.0.15、6.2.15版本呢?会发现内存消耗也会减少大约40MB
四、总结:
Redis 7针对hashtable结构的成本优化有两个
- 精简了dict结构,使得每个dict可以减少40字节,在有大量小hash、小zset的场景下有较大优化
 - 优化了ziplist转hashtable时候可能产生的rehash,这个优化就一行代码,比较隐蔽,在特殊场景下也能取得不错的优化。
 
文章转载自公众号:Redis开发运维实战




















