优化浏览器池和并发配置

1. 浏览器池改为按需启动模式
   - 启动时不创建浏览器,有截图任务时才启动
   - 空闲5分钟后自动关闭浏览器释放资源

2. 修复截图并发数保存问题
   - 修复database.py中缺少保存max_screenshot_concurrent的代码

3. 去掉并发数上限限制
   - 管理员可自由设置并发数,不再限制1-20/1-5

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2025-12-10 20:31:49 +08:00
parent 7954aeaf59
commit 32a29e61e9
4 changed files with 43 additions and 36 deletions

8
app.py
View File

@@ -3444,15 +3444,15 @@ if __name__ == '__main__':
print("默认管理员: admin/admin")
print("=" * 60 + "\n")
# 同步初始化浏览器必须在socketio.run之前否则eventlet会导致asyncio冲突
# 初始化浏览器工作线程池(按需模式,启动时不创建浏览器
try:
system_cfg = database.get_system_config()
pool_size = system_cfg.get('max_screenshot_concurrent', 3) if system_cfg else 3
print(f"正在预热 {pool_size} 个浏览器实例(截图加速...")
print(f"初始化截图线程池({pool_size}个worker按需启动浏览器空闲5分钟后自动关闭...")
init_browser_worker_pool(pool_size=pool_size)
print("浏览器池初始化完成")
print("截图线程池初始化完成")
except Exception as e:
print(f"警告: 浏览器池初始化失败: {e}")
print(f"警告: 截图线程池初始化失败: {e}")
socketio.run(app, host=config.SERVER_HOST, port=config.SERVER_PORT, debug=config.DEBUG, allow_unsafe_werkzeug=True)

View File

