Redisにおける主結合失効の原理と実現メカニズムの剖析

11891 ワード

無効なデータを定期的にクリーンアップする重要なメカニズムとして、プライマリ・キーの失効はほとんどのキャッシュ・システムに存在し、Redisも例外ではありません.Redisが提供する多くのコマンドでは、EXPIRE、EXPIREAT、PEXPIRE、PEXPIREAT、およびSETEX、およびPSETEXを使用して、KeyValueペアの失効時間を設定できます.一方、KeyValueペアは、失効時間が関連付けられると、失効時間が失効した後に自動的に削除されます(または、アクセスできなくなります).主鍵失効という概念は比較的理解しやすいと言えるが,Redisに具体的に実現するにはどうだろうか.最近,本ブロガーはRedisにおけるプライマリ・キーの失効メカニズムについていくつかの疑問を提起し,これらの疑問に基づいて詳細に探究し,以下のようにまとめ,視聴者の皆様に紹介した.
一、失効時間の制御
PERSISTコマンドを呼び出す以外に、プライマリ・キーの失効時間を取り消す場合はありませんか?答えは肯定的だ.まず、DELコマンドでプライマリ・キーを削除すると、失効時間は自然に取り消されます(これはくだらない話ではありませんか、ははは).次に、失効時間が設定されたプライマリ・キーが更新上書きされると、そのプライマリ・キーの失効時間も取り消されます(これもくだらない話のようですが、ははは).ただし、ここでいうのは、プライマリ・キーに対応するValueが更新上書きされるのではなく、プライマリ・キーが更新上書きされるため、SET、MSET、またはGETSETは、INCR、DECR、LPUSH、HSETなどのプライマリ・キーに対応する値を更新することで、プライマリ・キーの失効時間に触れない可能性がある.また、RENAMEという特殊なコマンドもあります.RENAMEを使用してプライマリ・キーの名前を変更すると、以前に関連付けられた失効時間が自動的に新しいプライマリ・キーに渡されますが、プライマリ・キーがRENAMEによって上書きされている場合(プライマリ・キーhelloがRENAME world helloによって上書きされる可能性がある場合など)、上書きされたプライマリ・キーの失効時間が自動的に取り消されます.新しいプライマリ・キーは、元のプライマリ・キーの特性を維持し続けます.
二、失効の内部実現
Redisのプライマリ・キーの失効はどのように実現されますか.すなわち、失効したプライマリ・キーはどのように削除されますか.実際、Redisが失効したプライマリ・キーを削除する方法は主に2つあります.
1.ネガティブメソッド(passive way)は、プライマリ・キーがアクセスされたときに無効であることが判明した場合に削除する2.アクティブメソッド(active way)は、失効時間が設定されたプライマリ・キーから失効したプライマリ・キーの一部を周期的に選択して削除する
無効な内部表示
次に、この2つの方法の具体的な実装をコードで探究しますが、その前に、Redisがプライマリ・キーをどのように管理および維持しているかを見てみましょう(注:このブログのソース・コードはすべてRedis-2.6.12から来ています).
【コードセグメント1】id以外は辞書を指すポインタであるRedisにおけるデータベースに関する構造体定義が与えられ、ここではdictとexpiresのみを参照し、前者は1つのRedisデータベースに含まれるすべてのKey-Value対を維持するために使用される(その構造はdict[key]:value、すなわちプライマリ・キーと値とのマッピングと理解できる).後者は、Redisデータベースで失効時間が設定されているプライマリ・キーを維持するために使用されます(その構造はexpires[key]:timeout、すなわちプライマリ・キーと失効時間のマッピングと理解できます).SETEXとPSETEXコマンドを使用してシステムにデータを挿入すると、RedisはまずキーとValueをdictという辞書テーブルに追加し、次にキーと失効時間をexpiresという辞書テーブルに追加します.EXPIRE、EXPIREAT、PEXPIRE、PEXPIRETコマンドを使用してプライマリ・キーの失効時間を設定すると、Redisはまずdictという辞書テーブルに設定するプライマリ・キーが存在するかどうかを検索し、存在する場合はexpiresという辞書テーブルにこのプライマリ・キーと失効時間を追加します.簡単にまとめると、失効時間を設定したプライマリ・キーと特定の失効時間はすべてexpiresという辞書表に維持されます.
【コードセグメント1】:
 
  
typedef struct redisDb {
    dict *dict;               
    dict *expires;             
    dict *blocking_keys;       
    dict *ready_keys;         
    dict *watched_keys;       
    int id;
} redisDb;

