Redis成本优化-版本升级-3.hashtable优化

ilikevc
发布于 2023-12-4 15:15
浏览
0收藏

本文是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

初步结论:

二、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

Redis成本优化-版本升级-3.hashtable优化-鸿蒙开发者社区

相关代码:

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中。

Redis成本优化-版本升级-3.hashtable优化-鸿蒙开发者社区

相关代码变为:

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. 问题分析

Redis成本优化-版本升级-3.hashtable优化-鸿蒙开发者社区

按照上一节分析,我们可以计算下相关实验

  • 实验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

Redis成本优化-版本升级-3.hashtable优化-鸿蒙开发者社区

每次对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

Redis成本优化-版本升级-3.hashtable优化-鸿蒙开发者社区

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开发运维实战

分类
标签
已于2023-12-4 15:15:09修改
收藏
回复
举报
回复
    相关推荐