Redis Server startup process parsed from source

Posted by fred12ned on Wed, 22 Dec 2021 09:50:10 +0100

Redis Server Startup Process

Today, let's start with Redis server.

We know that the main function is the entry to Redis's entire running program, and that when a Redis instance is running, it also starts executing from this main function. Also, since Redis is a typical Client-Server architecture, once the Redis instance starts running, the Redis server will start, and the main function will actually be responsible for starting the Redis server.

The basic control logic for Redis to run is server.c file, and the main function is in the server.c.

You can learn Redis's ideas for the following three questions:

  • What initialization will Redis server do after it starts?
  • What are the key configurations for Redis server initialization?
  • How does Redis server start processing client requests?

Okay, next, let's start with the main function to see how it is designed and implemented in Redis server.

Main function: The entry of Redis server Generally speaking, the code logic of a system software developed in C to start running is implemented in main function, so I want to share a small Tips with you before you can formally understand the implementation of main function in Redis, that is, when you read and learn the code of a system, you can first find main function. See how it works. So for Redis's main function, I've divided the work it does into five phases.

Stage 1: Basic Initialization

At this stage, the main function mainly completes some basic initialization work, including setting the time zone in which the server runs, setting the random seed of the hash function, and so on. The main calling functions for this part of the work are as follows:

// timezone
setlocale(LC_COLLATE,"");
tzset(); /* Populates 'timezone' global. */
zmalloc_set_oom_handler(redisOutOfMemoryHandler);
srand(time(NULL)^getpid());
srandom(time(NULL)^getpid());
gettimeofday(&tv,NULL);
init_genrand64(((long long) tv.tv_sec * 1000000 + tv.tv_usec) ^ getpid());
crc64_init();

/* Store umask value. Because umask(2) only offers a set-and-get API we have
     * to reset it and restore it back. We do this early to avoid a potential
     * race condition with threads that could be creating files or directories.
     */
umask(server.umask = umask(0777));
// Set up random seeds
uint8_t hashseed[16];
getRandomBytes(hashseed,sizeof(hashseed));
dictSetHashFunctionSeed(hashseed);

Here, you need to note that at the beginning of the main function, there is a section of code that the macro definition overrides. This part of the code is used if REDIS_is defined The main function executes the corresponding test program if the TEST macro is defined and the Redis server parameters match the test parameters at startup.

The code for this macro definition is shown below, and an example is the test function ziplistTest that calls ziplist:

#ifdef REDIS_TEST
// If the startup parameters have test and ziplist, then the ziplistTest function is called to test the ziplist
    if (argc >= 3 && !strcasecmp(argv[1], "test")) {
        int accurate = 0;
        for (j = 3; j < argc; j++) {
            if (!strcasecmp(argv[j], "--accurate")) {
                accurate = 1;
            }
        }

        if (!strcasecmp(argv[2], "all")) {
            int numtests = sizeof(redisTests)/sizeof(struct redisTest);
            for (j = 0; j < numtests; j++) {
                redisTests[j].failed = (redisTests[j].proc(argc,argv,accurate) != 0);
            }

            /* Report tests result */
            int failed_num = 0;
            for (j = 0; j < numtests; j++) {
                if (redisTests[j].failed) {
                    failed_num++;
                    printf("[failed] Test - %s\n", redisTests[j].name);
                } else {
                    printf("[ok] Test - %s\n", redisTests[j].name);
                }
            }

            printf("%d tests, %d passed, %d failed\n", numtests,
                   numtests-failed_num, failed_num);

            return failed_num == 0 ? 0 : 1;
        } else {
            redisTestProc *proc = getTestProcByName(argv[2]);
            if (!proc) return -1; /* test not found */
            return proc(argc,argv,accurate);
        }

        return 0;
    }
#endif

Stage 2: Check Sentinel Mode and whether RDB or AOF detection is to be performed

Redis server may be running in Sentry mode after it is started. Sentry mode servers differ from normal mode servers in parameter initialization, parameter settings, and operations to be performed during server startup. Therefore, the main function needs to check whether Sentry mode is set based on the parameters configured by Redis during execution. If Sentinel mode is set, the main function calls the initSentinelConfig function, initializes the parameters of Sentinel mode, and calls the initSentinel function to initialize the server that sets Sentinel mode to run. I will elaborate on the Redis server mechanisms for Sentinel mode operation later. The following code shows the main function for checking sentinel mode and initializing sentinel mode, as you can see:

/* 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. */
//Determine if server is set to Sentry mode
if (server.sentinel_mode) {
    //Initialize Sentry Configuration
    initSentinelConfig();
    //Initialize Sentry Mode
    initSentinel();
}

In addition to checking sentinel mode, the main function also checks whether RDB or AOF checks are to be performed, which corresponds to the actual programs running redis-check-rdb or redis-check-aof. In this case, the main function calls redis_check_rdb_main function or redis_check_aof_main function, detects RDB or AOF files. You can see the following code, which shows how the main function checks and calls this part:

/* Check if we need to start in redis-check-rdb/aof mode. We just execute
     * the program main. However the program is part of the Redis executable
     * so that we can easily execute an RDB check on loading errors. */
// If you are running the redis-check-rdb program, call redis_ Check_ Rdb_ The main function detects RDB files
if (strstr(argv[0],"redis-check-rdb") != NULL)
    redis_check_rdb_main(argc,argv,NULL);
//If you are running a redis-check-aof program, call redis_check_aof_main function detects AOF files    
else if (strstr(argv[0],"redis-check-aof") != NULL)
    redis_check_aof_main(argc,argv);

Phase 3: Analysis of Operating Parameters

At this stage, the main function parses the parameters passed in from the command line, calls the loadServerConfig function, merges the command line parameters with the parameters in the configuration file, and then sets appropriate values for the key parameters of the Redis functional modules so that the server can run efficiently.

/* Load the server configuration from the specified filename.
 * The function appends the additional configuration directives stored
 * in the 'options' string to the config file before loading.
 *
 * Both filename and options can be NULL, in such a case are considered
 * empty. This way loadServerConfig can be used to just load a file or
 * just load a string. */
