走近源码:Redis 的启动过程

栏目: 数据库 · 发布时间: 7年前

当我们对不断加深对某一项技术的了解时,一定会在一个特定的时间对它的实现方式产生兴趣。没错,这就是我现在的状态,所以,多年没有读/写 C语言 的我,决定要啃一下 Redis 的源码。

Redis大体上可以分为两部分:服务器和客户端(读者吐槽:你这分的也太大体了吧)。在使用时,我们先启动服务器,然后再启动客户端。由客户端向服务器发送命令,服务器处理后将结果返回给客户端。我们从“头”开始,一起来了解一下Redis服务器在启动的时候都做了哪些事情。

对于C语言来说,main函数是一个程序的的入口,Redis也不例外。Redis的main函数写在server.c文件中。由于redis启动过程相当复杂,需要判断许多条件,例如是否在集群中,或者是否是哨兵模式等等,因此我们只介绍单机redis启动过程中一些比较重要的步骤。

初始化全局服务器状态

如果redis-server命令启动时使用了test参数,那么就会先进行指定的测试。接下来调用了initServerConfig()函数,这个函数初始化了一个类型为redisServer的全局变量server。redisServer这个结构包含了非常多的字段,由于篇幅限制,我们不在这里列出,如果按类别划分的话,可以分为以下类别:

  • General

  • Modules

  • Networking

  • RDB / AOF loading information

  • Fast pointers to often looked up command

  • Fields used only for stats

  • Configuration

  • AOF / RDB persistence

  • Logging

  • Replication

  • Synchronous replication

  • Limits

  • Blocked clients

  • Sort parameters

  • Zip structure config

  • time cache

  • Pubsub

  • Cluster

  • Scripting

  • Lazy free

  • Latency monitor

  • Assert & bug reporting

  • System hardware info

如果用一句话来概括initServerConfig()函数作用,它就是用来给可以在配置文件(通常命名为redis.conf)中配置的变量初始化一个默认值。比较常用的变量有服务器端口号、日志等级等等。

设置commend table

在initServerConfig()函数中,会调用populateCommandTable()函数来设置服务器的命令表,命令表的结构如下。

1struct redisCommand redisCommandTable[] = {
2    {"module",moduleCommand,-2,"as",0,NULL,0,0,0,0,0},
3    {"get",getCommand,2,"rF",0,NULL,1,1,1,0,0},
4    {"set",setCommand,-3,"wm",0,NULL,1,1,1,0,0},
5    {"setnx",setnxCommand,3,"wmF",0,NULL,1,1,1,0,0},
6    ...
7}

每一项代表的含义是:

  1. name:命令的名称

  2. function:命令对应的函数名。redis-server处理命令时要执行的函数

  3. arity:命令的参数个数,如果是-N代表大于等于N

  4. sflags:命令标志,标识命令的类型(read/write/admin…)

  5. flags:位掩码,由Redis根据sflags计算

  6. get_keys_proc:可选函数,当下面三个项不能指定哪些参数是key时使用

  7. first_key_index:第一个是key的参数

  8. last_key_index:最后一个是key的参数

  9. key_step:key的“步长”,比如MSET的key_step是2,因为它的参数是key,val,key,val这样的形式

  10. microseconds:执行命令所需要的微秒数

  11. calls:该命令被调用总次数

设置好命令表后,redis-server还会对一些常用的命令设置快速查找方式,直接赋予server的成员指针。

 1server.delCommand = lookupCommandByCString("del");
 2server.multiCommand = lookupCommandByCString("multi");
 3server.lpushCommand = lookupCommandByCString("lpush");
 4server.lpopCommand = lookupCommandByCString("lpop");
 5server.rpopCommand = lookupCommandByCString("rpop");
 6server.zpopminCommand = lookupCommandByCString("zpopmin");
 7server.zpopmaxCommand = lookupCommandByCString("zpopmax");
 8server.sremCommand = lookupCommandByCString("srem");
 9server.execCommand = lookupCommandByCString("exec");
10server.expireCommand = lookupCommandByCString("expire");
11server.pexpireCommand = lookupCommandByCString("pexpire");
12server.xclaimCommand = lookupCommandByCString("xclaim");
13server.xgroupCommand = lookupCommandByCString("xgroup");

初始化哨兵模式

变量初始化以后,就会将启动命令的路径和参数保存起来,以备下次重启的时候使用。如果启动的服务是哨兵模式,那么就会调用initSentinelConfig()和initSentinel()这两个方法来初始化哨兵模式。对sentinel不了解的同学可以看这里。initSentinelConfig()和initSentinel()都在sentinel.c文件中。initSentinelConfig函数负责初始化sentinel的端口号,以及解除服务器的保护模式。initSentinel函数负责将command table设置为只支持sentinel命令,以及初始化sentinelState数据格式。

