背景

最近写了个系统,后端部分拆成了两个端,

  • Golang写的Center,负责调度任务、登录鉴权等基本操作。

  • Python写的Node,负责计算Center分配的任务。

显然,Python写的Node需要分布式部署计算才快(要不然Center还调度个啥)

Python我已经使用了uv 来管理环境,git 管理代码,但是一些东西不能上传git(模型之类的),而且图方便,像GPT-Sovits直接从GitHub下载了整合包,这些文件只在我的开发机上存在。

显然,部署起来很麻烦,而且分布式需要我在各个服务器上做重复工作。

有没有一种方法,可以方便地在每台电脑上部署呢?

当然有!(废话)

我可以直接在开发机上把项目复制过去!

显然,复制东西太不优雅了。

这种情况下,还是主流解决方案,Docker!

(下面是在Windows的Docker上测试的,但是并没有用图形界面,所以Linux也完全一样)

Docker中文文档:https://yeasy.gitbook.io/docker_practice

Windows Docker Desktop注意事项

Docker Desktop总是会安装在C:\Program Files\Docker ,存储数据总是在C:\Users\H3C\AppData\Local\Docker

据测试,Docker Desktop似乎是不能用任何方法移动到其他地方的,但是可以通过软链接的方式使其数据放在其他地方。

数据也是,而且动不动就几十上百G,这个可以在Docker装好后修改,也可以软链接。

什么?你不会用mklink命令建软链接?试试我在Github瞟见的这个工具

如果构建镜像时硬盘用完了,实测会报一个错误,需要重启机器后删除存储文件才能再次启动Docker(放心删,会再创建,但是一切容器和镜像都会丢失。你该不会在开发用的机器上部署了生产的东西吧?)(我不知道多等等能不能解决)

为了避免硬盘用完,构建错的镜像及时删除,即使清除构建缓存:

docker builder prune

构建镜像

首先明确一点,宿主机可以是任何Linux发行版,Ubuntu啦、CentOS啦、Debian啦,它们本质上是在Linux内核上加了自己的各种工具。但Docker不需要这些工具,Docker创建出来的容器也不允许你使用宿主机上的工具。

构建镜像可以从零开始,也可以从基于某个发行版镜像、别人的构建好的镜像。

如果基于了某个发行版镜像,那么Docker容器内就可以使用这个发行版的各种工具,发行版镜像类型和宿主机发行版类型没有任何关系。

选一个基础镜像

DockerHub是个好地方。

测试一遍构建镜像的流程

构建镜像一般通过Dockerfile,但是不经过测试直接写Dockerfile,万一失败了就得从头重新跑,先预演一下是很好的选择。

参考文档

使用下面的命令,创建一个容器,然后进入它的Shell:

docker run -it --gpus all --entrypoint bash 镜像名

使用下面的命令可以看到所有容器:

docker ps -a

如果你不小心停止了它(没停止也能再次进入终端),那么可以再次启动它:

docker start -ai 容器ID或名称

如果它正在跑(所有容器都适用),可以让它再开一个终端:

docker exec -it 容器ID或名称 bash

于是可以像下面这样,创建容器并导入当前项目的文件进行测试:

D:\project\MetaSpeaker_Node>docker run -it --gpus all --entrypoint bash ubuntu:20.04
root@81c2a5779eb9:/# exit
D:\project\MetaSpeaker_Node>docker cp ./checkpoints  81c2a5779eb9:/app/checkpoints
Successfully copied 993MB to 81c2a5779eb9:/app/checkpoints
D:\project\MetaSpeaker_Node>docker start -ai 81c2a5779eb9 
root@81c2a5779eb9:/# cd /app/checkpoints
root@81c2a5779eb9:/app/checkpoints# ls -l
total 969636
-rwxr-xr-x 1 root root 348632874 Apr  4 04:22 GFPGANv1.3.pth
-rwxr-xr-x 1 root root  85331193 Jul  6 05:48 parsing_parsenet.pth
-rwxr-xr-x 1 root root  15217721 Apr  4 05:38 rvm_mobilenetv3.pth
-rwxr-xr-x 1 root root 107905875 Apr  4 05:38 rvm_resnet50.pth
-rwxr-xr-x 1 root root 435807851 Apr  3 14:38 wav2lip.pth
root@81c2a5779eb9:/app/checkpoints# 

在完成所有操作后,可以使用docker commit 容器,把修改做成一个镜像。

但是一般来说不会这样做镜像,因为总不能每次版本更新都手动去做镜像吧,所以,这个容器测试过后直接删除即可,我们需要自动化的方式。