void loadServerConfig(char *filename, char config_from_stdin, char *options) {
    sds config = sdsempty();
    char buf[CONFIG_MAX_LINE+1];
    FILE *fp;

    /* Load the file content */
    // Load file contents
    if (filename) {
        if ((fp = fopen(filename,"r")) == NULL) {
            serverLog(LL_WARNING,"Fatal error, can't open config file '%s': %s", filename, strerror(errno));
            exit(1);
        }
        while(fgets(buf,CONFIG_MAX_LINE+1,fp) != NULL)
            config = sdscat(config,buf);
        fclose(fp);
    }
    // Append parameters entered from the command line
    /* Append content from stdin */
    if (config_from_stdin) {
        serverLog(LL_WARNING,"Reading config from stdin");
        fp = stdin;
        while(fgets(buf,CONFIG_MAX_LINE+1,fp) != NULL)
            config = sdscat(config,buf);
    }

    /* Append the additional options */
    // Append additional commands
    if (options) {
        config = sdscat(config,"\n");
        config = sdscat(config,options);
    }
    loadServerConfigFromString(config);
    sdsfree(config);
}

Stage 4: Initializing the server

After parsing and setting the run parameters, the main function calls the initServer function to initialize various resources at the server runtime. This mainly includes initialization of data structure, initialization of key value to database, initialization of server network framework, etc.

void initServer(void) {
    int j;

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

    if (server.syslog_enabled) {
        openlog(server.syslog_ident, LOG_PID | LOG_NDELAY | LOG_NOWAIT, server.syslog_facility);
    }

    // Initialize after setting default values from the configuration system
    /* Initialization after setting defaults from the config system. */
    server.aof_state = server.aof_enabled ? AOF_ON : AOF_OFF;
    server.hz = server.config_hz;
    server.pid = getpid();
    server.in_fork_child = CHILD_TYPE_NONE;
    server.main_thread_id = pthread_self();
    server.current_client = NULL;
    server.errors = raxNew();
    server.fixed_time_expire = 0;
    server.clients = listCreate();
    server.clients_index = raxNew();
    server.clients_to_close = listCreate();
    server.slaves = listCreate();
    server.monitors = listCreate();
    server.clients_pending_write = listCreate();
    server.clients_pending_read = listCreate();
    server.clients_timeout_table = raxNew();
    server.replication_allowed = 1;
    server.slaveseldb = -1; /* Force to emit the first SELECT command. */
    server.unblocked_clients = listCreate();
    server.ready_keys = listCreate();
    server.clients_waiting_acks = listCreate();
    server.get_ack_from_slaves = 0;
    server.client_pause_type = 0;
    server.paused_clients = listCreate();
    server.events_processed_while_blocked = 0;
    server.system_memory_size = zmalloc_get_memory_size();
    server.blocked_last_cron = 0;
    server.blocking_op_nesting = 0;

    if ((server.tls_port || server.tls_replication || server.tls_cluster)
                && tlsConfigure(&server.tls_ctx_config) == C_ERR) {
        serverLog(LL_WARNING, "Failed to configure TLS. Check logs for more info.");
        exit(1);
    }

    createSharedObjects();
    adjustOpenFilesLimit();
    const char *clk_msg = monotonicInit();
    serverLog(LL_NOTICE, "monotonic clock: %s", clk_msg);
    server.el = aeCreateEventLoop(server.maxclients+CONFIG_FDSET_INCR);
    if (server.el == NULL) {
        serverLog(LL_WARNING,
            "Failed creating the event loop. Error message: '%s'",
            strerror(errno));
        exit(1);
    }
    server.db = zmalloc(sizeof(redisDb)*server.dbnum);

    // Open TCP listening socket for user commands
    /* Open the TCP listening socket for the user commands. */
    if (server.port != 0 &&
        listenToPort(server.port,&server.ipfd) == C_ERR) {
        serverLog(LL_WARNING, "Failed listening on port %u (TCP), aborting.", server.port);
        exit(1);
    }
    if (server.tls_port != 0 &&
        listenToPort(server.tls_port,&server.tlsfd) == C_ERR) {
        serverLog(LL_WARNING, "Failed listening on port %u (TLS), aborting.", server.tls_port);
        exit(1);
    }

    // Open socket listening to Unix
    /* 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) {
            serverLog(LL_WARNING, "Opening Unix socket: %s", server.neterr);
            exit(1);
        }
        anetNonBlock(NULL,server.sofd);
        anetCloexec(server.sofd);
    }

    // Give up if you don't have a socket to listen on
    /* Abort if there are no listening sockets at all. */
    if (server.ipfd.count == 0 && server.tlsfd.count == 0 && server.sofd < 0) {
        serverLog(LL_WARNING, "Configured to not listen anywhere, exiting.");
        exit(1);
    }

    // Create a Redis database and initialize the internal state of the database
    /* Create the Redis databases, and initialize other internal state. */
    for (j = 0; j < server.dbnum; j++) {
        server.db[j].dict = dictCreate(&dbDictType,NULL);
        server.db[j].expires = dictCreate(&dbExpiresDictType,NULL);
        server.db[j].expires_cursor = 0;
        server.db[j].blocking_keys = dictCreate(&keylistDictType,NULL);
        server.db[j].ready_keys = dictCreate(&objectKeyPointerValueDictType,NULL);
        server.db[j].watched_keys = dictCreate(&keylistDictType,NULL);
        server.db[j].id = j;
        server.db[j].avg_ttl = 0;
        server.db[j].defrag_later = listCreate();
        listSetFreeMethod(server.db[j].defrag_later,(void (*)(void*))sdsfree);
    }
    // Initialize pool for LRU key
    evictionPoolAlloc(); /* Initialize the LRU keys pool. */
    server.pubsub_channels = dictCreate(&keylistDictType,NULL);
    server.pubsub_patterns = dictCreate(&keylistDictType,NULL);
    server.cronloops = 0;
    server.in_eval = 0;
    server.in_exec = 0;
    server.propagate_in_transaction = 0;
    server.client_pause_in_transaction = 0;
    server.child_pid = -1;
    server.child_type = CHILD_TYPE_NONE;
    server.rdb_child_type = RDB_CHILD_TYPE_NONE;
    server.rdb_pipe_conns = NULL;
    server.rdb_pipe_numconns = 0;
    server.rdb_pipe_numconns_writing = 0;
    server.rdb_pipe_buff = NULL;
    server.rdb_pipe_bufflen = 0;
    server.rdb_bgsave_scheduled = 0;
    server.child_info_pipe[0] = -1;
    server.child_info_pipe[1] = -1;
    server.child_info_nread = 0;
    aofRewriteBufferReset();
    server.aof_buf = sdsempty();
    server.lastsave = time(NULL); /* At startup we consider the DB saved. */
    server.lastbgsave_try = 0;    /* At startup we never tried to BGSAVE. */
    server.rdb_save_time_last = -1;
    server.rdb_save_time_start = -1;
    server.dirty = 0;
    resetServerStats();
    /* A few stats we don't want to reset: server startup time, and peak mem. */
    server.stat_starttime = time(NULL);
    server.stat_peak_memory = 0;
    server.stat_current_cow_bytes = 0;
    server.stat_current_cow_updated = 0;
    server.stat_current_save_keys_processed = 0;
    server.stat_current_save_keys_total = 0;
    server.stat_rdb_cow_bytes = 0;
    server.stat_aof_cow_bytes = 0;
    server.stat_module_cow_bytes = 0;
    server.stat_module_progress = 0;
    for (int j = 0; j < CLIENT_TYPE_COUNT; j++)
        server.stat_clients_type_memory[j] = 0;
    server.cron_malloc_stats.zmalloc_used = 0;
    server.cron_malloc_stats.process_rss = 0;
    server.cron_malloc_stats.allocator_allocated = 0;
    server.cron_malloc_stats.allocator_active = 0;
    server.cron_malloc_stats.allocator_resident = 0;
    server.lastbgsave_status = C_OK;
    server.aof_last_write_status = C_OK;
    server.aof_last_write_errno = 0;
    server.repl_good_slaves_count = 0;

    /* Create the timer callback, this is our way to process many background
     * operations incrementally, like clients timeout, eviction of unaccessed
     * expired keys and so forth. */
    if (aeCreateTimeEvent(server.el, 1, serverCron, NULL, NULL) == AE_ERR) {
        serverPanic("Can't create event loop timers.");
        exit(1);
    }

    /* Create an event handler for accepting new connections in TCP and Unix
     * domain sockets. */
    if (createSocketAcceptHandler(&server.ipfd, acceptTcpHandler) != C_OK) {
        serverPanic("Unrecoverable error creating TCP socket accept handler.");
    }
    if (createSocketAcceptHandler(&server.tlsfd, acceptTLSHandler) != C_OK) {
        serverPanic("Unrecoverable error creating TLS socket accept handler.");
    }
    if (server.sofd > 0 && aeCreateFileEvent(server.el,server.sofd,AE_READABLE,
        acceptUnixHandler,NULL) == AE_ERR) serverPanic("Unrecoverable error creating server.sofd file event.");


    /* Register a readable event for the pipe used to awake the event loop
     * when a blocked client in a module needs attention. */
    if (aeCreateFileEvent(server.el, server.module_blocked_pipe[0], AE_READABLE,
        moduleBlockedClientPipeReadable,NULL) == AE_ERR) {
            serverPanic(
                "Error registering the readable event for the module "
                "blocked clients subsystem.");
    }

    /* Register before and after sleep handlers (note this needs to be done
     * before loading persistence since it is used by processEventsWhileBlocked. */
    aeSetBeforeSleepProc(server.el,beforeSleep);
    aeSetAfterSleepProc(server.el,afterSleep);

    /* Open the AOF file if needed. */
    if (server.aof_state == AOF_ON) {
        server.aof_fd = open(server.aof_filename,
                               O_WRONLY|O_APPEND|O_CREAT,0644);
        if (server.aof_fd == -1) {
            serverLog(LL_WARNING, "Can't open the append-only file: %s",
                strerror(errno));
            exit(1);
        }
    }

    /* 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) {
        serverLog(LL_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 = MAXMEMORY_NO_EVICTION;
    }

    if (server.cluster_enabled) clusterInit();
    replicationScriptCacheInit();
    scriptingInit(1);
    slowlogInit();
    latencyMonitorInit();
    
    /* Initialize ACL default password if it exists */
    ACLUpdateDefaultUserPassword(server.requirepass);
}

