漸進的リハッシュ
漸進的リハッシュの実行時、キーバリューペアの操作に基づいてデータ移行を行うだけでなく、Redis自体にも定期的なタスクがリハッシュを実行します。キーバリューペアの操作がない場合、このタスクは周期的に(例えば100msごと)新しいハッシュテーブルに一部のデータを移動させ、これにより全体のリハッシュプロセスを短縮できます。
整数配列と圧縮リストの採用理由
整数配列と圧縮リストは検索時間の複雑さにおいて大きな利点があるわけではありませんが、なぜRedisはこれらを底層データ構造として採用するのでしょうか。理由は二つあります。
- メモリ利用率:配列と圧縮リストは非常にコンパクトなデータ構造であり、リンクリストよりも少ないメモリを占有します。Redisはメモリベースのデータベースであり、大量のデータをメモリに保存するため、可能な限り最適化を行いメモリ利用率を高める必要があります。
- CPUキャッシュの親和性:配列はCPUキャッシュにより友好であり、データ要素が少ない場合、Redisはデフォルトでメモリをコンパクトに配置して保存し、CPUキャッシュを活用してアクセス速度を低下させません。データ要素が設定した閾値を超えた場合、検索時間の複雑さが高くなるのを避けるため、ハッシュとスキップリストのデータ構造に切り替え、検索効率を保証します。
シングルスレッドRedisの高速性
- ほとんどの操作がメモリ上で完了する
- ハッシュテーブルやスキップリストなどの効率的なデータ構造の採用
- IO多重化機構の採用:ネットワークIO操作で多数のクライアントリクエストを並行処理できる
LinuxにおけるIO多重化モデルは、単一スレッドが複数のIOストリームを処理することを指し、私たちがよく聞くselect/epoll機構です。簡単に言えば、Redisが単一スレッドで実行されている場合、この機構はカーネル内に同時に複数のリスニングソケットと接続済みソケットを存在させることができます。カーネルはこれらのソケット上の接続要求やデータ要求を監視し続け、リクエストが到達するとRedisスレッドに処理を渡します。これにより、単一のRedisスレッドが複数のIOストリームを処理する効果を実現します。
AOFログの書き換えタイミング
AOFファイルのサイズを削減するため、手動で「bgrewriteaof」コマンドを送信し、子プロセスを使用してより小さなサイズのAOFを生成し、古く大きなサイズのAOFファイルと置き換えることができます。自動トリガーも設定可能です。
- auto-aof-rewrite-percentage 100
- auto-aof-rewrite-min-size 64mb
これらの設定項目の意味は、AOFファイルのサイズが64MBを超え、前回の書き換え後のサイズから100%増加した場合に自動的に書き換えをトリガーすることです。これらのパラメータを変更して、実際の要件に合わせることができます。
AOFとRDBの選択方法
- データの喪失を許容しない場合、メモリスナップショットとAOFの併用が良い選択です。
- 数分レベルのデータ喪失を許容できる場合、RDBのみを使用できます。
- AOFのみを使用する場合、everysec設定オプションを優先使用してください。これは信頼性と性能の間でバランスを取っているためです。
センチネルメカニズムの役割
- マスターデータベースの実行状態を監視し、マスターデータベースが客観的にオフラインであるか判断します。
- マスターデータベースが客観的にオフラインになった後、新しいマスターデータベースを選択します。
- 新しいマスターデータベースを選択した後、スレーブデータベースとクライアントに通知します。
センチネルのマスターセレクションプロセス
センチネルはオンライン状態、ネットワーク状態に基づいて、要件を満たさないスレーブデータベースをフィルタリングします。その後、優先度、複製進捗、ID番号の順に残りのスレーブデータベースにスコア付けを行い、最も高いスコアのスレーブデータベースが現れた時点でそれを新しいマスターデータベースとして選択します。
センチネルの動作原理
- pub/subメカニズムに基づくセンチネルクラスタの形成プロセス
- INFOコマンドに基づくスレーブデータベースリスト、これはセンチネルとスレーブデータベース間の接続構築に役立ちます
- センチネル自身のpub/sub機能、これはクライアントとセンチネル間のイベント通知を実現します
シャーディングクラスタの原理
Redisクラターソリューションは、ハッシュスロット(Hash Slot)を使用してデータとインスタンス間のマッピング関係を処理します。Redisクラスターソリューションでは、1つのシャーディングクラスタに合計16384個のハッシュスロットがあり、これらのスロットはデータパーティションに類似しています。各キーバリューペースはそのキーに基づいて、1つのハッシュスロットにマッピングされます。
クラスタのインスタンスの増減、または負荷分散を実現するためのデータの再分布により、ハッシュスロットとインスタンスのマッピング関係が変化します。クライアントがリクエストを送信すると、MOVEDやASKコマンドなどのエラーメッセージが返されます。
GEOタイプの原理
GEOタイプは、経度緯度の区間を符号化してSorted Set内の要素の重みスコアとして使用し、経度緯度に関連する車両IDをSorted Set内の要素として保存します。これにより、隣接する経度緯度のクエリは符号化値の範囲クエリを実現することで可能になります。
Redisインスタンスのブロッキングポイント
- クライアント側:ネットワークIO、キーバリューペアの追加・削除・更新・検索操作、データベース操作
- 集合の全量クエリと集約操作
- bigkeyの削除操作(テストにより、100万要素の集合を削除する場合、最大削除時間は1.98秒に達する - Hashタイプ)
- データベースのクリア
- ディスク:RDBスナップショットの生成、AOFログの記録、AOFログの書き換え
- AOFログの同期書き込み:Redisが直接AOFログを記録する場合、異なる書き込み戦略に基づいてデータをディスクに保存します。同期書き込み操作の所要時間は約1〜2msであり、大量の書き込み操作をAOFログに記録し同期書き戻しする場合、メインスレッドがブロックされます。
- マスター/スレーブノード:マスターデータベースでのRDBファイルの生成と送信、スレーブデータベースでのRDBファイルの受信、データベースのクリア、RDBファイルの読み込み
- シャーディングクラスタインスタンス:他のインスタンスへのハッシュスロット情報の伝送、bigkeyデータの移行
Redisブロッキング操作の最適化
Redisのパフォーマンスに影響を与える5つの主要なブロッキングポイントには、集合の全量クエリと集約操作、bigkeyの削除、データベースのクリア、AOFログの同期書き込み、スレーブデータベースでのRDBファイルの読み込みがあります。これら5つのブロッキングポイントのうち、bigkeyの削除、データベースのクリア、AOFログの同期書き込みは重要パス上の操作ではないため、非同期サブスレッドメカニズムを使用して完了できます。Redisは実行時に3つのサブスレッドを作成し、メインスレッドはタスクキューを介して3つのサブスレッドと対話します。サブスレッドはタスクの具体的なタイプに基づいて、対応する非同期操作を実行します。
集合の全量クエリと集約操作、スレーブデータベースでのRDBファイルの読み込みは重要パス上にあるため、非同期操作で完了することはできません。
- 集合の全量クエリと集約操作:SCANコマンドを使用してデータをバッチで読み取り、クライアント側で集約計算を実行します。
- スレーブデータベースでのRDBファイルの読み込み:マスターデータベースのデータ量を2〜4GB程度に制御し、RDBファイルを迅速に読み込めるようにします。
マルチコアCPUがRedisパフォーマンスに与える影響
マルチCPUアーキテクチャでは、アプリケーションが所在するソケットのローカルメモリにアクセスする遅延と、リモートメモリにアクセスする遅延は一致しません。このため、このアーキテクチャは非統一メモリアクセスアーキテクチャ(Non-Uniform Memory Access、NUMAアーキテクチャ)と呼ばれます。NUMAアーキテクチャでは、アプリケーションが1つのソケットから別のソケットにスケジュールされると、リモートメモリアクセスが発生する可能性があり、これが直接アプリケーションの実行時間を増加させます。
マルチCPUコア環境では、RedisインスタンスとCPUコアをバインドすることにより、Redisの遅延(レイテンシ)を効果的に低減できます。もちろん、コアバインドは遅延の低減に良いだけでなく、平均遅延の低減、スループットの向上、ひいてはRedisパフォーマンスの向上にも役立ちます。
Redisの応答遅延処理の考え方
- 複雑さの高いコマンドの使用(例:SORT/SUION/ZUNIONSTORE/KEYS)または全量データの一度のクエリ(例:LRANGE key 0 N、ただしNが大きい)
- 分析:
- slowlogにこれらのコマンドが存在するか確認
- RedisプロセスのCPU使用率が急増しているか(集約演算コマンドによる)
- 解決策:
- 複雑さの高いコマンドを使用しない、または他の方法で代替実行(クライアント側で実行)
- データを可能な限りバッチでクエリ(LRANGE key 0 N、N<=100を推奨、全量データクエリにはHSCAN/SSCAN/ZSCANを使用)
- bigkeyの操作
- 分析:
- slowlogに多くのSET/DELETE遅延コマンドが表示される(bigkeyのメモリ割り当てと解放が遅い)
- redis-cli -h $host -p $port --bigkeysで多くのbigkeyが検出される
- 解決策:
- ビジネスを最適化し、bigkeyの保存を避ける
- Redis 4.0以上ではlazy-freeメカニズムを有効にできる
- 大量のkeyが集中して期限切れになる
- 分析:
- ビジネスでEXPIREAT/PEXPIREATコマンドを使用している
- Redis infoのexpired_keys指標が短期間に急増
- 解決策:
- ビジネスを最適化し、期限切れにランダム時間を追加し、時間を分散させて期限切れkeyの削除圧力を軽減する
- 運用レベルでexpired_keys指標を監視し、短期間の急増がある場合はアラームを発報し調査する
- Redisメモリがmaxmemoryに達する
- 分析:
- インスタンスのメモリがmaxmemoryに達し、書き込み量が多いため、keyの淘汰圧力が大きい
- Redis infoのevicted_keys指標が短期間に急増
- 解決策:
- ビジネスレベルで、状況に応じて淘汰戦略を調整(ランダムはLRUより高速)
- 運用レベルでevicted_keys指標を監視し、短期間の急増がある場合はアラームを発報する
- クラスタ拡張し、複数のインスタンスでkeyの淘汰圧力を軽減する
- 大量の短い接続リクエスト
- 分析:Redisが大量の短い接続リクエストを処理する場合、TCPの3ウェイハンドシェイクと4ウェイハングアップも時間を増加させる
- 解決策:長い接続でRedisを操作する
(他のブロッキングポイントの分析と解決策も同様の形式で続きますが、ここでは省略します)
キャッシュ淘汰戦略
推奨:
- 可能な限りallkeys-lru戦略を使用してください。これにより、LRUという古典的なキャッシュアルゴリズムの利点を最大限に活用し、最近最もアクセスされたデータをキャッシュに残し、アプリケーションのアクセスパフォーマンスを向上させることができます。ビジネスデータに明確な冷熱データの区別がある場合、allkeys-lru戦略を使用することをお勧めします。ビジネスアプリケーションでのデータアクセス頻度に大きな差がない場合、明確な冷熱データの区別がないため、allkeys-random戦略を使用し、ランダムに淘汰するデータを選択することをお勧めします。ビジネスにトップ固定の要件がある場合(例:トップニュース、トップ動画)、volatile-lru戦略を使用し、これらのトップ固定データに有効期限を設定しないでください。これにより、これらのトップ固定データは常に削除されず、他のデータは有効期限切れ時にLRUルールに基づいて筛选されます。
LFU戦略の紹介
LFUキャッシュ戦略はLRU戦略に基づいており、各データにカウンタを追加してこのデータのアクセス回数を統計します。LFU戦略を使用してデータを淘汰する場合、まずデータのアクセス回数に基づいてデータを筛选し、アクセス回数が最も低いデータをキャッシュから淘汰します。2つのデータのアクセス回数が同じ場合、LFU戦略はさらにこれら2つのデータのアクセス時効性を比較し、最後にアクセスしてから時間が経っているデータを淘汰します。
キャッシュ雪崩、撃破、貫通の原因と対策
(詳細な説明は省略しますが、これらの現象とその対策について説明します)
Redisの2つの原子操作方法
- 複数の操作をRedis内で1つの操作として実現する、つまり単一コマンド操作;INCR/DECRコマンドはデータの増値/減値操作を行うことができ、それら自体は単一コマンド操作であり、Redisがそれらを実行する際には元々相互排他性を持っています。
- 複数の操作を1つのLuaスクリプトに書き込み、原子的に実行します。
Redis分散ロックの原理
単一Redisインスタンスに基づく分散ロックを実現する場合、ロック操作については3つの条件を満たす必要があります。
- ロックにはロック変数の読み取り、ロック変数値の確認、ロック変数値の設定という3つの操作が含まれますが、原子操作として完了する必要があるため、SETコマンドにNXオプションを付けてロックを実現します。
- ロック変数に有効期限を設定する必要があります。クライアントがロックを取得後に異常が発生し、ロックが常に解放されないのを防ぐため、SETコマンド実行時にEX/PXオプションを追加し、有効期限を設定します。
- ロック変数の値は異なるクライアントからのロック操作を区別できる必要があります。ロック解除時に誤った解放操作が発生しないように、SETコマンドでロック変数値を設定する際、各クライアントが設定する値はクライアントを識別するための一意の値です。
// ロック設定, unique_valueはクライアントの一意性の識別子
SET lock_key unique_value NX PX 10000
ロック解除もロック変数値の読み取り、ロック変数値の判断、ロック変数の削除という3つの操作を含みますが、単一コマンドで実現することはできないため、Luaスクリプトでロック解除操作を実行し、RedisがLuaスクリプトを原子的に実行することでロック解除操作の原子性を保証します。
// ロック解除Luaスクリプト unique_valueが等しいか比較し、誤った解放を避ける
if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end
redis-cli --eval unlock.script lock_key , unique_value
単一Redisインスタンスに基づく分散ロックを実現する場合、インスタンスの異常やクラッシュの状況に直面することがあり、これによりインスタンスがロック操作を提供できなくなります。このため、RedisはRedlockアルゴリズムも提供しており、複数インスタンスに基づく分散ロックを実現します。これにより、ロック変数は複数のインスタンスで維持され、インスタンスに障害が発生してもロック変数は依然として存在し、クライアントは引き続きロック操作を完了できます。Redlockでのロック設定手順:
- 最初に、クライアントが現在の時刻を取得します。
- 次に、クライアントは順番にN個のRedisインスタンスにロック設定操作を実行します。
- クライアントがすべてのRedisインスタンスとのロック設定操作を完了すると、クライアントはロック設定プロセス全体の総所要時間を計算します。
クライアントは、以下の2つの条件を満たした場合のみ、ロック設定が成功したと見なすことができます。
- 条件1:クライアントが半数以上(N/2+1以上)のRedisインスタンスでロックの取得に成功した。
- 条件2:クライアントがロックを取得した総所要時間がロックの有効時間を超えていない。
Redisトランザクションメカニズム
トランザクションの使用方法:
# トランザクション開始
127.0.0.1:6379> MULTI
OK
# a:stockを1減らす
127.0.0.1:6379> DECR a:stock
QUEUED
# b:stockを1減らす
127.0.0.1:6379> DECR b:stock
QUEUED
# トランザクションの実際の実行
127.0.0.1:6379> EXEC
1) (integer) 4
2) (integer) 9
トランザクションのACID属性は、私たちが正しい操作のためにトランザクションを使用するための基本的な要件です。Redisのトランザクションメカニズムは一貫性と分離性を保証できますが、永続性を保証できません。しかし、Redis自体はメモリベースのデータベースであるため、永続性は必須の属性ではなく、私たちがより注目すべきは原子性、一貫性、分離性の3つの属性です。原子性の場合は比較的複雑で、トランザクションで使用されるコマンドの構文が誤っている場合にのみ原子性が保証されない、その他の場合ではトランザクションは原子的に実行できます。
CodisとRedisクラスタの比較
Codisクラスタは合計1024個のスロットを持ち、番号は0から1023です。これらのスロットをcodis serverに手動で割り当てることができます。各serverには一部のスロットが含まれます。もちろん、codis dashboardが自動割り当てを行うこともできます。例えば、dashboardが1024個のスロットをすべてのserverに均等に割り当てます。クライアントがデータを読み書きする場合、CRC32アルゴリズムを使用してデータkeyのハッシュ値を計算し、このハッシュ値を1024で割ります。そして、割った後の値はスロットの番号に対応します。この時、最初のステップで割り当てたスロットとserverの対応関係に基づいて、データがどのserverに保存されているかを知ることができます。
Redisクラターソリューションはハッシュスロットを使用してデータとインスタンス間のマッピング関係を処理します。Redisクラスターソリューションでは、1つのシャーディングクラスタに合計16384個のハッシュスロットがあり、これらのスロットはデータパーティションに類似しています。各キーバリューペースはそのキーに基づいて、1つのハッシュスロットにマッピングされます。具体的なマッピングプロセスは2つの大きなステップに分かれます:まずキーバリューペースのキーに基づいてCRC16アルゴリズムを使用して16ビットの値を計算し、次にこの16ビットの値を16384で割って、0〜16383の範囲内の模数を得ます。各模数は対応する番号のハッシュスロットを表します。
比較:
- Redisクラスタは16384個のスロットを使用し、Codisは1024個のスロットを使用します
- Redisクラスタはノード間の通信プロトコルをネイティブにサポートし、Codisはプロキシ層を必要とします
- Redisクラスタはマスタースレーブレプリケーションをネイティブにサポートし、Codisは外部のマスタースレーブ構成を必要とします
秒殺シナリオにおけるRedisの応用
秒殺シナリオには2つの負荷特徴があります。瞬時の高並行リクエストと読み取り中心の書き込みです。Redisの優れた高並行処理能力と効率的なキーバリューペースの読み書き特性はまさに秒殺シナリオの要件を満たすことができます。秒殺シナリオでは、フロントエンドのCDNとブラウザキャッシュで大量の秒殺前のリクエストをブロックできます。実際の秒殺活動が行われている時点で、在庫確認と在庫減少は巨大な並行リクエスト圧力を受ける2つの操作であり、同時にこれら2つの操作の実行は原子性を保証する必要があります。Redisの原子操作(Lua)、分散ロックという2つの機能特性が秒殺シナリオの要件を効果的に支えます。
在庫確認と在庫減少は2つの操作であるため、1つのコマンドで完了することはできません。そのため、Luaスクリプトを使用してこれら2つの操作を原子的に実行する必要があります。Luaスクリプトの例:
# 商品在庫情報を取得
local counts = redis.call("HMGET", KEYS[1], "total", "ordered");
# 総在庫を数値に変換
local total = tonumber(counts[1])
# 秒殺された在庫を数値に変換
local ordered = tonumber(counts[2])
# 現在のリクエストの在庫量に秒殺された在庫量を加えても総在庫量より小さい場合、在庫を更新できる
if ordered + k <= total then
# 秒殺された在庫量を更新
redis.call("HINCRBY",KEYS[1],"ordered",k)
return k;
end
return 0
分散ロックを使用して秒殺シナリオを支える具体的な方法は、まずクライアントがRedisに分散ロックを申請し、ロックを取得したクライアントのみが在庫確認と在庫減少を実行することです。分散ロックの例:
// 商品IDをkeyとして使用
key = itemID
// クライアントの一意識別子をvalueとして使用
val = clientUniqueID
// 分散ロックを申請、Timeoutはタイムアウト時間
lock =acquireLock(key, val, Timeout)
// ロックを取得した後、在庫確認と減少を行う
if(lock == True) {
// 在庫確認と減少
availStock = DECR(key, k)
// 在庫が減りきったら、ロックを解放し、秒殺失敗を返す
if (availStock < 0) {
releaseLock(key, val)
return error
}
// 在庫減少成功、ロックを解放
else{
releaseLock(key, val)
// 注文処理
}
}
// ロックを取得できなかった場合、直接返す
else
return
bigkeyとは何か、どうやってbigkeyを避けるか
Redisは単一スレッドでデータを読み書きするため、bigkeyの読み書き操作はスレッドをブロックし、Redisの処理効率を低下させます。そのため、Redisを使用する際のvalueの設計規範において、bigkeyを避けることは非常に重要です。bigkeyには通常2つの状況があります。
- キーバリューペースの値自体が大きい場合、例えばvalueが1MBのStringタイプデータです。Stringタイプのbigkeyを避けるため、ビジネス層ではStringタイプのデータサイズを可能な限り10KB以下に制御してください。
- キーバリューペースの値が集合タイプであり、集合要素の数が非常に多い場合、例えば100万要素のHash集合タイプデータです。集合タイプのbigkeyを避けるため、設計規範として、集合タイプの要素数を可能な限り1万以下に制御することをお勧めします。もちろん、これらの提案はbigkeyをできる限り避けるためだけのものであり、ビジネス層のStringタイプのデータが本当に大きい場合は、データ圧縮によってデータサイズを減らすことができます。集合タイプの要素が本当に多い場合は、大きな集合を複数の小さな集合に分割して保存することもできます。
Redisメモリ使用量の推奨値
Redis単一インスタンスのメモリサイズは大きくしないでください。2〜6GB程度に設定することをお勧めします。これにより、RDBスナップショットであれ、マスタースレーブクラスタでのデータ同期であれ、いずれも迅速に完了し、通常のリクエスト処理をブロックしません。
Redisクラスタモードでのデータ傾斜の原因と解決方法
データ傾斜には2つのタイプがあります:
- データ量の傾斜:特定の場合、インスタンス上のデータ分布が不均衡であり、特定のインスタンス上のデータが非常に多い。
- データアクセスの傾斜:各クラスタインスタンス上のデータ量はそれほど差がないが、特定のインスタンス上のデータがホットデータであり、非常に頻繁にアクセスされている。
Hash Tagとは、キーバリューペースのkeyに含まれる一組の波括号{}です。この括弧はkeyの一部を囲み、クライアントがkeyのCRC16値を計算する際には、Hash Tagの波括号内のkeyの内容のみを計算します。Hash Tagを使用しない場合、クライアントはkey全体のCRC16値を計算します。
RedisとMemcachedの比較
MemcachedクラスタはRedisクラスタやCodisのように、スロットマッピングを使用してデータとインスタンスの対応保存関係を割り当てるのではなく、一貫性ハッシュアルゴリズムを使用してデータを複数のインスタンスに分散保存します。一貫性ハッシュの利点は大規模なクラスタをサポートできることです。したがって、大規模なキャッシュクラスタを展開する必要がある場合、Memcachedは良い選択肢となります。
Redis監視運用ツール
Redis自体が提供するINFOコマンドは豊富なインスタンス実行監視情報を返し、このコマンドはRedis監視ツールの基礎となります。