Trong bài viết này, tôi sẽ chia sẻ kinh nghiệm thực chiến khi xử lý một trong những vấn đề phổ biến nhất khi triển khai WebSocket cho ứng dụng hội thoại AI: rò rỉ kết nối (connection leak)rò rỉ tài nguyên (resource leak). Đây là bài học tôi đã đúc kết từ dự án thực tế với một startup AI tại Hà Nội.

Bối cảnh: Khi hệ thống WebSocket trở thành "con voi trắng"

Một startup AI ở Hà Nội chuyên cung cấp chatbot hỗ trợ khách hàng cho các sàn thương mại điện tử đã gặp phải tình trạng kỳ lạ: server liên tục crash sau 2-3 giờ hoạt động, độ trễ tăng dần từ 200ms lên tới hơn 5 giây, và bộ nhớ RAM leo thang không ngừng. Nhà cung cấp API AI cũ của họ không có công cụ giám sát connection pool, dẫn đến việc hàng trăm kết nối WebSocket bị treo mà không được giải phóng.

Sau khi di chuyển sang HolySheep AI với công cụ giám sát connection leak tích hợp sẵn, startup này đã tiết kiệm được 85% chi phí và giảm độ trễ từ 420ms xuống còn 180ms. Trong bài viết này, tôi sẽ hướng dẫn bạn cách triển khai giải pháp tương tự.

Rò rỉ kết nối WebSocket là gì và tại sao nó nguy hiểm?

Rò rỉ kết nối WebSocket xảy ra khi một kết nối được mở nhưng không được đóng đúng cách. Trong ngữ cảnh hội thoại AI, mỗi khi người dùng nhắn tin, hệ thống sẽ mở một WebSocket connection đến API AI streaming. Nếu connection này không được cleanup khi:

Kết quả là hàng trăm, thậm chí hàng nghìn connection sẽ tích tụ trong trạng thái TIME_WAIT hoặc CLOSE_WAIT, chiếm dụng file descriptor và bộ nhớ server.

Kiến trúc giải pháp với HolySheep AI

Dưới đây là kiến trúc tôi đã triển khai cho startup ở Hà Nội, sử dụng HolySheep AI với base_url https://api.holysheep.ai/v1:

1. Thiết lập WebSocket Manager với Connection Pooling

// websocket-manager.ts - Quản lý connection với auto-cleanup
import WebSocket from 'ws';
import { EventEmitter } from 'events';

interface ConnectionEntry {
  ws: WebSocket;
  createdAt: number;
  lastPing: number;
  sessionId: string;
  isActive: boolean;
}

class HolySheepWebSocketManager extends EventEmitter {
  private connections: Map<string, ConnectionEntry> = new Map();
  private readonly MAX_IDLE_TIME = 30000; // 30 giây không hoạt động
  private readonly MAX_CONNECTIONS = 1000;
  private readonly CONNECTION_TIMEOUT = 60000; // 1 phút
  private cleanupInterval: NodeJS.Timeout | null = null;

  constructor(
    private apiKey: string,
    private baseUrl: string = 'https://api.holysheep.ai/v1'
  ) {
    super();
    this.startCleanupRoutine();
  }

  async createConnection(sessionId: string): Promise<WebSocket> {
    // Kiểm tra giới hạn connection
    if (this.connections.size >= this.MAX_CONNECTIONS) {
      await this.cleanupIdleConnections();
      if (this.connections.size >= this.MAX_CONNECTIONS) {
        throw new Error('MAX_CONNECTIONS_EXCEEDED: Vượt quá giới hạn kết nối');
      }
    }

    // Đóng connection cũ nếu có
    const existingEntry = this.connections.get(sessionId);
    if (existingEntry) {
      this.safeClose(sessionId);
    }

    return new Promise((resolve, reject) => {
      const wsUrl = ${this.baseUrl}/ws/chat?session=${sessionId};
      const ws = new WebSocket(wsUrl, {
        headers: {
          'Authorization': Bearer ${this.apiKey},
          'Content-Type': 'application/json'
        },
        handshakeTimeout: 10000
      });

      const timeout = setTimeout(() => {
        ws.terminate();
        reject(new Error('CONNECTION_TIMEOUT'));
      }, 10000);

      ws.on('open', () => {
        clearTimeout(timeout);
        const entry: ConnectionEntry = {
          ws,
          createdAt: Date.now(),
          lastPing: Date.now(),
          sessionId,
          isActive: true
        };
        this.connections.set(sessionId, entry);
        console.log([WS Manager] Connection created: ${sessionId} (Total: ${this.connections.size}));
        resolve(ws);
      });

      ws.on('error', (error) => {
        clearTimeout(timeout);
        console.error([WS Manager] Error on ${sessionId}:, error.message);
        this.safeClose(sessionId);
        reject(error);
      });

      ws.on('close', (code, reason) => {
        console.log([WS Manager] Connection closed: ${sessionId}, code: ${code}, reason: ${reason});
        this.connections.delete(sessionId);
        this.emit('connectionClosed', { sessionId, code, reason });
      });

      ws.on('pong', () => {
        const entry = this.connections.get(sessionId);
        if (entry) {
          entry.lastPing = Date.now();
          entry.isActive = true;
        }
      });
    });
  }

