Pixie Stirling MySQL 追踪测试镜像解析:Python 客户端容器的构建、发布与 BPF 测试集成
Pixie Stirling MySQL 追踪测试镜像解析:Python 客户端容器的构建、发布与 BPF 测试集成
导读
本文深入解析 Pixie 开源仓库中用于 MySQL 协议追踪(MySQL tracing)端到端测试的专用 Python 客户端容器镜像,涵盖其 Dockerfile 设计动机(生成更丰富的协议消息、打印 PID 供测试过滤)、基于 Bazel 的镜像组装与发布更新流程,以及它在 mysql_trace_bpf_test.cc 中如何作为客户端与 MySQL 服务器容器协同,验证 BPF 抓包与协议解析的正确性。读完本文,你将掌握这套"测试专用容器镜像"从构建、发布到被 e2e 测试消费的完整闭环,并理解其每一项设计背后的可测试性考量。
为什么 MySQL 协议追踪测试需要专属客户端镜像
在 README.md 中,Pixie 明确说明了构建这一镜像的动机:目录中保存的是"一个容器内的 Python 版 MySQL 客户端"的构建文件。之所以选择 Python 客户端而不是更简单的命令行客户端,是因为 Python MySQL Connector 能产生比简单客户端更多样化的协议消息序列——它既会触发常规的 Query(0x03)、InitDB(0x02),也会触发 Prepared Statement 相关的 StmtPrepare(0x16)、StmtExecute(0x17)、StmtReset(0x1a)、StmtClose(0x19)等命令,从而让 BPF 追踪与 MySQL 协议解析器获得更充分的覆盖。
该镜像属于预构建(pre-built)镜像,通过 BUILD.bazel 以 Bazel 规则直接引用,无需在测试运行时现场构建。
镜像目录结构与构建文件解析
该目录位于 src/stirling/source_connectors/socket_tracer/testing/containers/mysql/,包含 6 个文件:
| 文件 | 作用 |
|---|---|
Dockerfile |
定义镜像基础层、Python 依赖、启动行为与 entrypoint |
requirements.in |
pip-compile 的输入:声明 mysql-connector-python==8.0.32 |
requirements.txt |
pip-compile 生成的带哈希锁定文件(含 protobuf==3.20.3 传递依赖) |
BUILD.bazel |
用 pkg_tar 与 container_layer 把测试 SQL/Python 脚本打包进 /scripts 层 |
update_ghcr.sh |
构建并推送镜像到 GHCR,输出需要更新的镜像 digest |
README.md |
本文所依据的使用与更新说明 |
Dockerfile 的三个关键设计
Dockerfile 的每一层都服务于测试需求:
-
锁定基础镜像与依赖哈希:基础镜像固定为
python:3.10.0-alpine3.14(带 sha256 digest),依赖通过pip install --require-hashes -r requirements.txt安装。requirements.txt由pip-compile --generate-hashes requirements.in自动生成(见文件头注释),确保可复现构建。 -
定制 Python 启动行为——打印 PID:这是本镜像最核心的设计。通过写入
sitecustomize.py(Python 启动时会自动加载的钩子文件):RUN mkdir -p /root/.local/lib/python3.10/site-packages RUN echo "import os" > /root/.local/lib/python3.10/site-packages/sitecustomize.py RUN echo "print(\"pid=\" + str(os.getpid()))" >> /root/.local/lib/python3.10/site-packages/sitecustomize.py任何 Python 进程启动时都会首先打印
pid=<pid>。测试侧正是靠解析这行输出来拿到客户端进程 PID,再据此从 Stirling 采集的记录中按 PID 过滤出属于该客户端的 MySQL 消息。该设计在测试代码的注释与解析逻辑中都有印证(见下文测试用例章节)。 -
Entrypoint 为 python,脚本挂载到 /scripts:
RUN mkdir /scripts ENTRYPOINT ["python"]容器 entrypoint 固定为
python,启动时传入的参数即是要运行的脚本路径。测试用的 SQL/Python 脚本可通过 volume 挂载到 /scripts 目录,或由 Bazel 在构建镜像时直接打入该目录。
BUILD.bazel:把测试脚本打进镜像层
BUILD.bazel 负责把 MySQL 协议测试脚本打包进镜像:
pkg_tar目标mysql_script_files收集 protocols/mysql/testing/BUILD.bazel 中定义的scriptsfilegroup(*.sql与script.py),设置mode = "0755"并剥离路径前缀;container_layer目标mysql_scripts将其装载到镜像的/scripts目录。
最终,在 testing/containers/BUILD.bazel 中,mysql_connector_image 以 @python_mysql_connector_image//image 为基础镜像、叠加 mysql_scripts 层,组合出供测试直接使用的完整镜像:
container_image(
name = "mysql_connector_image",
base = "@python_mysql_connector_image//image",
layers = [
"//src/stirling/source_connectors/socket_tracer/testing/containers/mysql:mysql_scripts",
],
)
更新镜像:从 Dockerfile 改动到 Bazel digest 同步
README 给出的更新流程如下:
- 按需修改
Dockerfile(及依赖文件); - 递增 update_ghcr.sh 中的版本号;
- 运行
update_ghcr.sh构建并上传镜像; - 用脚本打印出的新 SHA 更新 Bazel 工作区中的镜像引用。
脚本实际执行逻辑为:
version=1.3
tag="ghcr.io/pixie-io/python_mysql_connector:$version"
docker build . -t $tag
docker push $tag
sha=$(docker inspect --format='{{index .RepoDigests 0}}' $tag | cut -f2 -d'@')
echo "Image pushed!"
echo "IMPORTANT: Now update //bazel/container_images.bzl with the following digest: $sha"
几点基于仓库源码的事实与提醒:
-
版本号必须手工递增:脚本注释明确要求"Increment this number on every upload"(每次上传都递增),当前为
1.3。 -
digest 更新目标以脚本输出为准:README 中写的是更新
//bazel/pl_workspace.pl,而脚本实际打印的是//bazel/container_images.bzl。经核对仓库,bazel/container_images.bzl 中确实存在python_mysql_connector_image目标及其 digest 定义:# Custom-built container with python MySQL client, for MySQL tests. _container_image( name = "python_mysql_connector_image", repository = "python_mysql_connector", digest = "sha256:ae7fb76afe1ab7c34e2d31c351579ee340c019670559716fd671126e85894452", )可以推断 README 中的
pl_workspace.pl是文档更新滞后所致,实际应更新bazel/container_images.bzl中的该 digest 字段。同文件还引用了 MySQL 服务器镜像mysql_base_image(mysql/mysql-server:8.0.13的 digest),与客户端镜像配套使用。 -
push 前提:
docker push需要具备ghcr.io/pixie-io的推送权限;digest 通过docker inspect从镜像的 RepoDigests 中提取(cut -f2 -d'@'取@后的 sha256 部分)。
测试用例中的实际消费方式
README 末尾指向 mysql_container_bpf_test.cc 作为使用示例;仓库中对应的实际文件是 mysql_trace_bpf_test.cc(文件名略有出入,后者为当前生效的测试源文件)。该测试属于 MySQLTraceTest(继承 SocketTraceBPFTestFixture),同时持有两个容器:
- 服务器:
MySQLContainer server_(基于mysql_base_image,官方 MySQL Server 8.0.13); - 客户端:
PythonMySQLConnectorContainer client_,其定义见 python_mysql_connector_container.h,从mysql_connector_image.tar加载镜像,容器名前缀为mysql_client,就绪消息(kReadyMessage)正是"pid"——与 sitecustomize 打印的pid=<pid>输出严格对应。
测试中的两条客户端路径
MySQLTraceTest 提供两种产生流量的方式:
- RunSQLScript(bash + mysql CLI):在服务器容器内执行
bash -c 'echo "<sql>" | mysql --protocol=TCP --ssl-mode=DISABLED --host=localhost --port=3306 -uroot & echo $! && wait',通过echo $!拿到后台 mysql 进程的 PID; - RunPythonScript(本镜像客户端):以
--network=container:<server>共享服务器网络命名空间,把脚本路径作为容器参数传入(如/scripts/script.py),client_.Run(..., {"/scripts/script.py"})。随后从输出第一行解析pid=<pid>得到客户端 PID——这正是镜像打印 PID 设计的目的。测试代码注释明确写道:"The first line of the output should be pid=(the container's python init is set-up to print this automatically)"。
断言与覆盖范围
TEST_F(MySQLTraceTest, mysql_capture) 依次执行三类脚本并断言客户端与服务器两侧的追踪记录:
- script.sql:执行
SELECT ... FROM information_schema.tables、SHOW DATABASES、USE mysql、SELECT user, host FROM user等,产生 Init + 6 条命令记录; prepare_execute.sql:产生 Init + 8 条命令记录;- script.py:通过
mysql.connectorAPI 依次触发cmd_query(kQuery 0x03)、cmd_init_db(kInitDB 0x02)、prepared cursor 的executemany(kStmtPrepare 0x16 / kStmtExecute 0x17 / kStmtReset 0x1a)、cursor.execute(select_command)(含 kStmtClose 0x19)以及最后的cnx.close()(kQuit 0x01),脚本头部还注释了未启用但预留的cmd_statistics()(kStatistics 0x09)调用。
断言时,客户端侧按 client_pid 过滤、服务器侧按 server_.process_pid() 过滤,用 UnorderedElementsAre 与 EqMySQLRecord(...) 匹配器逐一核对每条记录的字段。其中服务器侧刻意省略 Init 记录:测试注释解释,MySQL 服务器会分两次 read 读取 4 字节头部与包体,为避免复杂化 BPF 代码,服务器端丢失用于协议推断的首个包是可接受的取舍。这一断言逻辑反向印证了客户端镜像打印 PID 的必要性——没有 PID 就无法将混杂流量精确归属到被测进程。
扩展阅读与调试技巧
- 测试期望输出基线:mysql_container_bpf_test.json(由
expected_outputsfilegroup 引用); - 若需本地"仅起容器、手动追流量"调试,可启用测试中的
--tracing_mode标志(DEFINE_bool(tracing_mode, false, "If true, only runs the containers and exits. For tracing.")),此时测试只启动容器并退出,便于人工观察客户端打印的 PID 与执行结果; - 服务器容器启动时通过
--env=MYSQL_ALLOW_EMPTY_PASSWORD=1、--env=MYSQL_ROOT_HOST=%允许空密码与任意主机 root 访问(见MySQLTraceTest构造函数),与script.py中"password": "", "ssl_disabled": "True"的配置遥相呼应。
总结
Pixie 为 MySQL 协议追踪测试专门构建的 Python 客户端镜像,本质是一个"为可测试性而设计"的容器:Python Connector 提供丰富的协议命令覆盖,sitecustomize.py 打印 PID 打通了 BPF 抓包结果与被测进程的关联,/scripts 目录 + 以脚本为参数的 entrypoint 让测试脚本可以随镜像或 volume 灵活注入,而 update_ghcr.sh 的版本递增与 digest 回填则保证了镜像变更能被 Bazel 构建系统安全、可复现地消费。理解这套镜像的设计与集成方式,不仅有助于读懂 Stirling 的 MySQL 追踪测试,也为在类似 e2e 观测测试中构造"专用流量生成容器"提供了可借鉴的范式。