Page Cache性能优化

我们前一章节中学习Linux是用Cache/Buffer缓存数据,提高系统的I/O性能,且有一个回刷任务在适当时候把脏数据回刷到储存介质中。那么本章重点学习优化机制,包括以下内容

什么时候触发回刷?
脏数据达到多少阈值还是定时触发呢?
内核是如何做到回写机制的
1. 配置概述
Linux内核在/proc/sys/vm中有透出数个配置文件,可以对触发回刷的时机进行调整。内核的回刷进程是怎么运作的呢?这数个配置文件有什么作用呢?

root@public-VirtualBox:~# sysctl -a | grep dirty
vm.dirty_background_bytes = 0
vm.dirty_background_ratio = 10
vm.dirty_bytes = 0
vm.dirty_expire_centisecs = 3000
vm.dirty_ratio = 20
vm.dirty_writeback_centisecs = 500
vm.dirtytime_expire_seconds = 43200
1
2
3
4
5
6
7
8
在/proc/sys/vm中有以下文件与回刷脏数据密切相关:

配置文件    功能    默认值
vm.dirty_background_ratio    触发回刷的脏数据占用内存的百分比    10
vm.dirty_background_bytes    触发回刷的脏数据量    0
vm.dirty_bytes    触发同步写的脏数据量    0
vm.dirty_ratio    触发同步写的脏数据占可用内存的百分比    20
vm.dirty_expire_centisecs    脏数据超时回刷时间(单位:1/100S)    3000
vm.dirty_writeback_centisecs    回刷进程定时唤醒时间(单位: 1/100S)    500
vm.dirty_background_ratio:

内存可以填充脏数据的百分比,这些脏数据稍后会写入磁盘。pdflush/flush/kdmflush这些后台进程会稍后清理脏数据。比如,我有32G内存,那么有3.2G(10%的比例)的脏数据可以待着内存里,超过3.2G的话就会有后台进程来清理。

vm.dirty_ratio

可以用脏数据填充的绝对最大系统内存量,当系统到达此点时,必须将所有脏数据提交到磁盘,同时所有新的I/O块都会被阻塞,直到脏数据被写入磁盘。这通常是长I/O卡顿的原因,但这也是保证内存中不会存在过量脏数据的保护机制。

vm.dirty_background_bytes 和 vm.dirty_bytes

另一种指定这些参数的方法。如果设置 xxx_bytes版本,则 xxx_ratio版本将变为0,反之亦然。

vm.dirty_expire_centisecs

指定脏数据能存活的时间。在这里它的值是30秒。当 pdflush/flush/kdmflush 在运行的时候,他们会检查是否有数据超过这个时限,如果有则会把它异步地写到磁盘中。毕竟数据在内存里待太久也会有丢失风险。

vm.dirty_writeback_centisecs

指定多长时间 pdflush/flush/kdmflush 这些进程会唤醒一次,然后检查是否有缓存需要清理。

实际上dirty_ratio的数字大于dirty_background_ratio,是不是就不会达到dirty_ratio呢?

首先达到dirty_background_ratio的条件后触发flush进程进行异步的回写操作,但是这一过程中应用进程仍然可以进行写操作,如果多个应用写入的量大于flush进程刷出的量,那自然就会达到vm.dirty_ratio这个参数所设定的阙值,此时操作系统会转入同步地进行脏页的过程,阻塞应用进程。

2. 配置实例
单纯的配置说明毕竟太抽象。结合网上的分享,我们看看在不同场景下,该如何配置?

场景1:尽可能不丢数据

有些产品形态的数据非常重要,例如行车记录仪。在满足性能要求的情况下,要做到尽可能不丢失数据。

/* 此配置不一定适合您的产品,请根据您的实际情况配置 */
dirty_background_ratio = 5
dirty_ratio = 10
dirty_writeback_centisecs = 50
dirty_expire_centisecs = 100

这样的配置有以下特点:

当脏数据达到可用内存的5%时唤醒回刷进程
脏数据达到可用内存的10%时,应用每一笔数据都必须同步等待
每隔500ms唤醒一次回刷进程
当脏数据达到可用内存的5%时唤醒回刷进程
由于发生交通事故时,行车记录仪随时可能断电,事故前1~2s的数据尤为关键。因此在保证性能满足不丢帧的情况下,尽可能回刷数据。

