✨ 优化浏览器池和并发配置
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:
8
app.py
8
app.py
@@ -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)
|
||||
|
||||
|
||||
@@ -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:
|
||||
"""
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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', {
|
||||
|
||||
Reference in New Issue
Block a user