redisソース分析(1)——初期化


最近ずっとredisのソースコードを見ていて、ソースコードに対する理解と読書の心得を記録して、忘れないようにしてみんなと分かち合って、話すことができます.コードの考え方を見るのは簡単で、main関数から直接歩いて、まず初期化過程を見てみましょう.
redisの最も重要なデータ構造はredisです.serverは、この構造のグローバル変数serverを作成し、現在のredisの構成と状態を表し、初期化の大部分の作業はこの構造の属性を設定することです.
初期化作業は主に4つの部分に分けることができます.
1)serverのデフォルト設定
          
2)コマンドラインパラメータの解析
          
3)解析プロファイル
          
4)serverの初期化
     
初期化が完了すると、イベントループが開始され、サービスリクエストが受信されます.次のステップでmain関数を解析しますが、sentinelロジックには注目しません.
(1)まずmain関数の開始部分であり,collateの設定,乱数シードの設定を含む簡単な初期化作業を主に行い,initServerConfig()を呼び出してグローバル変数serverのデフォルト値を設定する.
struct timeval tv;

    /* We need to initialize our libraries, and the server configuration. */
#ifdef INIT_SETPROCTITLE_REPLACEMENT
    spt_init(argc, argv);
#endif
    // <MM>
    //                
    // </MM>
    setlocale(LC_COLLATE,"");
    zmalloc_enable_thread_safeness();
    zmalloc_set_oom_handler(redisOutOfMemoryHandler);
    // <MM>
    //   random   ,        run id,      id  
    // </MM>
    srand(time(NULL)^getpid());
    gettimeofday(&tv,NULL);
    dictSetHashFunctionSeed(tv.tv_sec^tv.tv_usec^getpid());
    server.sentinel_mode = checkForSentinelMode(argc,argv);
    // <MM>
    //  redisServer       
    // </MM>
    initServerConfig();

    /* We need to init sentinel right now as parsing the configuration file
     * in sentinel mode will have the effect of populating the sentinel
     * data structures with master nodes to monitor. */
    if (server.sentinel_mode) {
        initSentinelConfig();
        initSentinel();
    }

InitServerConfig関数では、ほとんどがserverのプロパティにデフォルト値を設定し、一部はpopulateCommandTable関数を呼び出してredisのコマンドテーブルを初期化します.グローバル変数redisCommandTableは、redisCommandタイプの配列であり、redisがサポートするすべてのコマンドを保存します.server.commandsはdictで、redisCommandへのコマンド名のマッピングを保存します.populateCommandTable関数は、グローバルredisCommandTableを巡回し、各コマンドをserverに挿入します.commandsでは、各コマンドのプロパティに基づいてflagsを設定します.
redisCommand構造は次のとおりです.
struct redisCommand {
    //     , server.commands    ,     key
    char *name;
    //       
    redisCommandProc *proc;
    // command    ,>0         ,<0      arity    
    int arity;
    //           ,           
    char *sflags; /* Flags as string representation, one char per flag. */
    //      ,bitwise,  command   
    int flags;    /* The actual flags, obtained from the 'sflags' field. */

    //    4                   key,  mset k1, v1, k2, v2 ...
    /* Use a function to determine keys arguments in a command line. */
    redisGetKeysProc *getkeys_proc;
    /* What keys should be loaded in background when calling this command? */
    int firstkey; /* The first argument that's a key (0 = no keys) */
    int lastkey;  /* The last argument that's a key */
    int keystep;  /* The step between first and last key */
    //     
    long long microseconds, calls;
};

redisCommandには、コマンド処理関数を表す属性procがあります.クライアント要求を処理する際に、serverからコマンド名を取得する.commands辞書でredisCommandを取得し、proc関数をコールバックします.
(2)コマンドライン解析部は,主にコマンドライン構成オプションと,コマンドラインを介して入力された構成項目を解析する.次に、2つの関数で構成ファイルが解析されます.
          
loadServer Config:ファイルの内容を文字列に読み込む機能は簡単です.コマンドラインを介して入力された構成項目を文字列に追加します.
        