  private safeClose(sessionId: string): void {
    const entry = this.connections.get(sessionId);
    if (entry) {
      try {
        if (entry.ws.readyState === WebSocket.OPEN) {
          entry.ws.close(1000, 'NORMAL_CLOSURE');
        } else if (entry.ws.readyState === WebSocket.CONNECTING) {
          entry.ws.terminate();
        }
      } catch (error) {
        console.error([WS Manager] Error closing ${sessionId}:, error);
      } finally {
        this.connections.delete(sessionId);
      }
    }
  }

  private startCleanupRoutine(): void {
    // Chạy cleanup mỗi 10 giây
    this.cleanupInterval = setInterval(() => {
      this.performHealthCheck();
      this.cleanupIdleConnections();
    }, 10000);
  }

  private performHealthCheck(): void {
    const now = Date.now();
    let activeCount = 0;
    let idleCount = 0;

    this.connections.forEach((entry, sessionId) => {
      // Kiểm tra connection timeout
      if (now - entry.createdAt > this.CONNECTION_TIMEOUT) {
        console.warn([WS Manager] Connection timeout: ${sessionId});
        this.safeClose(sessionId);
        return;
      }

      // Kiểm tra idle time
      if (now - entry.lastPing > this.MAX_IDLE_TIME) {
        // Thử ping để kiểm tra
        if (entry.ws.readyState === WebSocket.OPEN) {
          entry.ws.ping();
          idleCount++;
        } else {
          this.safeClose(sessionId);
        }
      } else {
        activeCount++;
      }
    });

    if (this.connections.size > 0) {
      console.log([WS Manager] Health: ${activeCount} active, ${idleCount} idle, ${this.connections.size} total);
    }
  }

  private async cleanupIdleConnections(): Promise<number> {
    const now = Date.now();
    let cleanedCount = 0;

    for (const [sessionId, entry] of this.connections.entries()) {
      if (now - entry.lastPing > this.MAX_IDLE_TIME * 2) {
        console.log([WS Manager] Cleaning up idle connection: ${sessionId});
        this.safeClose(sessionId);
        cleanedCount++;
      }
    }

    return cleanedCount;
  }

  getStats() {
    return {
      totalConnections: this.connections.size,
      maxConnections: this.MAX_CONNECTIONS,
      utilizationPercent: (this.connections.size / this.MAX_CONNECTIONS) * 100,
      connections: Array.from(this.connections.entries()).map(([id, entry]) => ({
        sessionId: id,
        age: Date.now() - entry.createdAt,
        idleTime: Date.now() - entry.lastPing,
        isActive: entry.isActive
      }))
    };
  }

  destroy(): void {
    if (this.cleanupInterval) {
      clearInterval(this.cleanupInterval);
    }
    this.connections.forEach((_, sessionId) => this.safeClose(sessionId));
    this.removeAllListeners();
  }
}

export const wsManager = new HolySheepWebSocketManager(process.env.HOLYSHEEP_API_KEY!);

2. Client-side Implementation với Auto-reconnect

// ai-chat-client.ts - Client với connection leak prevention
class AIChatClient {
  private ws: WebSocket | null = null;
  private sessionId: string;
  private reconnectAttempts = 0;
  private readonly MAX_RECONNECT = 5;
  private readonly RECONNECT_DELAY = 1000;
  private messageQueue: any[] = [];
  private isIntentionalClose = false;