ネガティブメソッド
Redisが失効時間を設定したプライマリ・キーをどのように維持するかを大まかに理解した後、Redisが失効したプライマリ・キーを消極的に削除する方法を見てみましょう.【コードセグメント2】expireIfNeededという関数が与えられる.この関数は、データにアクセスする関数のいずれにおいても呼び出される.すなわち、Redisは、GET、MGET、HGET、LRANGEなどのデータの読み取りに関するすべてのコマンドを実装する際に呼び出される.データを読み取る前に、失効がないかどうかを確認し、失効したら削除するという意味である.【コードセグメント2】にはexpireIfNeeded関数のすべての関連記述が示されており、ここではその実装方法を繰り返さない.ここで説明する必要があるのは、expireIfNeeded関数で呼び出されたもう一つの関数propagateExpireです.この関数は、失効したプライマリ・キーが正式に削除される前に、このプライマリ・キーが失効した情報をブロードキャストするために使用されます.この情報は、2つの宛先に伝播します.1つは、AOFファイルに送信され、失効したプライマリ・キーを削除する操作をDEL Keyの標準コマンド・フォーマットで記録します.もう1つは現在のRedisサーバに送信されたすべてのSlaveであり、同様に失効したプライマリ・キーを削除する操作をDEL Keyの標準コマンド形式でこれらのSlaveにそれぞれの失効したプライマリ・キーを削除することを通知する.Slaveとして動作するすべてのRedisサーバは、ネガティブな方法で失効したプライマリ・キーを削除する必要はなく、Masterに従うだけでOKであることがわかります.
【コードセグメント2】:
 
  
int expireIfNeeded(redisDb *db, robj *key) {
    //
    long long when = getExpire(db,key);
    // , ( -1), 0
    if (when < 0) return 0;
   // Redis RDB , , 0
    if (server.loading) return 0;
    // Redis Slave , , Slave
    // Master ,
    // ,
    if (server.masterhost != NULL) {
        return mstime() > when;
    }
    // , ,
    // 0
    if (mstime() <= when) return 0;
    // , ,
    // ,
    server.stat_expiredkeys++;
    propagateExpire(db,key);
    return dbDelete(db,key);
}

【コードセグメント3】:
 
  
void propagateExpire(redisDb *db, robj *key) {
    robj *argv[2];
    //shared.del Redis Redis , DEL
    argv[0] = shared.del;
    argv[1] = key;
    incrRefCount(argv[0]);
    incrRefCount(argv[1]);
    // Redis AOF, DEL
    if (server.aof_state != REDIS_AOF_OFF)
        feedAppendOnlyFile(server.delCommand,db->id,argv,2);
    // Redis Slave, Slave DEL ,
    // expireIfNeeded Slave ,
    // Master OK
    if (listLength(server.slaves))
        replicationFeedSlaves(server.slaves,db->id,argv,2);
    decrRefCount(argv[0]);
    decrRefCount(argv[1]);
}