此配置通过减少Cache,更加频繁唤醒回刷进程的方式,尽可能让数据回刷。

此时的性能理论上会比每笔数据都O_SYNC略高,比默认配置性能低,相当于用性能换数据安全。

场景2:追求更高性能

有些产品形态不太可能会掉电,例如服务器。此时不需要考虑数据安全问题,要做到尽可能高的IO性能。

/* 此配置不一定适合您的产品,请根据您的实际情况配置 */
dirty_background_ratio = 50
dirty_ratio = 80
dirty_writeback_centisecs = 2000
dirty_expire_centisecs = 12000

这样的配置有以下特点:

当脏数据达到可用内存的50%时唤醒回刷进程
当脏数据达到可用内存的80%时,应用每一笔数据都必须同步等待
每隔20s唤醒一次回刷进程
内存中脏数据存在时间超过120s则在下一次唤醒时回刷
与场景1相比,场景2的配置通过 增大Cache,延迟回刷唤醒时间来尽可能缓存更多数据,进而实现提高性能

场景3:突然的IO峰值拖慢整体性能

什么是IO峰值?突然间大量的数据写入,导致瞬间IO压力飙升,导致瞬间IO性能狂跌,对行车记录仪而言,有可能触发视频丢帧。

/* 此配置不一定适合您的产品,请根据您的实际情况配置 */
dirty_background_ratio = 5
dirty_ratio = 80
dirty_writeback_centisecs = 500
dirty_expire_centisecs = 3000
1
2
3
4
5
这样的配置有以下特点:

当脏数据达到可用内存的5%时唤醒回刷进程
当脏数据达到可用内存的80%时,应用每一笔数据都必须同步等待
每隔5s唤醒一次回刷进程
内存中脏数据存在时间超过30s则在下一次唤醒时回刷
这样的配置,通过增大Cache总容量,更加频繁唤醒回刷的方式,解决IO峰值的问题,此时能保证脏数据比例保持在一个比较低的水平,当突然出现峰值,也有足够的Cache来缓存数据。

3. 内核演变
对于回写方式在之前的2.4内核中,使用 bdflush的线程专门负责writeback的操作,因为磁盘I/O操作很慢,而现代操作系统通常具有多个块设备,如果bdflush在其中一个块设备上等待I/O操作的完成,可能需要很长的时间,此时其他块设备还处于空闲状态,这时候,单线程模式的bdflush就称为了影响性能的瓶颈。而此时bdflush是没有周期扫描功能,因此需要配合kupdate线程一起使用。

bdflush 存在的问题:

整个系统仅仅只有一个 bdflush 线程,当系统回写任务较重时,bdflush 线程可能会阻塞在某个磁盘的I/O上,

导致其他磁盘的I/O回写操作不能及时执行

于是在2.6内核中,bdflush机制就被pdflush取代,pdflush是一组线程,根据块设备I/O负载情况,数量从最少的2个到最多的8个不等,如果1S内都没有空闲的pdflush线程可用,内核将创建一个新的pdflush线程,反之某个pdflush线程空闲超过1S,则该线程将会被销毁。pdflush 线程数目是动态的,取决于系统的I/O负载。它是面向系统中所有磁盘的全局任务的。

pdflush 存在的问题:

pdflush的数目是动态的,一定程度上缓解了 bdflush 的问题。但是由于 pdflush 是面向所有磁盘的,所以有可能出现多个 pdflush 线程全部阻塞在某个拥塞的磁盘上,同样导致其他磁盘的I/O回写不能及时执行。

于是在内最新的内核中,直接将一个块设备对应一个thread,这种内核线程被称为flusher threads,线程名为“Writeback",执行体为"wb_workfn",通过workqueue机制实现调度。

4. 内核实现
由于内核page cache的作用,写操作实际被延迟写入。当page cache里的数据被用户写入但是没有刷新到磁盘时,则该page为脏页(块设备page cache机制因为以前机械磁盘以扇区为单位读写,引入了buffer_head,每个4K的page进一步划分成8个buffer,通过buffer_head管理,因此可能只设置了部分buffer head为脏)。
脏页在以下情况下将被回写(write back)到磁盘上:

