k8s中使用s3fs和GPU冲突的解决方案

2018-12-15  本文已影响0人  橙橙爸爸

问题介绍

前段时间项目中碰到一个比较棘手的问题,同事在k8s平台上使用GPU做选练,训练数据存放在ceph对象存储中,为了方便我们使用了s3fs把对象存储挂载到容器文件系统中,这样训练代码就可以像访问本地文件系统一样操作了。但是问题是训练有时可以正常进行,重起pod后训练有时会失败,报GPU访问错误。通过这篇文章分享下问题的解决思路,希望对其他朋友有帮助。

原因分析

简单分析发现创建k8s任务时只申请了一个GPU,但从tensorflow错误日志看到应用启动时检测到了4个GPU,进一步分析原因是因为挂载s3fs时设置了privileged=true权限,导致容器中可以访问整个宿主机上的GPU,如果这些GPU都空闲着,训练不会出错,但是如果这中间某个GPU已经被k8s分配给了其他的pod,就会导致一些冲突。

这个问题有一些临时的变通方法:

有没有更好的办法?

解决方案

fuse简单介绍

fuse通过在user space运行一个daemon代理通过/dev/fuse这个虚拟设备和kernel打交道,从而使多种用户态文件系统在不改变kernel的前提下成为可能,比如sshfs, 或者本文中碰到的s3fs, /dev/fuse是在加载fuse内核模块后自动生成的。

容器中使用fuse的正确方法

如果要在容器中挂在sshfs或者s3fs这样的文件系统,必须能访问主机的/dev/fuse设备,而且要能使用mount所涉及到的syscall。

一个比较简单的做法就是给容器privileged权限,但这样的做法有很多弊端:

docker借助于内核内置的安全方案可以做到更细粒度的控制,比如apparmor, selinux, seccomp, linux capability 具体要看主机系统是哪个linux发行版,以及kernel配置中是否启用了对应的功能(grep -i armor /boot/config-'uname -r'), 具体可以查看相应的功能介绍,这里不详细展开。 通过这些kernel自带的功能可以做到非privileged访问/dev/fuse设备:

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表示禁用该限制功能。

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设备

通过前面的介绍我们大致已经能想到问题的解决思路:

但是因为k8s做了一层转化,事情没有想象的直接,下面分别作介绍

/dev/fuse注入到容器

docker提供了--device选项做到这个功能,但是查遍了k8s的文档,在网上也搜了一圈没有发现,这里还要提一下我们之前的一个错误做法:

    volumeMounts:
    - mountPath: /dev/fuse
      name: fuse
 ...
  volumes:
  - hostPath:
      path: /dev/fuse
      type: File | CharDevice
    name: fuse

其实这样是不生效的,真正在后台起作用的是设置的privileged.

后来联想到GPU是如何借助device-plugin功能不需要超级权限挂载到容器中的k8s中的,虽然GPU是通过设置环境变量NVIDIA_VISIBLE_DEVICES,然后nvidia-docker做了些特殊处理,和我们的情况不完全一样,但是从k8s代码看接口中确实提供了另一个选项可以直接设置pluginapi.DeviceRuntimeSpec结构的Device字段,抱着试试的想法,大功告成。

具体做法就是开发一个device plugin并以daemonset的形式部署在每个节点上,plugin会向kubelet注册所提供的资源类型,这里的资源定义为github.com/fuse

seq.JPG

实现代码已上传到https://github.com/JasonChenY/fuse-device-plugin

在容器中以普通权限作挂载文件系统

ubuntu

从k8s官方文档可以看到通过注解annotations: container.apparmor.security.beta.kubernetes.io可以控制使用哪个apparmor profile, 但是模仿docker尝试设置unconfined时报invalid,阅读K8s代码发现只支持localhost/xxxruntime/default这两种语法,不支持unconfined。其实runtime/default在启用apparmor时就对应docker的docker-default, docker-default这个profile的设置中有一条是不允许在容器中进行mount操作, 知道这个后问题就迎刃而解了,在docker-default的基础上创建一个新的profile

cat /etc/apparmor.d/docker | sed 's/deny mount/# deny mount/' | sed 's/profile docker-default/profile docker-fuse-2/' > /etc/apparmor.d/docker-fuse
apparmor_parser -a /etc/apparmor.d/docker-fuse

然后创建k8s对象时就设置

container.apparmor.security.beta.kubernetes.io/<container name>: localhost/docker-fuse

结合前面已经挂载的fuse设备就可以挂载文件系统了

mkdir /mnt/test
sshfs -o allow_other root@10.0.0.62:/root /mnt/test

RHEL, CentOS

SeLinux基于用户-角色-类型这个模型来运作的,类型系统是它的核心,可以做到非常细粒度的安全控制,f但是配置偏复杂,弄得不好会把IT自己搞死,所以一般都处于关闭状态,除非核心的关键服务器,这里不展开。

seccomp通过配置文件来限定可以使用哪些系统调用,docker也有个默认的docker/default,禁用了300多个系统调用里的40多个,确保容器里运行的进程不会对宿主系统产生安全威胁。

首先可以确认系统是否支持该功能

$ grep CONFIG_SECCOMP= /boot/config-$(uname -r)
CONFIG_SECCOMP=y
container.seccomp.security.beta.kubernetes.io/<container name>: localhost/<path>

我们的平台属于第二种情况用unconfined, seccomp没生效,所以直接使用fuse-device-plugin就成功了,也没有对第三点作实际验证,若有错误勿拍。

上一篇 下一篇

猜你喜欢

热点阅读