Posts Tagged ‘redis’

redis内存容量的预估和优化

redis是个内存全集的kv数据库,不存在部分数据在磁盘部分数据在内存里的情况,所以提前预估和节约内存非常重要.本文将以最常用的string和zipmap两类数据结构在jemalloc内存分配器下的内存容量预估和节约内存的方法.

redis2.4 backgroud thread

redis终于在2.4版本里引入了除主线程之外的后台线程,这个事情由来已久.早在2010年2月就有人提出aof的缺陷,提及的问题主要有: 1 主线程aof的每次fsync(everysecond模式)在高并发下时常出现100ms的延时,这源于fsync必不可少的磁盘操作,即便已经优化多次请求的离散小io转化成一次大的连续io(sina的同学也反映过这个问题). 2 主线程里backgroundRewriteDoneHandler函数在处理bgrewriteaof后台进程退出的时候存在一个rename new-aof-file old-aof-file,然后再close old-aof-file的操作, close是一个unlink的操作(最后的引用计数), unlink消耗的时间取决于文件的大小,是个容易阻塞的系统调用. 3 当发生bgsave或者bgrewriteaof的时候主线程和子进程同时写入不同的文件,这改变了原有连续写模式,不同写入点造成了磁盘磁头的寻道时间加长(其实一个台物理机多实例也有这个问题, 要避免同一时间点做bgrewriteaof), 这又加长了fsync时间. 经过漫长的设计和交流,antirez终于在2.4版里给出了实现, 这个设计保持了redis原有的keep it simple的风格,实现的特别简单且有效果,实现的主要原理就是把fsync和close操作都移动到background来执行.

redis2.4版的自动bgrewriteaof

2.4版本做了很多功能改进,尤其是aof这块变动较大.增加了自动的bgrewriteaof;开启两个后台线程来避免主线程fsync,rename,close等阻塞操作,另外修复了出现重复命令进入aof文件的bug,下面是基于2.4.1.的源码aof这块的改进分析.

redis源代码分析 – protocol

我们跟踪一个普通的get命令来遍历几个关键函数,熟悉协议处理的过程。 你可以通过telnet或者redis_cli、利用lib库发送请求给redis server。前者的是一种裸协议的请求发送到服务端,而后两者会对键入的请求进行协议组装帮助更好的解析(常见的是长度放到前头,还有添加阿协议类型)。 Requests格式 *参数的个数 CRLF $第一个参数的长度CRLF 第一个参数CRLF … $第N个参数的长度CRLF 第N个参数CRLF 例如在redis_cli里键入get a,经过协议组装后的请求为 2\r\n$3\r\nget\r\n$1\r\na\r\n 下面这个图涵盖了接收request,处理请求,调用函数,发送reply的过程。 redis的网络事件库,我们在前面的文章已经讲过,readQueryFromClient先从fd中读取数据,先存储在c->querybuf里(networking.c 823)。接下来函数processInputBuffer来解析querybuf,上面说过如果是telnet发送的裸协议数据是没有*打头的表示参数个数的辅助信息,针对telnet的数据跳到processInlineBuffer函数,而其他则通过函数processMultibulkBuffer。 这两个函数的作用一样,解析c->querybuf的字符串,分解成多参数到c->argc和c->argv里面,argc表示参数的个数,argv是个redis_object的指针数组,每个指针指向一个redis_object, object的ptr里存储具体的内容,对于”get a“的请求转化后,argc就是2,argv就是 (gdb) p (char*)(*c->argv[0])->ptr $28 = 0x80ea5ec “get” (gdb) p (char*)(*c->argv[1])->ptr $26 = 0x80e9fc4 “a” 协议解析后就执行命令。processCommand首先调用lookupCommand找到get对应的函数。在redis server 启动的时候会调用populateCommandTable函数(redis.c 830)把readonlyCommandTable数组转化成一个hash table(server.commands),lookupCommand就是一个简单的hash取值过程,通过key(get)找到相应的命令函数指针getCommand( t_string.c 437)。 getCommand比较简单,通过另一个全局的server.db这个hash table来查找key,并返回redis object,然后通过addReplyBulk函数返回结果。 下面介绍一下reply协议。 bulk replies是以$打头消息体,格式$值长度\r\n值\r\n,一般的get命令返回的结果就是这种个格式。 redis>get aaa $3\r\nbbb\r\n 对应的的处理函数addReplyBulk addReplyBulkLen(c,obj); addReply(c,obj); addReply(c,shared.crlf); error [...]

redis源代码分析 – persistence