After calling initServer, the main function will again determine if the current server is Sentry mode. If in Sentry mode, the main function calls the sentinelIsRunning function to set the start Sentry mode.

/* This function gets called when the server is in Sentinel mode, started,
 * loaded the configuration, and is ready for normal operations. */
// This function is called when the server is in Sentinel mode, the configuration is loaded, and ready for normal operation.
void sentinelIsRunning(void) {
    int j;

    /* If this Sentinel has yet no ID set in the configuration file, we
     * pick a random one and persist the config on disk. From now on this
     * will be this Sentinel ID across restarts. */
    // If the Sentinel has not yet set an ID in the configuration file, we randomly select one and persist the configuration on disk. From now on, this Sentry ID will be used on reboot.
    for (j = 0; j < CONFIG_RUN_ID_SIZE; j++)
        if (sentinel.myid[j] != 0) break;

    if (j == CONFIG_RUN_ID_SIZE) {
        /* Pick ID and persist the config. */
        getRandomHexChars(sentinel.myid,CONFIG_RUN_ID_SIZE);
        sentinelFlushConfig();
    }

    /* Log its ID to make debugging of issues simpler. */
    serverLog(LL_WARNING,"Sentinel ID is %s", sentinel.myid);

    /* We want to generate a +monitor event for every configured master
     * at startup. */
    // We want to generate a monitor event for each configured host at startup
    sentinelGenerateInitialMonitorEvents();
}

Otherwise, the main function calls the loadDataFromDisk function to load an AOF or RDB file from disk to restore the previous data.

