关于Redis的启动过程

一、简介

Redis的启动也就是main函数的执行,程序的入口在redis.c中,启动流程:

1. 初始化默认服务器配置,如果是sentinel模式还需进行额外的配置

2. 修改配置文件或配置选项,这其中包括处理诸如-h/--help,-v/--version,--test-memory的特殊选项,获取给定的配置文件,设定的配置选项,然后取得配置文件的绝对路径,重置保存条件,载入配置文件

3. 对服务器进行设置具体的包括:设置服务器为守护进程,创建并初始化服务器中的数据结构(如集群模式),为服务器进程设置名字,打印ASCII LOGO等等

4. 检查maxmemory配置,加载数据、运行事件处理器、监听事件

5.启动完成,AE会定时间去查询各个客户端是否有输入,如果有读取客户端输入并且对命令进行解析,命令信息保存在Hash表中

几个重要的函数

  • initServerConfig() 设定默认的参数值,并读取配置文件redis.conf,若用户配置了某个参数,则用该参数值替换默认值

  • initServer() 该函数主要对server进行初始化,内容包括: 调用anetTcpServer()函数创建socket server作为redis server,并将该server的句柄加到epoll/kqueue的监听队列中。 一旦有client接入,便会对该client触发操作acceptTcpHandler,该操作是调用aeCreateFileEvent注册的

  • anetTcpServer() 为建立网络套接字服务器的方法,对socket(), bind()和listen()等函数进行了封装

  • aeMain(aeEventLoop *eventLoop) 是启动事件轮询的入口,内部实现为一循环,不断处理来自客户的请求

二、初始化过程

1、执行下述语句

#ifdef INIT_SETPROCTITLE_REPLACEMENT
    spt_init(argc, argv);
#endif

其中INIT_SETPROCTITLE_REPLACEMENT的定义config.h中

/* Check if we can use setproctitle().
 * BSD systems have support for it, we provide an implementation for
 * Linux and osx. */
#if (defined __NetBSD__ || defined __FreeBSD__ || defined __OpenBSD__)
#define USE_SETPROCTITLE
#endif

#if ((defined __linux && defined(__GLIBC__)) || defined __APPLE__)
#define USE_SETPROCTITLE
#define INIT_SETPROCTITLE_REPLACEMENT
void spt_init(int argc, char *argv[]);
void setproctitle(const char *fmt, ...);
#endif

实现了对Linux和OSX的该功能的扩展(BSD系统已经支持该功能,而Linux和APPLE不支持)

2、setlocale

setlocale(LC_COLLATE,"");//系统调用,用来配置本地化信息。

3、zmalloc 的一些配置

zmalloc_enable_thread_safeness(); //开启了内存分配管理的线程安全变量,当内存分配时,
//redis会统计一个总内存分配量,这是一个共享资源,所以需要原子性操作,
//在redis的内存分配代码里,当需要原子操作时,就需要打开线程安全变量。
zmalloc_set_oom_handler(redisOutOfMemoryHandler);
//是一个内存分配错误处理,当无法得到需要的内存量时,会调用redisOutOfMemoryHandler函数。

4、随机函数种子

srand(time(NULL)^getpid());//设置随机种子
gettimeofday(&tv,NULL);//获取当前日期
dictSetHashFunctionSeed(tv.tv_sec^tv.tv_usec^getpid());
server.sentinel_mode = checkForSentinelMode(argc,argv);
//设置哈希函数需要使用的随机种子 服务器的启动模式:单机模式、Cluster模式、sentinel模式

5、初始化服务器配置

initServerConfig();
//在该函数中除了进行属性的初始化外,主要初始化了命令表。
//调用了函数populateCommandTable,将命令集分布到一个hash table中。
//避免使用if分支来做命令处理的效率底下问题,而放到hash table中,在理想的情况下只需一次就能定位命令的处理函数。

