注册 登录  
 加关注
   显示下一条  |  关闭
温馨提示!由于新浪微博认证机制调整,您的新浪微博帐号绑定已过期,请重新绑定!立即重新绑定新浪微博》  |  关闭

Koala++'s blog

计算广告学 RTB

 
 
 

日志

 
 

Redis本质论(Redis: under the hood 译文)[1]  

2011-07-30 16:05:31|  分类: Linux |  标签: |举报 |字号 订阅

  下载LOFTER 我的照片书  |

                                                       Redis本质论

Author: Paul Smith

译者: Koala++ / 屈伟

 

我曾对Redis的内部实现比较好奇,所以我就在Emacs跳来跳去地读它的源码,这样我总算是对它的代码熟悉了。当我一层层地将它源码撕开后,我意识到我过多地关注了许多细节,而我却不清楚这些细节是怎样合到一起工作的。所以我决定以叙述的方式来描述Redis实例如何启动和如何初始化,和它如何处理一个用户的请求/响应周期,我以这种方式来向我自己解释,希望这是一个清晰的方式。幸运的是Redis有着优雅,清晰的代码,所以阅读它的代码很容易。

         这篇文章以一个比较高的层次来看request/response处理周期。在将下来的一篇文章,我会深入到更多的细节,并跟踪一个简单的SET/GET命令在Redis中的处理。

Startup

         让我们从redis.c文件的main()函数开始:

Beginning global server state initialization

         首先,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 */

         在这个结构中有大量的成员,但是它们基本都能被划分到下面几个类别中:

1.       系统状态

2.       统计

3.       从配置文件中得到的配置。

4.       复制

5.       排序参数

6.       虚拟内存配置,状态,I/O 线程,状态

7.       Zip结构

8.       用于事件循环

9.       发布/订阅

比如,这个结构包含一些成员对应于配置文件(通常名为redis.conf)的选项,举例来说,配置中会有服务器监听的端口和日志的级别。也有指向已连接客户端链表和Redis从服务器链表,以及Redis数据库(s)链表的指针,也有自服务器启动后有多少命令被执行过的统计数据。

         initServerConfig()提供了可由用户可在redis.conf配置文件中配置的成员默认值。

Setting up command table

         main()函数对Redis中的命令表进行排序,这些命令在一个全局变量readonlyCommandTable中定义,它是一个struct redisCommands数组。

// 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;

    // ...

};

         这个只读的表在源码中以类型分组了,比如有字符串命令,列表命令,集合命令,等等,这样可以让程序员快速找到功能相近的命令。全局变量commandTable指向排序后的命令表,并可以用二分查找来查找Redis命令(lookupCommand),它返回一个指向redisCommand的指针)

Loading config file

         main()函数接下来处理由用户启动redis服务器时命令行中给出的参数。现在只有一个参数可用,就是指定配置文件的路径,当然不包括版本命令-v和帮助命令-h。如果指定了路径,Reids会读取配置文件,并用loadServerConfig函数覆盖所有由initServerConfig函数设置的默认值。这个函数比较单纯,循环读取配置文件中的每行,将配置的内容转换成相应的类型保存到server结构中。读取完后,Redis会成为守护进程。

initServer()

         initServer()会完成初始化server的任务。首先它设置信号处理函数(SIGHUPSIGPIPE被忽略了——这也是一个改进Redis的机会,可以增加一个收到SIGHUP信号重新读取配置文件的功能),包括当服务器收到一个SIGSEGV信号(或其它相关的信号)后打印Stacktrace,参见segvHandler()

         大量的双向列表(参见adlist.h)用来保存clientsslavesmonitors(被发送了MONITOR命令的client),和一个free对象列表。

Shared objects

         Redis中一个比较有趣的地方是它创建了大量的共享对象,它们可以通过全局的shared结构访问。通常许多Redis对象在多个不同中的命令中都是需要的,比如响应字符串,出错字符串,可以被共享而不用每次都分配它们,从而达到节约内存的目的,代价是启动的时候要花费更多的时间初始化。

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

