fix: cninfo 抓取限制浏览器并发,修复整机冻结 (06:00 任务压穿内存)

根因: _render_page 每次新建完整 headless Chromium(实测 857MB/实例),
crawl_watchlist 对 15 只股票全量并发 → 峰值需求 ≈12.5GB ≫ 7.9GB RAM,
2026-09-06/08/10 三次 06:0x 整机冻结(load 65 → 看门狗复位)。

- crawler/cninfo.py: 新增 MAX_RENDER_CONCURRENCY(默认 2,env 可配)
  + 模块级 _render_sem 信号量,渲染全程持槽;顺带清理 3 个死导入
- tests/test_cninfo.py: 新增 8 个(并发上限/串行/异常释放/env 解析)
- 实测: 峰值 1796MB / 22 进程 / load 2.75 / 耗时 441s(上限 900s)
- 全量 291 passed
This commit is contained in:
2026-09-10 20:54:30 +08:00
parent 6dede790c6
commit 2eaea2ee81
4 changed files with 251 additions and 7 deletions
+44
View File
@@ -4,6 +4,50 @@
---
## 本次完成 (2026-09-10) — cninfo 抓取压穿内存导致整机冻结的修复
**现象**:2026-09-06 / 09-08 / 09-10 连续三次早上 06:0x 整机冻结,看门狗(硬件 2min)硬复位。
**根因(证据链闭合)**:
- 三次冻结时刻 = `cninfo` 公告管道 06:00 定时任务:`logs/scheduler_error.log` 显示 09-10 06:00:03.180~.888 **0.7 秒内打印 15 条「抓取…公告」**(15 只股票协程同时进入渲染),随后日志全无直到 06:14:08 重启
- `data/raw/cninfo/` 缺 20260906/08/10 三个目录(存续目录 mtime 均为 06:02,而落盘在 `_save_items()` 中、抓取全部完成后才执行 → 崩在写数据之前)
- `pcp-pmie` 09-10 06:01:20 报 **load 65**(4 核);DNS 全面超时(frpc/dockerd resolver)
- `journalctl --list-boots` 与三次重启时刻吻合
**代码缺陷**(`crawler/cninfo.py`):
1. `_render_page` **每次调用都 `async with AsyncWebCrawler(...)` 新建完整 Chromium**(最贵的错误)
2. `crawl_watchlist` 用 `asyncio.as_completed` 对 watchlist **全量并发**(15 只)
3. **无任何并发限制**;service 亦无资源限制(`CPUQuota=infinity`、`TasksMax=9626`)
4. 本机 `cgroup_disable=memory` → **`MemoryMax` 不可用**(只会 `cpuset cpu io pids`)
**实测(本机 8GB)**:基线 chrome=0 → **2 并发峰值 1713MB / 20 进程 = 857MB/实例**;外推 15 并发 ≈**12.5GB** ≫ 7.9GB RAM → 必然压穿(时好时坏是 zram 与时序侥幸)
**修复(第一步:限并发)**:
- `crawler/cninfo.py`:新增 `MAX_RENDER_CONCURRENCY`(默认 **2**,env `CNINFO_RENDER_CONCURRENCY` 可覆盖)+ 模块级 `_render_sem` 信号量,`_render_page` 全程持槽(含浏览器启停)
- 顺带清理 3 个既有死导入(`time`/`datetime`/`Any`)
**实测验证(2026-09-10 20:43 实跑 `a-share cninfo`)**:
| 指标 | 修复前(15 并发) | 修复后(2 并发) |
|------|----------------|---------------|
| Chromium 内存峰值 | ≈12.5GB(外推) | **1796 MB** |
| 进程峰值 | ≈150 | **22** |
| load 峰值 | **65** | **2.75** |
| available 最低 | 压穿冻结 | **3561 MB** |
| 耗时 | 崩(无 END) | **441 s**(上限 900s) |
| 结果 | 无数据 | 抓 34 条/存 23 条,补上 09-10 缺失目录 ✅ |
**测试**:新增 `tests/test_cninfo.py` 8 个(并发上限/串行/异常释放槽位/env 解析);全量 **291 passed**(3 个 crawler 基线失败无关);ruff 干净
**遗留与后续优化**:
- 耗时由 127~145s 增至 441s(用时间换内存安全),`cninfo_crawl` 超时 900s 余量由 6 倍降至 2 倍;如嫌慢可 `CNINFO_RENDER_CONCURRENCY=3`(峰值约 2.6GB,仍安全)
- **第二步(未做)**:复用单个 `AsyncWebCrawler` + `arun_many()` 批量渲染(浏览器组 15→1,可同时提速降内存)
- **第三步(未做)**:`_fetch_irm_requests()` 是同步 `requests.get` 却在 async 中直接调用,**阻塞事件循环**;可改 `asyncio.to_thread`
- 长期:改用 cninfo 公告 POST JSON API(`hisAnnouncement/query`)彻底去掉浏览器依赖
- 未执行 systemd 资源限制:`CPUQuota` 对内存型崩溃基本无效(反而延长驻留),`TasksMax` 过小会让抓取永久失败
---
## 本次完成 (2026-08-23) — MCP 新闻查询服务确认与修复
**用户需求**:实现新闻查询 MCP 服务(阅读文档步骤,确认是否已实现)。