【redisソース】大きなkeyを削除することでredisマスターが切り替わる

6558 ワード

1.問題の概要
先日、アラームを受信し、同時にRedisチームはredisクラスタに主従切替が発生したことを監視した.
最終的な分析の原因は、大きなkeyを削除し、redisのメインサーバーがブロックされ、sentinel哨兵はメインサーバーがダウンタイムし、故障移転を行ったと考えている.次の図に示します.
Redisクラスタでは、アプリケーションはできるだけ大きなキーを使用しないようにします.直接影響すると、クラスタの容量と要求に「傾斜問題」が発生しやすく、同時に大きなキーを削除したり、キーを押したりして期限が切れたりすると、フェイルオーバーやアプリケーションの雪崩の障害が発生しやすくなります.
クエリーラインにはコレクションキーがあり、コレクションoea_set_star_ol_2017要素の個数は4300万に達した.このキーを削除したり、キーが期限切れになったりすると、redisメインプロセスがブロックされ、プライマリ・セカンダリ・スイッチングが発生します.(コレクション内の各要素オブジェクトはメモリ領域を解放し、時間の複雑さが高い)
2.ソリューション
周知のように、Redisは単一プロセスでコマンド要求を実行する.集合には4000万以上の要素があります.この集合を削除するには、直接削除することはできません.そうしないと、メインプロセスがブロックされます.
コレクションの要素を少しずつ削除することができます.
Redis 2.8以降では、SSCANコマンド、HSCANコマンド、ZSCANコマンドに関連するSCANコマンドが提供されています.
実行するたびに少量の要素しか返されません.(KEYSコマンド、SMEMBERSコマンドのような問題は発生しません.KEYSコマンドが大きなデータベースを処理するために使用された場合、またはSMEMBERSコマンドが大きなコレクションキーを処理するために使用された場合、サーバが数秒もブロックされる可能性があります.)
HSCANでは、500個のフィールドを取得するたびにHDELコマンドを使用して、1個のフィールドを削除することができます.
これにより、削除プロセスの時間的複雑さも高い(クライアントの複雑さを向上させるには、keyを複数回取得し、削除コマンドを一括で実行する必要がある)が、少なくともredisサーバをブロックすることはない.
3.より良いソリューション
redisもこの問題を発見した:delコマンドを直接使用して大きなkeyを削除するとRedisメインプロセスがブロックされる;ロットごとに削除し、クライアントの複雑度が高い.
したがって、Redis 4.0では、不活性削除lazyfreeが提案されている:ユーザがセットキーを削除するとき、またはセットキーが期限切れになって削除する必要があるとき、セット要素が64個より大きい場合、不活性削除を使用して、セットオブジェクトとデータベース辞書の関係だけを解除し、削除対象キューにセットイメージを入れ、バックグラウンドはキュー内のオブジェクトを順次取得し、真の削除を行う.
redis 4.0はlazyfreeのメカニズムを導入し、削除キーやデータベースの操作をバックグラウンドスレッドに配置して実行し、サーバのブロックをできるだけ避けることができます.
Lazyfreeの原理は想像に難くないが,オブジェクトを削除する際に論理的に削除するだけで,オブジェクトをバックグラウンドに捨て,バックグラウンドスレッドに真のdestructを実行させ,オブジェクトの体積が大きすぎるためにブロックを避けることである.
次に、redisソースコードに深く入り込み、redis不活性削除ポリシーを分析します.クライアントはコマンドを使用して大きなkeyを削除し、大きなkeyは期限切れに削除します.
3.1クライアント使用命令大key削除
redis 4.0削除要素には、delとunlinkの2つのコマンドがあります.delは以前のバージョンと同様に、オブジェクトを直接削除すると、メインプロセスがブロックされる可能性があります.unlinkは不活性削除です.
delコマンドとunlinkコマンドのコードロジックを見てみましょう.
{"unlink",unlinkCommand,-2,"wF",0,NULL,1,-1,1,0,0}
{"del",delCommand,-2,"w",0,NULL,1,-1,1,0,0},
 
void delCommand(client *c) {
    delGenericCommand(c,0);
}
 
void unlinkCommand(client *c) {
    delGenericCommand(c,1);
}
delGenericCommand        lazy  ;0    ,1  /    ,              ,             ;
//lazy       
void delGenericCommand(client *c, int lazy) {
    int numdel = 0, j;
 
    for (j = 1; j < c->argc; j++) {
        expireIfNeeded(c->db,c->argv[j]); //        (     ,redis        :          ,               )
        int deleted  = lazy ? dbAsyncDelete(c->db,c->argv[j]) : //  lazy      /      
                              dbSyncDelete(c->db,c->argv[j]);
        if (deleted) {
            signalModifiedKey(c->db,c->argv[j]);
            notifyKeyspaceEvent(NOTIFY_GENERIC,
                "del",c->argv[j],c->db->id);
            server.dirty++;
            numdel++;
        }
    }
    addReplyLongLong(c,numdel);
}

