虽然使用 Python 对我来说常常是一种美妙的体验,但随着项目规模的扩大,管理复杂的开发环境常常会带来一些挑战。
仅举几个例子,以下是我遇到的 Python 的 3 个主要问题:
1. 依赖环境变量的应用程序可能需要设置这些变量才能运行应用程序。
2. 使用 auth 证书在不同服务之间进行通信的应用程序可能需要在运行应用程序之前在本地生成这些证书。
3. 同一项目中不同微服务之间可能会发生依赖版本控制冲突。
当使用多个相互依赖的微服务时,事情会变得特别棘手,坦率地说,作为一名开发人员,我真的不想为了启动和运行而管理所有这些开销。如果我刚刚开始一个新项目,尤其如此。
我在开发 Python 应用程序时看到的一种常见解决方案是使用Python 虚拟环境,这是包含 Python 安装和所需包的隔离环境。然而,管理多个虚拟环境和其他与环境相关的配置仍然是耗时且麻烦的,因为虚拟环境仅在 Python 解释器级别提供隔离。这意味着其他与环境相关的设置,例如环境变量和端口分配,仍然为所有项目组件全局共享。
我将在本文中演示的解决方案是使用容器化,这是一种将应用程序及其依赖项打包到一个独立单元中的方法,可以在任何平台上轻松部署和运行。 Docker是用于开发、部署和运行容器化应用程序的流行平台,而Docker Compose是一种工具,可以使用单个 YAML 文件(通常命名为
docker-compose.yml
).尽管有minikube等替代解决方案,但为了简单起见,我将在本示例中坚持使用 Docker 和 Docker Compose。我将演示如何使用 Docker 和 Docker Compose 设置和使用容器化开发环境。我还将讨论使用容器化开发环境的一些挑战,以及如何通过配置 Docker 和 Docker compose 来克服这些挑战,以满足有效开发环境的以下关键要求:
1. 运行——运行模拟目标生产环境执行的端到端场景。
2. 部署 - 快速更改代码并重新部署,就像使用非容器化应用程序运行时堆栈一样。
3. 调试 - 设置断点并使用调试器单步执行代码,与非容器化应用程序运行时堆栈一样,以识别和修复错误。
为了通过示例说明这一点,我将定义一个简单的 Python 应用程序,它使用轻量级 Python Web 框架Flask创建一个 RESTful API 来查询有关作者及其帖子的信息。 API 有一个端点,
/authors/{author_id}
,可用于通过将作者的 ID 指定为路径参数来检索有关特定作者的信息。然后,应用程序使用请求模块向单独的帖子服务发出 HTTP 请求,该服务应提供该作者的帖子列表。为了保持代码简洁,所有数据都将使用Faker库随机生成。首先,我将初始化然后为该项目打开一个空目录。接下来,我将创建两个子目录:第一个将称为
authors_service
, 第二个posts_service
.在每个目录中,我将创建 3 个文件:1.
app.py
:Flask 应用程序的主要入口点,它定义应用程序、设置路由并指定向这些路由发出请求时要调用的函数。2.
requirements.txt
:一个纯文本文件,指定应用程序运行所需的 Python 包。3.
Dockerfile
:包含构建 Docker 镜像说明的文本文件,如上所述,它是一个轻量级、独立且可执行的包,其中包含运行应用程序所需的一切,包括代码、运行时、库、环境变量、以及其他任何东西。每个
app.py
文件,我将实现具有所需逻辑的 Flask 微服务。对于
authors_service
, 这app.py
文件如下所示: import os import flask import requests from faker import Faker app = flask.Flask(__name__) @app.route( "/authors/<string:author_id>" , methods=[ "GET" ] )
def get_author_by_id ( author_id: str ):
author = { "id" : author_id, "name" : Faker().name(), "email" : Faker().email(), "posts" : _get_authors_posts(author_id) } return flask.jsonify(author) def _get_authors_posts ( author_id: str ):
response = requests.get( f' {os.environ[ "POSTS_SERVICE_URL" ]} / {author_id} '
) return response.json() if __name__ == "__main__" : app.run( host=os.environ[ 'SERVICE_HOST' ], port= int (os.environ[ 'SERVICE_PORT' ]) )
此代码设置一个 Flask 应用程序并定义一个路由来处理对端点的 GET 请求
/authors/{author_id}
.访问此端点时,它会为具有所提供 ID 的作者生成模拟数据,并从单独的帖子服务中检索该作者的帖子列表。然后运行 Flask 应用程序,监听相应环境变量中指定的主机名和端口。flask
, requests
和Faker
包裹。为了解决这个问题,我将把它们添加到作者服务中requirements.txt
文件,如下: flask == 2 . 2 . 2
requests == 2 . 28 . 1
Faker == 15 . 3 . 4
请注意,对于本指南中引用的任何依赖项,没有特定的包版本控制要求。使用的版本是撰写本文时可用的最新版本。
为了
posts_service
, app.py
看起来如下: import os import uuid from random import randint import flask from faker import Faker app = flask.Flask(__name__) @app.route( '/posts/<string:author_id>' , methods=[ 'GET' ] )
def get_posts_by_author_id ( author_id: str ):
posts = [ { "id:" : str (uuid.uuid4()), "author_id" : author_id, "title" : Faker().sentence(), "body" : Faker().paragraph() } for _ in range (randint( 1 , 5 )) ] return flask.jsonify(posts) if __name__ == '__main__' : app.run( host=os.environ[ 'SERVICE_HOST' ], port= int (os.environ[ 'SERVICE_PORT' ]) )
在此代码中,当客户端(即
authors_service
) 向路由发送 GET 请求/posts/{author_id}
, 功能get_posts_by_author_id
被指定的调用author_id
作为参数。该函数为作者使用 Faker 库编写的 1 到 5 个帖子生成模拟数据,并将帖子列表作为 JSON 响应返回给客户端。我还需要将 flask 和 Faker 包添加到帖子服务的
requirements.txt
文件,如下: flask == 2 . 2 . 2
Faker == 15 . 3 . 4
在对这些服务进行容器化之前,让我们考虑一个示例,说明为什么我首先要将它们相互隔离地打包和运行。
两种服务都使用环境变量
SERVICE_HOST
和SERVICE_PORT
定义启动 Flask 服务器的套接字。尽管SERVICE_HOST
不是问题(多个服务可以在同一主机上侦听), SERVICE_PORT
会导致问题。如果我要在本地 Python 环境中安装所有依赖项并运行两个服务,则第一个启动的服务将使用指定的端口,导致第二个服务崩溃,因为它无法使用相同的端口。一种简单的解决方案是使用单独的环境变量(例如, AUTHORS_SERVICE_PORT
和POSTS_SERVICE_PORT
) 反而。然而,修改源代码以适应环境限制在扩展时可能会变得复杂。容器化通过设置适合应用程序的环境来帮助避免此类问题,而不是相反。在这种情况下,我可以设置
SERVICE_PORT
环境变量为每个服务设置不同的值,每个服务将能够使用自己的端口而不受其他服务的干扰。Dockerfile
在每个服务的目录中。该文件的内容(对于这两种服务)如下: FROM python: 3.8
RUN mkdir /app WORKDIR /app COPY requi rements.txt /app/
RUN pip install -r requi rements.txt
COPY . /app/ CMD [ "python" , "app.py" ]
这个
Dockerfile
基于 Python 3.8父映像构建并在容器中为应用程序设置一个目录。然后它复制requirements.txt
文件从主机到容器并安装该文件中列出的依赖项。最后,它将其余的应用程序代码从主机复制到容器,并在容器启动时运行主应用程序脚本。接下来,我将创建一个名为
docker-compose.yml
在根项目目录中。正如上面简要提到的,该文件用于定义和运行多容器 Docker 应用程序。在里面docker-compose.yml
文件,我可以定义构成应用程序的服务,指定它们之间的依赖关系,并配置它们的构建和运行方式。在这种情况下,它看起来如下: ---
# Specify the version of the Docker Compose file format
version: '3.9'
services:
# Define the authors_service service
authors_service:
# This service relies on, and is therefor dependent on, the below `posts_service` service
depends_on:
- posts_service
# Specify the path to the Dockerfile for this service
build:
context: ./authors_service
dockerfile: Dockerfile
# Define environment variables for this service
environment:
- SERVICE_HOST=0.0.0.0
- PYTHONPATH=/app
- SERVICE_PORT=5000
- POSTS_SERVICE_URL=http://posts_service:6000/posts
# Map port 5000 on the host machine to port 5000 on the container
ports:
- "5000:5000"
# Mount the authors_service source code directory on the host to the working directory on the container
volumes:
- ./authors_service:/app
# Define the posts_service service
posts_service:
# Specify the path to the Dockerfile for this service
build:
context: ./posts_service
dockerfile: Dockerfile
# Define environment variables for this service
environment:
- PYTHONPATH=/app
- SERVICE_HOST=0.0.0.0
- SERVICE_PORT=6000
# Mount the posts_service source code directory on the host to the working directory on the container
volumes:
- ./posts_service:/app
容器可以用
docker-compose up
命令。第一次运行时,将自动构建 docker 镜像。这就满足了上面第一个“跑”的核心需求。
请注意,在
docker-compose.yml
文件,卷挂载用于共享源代码目录authors_service
和posts_service
主机和容器之间的服务。这允许在主机上编辑代码,更改自动反映在容器中(反之亦然)。例如,下面一行安装了
./authors_service
主机上的目录到/app
中的目录authors_service
容器: volumes: - . /authors_service:/ app
在主机上所做的更改会立即在容器上可用,并且在容器中所做的更改会立即保存到主机的源代码目录中。这样就可以在不重建镜像的情况下,通过重启相关容器的方式快速重新部署变更,有效满足“部署”的第二个核心需求。
这是它涉及更多的地方。 Python 中的调试器使用解释器提供的调试工具来暂停程序的执行并在某些点检查其状态。这包括设置跟踪功能
sys.settrace()
在每一行代码中检查断点,以及使用调用堆栈和变量检查等功能。与调试在主机上运行的 Python 解释器相比,调试在容器内运行的 Python 解释器可能会增加复杂性。这是因为容器环境与宿主机是隔离的。为了克服这个问题,可以采用以下两种常规方法之一: 可以从容器本身内部调试代码,或者可以使用远程调试服务器进行调试。
首先,我将使用VSCode作为首选编辑器来演示如何进行此操作。之后,我将解释如何使用JetBrains PyCharm进行类似的工作。
从容器内部调试代码
要使用 VSCode 从正在运行的 docker 容器中开发和调试代码,我将:
1. 确保安装并启用了 VSCode 的Docker 扩展。
3. 单击左侧边栏中的 Docker 图标,打开 Docker 扩展的资源管理器视图。
4. 在资源管理器视图中,展开“正在运行的容器”部分并选择我要附加到的容器。
5. 右键单击容器并从上下文菜单中选择“附加 Visual Studio 代码”选项。
这会将 Visual Studio Code 附加到所选容器,并在容器内打开一个新的 VSCode 窗口。在这个新窗口中,我可以像在本地环境中一样编写、运行和调试代码。
为了避免每次容器重新启动时都必须安装 VSCode 扩展(例如Python ),我可以在存储 VSCode 扩展的容器内挂载一个卷。这样,当容器重新启动时,扩展仍然可用,因为它们存储在主机上。要在这个演示项目中使用 docker compose 来做到这一点,
docker-compose.yml
文件可以修改如下: ---
# Specify the version of the Docker Compose file format
version: '3.9'
services:
# Define the authors_service service
authors_service:
...
# Mount the authors_service source code directory on the host to the working directory on the container
volumes:
- ./authors_service:/app
# Mount the vscode extension directory on the host to the vscode extension directory on the container
- /path/to/host/extensions:/root/.vscode/extensions
# Define the posts_service service
posts_service:
...
请注意,VSCode 扩展通常位于
~/.vscode/extensions
在 Linux 和 macOS 上,或%USERPROFILE%\.vscode\extensions
在 Windows 上。使用远程 Python 调试服务器
上述调试方法适用于独立脚本或编写、运行和调试测试。但是,调试涉及在不同容器中运行的多个服务的逻辑流更为复杂。
当容器启动时,它包含的服务通常会立即启动。在这种情况下,两个服务上的 Flask 服务器在附加 VSCode 时已经在运行,因此单击“运行和调试”并启动 Flask 服务器的另一个实例是不切实际的,因为它会导致同一服务的多个实例运行在同一个容器上并相互竞争,这通常不是可靠的调试流程。
这让我想到了第二个选项;使用远程 Python 调试服务器。远程 Python 调试服务器是在远程主机上运行并配置为接受来自调试器的连接的 Python 解释器。这允许使用在本地运行的调试器来检查和控制在远程环境中运行的 Python 进程。
请务必注意,术语“远程”不一定指物理上远程的机器,甚至不一定指本地但隔离的环境,例如在主机上运行的 Docker 容器。 Python 远程调试服务器也可用于调试与调试器在同一环境中运行的 Python 进程。在此上下文中,我将使用一个远程调试服务器,该服务器与我正在调试的进程在同一容器中运行。此方法与我们介绍的第一个调试选项之间的主要区别在于,我将附加到一个预先存在的进程,而不是每次我想运行和调试代码时都创建一个新进程。
要开始,第一步是将debugpy包添加到
requirements.txt
两种服务的文件。 debugpy 是一个高级的开源 Python 调试器,可用于在本地或远程调试 Python 程序。我将在两者中添加以下行requirements.txt
文件: debugpy == 1 . 6 . 4
现在我需要重建图像以便在每个服务的 Docker 图像上安装 debugpy。我会运行
docker-compose build
命令来做到这一点。那我就跑docker-compose up
启动容器。接下来,我会将 VSCode 附加到包含我要调试的进程的运行容器,就像我上面所做的那样。
为了将调试器附加到正在运行的 python 应用程序,我需要将以下代码片段添加到我希望开始调试的代码中:
import debugpy; debugpy.listen( 5678 )
此代码段导入 debugpy 模块并调用
listen
函数,它启动一个 debugpy 服务器,该服务器侦听来自指定端口号(在本例中为 5678)的调试器的连接。如果我想调试
authors_service
,我可以将上面的代码片段放在get_author_by_id
中的函数声明app.py
文件 - 如下: import os import flask import requests from faker import Faker app = flask.Flask(__name__) import debugpy; debugpy.listen( 5678 ) @app.route( "/authors/<string:author_id>" , methods=[ "GET" ] )
def get_author_by_id ( author_id: str ):
...
这将在应用程序启动时启动一个 debugpy 服务器作为
app.py
脚本被执行。下一步是创建用于调试应用程序的 VSCode 启动配置。在我附加到其容器(以及我正在运行 VSCode 窗口)的服务的根目录中,我将创建一个名为
.vscode
.然后,在此文件夹中,我将创建一个名为launch.json
,内容如下: { "version" : "0.2.0" , "configurations" : [ { "name" : "Python: Remote Attach" , "type" : "python" , "request" : "attach" , "connect" : { "host" : "localhost" , "port" : 5678
} } ] }
此配置指定 VSCode 应附加到端口 5678 上在本地计算机(即当前容器)上运行的 Python 调试器 - 重要的是,这是调用时指定的端口
debugpy.listen
上面的功能。然后我将保存所有更改。在 Docker 扩展的资源管理器视图中,我将右键单击我当前附加到的容器并从上下文菜单中选择“重新启动容器”(在本地 VSCode 实例上完成)。重新启动容器后,容器内的 VSCode 窗口会显示一个对话框,询问我是否要重新加载窗口——正确答案是是。
现在剩下的就是看它的实际效果了!要开始调试,在容器上运行的 VSCode 实例中,我将打开要调试的脚本,然后按 F5 启动调试器。调试器将附加到脚本并在脚本所在的行暂停执行
debugpy.listen
函数被调用。 “调试”选项卡中的调试器控件现在可用于逐步执行代码、设置断点和检查变量。这满足了上述“调试”要求。
根据官方文档,使用 PyCharm 时有两种方法可以解决此问题:可以使用远程解释器功能和/或远程调试服务器配置从 Docker 映像检索解释器。请注意,这两个选项并不相互排斥。我个人通常主要依靠远程解释器功能进行开发,并在必要时使用远程调试服务器配置。
设置远程口译员
要在 PyCharm 上设置远程解释器,我将:
1. 单击 IDE 窗口右下角的解释器选项卡弹出菜单。
2. 点击Add new interpreter ,然后从弹出菜单中选择On docker compose...。
3. 在接下来的弹出窗口中,选择相关的docker compose 文件,然后从下拉列表中选择相关的服务。 PyCharm 现在将尝试连接到 docker 图像并检索可用的 python 解释器。
4. 在下一个窗口中,选择我希望使用的 python 解释器(例如
/usr/local/bin/python
).选择解释器后,单击“创建”。然后,PyCharm 将为新的解释器编制索引,之后我可以像往常一样运行或调试代码——只要我愿意,PyCharm 就会在幕后为我编排 docker compose。
设置远程调试服务器配置
为了设置远程调试服务器配置,我首先需要添加两个依赖到相关的
requirements.txt
文件: pydevd和pydevd_pycharm 。这些在功能上类似于上面演示的 debugpy 包,但是,正如其名称所示,pydevd_pycharm 是专门为使用 PyCharm 进行调试而设计的。在此演示项目的上下文中,我将向两者添加以下两行requirements.txt
文件: pydevd ~= 2 . 9 . 1
pydevd -pycharm== 223 . 8214 . 17
完成此操作并重建 docker 映像后,我可以将以下代码片段嵌入代码中,以在代码中我希望开始调试的位置启动 pydevd_pycharm 调试服务器:
import pydevd_pycharm; pydevd_pycharm.settrace( 'host.docker.internal' , 5678 )
请注意,与 debugpy 不同,我在这里指定了一个值为“host.docker.internal”的主机名地址,这是一个 DNS 名称,可从 Docker 容器中解析为主机的内部 IP 地址。这是因为我没有在容器上运行 PyCharm;相反,我有效地将调试服务器配置为侦听主机的端口 5678。
这个选项也存在于 debugpy 中,但是因为在那种情况下我在容器本身上运行了一个 VSCode 实例,它简化了一些事情,让主机名地址默认为“localhost”(即容器本身的环回接口,而不是主机)。
为此,我将:
1. 从主菜单中选择运行>编辑配置,打开运行/调试配置对话框。
2. 单击对话框左上角的+按钮,然后从下拉菜单中选择Python Remote Debug 。
3. 在名称字段中,输入运行配置的名称。
4. 在脚本路径字段中,指定我要调试的脚本的路径。
5. 在主机字段中,输入将运行调试器服务器的主机的 IP 地址。在这个例子中,它是“localhost”。
6. 在端口字段中,输入调试器服务器将侦听的端口号。在这个例子中,它是 5678。
7. 在路径映射部分,我可以指定主机上的路径如何映射到容器内的路径。如果我正在调试从主机安装到容器中的代码,这很有用,因为两种环境中的路径可能不同。在这个例子中,我想映射
path/to/project/on/host/authors_service
在主机上,到 /app
在用于调试 authors_service 的容器中,或者path/to/project/on/host/posts_service
到/app
在用于调试 posts_service 的容器上(这些需要是两个单独的运行配置)。 8. 单击确定保存运行配置。
要开始调试,我将从“运行”下拉菜单中选择上述运行配置并单击“调试”按钮,然后使用
docker-compose up
命令。 PyCharm 调试器将附加到脚本并在pydevd_pycharm.settrace
函数被调用,让我开始修复那些错误。在本指南中,我对什么是容器化 python 开发环境、它们为何有用以及如何使用它们编写、部署和调试 python 代码进行了一般但实用的概述。请注意,这绝不是使用这些环境的综合指南。它只是扩展的起点。以下是一些有用的链接:
4.用于远程调试的官方 JetBrains PyCharm 文档
5.用于在开发容器上开发 Python 的官方 VSCode 文档