1. Tại sao cần nghĩ về “tính chịu lỗi” trong ChatGPT App
Trong một ứng dụng web thông thường, người dùng ít nhất còn thấy URL, spinner của trình duyệt, có thể tải lại trang. Trong ChatGPT, người dùng chỉ thấy một màn hình: chat và App của bạn. Nếu có gì đó chậm, họ không phân biệt được ai là thủ phạm — OpenAI, Gateway của bạn, hệ thống thanh toán hay microservice phân tích của hàng xóm. Với họ, tất cả chỉ là “ChatGPT + App của bạn”.
Khi một tool-call treo 30–60 giây, mô hình chờ, chờ… và trong trường hợp tốt nhất sẽ xin lỗi vì chậm trễ. Tệ hơn — nó bịa ra câu trả lời thay vì dữ liệu từ backend của bạn. Vì vậy, tính chịu lỗi không chỉ là chuyện SRE và uptime, mà còn là về chất lượng câu trả lời, giọng điệu của mô hình và các metric trên Store.
Trong hệ sinh thái ChatGPT App, chúng ta có vài vòng độc lập:
- ChatGPT ↔ MCP Gateway.
- Gateway ↔ các dịch vụ backend/REST của bạn (Gift REST API, Commerce REST API, Analytics Service, v.v.).
- Các dịch vụ của bạn ↔ API bên ngoài (LLM, thanh toán, danh mục).
- Webhook đến (ACP, Stripe, các tích hợp khác) ↔ các handler của bạn.
Vấn đề là trục trặc ở một nơi có thể gây hiệu ứng dây chuyền: Gateway kiên nhẫn chờ dịch vụ bị treo, worker bị nghẽn, kết nối cạn kiệt, client bắt đầu retry, và sau vài phút bạn có kịch bản kinh điển “mọi thứ cháy và chìm cùng lúc”. Bốn pattern hôm nay giúp chúng ta chống lại điều đó:
- Timeouts — chúng ta không bao giờ chờ mãi.
- Circuit breaker — chúng ta không đập đầu vào cánh cửa đã đóng.
- Bulkheads — chúng ta tạo “khoang” để cả con tàu không chìm theo.
- Bảo vệ trước bão webhook — chấp nhận rằng webhook sẽ có trùng lặp, đột biến và retry, và chuẩn bị sẵn sàng.
2. Timeouts: chúng ta không chờ mãi
Timeout là gì và vì sao thiếu nó thì tệ
Timeout là thời gian tối đa mà mã của bạn sẵn sàng chờ phản hồi từ một phụ thuộc: database, MCP server, HTTP API bên ngoài, mô hình. Nếu quá thời gian mà vẫn chưa có phản hồi — coi như gọi thất bại, giải phóng tài nguyên và trả về lỗi dễ hiểu hoặc fallback.
Không có timeout, các request có thể:
- treo vĩnh viễn trong trạng thái chờ,
- chiếm kết nối và thread pool,
- chặn các request tiếp theo,
- gây ra sự cố dây chuyền.
Pattern rất đơn giản: “thà thất bại có thể dự đoán trong 3–5 giây còn hơn im lặng khó hiểu trong 5 phút”.
Hãy nhớ rằng chúng ta có timeout ở nhiều lớp:
- ở lớp proxy/load balancer (Cloudflare, Nginx),
- ở lớp MCP Gateway (HTTP client đến microservice),
- bên trong chính các service (gọi DB, API bên ngoài, LLM).
Với ChatGPT nói chung, hợp lý là hướng tới tổng thời gian tool-call trong khoảng 5–10 giây cho thao tác thường và tối đa 20–30 giây cho thao tác nặng. Dài hơn gần như chắc chắn gây UX tệ.
fetchWithTimeout đơn giản trong TypeScript
Bắt đầu bằng thực hành. Trong GiftGenius MCP Gateway, chúng ta có HTTP client tiện ích để gọi gift-picker, commerce-service, analytics. Hãy bọc fetch chuẩn vào hàm có timeout:
// src/gateway/httpClient.ts
export async function fetchWithTimeout(
url: string,
opts: RequestInit & { timeoutMs?: number } = {}
) {
const { timeoutMs = 5000, ...rest } = opts;
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
try {
return await fetch(url, { ...rest, signal: controller.signal });
} finally {
clearTimeout(timeoutId);
}
}
Giờ trong mã Gateway, chúng ta không bao giờ gọi fetch “trần”, mà luôn qua helper này:
// src/gateway/giftClient.ts
import { fetchWithTimeout } from "./httpClient";
export async function callGiftService(path: string) {
const res = await fetchWithTimeout(
process.env.GIFT_SERVICE_URL + path,
{ timeoutMs: 4000 }
);
if (!res.ok) {
throw new Error(`gift_service_${res.status}`);
}
return res.json();
}
Cách tiếp cận này đảm bảo rằng ngay cả khi gift-service bị treo, sau 4 giây chúng ta sẽ cắt kết nối và có thể trả MCP error cho ChatGPT, thay vì giữ kết nối đến cùng.
Đặt timeout ở đâu trong GiftGenius
Trong ví dụ GiftGenius của chúng ta:
- Ở lớp Gateway: timeout cho các gọi Gift REST API, Commerce REST API, Analytics Service / REST API.
- Bên trong các service đó: timeout cho gọi DB, ACP/thanh toán, API gợi ý bên ngoài.
- Ở đầu vào Gateway: timeout tổng cho request từ ChatGPT để tool-call không biến thành “spinner vô tận”.
Quan trọng là thời gian chờ ở lớp trên cùng nên lớn hơn một chút so với lớp bên trong. Ví dụ, nếu Gateway chờ backend 5 giây, còn backend chờ DB 3 giây, thì ta có khoảng đệm cho xử lý và tuần tự hoá kết quả.
Giải thích timeout với mô hình ChatGPT như thế nào
Với ChatGPT, tốt nhất là trả về lỗi có ngữ nghĩa, thay vì âm thầm làm rơi kết nối. Thay vì một 500 mơ hồ, hãy trả MCP error có cấu trúc để mô hình có thể nói lại với người dùng: “Dịch vụ gợi ý quà đang quá tải, thử lại sau một chút” v.v.
Điều đó có nghĩa là trong Gateway khi timeout bạn cần:
- Bắt AbortError hoặc mã timeout_… của chúng ta.
- Tạo phản hồi MCP với mã và mô tả ngắn gọn có ý nghĩa.
- Cho mô hình quyền quyết định cách giải thích điều này với người dùng.
Timeout giải quyết vấn đề request treo, nhưng nếu một phụ thuộc bắt đầu rơi hàng loạt thì chúng không cứu khỏi “tuyết lở” những lần thử thất bại giống nhau. Lúc này chúng ta cần lớp bảo vệ tiếp theo — circuit breaker.
3. Circuit breaker: “cầu dao” chống service hấp hối
Trực giác: vì sao chỉ timeout là chưa đủ
Chúng ta đã biết giới hạn thời gian chờ của từng lời gọi bằng timeout. Timeout bảo vệ cho từng lời gọi cụ thể. Nhưng nếu phụ thuộc “chết hẳn” (ví dụ commerce-service rơi OOM (Out Of Memory) ở mọi request), chúng ta vẫn tiếp tục gọi nó, mỗi lần lại chờ 3–5 giây, bắt lỗi, tải mạng và CPU rồi lại chờ.
Circuit breaker (cầu dao) bổ sung trí nhớ: nó theo dõi lỗi và timeout, và khi quá nhiều, ngừng hẳn việc gửi request đến service đó. Thay vào đó, nó trả về từ chối nhanh hoặc fallback. Sau một khoảng thời gian, nó thử lại một cách thận trọng ở chế độ half-open.
Các trạng thái kinh điển của cầu dao:
- Closed — mọi thứ bình thường, request vẫn đi.
- Open — service bị coi là “chết”, không gửi request, trả lỗi ngay.
- Half-open — thử một số request giới hạn; nếu thành công — quay về closed, nếu lại rớt — trở lại open.
Sơ đồ circuit breaker đơn giản
Sơ đồ nhỏ:
stateDiagram-v2
[*] --> Closed
Closed --> Open: quá nhiều lỗi
Open --> HalfOpen: hết thời gian cooldown
HalfOpen --> Closed: vài lần thành công liên tiếp
HalfOpen --> Open: lại lỗi
Open --> Open: từ chối nhanh
Mini-implementation circuit breaker trong TypeScript
Trong production thường dùng thư viện có sẵn (với Node.js có, ví dụ, opossum hoặc các giải pháp nhẹ tự viết), nhưng để hiểu cơ chế thì một lớp gọn là đủ.
Ví dụ breaker tối giản bọc quanh lời gọi module commerce:
// src/gateway/circuitBreaker.ts
type State = "closed" | "open" | "half-open";
export class CircuitBreaker {
private state: State = "closed";
private failureCount = 0;
private nextAttemptAt = 0;
constructor(
private readonly failureThreshold = 5,
private readonly cooldownMs = 30_000
) {}
async call<T>(fn: () => Promise<T>): Promise<T> {
const now = Date.now();
if (this.state === "open") {
if (now < this.nextAttemptAt) {
throw new Error("circuit_open");
}
this.state = "half-open";
}
try {
const result = await fn();
this.onSuccess();
return result;
} catch (err) {
this.onFailure();
throw err;
}
}
private onSuccess() {
this.failureCount = 0;
this.state = "closed";
}
private onFailure() {
this.failureCount++;
if (this.failureCount >= this.failureThreshold) {
this.state = "open";
this.nextAttemptAt = Date.now() + this.cooldownMs;
}
}
}
Và sử dụng trong client của commerce-service:
// src/gateway/commerceClient.ts
const commerceBreaker = new CircuitBreaker(3, 20_000);
export async function callCommerce(path: string) {
return commerceBreaker.call(async () => {
const res = await fetchWithTimeout(
process.env.COMMERCE_URL + path,
{ timeoutMs: 3000 }
);
if (!res.ok) throw new Error(`commerce_${res.status}`);
return res.json();
});
}
Ở đây, khi commerce bắt đầu trả lỗi hàng loạt hoặc không kịp trước timeout, sau vài lần thất bại breaker sẽ chuyển sang open. Trong trạng thái này, trong suốt cooldownMs chúng ta không cố gọi service nữa mà trả ngay lỗi circuit_open.
ChatGPT nên thấy gì khi breaker “cắt” service
Từ góc nhìn ChatGPT, tốt nhất là bạn:
- Phản hồi nhanh bằng MCP error “commerce_unavailable” hoặc “gift_service_overloaded”.
- Thêm mô tả rõ ràng: “Dịch vụ thanh toán tạm thời không khả dụng, hãy thử lại sau”.
- Không che giấu lỗi bằng retry vô tận.
Đây là trường hợp “từ chối nhanh và trung thực” tốt hơn treo lâu. Đặc biệt ở checkout: người dùng chấp nhận thông báo trung thực hơn là nhìn spinner 40 giây rồi nhận “đã có lỗi xảy ra”.
Timeout và breaker bảo vệ chúng ta khỏi phụ thuộc “xấu” hoặc nằm, nhưng chúng không giải quyết vấn đề khi một loại tải ăn hết tài nguyên và bóp nghẹt các phần khác của hệ thống. Cần thêm một lớp nữa — bulkheads.
4. Bulkheads: cô lập “khoang” để một khoang không nhấn chìm cả tàu
Ẩn dụ con tàu
Pattern bulkhead được đặt theo vách ngăn của con tàu: nếu một khoang thủng, nước sẽ không tràn ra toàn bộ. Trong kiến trúc, điều đó nghĩa là: phân chia tài nguyên cho các hướng công việc khác nhau, để một service quá tải không ăn sạch — CPU, kết nối, pool — và không làm gãy các luồng quan trọng.
Trong microservice, điều này thường thực hiện qua các:
- pool kết nối HTTP riêng,
- pool thread/worker riêng,
- queue/topic riêng,
- thậm chí cluster DB riêng cho các tác vụ tối quan trọng.
Ý tưởng là: nếu service gợi ý quà chậm lại và khựng, nó chỉ cạn tài nguyên của riêng nó, chứ không làm hỏng checkout và đăng nhập.
Bulkheads trong thế giới Node.js và MCP Gateway
Trong Node.js, chúng ta không có thread theo nghĩa cổ điển (có event loop và worker), nhưng có thể giới hạn số lượng tác vụ song song cho từng hướng.
Ví dụ: trong Gateway có ba phụ thuộc bên ngoài:
- Gift-service (gợi ý quà, gọi LLM nặng),
- Commerce-service (checkout, ACP),
- Analytics-service (ghi log sự kiện).
Chúng ta có thể đặt các giới hạn đơn giản cho số request đồng thời đến từng service.
Ví dụ một “semaphore” nhỏ để giới hạn song song:
// src/gateway/bulkhead.ts
export class Bulkhead {
private active = 0;
private queue: (() => void)[] = [];
constructor(private readonly maxConcurrent: number) {}
async run<T>(fn: () => Promise<T>): Promise<T> {
if (this.active >= this.maxConcurrent) {
await new Promise<void>((resolve) => this.queue.push(resolve));
}
this.active++;
try {
return await fn();
} finally {
this.active--;
const next = this.queue.shift();
if (next) next();
}
}
}
Và sử dụng cho các service:
// src/gateway/clients.ts
import { Bulkhead } from "./bulkhead";
const giftBulkhead = new Bulkhead(10); // tối đa 10 song song
const commerceBulkhead = new Bulkhead(3); // checkout bị hạn chế chặt
const analyticsBulkhead = new Bulkhead(50); // có thể nhiều
export async function callGiftWithBulkhead(fn: () => Promise<any>) {
return giftBulkhead.run(fn);
}
export async function callCommerceWithBulkhead(fn: () => Promise<any>) {
return commerceBulkhead.run(fn);
}
Như vậy, ngay cả khi GPT quyết định hỏi “làm cho tôi 30 gợi ý quà phức tạp”, chúng sẽ được thực thi tối đa 10 cùng lúc, còn checkout vẫn hoạt động nhờ giới hạn riêng của nó.
GiftGenius: chúng ta muốn những “khoang” nào
Trong GiftGenius, hợp lý là tách riêng các khoang cho:
- Gợi ý quà (nặng về LLM, ít quan trọng hơn, có thể chậm lại),
- Checkout/ACP (cực kỳ quan trọng, cần bảo vệ tối đa),
- Analytics/log (quan trọng, nhưng có thể chịu trễ một chút).
Ở kiến trúc nâng cao hơn, bạn còn triển khai chúng như các cluster khác nhau với tài nguyên riêng, nhưng trong phạm vi bài giảng này, điều quan trọng là ý tưởng: không để tính năng thứ yếu “ăn” hết oxy.
Ba pattern — timeout, circuit breaker và bulkheads — xử lý cách bạn gọi ra bên ngoài, đến các phụ thuộc. Nhưng còn một lớp rủi ro khác cho tính chịu lỗi: các luồng sự kiện đầu vào có thể nhấn chìm bạn ngay cả khi outbound đã cấu hình hoàn hảo. Ví dụ điển hình nhất — bão webhook.
5. Bão webhook: khi thế giới gửi sự kiện nhanh hơn bạn có thể xử lý
Webhook vận hành thế nào trong thực tế
Nguồn vấn đề thứ tư với tính chịu lỗi là các sự kiện đầu vào: webhook từ ACP, Stripe và các hệ thống khác. Chính chúng có thể tạo nên “cơn bão” thực sự, ngay cả khi bạn đã có timeout, circuit breaker và bulkhead.
Webhook không phải HTTP request “theo yêu cầu”, mà là sự kiện “push” từ hệ thống bên ngoài (Stripe, ACP, cửa hàng bên ngoài, v.v.). Chúng có vài đặc tính không mấy dễ chịu:
- Giao hàng ít nhất một lần (at-least-once) — nghĩa là trùng lặp là không tránh khỏi.
- Thứ tự giao không được đảm bảo.
- Khi gặp lỗi, chúng rất thích retry: lúc đầu sau một giây, rồi 10, rồi một phút… cho đến khi bạn trả 2xx.
- Ở đỉnh điểm (ví dụ mùa sale), chúng đến theo lô, tạo ra “cơn bão”.
Nếu handler của bạn không idempotent và chạy quá lâu, nó trở thành nút cổ chai, toàn bộ hàng đợi bị nghẽn, còn retry chỉ làm cơn bão mạnh thêm. Hệ quả là bạn có thể làm sập DB, queue, worker pool — và kéo theo các phần khác của hệ thống.
Nguyên tắc cơ bản để chống bão
Có vài ý tưởng giúp tăng cơ hội sống sót khi bão đến:
Thứ nhất, queue-first, process-later. Lý tưởng là webhook đầu vào không nên thực hiện công việc nặng theo cách đồng bộ. Thay vào đó, nó nhanh nhất có thể xác thực chữ ký/định dạng, đưa nhiệm vụ vào hàng đợi và trả về 200 OK. Xử lý diễn ra bất đồng bộ trong worker. Nếu bạn cần “xác nhận nhanh” cho ChatGPT, có thể duy trì một luồng thông báo riêng.
Thứ hai, idempotency của handler. Webhook lặp lại cho cùng một thao tác không được “tạo đơn thêm lần nữa” hay “trừ tiền hai lần”. Thường xử lý bằng cách lưu idempotency key hoặc eventId và kiểm tra xem sự kiện đó đã được xử lý chưa.
Thứ ba, rate limiting và circuit breaker ở phía nhận. Ngay cả khi phía gửi đang bão, bạn có thể:
- giới hạn RPS theo IP/đăng ký/endpoint,
- tạm thời trả 429 hoặc 503 để làm chậm retry,
- dùng breaker để không đổ dòng sự kiện vào downstream đang hỏng (ví dụ DB đơn hàng).
Ví dụ handler webhook Next.js trong GiftGenius
Giả sử chúng ta có ACP/hệ thống thanh toán gửi webhook về trạng thái đơn hàng đến POST /api/commerce/webhook. Mục tiêu của chúng ta:
- nhanh chóng nhận sự kiện và đưa vào hàng đợi,
- không xử lý đồng bộ,
- không gãy vì trùng lặp.
Ví dụ giản lược (chưa kiểm tra chữ ký và hàng đợi thực — sẽ có trong module bảo mật và hàng đợi):
// app/api/commerce/webhook/route.ts
import { NextRequest, NextResponse } from "next/server";
// Ở đây lẽ ra có Redis/hàng đợi, tạm thời giả lập bằng mảng
const inMemoryQueue: any[] = [];
const processedEvents = new Set<string>(); // idempotency (demo)
export async function POST(req: NextRequest) {
const event = await req.json();
const eventId = event.id as string;
if (processedEvents.has(eventId)) {
return NextResponse.json({ ok: true, duplicate: true });
}
// Trong thực tế sẽ có kiểm tra chữ ký và schema
inMemoryQueue.push(event); // đưa vào hàng đợi để xử lý nền
// Worker nền sẽ xử lý sau và đánh dấu ID là đã xử lý
return NextResponse.json({ ok: true });
}
Tạm thời đây là bản giả lập, nhưng có hai điểm quan trọng:
- Phần đồng bộ nhẹ nhất có thể.
- Chúng ta xây dựng idempotency quanh event.id.
Trong thực tế bạn sẽ:
- dùng hàng đợi ngoài (SQS, RabbitMQ, Kafka),
- lưu sự kiện đã xử lý trong DB,
- kiểm tra chữ ký webhook và phiên bản payload,
- có thể áp dụng Bulkhead/Breaker riêng quanh handler.
Trông như thế nào trong bối cảnh GiftGenius
Với GiftGenius tích hợp ACP/Stripe qua webhook, khả năng chống bão đặc biệt quan trọng vào mùa cao điểm (Năm mới, Thứ Sáu Đen). Khi đó có rất nhiều sự kiện:
- tạo intent,
- xác nhận thanh toán,
- huỷ,
- hoàn tiền.
Nếu handler của bạn bắt đầu “kéo dài” (ví dụ vì gọi API bên ngoài), bạn có nguy cơ:
- ACP bắt đầu retry,
- sự kiện đến theo lô,
- DB đơn hàng và worker pool bị nghẽn.
Pattern “queue first” + idempotency + rate limiting ở đầu vào chính là bảo hiểm cho các kịch bản như vậy.
6. Các pattern này phối hợp với nhau ra sao
Giờ hãy ghép các pattern vào một kịch bản và xem chúng hoạt động thế nào trong flow “Gợi ý quà và thanh toán ngay”.
Xem chuỗi “ChatGPT → Gateway → Gift Service → Commerce → webhook” trong kịch bản:
Người dùng nói trong chat: “Gợi ý quà và thanh toán ngay”.
- Mô hình quyết định gọi tool của bạn suggest_and_checkout.
- Gateway gọi gift-service thông qua fetchWithTimeout và bulkhead của gift-service.
- Nếu gift-service treo — timeout kích hoạt; breaker quanh nó sau một số lỗi sẽ chuyển sang open, và các request sau sẽ nhận ngay MCP error “gift_service_unavailable”.
- Nếu gift-service trả lời, Gateway gọi commerce-service (lại với timeout và bulkhead riêng).
- Bất kỳ vấn đề nào với commerce sẽ kích hoạt circuit breaker riêng, cấu hình nghiêm ngặt hơn gift (vì checkout là tối quan trọng).
- Đơn hàng thành công dẫn đến webhook từ ACP đến /api/commerce/webhook của bạn, nơi sự kiện được đưa vào hàng đợi và trả lời nhanh; worker nền xử lý, còn webhook lặp lại với cùng eventId bị bỏ qua như trùng lặp.
Kết quả:
- Service gợi ý bị treo không làm sập checkout.
- Commerce bị treo không biến mọi tool-calls thành spinner một phút — ChatGPT nhận lỗi có ngữ nghĩa nhanh chóng.
- Bão webhook không làm gãy luồng HTTP chính của bạn.
- Bạn kiểm soát nơi xuống cấp: tốt hơn tạm tắt gợi ý cá nhân hoá còn hơn làm gãy thanh toán.
7. Checklist thực hành nhỏ cho App của bạn (dưới dạng tường thuật)
Tổng quát lại, trong một ChatGPT App với MCP/Gateway, đáng để lần lượt đi qua các câu hỏi sau.
Đầu tiên bạn kiểm tra xem đã có timeout cho mọi gọi ra bên ngoài chưa. Toàn bộ mã fetch, request đến DB và LLM phải dùng wrapper như fetchWithTimeout với giá trị hợp lý. Quan trọng là không có chỗ nào request có thể treo vô hạn.
Tiếp theo bạn xác định các phụ thuộc mong manh nhất. Thường là hệ thống thanh toán, ACP, API bên ngoài lớn và đôi khi chính DB đơn hàng của bạn. Quanh chúng nên thêm circuit breaker để tự vệ khỏi cơn “tuyết lở” lặp lại vào service đã chết. Đồng thời, bạn quyết định ngay ChatGPT sẽ hành xử thế nào khi breaker ở trạng thái open.
Sau đó bạn nhìn tài nguyên của mình như các “khoang”. Mọi thứ có đang đi qua cùng một connection pool và worker pool không, hay các thao tác trọng yếu (login, checkout) có giới hạn song song riêng, độc lập với service gợi ý và analytics. Nếu chưa — thêm implement bulkhead đơn giản, ít nhất là giới hạn số tác vụ song song thô sơ.
Cuối cùng, bạn audit toàn bộ webhook đầu vào. Kiểm tra xem có idempotency key hay eventId không, bạn có đang làm việc nặng theo cách đồng bộ trong HTTP handler không và liệu bạn có chịu được làn sóng retry nếu downstream tạm thời sập. Nếu chưa — chuyển logic vào hàng đợi và worker nền.
Chuỗi bước như vậy đem lại mức tăng đáng kể về tính chịu lỗi ngay cả khi hạ tầng chưa quá phức tạp.
8. Những lỗi thường gặp với timeouts, circuit breakers, bulkheads và bão webhook
Lỗi số 1: thiếu timeout “ở đâu đó bên dưới”.
Lập trình viên hay chỉ đặt timeout ở Gateway hoặc frontend, quên rằng bên trong backend còn có DB, API bên ngoài và LLM. Kết quả là request bên ngoài có timeout 5 giây, nhưng bên trong một gọi DB hay thanh toán có thể treo hàng phút, làm nghẽn connection pool và gây sự cố dây chuyền.
Lỗi số 2: timeout khổng lồ “cho chắc”.
Đôi khi đặt timeout 60–120 giây: “để nó kịp chạy”. Trong ngữ cảnh ChatGPT, gần như luôn tệ. Người dùng rời đi, mô hình bắt đầu bịa, còn tài nguyên của bạn bị khoá suốt thời gian đó. Tốt hơn nhiều là từ chối trung thực sau 5–10 giây với mô tả dễ hiểu.
Lỗi số 3: circuit breaker không kèm UX được suy nghĩ kỹ.
Đôi khi breaker được thêm “cho có”, nhưng khi kích hoạt, người dùng hoặc mô hình nhận một 500 khó hiểu, “ECONNREFUSED” hay “axios error”. Kết quả GPT không thể giải thích hợp lý và bắt đầu bịa. Nên thiết kế sẵn thông điệp lỗi dễ hiểu cho cả người và mô hình.
Lỗi số 4: trộn lẫn tài nguyên mà không có bulkhead.
Kịch bản kinh điển: một service gợi ý (hoặc analytics) bắt đầu chậm, ăn sạch pool kết nối DB hoặc thread pool, và kéo theo checkout và login chết theo. Tất cả vì tài nguyên không được tách. Thiếu bulkhead, một tính năng thứ yếu có thể làm sập cả production.
Lỗi số 5: xử lý webhook như request bình thường.
Người mới thường viết handler webhook như controller thường: logic nghiệp vụ dài, gọi API bên thứ ba, thiếu idempotency. Trong điều kiện retry và trùng lặp, điều này dẫn tới xử lý sự kiện hai lần, trạng thái đơn hàng kỳ lạ và sập tải khi bão.
Lỗi số 6: bỏ qua idempotency trong kịch bản commerce.
Đặc biệt nguy hiểm khi webhook thanh toán có thể tạo đơn thêm lần nữa hoặc thay đổi trạng thái lặp lại. Không kiểm tra idempotency key và lưu trạng thái xử lý sự kiện, sớm muộn bạn sẽ gặp trừ tiền kép hoặc trùng lặp đơn hàng.
Lỗi số 7: cố sửa mọi thứ bằng setTimeout và “độ trễ ma thuật”.
Đôi khi người ta muốn lách race condition và bão bằng “chờ 100 ms là ổn”. Thực tế điều này làm hành vi càng bất ổn và không bảo vệ khỏi sự cố thực. Con đường đúng là timeout rõ ràng, circuit breaker, hàng đợi và idempotency, chứ không phải “ma thuật độ trễ”.
Lỗi số 8: không ưu tiên các luồng then chốt.
Khi checkout và login nằm trong cùng giới hạn với analytics hay logic gợi ý, bất kỳ quá tải nào cũng có thể đánh gục cả phần quan trọng lẫn thứ yếu. Trong thiết kế chịu lỗi, checkout và auth là “bò thần thánh”: tài nguyên riêng, giới hạn riêng, cảnh báo riêng và SLO riêng.
GO TO FULL VERSION