如何在kubernetes容器中同时使用GPU和fuse设备
问题介绍
前段时间项目中碰到一个比较棘手的问题,同事在k8s平台上使用GPU做选练,训练数据存放在ceph对象共享存储中,为了方便我们使用了s3fs(https://github.com/s3fs-fuse/s3fs-fuse)来把对象存储挂载到容器本地文件系统中,这样训练代码就可以像访问本地文件系统一样。有时训练可以正常进行,但有时重起k8s任务后训练就会失败,报GPU访问错误。通过这篇文章分享下问题的解决思路,希望对其他朋友有帮助。
原因分析
简单分析发现创建k8s任务时只申请了一个GPU,但从tensorflow错误日志看到应用启动时检测到了4个GPU,进一步分析原因是使用s3fs挂载时设置了privileged=true权限,导致容器中可以访问整个host上的GPU,如果这些GPU都空闲着,训练不会出错,但是如果某些GPU已经被k8s分配给了其他的pod并且正在使用中,这样就会出现一些冲突或者显存不足的问题。
这个问题可以通过把对象存储里的东西挪到cephfs里,但是数据量太大,而且失去了在存储层面进行用户隔离的功能,或者调用S3 api直接访问对象,但这样对用户不友好,没有直接访问文件方便。有没有更好的办法?
解决方案
fuse简单介绍
fuse通过在user space运行一个daemon代理通过/dev/fuse这个虚拟设备和kernel打交道,从而使多种用户态文件系统在不改变kernel的前提下成为可能,比如sshfs, 或者本文中碰到的s3fs, /dev/fuse是在加载fuse内核后自动生成的
容器中使用fuse的正确方法
如果要在容器中挂在sshfs或者s3fs这样的文件系统,必须能访问主机的/dev/fuse设备,而且要能使用mount所涉及到的syscall。
一个比较简单的做法就是给容器privileged权限,这样的做法有很多弊端:
- 违背了容器的安全隔离性原则,相当于容器可以访问主机kernel的所有功能,容器中运行的恶意代码可能导致整个主机出问题
- 容器可以看到主机上的所有设备,比如GPU导致tensorflow这样的计算框架出错
docker借助于一些kernel内部的安全方案可以做到更细粒度的控制,比如apparmor, selinux, seccomp, linux capability 具体要看主机系统是哪个linux发行版,以及kernel配置中是否启用了对应的功能(grep -i armor /boot/config-‘uname -r’), 具体可以查看相应的功能介绍,这里不详细展开。 通过这些kernel自带的功能可以做到非privileged访问/dev/fuse设备:
- 在ubuntu运行如下命令
docker run -it --rm --device /dev/fuse --security-opt apparmor:unconfined --cap-add SYS_ADMIN image-registry:5000/ubuntu:16.04-sshfs /bin/bash
通过aa-status
可以看到系统中定义的profile,以及profile是enforce还是complain模式, profile中定义可以使用哪些功能,比如网络访问,capability,mount,对文件的读写访问权限。
docker daemon启动时会创建缺省的docker-default profile,如果不特别指定会使用这个profile,使用apparmor:unconfined表示禁用该限制功能。
- RHEL, CentOS运行如下命令
docker run -it --rm --device /dev/fuse --security-opt seccomp:unconfined --cap-add SYS_ADMIN image-registry:5000/ubuntu:16.04-sshfs /bin/bash
注意rhel上可能还会用到selinux,也可以做一些类似的设置。--device /dev/fuse
就是说把host上的/dev/fuse设备挂载到容器中,--cap-add SYS_ADMIN
允许容器中运行的进程执行系统管理任务,如挂载/卸载文件系统,设置磁盘配额,开/关交换设备和文件等。
在k8s容器中使用fuse设备
通过前面的介绍我们大致已经能想到问题的解决思路:
- 把/dev/fuse设备注入到容器
- 给容器分配尽可能少的权限,比如可以执行mount命令挂载文件系统,避免使用privileged访问到所有的GPU
但是因为k8s做了一层转化,事情没有想象的直接,下面分别作介绍
/dev/fuse注入到容器
docker提供了