Redis与磁盘IO阻塞

内存中的数据写到磁盘,会经过vfs~fs~逻辑卷/软raid~pagecache~块操作调度~磁盘等一系列过程。
磁盘IO阻塞会涉及到操作系统方面的很多细节。了解这些细节,对编程开发以及运维工作都是有利的。
本文中将结合Redis,讲述磁盘IO阻塞。

一、IO阻塞场景

Redis容易出现磁盘IO阻塞场景:
a)追加aof日志
b)rewrite aof
c)频繁dump rdb

分析这几个场景的代码,发现在Redis中调用的posix接口中,WritefSyncCloseRenameUnlink都有可能造成阻塞。
下面我们结合代码来分析它们是如何造成阻塞的。

二、场景分析

1.追加aof日志

#define AOF_WRITE_LOG_ERROR_RATE 30 /* Seconds between errors logging. */
void flushAppendOnlyFile(int force) {
    // 每秒 fsync
    if (server.aof_fsync == AOF_FSYNC_EVERYSEC && !force) {
       // 有 fsync 正在后台进行 
        if (sync_in_progress) {
            if (server.aof_flush_postponed_start == 0) {
                /*
                 * 前面没有推迟过 write 操作,将推迟写操作的时间记录下来
                 * 不执行 write 或者 fsync
                 */
                server.aof_flush_postponed_start = server.unixtime;
                return;
            } else if (server.unixtime - server.aof_flush_postponed_start < 2) {
                /*
                 * 如果之前已经因为 fsync 而推迟了 write 操作
                 * 但是推迟的时间不超过 2 秒
                 * 不执行 write 或者 fsync
                 */
                return;
            }
            / *
             * 如果后台还有 fsync 在执行,并且 write 已经推迟 >= 2 秒
             * 那么执行写操作
             */
            redisLog(REDIS_NOTICE,"Asynchronous AOF fsync is taking too long (disk is busy?). Writing the AOF buffer without waiting for fsync to complete, this may slow down Redis.");
        }
    }
    /*
     * 执行到这里,程序会对 AOF 文件进行写入。
     */
    nwritten = write(server.aof_fd,server.aof_buf,sdslen(server.aof_buf));
         }
}

从上面的流程可以看出,当有bio线程运行fsync,就会推迟write(aof_buf)
推迟超过2s之后,不管是否有bio线程运行fsync,都会直接调用wirte
这个时候,如果fsync正在执行的的话,就会导致write阻塞,Redis服务也就阻塞了。
这里的IO阻塞跟IO繁忙无关,只是因为fsyncwrite都在操作同一个fd

When the fsync policy is set to 'everysec' we may delay the flush if there  is still an fsync() going on in the background thread, since for instance on Linux write(2) will be blocked by the background fsync anyway.

题外话:那么为什么要将fsync放入后台线程中执行呢?

比如,在SSD上连续写1G,如果每次写入4k,就使用fsync刷page cache的话,需要20+min才能执行完成。
而如果所有1G先write再调用fsync()刷盘,2s就写成功了。
时间相差600倍。可见频繁fsync会导致redis性能大大降低。

2.重写aof日志:

其中有三处可能会出现阻塞:
1)将累积的aof_rewrite_bufwritetmpfile文件中
2)rename tmpfileaof

  1. 将1)中write的缓存,fsync到磁盘中
    4)删除旧aof文件

2.1 对于3)fysnc

是因为fsync本身是一个耗时的操作,所以放入bio线程中执行。

2.2 对于2)、4)

作者将close放入bio线程中执行。这里比较难理解,需要我们理解close,unlink,rename和文件删除的关系。

先来看下linux man中说明(只讨论文件):
Close:关闭文件描述符

a)调用过unlink(使得链接数为0),且close的这个fd是对该file最后一个引用,会触发文件的删除。
if the file descriptor was the last reference to a file which has been removed using unlink(2), the file is deleted.
b)当调用close的时候,缓存pagecache并不会刷入磁盘。
Typically, filesystems do not flush buffers when a file is closed.  If you need to be sure that the data is physically stored on the underlying disk, use fsync(2). 

所以说如果close发生了阻塞,应该就是close触发了文件删除。

Unlink:删除目录项(i节点),并将pathname所引用文件链接数(硬链接)计数减一。