  constructor(
    private apiKey: string,
    private baseUrl: string = 'https://api.holysheep.ai/v1',
    private onMessage?: (content: string, done: boolean) => void,
    private onError?: (error: Error) => void
  ) {
    this.sessionId = this.generateSessionId();
    this.setupVisibilityHandler();
    this.setupBeforeUnloadHandler();
  }

  private generateSessionId(): string {
    return session_${Date.now()}_${Math.random().toString(36).substr(2, 9)};
  }

  private setupVisibilityHandler(): void {
    document.addEventListener('visibilitychange', () => {
      if (document.visibilityState === 'hidden') {
        // Đánh dấu là đóng có chủ đích khi tab bị ẩn
        this.isIntentionalClose = true;
      }
    });
  }

  private setupBeforeUnloadHandler(): void {
    window.addEventListener('beforeunload', () => {
      // Đánh dấu là đóng có chủ đích
      this.isIntentionalClose = true;
    });
  }

  async connect(): Promise<void> {
    return new Promise((resolve, reject) => {
      try {
        const wsUrl = ${this.baseUrl}/ws/chat;
        
        this.ws = new WebSocket(wsUrl);

        // Timeout cho connection
        const connectionTimeout = setTimeout(() => {
          if (this.ws && this.ws.readyState !== WebSocket.OPEN) {
            this.ws.terminate();
            reject(new Error('Connection timeout after 10s'));
          }
        }, 10000);

        this.ws.onopen = () => {
          clearTimeout(connectionTimeout);
          console.log([Chat Client] Connected: ${this.sessionId});
          this.reconnectAttempts = 0;
          this.flushMessageQueue();
          resolve();
        };

        this.ws.onmessage = (event) => {
          try {
            const data = JSON.parse(event.data);
            
            if (data.type === 'pong') {
              console.log('[Chat Client] Heartbeat confirmed');
              return;
            }

            if (data.type === 'error') {
              this.onError?.(new Error(data.message));
              return;
            }

            // Xử lý message từ AI
            if (data.content !== undefined) {
              this.onMessage?.(data.content, data.done || false);
            }
          } catch (parseError) {
            console.error('[Chat Client] Parse error:', parseError);
          }
        };

        this.ws.onerror = (event) => {
          console.error('[Chat Client] WebSocket error:', event);
          this.onError?.(new Error('WebSocket connection error'));
        };

        this.ws.onclose = (event) => {
          console.log([Chat Client] Connection closed: ${event.code} - ${event.reason});
          
          // Cleanup reference ngay lập tức
          this.ws = null;

          if (!this.isIntentionalClose && this.reconnectAttempts < this.MAX_RECONNECT) {
            this.attemptReconnect();
          } else if (this.isIntentionalClose) {
            console.log('[Chat Client] Intentional close - not reconnecting');
            this.isIntentionalClose = false;
          } else {
            this.onError?.(new Error('Max reconnection attempts exceeded'));
          }
        };

      } catch (error) {
        reject(error);
      }
    });
  }

  private attemptReconnect(): void {
    this.reconnectAttempts++;
    const delay = this.RECONNECT_DELAY * Math.pow(2, this.reconnectAttempts - 1);
    
    console.log([Chat Client] Attempting reconnect ${this.reconnectAttempts}/${this.MAX_RECONNECT} in ${delay}ms);
    
    setTimeout(async () => {
      try {
        await this.connect();
      } catch (error) {
        console.error('[Chat Client] Reconnect failed:', error);
      }
    }, delay);
  }

  sendMessage(content: string): void {
    const message = {
      type: 'chat',
      session_id: this.sessionId,
      content,
      timestamp: Date.now()
    };

    if (this.ws && this.ws.readyState === WebSocket.OPEN) {
      this.ws.send(JSON.stringify(message));
    } else {
      // Queue message nếu chưa connected
      this.messageQueue.push(message);
    }
  }

  private flushMessageQueue(): void {
    while (this.messageQueue.length > 0 &amp;&amp; this.ws?.readyState === WebSocket.OPEN) {
      const message = this.messageQueue.shift();
      this.ws.send(JSON.stringify(message));
    }
  }

