1. 写在前面
之前使用 C 语言开发程序时曾经接触过简单的跨处理器架构部署,交叉编译可以实现在当前处理器架构下编译运行于另一处理器架构之上的目标程序,当时的实践是在基于 amd64 处理器架构的 Ubuntu 20.04 操作系统中借助 gcc-aarch64-linux-gnu 工具经交叉编译得到运行在基于 arm64 处理器架构的 Centos7 操作系统中的程序。但其中涉及到的问题并不仅限于此,还有操作系统版本差异导致的 glibc 等基础库版本不同造成的冲突问题,在此不作详细展开,但吸取的教训是深刻的:涉及多人开发的工程,在部署任务时一定要同步开发所使用的处理器架构、操作系统及其版本、基础库和软件的版本等。
这次任务的一部分内容是将在基于 amd64 处理器架构的 Windows 操作系统中开发的 python 工程迁移至基于 arm64 处理器架构的 Ubuntu 20.04 操作系统中,共尝试了两种方案:
- 基于 conda 的 python 环境隔离部署;
- 基于 docker 的容器化部署。
期间还遇到了不同 python 版本之间的兼容性问题,在此一并记录之。
2. 前置步骤(同事完成,供参考)
由于目标服务器处于离线环境,且有且仅有目标服务器为 arm64 处理器架构,因此部分操作须提前在联网主机上完成,且须留意依赖的运行架构平台。
2.1 准备 requirements.txt
requirements.txt 为依赖清单,其基本格式为每行一个依赖包,通常固定版本号以保证离线环境稳定性:
# 示例内容
numpy==1.26.4
pandas==2.2.2
scipy==1.14.1
在联网主机上,执行
pip freeze > requirements.txt会自动生成当前环境的全部依赖(含版本号)。
2.2 离线依赖包下载
在联网主机上,执行以下命令下载适配 arm64 架构的依赖包(.whl 格式):
# 创建一个目录存放离线包
$ mkdir offline_pkgs
# 下载所有依赖,确保平台为ARM64
$ pip download \
-r requirements.txt \
-d offline_pkgs \
--platform manylinux2014_aarch64 \
--python-version 3.14 \
--only-binary=:all:
将 requirements.txt 、 offline_pkgs/ 以及 python 工程传输至离线服务器备用。
3. 基于 conda 的 python 环境隔离部署
离线服务器已预置了 26.2.1 版本的 conda 和 3.13.2 版本的 python,但由于 python 工程使用 3.14.6 版本的 python 开发。起初担心版本兼容性问题,在 Anaconda官方仓库 下载了适用于 arm64 架构的 python-3.14.6-hb85140a_101_cp314.tar.bz2 并传输至离线服务器。
创建新环境:
$ conda create --name py314 --offline
激活环境:
$ conda activate py314
从本地文件安装指定版本的 python:
$ conda install /path/to/downloaded/python-3.14.6-hb85140a_101_cp314.tar.bz2 --offline
完成安装后,可以执行 python --version 验证版本。
这里遇到一个问题:新安装的
py314环境中没有自带pip,执行pip install时默认使用的是 Ubuntu 20.04 自带 python 3.8 的低版本pip程序,其无法识别高版本的 python 依赖(写到这里突然想到,如果是用conda install命令可不可行?)。鉴于此,尝试性地在具备pip的3.13.2版本 python 的 conda 环境下进行部署,使用pip download重新下载适配3.13.2版本 python 的依赖包,并迁移至离线服务器。
在 conda 环境中安装离线依赖包:
$ pip install --no-index --find-links=./path/to/whl_pkgs/ *.whl
在 python 工程目录下运行程序,程序成功运行。
该部署方案能够成功的前提应该是既有 conda 内较新的 python 版本(
3.13.2)与开发程序所使用的 python 版本(3.14.6)版本差别不大,否则预期在处理不同依赖的版本时会遇到更多问题。
4. 基于 docker 的容器化部署
该方案其实是一早就确定的部署方式,但期间受制于一些因素暂时搁置,好在最终也是实现了这种方案,并且也是实际部署方式。
4.1 docker 基础镜像下载(同事完成)
在 docker hub 上下载了 python:3.14.7-trixie 镜像,使用 docker save 打包成 tar 归档文件后,迁移至离线服务器,再使用 docker load 加载镜像至本地。
python:3.14.7-trixie 内置了对应版本的 pip,因此可以在 Dockerfile 中指定构建镜像时执行 pip install 命令。
4.2 编写 .dockerignore 文件
在工程根目录下创建并编写 .dockerignore 文件,目的是尽可能缩小构建的镜像体积。.dockerignore 文件内容参考:
__pycache__
*.pyc
.git
.env
data/
Dockerfile
.dockerignore
这里排除 data/ 是因为目录体积较大,且不排除后续更新源数据的可能,因此将采用挂载的方式将存放数据的目录映射到容器内部。
4.3 编写 Dockerfile 文件
在工程根目录下创建并编写 Dockerfile,文件内容参考:
# 基础镜像
FROM python:3.14.7-trixie
# 设置环境变量:保证屏幕日志实时打印
ENV PYTHONUNBUFFERED=1
# 设置工作目录
WORKDIR /app
# 1. 复制离线依赖包(构建时用)
COPY offline_packages /offline_packages
COPY requirements.txt .
# 2. 离线安装依赖(装完立即删除临时包,减小镜像体积)
RUN pip install --no-cache-dir --no-index --find-links=/offline_packages -r requirements.txt \
&& rm -rf /offline_packages
# 3. 只复制代码文件,不复制 data 文件夹
# 如果代码有多个 .py 文件或子模块,可以 COPY . . 但确保 .dockerignore 排除了 data/
COPY *.py ./
# 如果还有子文件夹(如 utils/、models/ 等),按需复制
# COPY utils/ ./utils/
# 启动命令
CMD ["python", "main.py", "--log_active_slots"]
由于数据不打包,Dockerfile 只需复制代码文件。工作目录设置为 /app,由于代码中写死了从 ./data 中读取,因此需要在启动容器时将源数据目录挂载到 /app/data,才能被程序读取到。
4.4 构建 docker 镜像
在 Dockerfile 同级目录下执行如下命令构建镜像:
$ docker build -t my_image .
完成构建后,执行 docker images 应能查看到 my_image 镜像信息。
4.5 运行 docker 容器
执行以如下命令以启动容器:
$ docker run --rm -v /path/to/data:/app/data -v /path/to/log:/app/logs -v /path/to/config.json:/app/config.json my_image
其中:
-
--rm参数指示在容器停止运行后自动删除容器; -
-v /path/to/data:/app/data将源数据目录挂载至容器内; -
-v /path/to/log:/app/logs将容器运行产生的日志同步至宿主机指定目录; -
-v /path/to/config.json:/app/config.json将配置文件挂载至容器内。
容器启动会自动执行写在 Dockerfile 末尾的命令 python main.py --log_active_slots,程序正常运行。
由于启动指令较长,考虑将其存为 bash 文件便捷启动:
$ echo "docker run --rm -v /path/to/data:/app/data -v /path/to/log:/app/logs -v /path/to/config.json:/app/config.json my_image" >> run.sh
$ chmod +x run.sh
这样,进入 run.sh 所在目录,执行如下命令即可一键启动:
$ ./run.sh
5. 写在最后
之前是在摸索 OpenAirInterface 的时候接触了 docker,由于项目提供了 docker compose 的脚本,因此虽然知道 docker 是个有用的好技术,但并没有相对系统性地学习过。这次正好有契机和需求,于是稍微深入了解了下,自己手搓了第 4 节中的 Dockerfile,虽然也并不复杂,但一次性写成并且能用带来的成就感还是蛮足的。
受项目驱动,后续大概率还会接触 docker 容器间通信的内容,同时当前已经初步涉及的关于挂载的内容暂时还不甚明晰,还需要进一步学习,这些内容留待之后着手去学的时候再写吧~