redis源码阅读2----------ae事件
书接上回,redis进入ae事件监听了。
相关的c文件都以ae开头,比如ae.c ae_epoll.c ae_ecport.c ae_select.c ae_kqueue.c
ae相关的结构体在server.h的redisServer下的aeEventLoop结构体(eventloop)。
/* State of an event based program */ typedef struct aeEventLoop { int maxfd; /* highest file descriptor currently registered */ int setsize; /* max number of file descriptors tracked */ long long timeEventNextId; aeFileEvent *events; /* Registered events */ aeFiredEvent *fired; /* Fired events */ aeTimeEvent *timeEventHead; int stop; void *apidata; /* This is used for polling API specific data */ aeBeforeSleepProc *beforesleep; aeBeforeSleepProc *aftersleep; int flags; } aeEventLoop; 然后在开始源码阅读前,我想先记录下ae是干什么的。 1.处理file event。最简单的事,客户端来了个set key value。你得处理了。 2.time event。在给定时间点去执行一些事情。比如持久化。 最后,声明,本次的ae流程,没有任何的外部操作,仅仅只看一个空循环的ae流程,到底干了什么事。 好的,现在我们从上次的结尾,ae.c/aeMain开始(484行) 指定stop的值。eventloop结构体在上面有提到。然后开启事件循环,其中AE_ALL_EVENTS是3(其实是(AE_FILE_EVENTS|AE_TIME_EVENTS),前者是1,后者是2),AE_CALL_BEFORE_SLEEP是8, AE_CALL_AFTER_SLEEP是16.为什么这么设定暂时还不知道。看看后面会不会有解答。 void aeMain(aeEventLoop *eventLoop) { eventLoop->stop = 0; while (!eventLoop->stop) { aeProcessEvents(eventLoop, AE_ALL_EVENTS| AE_CALL_BEFORE_SLEEP| AE_CALL_AFTER_SLEEP); } } 进入aeProcessEvents 函数注释如下: /* Process every pending time event, then every pending file event * (that may be registered by time event callbacks just processed). * Without special flags the function sleeps until some file event * fires, or when the next time event occurs (if any). * * If flags is 0, the function does nothing and returns. * if flags has AE_ALL_EVENTS set, all the kind of events are processed. * if flags has AE_FILE_EVENTS set, file events are processed. * if flags has AE_TIME_EVENTS set, time events are processed. * if flags has AE_DONT_WAIT set the function returns ASAP until all * the events that's possible to process without to wait are processed. * if flags has AE_CALL_AFTER_SLEEP set, the aftersleep callback is called. * if flags has AE_CALL_BEFORE_SLEEP set, the beforesleep callback is called. * * The function returns the number of events processed. */ 首先判定是否有AE_TIME_EVENT或者AE_FILEEVENT. 就像上面说的,时间事件或者文件事件。 if (!(flags & AE_TIME_EVENTS) && !(flags & AE_FILE_EVENTS)) return 0; 此处,初始的flags是27,AE_TIME_EVENTS和AE_FILE_EVENTS都是初始值。 然后会进行一个if判断 if (eventLoop->maxfd != -1 || ((flags & AE_TIME_EVENTS) && !(flags & AE_DONT_WAIT))) 判断的作用是,即使没有文件事件需要去处理,但是只要我们想处理时间时间,就能等待到下一次事件触发。 接下来进入ae.c/ static int64_t usUntilEarliestTimer(aeEventLoop *eventLoop) 作用是返回一个时间,离第一次计时器启动还有多少微妙。这个计时器还不知道是啥玩意。只是看的注释。 没有计时器的话,就返回-1,有的话做一下如下判断: monotime now = getMonotonicUs(); return (now >= earliest->when) ? 0 : earliest->when - now; 代码非常简单,我一步一步调试得很慢,随意return的判断是1,整体函数的返回值是0. 注释还特别注明了时间复杂度是O(n),因为时间事件是未排序的(unsorted)。 作者还注释了一下未来可能的改进措施,希望将此处的时间复杂度降到O(1)或者O(log(n)),不过看注释,作者暂时还不想动这段代码。 接下来判断刚刚的返回值: if (usUntilTimer >= 0) { tv.tv_sec = usUntilTimer / 1000000; tv.tv_usec = usUntilTimer % 1000000; tvp = &tv; } else { /* If we have to check for events but need to return * ASAP because of AE_DONT_WAIT we need to set the timeout * to zero */ if (flags & AE_DONT_WAIT) { tv.tv_sec = tv.tv_usec = 0; tvp = &tv; } else { /* Otherwise we can block */ tvp = NULL; /* wait forever */ } } 很好奇usUntilTimer<0会是什么情况,看看接下来能不能遇见。 然后下一个逻辑是: if (eventLoop->beforesleep != NULL && flags & AE_CALL_BEFORE_SLEEP) eventLoop->beforesleep(eventLoop); 官方对这段函数的注释为:redis每次进入事件驱动库的主循环时(main loop of the event driven library),在休眠以获取文件描述符之前(before to sleep for ready file descriptors),都会调用这个函数。 并且注明了此循环由两个函数调用, 第一个就是aeMain. 第二个是processEventsWhileBlocked.在加载aof/rdb期间处理客户端。(我猜测是bgaofrewrite,bgsave那几个不阻塞用户进程的命令调用的,注意是猜测) 接下来这段注释就挺让人费解了。 如果是第二种方式调用的,说明我们不想执行所有操作(比如密钥过期),但是有部分操作又必须执行。 其中最重要的是freeClientsInAsyncFreeQueue,但同时也调用其他低风险函数。 注释完毕。接下来我们来看看这个冗长的函数。 size_t zmalloc_used = zmalloc_used_memory(); 这个函数是把um赋值给used_memory。这步是个原子操作,有锁。然后return um。 if (zmalloc_used > server.stat_peak_memory) server.stat_peak_memory = zmalloc_used; stat_peak_memory初始为0.判断成功。 以下判断第一次没有进入,ProcessingEventsWhileBlocked初始为0.注释在这里提到这里只要调用重要函数的子集,以防止从processEventsWhileBlocked()重新进入事件循环。同时还会跟踪事件个数,因为想要停止ASAP,在没有事件处理时。 暂时不明白为什么这么做。等后面看到processEventsWhileBlocked()时再说。 if (ProcessingEventsWhileBlocked) { uint64_t processed = 0; processed += handleClientsWithPendingReadsUsingThreads(); processed += tlsProcessPendingData(); processed += handleClientsWithPendingWrites(); processed += freeClientsInAsyncFreeQueue(); server.events_processed_while_blocked += processed; return; } 下一个函数:timeout.c下 handleBlockedClientsTimeout(); 注释:处理超时的被阻塞的客户端 接下来的注释有点拗口,我先上原文: this function is called in beforeSleep() in order to unblock clients that are waiting in blocking operations with a timeout set。 没理解错的话,主要是释放客户端连接,而且还是超时的被阻塞的客户端。 涉及到的结构体是 server.clients_timeout_table。 在rax.h下98-137行 因为我这里没有被阻塞或者超时的客户端,所以在第一个判断 if (raxSize(server.clients_timeout_table) == 0) return; 就直接跳出此函数了。 下一个函数:networking.c下 handleClientsWithPendingReadsUsingThreads(); 处理挂起的读客户端。 When threaded I/O is also enabled for the reading + parsing side, the readable handler will just put normal clients into a queue of clients to process (instead of serving them synchronously). This function runs the queue using the I/O threads, and process them in order to accumulate the reads in the buffers, and also parse the first command available rendering it in the client structures 将客户端放入队列,用IO线程运行队列,并进行处理,还解析在客户端结构中呈现的第一个命令。 这个函数看上去非常复杂,应该是一个很关键的函数。但是如前文所说,我们这里只研究在空事件下的事件循环,这里不展开研究。因为在第一个循环已经return了。不过我相信在接下来的2,3章内,这个函数会被重点研究。 下一个函数:tls.c下 tlsProcessPendingData(); 处理TLS挂起的数据。(必须在flushAppendOnlyFile之前完成) 进入函数后竟然直接return 0;没有任何操作,把我吓一跳。然后仔细一看,在tls.c下有两个宏定义,如果打开了openssl,那么会进入到对应的宏下。但是我们没有打开,所以直接return了。对这部分有兴趣的同学可以把ssl打开,研究一下。 下一个函数:ae.c下 aeSetDontWait(server.el, tlsHasPendingData()); 如果tls还有没处理的数据,就不要sleep。并通知事件处理的下一次迭代将超时设置为0 下一个函数: if (server.cluster_enabled) clusterBeforeSleep(); 在sleep前调用Redis群集功能。请注意,此函数可能会更改Redis Cluster的状态(从ok更改为fail,反之亦然),因此最好在稍后使用此函数为未阻塞的客户端提供服务之前调用它。 很明显,我们没有开启cluster。这个函数的研究要在很久很久以后了。(估计至少半年后吧) 下一个函数:expire.c下 if (server.active_expire_enabled && server.masterhost == NULL) activeExpireCycle(ACTIVE_EXPIRE_CYCLE_FAST); 运行快速过期循环(如果不需要快速循环,被调用函数将尽快返回) 根据配置的过期工作调整运行参数。默认工作量为1,最大可配置工作量为10 然后对 config_keys_per_loop=20 config_cycle_fast_duration=1000 config_cycle_slow_time_perc=25 config_cycle_acceptable_stale=10 四个变量进行设置。第一次设置的大小如上 然后定义了几个全局变量,方便其他调用 static unsigned int current_db = 0; /* Next DB to test. */ static int timelimit_exit = 0; /* Time limit hit in previous call? */ static long long last_fast_cycle = 0; /* When last fast cycle ran. */ if (checkClientPauseTimeoutAndReturnIfPaused()) return; 当客户端暂停时,数据集应该是静态的,不仅是因为客户端无法写入,还因为过期和未执行key收回。 如果前一个循环没有在时间限制内退出,不要启动快速循环,除非估计的过期key的百分比太高。此外,切勿在与快速循环总持续时间相同的时间内重复快速循环。 if (!timelimit_exit && server.stat_expired_stale_perc < config_cycle_acceptable_stale) return; 这里竟然直接return了。而且看了下接下来的判断,即使这里没有return,下面的判断也会return。 不过还是把接下来的注释翻译一下,起码知道干了啥。 。。。。。。。 下一个函数: if (listLength(server.clients_waiting_acks)) processClientsWaitingReplicas(); 解除WAIT下的synchronous replication阻塞客户端 下一个函数: if (moduleCount()) moduleHandleBlockedClients(); 检查是否有客户端被实现阻塞状态的modules解锁 下一个函数: if (listLength(server.unblocked_clients)) processUnblockedClients(); 尝试为刚刚解除阻塞状态的客户端处理命令 下一个函数: if (server.get_ack_from_slaves && !checkClientPauseTimeoutAndReturnIfPaused()) { robj *argv[3];argv[0] = shared.replconf; argv[1] = shared.getack; argv[2] = shared.special_asterick; /* Not used argument. */ replicationFeedSlaves(server.slaves, server.slaveseldb, argv, 3); server.get_ack_from_slaves = 0; } 如果在上一个事件循环迭代期间至少有一个客户端被阻塞,则向所有从属服务器发送ACK请求。请注意,我们在processUnblockedClients()之后执行此操作,因此如果有多个流水线等待,并且刚刚解除阻止的等待再次被阻止,我们不必在没有其他事件循环事件的情况下等待服务器cron周期。见#6623 我们也不会在客户端暂停时发送ACK,因为它会增加复制积压工作,如果我们仍然是主服务器,它们将在暂停后发送。 下一个函数: updateFailoverStatus(); 我们可能已收到客户关于其当前偏移量的更新。注意:在接收到ACK的情况下无法执行此操作,因为故障切换将断开客户端的连接 下一个函数: trackingBroadcastInvalidationMessages(); 以广播(BCAST)模式向参与客户端缓存协议的客户端发送失效消息。 下一个函数: if (server.aof_state == AOF_ON) flushAppendOnlyFile(0); 将AOF buffer写入磁盘。 下一个函数: handleClientsWithPendingWritesUsingThreads(); 处理写操作。 下一个操作: freeClientsInAsyncFreeQueue(); 关闭需要异步关闭的客户端 下一个函数: handleClientsBlockedOnKeys(); 尝试每隔一段时间处理被阻塞的客户端。示例:一个模块在计时器回调中调用RM_SignalKeyAsReady(因此我们根本不访问processCommand()。 下一个函数: if (moduleCount()) moduleReleaseGIL(); 在sleep之前,让线程通过释放GIL来访问数据集。Redis主线程此时不会接触任何东西 redis的作者还特别强调,一定要将这个函数放在beforesleep的最后。 到这里beforesleep的逻辑就结束了。我们又回到aeProcessEvents。 numevents = aeApiPoll(eventLoop, tvp); 调用多路复用API,将仅在超时或某些事件触发时返回 这里返回的是event的个数。 if (eventLoop->aftersleep != NULL && flags & AE_CALL_AFTER_SLEEP) eventLoop->aftersleep(eventLoop); 这个函数在返回事件循环多路复用API后立即被调用,control很快将通过调用不同的事件回调返回到Redis。 后面有一大段函数我们没有进入,因为那个用于write或者read的。会在后续的章节中用到。不过我在这里,先把它贴过来,然后把注释翻译一下,留一个印象。 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 fired = 0; /* Number of events fired for current fd. */
/* Normally we execute the readable event first, and the writable * event later. This is useful as sometimes we may be able * to serve the reply of a query immediately after processing the * query.通常我们先执行可读事件,然后执行可写事件。因为有时我们可以在处理查询后立即提供查询回复。 * * However if AE_BARRIER is set in the mask, our application is * asking us to do the reverse: never fire the writable event * after the readable. In such a case, we invert the calls. * This is useful when, for instance, we want to do things * in the beforeSleep() hook, like fsyncing a file to disk, * before replying to a client. 然而,如果设置了AE_BARRIER,我们的应用程序会要求我们做相反的事情:永远不要在可读取事件之后触发可写事件。在这种情况下,我们反转调用。例如,当我们想在beforeSleep()钩子中做一些事情,比如在回复客户机之前将文件同步到磁盘时*/ int invert = fe->mask & AE_BARRIER;
/* 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.注意“fe->mask&mask&…”代码:可能一个已经处理的事件删除了一个触发的元素,而我们仍然没有处理,所以我们检查该事件是否仍然有效。 * * Fire the readable event if the call sequence is not如果调用顺序没有反转,则触发可读事件 * inverted. */ if (!invert && fe->mask & mask & AE_READABLE) { fe->rfileProc(eventLoop,fd,fe->clientData,mask); fired++; fe = &eventLoop->events[fd]; /* Refresh in case of resize. */调整大小时刷新 }
/* Fire the writable event. */触发可写事件 if (fe->mask & mask & AE_WRITABLE) { if (!fired || fe->wfileProc != fe->rfileProc) { fe->wfileProc(eventLoop,fd,fe->clientData,mask); fired++; } }
/* If we have to invert the call, fire the readable event now * after the writable one. */如果我们必须反转调用,现在在可写事件之后触发可读事件。 if (invert) { fe = &eventLoop->events[fd]; /* Refresh in case of resize. */ if ((fe->mask & mask & AE_READABLE) && (!fired || fe->wfileProc != fe->rfileProc)) { fe->rfileProc(eventLoop,fd,fe->clientData,mask); fired++; } }
processed++; } } 最后检查时间事件,代码很少,但是其中涉及的模块很多。我们后续再研究。 if (flags & AE_TIME_EVENTS) processed += processTimeEvents(eventLoop); 最后返回全部事件的总数: return processed 至此,整个空的事件循环就结束了。