redis有全量(save/bgsave)和增量(aof)的持久化命令。 全量的原理就是遍历里所有的DB,在每个bucket,读取链表的key和value并写入dump.rdb文件(rdb.c 405)。 save命令直接调度rdbSave函数,这会阻塞主线程的工作,通常我们使用bgsave。 bgsave命令调度rdbSaveBackground函数启动了一个子进程然后调度了rdbSave函数,子进程的退出状态由 serverCron的backgroundSaveDoneHandler来判断,这个在复制这篇文章里讲到过,这里就不罗嗦了。 除了直接的save、bgsave命令之外,还有几个地方还调用到rdbSaveBackground和rdbSave函数。 shutdown:redis关闭时需要对数据持久化,保证重启后数据一致性,会调用rdbSave()。 flushallCommand:清空redis数据后,如果不做立即执行一个rdbSave(),出现crash后,可能会载入含有老数据的dump.rdb。 void flushallCommand(redisClient *c) { touchWatchedKeysOnFlush(-1); server.dirty += emptyDb(); // 清空数据 addReply(c,shared.ok); if (server.bgsavechildpid != -1) { kill(server.bgsavechildpid,SIGKILL); rdbRemoveTempFile(server.bgsavechildpid); } rdbSave(server.dbfilename); //没有数据的dump.db server.dirty++; } sync:当master接收到slave发来的该命令的时候,会执行rdbSaveBackground,这个以前也有提过。 数据发生变化:在多少秒内出现了多少次变化则触发一次bgsave,这个可以在conf里配置 for (j = 0; j < server.saveparamslen; j++) { struct saveparam *sp = server.saveparams+j; if (server.dirty >= sp->changes && now-server.lastsave > [...]

redis源代码分析- replication

redis的复制方法和机制都比较简单。 slaveof masterip port 在slave端键入命令之后,就开启了从master到slave的复制。一个master可以有多个slave,master有变化的时候会主动的把命令传播给每个slave。slave同时可以作为其他的slave的master,前提条件是这个slave已经处于稳定状态(REDIS_REPL_CONNECTED)。slave在复制的开始阶段处于阻塞状态(sync_readline)无法对外提供服务。 数据的有向图会让redis的运维很有搞头。 slaveof no one 从slave状态的转换回master状态,切断与原master的数据同步。 下面根据图形来描述一个复制全过程。 复制的顺序是从左到右。绿色的过程表示replstate复制状态的变迁,复制相关的函数主要在src/replication.c里,红色为master的复制函数,蓝色的为slave使用的复制函数。 slave端接收到客户端的slaveof masterip ort命令之后,根据readonlyCommandTable找到对应的函数为slaveofCommand。slaveofCommand会保存masterip,masterport,并把server.replstate改成REDIS_REPL_CONNECT,然后返回给客户端OK。 slave端主线程在已经注册时间事件serverConn(redis.c 518)里执行replicationCron(redis.c 646)来开始与master的连接。调用syncWithMaster()开始与master的通信。经过校验之后,会发送一个SYNC command给master端,然后打开一个临时文件用于存储接下来master发过来的rdb文件数据。再添加一个文件事件注册readSyncBulkPayload函数,这个就是接收rdb文件的数据的函数,然后修改状态为REDIS_REPL_TRANSFER。 master接收到SYNC command后,跳转到syncCommand函数(replication.c 556)。syncCommand会调度rdbSaveBackground函数,启动一个子进程做一个全库的持久化rdb文件,并把状态改为REDIS_REPL_WAIT_BGSAVE_END。 master的主线程的serverCron会等待做持久化的子进程的退出,并判断退出的状态是否是正常。 if ((pid = wait3(&statloc,WNOHANG,NULL)) != 0) { if (pid == server.bgsavechildpid) { backgroundSaveDoneHandler(statloc); } else { …. 如果子进程正常退出,会转到backgroundSaveDoneHandler函数。backgroundSaveDoneHandler 处理每次rdbSaveBackground成功后的收尾工作,并打开刚刚产生的rdb文件。然后注册一个sendBulkToSlave函数用于发送rdb文件,状态切换至REDIS_REPL_SEND_BULK。 sendBulkToSlave作用就是根据上面打开的rdb文件,读取并发送到slave端,当文件全部发送完毕之后修改状态为REDIS_REPL_ONLINE。 我们回到slave,上面讲到slave通过readSyncBulkPayload接收rdb数据,接收完整个rdb文件后,会清空整个数据库emptyDb()(replication.c 374)。然后就通过rdbLoad函数载入接收到的rdb文件,于是slave和master数据就一致了,再把状态修改为REDIS_REPL_CONNECTED。 接下来就是master和slave之间增量的传递的增量数据,另外slave和master在应用层有心跳检测(replication.c 543),和超时退出(replication.c 511)。 最后再给出一个replstate状态图 slave端复制状态 REDIS_REPL_NONE:未复制的状态 REDIS_REPL_CONNECT:已经接收到slaveof命令,但未发出sync命令给master REDIS_REPL_TRANSFER:已经发出sync,但还没接收完rdb文件 REDIS_REPL_CONNECTED:已经接收完rdb文件,开始增量接收复制内容 maste端redis_client的复制状态 [...]

redis源代码分析 – event library

每个cs程序尤其是高并发的网络服务端程序都有自己的网络异步事件处理库,redis不例外。 事件库仅仅包括ae.c、ae.h,还有3个不同的多路复用(本文仅描述epoll)的wrapper文件,事件库封装了框架调用的主循环函数,暴露了时间、文件事件注册和销毁函数,典型的依赖反转模式。 网络操作都在networking.c里,封装了常见的socket操作。 我们从redis启动的main函数开始,从用户发出连接键入命令开始遍历网络事件库所涉及的函数,unix套接口相关函数不表。 首先对几个最常用对象进行解释。 //redis启动的时候(init_server())会创建的一个全局使用的事件循环结构 typedef struct aeEventLoop { int maxfd; //仅仅是select 使用 long long timeEventNextId; aeFileEvent events[AE_SETSIZE]; //用于保存epoll需要关注的文件事件的fd、触发条件、注册函数。 aeFiredEvent fired[AE_SETSIZE]; //epoll_wait之后获得可读或者可写的fd数组,通过aeFiredEvent->fd再定位到events。 aeTimeEvent *timeEventHead; //以链表形式保存多个时间事件,每隔一段时间机会触发注册的函数。 int stop; void *apidata; //每种多路复用方法的使用的私有变量,例如epoll就是epfd和一个事件数组;而select是保存rset、wset。 aeBeforeSleepProc *beforesleep; // sleep之前调用的函数,有些事情是每次循环必须做的,并非文件、时间事件。 } aeEventLoop; //文件可读写事件 typedef struct aeFileEvent { int mask; //触发条件:读、写 aeFileProc *rfileProc; //当fd可读时执行的事件(accept,read) aeFileProc *wfileProc; //当fd可写时执行的事件(write) void *clientData; //caller 传入的数据指针 [...]

redis源代码分析 – hash table

hashtable的实现有很多,redis的dict.c 是其中之一。 dict 包含了2个dictht hashtable ht[0], ht[1]。 client版本的dict是没有dictht的概念。加入dictht的概念存在2个ht的目的是为了在rehash的时候可以平滑的迁移bucket里的数据,而不像client的dict要把老的hash table里的一次性的全部数据迁移到新的hash table,这在造成一个密集型的操作,在业务高峰期不可取。 ht是hashtable的简称,实际上是一个指针数组,数组的个数由dictht->size决定,是DICT_HT_INITIAL_SIZE的整数倍。每个元素(bucket)指向一个dictEntry的单链表来解决hash的conflict。查询某个key,需要先hash,定位到bucket,再通过链表遍历。 key经过hash函数后,与dictht->sizemask求与均分到ht的每个bucket上。dictht->used表示这个ht里包含的key的个数,也就是dictEntry的个数,每次dictAdd成功+1。链表的加入为头指针的方法加入,这样dictAdd更加的方便。 随着key不断的添加,bucket下的单链表越来越长,查找、删除效率越来越低,需要对ht进行expand,增加bucket个数,让链表的长度减少。bucket数量的增多,原有bucket的key需要迁移到新的bucket上,于是有了rehash的这个过程。 ht[1]就是为了rehash而产生,新的ht size是ht[0]的两倍2, 随着dictAdd,dictFind函数的调用,ht[0]的每个bucket会rehash加入到ht[1]里。dict->rehashidx 是ht[0] 需要rehash就是迁移到ht[1]的bucket的索引,从0开始直到ht->used==0。 rehash除了每次伴随dictAdd,dcitFind而迁移一个bucket的所有dictEntry,还有一种一次hash100个bucket,直到消耗了某个时间点为止的做法。 rehash的步骤: 拿到一个bucket, 遍历这个链表的每个kv,对key进行hash然后于sizemask求与,定位ht[1]的新bucket位置, 加入到链表,迁移后ht[0].used–,ht[1].used++。 直到ht[0].used=0,释放ht[0]的table,再赋值ht[0]= ht[1],再把则ht[1]reset。 rehash的期间: 由于ht[1]是ht[0]size的2倍,每次dictAdd的时候都会rehash一次,不会出现后ht[1] 满了,而ht[0]还有数据的事情。 查询会先查ht[0]再查询ht[1],在rehash的过程中,不会出现再次expand。 新的key加到ht[1]。 expand的条件: table的位置已经满了,糟糕的hash函数造成的skrew导致永远不会expand。 key的个数除以table的大小,超过了dict_force_resize_ratio。