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) và 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:
- Người dùng đóng tab trình duyệt đột ngột
- Mạng bị ngắt kết nối
- Request bị timeout nhưng callback chưa được gọi
- Lỗi xảy ra trong quá trình xử lý response
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 && 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:
- Độ trễ trung bình: 420ms → 180ms (giảm 57%)
- Connection leak: 0 (trước đó 150+ connection/giờ)
- Bộ nhớ RAM: Ổn định ở 2GB (trước đó tăng không giới hạn)
- Chi phí hàng tháng: $4,200 → $680 (giảm 84%)
- Uptime: 99.9% (trước đó 94%)
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