脏页在内存里的时间超过了阈值。
系统的内存紧张,低于某个阈值时,必须将所有脏页回写。
用户强制要求刷盘,如调用sync()、fsync()、close()等系统调用。
以前的Linux通过pbflush机制管理脏页的回写,但因为其管理了所有的磁盘的page/buffer_head,存在严重的性能瓶颈,因此从Linux 2.6.32开始,脏页回写的工作由bdi_writeback机制负责。bdi_writeback机制为每个磁盘都创建一个线程,专门负责这个磁盘的page cache或者buffer cache的数据刷新工作,以提高I/O性能。

在 kernel/sysctl.c中列出了所有的配置文件的信息

static struct ctl_table vm_table[] = {
    ...
    {
        .procname    = "dirty_background_ratio",
        .data        = &dirty_background_ratio,
        .maxlen        = sizeof(dirty_background_ratio),
        .mode        = 0644,
        .proc_handler    = dirty_background_ratio_handler,
        .extra1        = &zero,
        .extra2        = &one_hundred,
    },
    {
        .procname    = "dirty_ratio",
        .data        = &vm_dirty_ratio,
        .maxlen        = sizeof(vm_dirty_ratio),
        .mode        = 0644,
        .proc_handler    = dirty_ratio_handler,
        .extra1        = &zero,
        .extra2        = &one_hundred,
    },
    {
        .procname    = "dirty_writeback_centisecs",
        .data        = &dirty_writeback_interval,
        .maxlen        = sizeof(dirty_writeback_interval),
        .mode        = 0644,
        .proc_handler    = dirty_writeback_centisecs_handler,
    },
    ...
}

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
这些值在mm/page-writeback.c中有全局变量定义

int dirty_background_ratio = 10;
int vm_dirty_ratio = 20;
unsigned int dirty_writeback_interval = 5 * 100; /* centiseconds */
1
2
3
通过ps -aux,我们可以看到writeback的内核进程

这实际上是一个工作队列对应的进程,在default_bdi_init()中创建(mm/backing-dev.c)

