22:47 ngày 11/11, tôi đang ngồi canh dashboard CSKH của sàn thương mại điện tử X. đợt peak "Lễ độc thân". Lưu lượng chatbot AI nhảy từ 800 RPM lên 4.700 RPM chỉ trong 8 phút. Tôi - tác giả blog kỹ thuật HolySheep AI, cũng là kỹ sư lead phụ trách tích hợp - đã thấy log stream closed unexpectedly tràn ngập. Nguyên nhân không phải vì LLM chậm, mà vì gateway upstream của chúng tôi đang nuốt mất chunk SSE sau khi connection HTTP giữ im lặng quá 30 giây. Đó là lúc tôi viết lại toàn bộ lớp relay để xử lý timeout phản hồi SSE streaming từ HolySheep API. Bài viết hôm nay là phiên bản chỉn chu của những dòng code "sinh tử" đó.
1. Vì sao SSE streaming lại dễ chết vì timeout?
Server-Sent Events (SSE) là giao thức một chiều, server đẩy từng chunk data: {...}\n\n về client. Khác với WebSocket, SSE không có cơ chế ping nội tại và phụ thuộc hoàn toàn vào TCP keep-alive. Khi LLM sinh token quá chậm (ví dụ chain-of-thought dài 4.000 token), hoặc khi gateway trung gian đặt proxy_read_timeout thấp, kết nối sẽ bị đóng từ phía trước khi chunk cuối cùng kịp về.
Trong relay tới HolySheep AI, tôi nhận ra ba điểm nghẽn cốt lõi:
- Inactivity timeout: client đóng nếu không nhận byte nào trong N giây, dù stream vẫn sống ở upstream.
- Total stream timeout: tổng thời gian stream vượt ngưỡng (ví dụ 120s cho prompt lớn).
- Heartbeat spoofing: cần chèn comment
: heartbeat\n\nđịnh kỳ để đánh lừa proxy trung gian.
2. Mã nguồn tham chiếu - Node.js Relay có chống timeout
// File: relay/holysheep-sse.mjs
import express from 'express';
import fetch from 'node-fetch';
const HOLYSHEEP_URL = 'https://api.holysheep.ai/v1';
const API_KEY = process.env.HOLYSHEEP_API_KEY || 'YOUR_HOLYSHEEP_API_KEY';
// Ngưỡng chống timeout (ms)
const HEARTBEAT_MS = 15_000; // đẩy comment mỗi 15s
const INACTIVITY_MS = 45_000; // đóng nếu upstream im 45s
const TOTAL_BUDGET_MS = 180_000; // tổng budget 180s
export function createRelay() {
const app = express();
app.use(express.json({ limit: '2mb' }));
app.post('/v1/chat/completions', async (req, res) => {
const isStream = req.body.stream === true;
const t0 = Date.now();
// Header bắt buộc cho SSE
res.setHeader('Content-Type', 'text/event-stream; charset=utf-8');
res.setHeader('Cache-Control', 'no-cache, no-transform');
res.setHeader('Connection', 'keep-alive');
res.setHeader('X-Accel-Buffering', 'no'); // tắt buffer trên nginx
res.flushHeaders?.();
const controller = new AbortController();
const safetyTimer = setTimeout(() => controller.abort(), TOTAL_BUDGET_MS);
try {
const upstream = await fetch(${HOLYSHEEP_URL}/chat/completions, {
method: 'POST',
headers: {
'Authorization': Bearer ${API_KEY},
'Content-Type': 'application/json',
'Accept': isStream ? 'text/event-stream' : 'application/json'
},
body: JSON.stringify(req.body),
signal: controller.signal
});
if (!upstream.ok) {
const errText = await upstream.text();
res.write(data: ${JSON.stringify({ error: errText })}\n\n);
return res.end();
}
if (!isStream) {
const data = await upstream.text();
res.send(data);
return;
}
// ===== PHẦN CỐT LÕI: chống timeout SSE =====
let lastByteAt = Date.now();
const heartbeat = setInterval(() => {
// Comment line không phá parser phía client
res.write(: hb-${Date.now()}\n\n);
// Đẩy inactivity nếu upstream im quá 45