ClickHouse CI 集成测试排障指南:logs.tar.gz 产物包结构、按失败类型定位日志与精准解包方法
【免费下载链接】ClickHouseClickHouse® is a real-time analytics database management system项目地址: https://gitcode.com/GitHub_Trending/cli/ClickHouse
本文基于 ClickHouse 仓库investigate-ci技能中的集成测试产物文档,完整解析 CI 集成测试失败时的日志产物包logs.tar.gz:它的生成机制、压缩包内的目录布局、pytest_parallel.jsonl的结构化结果查询方法,以及按失败类型(断言错误、服务端异常、容器启动失败、执行轨迹)选取正确日志文件的实操流程。读完后,你将掌握从一份 CI 集成测试报告中快速下载产物包、只解压需要的成员、并用grep/jq精确定位失败根因的完整工作流,理解这些产物在 集成测试作业脚本 中是如何被收集和上传的。
一、产物包从哪里来:logs.tar.gz 的生成与上传机制
集成测试失败时的首要证据来源是logs.tar.gz产物包(在.claude/tools/fetch_ci_report.js使用--links参数时,会列在=== Artifact Links ===小节下)。下载它有两种方式:
- 用
fetch_ci_report.js --download-logs <path>参数直接下载到指定文件路径; - 或直接用
curl抓取--links输出中的 URL。
注意:压缩格式取决于文件大小。产物包本身始终是 tar 归档,但内部成员文件的压缩可能各不相同;更重要的是,产物包 URL 可能是
logs.tar.gz也可能是logs.tar.zst(取决于大小)。应以--links输出的实际 URL 为准,并统一使用tar -xf(自动探测压缩格式)来解包,而不是写死tar -xzf。
产物包由 on_error_hook 在作业出错时打包
从 集成测试作业脚本 的源码可以看到,main()一开始就为作业注册了一个on_error_hook(ci/jobs/integration_test_job.py#L1494-L1512),在硬性超时或作业出错时收集现场:
dmesg -T > ./ci/tmp/dmesg.log sudo chown -R $(id -u):$(id -g) ./tests/integration tar -czf ./ci/tmp/logs.tar.gz \ ./tests/integration/test_*/_instances*/ \ ./ci/tmp/*.log \ ./ci/tmp/*.jsonl || :也就是说,产物包内容正好是三类:每个测试模块下 pytest 工作实例目录_instances*/(含各节点的 ClickHouse 服务器日志)、ci/tmp/下的全部.log(各 pytest 输出与作业日志)和全部.jsonl(结构化测试结果)。随后set_files()将./ci/tmp/logs.tar.gz、./ci/tmp/dmesg.log、dmesg 跟随日志和 docker-in-docker 日志 声明为要上传的附件(strict=False,缺失不致命)。这解释了产物包为什么以相对仓库根的路径(ci/tmp/...、tests/integration/...)组织成员——tar 就是以相对路径./ci/tmp/...打进去的。
脚本中还有一处细节与排障相关:作业在收尾阶段会把失败测试的独立日志文件与logs.tar.gz一起附加上报(ci/jobs/integration_test_job.py#L2169),因此--links中除整包外可能还有零散的日志文件可直接抓取。
二、完整归档布局:Archive Layout
解包后,logs.tar.gz的内部布局如下(来自 产物文档):
ci/tmp/ pytest_parallel.jsonl # 结构化逐测试结果(每行一个 JSON 对象) pytest_parallel.log # 人类可读的合并输出 pytest_parallel-gw0.log # 逐 worker 输出:完整的 docker/cluster 调用 DEBUG 日志 pytest_parallel-gw1.log ... parallel.log # 顶层并行运行器日志 job.log # CI 作业脚本执行日志 docker-in-docker.log # Docker 守护进程输出 tests/integration/<test_module>/ _instances-conftest.py-gw<N>/ docker.log # 本 worker 的 docker compose up/down 输出 node1/logs/ clickhouse-server.log # node1 的 ClickHouse 服务器日志 clickhouse-server.err.log node2/logs/ clickhouse-server.log ...各成员文件的来源可以在源码中找到对应关系:
ci/tmp/pytest_parallel.jsonl/pytest_parallel.log:由并行阶段调用run_pytest_and_collect_results(command=..., report_name="parallel", ...)产生(ci/jobs/integration_test_job.py 中并行批次以report_name="parallel"运行 pytest),report_name决定了ci/tmp/下pytest_<report_name>.*系列文件的命名。pytest_parallel-gw<N>.log:pytest-xdist 为每个 worker(gw0、gw1、…)生成的独立日志。worker 数量由--dist=loadfile -n {workers}决定,而workers在脚本中按“每 worker 最多 5 核、11 GiB 内存”的预算模型推导(MAX_CPUS_PER_WORKER/MAX_MEM_PER_WORKER,见 ci/jobs/integration_test_job.py#L114-L115)。所以产物包里gw<N>的下标上限与作业跑在多大的 runner 上直接相关。tests/integration/<test_module>/_instances-conftest.py-gw<N>/:pytest 为每个 worker 在测试模块目录下创建的 conftest 实例目录(目录名含conftest.py与 worker 编号),其中docker.log记录该 worker 的docker compose启停输出,node<M>/logs/clickhouse-server.log(及.err.log)则是该集群每个节点上 ClickHouse 服务进程的完整日志。ci/tmp/docker-in-docker.log:CI 容器内嵌套启动 dockerd 的输出。脚本中常量DOCKER_IN_DOCKER_LOG = "./ci/tmp/docker-in-docker.log"(ci/jobs/integration_test_job.py#L66)声明了它的路径,start_docker_in_docker()把docker_in_docker.sh的全部 stdout/stderr 重定向写入该文件;若 daemon 20 次docker info探测后仍无响应,会直接抛出 "Docker daemon didn't responded after 20 attempts" 并指向这个文件。
三、按失败类型选取关键文件
3.1 断言 / 结果错误(AssertionError、行数不对等)
首选pytest_parallel.jsonl——它是结构化的逐测试结果,每个失败一行 JSON。按测试名(nodeid)查询并抽取关键信息:
grep -F '<test_name>' "tmp/investigate/$SHA/ci/tmp/pytest_parallel.jsonl" \ | jq 'select(.outcome == "failed") | { crash: .longrepr.reprcrash.message, lines: [.longrepr.reprtraceback.reprentries[].data.lines[]?], stderr: (.sections[] | select(.[0] == "Captured stderr call") | .[1])?, captured: (.sections[] | select(.[0] == "Captured log call") | .[1])? }'两个关键的字段陷阱:
longrepr是 JSON 对象,不是字符串。断言文本在.longrepr.reprcrash.message,回溯逐行内容在.longrepr.reprtraceback.reprentries[].data.lines[]。- pytest 捕获的输出在
.sections[]中,形如[标题, 内容]二元组,其中"Captured stderr call"与"Captured log call"两类标题分别包含 docker 调用、ClickHouse 查询、Spark 协议交互等测试期间捕获的 stderr 与日志。
另外,若只想粗看失败详情,可先拉出 longrepr 原文:
grep -F -- "<test_name>" "tmp/investigate/$SHA/ci/tmp/pytest_parallel.jsonl" \ | jq -r 'select((.longrepr // "") != "") | .longrepr'必须使用grep -F(固定字符串匹配):参数化测试名如test_foo[a]含有正则元字符,普通grep不会按字面匹配。
3.2 服务端异常 / 数据错误
目标文件是各节点的服务器日志:
tests/integration/<test_module>/_instances-conftest.py-gw<N>/node<M>/logs/clickhouse-server.log这是纯文本,位于归档内部。先只解压该成员再 grep:
tar -xf "tmp/investigate/$SHA/logs.tar.gz" -C "tmp/investigate/$SHA/" \ 'tests/integration/<test_module>/_instances-conftest.py-gw1/node1/logs/clickhouse-server.log' grep -i 'error\|exception\|fatal' \ "tmp/investigate/$SHA/tests/integration/<test_module>/_instances-conftest.py-gw1/node1/logs/clickhouse-server.log" \ | tail -50注意<N>(worker 编号)和<M>(节点编号)都要替换成实际值;一次失败的集群往往有 node1/node2 等多个节点,跨节点问题(如分布式表、副本同步)需要对照多个节点的日志时间戳。
3.3 Docker / 容器启动失败
看_instances-conftest.py-gw<N>/docker.log——这是该 worker 上docker compose up/down的原始输出,镜像拉取失败、端口冲突、compose 语法问题都会体现在这里。
结合 集成测试作业脚本 的源码可以推断出这类失败在 CI 侧的自动归类逻辑:脚本维护了一份基础设施错误模式表INFRASTRUCTURE_ERROR_PATTERNS(ci/jobs/integration_test_job.py#L639-L652),包括"Cannot connect to the Docker daemon"、"Error response from daemon"、"Connection reset by peer"、"No space left on device"、"OCI runtime create failed"等,以及超时模式"timed out after"/"TimeoutExpired"。_mark_infrastructure_errors()(ci/jobs/integration_test_job.py#L796-L809)会扫描每个测试结果,命中这些模式的失败会被打上INFRA标签并改判为SKIPPED——也就是说,如果你在 CI 报告里看到失败被标记为基础设施错误而状态是 SKIPPED,真正的证据仍在产物包的docker.log与docker-in-docker.log里,而不是测试断言本身。
3.4 详细测试轨迹(哪些查询、以什么顺序执行)
定位执行该测试的 worker 日志:
grep -l '<test_name>' tmp/investigate/$SHA/ci/tmp/pytest_parallel-gw*.log打开对应的pytest_parallel-gw<N>.log:它是该 worker 的完整 DEBUG 级输出,包含所有 docker/cluster 调用、下发的 SQL 及其顺序,适合回答"测试到底执行到了哪一步"这类问题。
四、只解压需要的成员,不全量解包
产物包可能很大(源码注释中提到一次普通归档约为 413 MiB 量级的日志,ci/jobs/integration_test_job.py#L75-L78),且部分上传可能被截断,因此推荐按成员名精确解压。例如只取 JSONL 和一个节点的服务器日志:
# 只解压 JSONL 和某个节点的 server 日志 tar -xf "tmp/investigate/$SHA/logs.tar.gz" -C "tmp/investigate/$SHA/" \ ci/tmp/pytest_parallel.jsonl \ 'tests/integration/<test_module>/_instances-conftest.py-gw1/node1/logs/clickhouse-server.log'配套的最佳实践:
解压失败本身就是一个发现。
tar报错(产物过期、包损坏、缺少zstd支持、成员不存在)应当作为结论上报,而不是让后续grep | jq静默产出空结果、被误判为"查无实据"。可以在解压后加一道校验:test -f "tmp/investigate/$SHA/ci/tmp/pytest_parallel.jsonl" \ || { echo "extraction FAILED — report the artifact problem"; false; }统一用
tar -xf自动探测格式。如前所述,URL 可能是.tar.zst;成员内容即使名为.log也可能实际是 zstd 压缩的,遇到二进制乱码时可用file命令确认。工作目录按 SHA 隔离。技能文档约定所有调查文件放在
tmp/investigate/<sha>/下(<sha>为报告 commit 的前 7 位),同一调查可重复访问而无需重新下载,不同 PR 的产物互不覆盖。
五、延伸:与整个 investigate-ci 工作流的关系
本文覆盖的产物包属于 investigate-ci 技能 中第 4 步"按需下载 harness 产物"的内容。该技能的完整流程是:从 PR / S3 报告 URL / flaky-test issue 出发,先抓取失败测试列表(node .claude/tools/fetch_ci_report.js <url> --failed --cidb),再对每个失败测试检索既有跟踪 issue 与修复 PR,然后用play.clickhouse.com的checks表查询 master 历史来区分 flaky 与真实回归,最后只对 REAL / UNCERTAIN / INFRA/BUILD 三类失败才下载并解压本文描述的产物包做根因分析。
同一技能目录下还有按作业家族划分的兄弟文档,可按需对照:
- artifacts-stateless.md —— 无状态 / Fast 测试:产物是独立文件而非整包(
clickhouse-server.log[.zst]、clickhouse-server.err.log[.zst]、stderr.log、job.log等),按--links列出的 URL 逐个抓取; - artifacts-stress.md —— 压力测试:关注
clickhouse-server.initial.log、fatal.log、hung_check.log等; - fetch_ci_report.js —— 提供
--failed、--links、--download-logs <path>、--binary、--report N等参数的报告/产物抓取工具; - 集成测试作业脚本 —— 产物包生成(
on_error_hook打包)、worker 预算模型、基础设施错误自动标注(INFRA/SKIPPED)等实现所在。
掌握以上内容后,面对任何一次 ClickHouse 集成测试 CI 失败,你都可以按"下载产物包 → 看归档布局 → 按失败类型选文件 → 精确解包 +grep/jq定位"的路径,在不需要全量解压的情况下拿到根因证据。
【免费下载链接】ClickHouseClickHouse® is a real-time analytics database management system项目地址: https://gitcode.com/GitHub_Trending/cli/ClickHouse
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考