ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

自动化流水线提效·微观篇:pytest 执行顺序优化

自动化流水线提效·微观篇:pytest 执行顺序优化 在上一篇流水线提效中我们提到“用例执行顺序优化会单独出文章讲解本文就来填这个坑。当用例数量上千、并发数开到 8 时经常会遇到一个反直觉现象并发越高反而越慢通过率也变低。根因不在并发本身而在用例执行顺序不合理导致的环境资源争抢。本文从问题定位、方案演进到最终代码实现一步步讲清如何用一段 20 行的 conftest 钩子把 pytest 用例顺序打散到文件级”。一、问题现象并发越高反而越慢某项目用例规模 ~2000 条pytest-xdist 8 进程并发。现象现象表现CPU 利用率波动大api 阶段 20%、compute 阶段 95% 反复横跳创建云主机用例集中爆发8 个 worker 同时打云平台 API触发限流用例整体耗时变长单条平均耗时从 2s → 6s通过率下降95% → 82%偶发超时增多直观观察 Jenkins 日志gw0: tests/api/test_login.py::test_login ← 低负载 gw1: tests/api/test_user.py::test_create_user ← 低负载 ... gw0: tests/compute/test_vm.py::test_create_vm ← 高负载 gw1: tests/compute/test_vm.py::test_clone_vm ← 高负载 gw2: tests/compute/test_disk.py::test_create_disk ← 高负载 ... 8 个 worker 全在打 compute云平台 API 限流资源使用呈现闲时很闲、忙时很忙的脉冲式波动——并发根本没有平滑负载。二、根因分析两个准则 一个默认行为自动化用例管理有两个不成文的准则准则 1用例按功能模块分类存放tests/ ├── api/ ← 接口测试低负载 │ ├── test_login.py │ └── test_user.py ├── compute/ ← 云主机模块高负载 │ ├── test_vm.py │ └── test_disk.py └── network/ └── test_subnet.py好处管理清晰、维护方便隐患相同功能的用例聚在一起负载特征也聚在一起准则 2pytest 默认按名称排序执行pytest 收集完用例后默认按nodeid字母顺序排序tests/api/test_login.py::test_a tests/api/test_login.py::test_b tests/api/test_user.py::test_a tests/compute/test_vm.py::test_a ← 高负载集中在这里 tests/compute/test_vm.py::test_b tests/compute/test_disk.py::test_a问题叠加并发 默认顺序xdist 默认--distload按用例逐个分发给 worker但分发顺序就是默认顺序按文件名结果所有 worker 同时从api转到compute高负载用例在同一时间窗集中爆发时间 → gw0: api_a → api_b → vm_a → vm_b → disk_a gw1: api_a → api_b → vm_a → vm_b → disk_a gw2: api_a → api_b → vm_a → vm_b → disk_a ↑ 所有 worker 同时进入高负载区这就是闲时很闲、忙时很忙的根因。三、方案一用例级随机打乱不推荐第一反应用现成插件打乱顺序即可插件粒度用法pytest-randomly用例级pip install pytest-randomlypytest-random-order用例级pytest --random-order装上后跑一遍结果更慢了。为什么反例分析假设tests/compute/test_vm.py有 3 个用例# tests/compute/test_vm.pypytest.fixture(scopemodule)defvm():module 级 fixture一次创建全文件共享vmcreate_vm()yieldvm delete_vm(vm)deftest_a(vm):...# 共用 vmdeftest_b(vm):...# 共用 vmdeftest_c(vm):...# 共用 vm单独跑文件1 次 create_vm 3 次测试 1 次 delete_vm 5 次云平台调用。用例级打乱后3 个用例被分到不同位置执行可能跨文件、跨 workerfixture 复用被破坏worker A: test_a(vm) → 创建 vm1 → 用 → 销毁 worker B: test_b(vm) → 创建 vm2 → 用 → 销毁 worker C: test_c(vm) → 创建 vm3 → 用 → 销毁变成 3 次 create_vm 3 次测试 3 次 delete_vm 9 次云平台调用。fixture 范围越大module/session用例级打乱的代价越大。四、方案二文件级随机打乱推荐正确的优化方向打乱到文件级即可文件内保持原顺序。原顺序 文件级打乱后 api/test_login.py compute/test_vm.py ← 高负载先来一个 api/test_user.py api/test_login.py compute/test_vm.py network/test_subnet.py compute/test_disk.py compute/test_disk.py network/test_subnet.py api/test_user.py好处维度收益负载分布高负载用例被打散到不同时间段避免集中爆发fixture 复用文件内顺序不变module/session fixture 仍然只跑一次环境压力云平台 API 请求被平滑到整个执行周期用一句话总结打散要恰到好处——足够打散负载峰值但不要破坏 fixture 复用。五、核心代码20 行 conftest 搞定文件级打散直接上代码加在项目根目录conftest.py# conftest.pyimportrandomdefpytest_collection_modifyitems(session,config,items:list):用于随机化打乱测试用例文件items_list[]alike[]foriinitems:i.add_marker(i.name)ifnotalikeori.parent.parent.nodeidalike[0].parent.parent.nodeid:alike.append(i)else:items_list.append(alike)alike[i]ifalike:items_list.append(alike)random.seed(80)random.shuffle(items_list)random.seed()items[:][itemforgroupinitems_listforitemingroup]用法放到conftest.py即可无需任何命令行参数。每次跑同一个种子80打出同样的顺序可复现。六、代码逐段拆解1. 钩子签名defpytest_collection_modifyitems(session,config,items:list):pytest_collection_modifyitemspytest 收集完用例后、开始执行前的钩子可修改 itemsitems所有用例对象列表按默认字母顺序排列修改items[:]即可改变执行顺序2. 按文件分组items_list[]alike[]foriinitems:i.add_marker(i.name)ifnotalikeori.parent.parent.nodeidalike[0].parent.parent.nodeid:alike.append(i)else:items_list.append(alike)alike[i]ifalike:items_list.append(alike)核心i.parent.parent.nodeid是用例所在的目录粗粒度相同目录的用例聚到一组alike碰到新目录就把当前组存起来、开启新组。最终items_list是目录块的列表items_list [ [api_test_a, api_test_b, api_test_c], ← api 目录 [compute_test_a, compute_test_b], ← compute 目录 [network_test_a], ← network 目录 ]注用i.parent.parent.nodeid祖父目录作为分组键。如果你的目录结构更深可以调整为i.parent.nodeid父目录即文件级或更上层。3. 随机打乱组random.seed(80)random.shuffle(items_list)random.seed()random.seed(80)固定种子保证每次跑顺序一致可复现 bugrandom.shuffle(items_list)打乱目录块的顺序块内顺序保持不变random.seed()重置种子避免影响后续代码的随机数4. 展平回 itemsitems[:][itemforgroupinitems_listforitemingroup]把列表的列表展平回一维列表赋值给items[:]——必须用items[:]而非items 前者是原地修改后者只是局部变量赋值对 pytest 不生效。5. 顺带加 markeri.add_marker(i.name)把每个用例的名字作为 marker 加上去方便后续用pytest -m test_a单独跑某条用例。可选删掉也不影响主逻辑。七、效果验证优化前worker 0: api_a → api_b → vm_a → vm_b → disk_a worker 1: api_a → api_b → vm_a → vm_b → disk_a ... CPU 使用率 ▁▁▂▃▆█████▆▃▂▁▁ 脉冲式 云平台 QPS ▁▁▁▁▁███▇▆▃▁▁▁ 集中爆发 通过率 82% 总耗时 24min优化后worker 0: vm_a → api_a → subnet_a → disk_a → api_b worker 1: api_b → vm_b → disk_b → api_a → subnet_b ... CPU 使用率 ▃▄▅▄▅▆▅▄▅▆▅▄▅ 平滑 云平台 QPS ▃▄▃▄▃▄▃▄▃▄▃▄▃ 均匀分布 通过率 94% 总耗时 14min关键指标改善指标优化前优化后提升总耗时24min14min-42%通过率82%94%12%CPU 利用率方差0.320.08平滑 4x云平台 API 限流次数170消除八、常见坑点速查坑现象解决用items 而非items[:]顺序没变必须原地修改种子不固定每次顺序不同bug 不可复现random.seed(80)固定分组粒度选错打散效果差或破坏 fixture按目录结构选parent或parent.parent与pytest-randomly共存两个随机冲突二选一本文方案与randomly互斥xdist--distloadscope冲突文件级打散被 scope 收回配合--distload用同文件用例依赖顺序打乱后失败本就不该有顺序依赖去掉依赖或加 marker 控session fixture 仍是热点多 worker 同时进入 fixturefixture 内部要幂等或加锁九、扩展思路1. 按 marker 加权打散让高负载 marker之间留间隔defpytest_collection_modifyitems(session,config,items):# 把高负载用例按固定间隔插入heavy[iforiinitemsifany(m.nameheavyformini.iter_markers())]light[iforiinitemsifinotinheavy]random.seed(42)random.shuffle(light)random.shuffle(heavy)# 每隔 N 个 light 插一个 heavyresult[]heavy_idx0foridx,iteminenumerate(light):result.append(item)ifidx%50andheavy_idxlen(heavy):result.append(heavy[heavy_idx])heavy_idx1items[:]result2. 按历史耗时排序记录每条用例的历史耗时长的用例先跑——早暴露问题defpytest_collection_modifyitems(session,config,items):durationssession.config.cache.get(case_durations,{})items.sort(keylambdai:-durations.get(i.nodeid,0))配合pytest --cache用。3. 配合--distloadgroup精细控制pytest.mark.group(heavy-vm)deftest_create_vm():...pytest--distloadgroup让带heavy-vmmarker 的用例集中到一个 worker避免并发打云平台。4. 失败用例优先重跑defpytest_collection_modifyitems(session,config,items):failedsession.config.cache.get(lastfailed,[])items.sort(keylambdai:i.nodeidinfailed,reverseTrue)上次失败的用例排前面早跑早暴露。5. 同目录但想拆开如果某目录用例太多单纯按目录打散不够可以按文件 序号奇偶再分foriinitems:group_key(i.parent.nodeid,hash(i.name)%2)十、总结pytest 用例执行顺序优化的核心思想打散负载峰值但保留 fixture 复用。掌握本文需要抓住三条主线根因默认字母排序 同功能聚集 负载脉冲粒度用例级打散破坏 fixture文件级/目录级打散刚刚好可复现用固定随机种子保证每次跑顺序一致bug 可复现一句话记法用例顺序问题不在打不打而在打多碎——文件级打散是性价比最高的平衡点。实践建议第一步用pytest --durations10找出最慢的 10 个用例看是不是同一目录第二步加上本文 20 行 conftest对比耗时与通过率第三步根据实际目录结构调整分组粒度parentvsparent.parent第四步把高负载用例显式打heavymarker配合loadgroup进一步精细化把执行顺序打散做对后并发才真正发挥威力——8 个 worker 才能稳定跑出 4-6 倍速而不是脉冲式争抢资源。建议动手实验在项目根conftest.py加上本文 20 行代码跑两次对比pytest --durations20输出——直观看到高负载用例被打散到不同时间段。
返回列表