loadServerConfigFromString:文字列から構成項目を解析し、serverの関連プロパティを設定します.
このステップが完了すると、serverの単純なプロパティ(整数、文字列)は基本的に設定されます.
(3)次に初期化部である.デーモンプロセスに設定し、pidファイルを作成し、プロセスtitleを設定し、起動ログを印刷します.その中で最も重要なのはinitServer関数がインフラストラクチャを初期化することです.初期化後、rdbまたはaofであってもよいディスクからデータをロードする必要があります.
    // <MM>
    //     
    // </MM>
    if (server.daemonize) daemonize();
    initServer();
    // <MM>
    //   pid  
    // </MM>
    if (server.daemonize) createPidFile();
    // <MM>
    //       
    // </MM>
    redisSetProcTitle(argv[0]);
    // <MM>
    //       
    // </MM>
    redisAsciiArt();

    if (!server.sentinel_mode) {
        /* Things not needed when running in Sentinel mode. */
        redisLog(REDIS_WARNING,"Server started, Redis version " REDIS_VERSION);
    #ifdef __linux__
        linuxOvercommitMemoryWarning();
    #endif
        // <MM>
        //        ,rdb aof
        // </MM>
        loadDataFromDisk();
        if (server.ipfd_count > 0)
            redisLog(REDIS_NOTICE,"The server is now ready to accept connections on port %d", server.port);
        if (server.sofd > 0)
            redisLog(REDIS_NOTICE,"The server is now ready to accept connections at %s", server.unixsocket);
    } else {
        sentinelIsRunning();
    }

    /* Warning the user about suspicious maxmemory setting. */
    if (server.maxmemory > 0 && server.maxmemory < 1024*1024) {
        redisLog(REDIS_WARNING,"WARNING: You specified a maxmemory value that is less than 1MB (current value is %llu bytes). Are you sure this is what you really want?", server.maxmemory);
    }
 
InitServer Configではserverの整数型、文字列タイプの属性設定が行われ、initServerは主に複合データ構造list、dictなどを初期化し、イベントループを作成し、リスニングsocketなどを初期化します.次にinitServer関数を見てみましょう.
まず、信号処理関数を登録する.
    int j;

    signal(SIGHUP, SIG_IGN);
    signal(SIGPIPE, SIG_IGN);
    setupSignalHandlers();

syslogをオンに設定すると初期化されます.
    if (server.syslog_enabled) {
        openlog(server.syslog_ident, LOG_PID | LOG_NDELAY | LOG_NOWAIT,
            server.syslog_facility);
    }

クライアント関連のデータ構造を初期化します.redisは、接続されているすべてのクライアント、slave、monitorのクライアントをチェーンテーブルに組織します.ここでは、主にこれらのチェーンテーブルを作成します.
    server.current_client = NULL;
    server.clients = listCreate();
    server.clients_to_close = listCreate();
    server.slaves = listCreate();
    server.monitors = listCreate();
    server.slaveseldb = -1; /* Force to emit the first SELECT command. */
    server.unblocked_clients = listCreate();
    server.ready_keys = listCreate();

redisでは、頻繁に作成、回収されるオーバーヘッドを回避するために、頻繁に使用されるオブジェクトに対して定数プールが作成されます.これらのオブジェクトには、よく使用される応答内容と10000未満の整数と、bulk個数を表す応答ヘッダとbulk長を表す応答ヘッダとが含まれ、この2つの応答ヘッダは32未満の長さのみを含む.
    // <MM>
    //      ,      
    // </MM>
    createSharedObjects();

オープンディスクリプタ制限を調整するには、getrlimitとsetrlimitを呼び出す必要があります.
    // <MM>
    //   maxclients  ,           
    // </MM>
    adjustOpenFilesLimit();

イベントループを初期化します.具体的な手順は、イベントループのセクションで説明します.次に、構成に基づいてdb配列を作成します.
    // <MM>
    //    event loop,  epoll  ,   epoll_create  epoll fd
    // </MM>
    server.el = aeCreateEventLoop(server.maxclients+REDIS_EVENTLOOP_FDSET_INCR);
    server.db = zmalloc(sizeof(redisDb)*server.dbnum);

リスニングsocketを初期化すると、socket、bind、listenなどを呼び出し、socketを非ブロックに設定します.Unix domain socketが構成されている場合も、対応する初期化が行われます.
    /* Open the TCP listening socket for the user commands. */
    if (server.port != 0 &&
        listenToPort(server.port,server.ipfd,&server.ipfd_count) == REDIS_ERR)
        exit(1);

    /* Open the listening Unix domain socket. */
    if (server.unixsocket != NULL) {
        unlink(server.unixsocket); /* don't care if this fails */
        server.sofd = anetUnixServer(server.neterr,server.unixsocket,
            server.unixsocketperm, server.tcp_backlog);
        if (server.sofd == ANET_ERR) {
            redisLog(REDIS_WARNING, "Opening socket: %s", server.neterr);
            exit(1);
        }
        anetNonBlock(NULL,server.sofd);
    }

    /* Abort if there are no listening sockets at all. */
    if (server.ipfd_count == 0 && server.sofd < 0) {
        redisLog(REDIS_WARNING, "Configured to not listen anywhere, exiting.");
        exit(1);
    }

