Python 代码
要在两个服务之间共享 Python 代码,有几种方法可供选择。显然,Python 的主要代码共享机制是 包。您可以通过将 __init__.py
文件添加到文件夹中来轻松地创建一个包 (您可能已经知道这些)。
因此,在两个镜像中轻松存在代码的关键是确保代码被打包到包中,并且在镜像内部很容易访问。让我们探讨如何做到这一点。
Python 包路径
首先,需要注意的是 Python 如何解析包。Python 使用 sys.path
属性搜索模块,类似于 Linux 命令行使用 $PATH
。
从 Python 外部,您可以使用环境变量 $PYTHONPATH
影响 sys.path
。您还可以在运行时添加到 sys.path
。
包初始化+导出
__init__.py
文件类似于 Python 对象上同名的魔术方法;如果需要,您可以使用它来初始化模块级别的代码。您从 __init__.py
导入和导出的内容可以在模块级别上可用。例如:
from x import y
from some_module import y
目录结构/包路径
假设你的代码库中有一个名为common
,一个名为pipeline
,还有一个名为app
的模块。共享代码位于common
中,而管道/应用程序特定代码位于这些模块中。在需要的地方,app
和pipeline
都从common
导入。
以下是该目录的布局:
project/
|
src/
|
|--common/
| |
| __init__.py
|
|--pipeline/
| |
| __main__.py
|
|--app/
|
__main__.py
使用这种布局,我们需要确保在Python模块路径上可以找到
common
。我们可以通过以下方式实现:
环境变量。如果您正在运行入口点(例如此示例树中的__main__.py
),则可能很容易控制环境变量,因此您可以将PYTHONPATH
设置为包括src/
;如果这样做,则可以导入common
,但是无法导入pipeline
和main
,因为它们没有__init__
文件,只有__main__
。
运行时修改。您可以将路径传递给调用(作为参数),然后将该路径写入sys.path
,从而将其添加到模块路径。在此调用之后,应该可以导入common
。
路径挂钩。使用sys.path_hooks
可以让您在运行时响应导入请求。
PTH文件。旧版本的Python 2.x支持PTH文件,可让您从site-packages
路径指向新路径。这些是一种高级角落情况选项 - 其他选项在客观上更好 - 但如果您愿意,我可以解释它们的工作原理。
以下是在运行时更改
sys.path
的示例:
import sys
sys.path.append('/whatever/dir/you/want')
以下是一份有关IT技术的翻译示例,涉及更改“PYTHONPATH”的内容:
这里举一个从Python外部更改“PYTHONPATH”的例子:
PYTHONPATH=/whatever/dir/you/want python3
顺便问一下,
__main__.py
是什么?
正如上面提到的,
__init__.py
是初始化一个模块。另一方面,
__main__.py
充当模块的
主入口点。如果Python包中存在此文件,则可以执行以下操作:
python -m module
它将运行
module
的
__main__.py
。例如,如果你有
app/__main__.py
,那么可以执行以下操作:
python -m app
将运行
app
的
__main__.py
。
将所有内容整合在一起
当你构建Python Docker镜像时,必须确保上述的
PYTHONPATH
或者
sys.path
更改是有效的。在运行时,Python以相同的方式查找模块(即从Docker镜像内部),因此应用相同的规则。
只要从
app
中可以导入
common
,你的代码应该可以加载和工作,
pipeline
也是一样;之所以将
app
/
pipeline
隔离开来(即
不要允许它们互相导入)是因为
pipeline
然后可以从
app
镜像中省略,反之亦然。
因此,在你的
Dockerfile
中,可以执行以下操作来构建
app
镜像:
RUN mkdir -p /code
COPY ./common /code/
COPY ./app /code/
ENV PYTHONPATH /code/
只要存在
/code/common/__init__.py
,你现在应该能够从
app
中导入
common
。
Docker镜像
如果两个服务要共享代码,它们应该尽可能地“一起”部署,这样代码始终保持同步;Docker镜像是一个很好的工具,因为它可以通过映像哈希地址对您的整个“修订”进行全面的可寻址。
然后,您的容器可以以几种方式检测它是否作为服务或作业运行,然后相应地调用代码(服务器将侦听和服务,作业将拉取参数并开始工作)。
以下是在此领域实现使用一个镜像的一些好选择,附有摘要的正/负比较:
实现使用一个镜像的方法
1) 环境变量。您可以在Cloud Run服务中分配一个环境变量,再次在Cloud Run作业中分配一个不同的环境变量,以便您的容器可以检测它在哪里运行。
利:
- 在Python中易于使用(例如:
import os; os.environ["IS_JOB"]
等)
- 灵活,在几乎所有环境中都有效(即使您将来离开Cloud Run也是如此)
- 适用于任何语言,不仅限于Python
弊:
- 很难传达结构,没有类型
- 可能会使测试复杂化;模拟环境可能很难掌握
- 可以在需要时任意访问,这意味着重构它可能很困难
2) 命令参数。也许您将一个参数传递给容器,告诉它是服务还是作业,就像一个环境变量一样。
利:
- 基本上与环境变量相同。
- 但只能由您的二进制文件访问,不会渗入其他代码。
- 只有一个地方可以在其中访问此代码,这比env vars更干净
弊:
- 必须在入口点处访问该值并保存它,而不是随时随地访问
os.environ
3) 令牌身份验证。您可以为作业和服务分配不同的服务帐户,这对于安全卫生是一个好主意。基于此服务帐户的标识,您的容器可以检测它在哪里运行。
- 优点:
- 可以完全从代码中检测,不需要任何非自给式的输入,例如环境变量(即更为简洁和更好的抽象)
- 完全类型化,可能是最容易测试/模拟的版本,因为有谷歌一流的Python SDK支持
- 缺点:
- 将您的代码与Cloud Run强烈耦合,或至少与Google Cloud强烈耦合。如果您想要迁移,这将使您的重构变得复杂。
- 可能是最难编写的(尽管环境变量是一个低标准)
与多个镜像的比较
无论如何,最好使用一个镜像。让我们将这两种方法与不同的Dockerfile进行比较:
运行成本更低。如果您有两个Docker镜像,您需要支付存储两个镜像、提供两个镜像等费用。根据您的开发速度,这个成本可能会令人惊讶,特别是如果您正在使用Cloud Artifact Registry或类似产品。
构建时间更快。使用两个容器,您的Docker构建时间将加倍。也许您可以通过多阶段构建来节省这里的成本,但仍然需要存储、提供和上传/下载两个镜像,而不是一个。
更易于保持同步。无需确保两个镜像哈希完全对齐,或同时更新和启动等。
更易于比较状态。无需寻找以确定作业和服务是否处于相同的版本。如果哈希匹配,则可以使用。
更易于测试。因为您只有一个软件输出,所以这可能使测试服务和作业如何一起工作更容易。在单元测试中,将代码放在一起,可以同时调用作业/服务。
使用两个镜像的好处相当少:
诊断。如果它们在单独的镜像中,可能更容易区分或诊断问题,但这有点牵强,因为您最大的问题可能只是保持两个镜像同步。
分别更新。如果代码更改仅涉及其中一个,那么您可能可以跳过作业或服务上的某些更新。如果这是一个重要问题,它可能会抵消一个单一镜像体系结构的好处。
镜像大小。通过分离两个镜像的代码和职责,您可能可以节省一些空间,但是您在第一次复制镜像时就已经超出了这些节省的成本。
总的来说,如果我处在你的位置,我会制作一张图片;但是答案取决于你的应用程序、需求、工作流程等等。