ポジティブメソッド
以上、expireIfNeeded関数について説明したように、Redisが失効したプライマリ・キーを消極的に削除する方法について理解しましたが、この方法だけでは十分ではありません.一部の失効したプライマリ・キーが再アクセスを待たないと、Redisはこれらのプライマリ・キーが失効したことを永遠に知らず、永遠に削除されないため、メモリ領域の浪費につながるに違いありません.したがって、Redisはまた、失効したプライマリ・キーのチェックと削除を含む特定の操作を一定時間ごとに中断して完了するRedisの時間イベントを利用して実現する積極的な削除方法を用意している.ここで時間イベントのコールバック関数は、Redisサーバの起動時に作成され、1秒当たりの実行回数がマクロによって定義されたREDIS_DEFAULT_HZで指定します.デフォルトでは1秒に10回実行されます.【コードセグメント4】redis.cファイルのinitServer関数にあります.実際、serverCronというコールバック関数は、失効したプライマリ・キーのチェックと削除だけでなく、統計情報の更新、クライアント接続のタイムアウトの制御、BGSAVEとAOFのトリガなども行います.ここでは、失効したプライマリ・キーの削除の実現、すなわち関数activeExpireCycleに注目します.
【コードセグメント4】:
 
  
if(aeCreateTimeEvent(server.el, 1, serverCron, NULL, NULL) == AE_ERR) {
        redisPanic("create time event failed");
        exit(1);
}

【コードセグメント5】関数activeExpireCycleの実装とその詳細な説明を示し、その主な実装原理は、Redisサーバ内の各データベースを処理するexpires辞書テーブルを遍歴し、そこからランダムにREDIS_をサンプリングしようとすることである.EXPIRELOOKUPS_PER_CRON(デフォルトは10)個の失効時間が設定されているプライマリ・キーは、失効したかどうかを確認し、失効したプライマリ・キーを削除します.失効したプライマリ・キーの数が今回のサンプリング個数に占める割合が25%を超えると、Redisは現在のデータベースの失効したプライマリ・キーが依然として多く、次のランダムなサンプリングと削除を継続し、先ほどの割合が25%未満になるまで現在のデータベースの処理を停止します.次のデータベースに移動します.ここで注意しなければならないのは、activeExpireCycle関数がRedis内のすべてのデータベースを一度に処理しようとするのではなく、最大REDIS_のみを処理することです.DBCRON_DBS_PER_CALL(デフォルトは16)に加え、activeExpireCycle関数には処理時間の制限があり、実行したいだけ実行するのではなく、実行したいだけ実行するのではなく、失効したプライマリ・キーの削除に多くのCPUリソースを消費しないようにすることが目的です.【コードセグメント5】activeExpireCycleのすべてのコードの詳細な説明があり、この関数の具体的な実装方法を理解することができる.
【コードセグメント5】:
 
  
void activeExpireCycle(void) {
    // activeExpireCycle Redis ,
    // Redis , activeExpireCycle
    // , current_db static ,
    // timelimit_exit activeExpireCycle
    // , static
    static unsigned int current_db = 0;
    static int timelimit_exit = 0;     
    unsigned int j, iteration = 0;
    // activeExpireCycle Redis REDIS_DBCRON_DBS_PER_CALL
    unsigned int dbs_per_call = REDIS_DBCRON_DBS_PER_CALL;
    long long start = ustime(), timelimit;
    // Redis REDIS_DBCRON_DBS_PER_CALL, ,
    // activeExpireCycle , ,
    //
    if (dbs_per_call > server.dbnum || timelimit_exit)
        dbs_per_call = server.dbnum;
    // activeExpireCycle ( ), REDIS_EXPIRELOOKUPS_TIME_PERC
    // activeExpireCycle CPU , 25,server.hz
    // activeExpireCycle , ,
    (1000000 * (REDIS_EXPIRELOOKUPS_TIME_PERC / 100)) / server.hz
    timelimit = 1000000*REDIS_EXPIRELOOKUPS_TIME_PERC/server.hz/100;
    timelimit_exit = 0;
    if (timelimit <= 0) timelimit = 1;
    // Redis
    for (j = 0; j < dbs_per_call; j++) {
        int expired;
        redisDb *db = server.db+(current_db % server.dbnum);
        // current_db ,
       // , activeExpireCycle ,
       //
        current_db++;
        //
        do {
            unsigned long num, slots;
            long long now;
            // expires 0, ,
           //
            if ((num = dictSize(db->expires)) == 0) break;
            slots = dictSlots(db->expires);
            now = mstime();
            // expires , 1%,
           // ,
            if (num && slots > DICT_HT_INITIAL_SIZE &&
                (num*100/slots < 1)) break;
            expired = 0;
            // expires entry , key
            if (num > REDIS_EXPIRELOOKUPS_PER_CRON)
                num = REDIS_EXPIRELOOKUPS_PER_CRON;
            while (num--) {
                dictEntry *de;
                long long t;
                // ,
                if ((de = dictGetRandomKey(db->expires)) == NULL) break;
                t = dictGetSignedIntegerVal(de);
                if (now > t) {
            // ,
                    sds key = dictGetKey(de);
                    robj *keyobj = createStringObject(key,sdslen(key));
                    //
                    propagateExpire(db,keyobj);
                    dbDelete(db,keyobj);
                    decrRefCount(keyobj);
                    expired++;
                    server.stat_expiredkeys++;
                }
            }
            // iteration , 16
           // , ,
            iteration++;
            if ((iteration & 0xf) == 0 &&
                (ustime()-start) > timelimit)
            {
                timelimit_exit = 1;
                return;
            }
        // 25%,
        } while (expired > REDIS_EXPIRELOOKUPS_PER_CRON/4);
    }
}

