Redis在分布式应用中占据着越来越重要的地位,短短的几万行代码,实现了一个高性能的数据存储服务。最近dump中心的cm8集群出现过几次redis超时的情况,但是查看redis机器的相关内存都没有发现内存不够,或者内存发生交换的情况,查看redis源码之后,发现在某些情况下redis会出现超时的状况,相关细节如下。
Redis在分布式应用中占据着越来越重要的地位,短短的几万行代码,实现了一个高性能的数据存储服务。最近dump中心的cm8集群出现过几次redis超时的情况,但是查看redis机器的相关内存都没有发现内存不够,或者内存发生交换的情况,查看redis源码之后,发现在某些情况下redis会出现超时的状况,相关细节如下。
1. 网络。Redis的处理与网络息息相关,如果网络出现闪断则容易发生redis超时的状况。如果出现这种状况首先应查看redis机器网络带宽信息,判断是否有闪断情况发生。
2. 内存。redis所有的数据都放在内存里,当物理内存不够时,linux os会使用swap内存,导致内存交换发生,这时如果有redis调用命令就会产生redis超时。这里可以通过调整/proc/sys/vm/swappiness参数,来设置物理内存使用超过多少就会进行swap。
- intrdbSaveBackground(char*filename){
- pid_tchildpid;
- longlongstart;
- if(server.rdb_child_pid!=-1)returnREDIS_ERR;
- serverserver.dirty_before_bgsave=server.dirty;
- server.lastbgsave_try=time(NULL);
- start=ustime();
- if((childpid=fork())==0){
- intretval;
- /*Child*/
- if(server.ipfd>0)close(server.ipfd);
- if(server.sofd>0)close(server.sofd);
- retval=rdbSave(filename);
- if(retval==REDIS_OK){
- size_tprivate_dirty=zmalloc_get_private_dirty();
- if(private_dirty){
- redisLog(REDIS_NOTICE,
- "RDB:%zuMBofmemoryusedbycopy-on-write",
- private_dirty/(1024*1024));
- }
- }
- exitFromChild((retval==REDIS_OK)?0:1);
- }else{
- /*Parent*/
- server.stat_fork_time=ustime()-start;
- if(childpid==-1){
- server.lastbgsave_status=REDIS_ERR;
- redisLog(REDIS_WARNING,"Can'tsaveinbackground:fork:%s",
- strerror(errno));
- returnREDIS_ERR;
- }
- redisLog(REDIS_NOTICE,"Backgroundsavingstartedbypid%d",childpid);
- server.rdb_save_time_start=time(NULL);
- server.rdb_child_pid=childpid;
- updateDictResizePolicy();
- returnREDIS_OK;
- }
- returnREDIS_OK;/*unreached*/
- }
程序1
另外还有一些特殊情况也会导致swap发生。当我们使用rdb做为redis集群持久化时可能会发生物理内存不够的情况(aof持久化只是保持支持不断的追加redis集群变化操作,不太容易引起swap)。当使用rdb持久化时,如程序1所示主进程会fork一个子进程去dump redis中所有的数据,主进程依然为客户端服务。此时主进程和子进程共享同一块内存区域, linux内核采用写时复制来保证数据的安全性。在这种模式下如果客户端发来写请求,内核将该页赋值到一个新的页面上并标记为写,在将写请求写入该页面。因此,在rdb持久化时,如果有其他请求,那么redis会使用更多的内存,更容易发生swap,因此在可以快速恢复的场景下尽量少使用rdb持久化可以将rdb dump的条件设的苛刻一点,当然也可以选择aof,但是aof也有他自身的缺点。另外也可以使用2.6以后的主从结构,将读写分离,这样不会出现server进程上又读又写的情景发生 3. Redis单进程处理命令。Redis支持udp和tcp两种连接,redis客户端向redis服务器发送包含redis命令的信息,redis服务器收到信息后解析命令后执行相应的操作,redis处理命令是串行的具体流程如下。首先服务端建立连接如程序2所示,在创建socket,bind,listen后返回文件描述符:
- server.ipfd=anetTcpServer(server.neterr,server.port,server.bindaddr);
程序2
对于redis这种服务来说,它需要处理成千上万个连接(***达到655350),需要使用多路复用来处理多个连接。这里redis提供了epoll,select, kqueue来实现,这里在默认使用epoll(ae.c)。拿到listen函数返回的文件描述符fd后,redis将fd和其处理acceptTcpHandler函数加入到事件驱动的链表中.实际上在加入事件队列中,程序4事件驱动程序将套接字相关的fd文件描述符加入到epoll的监听事件中。
- if(server.ipfd>0&&aeCreateFileEvent(server.el,server.ipfd,AE_READABLE,
- acceptTcpHandler,NULL)==AE_ERR)redisPanic("Unrecoverableerrorcreatingserver.ipfdfileevent.");
- intaeCreateFileEvent(aeEventLoop*eventLoop,intfd,intmask,
- aeFileProc*proc,void*clientData)
- {
- if(fd>=eventLoop->setsize){
- errno=ERANGE;
- returnAE_ERR;
- }
- aeFileEvent*fe=&eventLoop->events[fd];
- if(aeApiAddEvent(eventLoop,fd,mask)==-1)
- returnAE_ERR;
- fe->mask|=mask;
- if(mask&AE_READABLE)fe->rfileProc=proc;
- if(mask&AE_WRITABLE)fe->wfileProc=proc;
- fe->clientDataclientData=clientData;
- if(fd>eventLoop->maxfd)
- eventLoop->maxfd=fd;
- returnAE_OK;
- }
程序3
- staticintaeApiAddEvent(aeEventLoop*eventLoop,intfd,intmask){
- aeApiState*state=eventLoop->apidata;
- structepoll_eventee;
- /*Ifthefdwasalreadymonitoredforsomeevent,weneedaMOD
- *operation.OtherwiseweneedanADDoperation.*/
- intop=eventLoop->events[fd].mask==AE_NONE?
- EPOLL_CTL_ADD:EPOLL_CTL_MOD;
- ee.events=0;
- mask|=eventLoop->events[fd].mask;/*Mergeoldevents*/
- if(mask&AE_READABLE)ee.events|=EPOLLIN;
- if(mask&AE_WRITABLE)ee.events|=EPOLLOUT;
- ee.data.u64=0;/*avoidvalgrindwarning*/
- ee.data.fd=fd;
- if(epoll_ctl(state->epfd,op,fd,&ee)==-1)return-1;
- return0;
- }
程序4
在初始话完所有事件驱动后,如程序5所示主进程根据numevents = aeApiPoll(eventLoop, tvp)获得io就绪的文件描述符和其对应的处理程序,并对fd进行处理。大致流程是accept()->createclient()->readQueryFromClient()。其中readQueryFromClient()读取信息中的redis命令-> processInputBuffer()->call()***完成命令。
- voidaeMain(aeEventLoop*eventLoop){
- eventLoop->stop=0;
- while(!eventLoop->stop){
- if(eventLoop->beforesleep!=NULL)
- eventLoop->beforesleep(eventLoop);
- aeProcessEvents(eventLoop,AE_ALL_EVENTS);
- }
- }
- intaeProcessEvents(aeEventLoop*eventLoop,intflags)
- {-------------------------------
- numevents=aeApiPoll(eventLoop,tvp);
- for(j=0;j<numevents;j++){aeFileEvent*fe=&eventLoop->events[eventLoop->fired[j].fd];
- intmask=eventLoop->fired[j].mask;
- intfd=eventLoop->fired[j].fd;
- intrfired=0;
- /*notethefe->mask&mask&...code:maybeanalreadyprocessed
- *eventremovedanelementthatfiredandwestilldidn't
- *processed,sowecheckiftheeventisstillvalid.*/
- if(fe->mask&mask&AE_READABLE){
- rfired=1;
- fe->rfileProc(eventLoop,fd,fe->clientData,mask);
- }
- if(fe->mask&mask&AE_WRITABLE){
- if(!rfired||fe->wfileProc!=fe->rfileProc)
- fe->wfileProc(eventLoop,fd,fe->clientData,mask);
- }
- processed++;
- }
- }
程序5
从上述代码可以看出redis利用ae事件驱动结合epoll多路复用实现了串行式的命令处理。所以一些慢命令例如sort,hgetall,union,mget都会使得单命令处理时间较长,容易引起后续命令time out.所以我们***需要从业务上尽量避免使用慢命令,如将hash格式改为kv自行解析,第二增加redis实例个数,每个redis服务器调用尽量少的慢命令。
©本文为清一色官方代发,观点仅代表作者本人,与清一色无关。清一色对文中陈述、观点判断保持中立,不对所包含内容的准确性、可靠性或完整性提供任何明示或暗示的保证。本文不作为投资理财建议,请读者仅作参考,并请自行承担全部责任。文中部分文字/图片/视频/音频等来源于网络,如侵犯到著作权人的权利,请与我们联系(微信/QQ:1074760229)。转载请注明出处:清一色财经