1. Redis の再ハッシュ問題のレビュー
簡単に確認した後、キーのしきい値を超えた後は、追加のハッシュ テーブル メモリ サイズが必要になります。
キー値の数 | 追加のハッシュ テーブル メモリ サイズが必要 |
---|---|
134,217,728 | 2GB |
67,108,864 | 1GB |
33,554,432 | 512.0MB |
16,777,216 | 256.0MB |
8,388,608 | 128.0MB |
4,194,304 | 64.0MB |
2,097,152 | 32.0MB |
1,048,576 | 16.0MB |
524,288 | 8.0MB |
キーの数が増えると、再ハッシュに必要な追加メモリが増加し、その結果、可用性リスク (大量エビクション、Redis 同期) とデータ損失 (エビクション) リスクが高まります。
2. Redis 6.2&7+ Rehash 関連の最適化
Rehash は Redis 6.2 以降に最適化されました。
Limit the main db dictionaries expansion to prevent key eviction (#7954)
In the past big dictionary rehashing could result in massive data eviction.Now this rehashing is delayed (up to a limit), which can result in performance loss due to hash collisions.
簡単な翻訳:
在大的hash表(键值数多)场景下,rehash会延迟执行防止大量数据丢失和可用性问题。
3. 実験
1. バージョンの選択
-
Redis 6.0.15
-
Redis 7.0.11
2. 実験条件
-
最大メモリ = 5.6GB
-
最初の注入: 67,100,000 個のキー、メモリを観察する
-
2 回目のプッシュ: 2,000,000 キー、可用性とエビクションを監視
3. 実験を開始する
(1) Redis 6.0.15
最初の注入: 67,100,000 キー、メモリを観察します。
maxmemory: 5.6GB
used_memory_human: 5.5GB
dbsize: 67,100,000
2 回目のプッシュ: 2,000,000 キー、可用性とエビクションを監視
-
クライアントの実行がタイムアウトしました。タイムアウトはほぼ 30 秒以上でした。redis-cli --latency-history
min: 0, max: 1, avg: 0.06 (1485 samples) -- 15.01 seconds range min: 0, max: 36511, avg: 45.63 (801 samples) -- 44.61 seconds range
-
大量のキーを削除します: 500 万以上
evicted_keys:5,909,376
-
ピークメモリ: 再ハッシュ用にさらに 1 GB
used_memory_peak_human:6.50G
(2) Redis 7.0.11
最初の注入: 67,100,000 キー、メモリを観察します。
maxmemory: 5.6GB
used_memory_human: 5.5GB
dbsize: 67,100,000
2 回目のプッシュ: 2,000,000 キー、可用性とエビクションを監視
-
タイムアウトなしのクライアント実行、redis-cli --latency-history
min: 0, max: 2, avg: 0.05 (1484 samples) -- 15.01 seconds range min: 0, max: 3, avg: 0.24 (1454 samples) -- 15.00 seconds range
-
通常のエビクション (0.1GB 残り)
evicted_keys:152485
evicted_keys:443253
evicted_keys:751165
evicted_keys:1058191
evicted_keys:1367445
evicted_keys:1662485
evicted_keys:1662485
-
ピークメモリ: 再ハッシュなし
used_memory_peak_human:5.50G
4. 実験比較
バージョン | リハッシュが発生するかどうか | クリティカルタイムアウト |
---|---|---|
6.0.15 | 持っている | はい、30 秒以上利用できません |
7.0.11 | なし | なし、通常のエビクション |
4. コード分析
1.辞書:
新しく追加されたexpandAllowedは、dictが現在再ハッシュされているかどうかを決定します
typedef struct dictType {
....
int (*expandAllowed)(size_t moreMem, double usedRatio);
....
} dictType;
dictが展開されると、新しい判定が追加されます dictTypeExpandAllowed
/* Expand the hash table if needed */
static int _dictExpandIfNeeded(dict *d)
{
......
if (!dictTypeExpandAllowed(d))
return DICT_OK;
......
}
dictのexpandAllowedが空の場合は再ハッシュが許可され、それ以外の場合はexpandAllowedが実行されます。
static int dictTypeExpandAllowed(dict *d) {
if (d->type->expandAllowed == NULL) return 1;
return d->type->expandAllowed(
DICTHT_SIZE(_dictNextExp(d->ht_used[0] + 1)) * sizeof(dictEntry*),
(double)d->ht_used[0] / DICTHT_SIZE(d->ht_size_exp[0]));
}
2.redis辞書
Redis の dbDictType および dbExpiresDictType
/* Db->dict, keys are sds strings, vals are Redis objects. */
dictType dbDictType = {
dictSdsHash, /* hash function */
NULL, /* key dup */
NULL, /* val dup */
dictSdsKeyCompare, /* key compare */
dictSdsDestructor, /* key destructor */
dictObjectDestructor, /* val destructor */
dictExpandAllowed, /* allow to expand */
dictEntryMetadataSize /* size of entry metadata in bytes */
};
/* Db->expires */
dictType dbExpiresDictType = {
dictSdsHash, /* hash function */
NULL, /* key dup */
NULL, /* val dup */
dictSdsKeyCompare, /* key compare */
NULL, /* key destructor */
NULL, /* val destructor */
dictExpandAllowed /* allow to expand */
};
見られます:
-
dict の使用率がハッシュ負荷率より大きい: 再ハッシュが可能
-
dict の使用量がハッシュ負荷係数よりも小さい場合:
-
maxmemory=0 の場合、再ハッシュを行うことができます
-
現在使用されているメモリ + 再ハッシュに必要なメモリが maxmemory 未満の場合は、再ハッシュを実行できます。
-
現在のオブジェクト メモリ (使用済みメモリ - 追加メモリ (さまざまなバッファ)) + 再ハッシュに必要なメモリが maxmemory より少ない場合、再ハッシュを実行できます。
-
int dictExpandAllowed(size_t moreMem, double usedRatio) {
if (usedRatio <= HASHTABLE_MAX_LOAD_FACTOR) {
return !overMaxmemoryAfterAlloc(moreMem);
} else {
return 1;
}
}
/* Return 1 if used memory is more than maxmemory after allocating more memory,
* return 0 if not. Redis may reject user's requests or evict some keys if used
* memory exceeds maxmemory, especially, when we allocate huge memory at once. */
int overMaxmemoryAfterAlloc(size_t moremem) {
if (!server.maxmemory) return 0; /* No limit. */
/* Check quickly. */
size_t mem_used = zmalloc_used_memory();
if (mem_used + moremem <= server.maxmemory) return 0;
size_t overhead = freeMemoryGetNotCountedMemory();
mem_used = (mem_used > overhead) ? mem_used - overhead : 0;
return mem_used + moremem > server.maxmemory;
}
V. 結論
-
Redis6.2+ バージョンは、大規模な dict での再ハッシュによって引き起こされるユーザビリティとエビクションの問題を効果的に解決します。
-
Redis7 は現在比較的安定しており (2022 年 4 月リリース)、7.0.11 がオンラインで広く使用されています。
-
新しいバージョンでは、リハッシュの遅延処理のみを実行し、リハッシュの実行を停止するわけではありません。また、負荷率によって発生する可能性のあるパフォーマンスの問題には適切に注意する必要があります。
-
Redis6.2 以降では、エビクションに関するいくつかの最適化も行われています。これについては、後で紹介します。 増分エビクション処理