三、Memcachedが失効したプライマリ・キーを削除する方法はRedisと何が違いますか?
まず、Memcachedは、失効したプライマリ・キーを削除する際にも消極的な方法を採用しています.つまり、Memcached内部でもプライマリ・キーが失効したかどうかを監視するのではなく、Getを介してプライマリ・キーにアクセスしたときに失効したかどうかを確認します.次に、MemcachedとRedisのプライマリ・キー失効メカニズムの最大の違いは、MemcachedがRedisのように失効したプライマリ・キーを本当に削除するのではなく、単純に失効したプライマリ・キーが占有する空間を回収することである.これにより、新しいデータがシステムに書き込まれると、Memcachedは失効したプライマリ・キーの空間を優先的に使用します.失効したプライマリ・キーのスペースが切れた場合、MemcachedはLRUメカニズムによって長期にわたってアクセスできないスペースを回収することもできるため、MemcachedはRedisのような周期的な削除操作を必要とせず、Memcachedが使用するメモリ管理メカニズムによって決定される.また、ここでは、OOMが発生した場合にも、Redisがmaxmemory-policyというパラメータを構成することによって、LRUメカニズムを採用するかどうかを決定することができることを指摘する必要があります(@Jonathan_Daiさんが『RedisのLRUメカニズム』で原文を指摘してくれたことに感謝します).RedisではLRUがデフォルトのメカニズムですが、すべてのキーに有効期限が設定されておらず、Redisのメモリ消費量がmaxmemoryに達している場合、キーを追加または変更した場合はどうなりますか?適切なキーが削除できない場合、Redisは書き込み中にエラーを返します.バージョン2.8に基づくredisプロファイルの詳細を参照
四、Redisの主キーの失効メカニズムはシステムの性能に影響しますか?
以上、Redisのプライマリ・キー失効メカニズムについて説明したように、Redisは、失効時間が設定されているプライマリ・キーを定期的にチェックし、失効したプライマリ・キーを削除するが、データベースの個数を処理する毎の制限、activeExpireCycle関数の1秒以内の回数の制限、activeExpireCycle関数CPUに割り当てられた時間の制限、プライマリ・キーを削除し続ける失効プライマリ・キーの数の割合の制限により、Redisは、プライマリ・キーの失効メカニズムがシステム全体のパフォーマンスに及ぼす影響を大幅に低減しているが、実際のアプリケーションで多数のプライマリ・キーが短時間で同時に失効する場合、システムの応答能力が低下するため、このような状況は回避されるに違いない.