/* Function called at startup to load RDB or AOF file in memory. */
void loadDataFromDisk(void) {
    long long start = ustime();
    // If AOF configuration is on
    if (server.aof_state == AOF_ON) {
        // Load AOF Configuration
        if (loadAppendOnlyFile(server.aof_filename) == C_OK)
            serverLog(LL_NOTICE,"DB loaded from append only file: %.3f seconds",(float)(ustime()-start)/1000000);
    }
    // Otherwise, the AOF is not opened and the RDB file is loaded
    else {
        rdbSaveInfo rsi = RDB_SAVE_INFO_INIT;
        errno = 0; /* Prevent a stale value from affecting error checking */
        if (rdbLoad(server.rdb_filename,&rsi,RDBFLAGS_NONE) == C_OK) {
            serverLog(LL_NOTICE,"DB loaded from disk: %.3f seconds",
                (float)(ustime()-start)/1000000);

            /* Restore the replication ID / offset from the RDB file. */
            // Restore copy ID/offset from RDB file
            if ((server.masterhost ||
                (server.cluster_enabled &&
                nodeIsSlave(server.cluster->myself))) &&
                rsi.repl_id_is_set &&
                rsi.repl_offset != -1 &&
                /* Note that older implementations may save a repl_stream_db
                 * of -1 inside the RDB file in a wrong way, see more
                 * information in function rdbPopulateSaveInfo. */
                rsi.repl_stream_db != -1)
            {
                memcpy(server.replid,rsi.repl_id,sizeof(server.replid));
                server.master_repl_offset = rsi.repl_offset;
                /* If we are a slave, create a cached master from this
                 * information, in order to allow partial resynchronizations
                 * with masters. */
                // If we are a slave, create a cached host from this information to allow partial slave resynchronization of the host.
                replicationCacheMasterUsingMyself();
                selectDb(server.cached_master,rsi.repl_stream_db);
            }
        } else if (errno != ENOENT) {
            serverLog(LL_WARNING,"Fatal error loading the DB: %s. Exiting.",strerror(errno));
            exit(1);
        }
    }
}

Phase 5: Implementing an Event Driven Framework

To efficiently handle highly concurrent client connection requests, Redis uses an event-driven framework to concurrently handle connection and read-write requests from different clients. Therefore, when the main function is executed to the end, it calls the aeMain function to enter the event-driven framework and begin to loop through the various triggered events. I have drawn the key operations involved in the five stages just described in the following figure, which you can review again.

So, in these five phases, phases three, four and five actually include the key operations in the Redis server startup process. So next, we will learn the main work of the next three stages in turn.

Resolution and Setup of Redis Run Parameters

As we know, Redis provides a rich set of capabilities that support not only read and write access to data types for multiple key values, but also data persistence, master-slave replication, tile clusters, and so on. The efficient operation of these functions can not be separated from the configuration of key parameters of related functional modules.

For example, Redis designed a compact data structure to save memory for key-value pair types such as Hash and Sorted Set. However, after using a compact in-memory data structure, the access performance of key-value pairs can be affected if too many or too many elements are stored in the data structure. Therefore, to balance memory usage and system access performance, we can set and adjust the conditions under which a compact data structure in memory is used by parameters.

That is, knowing the settings of these key parameters can help us improve the efficiency of the Redis instance.

However, Redis has many parameters and we can't master all the parameter settings in one lesson. So let's start by learning about Redis's main parameter types so that we can get a comprehensive understanding of the various parameters. Also, I'll show you some parameters that are closely related to the server's operation and how to set them so that you can configure them so that the server can run efficiently.

Main parameter types for Redis

First, all the parameters required for Redis to run are uniformly defined in the server. The redisServer structure of the H file. Based on the scope of the parameters, I divide the parameters into seven types, including general parameters, data structure parameters, network parameters, persistence parameters, master-slave replication parameters, slice cluster parameters, and performance optimization parameters. Specifically, you can refer to the table below.

This way, if you can categorize Redis parameters according to the above classification method, you can see that these parameters actually correspond to Redis's main functional mechanism. Therefore, if you want to know more about the typical configuration values of these parameters, you need to understand how the corresponding functional mechanisms work.

Okay, now that we know the seven parameters of Redis and their basic scope of action, let's move on to learn how Redis sets these parameters.

Setting Redis Parameters

Redis's settings for run parameters actually go through three rounds of assignments, default configuration values, command line startup parameters, and profile configuration values.

First, Redis calls the initServerConfig function in the main function to set default values for various parameters. Default values for parameters are uniformly defined on the server.h file, all with CONFIG_ The macro at the beginning of DEFAULT defines variables. The code below shows the default values for some parameters, which you can see.

/* Static server configuration */
// Default frequency of server background tasks
#define CONFIG_DEFAULT_HZ        10             /* Time interrupt calls/sec. */
// Minimum frequency of server background tasks
#define CONFIG_MIN_HZ            1
// Maximum frequency of server background tasks
#define CONFIG_MAX_HZ            500
// Default TCP port for server listening
#define MAX_CLIENTS_PER_CLOCK_TICK 200          /* HZ is adapted based on that. */
#define CONFIG_MAX_LINE    1024
#define CRON_DBS_PER_CALL 16
#define NET_MAX_WRITES_PER_EVENT (1024*64)
#define PROTO_SHARED_SELECT_CMDS 10
#define OBJ_SHARED_INTEGERS 10000
#define OBJ_SHARED_BULKHDR_LEN 32
#define LOG_MAX_LEN    1024 /* Default maximum length of syslog messages.*/
#define AOF_REWRITE_ITEMS_PER_CMD 64
#define AOF_READ_DIFF_INTERVAL_BYTES (1024*10)
#define CONFIG_AUTHPASS_MAX_LEN 512
#define CONFIG_RUN_ID_SIZE 40
#define RDB_EOF_MARK_SIZE 40
#define CONFIG_REPL_BACKLOG_MIN_SIZE (1024*16)          /* 16k */
#define CONFIG_BGSAVE_RETRY_DELAY 5 /* Wait a few secs before trying again. */
#define CONFIG_DEFAULT_PID_FILE "/var/run/redis.pid"
#define CONFIG_DEFAULT_CLUSTER_CONFIG_FILE "nodes.conf"
#define CONFIG_DEFAULT_UNIX_SOCKET_PERM 0
#define CONFIG_DEFAULT_LOGFILE ""
#define NET_HOST_STR_LEN 256 /* Longest valid hostname */
#define NET_IP_STR_LEN 46 /* INET6_ADDRSTRLEN is 46, but we need to be sure */
#define NET_ADDR_STR_LEN (NET_IP_STR_LEN+32) /* Must be enough for ip:port */
#define NET_HOST_PORT_STR_LEN (NET_HOST_STR_LEN+32) /* Must be enough for hostname:port */
#define CONFIG_BINDADDR_MAX 16
#define CONFIG_MIN_RESERVED_FDS 32
#define CONFIG_DEFAULT_PROC_TITLE_TEMPLATE "{title} {listen-addr} {server-mode}"
/* Synchronous read timeout - slave side */
// Synchronous Read Timeout - From End
#define CONFIG_REPL_SYNCIO_TIMEOUT 5