initServerConfig()功能详细介绍:

  • 初始化服务器的状态

  • 初始化LRU时间

  • 设置保存条件

  • 初始化和复制相关的状态

  • 初始化PSYNC命令使用的backlog(回溯)

  • 设置客户端的输出缓冲区限制

  • 初始化浮点常量

  • 初始化命令表

  • 初始化慢查询日志

  • 初始化调试项

  • 初始化内存Swap相关设置

这个函数初始化了一个全局变量 struct redisServer server

struct redisServer {
    /* General */
    redisDb *db;
    dict *commands;             /* Command table hahs table */
    aeEventLoop *el;
    unsigned lruclock:22;       /* Clock incrementing every minute, for LRU */
    unsigned lruclock_padding:10;
    int shutdown_asap;          /* SHUTDOWN needed ASAP */
    int activerehashing;        /* Incremental rehash in serverCron() */
    char *requirepass;          /* Pass for AUTH command, or NULL */
    char *pidfile;              /* PID file path */
    int arch_bits;              /* 32 or 64 depending on sizeof(long) */
    int cronloops;              /* Number of times the cron function run */
    char runid[REDIS_RUN_ID_SIZE+1];  /* ID always different at every exec. */
    int sentinel_mode;          /* True if this instance is a Sentinel. */
    /* Networking */
    int port;                   /* TCP listening port */
    char *bindaddr;             /* Bind address or NULL */
    char *unixsocket;           /* UNIX socket path */
    mode_t unixsocketperm;      /* UNIX socket permission */
    int ipfd;                   /* TCP socket file descriptor */
    int sofd;                   /* Unix socket file descriptor */
    int cfd;                    /* 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 */
    char neterr[ANET_ERR_LEN];  /* Error buffer for anet.c */
    ……
    int bug_report_start; /* True if bug report header was already logged. */
    int watchdog_period;  /* Software watchdog period in ms. 0 = off */
};

在这个函数中,对server变量进行了部分成员的初始化,其中:

  • runid:运行该redis服务器端程序的唯一标识,即每次启动都会一个唯一ID,用来区分不同的redis服务器端程序

  • maxidletime:最大空闲时间,就是client连接到server时,如果超出这个值,就会被自动断开,当然,master和slave节点不包括;如果client有阻塞命令在运行,也不会断开

saveparams:这个存储的是redis服务器端程序从配置文件中读取的持久化参数,如配置文件所述

save 900 1
save 300 10
save 60 10000

lruclock:是redis实现LRU算法所需的,每个redis object都带有一个lruclock,用来从内存中移除空闲的对象

6、sentinel_mode

是否开启redis的哨兵模式,也就是是否监测,通知,自动错误恢复,是用来管理多个redis实例的方式。

if (server.sentinel_mode) {
    initSentinelConfig();
    initSentinel();
}

7、读取参数选项

if (argc >= 2) {
    int j = 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);

        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++;
    }
    if (server.sentinel_mode && configfile && *configfile == '-') {
        redisLog(REDIS_WARNING,
            "Sentinel config from STDIN not allowed.");
        redisLog(REDIS_WARNING,
            "Sentinel needs config file on disk to save state.  Exiting...");
        exit(1);
    }
    if (configfile) server.configfile = getAbsolutePath(configfile);
    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 ? "se
ntinel" : "redis");
}

8、服务器初始化

if (server.daemonize) daemonize();//daemonize()用于在shell启动时后台运行
initServer();
if (server.daemonize) createPidFile();
redisAsciiArt();

initServer主要做以下工作:

  • 设置信号处理程序,如sighup, sigpipe等

  • 打开系统日志文件

  • 继续初始化server结构,如server.clients, server.slaves, server.monitors等

  • 创建共享对象,这里的共享对象是一个struct sharedObjectsStruct shared;它用于全局文本信息的保存,避免每次发送固定格式的信息给clients都需要创建一个新的字符串。如:

shared.crlf = createObject(REDIS_STRING,sdsnew("\r\n"));
shared.ok = createObject(REDIS_STRING,sdsnew("+OK\r\n"));