削除コマンドの前にこのkeyが期限切れであることを検出した場合、期限切れの削除操作を実行します.
int expireIfNeeded(redisDb *db, robj *key) {
    mstime_t when = getExpire(db,key);
    mstime_t now;
 
    if (when < 0) return 0; //key        
 
    //    db,    
    if (server.loading) return 0;
    //slave  ,   
    if (server.masterhost != NULL) return now > when;
 
    //    
    if (now <= when) return 0;
 
    //  
    server.stat_expiredkeys++;
    propagateExpire(db,key,server.lazyfree_lazy_expire); //         aof slaves
    notifyKeyspaceEvent(NOTIFY_EXPIRED,
        "expired",key,db->id);
    return server.lazyfree_lazy_expire ? dbAsyncDelete(db,key) : //            /    (     )
                                         dbSyncDelete(db,key);
}

不活性削除時に非同期削除関数が実行されます
//      :
#define LAZYFREE_THRESHOLD 64
int dbAsyncDelete(redisDb *db, robj *key) {
    //      
    if (dictSize(db->expires) > 0) dictDelete(db->expires,key->ptr);
 
    //        ,   
    dictEntry *de = dictUnlink(db->dict,key->ptr);
    if (de) {
        robj *val = dictGetVal(de);
        size_t free_effort = lazyfreeGetFreeEffort(val); //        (      ,hash       。。。)
        //       64 ,        1,     ;
        //  bio      
        if (free_effort > LAZYFREE_THRESHOLD && val->refcount == 1) {
            atomicIncr(lazyfree_objects,1);
            bioCreateBackgroundJob(BIO_LAZY_FREE,val,NULL,NULL);  //     
            dictSetVal(db->dict,de,NULL);
        }
    }
 
    //     (     ,        )
    if (de) {
        dictFreeUnlinkedEntry(db->dict,de);
        if (server.cluster_enabled) slotToKeyDel(key);
        return 1;
    } else {
        return 0;
    }
}
//      ,    
int dbSyncDelete(redisDb *db, robj *key) {
    /* Deleting an entry from the expires dict will not free the sds of
     * the key, because it is shared with the main dictionary. */
    if (dictSize(db->expires) > 0) dictDelete(db->expires,key->ptr);
    if (dictDelete(db->dict,key->ptr) == DICT_OK) {
        if (server.cluster_enabled) slotToKeyDel(key);
        return 1;
    } else {
        return 0;
    }
}

3.2期限切れの削除
期限切れキーには、次の3つの検出ポリシーがあります.
1.タイマーの追加:keyの有効期限が設定されている場合、タイマーを追加し、期限切れの削除をタイミングで実行する(そうしない)
2.周期性検査:周期性検査でいくつかのkeyの期限切れを検査し、期限切れになったら削除する.
3.このキーにアクセスした場合、期限が切れている場合は削除
redisは2と3の2つの戦略を結合し、期限切れのキーの検出を実現する.
期限切れキー削除関数は次のとおりです.
//       
int activeExpireCycleTryExpire(redisDb *db, dictEntry *de, long long now) {
    long long t = dictGetSignedIntegerVal(de);
    if (now > t) {
        sds key = dictGetKey(de);
        robj *keyobj = createStringObject(key,sdslen(key)); //     key         ;    key    sds
 
        //       key    ;
        propagateExpire(db,keyobj,server.lazyfree_lazy_expire);
        if (server.lazyfree_lazy_expire)    //     ,          ,     ,server.lazyfree_lazy_expire
            dbAsyncDelete(db,keyobj);
        else
            dbSyncDelete(db,keyobj);
        notifyKeyspaceEvent(NOTIFY_EXPIRED,
            "expired",keyobj,db->id);
        decrRefCount(keyobj);
        server.stat_expiredkeys++;
        return 1;
    } else {
        return 0;
    }
}

4.まとめ
大きなkey削除については,上記の2つのスキームが提案されている.
  • 低バージョンredis 2.8以上4.0以下:scanコマンドを使用して大keyの要素をバッチで取得し、大keyのすべての要素を削除するまでバッチで削除します. 
  • クライアントが大keyを削除するとき、unlinkコマンドを使用して、それは不活性な削除ポリシーを実行します.ただ、論理的に大keyを削除します.本当の削除はバックグラウンドスレッドで行われます.期限切れの削除にはserverを構成する必要がある.lazyfree_lazy_expirは、redisが期限切れキーを削除するときに不活性削除ポリシーを実行するようにします.