redis-server 启动原理简析

Redis数据库存储引擎之下的东西。

Redis如何工作的呢?我对于redis的内部构造十分的感兴趣,因此我开始让自己熟悉源代码,通过大量的阅读和在Emacs编辑器里面的跳来跳去。当我将像剥洋葱片的外壳一样一层层将redis神秘的外衣剥落的时候,我意识到我正在努力尝试记住太多太多的细节,并且它并不是很清晰它是如何成为一个整体工作的。我因此决定写下我对于redis的理解,讲述它是如何启动redis server的和对它自己进行初始化工作,同时它是如何操作客户端发出的请求应答的,这个是对于我自己的理解而言的,它是一个很有时代前沿感的技术。幸运的是,redis有一个很漂亮干净的代码基础,它很容易阅读和跟踪。通过设定标签,以及使用GDB等等,我开始打算看看它在表层之下是如何工作的。很偶然的事情是,我是在研究redis 源代码基于antirez (redis的作者)发布的代码。当然,内部构造就像我下面描述的轮廓或许会有一点点的变化。然而,主要的redis服务器的构架不会有太大的变化,我现在试着努力记住一直这一点。

         这篇文章开始的时候探究redisserver如何启动,并且在一个很高的层次来看待请求应答循环。在一个子文章中,我将更多详细的描述并且跟踪SET/GET 命令对在redis中工作的情形。

目录

Redis 笔记... 1

目录... 1

1.      redis启动... 1

1.1        开始进行全局服务端状态初始化... 2

1.2        设立命令表... 3

1.3        导入配置文件... 4

1.4        initServer() 4

1.5        back to main() 7

2.      Processing to a request and return a respons. 8

2.1        Handling a new connection. 9

2.2        Reading  acommand from client. 10

2.3        Execute a command. 11

3.      Redis summary. 12

 

1.    redis启动

让我们开始阅读在redis.c中的main()函数。



1.1     开始进行全局服务端状态初始化

首先调用,initServerConfig()。这个函数将结构体变量server的部分变量进行初始化,server的类型是struct redisServer,这个作为全局的服务端状态。        

// redis.h:338

struct redisServer {

    pthread_t mainthread;

    int port;

    int fd;

    redisDb *db;

    // ...

};

// redis.c:69

struct redisServer server; /* server global state */

 

 

                  在这个结构体中有大量的成员变量,但是他们一般在下面这些类别里面:

·        general server state

·        statistics

·        configuration from config file

·        replication

·        sort parameters

·        virtual memory config, state, I/Othreads, & stats

·        zip structure

·        event loop helpers

·        pub/sub

例如,在这个结构体中包含的成员变量,一般都是会映射到配置文件里面(一般是叫做redis.conf),例如,服务器监听的端口和冗长的日志应该怎么形成,连接进来的客户端的链表的指针,以及redis的从服务器,(有时候使用主从结构),以及统计从开始到截止时候执行的所有命令的条数等等。

initServerConfig()提供了很多在通过配置文件redis.conf配置变量的默认参数,有的时候,配置文件并没有配置说明某个变量的值,这个时候initServerConfig()会给出默认参数。

1.2     设立命令表

下一件main()函数做的事情是,将redis的命令分类,这些命令是通过一个全局的变量readonlyCommandTable 这个变量是通过struct redisCommand.

// redis.c:70

struct redisCommand *commandTable;

struct redisCommand readonlyCommandTable[] = {

    {"get",getCommand,2,REDIS_CMD_INLINE,NULL,1,1,1},

    {"set",setCommand,3,REDIS_CMD_BULK|REDIS_CMD_DENYOOM,NULL,0,0,0},

    {"setnx",setnxCommand,3,REDIS_CMD_BULK|REDIS_CMD_DENYOOM,NULL,0,0,0},

    {"setex",setexCommand,4,REDIS_CMD_BULK|REDIS_CMD_DENYOOM,NULL,0,0,0},

    {"append",appendCommand,3,REDIS_CMD_BULK|REDIS_CMD_DENYOOM,NULL,1,1,1},

    // ...

};

// redis.h:458

typedef void redisCommandProc(redisClient *c);

// ...

struct redisCommand {

    char *name;

    redisCommandProc *proc;

    int arity;

    int flags;

    // ...

};