// ...

Shared integers

         对于用共享对象节约内存影响最大的是一个大的共享整型池。

// 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对象,许多Redis对象比如stringsetlist中通常包含许多小的整型数(比如ID或是计数),它们可以重用已经分配内存的相同对象。你可以试想一下REDIS_SHARED_INTEGERS这个定义多少个共享整型会被创建的值,可以通过配置开放给用户,用户可以基于他们的应用和需求,去增加共享整型的数量从而节约更多的内存。代价是Redis需要在启动的时候静态分配更多的空间,但是分配的这点空间相比典型的数据库来说,微不足道。

Event loop

         initServer()接下来会创建一个核心的事件循环,它由aeCreateEventLoop()(参见ae.c)创建,并将结果赋给server的成员el

         ae.h提供了一个对设置I/O事件通知循环平台无关的封装,它在Linux上用epoll,在BSD上用kqueue,在没有上两种选择时只好用selectRedis的事件循环(Event Loop)等待新的连接和I/O事件(读取请求和写响应到一个socket),当有新的事件到来时就会被触发。

Databases

         initServer()也初始化一些redisDb对象,redisDb对象中封装了一个特定的Redis数据库,包括跟踪过期的keys,正在被堵塞的keys(或是从B{L,R}POOP或是从I/O),这些keys被用来查看和设置(默认有16个独立的数据库,它们可以被视为Redis服务器中不同的命名空间)

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

}

Server cron

         initServer()进一步分配许多dicts和数据库列表和发布订阅的列表,重置统计值和许多标志,并设置服务器开始的时间戳。它用事件循环将serverCron注册为一个时间事件,每100ms执行一次函数。(这里有一点难懂,因为初始时,在initServerserverCron()被设置为每1ms执行一次,为的是让cron循环在服务器启动时马上开始运行,但是接下来serverCron的返回值是100,它被插入去计算下一次时间事件应该被处理的时间)

         serverCron()Redis执行许多周期性的任务,包括详细的数据库日志(keys和内存的使用量)和连接clients量,重新设置hash槽的大小,关闭空闲和过期的client连接,进行后台的保存或是AOF重写清除,如果配置中的保存条件满足了(多少keys在多少秒内改变了)就开始后台的保存,计算LRU住处和处理过期的keys(Redis仅在每个cron周期过期一些过时的keysRedis用一个可适应的统计方法过期keys,以防止完全占用服务器,但如果过期keys可以帮助防止出现out-of-memory的情况,它就会更激进),如果开启了虚拟内存,就将值写入磁盘,如果server是一个从服务器,就要进行主从同步。

Registering connection handler with event loop

         关键点,在initServer()中在事件循环中注册了服务器的TCP socket的描述符,当一个新的连接到来时,注册的acceptHandler()函数就会被调用。

// redis.c:821

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

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

Opening the AOF

         如何有相应的配置,initServer()会创建或打只可追加的文件(Append-only file AOF)

// redis.c:824

if (server.appendonly) {

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

         最后,如果进行了相应的配置,initServer()会初始化Redis的虚拟内存系统。

  评论这张
 
阅读(2471)| 评论(0)
推荐 转载

历史上的今天

评论

<#--最新日志,群博日志--> <#--推荐日志--> <#--引用记录--> <#--博主推荐--> <#--随机阅读--> <#--首页推荐--> <#--历史上的今天--> <#--被推荐日志--> <#--上一篇,下一篇--> <#-- 热度 --> <#-- 网易新闻广告 --> <#--右边模块结构--> <#--评论模块结构--> <#--引用模块结构--> <#--博主发起的投票-->
 
 
 
 
 
 
 
 
 
 
 
 
 
 

页脚

网易公司版权所有 ©1997-2017