On server. The default parameter values provided in H are typically configuration values. Therefore, if you do not know how Redis works during deployment using Redis instances, you can use the default configuration provided in the code. Of course, if you are familiar with the working mechanism of the Redis functional modules, you can also set your own operating parameters. You can set the value of the run parameter on the command line when you start the Redis program. For example, if you want to change the Redis server listening port from the default of 6379 to 7379, you can set the port parameter to 7379 on the command line, as follows:

./redis-server --port 7379

Here, you need to note that Redis's command line parameter settings require the use of ** two minus signs'-'** to represent the corresponding parameter names, otherwise Redis will not be able to recognize the set run parameters.

Redis sets default configuration values for parameters using the initServerConfig function

void initServerConfig(void) {
    int j;

    updateCachedTime(1);
    getRandomHexChars(server.runid,CONFIG_RUN_ID_SIZE);
    server.runid[CONFIG_RUN_ID_SIZE] = '\0';
    changeReplicationId();
    clearReplicationId2();
    server.hz = CONFIG_DEFAULT_HZ; /* Initialize it ASAP, even if it may get
                                      updated later after loading the config.
                                      This value may be used before the server
                                      is initialized. */
    server.timezone = getTimeZone(); /* Initialized by tzset(). */
    server.configfile = NULL;
    server.executable = NULL;
    server.arch_bits = (sizeof(long) == 8) ? 64 : 32;
    server.bindaddr_count = 0;
    server.unixsocketperm = CONFIG_DEFAULT_UNIX_SOCKET_PERM;
    server.ipfd.count = 0;
    server.tlsfd.count = 0;
    server.sofd = -1;
    server.active_expire_enabled = 1;
    server.skip_checksum_validation = 0;
    server.saveparams = NULL;
    server.loading = 0;
    server.loading_rdb_used_mem = 0;
    server.logfile = zstrdup(CONFIG_DEFAULT_LOGFILE);
    server.aof_state = AOF_OFF;
    server.aof_rewrite_base_size = 0;
    server.aof_rewrite_scheduled = 0;
    server.aof_flush_sleep = 0;
    server.aof_last_fsync = time(NULL);
    atomicSet(server.aof_bio_fsync_status,C_OK);
    server.aof_rewrite_time_last = -1;
    server.aof_rewrite_time_start = -1;
    server.aof_lastbgrewrite_status = C_OK;
    server.aof_delayed_fsync = 0;
    server.aof_fd = -1;
    server.aof_selected_db = -1; /* Make sure the first time will not match */
    server.aof_flush_postponed_start = 0;
    server.pidfile = NULL;
    server.active_defrag_running = 0;
    server.notify_keyspace_events = 0;
    server.blocked_clients = 0;
    memset(server.blocked_clients_by_type,0,
           sizeof(server.blocked_clients_by_type));
    server.shutdown_asap = 0;
    server.cluster_configfile = zstrdup(CONFIG_DEFAULT_CLUSTER_CONFIG_FILE);
    server.cluster_module_flags = CLUSTER_MODULE_FLAG_NONE;
    server.migrate_cached_sockets = dictCreate(&migrateCacheDictType,NULL);
    server.next_client_id = 1; /* Client IDs, start from 1 .*/
    server.loading_process_events_interval_bytes = (1024*1024*2);

    unsigned int lruclock = getLRUClock();
    atomicSet(server.lruclock,lruclock);
    resetServerSaveParams();

    appendServerSaveParams(60*60,1);  /* save after 1 hour and 1 change */
    appendServerSaveParams(300,100);  /* save after 5 minutes and 100 changes */
    appendServerSaveParams(60,10000); /* save after 1 minute and 10000 changes */

    /* Replication related */
    server.masterauth = NULL;
    server.masterhost = NULL;
    server.masterport = 6379;
    server.master = NULL;
    server.cached_master = NULL;
    server.master_initial_offset = -1;
    server.repl_state = REPL_STATE_NONE;
    server.repl_transfer_tmpfile = NULL;
    server.repl_transfer_fd = -1;
    server.repl_transfer_s = NULL;
    server.repl_syncio_timeout = CONFIG_REPL_SYNCIO_TIMEOUT;
    server.repl_down_since = 0; /* Never connected, repl is down since EVER. */
    server.master_repl_offset = 0;

    /* Replication partial resync backlog */
    server.repl_backlog = NULL;
    server.repl_backlog_histlen = 0;
    server.repl_backlog_idx = 0;
    server.repl_backlog_off = 0;
    server.repl_no_slaves_since = time(NULL);

    /* Failover related */
    server.failover_end_time = 0;
    server.force_failover = 0;
    server.target_replica_host = NULL;
    server.target_replica_port = 0;
    server.failover_state = NO_FAILOVER;

    /* Client output buffer limits */
    for (j = 0; j < CLIENT_TYPE_OBUF_COUNT; j++)
        server.client_obuf_limits[j] = clientBufferLimitsDefaults[j];

    /* Linux OOM Score config */
    for (j = 0; j < CONFIG_OOM_COUNT; j++)
        server.oom_score_adj_values[j] = configOOMScoreAdjValuesDefaults[j];

    /* Double constants initialization */
    R_Zero = 0.0;
    R_PosInf = 1.0/R_Zero;
    R_NegInf = -1.0/R_Zero;
    R_Nan = R_Zero/R_Zero;

    /* Command table -- we initialize it here as it is part of the
     * initial configuration, since command names may be changed via
     * redis.conf using the rename-command directive. */
    server.commands = dictCreate(&commandTableDictType,NULL);
    server.orig_commands = dictCreate(&commandTableDictType,NULL);
    populateCommandTable();
    server.delCommand = lookupCommandByCString("del");
    server.multiCommand = lookupCommandByCString("multi");
    server.lpushCommand = lookupCommandByCString("lpush");
    server.lpopCommand = lookupCommandByCString("lpop");
    server.rpopCommand = lookupCommandByCString("rpop");
    server.zpopminCommand = lookupCommandByCString("zpopmin");
    server.zpopmaxCommand = lookupCommandByCString("zpopmax");
    server.sremCommand = lookupCommandByCString("srem");
    server.execCommand = lookupCommandByCString("exec");
    server.expireCommand = lookupCommandByCString("expire");
    server.pexpireCommand = lookupCommandByCString("pexpire");
    server.xclaimCommand = lookupCommandByCString("xclaim");
    server.xgroupCommand = lookupCommandByCString("xgroup");
    server.rpoplpushCommand = lookupCommandByCString("rpoplpush");
    server.lmoveCommand = lookupCommandByCString("lmove");

    /* Debugging */
    server.watchdog_period = 0;

    /* By default we want scripts to be always replicated by effects
     * (single commands executed by the script), and not by sending the
     * script to the slave / AOF. This is the new way starting from
     * Redis 5. However it is possible to revert it via redis.conf. */
    server.lua_always_replicate_commands = 1;

    /* Client Pause related */
    server.client_pause_type = CLIENT_PAUSE_OFF;
    server.client_pause_end_time = 0;   

    initConfigValues();
}

