暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

dirty writeback 机制的优化与演进

术道经纬 2021-05-14
685

前面文章在介绍writeback时,提到了dirtyable memory的概念,来看下它的计算方式:

大概就是空闲内存加上page cache,再减去totalreserve,和available memory的计算比较类似。当以dirtyable memory作为ratio的基数计算得到的“脏页面”数目超过dirty limit,进程将阻塞进行同步的writeback,以避免其继续产生更多的dirty page。

但是,由于同步的writeback可能发生在很多的代码执行点,这可能造成kernel stack的overflow。此外,如果进程产生dirty page的速度过快,还是会给I/O造成很大的负担。


一个改进的做法是将「同步」变为「异步」:超过阈值后,强制进程(dirtier)休眠,由background的flusher thread来处理writeback。就是不管是内存回收引起的,还是周期性触发的,亦或dirty page超限的,最后通通交给flusher thread。


包括direct reclaim和kswapd在内的内存回收,线程都是将flusher thread加入workqueue后唤醒,自己调用cond_resched()暂时让出CPU,并不休眠。而对dirtier不能采用这种方式,因此休眠时间的度量就成了一个新的问题。目前的公式大概是这样的:


公式 1


产生的dirty page数量越多(对应公式中的"pages_dirtied"),需要消化的时间也越长,sleep时间也应越长。


dirtier和flusher线程的关系其实可以用consumer-producer模型来描述,producer向管道里输入,consumer从管道里获取。管道是否拥塞,和管道的口径密不可分,具体说来就是I/O的吞吐量,比如SSD(假设如下图bdi设备编号8:0)通常就比USB(假设如下图bdi设备编号8:48)的读写速度更快,吸收dirty page的能力就更强。

并且,同样口径的管道,一个dirtier往里面写(上图的task C),和两个dirtiers往里面写(上图的task A和task B),是不一样的。不过这些信息内核事先是不知道的,得先观察一段时间,才能估算出bdi的bandwidth和"N"值,进而划给每个任务1/N的bandwidth。


由于这些信息是随时在变化的,因此每隔"HZ/5"时间需要根据过去这段时间的处理能力(对应下面公式的"written"),重新估算,如果以HZ为1000计,那么间隔周期约为200ms(对应"elapsed"),并且采用滑动平均的算法进行调整("period" 表示的范围为最接近"3*HZ"的2的幂并向上取整,应该是4096,差不多4s的样子,所以最近的新值占约1/20的权重)。


公式 2


为了让调控达到更平滑的效果,在公式 1计算sleep时间的分子中,还引入了一个"pos_ratio"作为矫正系数。

每个bdi有一个理想的最佳值setpoint,当偏离setpoint的时候,会尝试把它拉回来,偏离的越多,拉的越狠。

具体实现依靠的是一个三阶多项式,「三阶」其实是提供了一种正负的反馈控制(猜想是因为patch作者吴峰光老师的自动化专业背景么……)

sleep时间太长会影响任务的响应,比较合适的是10ms,最多HZ/5。如果进程被判断为需要sleep,将被设为"TASK_KILLABLE"状态,然后唤醒flush线程。

往Linux的内存管理模块添加代码牵涉众多,而且page writeback功能除了和具体的文件系统有关(像NFS啊,FUSE啊),还和不同的存储介质有关(从SSD到USB),存在各种overlay,各种combination(矩阵式的),需要面对不同场景做大量的调测。为此作者花费了相当多的时间,最后还催生了"lkp-tests"这套性能测试工具。

融入cgroup

为了实现更精细的控制,writeback模块也引入了cgroup功能。超过了所在cgroup的thresh不行,超过了global的thresh也不行,两者谁更小谁更先发挥限制作用。


不过比较麻烦的是,writeback既和memory有关,也和磁盘block有关,而在cgroup v1中,memcg和blkcg是分离的,两者没有直接的关系。此外,writeback是以inode为控制单元的,考虑到磁盘I/O的locality,flusher线程会将同一个文件的dirty page一起处理。而memory controller是基于page的,怎么协调呢?


一个相对简单的做法是计算这个inode对应的page归属那个memcg最多,就让这个memcg成为这个inode的"owner",所有关于此inode的writeback都算到对应owner的头上。但是情况是动态变化的,有可能过一段时间,一个memcg拥有一个inode的page数目已经不占多数了,那就成了冤大头了。


这种做法并不完美,但如果要实现更精准的accounting,所增加的复杂度可能让结果得不偿失。随着cgroup本身的发展,比如以进程group自身,而不是特性subsystem为组织方式的v2,有望让这一问题得到进一步的优化。


文章转载自术道经纬,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论