GPU和fuse设备

如何在kubernetes容器中同时使用GPU和fuse设备

  • 问题介绍
  • 原因分析
  • 解决方案
    • fuse简单介绍
    • 容器中使用fuse的正确方法
    • 在k8s容器中使用fuse设备
      • /dev/fuse注入到容器
      • 在容器中已普通权限作挂载文件系统
        • Ubuntu
        • RHEL, CentOS

问题介绍

前段时间项目中碰到一个比较棘手的问题,同事在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提供了--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

deviceplugin kubelet apiserver scheduler 我代理fuse资源 我有fuse资源 同步 schedule r获得所有资源类 型以及在哪儿的信 息,根据资源请求 调度pod到对应 的节点 调度到对应的节点 allocate response DeviceRuntimeSpec kubelet根据Devic eRuntimeSpec把/ dev/fuse挂载到容器中 deviceplugin kubelet apiserver scheduler

实现代码已上传到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, 尝试了打开PodSecurityPolicy admission-controller后放弃,最后采用的办法是docker-default的基础上创建一个新的profile

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

创建k8s对象时就设置

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

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

mkdir /mnt/test
sshfs -o allow_other [email protected]:/root /mnt/test

RHEL, CentOS

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

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

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

$ grep CONFIG_SECCOMP= /boot/config-$(uname -r)
CONFIG_SECCOMP=y
  • 如果不支持,容器就可以随心所欲了?记住文章开始提到的capability,命名空间的隔离, docker缺省限制很多capability, 比如使用raw socket, 加载内核模块,重起机器等等, 处处把关 。

  • 如果支持,那还要看k8s的行为,到目前为止(1.12版本),seccomp还处于alpha状态,缺省是用unconfined配置, 也就是说不对k8s容器启用任何seccomp配置,等同于上面不支持的效果。

  • 如果想通过seccomp做一些定制的细粒度控制,那要启用PodSecurityPolicy admissionController并设置seccomp.security.alpha.kubernetes.io/allowedProfileNames, 官方文档里说支持docker/default, unconfined, localhost/ , 或者*这些格式。 其中path是kubelet的启动参数seccomp-profile-root指定的目录下的文件名,在对应的文件里设置想限制容器做哪些系统调用。
    最后在yaml文件里设置对应的profile

container.seccomp.security.beta.kubernetes.io/: localhost/

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

你可能感兴趣的:(GPU和fuse设备)