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:
+39
-6
@@ -19,10 +19,8 @@ import hashlib
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import time
|
||||
from datetime import date, datetime, timedelta
|
||||
from datetime import date, timedelta
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import requests
|
||||
from bs4 import BeautifulSoup
|
||||
@@ -48,6 +46,34 @@ MAX_IRM_ITEMS = 20
|
||||
# 请求间隔(秒)
|
||||
REQUEST_DELAY = 0.5
|
||||
|
||||
# 浏览器渲染并发上限。
|
||||
# 每次 _render_page 都会启动一个完整的 headless Chromium(实测约 850MB / 10 进程),
|
||||
# 而 watchlist 是 15 只股票全量并发 → 峰值需求 ≈12.5GB,远超本机 7.9GB RAM,
|
||||
# 靠 zram swap 侥幸时好时坏,一旦压穿即整机冻结(2026-09-06/08/10 早崩根因)。
|
||||
# 限流到 2 后峰值 ≈1.7GB。可用 env CNINFO_RENDER_CONCURRENCY 覆盖。
|
||||
DEFAULT_RENDER_CONCURRENCY = 2
|
||||
|
||||
|
||||
def _resolve_render_concurrency() -> int:
|
||||
"""解析渲染并发上限(env CNINFO_RENDER_CONCURRENCY 可覆盖,非法值回退默认)。"""
|
||||
raw = os.environ.get("CNINFO_RENDER_CONCURRENCY", "")
|
||||
if not raw.strip():
|
||||
return DEFAULT_RENDER_CONCURRENCY
|
||||
try:
|
||||
return max(1, int(raw))
|
||||
except ValueError:
|
||||
logger.warning(
|
||||
"CNINFO_RENDER_CONCURRENCY 非法({!r}),回退默认 {}",
|
||||
raw, DEFAULT_RENDER_CONCURRENCY,
|
||||
)
|
||||
return DEFAULT_RENDER_CONCURRENCY
|
||||
|
||||
|
||||
MAX_RENDER_CONCURRENCY = _resolve_render_concurrency()
|
||||
|
||||
# 渲染信号量:限制同时存活的 Chromium 实例数(防内存压穿)
|
||||
_render_sem = asyncio.Semaphore(MAX_RENDER_CONCURRENCY)
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# 工具函数
|
||||
@@ -91,7 +117,11 @@ def _build_stock_url(code: str, org_id: str) -> str:
|
||||
|
||||
async def _render_page(url: str, timeout_ms: int = 60000,
|
||||
delay_ms: int = 20) -> str:
|
||||
"""用 Crawl4AI 渲染 SPA 页面,返回 HTML 字符串。"""
|
||||
"""用 Crawl4AI 渲染 SPA 页面,返回 HTML 字符串。
|
||||
|
||||
通过模块级信号量 _render_sem 限制并发:每次渲染都会启动一个完整的
|
||||
headless Chromium(约 850MB),必须限流以免同时驻留过多浏览器压穿内存。
|
||||
"""
|
||||
from crawl4ai import AsyncWebCrawler, BrowserConfig, CacheMode, CrawlerRunConfig
|
||||
|
||||
bconf = BrowserConfig(headless=True, verbose=False)
|
||||
@@ -100,8 +130,11 @@ async def _render_page(url: str, timeout_ms: int = 60000,
|
||||
page_timeout=timeout_ms,
|
||||
delay_before_return_html=delay_ms,
|
||||
)
|
||||
async with AsyncWebCrawler(config=bconf) as c:
|
||||
result = await c.arun(url=url, config=rconf)
|
||||
# 限流:等待空闲渲染槽位(槽位内包含浏览器启动→渲染→关闭全过程)
|
||||
async with _render_sem:
|
||||
logger.debug("获得渲染槽位(并发上限 {}): {}", MAX_RENDER_CONCURRENCY, url[:70])
|
||||
async with AsyncWebCrawler(config=bconf) as c:
|
||||
result = await c.arun(url=url, config=rconf)
|
||||
return getattr(result, "html", "") or ""
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user