冷饭重炒
这其实是在19年就应该发出来的一点笔记,只是以前都存在自己脑子里,而且现在随着版本的变动,配置上也有了一点点小变化,所以炒一下冷饭。
需求是什么
在一个小公司里面,如何为算法工程师分配GPU资源是个问题。大厂当然有成熟的k8s+各种gpu虚拟化黑科技。但很显然,这需要完整的体系,从用户认证到临时存储、持久化存储、日志存储、资源配额、资源用量统计、任务队列等等的配套,才能结合k8s的调度一起来完成GPU资源分配。 在2019年的时候,服务器少,人也少,项目和任务单一,所以那时候只搞了服务器端的ldap认证,不存在什么资源协调问题,所以裸机直接上了。 到了20年,机器增多,人也增多,任务和环境的诉求也不一样,这个时候,再走裸机,很显然,无论从资源利用率、数据安全、环境冲突(即使有Anaconda和virtualenv)这样的环境管理,也无法在延续之前的裸机方式了。
所以呢,准备基于docker容器特性,来实现GPU资源的交付和隔离。那就意味着,在容器内,涉及容器内ssh服务(算法工程师大多喜欢通过vscode直连服务器ssh)、notebook的认证,延续之前的ldap认证。
规划是这样的:
-
采用nvidia-container-toolkit结合docker交付GPU资源
-
建设内部镜像库,涵盖常见的发行版和cuda版本;
-
基于内部镜像库build每个工程师的容器镜像
-
每个工程师的工程目录,即ldap里面账户的/home目录,一般在/home/username 下面
-
每个工程师能够访问的数据通过挂载host上的目录透传到容器中
-
统一管理容器内的时间戳、认证、权限(sudoers)
实际的操作
- 容器镜像build的时候安装openssh-server、生成ssh服务所需要的key文件
- 容器镜像build的时候,安装jupyterhub和jupyterhub-ldapauthenticator 和nodejs、npm包configurable-http-proxy
- 容器镜像build的时候安装nss-pam-ldapd 认证模块
- 创建容器
docker run -d --rm=false --net=host --restart=unless-stopped --name username --shm-size=8g (cat /etc/hosts|awk -F ' ' '{if(NR>2){print "--add-host "$2":"$1}}') -it -v /data-directory:/data-directory -v /home/username:/home/username -v /var/log/username:/var/log/username -v /etc/jupyterhub/username_config.py:/etc/jupyterhub/username_config.py -e TZ=Asia/Shanghai -v /etc/pam.d:/etc/pam.d:ro -v /etc/nslcd.conf:/etc/nslcd.conf:ro -v /etc/nsswitch.conf:/etc/nsswitch.conf:ro -v /var/run/nslcd:/var/run/nslcd -v /etc/passwd:/etc/passwd:ro -v /etc/group:/etc/group:ro -v /etc/shadow:/etc/shadow:ro -v /etc/gshadow:/etc/gshadow:ro -v /etc/sudoers:/etc/sudoers:ro --runtime nvidia --privileged image:tag /home/username/start.sh
几个特殊的挂载点和参数,解释一下:
–net=host 选定特定的网络,如果选择桥接,则需要确保容器内hosts文件的内容,并需要将该容器ssh、notebook所使用的端口(分配在/home/username/start.sh 和/etc/jupyterhub/username_config.py中配置)以-p形式暴露出来。
–name username ldap认证里面的用户名直接当容器名字,以便于识别,同用户容器可以在后续跟上编号
(cat /etc/hosts|awk -F ' ' ‘{if(NR>2){print “–add-host “$2”:"$1}}') 当网络模式是hosts时,把宿主机hosts文件添加到容器内hosts文件,不然容器暴露的端口jupyter hub的http proxy会反代失败,如果网络模式是桥接,则不需要此命令,但建议 –add-host 确保localhost=127.0.0.1
/etc/jupyterhub/username_config.py:/etc/jupyterhub/username_config.py 把该用户的jupyterhub配置文件透传给容器
-e TZ=Asia/Shanghai 确保容器内外时区一致
-v /etc/pam.d:/etc/pam.d:ro -v /etc/nslcd.conf:/etc/nslcd.conf:ro -v /etc/nsswitch.conf:/etc/nsswitch.conf:ro -v /var/run/nslcd:/var/run/nslcd -v /etc/passwd:/etc/passwd:ro -v /etc/group:/etc/group:ro -v /etc/shadow:/etc/shadow:ro -v /etc/gshadow:/etc/gshadow:ro -v /etc/sudoers:/etc/sudoers:ro 非常重要的一段,把hosts上认证相关的内容以只读形式挂载给容器
- start.sh的内容
#!/bin/bash
set -e
ldconfig
#python3 auto_restart.py >> ./nohup.out #用于故障时候排错
/usr/sbin/sshd -p 端口号 &&
jupyterhub --config=/etc/jupyterhub/username_config.py --no-ssl >> /var/log/username/username.log
- jupyterhub的配置
c.JupyterHub.ip = 'IP,如果容器网络模式是host,那么直接填写host IP,如果是birdge,则为127.0.0.1即可或不配置此项'
c.JupyterHub.port = 端口号
c.Spawner.ip = 'IP,如果容器网络模式是host,那么直接填写host IP,如果是birdge,则为127.0.0.1即可或不配置此项'
c.JupyterHub.authenticator_class = 'ldapauthenticator.LDAPAuthenticator'
c.LDAPAuthenticator.server_address = "ldap://服务器IP或域名:ldap服务端口号,默认389"
c.LDAPAuthenticator.bind_dn_template = 'uid={username},ou=people,dc=test,dc=com'
- 启动sshd服务
启动之前,先生成sshd key文件
ssh-keygen -t rsa -f /etc/ssh/ssh_host_rsa_key
ssh-keygen -t dsa -f /etc/ssh/ssh_host_dsa_key
ssh-keygen -t ecdsa -f /etc/ssh/ssh_host_ecdsa_key
ssh-keygen -t ed25519 -f /etc/ssh/ssh_host_ed25519_key
然后再start.sh里面
/usr/sbin/sshd -p 端口号 #### centos路径