7

使用 Golang 和 Docker 运行 Python 代码

 1 year ago
source link: https://soulteary.com/2023/05/21/run-python-code-with-golang-and-docker.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

使用 Golang 和 Docker 运行 Python 代码

2023年05月21日阅读Markdown格式6730字14分钟阅读

本篇文章聊聊如何使用 Golang 来运行 Python 代码,用 Python 现成软件包来偷个懒儿,来少写一些代码。

最近折腾了一些“陈年项目”,不少都是使用 Python 实现的。而我在折腾的项目的代码主要是使用 Golang 实现的。改写这些项目中的基础逻辑并不麻烦,借助 ChatGPT ,都是分分钟的事情。

但是有一些项目依赖的 Python 软件包,却让我为了难:

  1. Go 官方没有提供功能相等的,测试完备的替代包。
  2. 开源社区没有实现功能相近的软件包,或者实现的程序缺乏测试保障。
  3. 重新从零到一实现,意味着大量的时间消耗,尤其是具备大量测试用例的 Python 项目,比如:https://github.com/derek73/python-nameparser

作为一个有追求的工程师,我们首先需要排除掉使用 os/exec 这类方式,丑陋(不可靠、不稳定)的使用 Shell 来执行 Python 代码。

完整代码开源在 soulteary/docker-python-in-go,你可以自取。

在折腾之前,我们先聊聊原理和场景限制。

实现原理和场景限制

2018 年 11 月,DataDog 团队借鉴社区成名已久的 sbinet/go-python 项目,创建了 DataDog/go-python3 项目,提供了 Go 语言和 CPython-3 API 的绑定。

但可惜的是,在 2021 年 12 月 1 日,DataDog 团队宣布存档项目。值得庆幸的是,官方宣布项目交由 go-python/cpy3 继续维护。

不过,随着 Python 的版本迭代和变动,项目陷入了困境:

  • Python 3.8 中,需要调整 Python 源码实现,移除 PyEval_ReInitThreads 函数,才能够正常工作。
  • Python 3.9 之后,Python C API 中更是移除了 PyDict_ClearFreeList 的接口支持,导致项目不能继续兼容运行。

所以,如果我们愿意调整 Python 源码,那么我们可以使用 3.8 版本的 Python,否则方案就只能在 3.7 版本的 Python 运行。

除了不同版本 Python 本身的“接口”限制之外,还有一个硬件相关的限制。我们身边越来越多同学购置了 M1 / M2 芯片的 Mac ,但是 Python 3.8 之前,支持 ARM 芯片,会出现比较多的编译问题。除了 Python 之外,Golang 1.17.7 及之前的版本也对 M1 / M2 芯片存在兼容性问题。

所以,如果我们需要支持 M1 / M2 的设备,那么我们需要使用社区维护者调整过源码的 Python 3.8,以及比较新版本的 Golang。

使用 Docker 解决上面的环境依赖问题

在 2023 年,许多系统、软件都产生了非常多的变化。如果我们按照网上的方式来,可能会遇到这样或者那样的问题。好在,我们还有一条简单可靠的路:Docker。

虽然,社区维护者 Christian Korneck 提供了一些例子,但其实在容器里无论是安装 Python 还是 Golang,都会引入不必要的额外变量。

我们有更好的方案,直接基于 Python 和 Golang 的官方提供的镜像,来制作构建环境和运行环境,让 Docker 容器既小巧又可靠。

好了,前置的相关知识,到这里就了解的差不多了。下面,我们来聊聊如何折腾它。

准备 Python 程序

我们以前文中提到的 Python 软件包 derek73/python-nameparser 为例,编写一个简单的 Python 程序片段,能够“简单快速的解析人名”。

from nameparser import HumanName

print(HumanName("Dr. Juan Q. Xavier de la Vega III (Doc Vega)").as_dict())

将上面的代码保存为 app.py,然后使用 python app.py 执行这个程序,验证程序能够正常运行。

# python app.py

{'title': 'Dr.', 'first': 'Juan', 'middle': 'Q. Xavier', 'last': 'de la Vega', 'suffix': 'III', 'nickname': 'Doc Vega'}

程序准备完毕之后,我们来完成 Golang 部分的实现。

实现 Golang 程序

Golang 的程序实现也不复杂,我们可以将上面的代码直接 HardCode 到 Go 里,或者使用 osio 包里的函数,来读取我们的 Python 程序,大概 20 行内就能解决战斗。

package main

import (
	python3 "github.com/go-python/cpy3"
)

func main() {
	defer python3.Py_Finalize()
	python3.Py_Initialize()

	code := `
from nameparser import HumanName

print(HumanName("Dr. Juan Q. Xavier de la Vega III (Doc Vega)").as_dict())
`
	python3.PyRun_SimpleString(code)
}

这部分代码的备份,你在 soulteary/docker-python-in-go/app/main.go 可以找到。

使用 Docker 完成程序构建

这里,我们先来实现一个最简的 Docker 配置:

# Base Images
FROM golang:1.20.4-alpine3.18 AS go-builder
FROM python:3.7-alpine3.18 AS builder
# Base Builder Env
COPY --from=go-builder /usr/local/go/ /usr/local/go/
ENV PATH="/usr/local/go/bin:${PATH}"
RUN python -m pip install --upgrade pip
RUN apk add build-base pkgconfig
ENV PKG_CONFIG_PATH=/usr/local/lib/pkgconfig/
ENV CGO_ENABLED=1
# Copy source code
COPY app /app
WORKDIR /app
# Install deps
RUN pip install nameparser && \
    go mod init human-name && \
    go mod tidy