这个只读的命令表是通过分类排序的,例如string 命令,list命令,set命令,等等,这些都是为了使这个可以给程序员更容易的浏览类似的命令。这个已经排好序的表的命令由变量名commandTable来声明的,并且使用标准的二分查找来查找命令,用命令lookupCommand(), 这个将会返回一个redisCommand的指针。(初始化变量在initServerConfig())中进行的,因为命令表被作者认为是初始化配置的一部分。) 一个redisCommand 结构体记录了命令的名字-有助于记忆的,如get 是一个指向执行实际命令的C 语言函数指针,同时这个命令的参数,例如命令的标志是否返回一个大块的响应和一个VM的特殊成员。

1.3     导入配置文件

Main() 函数开始处理由用户输入的命令行参数,这样开始启动redis-server的执行。现在,有一个仅仅只有一个参数的情况下,redis server通常version –v 并且help –h,或者路径中说明配置文件的路径。如果,路径已经给出来,redis加载配置文件并且override 那些在initServerConfig中设置的默认参数,通过调用loadServerConfig()。这个函数是相当的简单的,循环读取配置文件中的每一行,并且将那些可以匹配到在server结构体中成员变量的变量名值转化出来。在这点上,redis将会使这个成为守护进程并且将这个从终端分离如果用户已经设置了这样做的话。

 

1.4     initServer()