static int __init default_bdi_init(void)
{
    int err;

    bdi_wq = alloc_workqueue("writeback", WQ_MEM_RECLAIM | WQ_FREEZABLE |
                          WQ_UNBOUND | WQ_SYSFS, 0);
    if (!bdi_wq)
        return -ENOMEM;

    err = bdi_init(&noop_backing_dev_info);

    return err;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
回刷进程的核心是函数wb_workfn(),通过函数wb_init()绑定。

static int wb_init(struct bdi_writeback *wb, struct backing_dev_info *bdi
        int blkcg_id, gfp_t gfp)
{
    ...
    INIT_DELAYED_WORK(&wb->dwork, wb_workfn);
    ...
}
1
2
3
4
5
6
7
唤醒回刷进程的操作是这样的

static void wb_wakeup(struct bdi_writeback *wb)
{
    spin_lock_bh(&wb->work_lock);
    if (test_bit(WB_registered, &wb->state))
        mod_delayed_work(bdi_wq, &wb->dwork, 0);
    spin_unlock_bh(&wb->work_lock);
}
1
2
3
4
5
6
7
表示唤醒的回刷任务在工作队列writeback中执行,这样,就把工作队列和回刷工作绑定了,重点看看这个接口做了些什么工作

void wb_workfn(struct work_struct *work)
{
    struct bdi_writeback *wb = container_of(to_delayed_work(work),
                        struct bdi_writeback, dwork);
    long pages_written;

    set_worker_desc("flush-%s", dev_name(wb->bdi->dev));
    current->flags |= PF_SWAPWRITE;

       //如果当前不是一个救援工作队列,或者当前bdi设备已注册,这是一般路径
    if (likely(!current_is_workqueue_rescuer() || 
           !test_bit(WB_registered, &wb->state))) {                                                          ------------------------(1)
        /*
         * The normal path.  Keep writing back @wb until its
         * work_list is empty.  Note that this path is also taken
         * if @wb is shutting down even when we're running off the
         * rescuer as work_list needs to be drained.
         */
        do {从bdi的work_list取出队列里的任务,执行脏页回写
            pages_written = wb_do_writeback(wb);
            trace_writeback_pages_written(pages_written);
        } while (!list_empty(&wb->work_list));
    } else {                                                                                                                          -----------------------(2)
        /*
         * bdi_wq can't get enough workers and we're running off
         * the emergency worker.  Don't hog it.  Hopefully, 1024 is
         * enough for efficient IO.
         */
        pages_written = writeback_inodes_wb(wb, 1024,//只提交一个work并限制写入1024个pages
                            WB_REASON_FORKER_THREAD);
        trace_writeback_pages_written(pages_written);
    }
    //如果上面处理完到现在这段间隔又有了work,再次立马启动回写进程
    if (!list_empty(&wb->work_list))                                                                                 -----------------------(3)
        mod_delayed_work(bdi_wq, &wb->dwork, 0);
    else if (wb_has_dirty_io(wb) && dirty_writeback_interval)
        wb_wakeup_delayed(wb);//如果所有bdi设备上挂的dirty inode回写完,那么就重置定制器,
        //再过dirty_writeback_interval,即5s后再唤醒回写进程

    current->flags &= ~PF_SWAPWRITE;
}

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
41
正常路径,rescue workerrescue内核线程,内存紧张时创建新的工作线程可能会失败,如果内核中有会需要回收的内存,就调用wb_do_writeback进行回收
如果当前workqueue不能获得足够的worker进行处理,只提交一个work并限制写入1024个pages
这也过程代码较多,暂不去深入分析,重点关注相关的配置是如何起作用的。

5. 触发回写方式
触发writeback的地方主要有以下几处:

5.1 主动发起
手动执行sysn命令
sync->
    SYSCALL_DEFINE0(sync)->
        sync_inodes_one_sb->
            sync_inodes_sb->
                bdi_queue_work
1
2
3
4
5
syncfs系统调用
SYSCALL_DEFINE1(syncfs, int, fd)->
    sync_filesystem->
        __sync_filesystem->
            sync_inodes_sb->
                bdi_queue_work
1
2
3
4
5
直接内存回收,内存不足时调用
free_more_memory->
    wakeup_flusher_threads->
        __bdi_start_writeback->
            bdi_queue_work
1
2
3
4
分配内存空间不足,触发回写脏页腾出内存空间
__alloc_pages_nodemask->
    __alloc_pages_slowpath->
        __alloc_pages_direct_reclaim->
            __perform_reclaim->
                try_to_free_pages->
                    do_try_to_free_pages->
                        wakeup_flusher_threads->
                            __bdi_start_writeback->
                                bdi_queue_work
1
2
3
4
5
6
7
8
9
remount/umount操作,需要先将脏页写回
5.2 空间层面
当系统的“dirty”的内存大于某个阈值,该阈值是在总共的“可用内存”(包括free pages 和reclaimable pages)中的占比。

参数“dirty_background_ratio”(默认值10%),或者是绝对字节数“dirty_background_bytes”(默认值为0,表示生效)。两个参数只要谁先达到即可执行,此时就会交给专门负责writeback的background线程去处理。

参数“dirty_ratio”(默认值30%)和“dirty_bates”(默认值为0,表示生效),当“dirty”的内存达到这个比例或数量,进程则会停下write操作(被阻塞),先把“dirty”进行writeback。

5.3 时间层面
周期性的扫描,扫描间隔用参数:dirty_writeback_interval表示,以毫秒为单位。发现存在最近一次更新时间超过某个阈值(参数:dirty_expire_interval,单位毫秒)的pages。如果每个page都维护最近更新时间,开销会很大且扫描会很耗时,因此具体实现不会以page为粒度,而是按inode中记录的dirtying-time来计算。

6. 总结
文件缓存是一项重要的性能改进,在大多数情况下,读缓存在绝大多数情况下是有益无害的(程序可以直接从RAM中读取数据)。写缓存比较复杂,Linux内核将磁盘写入缓存,过段时间再异步将它们刷新到磁盘。这对加速磁盘I/O有很好的效果,但是当数据未写入磁盘时,丢失数据的可能性会增加。
————————————————
版权声明:本文为CSDN博主「奇小葩」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/u012489236/article/details/115579851

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值