写Dockerfile

参考文档

虽然我是第一次使用Docker,但是不用多阶段构造我会很不舒服,把构造使用的工具打包进镜像我觉得很不舒服。

下面是导入当前目录,安装python环境,然后将产物拷贝到一个CUDA环境,供参考:

FROM ubuntu:20.04 AS builder

COPY . /app
WORKDIR /app
ENV PATH="/root/.local/bin:$PATH" DEBIAN_FRONTEND=noninteractive
RUN apt-get update && apt-get install -y curl git cmake build-essential&&\
    curl -LsSf https://astral.sh/uv/install.sh | sh
RUN uv sync


FROM nvidia/cuda:12.1.0-cudnn8-runtime-ubuntu20.04 AS runtime

RUN apt-get update && apt-get install -y ffmpeg

ENV GPU_ID=0 NODE_NAME="" CENTER_ADDRESS="" RUNNER_KEY=""
COPY --from=0 /app /app
COPY --from=0 /root/.local /root/.local
WORKDIR /app

ENTRYPOINT [".venv/bin/python","main.py"]

小贴士:不要像我这样做写Copy整个项目复制过去,非要这样也要把Dockerfile排除调,不然Copy的缓存总是不命中,反复修改Dockerfile测试时是很烦的···

构建

工作目录在Dockerfile所在目录,然后执行一条命令等着就行啦:

docker build . -t 镜像名:版本号

完成后就能用docker images 看到啦。

如果你不小心忘记写-t了,导致它的名字和版本都是<none>,可以使用下面的方法再打标签:

首先找到这个镜像的ID:docker images

然后执行:

docker tag 镜像ID 镜像名:版本号

部署

基础创建容器

最基本的创建命令(需要加其他选型配置其他东西):

docker run -it --gpus all 镜像ID或名字和版本号

其中对于用显卡的应用来说--gpus all 是必要的。

我觉得这样很麻烦,还要配置环境变量、卷、端口甚至网络之类的。

所以当然有更方便的方法啦~( ¯•ω•¯ )~

写docker-compose.yml

参考文档

示例文件:

version: "3"

services:
  MetaSpeaker_Node:
    image: ffeng123/metaspeaker_node:1.0
    ports: []
    volumes: []
    environment:
      - GPU_ID=0
      - NODE_NAME="本地计算单片"
      - CENTER_ADDRESS=""
      - RUNNER_KEY="XXX"
      - NVIDIA_VISIBLE_DEVICES=all
      - NVIDIA_DRIVER_CAPABILITIES=all
    deploy: {}
    runtime: nvidia

一键部署

工作目录在docker-compose.yml所在目录,执行一条命令:

docker-compose up

分发镜像

上面的所有操作都是在本机上进行的。

分布式部署,肯定要多机啦,分发镜像就是必要的啦。

往DockerHub上面提交就算啦,看看只在本地处理的方法:

# 导出
docker save -o XXX.tar 镜像名:版本号
# 导入
docker load -i XXX.tar

然后其他机器就可以愉快地一键部署啦~

临时环境

某些时候我很喜欢使用Google Colab,在我的笔记本上有一个8GB大小的内存盘,我觉得装环境太麻烦,而且如果只是临时使用,清理环境更麻烦,而且很多时候因为各种原因还装不上环境,于是,Docker现在成了我的测试环境、临时使用的首选。

对于需要用显卡的场景:

docker run -it --gpus all -p 6222:22 --entrypoint bash nvidia/cuda:12.6.0-cudnn-runtime-ubuntu20.04

不用显卡的话:

docker run -it -p 6222:22 --entrypoint bash ubuntu:20.04

进去之后装个ssh方便传文件。

apt update && apt install -y openssh-server net-tools
sed -i 's/#\?PermitRootLogin.*/PermitRootLogin yes/' /etc/ssh/sshd_config
sed -i 's/#\?PasswordAuthentication.*/PasswordAuthentication yes/' /etc/ssh/sshd_config
echo 'root:123456' | chpasswd
service ssh start
netstat -tlnp | grep ssh

看容器IP:docker inspect -f "{{ .NetworkSettings.IPAddress }}" 容器名 (并不需要)

IP创建的时候已经映射到 6222

注意Windows上Windows和WSL之间有一个网桥,这导致Windows无法使用容器的IP+端口访问。

总结

以前觉得永远用不到的东西也用到了呢。

咱也是为未来分布式打下基础了呢~

我能想到的,最大的成功就是无愧于自己的心。