overlayfs挂载选项volatile

引入

最近在公司遇到了容器相关的线上问题, 简单来说就是K8s会出现PLEG not healthy的报错. 经过初步的排查, 发现是docker有个goroutine卡在了umount, 同时这个这个groutine会占用某个锁, 导致查询状态的handler里拿不到锁, 最终导致k8s层面的报错.

通过对umount事件的监控, 以及相关源码代码的阅读, 发现是overlayfs在umount的时候, 会对upper层所在的fs进行一次sync, 导致大量脏页回写. 如果这个机器内存较大, 并且有过频繁的IO, 那么就会脏页较多, overlayfs umount时等待磁盘IO完成而阻塞过久.

umount的stack trace如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
[<0>] wb_wait_for_completion+0x5a/0x90
[<0>] __writeback_inodes_sb_nr+0xa0/0xd0
[<0>] writeback_inodes_sb+0x3d/0x50
[<0>] _sync_filesystem+0x55/0x60
[<0>] sync_filesystem+0x33/0x50
[<0>] ovl_sync_fs+0x61/0xa0 [overlay]
[<0>] _sync_filesystem+0x33/0x60
[<0>] sync_filesystem+0x44/0x50
[<0>] generic_shutdown_super+0x27/0x120
[<0>] kill_anon_super+0x18/0x30
[<0>] deactivate_locked_super+0x3b/0x90
[<0>] deactivate_super+0x42/0x50
[<0>] cleanup_mnt+0x109/0x170
[<0>] _cleanup_mnt+0x12/0x20
[<0>] task_work_run+0x70/0xb0
[<0>] exit_to_user_mode_prepare+0x1b6/0x1c0
[<0>] syscall_exit_to_user_mode+0x27/0x50
[<0>] do_syscall_64+0x69/0xc0
[<0>] entry_SYSCALL_64_after_hwframe+0x61/0xcb

重点在这个ovl_sync_fs函数, 它会对整个overlayfs的uppser层所在文件系统进行sync操作:

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
// from kernel 5.10
/* Sync real dirty inodes in upper filesystem (if it exists) */
static int ovl_sync_fs(struct super_block *sb, int wait)
{
struct ovl_fs *ofs = sb->s_fs_info;
struct super_block *upper_sb;
int ret;

if (!ovl_upper_mnt(ofs))
return 0;

if (!ovl_should_sync(ofs))
return 0;
/*
* Not called for sync(2) call or an emergency sync (SB_I_SKIP_SYNC).
* All the super blocks will be iterated, including upper_sb.
*
* If this is a syncfs(2) call, then we do need to call
* sync_filesystem() on upper_sb, but enough if we do it when being
* called with wait == 1.
*/
if (!wait)
return 0;

// 找到upper层所在的fs
upper_sb = ovl_upper_mnt(ofs)->mnt_sb;

down_read(&upper_sb->s_umount);
// 执行sync, 会造成整个fs脏页回写磁盘, 耗时很长
ret = sync_filesystem(upper_sb);
up_read(&upper_sb->s_umount);

return ret;
}

很显然, 对于k8s的很多场景来说, sync文件系统是多余的操作. k8s每次实例启动都重新挂载rootfs, 实例退出后将rootfs umount并删除(可能描述的不对). 所以最好有办法能够避免overlayfs umount时候的强制刷盘.

解决方案

如果仔细查看ovl_sync_fs函数, 可以发现它会在函数开头执行两个判断, 一次是判断upper层是否存在, 一次是ovl_should_sync判断overlayfs是否应该进行sync. 解决问题的关键可能就在于ovl_should_sync能否绕过sync.

kernel

ovl_should_sync函数被包含在2020.8.31提的patch里

[PATCH v5] overlayfs: Provide a mount option “ volatile” to skip sync - Vivek Goyal

这个patch给overlayfs提供了一个新的挂载选项volatile, 当挂载这个选项后, overlayfs会去掉一些sync操作, 包括针对单独文件的sync以及整个文件系统的sync.

影响的地方如下:

  1. umount不会再强制对upper层文件系统执行sync, 也就是针对本次问题出现的场景.

  2. remount的时候, 也可能对upper层所在文件系统进行sync, 这是2020年加内核主线的patch.
    [PATCH] ovl: sync dirty data when remounting to ro mode ‒ Union Filesystem
    这是因为把overlayfs remount成只读之后, 在umount overlayfs时,kill_anon_super -> generic_shutdown_super -> sync_filesystem检查overlayfs为只读时会跳过sync_filesystem. 所以把overlayfs从可写remount成只读的时候, 直接进行一次sync_filesystem, 避免最终umount的时候遗漏sync_filesystem操作. volatile选项会取消这一次sync_filesystem操作.

  3. 针对单个文件fsync调用, 如果带有volatile挂载选项, 会跳过.

  4. 当文件copy up到upper层的时候, 也会进行vfs_fsync()操作. 如果带有volatile选项, 会跳过.

  5. O_SYNC的场景, 如果有volatile选项, 也会绕过sync退化成overlayfs默认的写行为.

本质上这些sync操作都是为了避免系统crash造成overlayfs磁盘数据丢失. volatile挂载选项和Kubernetes的使用场景十分契合. 如果内核在向overlayfs写入数据时崩溃, kubelet总是会重新创建新的容器, 而不会复用之前的rootfs. 因此,在 kubernetes中, 容器的rootfs是临时的. 在pod中使用 volatile 选项是安全的, 因为我们没有机会重复使用旧的rootfs. 在有状态容器中使用这种配置也是安全的, 因为需要持久化的数据理应写入外部卷, 在运行时不会受到volatile标志的影响.

contianerd

当然, 新的挂载选项需要runtime的支持, 才能够在挂载rootfs带上这个选项.

在contaienrd社区已经有了许多相关讨论:

这个pr [overlay] add configurable mount options to overlay snapshotter by dmcgowan · Pull Request #8676 允许对overlayfs的挂载选项进行设置, 并且被backport到了1.6.24.

requirements

总结一下, overlayfs volatile特性, 需要的版本如下:

containerd配置:

1
2
3
4
5
# /etc/containerd/config.toml
version = 2
[plugins]
[plugins."io.containerd.snapshotter.v1.overlayfs"]
mount_options = ["volatile"]

reference

  1. https://lore.kernel.org/all/20200722175024.GA608248@redhat.com/
  2. https://github.com/containerd/containerd/pull/8676
  3. https://fuweid.com/post/2023-08-sync-containerd-issue/
  4. https://www.redhat.com/sysadmin/container-volatile-overlay-mounts
  5. https://docs.kernel.org/filesystems/overlayfs.html#volatile-mount