  sendHeartbeat(): void {
    if (this.ws && this.ws.readyState === WebSocket.OPEN) {
      this.ws.send(JSON.stringify({ type: 'ping', timestamp: Date.now() }));
    }
  }

  close(): void {
    this.isIntentionalClose = true;
    if (this.ws) {
      this.ws.close(1000, 'Client initiated close');
      this.ws = null;
    }
    this.messageQueue = [];
  }

  // Giải phóng tài nguyên khi component unmount
  destroy(): void {
    console.log('[Chat Client] Destroying client and cleaning up resources');
    this.isIntentionalClose = true;
    
    if (this.ws) {
      try {
        this.ws.close(1000, 'Component destroyed');
      } catch (e) {
        // Ignore close errors
      }
      this.ws = null;
    }
    
    this.messageQueue = [];
    this.onMessage = undefined;
    this.onError = undefined;
    
    // Xóa event listeners
    window.removeEventListener('beforeunload', () => {});
    document.removeEventListener('visibilitychange', () => {});
  }
}

// Sử dụng với React Hook
import { useEffect, useRef, useCallback } from 'react';

export function useAIChat(apiKey: string) {
  const clientRef = useRef<AIChatClient | null>(null);
  const heartbeatInterval = useRef<NodeJS.Timeout>();

  useEffect(() => {
    const client = new AIChatClient(
      apiKey,
      'https://api.holysheep.ai/v1',
      (content, done) => {
        // Xử lý message
      },
      (error) => {
        console.error('Chat error:', error);
      }
    );

    client.connect().catch(console.error);
    clientRef.current = client;

    // Heartbeat mỗi 25 giây
    heartbeatInterval.current = setInterval(() => {
      client.sendHeartbeat();
    }, 25000);

    return () => {
      if (heartbeatInterval.current) {
        clearInterval(heartbeatInterval.current);
      }
      client.destroy();
      clientRef.current = null;
    };
  }, [apiKey]);

  const sendMessage = useCallback((content: string) => {
    clientRef.current?.sendMessage(content);
  }, []);

  return { sendMessage };
}

3. Server-side Monitoring Dashboard

// metrics-server.ts - Server giám sát connection leak
import express from 'express';
import { wsManager } from './websocket-manager';

const app = express();
app.use(express.json());

// Endpoint lấy thống kê real-time
app.get('/api/ws/stats', (req, res) => {
  const stats = wsManager.getStats();
  res.json({
    timestamp: new Date().toISOString(),
    ...stats,
    alerts: []
  });
});

// Endpoint kiểm tra connection leak
app.get('/api/ws/health', async (req, res) => {
  const stats = wsManager.getStats();
  
  const alerts: string[] = [];
  
  // Kiểm tra utilization
  if (stats.utilizationPercent > 80) {
    alerts.push(WARNING: Connection utilization at ${stats.utilizationPercent.toFixed(1)}%);
  }
  
  // Kiểm tra connections cũ
  const oldConnections = stats.connections.filter(c => c.age > 300000); // > 5 phút
  if (oldConnections.length > 10) {
    alerts.push(WARNING: ${oldConnections.length} connections older than 5 minutes);
  }

  // Kiểm tra memory leak tiềm năng
  const memoryUsage = process.memoryUsage();
  if (memoryUsage.heapUsed / memoryUsage.heapTotal > 0.9) {
    alerts.push(CRITICAL: Heap usage at ${((memoryUsage.heapUsed / memoryUsage.heapTotal) * 100).toFixed(1)}%);
  }

  res.json({
    status: alerts.some(a => a.startsWith('CRITICAL')) ? 'CRITICAL' 
           : alerts.length > 0 ? 'WARNING' : 'HEALTHY',
    timestamp: new Date().toISOString(),
    stats,
    alerts,
    memory: {
      heapUsed: Math.round(memoryUsage.heapUsed / 1024 / 1024),
      heapTotal: Math.round(memoryUsage.heapTotal / 1024 / 1024),
      rss: Math.round(memoryUsage.rss / 1024 / 1024)
    }
  });
});

// Endpoint force cleanup
app.post('/api/ws/cleanup', async (req, res) => {
  const cleanedCount = await wsManager.cleanupIdleConnections();
  res.json({ cleanedConnections: cleanedCount, timestamp: new Date().toISOString() });
});

