aggregator —— 并发抓取聚合器(第 3 周小项目)¶
给一批 URL,并发抓取 JSON,限流、超时、重试,输出汇总(终端表格 / JSON / CSV),并对比"顺序 / 线程池 / asyncio"三种实现的耗时。自带桩服务器(asyncio 写的假 API,可配置延迟与失败率),演示、测试、基准全部打本地,不依赖外网。规格见 plan/week3.md 末节。
运行¶
uv sync # 仓库根目录执行一次
uv run aggregator serve-stub --port 8001 --delay 0.05 --fail-rate 0.2 # 终端 1:起桩服务器
uv run aggregator fetch --count 50 --limit 8 --timeout 2 -o summary.json # 终端 2:抓取并导出
uv run aggregator bench --count 30 --limit 10 # 三种实现对比
uv run pytest projects/aggregator --cov=aggregator --cov-report=term-missing
uv run ruff check projects/aggregator
配置前缀 AGG_(见 .env.example):AGG_BASE_URL、AGG_COUNT、AGG_LIMIT、AGG_TIMEOUT、AGG_RETRIES、AGG_MAX_WORKERS、AGG_LOG_LEVEL。
结构与你要做的事¶
| 文件 | 状态 |
|---|---|
settings.py models.py logging_config.py stub_server.py cli.py |
已完整,读懂即可(stub_server.py 本身就是一个 asyncio 服务器范例) |
client.py |
TODO:make_async_client / make_sync_client / classify_error / fetch_one / fetch_one_sync(tenacity 重试、错误分类、永不抛异常) |
engine.py |
TODO:build_urls / fetch_all_async(TaskGroup + Semaphore)/ fetch_all_threaded / fetch_all_sequential |
report.py |
TODO:percentile / summarize / results_table / summary_table / write_json / write_csv(原子写) |
tests/ |
已完整:test_client.py、test_engine.py 用 MockTransport 离线跑;test_stub_server.py、test_integration.py、test_cli.py 打会话级本地桩服务器 |
顺序建议:client.py → engine.py → report.py,每写完一个文件跑对应测试。骨架里被删掉的 import(asyncio、tenacity、ThreadPoolExecutor、csv、json、os、Counter…)由你自己加回来。
验收¶
uv run pytest projects/aggregator --cov=aggregator全绿,覆盖率 ≥80%(参考答案 94%)ruff check projects/aggregator零警告;Pylance 零错误bench --count 30:线程池比顺序快 ≥5 倍;asyncio 版至少快 2 倍fail-rate 0.2时成功率因重试明显高于 80%;Ctrl+C能干净退出- ≥8 次 Conventional Commits 推送到 cnb.cool
一个真实的性能观察(第 3 周 M16 的活教材)¶
参考答案在这台 Windows 机器上的 bench --count 30 --limit 10 实测:顺序 ≈ 1600 ms,线程池 ≈ 220 ms(7×),asyncio ≈ 1000 ms(只有 2×)。排查过程记录在这里,因为它比"asyncio 一定最快"这句话更有价值:
- 先怀疑桩服务器:标准库
ThreadingHTTPServer换成 asyncio 服务器(现在的stub_server.py)——数字几乎没变,排除。 - 再用裸
asyncio.open_connection直连桩服务器:30 个并发 70 ms——asyncio 本身没问题。 - 用 httpx
AsyncClient顺序发 5 个请求:每个约 73 ms,而服务器只睡 50 ms——httpx 异步栈在本机每个请求多花约 20 ms 的 CPU,30 个并发时这些开销在事件循环里串行累加,就成了 ~450 ms 起步。 - 同步
httpx.Client的每请求开销小得多,所以线程池版本快。
结论:这是本机 httpx 异步实现的开销问题,不是你的代码写错了;同一份代码在 H20 的 Linux 上重跑,通常 asyncio 与线程池接近。性能要测,不要猜;测出反直觉的结果,先隔离变量再下结论。
参考答案:exercises/solutions/week3/aggregator_solution/src/aggregator/。重置为骨架:uv run python tools/aggregator_skeleton.py;装回答案对照运行:uv run python tools/aggregator_skeleton.py --solution。