Next, the main function parses the command line parameters one by one when the Redis program starts. The main function saves the parsed parameters and their values as strings, and the main function then calls the loadServerConfig function for the second and third rounds of assignments.

/* Load the server configuration from the specified filename.
 * The function appends the additional configuration directives stored
 * in the 'options' string to the config file before loading.
 *
 * Both filename and options can be NULL, in such a case are considered
 * empty. This way loadServerConfig can be used to just load a file or
 * just load a string. */
void loadServerConfig(char *filename, char config_from_stdin, char *options) {
    sds config = sdsempty();
    char buf[CONFIG_MAX_LINE+1];
    FILE *fp;

    /* Load the file content */
    if (filename) {
        if ((fp = fopen(filename,"r")) == NULL) {
            serverLog(LL_WARNING,
                    "Fatal error, can't open config file '%s': %s",
                    filename, strerror(errno));
            exit(1);
        }
        while(fgets(buf,CONFIG_MAX_LINE+1,fp) != NULL)
            config = sdscat(config,buf);
        fclose(fp);
    }
    /* Append content from stdin */
    if (config_from_stdin) {
        serverLog(LL_WARNING,"Reading config from stdin");
        fp = stdin;
        while(fgets(buf,CONFIG_MAX_LINE+1,fp) != NULL)
            config = sdscat(config,buf);
    }

    /* Append the additional options */
    if (options) {
        config = sdscat(config,"\n");
        config = sdscat(config,options);
    }
    loadServerConfigFromString(config);
    sdsfree(config);
}

The following code shows how the main function parses command line parameters and how to call the loadServerConfig function, as you can see.

int main(int argc, char **argv) {
	......
    // Save command line parameters
    for (j = 0; j < argc; j++) server.exec_argv[j] = zstrdup(argv[j]);
	......
    // If the number of parameters is greater than or equal to two   
    if (argc >= 2) {
        j = 1; /* First option to parse in argv[] */
        sds options = sdsempty();

        /* Handle special options --help and --version */
        if (strcmp(argv[1], "-v") == 0 ||
            strcmp(argv[1], "--version") == 0) version();
        if (strcmp(argv[1], "--help") == 0 ||
            strcmp(argv[1], "-h") == 0) usage();
        if (strcmp(argv[1], "--test-memory") == 0) {
            if (argc == 3) {
                memtest(atoi(argv[2]),50);
                exit(0);
            } else {
                fprintf(stderr,"Please specify the amount of memory to test in megabytes.\n");
                fprintf(stderr,"Example: ./redis-server --test-memory 4096\n\n");
                exit(1);
            }
        }
        /* Parse command line options
         * Precedence wise, File, stdin, explicit options -- last config is the one that matters.
         *
         * First argument is the config file name? */
        if (argv[1][0] != '-') {
            /* Replace the config file in server.exec_argv with its absolute path. */
            server.configfile = getAbsolutePath(argv[1]);
            zfree(server.exec_argv[1]);
            server.exec_argv[1] = zstrdup(server.configfile);
            j = 2; // Skip this arg when parsing options
        }
        // Parse each run-time parameter
        while(j < argc) {
            /* Either first or last argument - Should we read config from stdin? */
            if (argv[j][0] == '-' && argv[j][1] == '\0' && (j == 1 || j == argc-1)) {
                config_from_stdin = 1;
            }
            /* All the other options are parsed and conceptually appended to the
             * configuration file. For instance --port 6380 will generate the
             * string "port 6380\n" to be parsed after the actual config file
             * and stdin input are parsed (if they exist). */
            // Is it two--
            else if (argv[j][0] == '-' && argv[j][1] == '-') {
                /* Option name */
                if (sdslen(options)) options = sdscat(options,"\n");
                options = sdscat(options,argv[j]+2);
                options = sdscat(options," ");
            } else {
                /* Option argument */
                options = sdscatrepr(options,argv[j],strlen(argv[j]));
                options = sdscat(options," ");
            }
            j++;
        }

        loadServerConfig(server.configfile, config_from_stdin, options);
        if (server.sentinel_mode) loadSentinelConfigFromQueue();
        sdsfree(options);
    }
    return 0;
}

