Jelajahi Sumber

fix(browser): BrowserManager无头浏览器增加5分钟空闲自动关闭机制,减少CPU占用;服务关闭时同步关闭浏览器

ethanfly 3 hari lalu
induk
melakukan
3b83a9c46d
2 mengubah file dengan 82 tambahan dan 20 penghapusan
  1. 3 0
      server/src/app.ts
  2. 79 20
      server/src/automation/browser.ts

+ 3 - 0
server/src/app.ts

@@ -19,6 +19,7 @@ import { registerTaskExecutors } from './services/taskExecutors.js';
 import { taskQueueService, initTaskQueue } from './services/TaskQueueService.js';
 import { loginServiceManager } from './services/login/index.js';
 import { wsManager } from './websocket/index.js';
+import { BrowserManager } from './automation/browser.js';
 
 const execAsync = promisify(exec);
 
@@ -293,6 +294,8 @@ process.on('SIGTERM', async () => {
   logger.info('SIGTERM received, shutting down gracefully');
   taskScheduler.stop();
   await taskQueueService.close();
+  // 关闭所有无头/有头浏览器,释放资源
+  await BrowserManager.closeBrowser();
   httpServer.close(() => {
     logger.info('Server closed');
     process.exit(0);

+ 79 - 20
server/src/automation/browser.ts

@@ -5,33 +5,43 @@ export interface BrowserOptions {
   headless?: boolean;
 }
 
+/** 无头浏览器空闲超时(毫秒),超时后自动关闭,节省资源 */
+const HEADLESS_IDLE_TIMEOUT_MS = 5 * 60 * 1000; // 5 分钟空闲后关闭
+
 /**
  * 浏览器管理器
  * 支持有头和无头两种模式的浏览器实例
+ * 无头浏览器在空闲一定时间后自动关闭,节省资源
  */
 export class BrowserManager {
   // 有头浏览器(用于用户交互,如登录)
   private static headfulBrowser: Browser | null = null;
   private static isHeadfulInitializing = false;
-  
+
   // 无头浏览器(用于后台任务,如发布视频)
   private static headlessBrowser: Browser | null = null;
   private static isHeadlessInitializing = false;
-  
+
+  /** 无头浏览器最后使用时间戳 */
+  private static headlessLastUsed = 0;
+
+  /** 无头浏览器空闲关闭定时器 */
+  private static headlessIdleTimer: NodeJS.Timeout | null = null;
+
   /**
    * 获取浏览器实例
    * @param options.headless 是否使用无头模式,默认 false
    */
   static async getBrowser(options?: BrowserOptions): Promise<Browser> {
     const headless = options?.headless ?? false;
-    
+
     if (headless) {
       return this.getHeadlessBrowser();
     } else {
       return this.getHeadfulBrowser();
     }
   }
-  
+
   /**
    * 获取有头浏览器实例(用于用户交互)
    */
@@ -39,18 +49,18 @@ export class BrowserManager {
     if (this.headfulBrowser && this.headfulBrowser.isConnected()) {
       return this.headfulBrowser;
     }
-    
+
     // 防止并发初始化
     if (this.isHeadfulInitializing) {
       await new Promise(resolve => setTimeout(resolve, 1000));
       return this.getHeadfulBrowser();
     }
-    
+
     this.isHeadfulInitializing = true;
-    
+
     try {
       logger.info('Launching headful browser...');
-      
+
       this.headfulBrowser = await chromium.launch({
         headless: false,
         args: [
@@ -62,12 +72,12 @@ export class BrowserManager {
           '--window-size=1920,1080',
         ],
       });
-      
+
       this.headfulBrowser.on('disconnected', () => {
         logger.warn('Headful browser disconnected');
         this.headfulBrowser = null;
       });
-      
+
       logger.info('Headful browser launched successfully');
       return this.headfulBrowser;
     } catch (error) {
@@ -77,26 +87,34 @@ export class BrowserManager {
       this.isHeadfulInitializing = false;
     }
   }
-  
+
   /**
    * 获取无头浏览器实例(用于后台任务)
+   * 有空时自动关闭机制,减少资源占用
    */
   private static async getHeadlessBrowser(): Promise<Browser> {
+    // 清除之前的空闲关闭定时器
+    if (this.headlessIdleTimer) {
+      clearTimeout(this.headlessIdleTimer);
+      this.headlessIdleTimer = null;
+    }
+
     if (this.headlessBrowser && this.headlessBrowser.isConnected()) {
+      this.headlessLastUsed = Date.now();
       return this.headlessBrowser;
     }
-    
+
     // 防止并发初始化
     if (this.isHeadlessInitializing) {
       await new Promise(resolve => setTimeout(resolve, 1000));
       return this.getHeadlessBrowser();
     }
-    
+
     this.isHeadlessInitializing = true;
-    
+
     try {
       logger.info('Launching headless browser...');
-      
+
       this.headlessBrowser = await chromium.launch({
         headless: true,
         args: [
@@ -106,14 +124,22 @@ export class BrowserManager {
           '--disable-accelerated-2d-canvas',
           '--disable-gpu',
           '--window-size=1920,1080',
+          // 限制 Chromium 子进程数,降低资源占用
+          '--max-process-count=3',
         ],
       });
-      
+
+      this.headlessLastUsed = Date.now();
+
       this.headlessBrowser.on('disconnected', () => {
         logger.warn('Headless browser disconnected');
         this.headlessBrowser = null;
+        this.headlessLastUsed = 0;
       });
-      
+
+      // 启动空闲关闭定时器
+      this.scheduleIdleClose();
+
       logger.info('Headless browser launched successfully');
       return this.headlessBrowser;
     } catch (error) {
@@ -123,7 +149,30 @@ export class BrowserManager {
       this.isHeadlessInitializing = false;
     }
   }
-  
+
+  /**
+   * 安排空闲关闭定时器(无头浏览器在空闲超时后自动关闭)
+   */
+  private static scheduleIdleClose(): void {
+    if (this.headlessIdleTimer) {
+      clearTimeout(this.headlessIdleTimer);
+    }
+    this.headlessIdleTimer = setTimeout(async () => {
+      if (this.headlessBrowser && this.headlessBrowser.isConnected()) {
+        const idle = Date.now() - this.headlessLastUsed;
+        if (idle >= HEADLESS_IDLE_TIMEOUT_MS) {
+          logger.info(`[BrowserManager] Headless browser idle for ${Math.round(idle / 1000)}s, closing to save resources...`);
+          try {
+            await this.headlessBrowser.close();
+          } catch {}
+          this.headlessBrowser = null;
+          this.headlessLastUsed = 0;
+        }
+      }
+      this.headlessIdleTimer = null;
+    }, HEADLESS_IDLE_TIMEOUT_MS);
+  }
+
   /**
    * 关闭浏览器
    * @param options.headless 指定关闭哪种浏览器,不指定则关闭所有
@@ -131,9 +180,14 @@ export class BrowserManager {
   static async closeBrowser(options?: BrowserOptions): Promise<void> {
     if (options?.headless === true) {
       // 只关闭无头浏览器
+      if (this.headlessIdleTimer) {
+        clearTimeout(this.headlessIdleTimer);
+        this.headlessIdleTimer = null;
+      }
       if (this.headlessBrowser) {
         await this.headlessBrowser.close();
         this.headlessBrowser = null;
+        this.headlessLastUsed = 0;
         logger.info('Headless browser closed');
       }
     } else if (options?.headless === false) {
@@ -151,13 +205,18 @@ export class BrowserManager {
         logger.info('Headful browser closed');
       }
       if (this.headlessBrowser) {
+        if (this.headlessIdleTimer) {
+          clearTimeout(this.headlessIdleTimer);
+          this.headlessIdleTimer = null;
+        }
         await this.headlessBrowser.close();
         this.headlessBrowser = null;
+        this.headlessLastUsed = 0;
         logger.info('Headless browser closed');
       }
     }
   }
-  
+
   /**
    * 检查浏览器状态
    */
@@ -168,7 +227,7 @@ export class BrowserManager {
       return this.headfulBrowser?.isConnected() ?? false;
     }
     // 任一浏览器连接即返回 true
-    return (this.headfulBrowser?.isConnected() ?? false) || 
+    return (this.headfulBrowser?.isConnected() ?? false) ||
            (this.headlessBrowser?.isConnected() ?? false);
   }
 }