// Endpoint health check cho load balancer
app.get('/health', (req, res) => {
  const stats = wsManager.getStats();
  res.json({
    status: 'ok',
    connections: stats.totalConnections,
    uptime: process.uptime()
  });
});

const PORT = process.env.METRICS_PORT || 3001;
app.listen(PORT, () => {
  console.log([Metrics Server] Running on port ${PORT});
});

Bảng giá so sánh: HolySheep AI vs nhà cung cấp cũ

Đây là lý do quan trọng khiến startup ở Hà Nội chuyển sang HolySheep AI:

Model Giá cũ ($/MTok) Giá HolySheep ($/MTok) Tiết kiệm
GPT-4.1 $60 $8 86.7%
Claude Sonnet 4.5 $90 $15 83.3%
Gemini 2.5 Flash $15 $2.50 83.3%
DeepSeek V3.2 $3 $0.42 86%

Với tỷ giá ¥1 = $1, startup này đã tiết kiệm được $3,520/tháng chỉ riêng chi phí API.

Kết quả sau 30 ngày triển khai

Sau khi triển khai giải pháp WebSocket leak detection trên HolySheep AI:

Lỗi thường gặp và cách khắc phục

1. Lỗi "Too many open files" - File Descriptor Leak

Mô tả: Server crash với lỗi EMFILE khi số lượng connection vượt giới hạn file descriptor của hệ điều hành (thường là 1024 hoặc 4096).

Nguyên nhân gốc: WebSocket connection không được đóng đúng cách, dẫn đến file descriptor không được giải phóng.

# Kiểm tra giới hạn file descriptor hiện tại
ulimit -n

Tăng giới hạn tạm thời

ulimit -n 65535

Để tăng vĩnh viễn, thêm vào /etc/security/limits.conf

* soft nofile 65535

* hard nofile 65535

Kiểm tra số connection đang mở

lsof -p <PID> | wc -l

Kiểm tra connection theo trạng thái

ss -s

hoặc

netstat -an | awk '/^tcp/ {A[$NF]++} END {for (k in A) print k, A[k]}'

Mã khắc phục trong Node.js:

// server.ts - Thiết lập giới hạn và cleanup đúng cách
import { createServer } from 'http';
import { WebSocketServer } from 'ws';

const PORT = process.env.PORT || 3000;

// Tăng giới hạn listener
process.setMaxListeners(100);

const server = createServer((req, res) => {
  // Health check endpoint
  if (req.url === '/health') {
    res.writeHead(200, { 'Content-Type': 'application/json' });
    res.end(JSON.stringify({ status: 'ok', uptime: process.uptime() }));
  }
});

const wss = new WebSocketServer({ 
  server,
  maxPayload: 1024 * 1024, // 1MB max message
  clientTracking: true
});

// Theo dõi số lượng client
let activeClients = 0;

wss.on('connection', (ws, req) => {
  activeClients++;
  console.log([Server] Client connected. Total: ${activeClients});

  // Timeout cho connection
  const connectionTimeout = setTimeout(() => {
    if (ws.readyState === ws.CONNECTING) {
      ws.terminate();
      console.warn('[Server] Connection timeout - terminated');
    }
  }, 10000);

  ws.on('open', () => clearTimeout(connectionTimeout));

  ws.on('close', (code, reason) => {
    activeClients--;
    console.log([Server] Client disconnected. Total: ${activeClients}, Code: ${code});
  });

  ws.on('error', (error) => {
    console.error('[Server] WebSocket error:', error);
  });

  // Heartbeat để giữ connection alive
  const heartbeatInterval = setInterval(() => {
    if (ws.readyState === ws.OPEN) {
      ws.ping();
    } else {
      clearInterval(heartbeatInterval);
    }
  }, 30000);

  ws.on('close', () => clearInterval(heartbeatInterval));

  // Xử lý message
  ws.on('message', (data) => {
    try {
      const message = JSON.parse(data.toString());
      // Xử lý message...
    } catch (error) {
      ws.send(JSON.stringify({ error: 'Invalid message format' }));
    }
  });
});