What you need to know here is that the loadServerConfig function is config. Implemented in the C file, this function reads out all the configuration items in the configuration file to form a string using the parsing strings of Redis configuration file and command line parameters as parameters. Next, the loadServerConfig function appends the parsed command line parameters to the configuration item string formed by the configuration file.

This way, the configuration item string contains both the parameters set in the configuration file and those set on the command line.

Finally, the loadServerConfig function further calls the loadServerConfigFromString function to match each configuration item in the configuration item string. Once the match is successful, the loadServerConfigFromString function sets the server's parameters according to the value of the configuration item. The following code shows part of the loadServerConfigFromString function. This part of the code uses conditional branching to compare whether a configuration item is "timeout" and "tcp-keepalive" in turn, and if it matches, sets the server parameter to the value of the configuration item. The code also checks whether the value of the configuration item is reasonable, such as less than 0. If the parameter value is unreasonable, the program will fail at run time. In addition, for other configuration items, the loadServerConfigFromString function will continue to use the else branch for judgment.

Config. View in C file

loadServerConfigFromString(char *config) {
   ...
   //Parameter name matches, check if the parameter is "timeout"
   if (!strcasecmp(argv[0],"timeout") && argc == 2) {
       //Setting the server's maxidletime parameter
       server.maxidletime = atoi(argv[1]);
       //Check if parameter value is less than 0, error if less than 0
       if (server.maxidletime < 0) {
           err = "Invalid timeout value"; goto loaderr;
       }
   }
  //Parameter name matches, check if the parameter is "tcp-keepalive"
  else if (!strcasecmp(argv[0],"tcp-keepalive") && argc == 2) {
      //Set tcpkeepalive parameter for server
      server.tcpkeepalive = atoi(argv[1]);
	  //Check if parameter value is less than 0, error if less than 0
      if (server.tcpkeepalive < 0) {
          err = "Invalid tcp-keepalive value"; goto loaderr;
      }
   }
   ...
}

Okay, so here you should have seen the steps for configuring Redis server's run parameters, and I've drawn a diagram to help you understand the process more intuitively.

After configuring the parameters, the main function starts calling the initServer function to initialize the server. So next, let's move on to understand the key operations when Redis server is initialized.

initServer: Initialize

The initial operation of Redis server Redis server can be divided into three main steps.

  • First, the Redis server runtime needs to manage multiple resources.

For example, client connections to servers, slave libraries, and so on, Redis as a replacement candidate set for caching, as well as server runtime status information, the management information for these resources is initialized in the initServer function. Let me give you an example. The initServer function creates a chain table to maintain the client and slave libraries, respectively, and calls the evictionPoolAlloc function (in evict.c).

/* Create a new eviction pool. */
void evictionPoolAlloc(void) {
    struct evictionPoolEntry *ep;
    int j;
    ep = zmalloc(sizeof(*ep)*EVPOOL_SIZE);
    for (j = 0; j < EVPOOL_SIZE; j++) {
        ep[j].idle = 0;
        ep[j].key = NULL;
        ep[j].cached = sdsnewlen(NULL,EVPOOL_CACHED_SDS_SIZE);
        ep[j].dbid = 0;
    }
    EvictionPoolLRU = ep;
}

Sampling generates a set of candidate key s for elimination. The initServer function also calls the resetServerStats function (in server.c) to reset the server running state information.

/* Resets the stats that we expose via INFO or other means that we want
 * to reset via CONFIG RESETSTAT. The function is also used in order to
 * initialize these fields in initServer() at server startup. */
void resetServerStats(void) {
    int j;

    server.stat_numcommands = 0;
    server.stat_numconnections = 0;
    server.stat_expiredkeys = 0;
    server.stat_expired_stale_perc = 0;
    server.stat_expired_time_cap_reached_count = 0;
    server.stat_expire_cycle_time_used = 0;
    server.stat_evictedkeys = 0;
    server.stat_keyspace_misses = 0;
    server.stat_keyspace_hits = 0;
    server.stat_active_defrag_hits = 0;
    server.stat_active_defrag_misses = 0;
    server.stat_active_defrag_key_hits = 0;
    server.stat_active_defrag_key_misses = 0;
    server.stat_active_defrag_scanned = 0;
    server.stat_fork_time = 0;
    server.stat_fork_rate = 0;
    server.stat_total_forks = 0;
    server.stat_rejected_conn = 0;
    server.stat_sync_full = 0;
    server.stat_sync_partial_ok = 0;
    server.stat_sync_partial_err = 0;
    server.stat_io_reads_processed = 0;
    atomicSet(server.stat_total_reads_processed, 0);
    server.stat_io_writes_processed = 0;
    atomicSet(server.stat_total_writes_processed, 0);
    for (j = 0; j < STATS_METRIC_COUNT; j++) {
        server.inst_metric[j].idx = 0;
        server.inst_metric[j].last_sample_time = mstime();
        server.inst_metric[j].last_sample_count = 0;
        memset(server.inst_metric[j].samples,0,
            sizeof(server.inst_metric[j].samples));
    }
    atomicSet(server.stat_net_input_bytes, 0);
    atomicSet(server.stat_net_output_bytes, 0);
    server.stat_unexpected_error_replies = 0;
    server.stat_total_error_replies = 0;
    server.stat_dump_payload_sanitizations = 0;
    server.aof_delayed_fsync = 0;
}
  • In the second step, the initServer function initializes the Redis database after initializing the resource management information.

Since a Redis instance can run multiple databases at the same time, the initServer function uses a loop to create corresponding data structures for each database in turn. This code logic is implemented in the initServer function, which performs initialization operations for each database, including creating a global hash table, creating corresponding information tables for expired keys, keys blocked by BLPOP, keys to be PUSH, and keys to be listened on.

/* Create the Redis databases, and initialize other internal state. */
for (j = 0; j < server.dbnum; j++) {
    //Create a global hash table
    server.db[j].dict = dictCreate(&dbDictType,NULL);
    //Create information table for expired key s
    server.db[j].expires = dictCreate(&dbExpiresDictType,NULL);
    server.db[j].expires_cursor = 0;
    //Create information tables for key s blocked by BLPOP
    server.db[j].blocking_keys = dictCreate(&keylistDictType,NULL);
    //Create an information table for the blocking key that will execute PUSH
    server.db[j].ready_keys = dictCreate(&objectKeyPointerValueDictType,NULL);
    //Create information tables for key s listened for by MULTI/WATCH operations
    server.db[j].watched_keys = dictCreate(&keylistDictType,NULL);
    server.db[j].id = j;
    server.db[j].avg_ttl = 0;
    server.db[j].defrag_later = listCreate();
    listSetFreeMethod(server.db[j].defrag_later,(void (*)(void*))sdsfree);
}
  • In the third step, the initServer function creates an event-driven framework for the running Redis server and starts port listening to receive external requests.

Note that in order to efficiently handle highly concurrent external requests, initServer creates listening events in the event framework for each possible client connection on the listening IP to listen for client connection requests. At the same time, initServer sets the corresponding handler function acceptTcpHandler for listening for events.

Networking. View in C file

void acceptTcpHandler(aeEventLoop *el, int fd, void *privdata, int mask) {
    int cport, cfd, max = MAX_ACCEPTS_PER_CALL;
    char cip[NET_IP_STR_LEN];
    UNUSED(el);
    UNUSED(mask);
    UNUSED(privdata);

    while(max--) {
        cfd = anetTcpAccept(server.neterr, fd, cip, sizeof(cip), &cport);
        if (cfd == ANET_ERR) {
            if (errno != EWOULDBLOCK)
                serverLog(LL_WARNING,
                    "Accepting client connection: %s", server.neterr);
            return;
        }
        anetCloexec(cfd);
        serverLog(LL_VERBOSE,"Accepted %s:%d", cip, cport);
        acceptCommonHandler(connCreateAcceptedSocket(cfd),0,cip);
    }
}

This way, whenever a client connects to the IP and port that the server is listening on, the event-driven framework detects that a connection event occurs and then calls the acceptTcpHandler function to handle the specific connection. You can refer to the processing logic shown in the following code:

//Create Event Loop Framework
server.el = aeCreateEventLoop(server.maxclients+CONFIG_FDSET_INCR);
if (server.el == NULL) {
    serverLog(LL_WARNING,"Failed creating the event loop. Error message: '%s'",strerror(errno));
    exit(1);
}
// Allocate memory to database
server.db = zmalloc(sizeof(redisDb)*server.dbnum);

//Start listening for set network ports
/* Open the TCP listening socket for the user commands. */
if (server.port != 0 &&
    listenToPort(server.port,&server.ipfd) == C_ERR) {
    serverLog(LL_WARNING, "Failed listening on port %u (TCP), aborting.", server.port);
    exit(1);
} 
/* Create the timer callback, this is our way to process many background
     * operations incrementally, like clients timeout, eviction of unaccessed
     * expired keys and so forth. */
//Create timed events for server background tasks
if (aeCreateTimeEvent(server.el, 1, serverCron, NULL, NULL) == AE_ERR) {
    serverPanic("Can't create event loop timers.");
    exit(1);
}

Now that Redis server has finished setting and initializing the run parameters, it can begin processing client requests. In order to continuously process concurrent client requests, the server enters the Event Driven Loop mechanism at the end of the main function. And that's what we'll look at next.

Execute Event Driven Framework

The Event Driven Framework is the core of the Redis server. Once the framework is started, it is executed in a loop that handles a batch of triggered network read and write events. Regarding the design idea and implementation method of Event Driven Framework itself, here we mainly learn how to transform to Event Driven Framework in main function of Redis entrance for execution.

In fact, entering the event-driven framework to start execution is not complicated. The main function directly calls the event framework's main function aeMain (in the ae.c file) and then enters the event processing loop.

Of course, before entering an event-driven loop, the main function calls the aeSetBeforeSleepProc and aeSetAfterSleepProc functions to set what the server needs to do before each event loop enters and what the server needs to do after each event loop ends. The code below shows this part of the execution logic, which you can see.

int main(int argc, char **argv) {
    ...
   /* Register before and after sleep handlers (note this needs to be done
     * before loading persistence since it is used by processEventsWhileBlocked. */
    aeSetBeforeSleepProc(server.el,beforeSleep);
    aeSetAfterSleepProc(server.el,afterSleep);
    aeMain(server.el);
    aeDeleteEventLoop(server.el);
  ...
}

summary

We pass through the server. The main function in the C file is designed and implemented to understand the five main stages after Redis server is started. In these five phases, running parameter resolution, server initialization, and execution of event-driven frameworks are the three key phases in the Redis sever startup process. Accordingly, we need to focus on the following three main points.

  • First, the main function sets default values for the server run parameters using initServerConfig, then parses the command line parameters, reads the configuration file parameter values through loadServerConfig, and appends the command line parameters to the configuration item string. Finally, Redis calls the loadServerConfigFromString function to complete the configuration file parameters and command line parameters.
  • Second, after Redis server finishes setting parameters, the initServer function is invoked to initialize the main structure of server resource management, initialize the database startup state, and complete server listening on IP and port settings.
  • Third, once servers can receive requests from external clients, the main function gives control of the program's principal to the event-driven framework's entry function, the aeMain function. The aeMain function continues to loop through incoming client requests. So far, server. The main function in C is complete, program control is given to the Event Driven Loop framework, and Redis can handle client requests normally.

In fact, the Redis server startup process ranges from basic initialization operations to parameter resolution settings for command lines and configuration files, to initialization of server data structures, and finally to the Event Driven Framework, which is a typical network server execution process that you can use as a reference when developing a network server. In addition, having a good grasp of the initialization during startup can help you solve some of the problems in use. For example, if Redis starts reading an RDB file or an AOF file first. If you know how Redis server is started, you can see from the loadDataFromDisk function that Redis server reads the AOF first. If there is no AOF, then the RDB is read. So mastering the Redis server startup process can help you better understand the details of how Redis works so that when you encounter problems, you know that you can also trace the various initial states of the server from the startup process to help you solve the problem better.

The above Redis source profiles and battles from Geek Time are interesting to see, but on his basis, I added what kind of source each came from and adjusted some of my personal formatting preferences.

Topics: Database Redis