修复持久化文件

启动模式如果是redis-check-rdb/aof,那么就会执行redis_check_rdb_main()或redis_check_aof_main()这两个函数来修复持久化文件,不过redis_check_rdb_main函数所做的事情在Redis启动过程中已经做了,所以这里不需要做,直接使这个函数加载错误就可以了。

处理参数

如果是简单的参数例如-v或--version、-h或--help,就会直接调用相应的方法,打印信息。如果是使用其他配置文件,则修改server.exec_argv。对于其他信息,会将他们转换成字符串,然后添加进配置文件,例如“--port 6380”就会被转换成“port 6380\n”加进配置文件。这时,redis就会调用loadServerConfig()函数来加载配置文件,这个过程会覆盖掉前面初始化默认配置文件的变量的值。

initServer()

initServer()函数负责结束server变量初始化工作。首先设置处理信号(SIGHUP和SIGPIPE除外),接着会创建一些双向列表用来跟踪客户端、从节点等。

 1server.current_client = NULL;
 2server.clients = listCreate();
 3server.clients_index = raxNew();
 4server.clients_to_close = listCreate();
 5server.slaves = listCreate();
 6server.monitors = listCreate();
 7server.clients_pending_write = listCreate();
 8server.slaveseldb = -1; /* Force to emit the first SELECT command. */
 9server.unblocked_clients = listCreate();
10server.ready_keys = listCreate();
11server.clients_waiting_acks = listCreate();

Shared object

createSharedObjects()函数会创建一些shared对象保存在全局的shared变量中,对于不同的命令,可能会有相同的返回值(比如报错)。这样在返回时就不必每次都去新增对象了,保存到内存中了。这个设计就是以Redis启动时多消耗一些时间为代价,换取运行的更小的延迟。

 1shared.crlf = createObject(OBJ_STRING,sdsnew("\r\n"));
 2shared.ok = createObject(OBJ_STRING,sdsnew("+OK\r\n"));
 3shared.err = createObject(OBJ_STRING,sdsnew("-ERR\r\n"));
 4shared.emptybulk = createObject(OBJ_STRING,sdsnew("$0\r\n\r\n"));
 5shared.czero = createObject(OBJ_STRING,sdsnew(":0\r\n"));
 6shared.cone = createObject(OBJ_STRING,sdsnew(":1\r\n"));
 7shared.cnegone = createObject(OBJ_STRING,sdsnew(":-1\r\n"));
 8shared.nullbulk = createObject(OBJ_STRING,sdsnew("$-1\r\n"));
 9shared.nullmultibulk = createObject(OBJ_STRING,sdsnew("*-1\r\n"));
10shared.emptymultibulk = createObject(OBJ_STRING,sdsnew("*0\r\n"));
11shared.pong = createObject(OBJ_STRING,sdsnew("+PONG\r\n"));
12shared.queued = createObject(OBJ_STRING,sdsnew("+QUEUED\r\n"));
13shared.emptyscan = createObject(OBJ_STRING,sdsnew("*2\r\n$1\r\n0\r\n*0\r\n"));
14shared.wrongtypeerr = createObject(OBJ_STRING,sdsnew(
15    "-WRONGTYPE Operation against a key holding the wrong kind of value\r\n"));
16shared.nokeyerr = createObject(OBJ_STRING,sdsnew(
17    "-ERR no such key\r\n"));

Shared integers

除了上述的一些返回值以外,createSharedObjects()函数还会创建一些共享的整数对象。对Redis来说,有许多类型(比如lists或者sets)都需要一些整数(比如数量),这时就可以复用这些已经创建好的整数对象,而不需要重新分配内存并创建。这同样是牺牲了启动时间来换取运行时间。

新增循环事件

initServer()函数调用aeCreateEventLoop()函数(ae.c文件)来增加循环事件,并将结果返回给server的el成员。Redis使用不同的函数来兼容各个平台,在 Linux 平台使用epoll,在BSD使用kqueue,都不是的话,最终会使用select。Redis轮询新的连接以及I/O事件,有新的事件到来时就会及时作出响应。

分配数据库

Redis初始化需要的数据库,并将结果赋给server的db成员。

1server.db = zmalloc(sizeof(redisDb)*server.dbnum);

监听TCP端口

listenToPort()用来初始化一些文件描述符,从而监听server配置的地址和端口。listenToPort函数会根据参数中的地址判断要监听的是IPv4还是IPv6,对应的调用anetTcpServer()或anetTcp6Server()函数,如果参数中未指明地址,则会强行绑定0.0.0.0

初始化LRU键池