// Graceful shutdown
process.on('SIGTERM', () => {
  console.log('[Server] SIGTERM received - closing gracefully');
  
  wss.clients.forEach((client) => {
    client.close(1001, 'Server shutting down');
  });

  server.close(() => {
    console.log('[Server] HTTP server closed');
    process.exit(0);
  });

  // Force close sau 10s
  setTimeout(() => {
    console.error('[Server] Could not close connections in time, forcefully shutting down');
    process.exit(1);
  }, 10000);
});

server.listen(PORT, () => {
  console.log([Server] Listening on port ${PORT});
});

2. Lỗi "WebSocket connection closed unexpectedly" - Memory Leak

Mô tả: Bộ nhớ server tăng dần theo thời gian cho đến khi OOM (Out of Memory) kill process.

Nguyên nhân gốc: Các object listener, buffer, hoặc message queue không được giải phóng khi connection đóng.

// memory-leak-fix.ts - Cách tránh memory leak trong WebSocket
class NonLeakingWebSocketHandler {
  private ws: WebSocket;
  private messageBuffer: Buffer[] = [];
  private listeners: Map<string, Function[]> = new Map();
  private cleanupTimeout: NodeJS.Timeout | null = null;
  
  constructor(ws: WebSocket) {
    this.ws = ws;
    this.setupCleanup();
  }

  private setupCleanup(): void {
    // Cleanup khi connection đóng
    this.ws.on('close', () => {
      this.cleanup();
    });

    this.ws.on('error', () => {
      this.cleanup();
    });

    // Auto-cleanup sau 5 phút không hoạt động
    this.cleanupTimeout = setTimeout(() => {
      if (this.ws.readyState !== WebSocket.OPEN) {
        this.cleanup();
      }
    }, 5 * 60 * 1000);
  }

  on(event: string, callback: Function): void {
    if (!this.listeners.has(event)) {
      this.listeners.set(event, []);
    }
    this.listeners.get(event)!.push(callback);
    
    // Thêm listener vào ws
    this.ws.on(event, callback);
  }

  // QUAN TRỌNG: Xóa listener khi không cần
  off(event: string, callback: Function): void {
    const callbacks = this.listeners.get(event);
    if (callbacks) {
      const index = callbacks.indexOf(callback);
      if (index > -1) {
        callbacks.splice(index, 1);
        this.ws.off(event, callback);
      }
    }
  }

  send(data: any): void {
    if (this.ws.readyState === WebSocket.OPEN) {
      const message = typeof data === 'string' ? data : JSON.stringify(data);
      this.ws.send(message);
    }
  }

  // QUAN TRỌNG: Cleanup method để giải phóng bộ nhớ
  private cleanup(): void {
    console.log('[Handler] Cleaning up resources');
    
    // Xóa buffer
    this.messageBuffer = [];
    
    // Xóa timeout
    if (this.cleanupTimeout) {
      clearTimeout(this.cleanupTimeout);
      this.cleanupTimeout = null;
    }

    // Xóa tất cả listeners
    this.listeners.forEach((callbacks, event) => {
      callbacks.forEach(callback => {
        this.ws.off(event, callback);
      });
    });
    this.listeners.clear();

    // Xóa tham chiếu
    this.ws = null as any;
  }

  destroy(): void {
    this.cleanup();
  }
}

// Sử dụng WeakMap để theo dõi handlers mà không gây leak
const handlers = new WeakMap<WebSocket, NonLeakingWebSocketHandler>();

wss.on('connection', (ws) => {
  const handler = new NonLeakingWebSocketHandler(ws);
  handlers.set(ws, handler);

  ws.on('close', () => {
    const handler = handlers.get(ws);
    handler?.destroy();
    handlers.delete(ws);
  });
});

3. Lỗi "Connection timeout in pool" - Connection Pool Exhaustion

Mô tả: Request mới không được xử lý vì tất cả connection trong pool đã bị chiếm dụng bởi các connection "zombie".

Nguyên nhân gốc: Connection không được return về pool sau khi sử dụng, thường do exception không được catch.

// connection-pool-safe.ts - Connection pool với error handling đúng cách
import { Pool, PoolConnection } from 'mysql2/promise';

class SafeConnectionPool {
  private pool: Pool;
  private readonly POOL_SIZE = 20;
  private activeConnections: Set<PoolConnection> = new Set();
  private