initServer() 完成初始化server 结构体的工作,这个最开始是从initServerConfig()开始的。首先,它建立起信号处理,(SIGHUP 和SIGPIPE被忽略的,当接受到SIGHUP信号的时候,可以重新加载配置文件,就像其他的守护进程一样。包括打印堆栈信息,如果server收到一个SIGSEGV,更多详情可以看 segvHandler().

很多的双向链表(参见 adlist.h)在这里被建立起来用来跟踪客户端和从服务器和监视器(一个客户端有MONITOR 命令)还有一个object 的空链表。

1.4.1   共享object

一个有趣的事情是,redis创建了大量的共享的object,这些都是可以通过全局变量shared 结构体来访问的。普通的redis object需要很多的不同的命令,对于响应字符串和错误信息的响应,例如,可以被共享而不需要每一次都申请他们,这样可以节省内存,这个是通过启动的时候做更多的事情来做的一个折中。在initServer函数中调用createSharedObjects();来创建共享的object。

// redis.c:662

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"));

// ...

 

 

 

 

1.4.1.1共享的整型

通过共享内存节省的内存的最大来源是建立的一个巨大的共享整型池。

// redis.c:705

for (j = 0; j < REDIS_SHARED_INTEGERS; j++) {

    shared.integers[j] = createObject(REDIS_STRING,(void*)(long)j);

    shared.integers[j]->encoding = REDIS_ENCODING_INT;

}

 

 

createSharedObjects();首先建立10,000个非负整数的数组来作为redis 的object,(string 有一个整型的编码).多种的redis object像 string ,set , list,常常包含了许多小的整数,例如他们的ID,或者计数值。并且他们可以在已经申请到的内存空间来复用这些相同的object,这样可以节省很多内存。你可以想象一下,常数个共享整型被建立之后,REDIS_SHARED_INTEGERS,这个值是可以被用户在配置文件中配置的,这个需要基于他们的应用程序的大小和需要,为了增加共享整数的节省的内存,这个是需要用户来设定的。开始的时候采用的是折中的方式,也就是在启动的时候稍微多申请一些内存,然而这个大小相对于整个数据库的大小和潜在节省空间来说是很小的一部分。

 

1.4.2   Event Loop

InitServer()继续执行,通过调用aeCreateEventLoop() (see ae.c),创建出了核心的Event Loop,并且赋值给server.el。

ae.h 提供了一个独立的封装平台,用来建立监听I/O 事件监听循环,他们都是使用Linux 中的epoll, BSD上的kqueue,然后回调之后选择它期望的选择。Redis 的事件循环轮询给新的连接,和I/O事件(读请求和写请求到一个socket)当一个事件到达时候被触发。这个就是为什么吗redis 响应快的原因了。它可以为上千个客户端同时服务,而不会产生阻塞,这个时候会为每一个请求做响应。

1.4.3   Database

initServer() 也对大量的redisDB object进行初始化,这些都是结构体封装好了一个特殊的redis database ,包括跟踪的expiring keys,这是会阻塞的,这些键将会被检查设置。默认情况下,有16个不同的数据库,这个可以被用来作为redis server 的命名空间。

1.4.4   TCP Socket

InitServer() 建立了redis监听的端口的连接,(默认情况下是6379),另外一个redis-local的封装,anet.h,设置了anetTcpServer(),并且很多其他的函数,那些用来简化通常情况下面很复杂的建立一个socket,监听一个端口的函数。

// redis.c:791

server.fd = anetTcpServer(server.neterr, server.port, server.bindaddr);

if (server.fd == -1) {

    redisLog(REDIS_WARNING, "Opening TCP port: %s", server.neterr);

    exit(1);

}

 

 

1.4.5   Server cron

initServer() 进一步的情况下面,申请很多dict和list给database,并且给pub/sub,resets 静态参数和可变的标志,并且记下unix的时间标志。它注册了serverCron()Event Loop来作为time event,每隔100ms执行一次。(这个有点狡猾,因为初始化的时候,serverCron(),被设置为使每隔1毫秒执行一次,为了让cron这个循环能够在启动的时候正确的开始,同时能够返回serverCron(),于是设置为100,这个是下次时间时间被执行的时候将会被处理。

    ServerCron(),执行了大量redis所需要的周期性的工作,包括大量的日志(关于keys和memory的使用问题),连接的客户端,重新设置hash table的表,关闭那些闲置的超时的客户端的连接,执行任何被扔到后台运行的保存和AOF重写、开始后台保存如果配置文件配置了的情形,计算出LRU的信息,用来处理替换,redis 仅仅替换出去一小部分超时的key在每一个cron周期,使用一个可适性好的和统计的方法来避免捆绑服务端。但是,这个也将变得更加好,如果设置到时间的键可以帮助避免内存不足的情形。如果虚拟磁盘是被允许的,那么swap out这些值,并且同步到主机中如果他是一个从服务器。

1.4.6   Register connectionhandler with event loop

         关键性的,initServer()首先用server 的TCP socket 通过socket的描述符来event loop,当一个新的连接被接受的时候,注册acceptHandler()被调用。     

// redis.c:821

if (aeCreateFileEvent(server.el, server.fd, AE_READABLE,

    acceptHandler, NULL) == AE_ERR) oom("creating file event");

 

1.4.7   Opening the AOF

initServer() 创建或者打开append-ony-file(AOF),如果服务器配置文件中告诉了使用这个的话。

// redis.c:824

if (server.appendonly) {

    server.appendfd = open(server.appendfilename,O_WRONLY|O_APPEND|O_CREAT,0644);

 

 

1.5     back to main()

如果server 被配置为成为守护进程,redis现在将会试着写一个pid文件,这个路径是根据配置文件的,默认情况下面是/var/run/redis.pid。在这一点上,server已经启动了,redis将会把这些写入到日志文件中。然而,要main()全部执行准备好还有一些事情要做。

1.5.1   Restoring Data

如果有一个AOF文件或者数据库的dump文件,(eg,dump.rdb),它将被载入,重建数据到服务端从一个以前的会话状态(如果两个都存在AOF的优先级更高)。

// redis.c:1452

if (server.appendonly) {

    if (loadAppendOnlyFile(server.appendfilename) == REDIS_OK)

        redisLog(REDIS_NOTICE,"DB loaded from append only file: %ld seconds",time(NULL)-start);

} else {

    if (rdbLoad(server.dbfilename) == REDIS_OK)

        redisLog(REDIS_NOTICE,"DB loaded from disk: %ld seconds",time(NULL)-start);

}

 

                            现在服务端已经准备好了接受用户的请求了。

1.5.2   Event Loop Setup

最后,redis 注册一个函数用来每次调用进入event loop,beforeSleep()(当进程实际上进入睡眠状态,处于等待接收通知的时候。) beforeSleep()做了两件事,他处理那些服务着客户端那些有请求键被调入了磁盘如果虚拟存储是被允许的情况和它在将AOF写入到磁盘中的时候。写入到AOF文件是通过flushAppendOnlyFile()来处理的。这个函数封装了一些狡猾的逻辑用来将buffer中的数据写入,并且有一些控制写入AOF的参数(写的频率是由用户来决定的)

1.5.3   Enter Event Loop

Redis现在进入到主的Event Loop通过调用aeMain(),带着参数server.el,记住这个参数包含了一个指向aeEventLoop的指针。如果在任何时候,或者有文件事件通过循环每一次处理的时候,他们的Handler函数将会被调用。aeProcessEvent()封装了这个逻辑-时间事件是像传统的逻辑事件一样被处理的,与之相对应的是用比较重要的epoll或者kpueue或者选择I/O时间监听系统。因为redis需要对时间事件或者文件或者I/O事件进行响应,它应用了一个传统的事件轮询的循环aeMain().通过检查看是否有时间事件要被处理,或者实施文件事件监听,这个事件循环可以很有效的进入睡眠直到有了工作可以来做,并不是一直占用CPU的一个很死的while 循环。

 

2.    Processing to a request and return a respons

我们现在在redis的主的Event 轮询循环里面,监听了一个端口,并且等待一个客户端连接进来。现在是时候开始关注redis是如何处理一个命令请求的了。


2.1     Handling a new connection

在initServer()之后,redis注册acceptHandler()这个函数,这个函数在当有I/O事件关联server正在监听的端口的socket的文件描述符(在各个socket等待去读或者写)acceptHandler()创建一个客户端对象,指针指向redisClient,一个结构体在redis.h定义的,用来代表一个新的连接。

// networking.c:347

cfd = anetAccept(server.neterr, fd, cip, &cport);

if (cfd == AE_ERR) {

    redisLog(REDIS_VERBOSE,"Accepting client connection: %s", server.neterr);

    return;

}

redisLog(REDIS_VERBOSE,"Accepted %s:%d", cip, cport);

if ((c = createClient(cfd)) == NULL) {

    redisLog(REDIS_WARNING,"Error allocating resoures for the client");

    close(cfd); /* May be already closed, just ingore errors */

    return;

}

 

 

createClient()调用是用来产生和初始化一个client object的。他选择database 0 默认的情况下(由于在每一个的服务端至少有一个redis db),并且与在acceptHandler()中接受的客户端文件的描述符相关联。其他的标志和成员已经初始化好了,并且最终,客户端被加到全局的客户端的链表在server.clients这个末尾。Redis在createClient()做的比较关键的事情注册一个Handler来关联Event Loop,这个函数是readQueryFromClient(),当连接的客户端有数据读入的时候就会调用了。

// networking.c:20

if (aeCreateFileEvent(server.el,fd,AE_READABLE, readQueryFromClient, c) == AE_ERR)

{

    close(fd);

    zfree(c);

    return NULL;

}

2.2     Reading  a command from client

readQueryFromClient() 在主的Event Loop循环中被调用,当客户端发出一个命令请求的时候。(如果你正在用GDB debug,这个是一个很好的用来设置断点的函数)。它将命令的内容尽可能多的读入进来,最多1024个字节,到一个临时的buffer,然后将这个加入到一个特殊的查询buffer。这个允许redis处理命令,当有效负载(命令的名字加上参数)超过了1024个字节的时候,或者I/O的原因已经将这个分解为几个读取的事件。它必须调用processInputBuffer(),来传递客户端对象来作为一个参数。

// networking.c:754

void readQueryFromClient(aeEventLoop *el, int fd, void *privdata, int mask) {

    redisClient *c = (redisClient*) privdata;

    char buf[REDIS_IOBUF_LEN];

    int nread;

    // ...

 

    nread = read(fd, buf, REDIS_IOBUF_LEN);

    // ...

    if (nread) {

        size_t oldlen = sdslen(c->querybuf);

        c->querybuf = sdscatlen(c->querybuf, buf, nread);

        c->lastinteraction = time(NULL);

        /* Scan this new piece of the query for the newline. We do this

         * here in order to make sure we perform this scan just one time

         * per piece of buffer, leading to an O(N) scan instead of O(N*N) */

        if (c->bulklen == -1 && c->newline == NULL)

            c->newline = strchr(c->querybuf+oldlen,'\n');

    } else {

        return;

    }

    Processinputbuffer(c);

}

 

processInputBuffer()将最开始的从客户端发出的查询解析成一个redis命令执行的参数。他开始必须将客户端在B{L,R} POP 命令的地方阻塞,然后解释完成如果符合情况。这个函数解析了原始的查询到了参数了之后,创建redis的string 对象给每一个并且将他们储存在要给客户端对象的数组里面。这个查询是遵循redis 协议的格式的。processInputBuffer()是真的一个协议解析器,这个查询的全部的查询都是通过回调函数processCommand(). 有点迷惑的事情是,这个源代码的注释描述这个解析过程是多部分的命令类型,一个可选择的协议原始的命令像MSET,但是它实际上是在主的redis 对于所有命令的协议。它是二进制安全的和易于解析和debug的。现在,是该真正执行这些客户端发送过来的命令了,通过调用客户端对象的processCommand().

processCommand()得到从客户端的命令的参数,并执行这个。在它去实际执行这个命令之前,如果有任何的检查失败了,它将错误信息日志加入到客户端对象的回复链表并且返回到调用者,processInputBuffer()。当处理了QUIT命令之后,作为一个特殊的命令,(用来安全的关闭客户端),processCommand()检查命令的名字在commandTable()在之前redis启动的周期中建立的。如果是一个未知的命令,或者客户端得到了命令的参数错误,它就是一个错误。当它不是被普遍使用的时候,redis 可以被注册为需要密码来认证一个客户端在它接收命令之前,并且这个阶段是在redis检查是否已经认证,否则将会被设置为一个错误。如果redis 配置了使用最大的内存,它将在这个点上试着释放内存,如果可能的话,(在释放对象之前从空的链表或者从过期的键移除),否则如果server超过了限制,它将不能处理命令有redis_cmd_denyoom标识设置,主要写的像SET、INCR、RPUSH、ZADD,等等)再次,一个错误。一个最终的redis做的检查是客户端可以只可以主题subsribe 和unsubscribe命令当有一个特殊的相关的,否则她是一个错误。当所有的检查通过之后,这个命令将会通过call()来执行,带着客户端对象的和命令对象来做为参数。

2.3     Execute a command

Call(),获得一个指向结构体redisCommandProc的指针,从command对象的proc成员,这个是只有单个参数,就是这个客户端对象。这个redis命令程序被调用。

// redis.c:864

void call(redisClient *c, struct redisCommand *cmd) {

    long long dirty;

 

    dirty = server.dirty;

    cmd->proc(c);

    dirty = server.dirty-dirty;

}

// ...

 

 

写命令,像SET和SADD,使server的数据变得dirty,换句话说,就是server有很多内存中的页得数据已经被改变了。这个对于自动保存进程来说很关键,这个是用来保持跟踪有多少键已经在某个时期被改变了,或者写入到AOF文件。这个函数是feedAppendOnlyFile(),如果使用了AOF已经被允许使用,这个将command buffer 从客户端写入到AOF文件中,因此这个命令可以被回复。它将命令翻译并且设置一个关联的生命期键给一个绝对值得寿命,但是另一面,它将在客户端进入的时候刚刚开始时复制命令,可以参见catAppendOnlyGenericCommand()。如果任何的从服务器被连接了,call(),将会把这些命令转发给他们各自,让命令在他们各自的本地执行,看replicationFeedSlaves()。否则,如果任何的客户端连接有关键词MONITOR的话,redis将会发送一个代表命令,在时间邮票上是有前缀的,看replicationFeedMonitors();

// redis.c:871 (call() cont.'d)

    // ...

    if (server.appendonly && dirty)

        feedAppendOnlyFile(cmd,c->db->id,c->argv,c->argc);

    if ((dirty || cmd->flags & REDIS_CMD_FORCE_REPLICATION) &&

        listLength(server.slaves))

        replicationFeedSlaves(server.slaves,c->db->id,c->argv,c->argc);

    if (listLength(server.monitors))

        replicationFeedMonitors(server.monitors,c->db->id,c->argv,c->argc);

    server.stat_numcommands++;

}

 

         控制从调用者返回,processCommand(),这个将重置客户端对象对于后来的命令来说。就像之前提到的,每一个redis命令程序都是他们自己有责任将相应发送给客户端。在readQueryFromClient()推出之后,并且redis返回到Event Loop在aeMain(),aeProcessEvents()将会得到等待相应在写的缓冲并且将会将它复制给它的的客户端连接的socket。所有的都是这些,这个相应已经被发出。并且server 和client都回到一个状态他们可以分别发出和处理更多的redis 命令。

 

 

 

 

3.    Redis summary

         Redis通过初始化一个全局的serverstate 变量,读取配置文件中的选项来覆盖默认参数,来启动程序。他设立了一个全局的命令表,那些连接的命令连接着实际的函数用来实现命令的。它创建了一个Event Loop,使用当前可以使用的库和event /readiness监听,并且注册要给Handler函数当有一个新的客户端连接进来被接受的时候。它同时也注册一个周期的控制函数,像cron任务等等。一旦客户端已经连接之后,一个函数注册在了Event Loop,用来监听客户端的数据是否有读入操作。客户端的查询解析之后,一个命令控制Handler被用来调用去执行这个命令和写回应到客户端。写数据回客户端也是被event 监听循环控制的,这个客户端对象将会被重置,以便服务端准备处理下一个命令。



参考地址 :http://pauladamsmith.com/articles/redis-under-the-hood.html


PS:第一次翻译外文的东西,发现确实是很多英文的东西用中文表述起来特别特别的蹩脚,以前总是喜欢骂一些书中文翻译的不好,现在也不敢去骂,自己没有那么水平,所以没有资格对别人的翻译指手画脚太多,带着更理解的心态去看这些了。



你可能感兴趣的:(redis)