Redis超时问题分析汇总

运维 系统运维 Redis
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。

int rdbSaveBackground(char *filename) { 
    pid_t childpid; 
    long long start; 
  
    if (server.rdb_child_pid != -1) return REDIS_ERR; 
    serverserver.dirty_before_bgsave = server.dirty; 
    server.lastbgsave_try = time(NULL); 
    start = ustime(); 
    if ((childpid = fork()) == 0) { 
        int retval; 
        /* Child */ 
        if (server.ipfd > 0) close(server.ipfd); 
        if (server.sofd > 0) close(server.sofd); 
        retval = rdbSave(filename); 
        if (retval == REDIS_OK) { 
            size_t private_dirty = zmalloc_get_private_dirty(); 
            if (private_dirty) { 
                redisLog(REDIS_NOTICE, 
                    "RDB: %zu MB of memory used by copy-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't save in background: fork: %s", 
                strerror(errno)); 
            return REDIS_ERR; 
        } 
        redisLog(REDIS_NOTICE,"Background saving started by pid %d",childpid); 
        server.rdb_save_time_start = time(NULL); 
        server.rdb_child_pid = childpid
        updateDictResizePolicy(); 
        return REDIS_OK; 
    } 
    return REDIS_OK; /* unreached */ 

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.

 

程序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); 
  • 1.

 

程序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("Unrecoverable error creating server.ipfd file event."); 
  
int aeCreateFileEvent(aeEventLoop *eventLoop, int fd, int mask, 
        aeFileProc *proc, void *clientData) 

    if (fd >= eventLoop->setsize) { 
        errno = ERANGE
        return AE_ERR; 
    } 
    aeFileEvent *fe = &eventLoop->events[fd]; 
  
    if (aeApiAddEvent(eventLoop, fd, mask) == -1) 
        return AE_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; 
    return AE_OK; 

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.

 

程序3

 

static int aeApiAddEvent(aeEventLoop *eventLoop, int fd, int mask) {  
    aeApiState *state = eventLoop->apidata;  
    struct epoll_event ee;  
    /* If the fd was already monitored for some event, we need a MOD  
     * operation. Otherwise we need an ADD operation. */  
    int op = eventLoop->events[fd].mask == AE_NONE ?  
            EPOLL_CTL_ADD : EPOLL_CTL_MOD;  
    ee.events = 0;  
    mask |= eventLoop->events[fd].mask; /* Merge old events */  
    if (mask & AE_READABLE) ee.events |= EPOLLIN;  
    if (mask & AE_WRITABLE) ee.events |= EPOLLOUT;  
    ee.data.u64 = 0; /* avoid valgrind warning */  
    ee.data.fd = fd;  
    if (epoll_ctl(state->epfd,op,fd,&ee) == -1) return -1;  
    return 0;  
}  
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.

 

程序4

在初始话完所有事件驱动后,如程序5所示主进程根据numevents = aeApiPoll(eventLoop, tvp)获得io就绪的文件描述符和其对应的处理程序,并对fd进行处理。大致流程是accept()->createclient()->readQueryFromClient()。其中readQueryFromClient()读取信息中的redis命令-> processInputBuffer()->call()***完成命令。

void aeMain(aeEventLoop *eventLoop) { 
    eventLoop->stop = 0
    while (!eventLoop->stop) { 
        if (eventLoop->beforesleep != NULL) 
            eventLoop->beforesleep(eventLoop); 
        aeProcessEvents(eventLoop, AE_ALL_EVENTS); 
    } 

int aeProcessEvents(aeEventLoop *eventLoop, int flags) 
{------------------------------- 
 numevents = aeApiPoll(eventLoop, tvp); 
        for (j = 0; j < numevents; j++) {             aeFileEvent *fe = &eventLoop->events[eventLoop->fired[j].fd]; 
            int mask = eventLoop->fired[j].mask; 
            int fd = eventLoop->fired[j].fd; 
            int rfired = 0
  
            /* note the fe->mask & mask & ... code: maybe an already processed 
             * event removed an element that fired and we still didn't 
             * processed, so we check if the event is still valid. */ 
            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++; 
        } 

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.

 

程序5

从上述代码可以看出redis利用ae事件驱动结合epoll多路复用实现了串行式的命令处理。所以一些慢命令例如sort,hgetall,union,mget都会使得单命令处理时间较长,容易引起后续命令time out.所以我们***需要从业务上尽量避免使用慢命令,如将hash格式改为kv自行解析,第二增加redis实例个数,每个redis服务器调用尽量少的慢命令。

责任编辑:黄丹 来源: 搜索技术博客
相关推荐

2014-07-01 11:18:37

Android Stu问题汇总

2022-09-28 14:13:03

Linux工具

2010-09-15 09:20:40

2011-08-12 09:52:35

iPhone开发TableviewUITextField

2010-11-25 11:15:11

MySQL查询超时

2021-11-15 12:42:25

C# 定位gRPC

2011-04-02 10:29:20

Linux工具

2009-07-22 08:59:01

Windows xp升Windows 7系统升级

2010-05-13 13:27:23

2009-09-22 09:22:03

.NET常见问题

2012-02-06 10:37:07

Java

2013-08-21 14:57:42

objective-c问题

2017-05-17 15:09:46

Linux分析性能工具

2010-10-14 09:15:20

MySQL查询

2010-08-17 09:16:23

2020-10-18 12:00:27

前端开发架构

2010-07-21 09:29:33

Perl常见问题

2010-02-03 16:32:13

2020-12-28 11:08:18

MySQL数据库服务器

2010-01-05 09:36:40

ADO超时
点赞
收藏

51CTO技术栈公众号