次のコードはserverを初期化します.db配列の各dbは、主に関連するdictを作成します.次にpubsub,rdb,aof,replication関連データ構造の初期化について述べる.コードは貼らない.
次にタイマイベントserverCronを追加し、イベントループを1 ms起動した後に実行します.redisのイベントループはこれだけで、0.1 s(約)ごとに実行され、主にexpired keysをクリアしたり、統計を更新したり、checkがrdb、aofサブプロセスの状態を行ったりして、clientCronやdatabaseCronなどの非同期タスクを処理します.
    // <MM>
    //   serverCrom    ,  event loop 1ms   
    // </MM>
    /* Create the serverCron() time event, that's our main way to process
     * background operations. */
    if(aeCreateTimeEvent(server.el, 1, serverCron, NULL, NULL) == AE_ERR) {
        redisPanic("Can't create the serverCron time event.");
        exit(1);
    }

すべてのリスニングsocketをイベントループに追加します.クライアント接続の確立を処理するacceptTcpHandlerが表示されます.
    // <MM>
    //       socket  file event,      acceptTcpHandler         
    // </MM>
    /* Create an event handler for accepting new connections in TCP and Unix
     * domain sockets. */
    for (j = 0; j < server.ipfd_count; j++) {
        if (aeCreateFileEvent(server.el, server.ipfd[j], AE_READABLE,
            acceptTcpHandler,NULL) == AE_ERR)
            {
                redisPanic(
                    "Unrecoverable error creating server.ipfd file event.");
            }
    }
    if (server.sofd > 0 && aeCreateFileEvent(server.el,server.sofd,AE_READABLE,
        acceptUnixHandler,NULL) == AE_ERR) redisPanic("Unrecoverable error creating server.sofd file event.");

aofをオンにすると、aofファイルが作成されます.
    /* Open the AOF file if needed. */
    if (server.aof_state == REDIS_AOF_ON) {
        server.aof_fd = open(server.aof_filename,
                               O_WRONLY|O_APPEND|O_CREAT,0644);
        if (server.aof_fd == -1) {
            redisLog(REDIS_WARNING, "Can't open the append-only file: %s",
                strerror(errno));
            exit(1);
        }
    }

最後のセクションでは、maxmemory構成を検証して設定し、luaスクリプト、slow log、latency monitorを初期化し、最後のbioInitは非同期ioスレッドを初期化します.redisは単一スレッドであり、aofのfsyncなどの再IO操作の呼び出しは非同期スレッドによって呼び出され、プライマリスレッドのイベントループをブロックしないようにします.以上、initServer関数です.
    /* 32 bit instances are limited to 4GB of address space, so if there is
     * no explicit limit in the user provided configuration we set a limit
     * at 3 GB using maxmemory with 'noeviction' policy'. This avoids
     * useless crashes of the Redis instance for out of memory. */
    if (server.arch_bits == 32 && server.maxmemory == 0) {
        redisLog(REDIS_WARNING,"Warning: 32 bit instance detected but no memory limit set. Setting 3 GB maxmemory limit with 'noeviction' policy now.");
        server.maxmemory = 3072LL*(1024*1024); /* 3 GB */
        server.maxmemory_policy = REDIS_MAXMEMORY_NO_EVICTION;
    }

    replicationScriptCacheInit();
    scriptingInit();
    slowlogInit();
    latencyMonitorInit();
    // <MM>
    //        IO    
    // </MM>
    bioInit();

(4)main関数の最後に,イベントループを起動する.イベントループの反復のたびに、sleepの前にbeforeSleep関数が呼び出され、いくつかの非同期処理が行われます.ここではまずbeforeSleep関数を設定し、aeMainイベントループを開始します.イベントループから終了すると、イベントループをクリーンアップし、終了します.
    aeSetBeforeSleepProc(server.el,beforeSleep);
    // <MM>
    //   event loop
    // </MM>
    aeMain(server.el);
    aeDeleteEventLoop(server.el);
    return 0;

以上がredisの初期化・起動プロセスである.全体的に、比較的簡単で、nginxのプロファイル解析、モジュールの初期化に比べて、プロセスの初期化はずっと簡単で、読むのにあまり苦労しません.
次の記事では、イベントループについて説明します.