evictionPoolAlloc()(evict.c文件中)用于初始化LRU的键池,Redis的key过期策略是近似LRU算法。

 1void evictionPoolAlloc(void) {
 2    struct evictionPoolEntry *ep;
 3    int j;
 4
 5    ep = zmalloc(sizeof(*ep)*EVPOOL_SIZE);
 6    for (j = 0; j < EVPOOL_SIZE; j++) {
 7        ep[j].idle = 0;
 8        ep[j].key = NULL;
 9        ep[j].cached = sdsnewlen(NULL,EVPOOL_CACHED_SDS_SIZE);
10        ep[j].dbid = 0;
11    }
12    EvictionPoolLRU = ep;
13}

Server cron

initServer()函数接下来会为数据库和pub/sub再生成一些列表和字典,重置一些状态,标记系统启动时间。在这之后,Redis会执行aeCreateTimeEvent()(在ae.c文件中)函数,用来新建一个循环执行serverCron()函数的事件。serverCron()默认每100毫秒执行一次。

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

可以看到,代码中创建循环事件时指定每毫秒执行一次serverCron()函数,这是为了使循环马上启动,但是serverCron()函数的返回值又会被作为下次执行的时间间隔。默认为1000/server.hz。server.hz随着客户端数量的增加而增加。

serverCron()函数做了许多定时执行的任务,包括rehash、后台持久化,AOF重新与清理、清理过期key,交换虚拟内存、同步主从节点等等。总之能想到的Redis的定时任务几乎都在serverCron()函数中处理。

打开AOF文件

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

最大内存限制

对于32位系统,最大内存是4GB,如果用户没有明确指出Redis可使用的最大内存,那么这里默认限制为3GB。

1/* 32 bit instances are limited to 4GB of address space, so if there is
2     * no explicit limit in the user provided configuration we set a limit
3     * at 3 GB using maxmemory with 'noeviction' policy'. This avoids
4     * useless crashes of the Redis instance for out of memory. */
5if (server.arch_bits == 32 && server.maxmemory == 0) {
6    serverLog(LL_WARNING,"Warning: 32 bit instance detected but no memory limit set. Setting 3 GB maxmemory limit with 'noeviction' policy now.");
7    server.maxmemory = 3072LL*(1024*1024); /* 3 GB */
8    server.maxmemory_policy = MAXMEMORY_NO_EVICTION;
9}

Redis Server启动

如果Redis被设置为后台运行,此时Redis会尝试写pid文件,默认路径是/var/run/redis.pid。这时,Redis服务器已经启动,不过还有一些事情要做。

从磁盘加载数据

如果存在AOF文件或者dump文件(都有的话AOF文件的优先级高),loadDataFromDisk()函数负责将数据从磁盘加载到内存。

最后的设置

每次进入循环事件时,要调用beforeSleep()函数,它做了以下这些事情:

  • 如果server是cluster中的一个节点,调用clusterBeforeSleep()函数

  • 执行一个快速的周期

  • 如果有客户端在前一个循环事件被阻塞了,向所有的从节点发送ACK请求

  • 取消在同步备份过程中被阻塞的客户端的阻塞状态

  • 检查是否有因为阻塞命令而被阻塞的客户端,如果有,解除

  • 把AOF缓冲区写到磁盘

  • 线程释放GIL

进入主循环事件

程序调用aeMain()函数,进入主循环,这时其他的一些循环事件也会分别被调用

1void aeMain(aeEventLoop *eventLoop) {
2    eventLoop->stop = 0;
3    while (!eventLoop->stop) {
4        if (eventLoop->beforesleep != NULL)
5            eventLoop->beforesleep(eventLoop);
6        aeProcessEvents(eventLoop, AE_ALL_EVENTS|AE_CALL_AFTER_SLEEP);
7    }
8}

到此,Redis服务器已经完全准备好处理各种事件了。后面我们会继续了解Redis命令执行过程究竟做了哪些事情。

参考 :Redis: under the hood (点击阅读原文查看文章)

走近源码:Redis 的启动过程

走近源码:Redis 的启动过程

我怎么这么好看


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。

Beginning iPhone and iPad Web Apps

Beginning iPhone and iPad Web Apps

Chris Apers、Daniel Paterson / Apress / 2010-12-15 / USD 39.99

It seems that everyone and her sister has developed an iPhone App—everyone except you, the hard-working web professional. And now with the introduction of the iPad, you may even feel farther behind. B......一起来看看 《Beginning iPhone and iPad Web Apps》 这本书的介绍吧!

RGB转16进制工具
RGB转16进制工具

RGB HEX 互转工具

正则表达式在线测试
正则表达式在线测试

正则表达式在线测试

HEX CMYK 转换工具
HEX CMYK 转换工具

HEX CMYK 互转工具