# Build the binary
RUN go build -o HumanName

# Run Image
FROM python:3.7-alpine3.18
# Copy Python Deps
COPY --from=builder /usr/local/lib/python3.7/site-packages/nameparser /usr/local/lib/python3.7/site-packages/nameparser
# Copy binary
COPY --from=builder /app/HumanName /HumanName
CMD ["/HumanName"]

将上面的内容保存为 Dockerfile,然后使用下面的命令构建镜像:

docker build -t soulteary/python-in-golang .

镜像构建过程中,我们将看到类似下面的日志:

# docker build -t soulteary/python-in-golang .

[+] Building 2.5s (18/18) FINISHED                                                                                                 
 => [internal] load build definition from Dockerfile                                                                          0.0s
 => => transferring dockerfile: 37B                                                                                           0.0s
 => [internal] load .dockerignore                                                                                             0.0s
 => => transferring context: 2B                                                                                               0.0s
 => [internal] load metadata for docker.io/library/python:3.7-alpine3.18                                                      2.4s
 => [internal] load metadata for docker.io/library/golang:1.20.4-alpine3.18                                                   1.5s
 => [auth] library/python:pull token for registry-1.docker.io                                                                 0.0s
 => [builder 1/8] FROM docker.io/library/python:3.7-alpine3.18@sha256:f48c5f6a8a22a73558ea93eb26d2c7928d23f2acb2bb9270be9a08  0.0s
 => [go-builder 1/1] FROM docker.io/library/golang:1.20.4-alpine3.18@sha256:ee2f23f1a612da71b8a4cd78fec827f1e67b0a8546a98d25  0.0s
 => [internal] load build context                                                                                             0.0s
 => => transferring context: 56B                                                                                              0.0s
 => CACHED [builder 2/8] COPY --from=go-builder /usr/local/go/ /usr/local/go/                                                 0.0s
 => CACHED [builder 3/8] RUN python -m pip install --upgrade pip                                                              0.0s
 => CACHED [builder 4/8] RUN apk add build-base pkgconfig                                                                     0.0s
 => CACHED [builder 5/8] COPY app /app                                                                                        0.0s
 => CACHED [builder 6/8] WORKDIR /app                                                                                         0.0s
 => CACHED [builder 7/8] RUN pip install nameparser &&     go mod init human-name &&     go mod tidy                          0.0s
 => CACHED [builder 8/8] RUN go build -o HumanName                                                                            0.0s
 => CACHED [stage-2 2/3] COPY --from=builder /usr/local/lib/python3.7/site-packages /usr/local/lib/python3.7/site-packages    0.0s
 => CACHED [stage-2 3/3] COPY --from=builder /app/HumanName /HumanName                                                        0.0s
 => exporting to image                                                                                                        0.0s
 => => exporting layers                                                                                                       0.0s
 => => writing image sha256:8c4e43d26afff57ef65cc8431d3c0a01e57ad43e2acbe3f90ac5a5b6c5edab12                                  0.0s
 => => naming to docker.io/soulteary/python-in-golang 

镜像构建完毕,执行下面的命令,就能够验证程序是否正常了:

docker run --rm -it soulteary/python-in-golang

不出意外,程序将输出下面的内容:

# docker run --rm -it soulteary/python-in-golang

{'title': 'Dr.', 'first': 'Juan', 'middle': 'Q. Xavier', 'last': 'de la Vega', 'suffix': 'III', 'nickname': 'Doc Vega'}

好了,在 Golang 中运行 Python 程序,到这里就基本搞定啦。

当我们观察构建好的镜像,能够看到,我们构建的镜像仅仅比官方原始镜像增加了 1.9 MB,是不是非常“环保”。

# go-name docker images | grep python

soulteary/python-in-golang    latest           8218d4d4b16b   About a minute ago   48.9MB
python                        3.7-alpine3.18   e4fbc12a05a9   11 days ago          47MB

使用镜像加速构建过程

为了能够让镜像构建速度加快,我们可以为 Python 和 Golang ,以及我们所使用的系统 Alpine 添加软件源镜像。

# Base Images
FROM golang:1.20.4-alpine3.18 AS go-builder
FROM python:3.7-alpine3.18 AS builder
# Base Builder Env
COPY --from=go-builder /usr/local/go/ /usr/local/go/
ENV PATH="/usr/local/go/bin:${PATH}"
# Set Alpine Mirror
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.tuna.tsinghua.edu.cn/g' /etc/apk/repositories
RUN python -m pip install --upgrade pip
# Set PyPi Mirror
RUN pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple
RUN apk add build-base pkgconfig
ENV PKG_CONFIG_PATH=/usr/local/lib/pkgconfig/
ENV CGO_ENABLED=1
# Set Golang Mirror
ENV GOPROXY="https://goproxy.cn"
# Copy source code
COPY app /app
WORKDIR /app
# Install deps
RUN pip install nameparser && \
    go mod init human-name && \
    go mod tidy
# Build the binary
RUN go build -o HumanName

# Run Image
FROM python:3.7-alpine3.18
# Copy Python Deps
COPY --from=builder /usr/local/lib/python3.7/site-packages/nameparser /usr/local/lib/python3.7/site-packages/nameparser
# Copy binary
COPY --from=builder /app/HumanName /HumanName
CMD ["/HumanName"]

其实在 Golang 中运行 Python 除了本文的方法,以及在文章前面我们避免的方法之外,还有两种方案,有机会我们再展开聊聊。

好了,这篇文章,就先写到这里啦。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK