解读Redis运行核心循环过程

来源:互联网 发布:梦里花落知多少评论 编辑:程序博客网 时间:2024/04/29 15:17

让我们直接main函数开始:

?
1
2
3
4
5
6
7
8
9
10
11
12
intmain(intargc, char**argv) {
    structtimeval tv;
 
    /* We need to initialize our libraries, and the server configuration. */
    zmalloc_enable_thread_safeness();
    zmalloc_set_oom_handler(redisOutOfMemoryHandler);
    srand(time(NULL)^getpid());
    gettimeofday(&tv,NULL);
    dictSetHashFunctionSeed(tv.tv_sec^tv.tv_usec^getpid());
    server.sentinel_mode = checkForSentinelMode(argc,argv);
 
        initServerConfig();

zmalloc_enable_thread_safeness()开启了内存分配管理的线程安全变量,当内存分配时,redis会统计一个总内存分配量,这是一个共享资源,所以需要原子性操作,在redis的内存分配代码里,当需要原子操作时,就需要打开线程安全变量。
zmalloc_set_oom_handler(redisOutOfMemoryHandler)是一个内存分配错误处理,当无法得到需要的内存量时,会调用redisOutOfMemoryHandler函数。
srand(time(NULL)^getpid());
gettimeofday(&tv,NULL);
dictSetHashFunctionSeed(tv.tv_sec^tv.tv_usec^getpid());
都是为了设置随机函数种子。

** initServerConfig
这个函数初始化了一个全局变量struct redisServer server; server结构包括了redis服务器端程序的大部分配置:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
structredisServer {
    /* General */
        redisDb *db;
    dict *commands;             /* Command table hahs table */
    aeEventLoop *el;
    unsigned lruclock:22;       /* Clock incrementing every minute, for LRU */
    unsigned lruclock_padding:10;
    intshutdown_asap;          /* SHUTDOWN needed ASAP */
    intactiverehashing;        /* Incremental rehash in serverCron() */
    char*requirepass;          /* Pass for AUTH command, or NULL */
    char*pidfile;              /* PID file path */
    intarch_bits;              /* 32 or 64 depending on sizeof(long) */
    intcronloops;              /* Number of times the cron function run */
    charrunid[REDIS_RUN_ID_SIZE+1];  /* ID always different at every exec. */
    intsentinel_mode;          /* True if this instance is a Sentinel. */
    /* Networking */
    intport;                   /* TCP listening port */
    char*bindaddr;             /* Bind address or NULL */
    char*unixsocket;           /* UNIX socket path */
    mode_t unixsocketperm;      /* UNIX socket permission */
    intipfd;                   /* TCP socket file descriptor */
    intsofd;                   /* Unix socket file descriptor */
    intcfd;                    /* Cluster bus lisetning socket */
    list *clients;              /* List of active clients */
    list *clients_to_close;     /* Clients to close asynchronously */
    list *slaves, *monitors;    /* List of slaves and MONITORs */
    redisClient *current_client; /* Current client, only used on crash report */
    charneterr[ANET_ERR_LEN];  /* Error buffer for anet.c */
…….  
    intbug_report_start; /* True if bug report header was already logged. */
    intwatchdog_period;  /* Software watchdog period in ms. 0 = off */
};

在这个函数中,对server变量进行了部分成员的初始化,其中:
runid:运行该redis服务器端程序的唯一标识,即每次启动都会一个唯一ID,用来区分不同的redis服务器端程序。
maxidletime:最大空闲时间,就是client连接到server时,如果超出这个值,就会被自动断开,当然,master和slave节点不包括。如果client有阻塞命令在运行,也不会断开。
saveparams:这个存储的是redis服务器端程序从配置文件中读取的持久化参数,如配置文件所述:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# Save the DB on disk:
# save <seconds> <changes>
# Will save the DB if both the given number of seconds and the given
# number of write operations against the DB occurred.
# In the example below the behaviour will be to save:
# after 900 sec (15 min) if at least 1 key changed
# after 300 sec (5 min) if at least 10 keys changed
# after 60 sec if at least 10000 keys changed
# It is also possible to remove all the previously configured save
# points by adding a save directive with a single empty string argument like in the following example:
 
save 900 1
save 300 10
save 60 10000

lruclock:是redis实现LRU算法所需的,每个redis object都带有一个lruclock,用来从内存中移除空闲的对象。
commands:是redis命令的字符数组。
sentinel_mode:是否开启redis的哨兵模式,也就是是否监测,通知,自动错误恢复,是用来管理多个redis实例的方式。

  • 回到main函数
?
1
2
3
4
if(server.sentinel_mode) {
        initSentinelConfig();
        initSentinel();
    }

是redis哨兵模式的配置和初始化。

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
if(argc >= 2) {
        intj = 1; /* First option to parse in argv[] */
        sds options = sdsempty();
        char*configfile = NULL;
 
        /* 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);
            }
        }
 
        /* First argument is the config file name? */
        if(argv[j][0] != '-'|| argv[j][1] != '-')
            configfile = argv[j++];
        /* 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 file name
         * is parsed, if any. */
        while(j != argc) {
            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++;
        }
        resetServerSaveParams();
        loadServerConfig(configfile,options);
        sdsfree(options);
    }else{
        redisLog(REDIS_WARNING,"Warning: no config file specified, using the default config. In order to specify a config file use %s /path/to/%s.conf", argv[0], server.sentinel_mode ? "sentinel": "redis");
    }

以上代码从启动redis的命令行中读取选项,任何可以在redis.conf配置的选项名都可以在redis启动命令行上直接指定,并且生效。然后再读取redis.conf。

?
1
2
3
4
if(server.daemonize) daemonize();
initServer();
if(server.daemonize) createPidFile();
redisAsciiArt();

daemonize()用于在shell启动时后台运行。

  • initServer
    主要做以下工作:
  1. 设置信号处理程序,如sighup, sigpipe等
  2. 打开系统日志文件
  3. 继续初始化server结构,如server.clients, server.slaves, server.monitors等
  4. 创建共享对象,这里的共享对象是一个struct sharedObjectsStruct shared;它用于全局文本信息的保存,避免每次发送固定格式的信息给clients都需要创建一个新的字符串。如:
?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
shared.crlf = createObject(REDIS_STRING,sdsnew("\r\n"));
shared.ok = createObject(REDIS_STRING,sdsnew("+OK\r\n"));
shared.err = createObject(REDIS_STRING,sdsnew("-ERR\r\n"));
shared.emptybulk = createObject(REDIS_STRING,sdsnew("$0\r\n\r\n"));
shared.czero = createObject(REDIS_STRING,sdsnew(":0\r\n"));
shared.cone = createObject(REDIS_STRING,sdsnew(":1\r\n"));
shared.cnegone = createObject(REDIS_STRING,sdsnew(":-1\r\n"));
shared.nullbulk = createObject(REDIS_STRING,sdsnew("$-1\r\n"));
shared.nullmultibulk = createObject(REDIS_STRING,sdsnew("*-1\r\n"));
shared.emptymultibulk = createObject(REDIS_STRING,sdsnew("*0\r\n"));
shared.pong = createObject(REDIS_STRING,sdsnew("+PONG\r\n"));
shared.queued = createObject(REDIS_STRING,sdsnew("+QUEUED\r\n"));
shared.wrongtypeerr = createObject(REDIS_STRING,sdsnew(
"-ERR Operation against a key holding the wrong kind of value\r\n"));
shared.nokeyerr = createObject(REDIS_STRING,sdsnew(
"-ERR no such key\r\n"));
shared.syntaxerr = createObject(REDIS_STRING,sdsnew(
"-ERR syntax error\r\n"));

还包括了从1到10000的redis 整数对象的创建,在这部分范围的整数经常被用到,所以先创建了,可以反复重用。
5. 调整系统对文件操作参数的约束大小,如最大打开的文件数。
6. 创建ae事件循环
7. 初始化server.db
8. 开启监听redis端口或者unix socket。
9. 初始化server结构的统计变量,如执行的命令数,连接数,过期键等等,还有跟踪每秒操作的时间和命令数。
10. 创建ae时间事件,也是redis的核心循环,该过程是serverCron,每秒调用次数由一个叫REDIS_HZ的宏决定,默认是每10微秒超时,即每10微秒该ae时间事件处理过程serverCron会被过期调用。
11. 创建ae文件事件,对redis的TCP或者unix socket端口进行监听,使用相应的处理函数注册。每次得到clients连接后,都会创建ae文件事件,异步接收命令。
12. 针对配置文件,设置是否开启aof和最大使用内存
13. 如果有集群设置,初始化集群。初始化lua脚本处理,初始化slowlog和bio(background io)。bio是异步io操作,用于redis读取或存取时的io操作。

  • serverCron核心循环
    serverCron才是redis真正的循环例程,在这里定义了一个特殊的宏:
?
1
#define run_with_period(_ms_) if (!(server.cronloops%((_ms_)/(1000/REDIS_HZ))))

这个宏类似于条件判断,每ms时间执行一次后续的操作。如:
run_with_period(100) trackOperationsPerSecond();
每百微秒,执行一次跟踪操作函数,记录这段时间的命令执行情况。
这个循环有以下任务需要执行:
1. 如果设置了watchdog_period,那么每过watchdog_period,都会发送sigalrm信号,该信号又会得到处理,来记录此时执行的命令。这个过程主要是为了了解一些过长命令的执行影响服务器的整体运行,是一个debug过程。
2. 每百微秒记录过去每秒的命令执行情况。
3. 更新统计变量,如内存使用总数,更新server.lruclock
4. 是否得到关闭程序的信号,如果是,就进入关闭程序的节奏,如aof,rdb文件的处理,文件描述符的关闭等
5. 每5秒输出一次redis数据库的使用情况,连接数,总键值数
6. 每次都尝试resize每个db,resize是让每个db的dict结构进入rehash状态,rehash是为了扩容dict或者缩小dict。然后每次都尝试执行每个db的rehash过程一微秒。
7. 每次调用clientCron例程,这是一个对server.clients列表进行处理的过程。再每次执行clientCron时,会对server.clients进行迭代,并且保证 1/(REDIS_HZ*10) of clients per call。也就是每次执行clientCron,如果clients过多,clientCron不会遍历所有clients,而是遍历一部分clients,但是保证每个clients都会在一定时间内得到处理。处理过程主要是检测client连接是否idle超时,或者block超时,然后会调解每个client的缓冲区大小。
8. 对aof,rdb等过程进行开启或终结。
9. 如果是master节点的话,就开始对过期的键值进行处理,与处理clients类似,不是多所有有时间限制的键值进行迭代,而是在一个限定的数量内迭代一部分,保证一定时间内能检测所有键值。
10. 对异步io过程中可能需要关闭的clients进行处理。
11. 每秒调用复制例程和集群例程,每0.1秒调用哨兵例程。

  • 回到main函数
?
1
2
3
aeSetBeforeSleepProc(server.el,beforeSleep);
aeMain(server.el);
aeDeleteEventLoop(server.el);

在每次ae循环进入阻塞时,都会先执行beforeSleep(),在该函数中,会对unblock的clients(指使用blpop等阻塞命令的clients)进行处理,并且执行fsync函数,同步内存到磁盘上。

  • 总结
    redis启动->初始化server结构部分变量->从命令行和配置文件中读取配置选项进行初始化->创建ae事件循环->创建ae时间事件调用redis运行的必需任务(serverCron)和创建ae文件事件监听端口->收到client连接时,创建对应的文件事件来纳入ae事件循环进行异步接受->收到关闭请求,在serverCron中执行关闭步骤->redis关闭

  • 感受
    作者在redis代码中体现了对程序运行时间的高度把控到了微秒级别,作者在代码中处处对运行例程进行约束,保证不过长的陷入某一个不友好的命令中,如检查过期键值和处理过多的clients。

0 0