【实践分享】如何把项目部署在没有网的国产虚拟机上的
依旧前言
比较新手向的一个新手向教程。
项目部署
裸奔
关于部署项目,我使用过的有两种方式,一种是直接裸奔。
比如打个jar包直接裸奔,日志就放在旁边直接跑。
nohup ./jdk-11.0.20.1+6/bin/java -XX:+PrintCommandLineFlags -Xms1024M -Xmx2048M -jar 稳我的项目.jar --spring.profiles.active=生产配置 >> app.log 2>&1 &
或者Python直接跑,这个是fastAPI框架的项目
nohup uvicorn main:app --host 0.0.0.0 --port 7301 --workers 1 > app.log 2>&1 &
裸奔的好处就是简单,尤其是项目不复杂的时候,jar包一扔(Python的话麻烦一点,需要打成zip,然后用unzip解压),然后命令一跑,项目就起来了。
简单也意味着管理比较困难,比如我部署完这个Python项目之后,我还得再看看是不是真的在后端起起来了。
补充一:项目可能没有真的启动# 看进程
ps -ef | grep java
ps -ef | grep uvicorn
# 然后看日志
tail -f app.log
补充二:Java项目得自己带JDK
上面我有写一个Java启动命令,里面的./jdk-11.0.20.1+6/bin/java就是指定Java的路径,裸奔一般是项目初期,或者是我验证功能的时候用的,干脆就把项目和JDK放在一起。
那就去掉这个直接跑,但是要确认,端口没有被强用,管理权限设置的环境变量你能用;如果不能用的话,你要自己指定。
镜像
另一种就是通过Docker,写好对应的Dockerfile,输出成镜像。
docker build
docker load
docker run
这种方式的好处是环境相对稳定。你需要的Python版本、系统依赖、项目代码、启动命令,都可以放进镜像里。对于没网的机器来说,这是比较好的办法。
但是也有几个问题。
补充一:必须在有网的虚拟机上把一切需要用的东西都打到Docker镜像里面比如你在Dockerfile里面写:
FROM python:3.12
在有网环境里没问题,构建的时候会自动去拉。但是到了没网的机器上,会直接报超时。所以基础镜像、项目镜像、中间件镜像,都要提前准备好。
补充二:你的目标是什么架构的虚拟机,你打Docker镜像就要去什么虚拟机上打你在Windows电脑或者x86_64服务器上构建的镜像,大概率是linux/amd64平台的。
如果你直接把这个Docker镜像放到ARM64/aarch64架构的虚拟机上run,是run不起来的。
所以说,你要把项目部署到对应架构的虚拟机上,你还得有这么个虚拟机才行。
补充三:镜像里面的系统依赖也必须是对应的比如你项目依赖OpenCV、PaddlePaddle、NumPy、FAISS、ONNX Runtime这类包,它们有些底层会依赖系统动态库。镜像能构建成功,不代表换个CPU架构也一样成功。
所以我一般在部署前会先查目标机器信息:
uname -m
cat /etc/os-release
java -version
python3 --version
docker version
这里最关键的是uname -m。如果输出是x86_64,一般对应amd64。如果输出是aarch64,一般对应arm64。
为什么要特地强调没网
大家在开发部署的过程中,或多或少会接触到内网的项目。
内网的项目是不能连接公网的,那么很多命令就都用不了了。
例如pip install/conda install、npm install/npm build或者Java项目的compile。
举两个比较典型的例子:
(1)你是一个依赖某些开源模型的Python项目,但是开源模型并没有下载到本地,比较常见的就是依赖PaddlePaddle的模型,结果到了内网项目直接用不了。
(2)另一个就是老项目,然后依赖的中间件或者插件升级了,然后不兼容,本机测试完全没问题,结果到了没网的虚拟机上直接不行了。
人话就是,项目要跑起来,但是我们的依赖没有,我们要的包没有,需要去网上下载,但是没网。
这里的“依赖”还不只是代码依赖。
很多时候它包括:
1. 语言运行时,比如JDK、Python、Node.js。
2. 项目依赖,比如jar包、wheel包、node_modules。
3. 系统依赖,比如libGL、libgomp、GCC、CMake、字体库。
4. 模型文件,比如PaddleOCR模型、Embedding模型、Transformers模型。
5. Docker基础镜像,比如Python、OpenJDK、Nginx、MySQL、Redis。
6. 中间件和配置,比如数据库驱动、证书、字体、时区文件。
所以离线部署的核心是““把项目运行需要的一整套东西拷过去”。
当然这个一般和Python项目关系最大,因为Python生态里很多包并不是纯代码包,里面会带C/C++扩展,会和CPU架构、Python版本、系统版本绑定。
这里比较典型的坑就是,你的本机有个无敌的Conda环境,结果到了虚拟机上,Conda里面什么包都没用。
Java相对好一些,尤其是Spring Boot这种fat jar,很多依赖会被打到jar包里面。
这个时候也有聪明的方法和笨的方法。
(1)把要用的包一个个拉下来
这个方法我以前用过,当时什么也不懂只能说。
1.我用pip freeze > requirements.txt生成依赖。
2.对着依赖,去官网下载wheel包。
3.把项目整个扔到没网的虚拟机上
4.python3 -m pip install xxx.whl安装一个又一个轮子
(额外提一嘴,可以给你的虚拟机是白的,上面连Python都没有,那你先安装Python)
5.python3 xxx.py跑项目
这里有很多个坑。
pip freeze > requirements.txt这个命令下载的依赖,他不会下载可选依赖!
举例:
下载requests包的时候,不会下载requests[socks]这个包。
其次,平台不匹配的包可能下不到。
你在x86_64机器上下载,默认会按当前机器环境解析。目标机器如果是ARM64/aarch64,可能下载到不能用的wheel包。
还有类似的,模型文件不会下载、系统依赖不会下载,下面会提到。
再有一个是,注意轮子的版本。
比如同样是numpy,它可能长这样:
numpy-1.26.4-cp310-cp310-manylinux_2_17_x86_64.whl
numpy-1.26.4-cp310-cp310-manylinux_2_17_aarch64.whl
好消息是,后来我也迭代了一下,有了个比较方便的wheel包下载:
pip freeze > requirements.txt
pip download -r requirements.txt -d wheels(直接拿下)
然后把requirements.txt和wheels目录一起拷到没网机器上,再离线安装:
pip install --no-index --find-links=./wheels -r requirements.txt
这里的--no-index意思是不要去公网找包,--find-links=./wheels意思是只从本地目录找包。
另外,模型类项目还要注意模型文件。
比如PaddleOCR、PP-Structure、Embedding服务、Transformers项目,经常第一次运行时会自动下载模型。本机运行没问题,是因为模型已经缓存在本机了;内网没网,就下不了。
这种项目要提前把模型目录准备好。比如有些项目会缓存到:
~/.paddleocr
~/.cache
~/.cache/huggingface
你要么在代码里指定本地模型路径,要么把缓存目录一起带过去。
(2)直接打成Docker镜像
以前这一步很困难的,因为Dockerfile怎么写需要抄一抄其他项目,如果你抄不到,那么恭喜你,要自己想办法了。
现在的话让AI生成一下就行,很简单。简单说一下流程:
# 在有网机器上构建镜像
docker build -t my-app:1.0 .
# 导出镜像
docker save -o my-app-1.0.tar my-app:1.0
# 把tar包拷贝到内网机器
# 在内网机器上导入镜像
docker load -i my-app-1.0.tar
# 启动容器
docker run -d --name my-app -p 7301:7301 my-app:1.0
如果是有中间件的项目,比如还要Redis、MySQL、Nginx,那这些镜像也要一起docker save出来。
但还是那句话,不要跨架构,不要跨架构,不要跨架构。
为什么要强调国产虚拟机
因为央国企都强调国产化部署,可能这个以后也是一种趋势。
为什么要说这个呢,因为不同的虚拟机架构是不一样的。
大家手上的Windows电脑,一般是x86_64/amd64架构的。
而国产虚拟机不这样,国产化虚拟机常见的是ARM64/aarch64架构,当然也有x86_64/amd64架构的。
在我们部署项目的时候,就必须考虑到兼容性,依赖的兼容性、架构的兼容性、代码的兼容性。
当然额外提醒一句,这里的兼容性一般针对Python项目,因为Java有自己的JVM,而且本来就是以兼容性著称的。
(1)依赖的兼容性
这里也是以Python为例啊。
比如同样是NumPy这个包,不同架构的虚拟机对应的依赖是不一样的,也就是说它们不是同用一个NumPy包。你在Windows或x86_64上下载好的包,拿到ARM64/aarch64的国产虚拟机上,很可能直接提示不支持。
有些包不是单纯的Python代码。它背后可能有.so动态库,可能有C++扩展,可能还要系统里安装某些运行库。比如图像处理、OCR、向量检索、模型推理这些。
常见报错如下:
not a supported wheel on this platform
cannot open shared object file
Illegal instruction
No module named xxx
not a supported wheel on this platform一般是wheel包和当前Python版本、系统或CPU架构不匹配。
cannot open shared object file一般是系统动态库缺失。
Illegal instruction是最常见的,它一般意思是是二进制包用了当前CPU不支持的指令集。比如在某些机器上,包是给支持AVX2的x86_64CPU编译的,ARM64/aarch64的就是不支持。
所以在项目部署前,要分三个方面看看,依赖有没有问题:
-
wheel标签
-
CPU架构
-
操作系统、动态库、字体、编译工具链。
以我踩过的一个坑为例,就算是打在Docker镜像里,我们的Python版本也要用兼容版的python:3.12-slim-bullseye,而不是直接用python:3.12。
(2)架构的兼容性
这里说的是代码调用的模型架构和虚拟机架构之间的兼容。
目前遇到一个比较典型的问题就是,在x86_64架构上跑的很快的Python项目,到ARM64/aarch64架构上跑的速度就慢了4-20倍不等。
最典型的一点就是,ARM64/aarch64的虚拟机没有AVX2。
严格说,AVX2是x86_64架构上的SIMD指令集,ARM64上对应的方向是NEON这类指令集。
很多主流Embedding、向量检索、科学计算、图像处理库,在x86_64上可能有AVX2优化,在ARM64上可能走NEON,也可能退回通用实现。
这个差距带来的影响就是:同一个Embedding服务,x86_64可能走AVX2优化内核,ARM64可能只走NEON,甚至退回通用实现。
当然这是针对CPU而言啊,GPU的话我用的比较少,因为GPU都被抢去部大模型了。
又或者说我们在CPU上跑PaddleOCR,或者PP-Structure。
实测,在同16C32G的虚拟机上。
x86_64的速度如果在5s左右,ARM64/aarch64的速度一般在30s左右。
所以如果项目里有模型推理,我至少测这几个东西:
1. 单次请求耗时。
2. 并发请求耗时。
3. CPU占用。
4. 内存占用。
5. 模型首次加载时间。(嫌麻烦可以不用,现在比较流行懒加载)
不要只测curl能不能返回。
这里也是一个坑,我们的接口一般是有超时时间的,无论是网关、代码还是哪里的设置,不测就等着超时吧。
(3)代码的兼容性
代码兼容性主要看代码里有没有写死环境假设。
比如路径分隔符、文件编码、临时目录、字体路径、模型路径、动态库路径、GPU/CPU判断、系统命令调用,这些都可能在换机器后出问题。常见问题有:
# 写死Windows路径
model_path = "D:\\models\\xxx"
# 写死Linux某个路径
font_path = "/usr/share/fonts/xxx.ttf"
# 直接调用系统命令
os.system("ps -ef | grep xxx")
这些代码在本机能跑,不代表在国产化虚拟机上也能跑。
超级典型的一个问题就是挂载NAS的时候,同样的虚拟机,读取中文的编码居然不一样!
这个问题当时排查了好久,才知道原来一个是GBK读取的,一个是UTF-8读取的。
省流
离线国产化部署,本质是复制代码+复制运行环境。
把依赖和架构(代码运行、代码的依赖的运行依赖的架构)都考虑在内才行。
还有,我在要不要给专有名词加``这个事情纠结了很久,最后还是决定加了。
吃个午饭,先发后改
3 个帖子 - 3 位参与者