int unlink(const char *pathname)
只有当链接数到达0,文件内容才可能被删除。但是,当有进程打开这个文件,文件内容也不能被删除。

所以说:
a)close本身并不会删除文件,除非之前调用过unlink。使得引用数和链接数都为0。
b)unlink本身也不会删除文件,除非此时引用数和链接为0。
只有引用数和链接数都为0,closeunlink才会删除文件,导致阻塞。

Rename:

int rename(const char *oldname, const char *newname) 
Rename这个系统调用会unlink newname对应的文件,然后将旧文件的名字改成新名字。
涉及到删除文件,遵守上文所述删除文件规则,也就是说rename也不一定会真正删除文件。
If the link named by the new argument exists, it shall be removed and old renamed to new.

所以2)中rename tmpfile时,由于fd仍然被引用,并不会真正的删除文件。到4)时,调用close,才会真正删除文件。由于We don't want close(2) or rename(2) calls to block the server on old file deletion.此时将close放入bio线程中执行,避免服务阻塞。

2.3 对于1)

其实这种情况在开始aofredis实例中并不少见。

以下为一个故障现象整理:

子进程做aof,对redis资源消耗。此时redis服务正常。
a)aof rewrite耗时20:04:41-20:26:41共9分钟
b)aof_rewrite_buf使用9280M
c)用户大部分时间平均每秒写入10M/S,高峰写入50M/S
子进程生成新aof,主进程将aof_rewrite_buf写入aof文件中。
此时redis阻塞,不相应外部服务
耗时:20:26:42-20:29:51共3分11秒。

正常情况下,这应该是秒级别就完成的操作。
之所以阻塞,是由页回写机制造成的,我们有两个方向可以尝试解决这个问题:

2.3.1 在程序层面,将集中write改成多次频繁写。

redis4.0利用管道优化aofwrite,具体可参见 https://yq.aliyun.com/articles/177819

2.3.2 系统层面调优,优化内核参数,将写活动高峰分布成频繁的多次写。

首先我们需要了解,脏页是什么时候回写的:

a)空闲内存低于阈值:/proc/sys/vm/dirty_background_ratio 
vm.dirty_background_ratio is the percentage of system memory that can be filled with dirty pages — memory pages that still need to be written to disk — before the pdflush/flush/kdmflush background processes kick in to write it to disk.

b)脏页在内存中驻留的时间超过一个特定的阈值:/proc/sys/vm/dirty_expire_centisecs
When the pdflush/flush/kdmflush processes kick in they will check to see how old a dirty page is, and if it’s older than this value it’ll be written asynchronously to disk.

c)进程调用sync/fsync

d)进程调用write写文件刷新缓存。
WRITE写的时候,缓存超过dirty_ratio,则会阻塞写操作,回刷脏页,直到缓存低于dirty_ratio;如果缓存高于background_writeout,则会在写操作时,唤醒pdflush进程刷脏页,不阻塞写操作。

注:
 在Linux-3.2新内核中,page cache和buffer cache的刷新机制发生了改变。放弃了原有的pdflush机制,改成了bdi_writeback机制。这种变化主要解决原有pdflush机制存在的一个问题:在多磁盘的系统中,pdflush管理了所有磁盘的page/buffer cache,从而导致一定程度的IO性能瓶颈。bdi_writeback机制为每个磁盘都创建一个线程,专门负责这个磁盘的pagecache或者buffer cache的数据刷新工作,从而实现了每个磁盘的数据刷新程序在线程级的分离,这种处理可以提高IO性能。
https://blog.csdn.net/younger_china/article/details/55187057

从d)中可以看出,当磁盘写入繁忙,导致脏页占用内存比率增大。此时调用wirte,fsync都会导致调用进程时间挂起。这也是前面aof write耗费3分11秒的原因。

针对上述脏页回收时机,可以做如下参数调优:

减少内存使用 
vm.dirty_background_ratio = 5
vm.dirty_ratio = 10
最大化的使用内存 
vm.dirty_background_ratio = 50
vm.dirty_ratio = 80
优化写入性能, 可以使用内存, 但是等到空闲的时候希望内存被回收, 比较经常用在应对突然有峰值的这种情况 
vm.dirty_background_ratio = 5
vm.dirty_ratio = 80

参见 https://mp.weixin.qq.com/s/9AwI6UfMTk3bcs1BlpfCYA

你可能感兴趣的:(Redis与磁盘IO阻塞)