AOF运行时间:
在beforeSleep()中,即
//事件处理器的主循环
void aeMain(aeEventLoop *eventLoop) {
eventLoop->stop = 0;
while (!eventLoop->stop) {
// 如果有需要在事件处理前执行的函数,那么运行它
if (eventLoop->beforesleep != NULL)
eventLoop->beforesleep(eventLoop);
// 开始处理事件
aeProcessEvents(eventLoop, AE_ALL_EVENTS);
}
}
我们知道AOF是用来把客户端的写同步到硬盘上的,客户端的写发生在eventLoop中(即文件事件驱动)。
因此是下一次循环的事件触发前,对上一次的数据做beforeSleep处理,即AOF处理。所以这里自然需要一个缓冲区来操作。
将数据刷到硬盘则需要通过系统调用fsync(阻塞操作)。由于IO速度比CPU处理速度慢太多,如果Redis每执行一次写命令,我们就通过系统调用write和fsync往硬盘中相应的AOF文件写一条记录,由于其为阻塞I/O的特性,导致主进程的主线程挂起,在该段时间内无法服务新的请求,因而吞吐量受到影响。
fsync有三种策略:
1.Always:每次事件循环都进行一次同步操作,主线程阻塞着做。极值完整度。
2.Everysec:每秒进行一次同步操作,开一个专门操作IO的线程。丢失一秒数据,完整度和性能的平衡。
3.No:由操作系统控制同步操作,操作系统选取落盘时间,不阻塞主线程。极值性能。
通过阅读源码,我们可以发现以下几点结论:
1.在每秒(EverySec)模式下,AOF不一定立刻能把缓冲区的内容IO完。所以可能下一次主线程跑到beforeSleep时,上一次的IO还没做完。所以这是fsync的运行标志位(sync_in_progress)就还是1。这样AOF会有一个推迟2秒的操作,如果推迟了2秒还没做完,主线程就会阻塞着帮忙做了write。
2.强制执行(配置force值),也是主线程阻塞着帮忙write。
3.这里就产生了多线程问题,为了安全性这边用的是原子操作。
4.关于AOF缓存的内存空间大小。如果这个缓存区足够小(小于4K),缓存就会留着被重用。否则释放这个AOF缓存。
关于重写:
有时候反复对一条数据修改,已落盘的AOF会太大。
所以就有了AOF重写机制。它其实是根据现有数据库的情况,直接重新生成一个AOF重写日志,只将当前有效并存在的数据转义成符合AOF日志格式的记录,接着依次写入该日志文件,最后通过fsync系统调用刷入硬盘。这些操作都是在子进程中进行的。
子进程还带来了一个问题,就是在子进程在生成AOF重写日志的时候,主进程还在处理新的请求,那么这段区间的写命令是没有记录下来的。如下图所示,这里AOF重写缓冲区可以接收重写期间的服务器操作,然后主线程调用write,阻塞得去做追加这件事情。出自于源码函数backgroudRewriteDoneHandle。
Huge Page问题
虽然AOF重写日志的生成是在子进程中进行的,一般不会阻塞主进程,但还是有一定几率会发生阻塞的,这就是fork的原因。
fork理论上该子进程会有父进程内存空间的完整拷贝,但Linux对fork做了优化,引入了Copy on Write(写时复制,读时共享)机制,也就是只有当主进程修改了内存页的时候,操作系统会在此操作前拷贝一个副本,将其复制到另一个内存页中,供子进程使用。
那么当内存页设置的过大时,在进行副本拷贝时,申请内存页时,可能需要等待内存页被释放,失败的概率会大一些,为了保证子进程能获得副本,应而导致主进程发生阻塞,无法处理新来的请求,导致吞吐量下降。
可以通过对/sys/kernel/mm/transparent_hugepage/enabled进行设置来关闭这个功能。
主从复制也有可能产生这个问题。同样也是fork的原因。当某个主服务器的redis实例太大了,在fork的时候需要拷贝一套内存页表给子进程,当实例内存过大,页表大小也会过大,这个拷贝页表的动作是主线程阻塞着做的。
总结:
AOF重写和主从复制其实本质上都是把当前的redis内存实例数据落盘。由于涉及IO都需要开子进程来做,以避免主线程阻塞。
开子进程用fork(),由于有写时复制读时共享的机制(在这种机制下,fork的唯一开销就是页表复制),所以不需要真的完全复制一份父进程资源给子进程,但是至少需要一份子进程的内存页表。然后子进程和父进程的虚拟内存就都有了,一开始映射到同样的物理内存上,然后通过写时复制再去给子进程申请新的物理内存。
所以规避法则是:
1.要么干脆不做AOF重写
2.要么保证单个redis实例不是太大