还包括了从1到10000的redis 整数对象的创建,在这部分范围的整数经常被用到,所以先创建了,可以反复重用。

  • 调整系统对文件操作参数的约束大小,如最大打开的文件数。

  • 创建ae事件循环

  • 初始化server.db

  • 开启监听redis端口或者unix socket

  • 创建Pub/Sub通道
  • 初始化server结构的统计变量,如执行的命令数,连接数,过期键等等,还有跟踪每秒操作的时间和命令数

  • 创建ae时间事件,也是redis的核心循环,该过程是serverCron,每秒调用次数由一个叫REDIS_HZ的宏决定,默认是每10微秒超时,即每10微秒该ae时间事件处理过程serverCron会被过期调用

  • 创建ae文件事件,对redis的TCP或者unix socket端口进行监听,使用相应的处理函数注册。每次得到clients连接后,都会创建ae文件事件,异步接收命令

  • 针对配置文件,设置是否开启aof和最大使用内存

  • 如果有集群设置,初始化集群。初始化lua脚本处理,初始化slowlog和bio(background io)。bio是异步io操作,用于redis读取或存取时的io操作

  • 如果开启了VM,则初始化虚拟内存相关的IO/Thread

9、serverCron核心循环

#define run_with_period(_ms_) if (!(server.cronloops%((_ms_)/(1000/REDIS_HZ))))

这个宏类似于条件判断,每ms时间执行一次后续的操作。如:

run_with_period(100) trackOperationsPerSecond();

每百微秒,执行一次跟踪操作函数,记录这段时间的命令执行情况。这个循环有以下任务需要执行:

  • 如果设置了watchdog_period,那么每过watchdog_period,都会发送sigalrm信号,该信号又会得到处理,来记录此时执行的命令。这个过程主要是为了了解一些过长命令的执行影响服务器的整体运行,是一个debug过程。

  • 每百微秒记录过去每秒的命令执行情况。

  • 更新统计变量,如内存使用总数,更新server.lruclock

  • 是否得到关闭程序的信号,如果是,就进入关闭程序的节奏,如aof,rdb文件的处理,文件描述符的关闭等

  • 每5秒输出一次redis数据库的使用情况,连接数,总键值数

  • 每次都尝试resize每个db,resize是让每个db的dict结构进入rehash状态,rehash是为了扩容dict或者缩小dict。然后每次都尝试执行每个db的rehash过程一微秒。

  • 每次调用clientCron例程,这是一个对server.clients列表进行处理的过程。再每次执行clientCron时,会对server.clients进行迭代,并且保证 1/(REDIS_HZ*10) of clients per call。也就是每次执行clientCron,如果clients过多,clientCron不会遍历所有clients,而是遍历一部分clients,但是保证每个clients都会在一定时间内得到处理。处理过程主要是检测client连接是否idle超时,或者block超时,然后会调解每个client的缓冲区大小。

  • 对aof,rdb等过程进行开启或终结。

  • 如果是master节点的话,就开始对过期的键值进行处理,与处理clients类似,不是多所有有时间限制的键值进行迭代,而是在一个限定的数量内迭代一部分,保证一定时间内能检测所有键值。

  • 对异步io过程中可能需要关闭的clients进行处理。

  • 每秒调用复制例程和集群例程,每0.1秒调用哨兵例程。

10、aeMain

aeSetBeforeSleepProc(server.el,beforeSleep);
aeMain(server.el);
aeDeleteEventLoop(server.el);

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

11、总结

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

作者在代码中处处对运行例程进行约束,保证不过长的陷入某一个不友好的命令中,如检查过期键值和处理过多的clients。

  

 参考文章

http://olylakers.iteye.com/blog/1228198

http://blog.csdn.net/zwan0518/article/details/50281175

http://www.wzxue.com/%E8%A7%A3%E8%AF%BBredis%E8%BF%90%E8%A1%8C%E6%A0%B8%E5%BF%83%E5%BE%AA%E7%8E%AF%E8%BF%87%E7%A8%8B/

http://studentdeng.github.io/blog/2013/08/19/redis-start-up/

http://olylakers.iteye.com/blog/1277256

posted @ 2016-03-02 18:01  踏雪无痕SS  阅读(2818)  评论(0编辑  收藏  举报