浏览器重启逻辑

This commit is contained in:
2026-06-08 16:31:53 +08:00
parent 518c6b3c7a
commit a517e8c442
4 changed files with 182 additions and 0 deletions
+2
View File
@@ -53,6 +53,8 @@ class Settings(BaseSettings):
browser_channel: str | None = "chrome"
browser_pool_size: int = 3
max_site_concurrency: int = 5 # 最大并发页面数
browser_recycle_after_contexts: int = 200 # 浏览器累计创建上下文达到该值后主动回收,0 关闭
browser_recycle_after_seconds: float = 3600.0 # 浏览器运行超过该秒数后主动回收,0 关闭
page_acquire_timeout_seconds: float = 60.0 # 获取页面槽位超时
request_timeout_seconds: float = 120.0 # 页面请求超时
cloudflare_wait_timeout_seconds: float = 60.0 # 等待 Cloudflare 挑战超时
+61
View File
@@ -32,6 +32,10 @@ class BrowserPool:
self._startup_error: str | None = None
self._retained_contexts: list[Any] = []
self._retained_pages: list[Any] = []
# 主动回收相关计数:累计创建的上下文数、本次浏览器启动时刻、当前在用上下文数
self._contexts_since_start = 0
self._browser_started_at: float | None = None
self._active_contexts = 0
@property
def enabled(self) -> bool:
@@ -95,6 +99,8 @@ class BrowserPool:
)
self._started = True
self._startup_error = None
self._contexts_since_start = 0
self._browser_started_at = asyncio.get_running_loop().time()
logger.info("浏览器启动完成")
except Exception as exc:
self._playwright = None
@@ -163,6 +169,51 @@ class BrowserPool:
await self._restart_browser("browser is disconnected before acquiring page session")
def _recycle_due(self) -> bool:
"""判断浏览器是否达到主动回收阈值(累计上下文数或运行时长任一超限)。"""
max_contexts = self._settings.browser_recycle_after_contexts
if max_contexts > 0 and self._contexts_since_start >= max_contexts:
return True
max_age = self._settings.browser_recycle_after_seconds
if max_age > 0 and self._browser_started_at is not None:
elapsed = asyncio.get_running_loop().time() - self._browser_started_at
if elapsed >= max_age:
return True
return False
async def _maybe_recycle(self) -> None:
"""达到回收阈值且当前无在用上下文时,主动重建浏览器以释放累积内存。
采用「空闲时回收」策略:若仍有其他会话在使用上下文,则跳过本次,
交由后续空闲时机处理,避免关闭浏览器时打断进行中的请求。
"""
if not self._recycle_due():
return
async with self._restart_lock:
# 持锁后二次确认,避免与并发回收/重建重复执行
if not self._recycle_due():
return
if self._active_contexts > 0:
logger.debug(
"浏览器达到回收阈值但仍有 %s 个上下文在用,延迟回收",
self._active_contexts,
)
return
logger.info(
"浏览器达到回收阈值,主动重建:contexts=%s elapsed=%.0fs",
self._contexts_since_start,
0.0 if self._browser_started_at is None
else asyncio.get_running_loop().time() - self._browser_started_at,
)
await self.close()
await self.start()
if not self._is_browser_connected():
raise BrowserUnavailableError(self._startup_error or "Browser recycle failed")
@staticmethod
def _should_retry_context_creation(exc: Exception) -> bool:
"""识别 Playwright driver 断链类错误,允许执行一次重建后重试。"""
@@ -240,7 +291,10 @@ class BrowserPool:
context = None
page = None
context_counted = False
try:
# 创建上下文前检查是否需要主动回收(仅在空闲时执行,不打断进行中的会话)
await self._maybe_recycle()
for attempt in range(2):
try:
context = await self._browser.new_context(
@@ -258,6 +312,11 @@ class BrowserPool:
continue
raise
# 上下文创建成功,计入回收阈值统计并标记为在用
self._contexts_since_start += 1
self._active_contexts += 1
context_counted = True
if self._settings.browser_stealth_enabled:
try:
from playwright_stealth import Stealth
@@ -296,4 +355,6 @@ class BrowserPool:
await page.close()
if context is not None:
await context.close()
if context_counted:
self._active_contexts -= 1
self._semaphore.release()