导入
近来看了不少python的开源项目,大多是个人项目,所以有个问题比较突出,就是可复现性很多都比较差。虽然很多稍微调试下也都能跑通,但是并不是我懒得调试,而是不能直接运行的项目不能算是一个健全的项目。如果是生产环境下,要求就更高了。我敢说就连他们自己过段时间都不能跑通自己的代码,因为他们的电脑环境也在不断变化,python版本,第三方库版本的变化都可能影响项目的跑通。
虚拟环境
对于个人python项目,除非是一个文件的简单脚本,只要是稍微有点代码量的项目,我都是建议进入代码阶段之前一定要先创建一个虚拟环境,就用python自带的标准库venv
就行,一个项目一个环境,保证哪怕几年后,你还能正常跑通这个项目。还有anaconda的虚拟环境,我更倾向于是全局环境,比如一个全局的特定python版本,或者pytorch,tensorflow版本,如果针对每个项目都创建一个anaconda环境,那么你可能过段时间都不知道哪个环境对应哪个项目了,因为太多了,而anaconda环境是在anaconda的安装目录的。因此我更建议使用轻量话的 venv
标准库,venv
创建的环境是在项目目录中的,永远不会混淆。
关于venv
的具体用法就不多说了,非常简单,关键就是有没有随时创建独立虚拟环境的这种习惯。
创建虚拟环境的另一个好处是发布时候很方便,因为虚拟环境中使用pip freeze
生成依赖包,也就是requirement.txt
非常的准确,不用虚拟环境的话,使用pip freeze
是会包含一些没有用到的库的。发布项目,比如上传github,一定要带依赖包requirement.txt
,并且要精确使用库的具体版本,这样别人才好复现,只需要把项目下载到本地,创建虚拟环境,然后在激活的环境中安装requirement.txt
中的依赖即可。
anaconda的虚拟环境中可以使用conda env export > environment.yml
导入yaml文件的环境配置,然后其他人只需要利用这个yaml文件创建conda环境即可,比如conda env create -f environment.yml
docker
虚拟环境更多的还是自己以后复现自己的代码简单,不至于过段时间就突然跑不通了。发布时候只需要写好requirement.txt
,并且精准到版本,一般也没有太大问题。但是真正的产品级发布还是要用docker。docker容器技术是虚拟化技术的一种,所有的虚拟化技术的本质都是资源隔离,我不会在这篇博客中过多介绍docker,我只能说,docker一定是建议所有开发者都要学习的,并且docker本身并不难。用了docker就完全解决了一个问题”明明我的电脑上可以运行的“。
我简单的拿一个小python脚本举例如何使用docker发布项目:
import requests
url = "https://api.bilibili.com/pgc/web/timeline?types=1&before=6&after=6"
headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) "
"Chrome/89.0.4389.114 Safari/537.36",
}
response = requests.get(url=url, headers=headers)
data_json = response.json()
for item in data_json["result"]:
tmp = []
tmp.append(item["date"])
tmp.append(item["day_of_week"])
for episode in item["episodes"]:
tmp.append(episode["title"])
tmp.append(episode["pub_index"])
tmp.append(episode["pub_time"])
print(tmp)
这是一个简单的爬取动漫时间表的小脚本,这个脚本用来测试docker其实不太好,跟准确来说用来发布不太好,因为用到了api,而这个api不知道什么时候就会失效,这时候即使用docker也运行不了,不过这里只是随便找个例子。
我们只需要在这个目录下新建一个Dockerfile,然后把这个脚本的运行环境配置好就行,具体环境是:
-
装有python的系统
-
有requests第三方库
对着个脚本来说就这么简单,因此:
FROM python:3.5-slim
WORKDIR /app
# 安装依赖
RUN pip install requests
# 复制代码文件
COPY bangumi.py ./
# 设置默认执行命令
CMD ["python", "bangumi.py"]
选择官方的python镜像的轻量化slim版本作为基础镜像,背后的运行环境是Debian,然后设置工作目录,安装依赖,复制文件到工作目录下,执行脚本即可。
然后使用docker build -t test .
即可构建docker镜像,然后利用docker run --rm test
即可利用这个镜像运行一个容器,镜像和容器的关系就是模具和糕点的关系,模具可以生产糕点,并且可以生成多个糕点,而容器本身就是一个进程,通过docker ps
即可查看,docker images
可以查看镜像列表,包括大小,这个镜像大小近200MB。而镜像就是我们发布的产品,发布镜像到docker hub中,别人就可以docker pull
进行拉取,拉取完创建容器就能运行。因此对于这个小脚本发布却近200MB实在有点大了,不过我们可以进一步优化。
使用多阶段构建
-
构建阶段
-
选择一个基础镜像
-
安装requests第三方库
-
安装pyinstaller,把脚本打包成一个可执行文件
-
-
运行阶段
-
选择一个更加轻量级的发布镜像,不需要包含构建的工具,比如pyinstaller
-
将构建阶段的可执行文件复制到运行阶段的工作目录中
-
执行可执行文件
-
到最后我们发布的就只是运行阶段的镜像,不包含构建的工具,因此镜像大小会显著降低。修改使用多阶段构建的Dockerfile是这样:
# ===== 构建阶段 =====
FROM python:3.5-alpine AS builder
# 设置工作目录
WORKDIR /app
# 安装依赖:pyinstaller 用于打包,requests 是脚本的依赖
RUN pip install --no-cache-dir pyinstaller requests
# 复制脚本到工作目录
COPY bangumi.py .
RUN apk update && apk add binutils
# 使用 PyInstaller 将脚本打包成单个可执行文件
RUN pyinstaller --name bangumi --onefile --clean bangumi.py
# ===== 运行阶段 =====
# 使用一个非常轻量的 Alpine 镜像作为最终的运行环境
FROM alpine:latest
# 设置工作目录
WORKDIR /app
# Alpine 默认缺少一些 Python 网络请求可能需要的证书和库,这里安装一下
RUN apk add --no-cache ca-certificates libcrypto3 libssl3
# 从构建阶段(builder)复制已编译好的可执行文件到当前阶段
COPY --from=builder /app/dist/bangumi .
# 设置容器启动时要执行的命令
CMD ["/app/bangumi"]
注意点:
-
运行阶段使用Alpine Linux,只有几MB的大小,非常轻量
-
构建阶段的基础镜像也使用了Alpine,这是因为Alpine是没有glibc的,而是musl libc作为标准c库,如果还是使用Debian的镜像,会出现很多问题。而多阶段构建,这种问题还是比较多的,因此为了不那么麻烦,构建阶段也使用带python的Alpine。
-
pyinstaller 打包需要objdump工具,包含在GNU二进制工具包binutils中,需要安装,而Alpine的包管理工具是apk,安装使用
apk add
-
Alpine 默认缺少一些 Python 网络请求可能需要的证书和库,需要运行阶段安装
好了,接下来运行docker build -t test2 .
构建即可,同样使用docker run --rm test2
运行镜像对应的容器,不出意外,就能看到脚本运行结果了。如果使用docker images
查看镜像列表,你就会发现,此时镜像大小只有20多MB,大幅减少,因此多阶段构建也是实际发布时候建议使用的。
总结
docker一定会成为生产发布的标准,因此一定要学习使用;docker构建多采用多阶段构建,减少发布的镜像体积;尽量不要在运行阶段运行脚本,而是都在构建阶段打包成可执行文件。
docker还有很多重要的部分,比如挂在卷,网络,端口等
此外docker还有一个compose技术很重要,不是什么项目都能打包成一个exe的,有的项目包含多个组件,比如服务器,客户端,数据库,如果都装在一起,不仅复杂度提升,而且也不好维护,牵一发动全身。而compose技术就是将多个容器组合在一起,虽然容器本身是隔离的,包括网络,但是通过network技术可以让容器之间处于同一网络环境下,进而可以通信。