@@ -98,26 +98,33 @@ class BrowserWorker(threading.Thread):
return self._create_browser()
def run(self):
"""工作线程主循环"""
self.log("Worker启动")
# 初始创建浏览器
if not self._create_browser():
self.log("初始浏览器创建失败Worker退出")
return
"""工作线程主循环 - 按需启动浏览器模式"""
self.log("Worker启动(按需模式,等待任务时不占用浏览器资源)")
last_task_time = 0
IDLE_TIMEOUT = 300 # 空闲5分钟后关闭浏览器
while self.running:
try:
# 从队列获取任务(带超时,以便能响应停止信号)
# 从队列获取任务(带超时,以便能响应停止信号和空闲检查
self.idle = True
task = self.task_queue.get(timeout=1)
try:
task = self.task_queue.get(timeout=10)
except queue.Empty:
# 检查是否需要关闭空闲的浏览器
if self.browser_instance and last_task_time > 0:
idle_time = time.time() - last_task_time
if idle_time > IDLE_TIMEOUT:
self.log(f"空闲{int(idle_time)}秒,关闭浏览器释放资源")
self._close_browser()
continue
self.idle = False
if task is None: # None作为停止信号
self.log("收到停止信号")
break
# 确保浏览器可用
# 按需创建或确保浏览器可用
if not self._ensure_browser():
self.log("浏览器不可用,任务失败")
task['callback'](None, "浏览器不可用")
@@ -140,20 +147,19 @@ class BrowserWorker(threading.Thread):
result = task_func(self.browser_instance, *task_args, **task_kwargs)
callback(result, None)
self.log(f"任务执行成功")
last_task_time = time.time()
except Exception as e:
self.log(f"任务执行失败: {e}")
callback(None, str(e))
self.failed_tasks += 1
last_task_time = time.time()
# 任务失败后,检查浏览器健康
if not self._check_browser_health():
self.log("任务失败导致浏览器异常,将在下次任务前重建")
self._close_browser()
except queue.Empty:
# 队列为空,继续等待
continue
except Exception as e:
self.log(f"Worker出错: {e}")
time.sleep(1)
@@ -186,12 +192,12 @@ class BrowserWorkerPool:
print(f"[浏览器池] {message}")
def initialize(self):
"""初始化工作线程池"""
"""初始化工作线程池(按需模式,启动时不创建浏览器)"""
with self.lock:
if self.initialized:
return
self.log(f"正在初始化工作线程池({self.pool_size}个worker...")
self.log(f"正在初始化工作线程池({self.pool_size}个worker,按需启动浏览器...")
for i in range(self.pool_size):
worker = BrowserWorker(
@@ -202,11 +208,8 @@ class BrowserWorkerPool:
worker.start()
self.workers.append(worker)
# 等待所有worker准备就绪
time.sleep(2)
self.initialized = True
self.log(f"✓ 工作线程池初始化完成({self.pool_size}个worker就绪)")
self.log(f"✓ 工作线程池初始化完成({self.pool_size}个worker就绪,浏览器将在有任务时按需启动")
def submit_task(self, task_func: Callable, callback: Callable, *args, **kwargs) -> bool:
"""

View File

@@ -1013,6 +1013,10 @@ def update_system_config(max_concurrent=None, schedule_enabled=None, schedule_ti
updates.append('max_concurrent_per_account = ?')
params.append(max_concurrent_per_account)
if max_screenshot_concurrent is not None:
updates.append('max_screenshot_concurrent = ?')
params.append(max_screenshot_concurrent)
if schedule_weekdays is not None:
updates.append('schedule_weekdays = ?')
params.append(schedule_weekdays)

View File

@@ -579,26 +579,26 @@
<h3 style="margin-bottom: 15px; font-size: 16px;">系统并发配置</h3>
<div class="form-group">
<label>全局最大并发数 (1-20)</label>
<label>全局最大并发数</label>
<input type="number" id="maxConcurrent" min="1" value="2" style="max-width: 200px;">
<div style="font-size: 12px; color: #666; margin-top: 5px;">
说明同时最多运行的账号数量。浏览任务使用API方式资源占用极低截图任务会启动浏览器。建议设置2-5
说明同时最多运行的账号数量。浏览任务使用API方式资源占用极低。
</div>
</div>
<div class="form-group">
<label>单账号最大并发数 (1-5)</label>
<label>单账号最大并发数</label>
<input type="number" id="maxConcurrentPerAccount" min="1" value="1" style="max-width: 200px;">
<div style="font-size: 12px; color: #666; margin-top: 5px;">
说明:单个账号同时最多运行的任务数量。建议设置1-2。
说明:单个账号同时最多运行的任务数量。
</div>
</div>
<div class="form-group">
<label>截图最大并发数 (1-5)</label>
<label>截图最大并发数</label>
<input type="number" id="maxScreenshotConcurrent" min="1" value="3" style="max-width: 200px;">
<div style="font-size: 12px; color: #666; margin-top: 5px;">
说明:同时进行截图的最大数量。截图使用浏览器建议设置2-3
说明:同时进行截图的最大数量。每个浏览器约占用200MB内存
</div>
</div>
@@ -1509,22 +1509,22 @@
const maxConcurrentPerAccount = parseInt(document.getElementById('maxConcurrentPerAccount').value);
const maxScreenshotConcurrent = parseInt(document.getElementById('maxScreenshotConcurrent').value);
if (maxConcurrent < 1 || maxConcurrent > 20) {
showNotification('全局并发数必须在1-20之间', 'error');
if (maxConcurrent < 1) {
showNotification('全局并发数必须大于0', 'error');
return;
}
if (maxConcurrentPerAccount < 1 || maxConcurrentPerAccount > 5) {
showNotification('单账号并发数必须在1-5之间', 'error');
if (maxConcurrentPerAccount < 1) {
showNotification('单账号并发数必须大于0', 'error');
return;
}
if (maxScreenshotConcurrent < 1 || maxScreenshotConcurrent > 5) {
showNotification('截图并发数必须在1-5之间', 'error');
if (maxScreenshotConcurrent < 1) {
showNotification('截图并发数必须大于0', 'error');
return;
}
if (!confirm(`确定更新并发配置吗?\n\n全局并发数: ${maxConcurrent}\n单账号并发数: ${maxConcurrentPerAccount}\n\n建议并发数影响任务执行速度过高可能触发目标服务器限制。全局建议2-5单账号建议1-2`)) return;
if (!confirm(`确定更新并发配置吗?\n\n全局并发数: ${maxConcurrent}\n单账号并发数: ${maxConcurrentPerAccount}\n截图并发数: ${maxScreenshotConcurrent}`)) return;
try {
const response = await fetch('/yuyx/api/system/config', {