Temporal
Temporal là nền tảng durable execution: bạn viết quy trình nhiều bước như code bình thường, Temporal ghi lại lịch sử và chạy tiếp sau mọi sự cố. Bài này giải thích workflow và activity, quy tắc determinism, retry, timer, signal, ví dụ TypeScript và khi nào nên dùng thay cho queue cộng bảng saga.
Temporal là gì và mô hình cốt lõi
Nhiều nghiệp vụ là một chuỗi bước kéo dài: đặt hàng thì giữ kho, trừ tiền, tạo vận đơn, gửi email, chờ giao hàng 3 ngày rồi mới chốt doanh thu. Mỗi bước gọi một hệ thống khác, có thể lỗi, có thể mất vài giây hoặc vài ngày. Cách làm truyền thống là tự ghép hàng đợi, cron, bảng trạng thái và rất nhiều code retry. Microservices Patterns gọi chuỗi này là một saga: dãy giao dịch cục bộ, mỗi bước cập nhật dữ liệu trong một service, và khi một bước lỗi thì phải chạy các giao dịch bù trừ theo thứ tự ngược lại.1
Temporal giải quyết bài toán này bằng durable execution. Bạn viết quy trình như một hàm async bình thường. Temporal ghi lại mọi sự kiện quan trọng (bước nào bắt đầu, kết quả trả về gì, timer nào được đặt) vào một event history bền vững. Nếu worker chạy hàm đó chết giữa chừng, một worker khác lấy history ra, chạy lại hàm từ đầu nhưng thay vì gọi lại các bước đã xong, nó lấy kết quả có sẵn trong history. Hàm "tiếp tục" đúng chỗ đã dừng.2 Ý tưởng này gần với điều Grokking nói về task chạy lâu: khi máy đang chạy task chết, task phải idempotent hoặc khôi phục được trạng thái từ checkpoint đã lưu để chạy tiếp ở máy khác.3
Hai khái niệm trung tâm:
| Workflow | Activity | |
|---|---|---|
| Vai trò | Điều phối: thứ tự bước, rẽ nhánh, chờ, bù trừ | Làm việc thật: gọi API, ghi DB, gửi email |
| Yêu cầu | Phải deterministic | Có thể làm bất cứ gì, nên idempotent |
| Khi lỗi | Không retry theo kiểu thường, được replay từ history | Retry tự động theo retry policy |
| Thời gian sống | Có thể kéo dài (giờ, ngày) | Thường ngắn, có heartbeat nếu lâu |
Nói ngắn gọn: workflow là "bộ não" chỉ được ra quyết định, activity là "tay chân" được phép chạm vào thế giới bên ngoài.2
Kiến trúc
Temporal gồm hai phần: Temporal Service (server) và worker do bạn viết và chạy. Mô tả kiến trúc dưới đây là kiến thức về sản phẩm, thư viện chưa có sách để kiểm chứng.2
- Frontend nhận lệnh: bắt đầu workflow, gửi signal, query trạng thái.
- History service giữ event history của từng workflow.
- Matching service quản lý task queue. Worker kéo (poll) task từ đây.
- Persistence: một database bền vững lưu history và trạng thái.
- Worker: tiến trình của bạn, chứa code workflow và activity. Worker không cần mở port, nó chỉ kết nối ra Temporal.
Luồng chạy: API gọi start với một workflow id (ví dụ order-123). Temporal tạo task trong task queue. Một worker nhận task, chạy code workflow tới khi gặp lệnh gọi activity, rồi báo lại "cần chạy activity X". Temporal ghi sự kiện, đẩy activity task vào queue. Worker (có thể là worker khác) chạy activity, trả kết quả. Temporal ghi kết quả vào history và đánh thức workflow để chạy tiếp.
Workflow id là duy nhất trong một namespace, nên gọi start hai lần với cùng id không tạo hai quy trình. Đây là cách chống trùng tự nhiên khi API bị retry, cùng tinh thần với điều Microservices Patterns nói: tạo đơn hàng với ID do client cung cấp là một thao tác idempotent.4
Quy tắc determinism
Vì workflow được replay từ history, chạy lại hàm workflow với cùng history phải ra cùng chuỗi lệnh.2 Do đó code workflow không được:
- Gọi mạng, đọc DB, đọc file trực tiếp. Mọi I/O phải đi qua activity.
- Dùng thời gian hệ thống hoặc số ngẫu nhiên tự do. Phải dùng phiên bản deterministic mà SDK cung cấp.
- Dùng
setTimeoutthật. Dùngsleep()của Temporal, nó tạo một timer bền vững. - Đổi thứ tự hoặc thêm bớt bước trong code đang có workflow chạy dở mà không có versioning. Replay sẽ không khớp history và báo lỗi non-determinism. SDK có cơ chế versioning để rẽ nhánh code cũ và mới.
Sai lầm hay gặp: Gọi fetch() trong workflow để "lấy nhanh" cấu hình. Lần đầu chạy được, nhưng khi replay sau vài ngày, cấu hình đã khác và workflow lệch history. Đưa nó vào activity.
Ví dụ TypeScript: đặt hàng với bù trừ
Ví dụ dưới đây là bản Temporal của một saga đặt hàng có bù trừ: bước nào lỗi thì chạy giao dịch bù trừ cho các bước đã xong, theo thứ tự ngược lại.1 Cú pháp SDK là kiến thức sản phẩm, chưa có nguồn sách.2
Activity là hàm bình thường:
// activities.ts
export async function reserveStock(orderId: string): Promise<void> { /* gọi inventory, idempotent theo orderId */ }
export async function releaseStock(orderId: string): Promise<void> { /* ... */ }
export async function chargeCard(orderId: string, amount: number): Promise<string> { /* trả paymentId */ return "pay_1"; }
export async function refund(paymentId: string): Promise<void> { /* ... */ }
export async function createShipment(orderId: string): Promise<void> { /* ... */ }
Workflow điều phối, có retry, timer và signal:
// workflows.ts
import { proxyActivities, defineSignal, setHandler, condition, sleep } from "@temporalio/workflow";
import type * as acts from "./activities";
const a = proxyActivities<typeof acts>({
startToCloseTimeout: "30 seconds",
retry: { initialInterval: "1s", backoffCoefficient: 2, maximumAttempts: 5 },
});
export const cancelSignal = defineSignal("cancel");
export async function orderWorkflow(orderId: string, amount: number) {
let cancelled = false;
setHandler(cancelSignal, () => { cancelled = true; });
await a.reserveStock(orderId);
let paymentId: string;
try {
paymentId = await a.chargeCard(orderId, amount);
} catch (e) {
await a.releaseStock(orderId);
throw e;
}
// Cho khách 30 phút để hủy, timer bền vững qua mọi lần restart
if (await condition(() => cancelled, "30 minutes")) {
await a.refund(paymentId);
await a.releaseStock(orderId);
return "cancelled";
}
await a.createShipment(orderId);
return "shipped";
}
Khởi động từ API:
await client.workflow.start(orderWorkflow, {
taskQueue: "orders",
workflowId: `order-${orderId}`,
args: [orderId, amount],
});
// Hủy: await client.workflow.getHandle(`order-${orderId}`).signal(cancelSignal);
Những gì Temporal làm thay bạn trong đoạn code trên: retry chargeCard với backoff, lưu trạng thái paymentId qua mọi lần worker restart, giữ timer 30 phút mà không cần cron, nhận signal hủy từ API, và ghi lại toàn bộ lịch sử để debug. Workflow ở đây đóng vai saga orchestrator: chỉ lo thứ tự bước và bù trừ, không chứa logic nghiệp vụ khác, đúng lời khuyên của Microservices Patterns để tránh orchestrator "thông minh" điều khiển các service "ngốc".5
- Signal: gửi dữ liệu vào workflow đang chạy (hủy đơn, duyệt yêu cầu).
- Query: đọc trạng thái hiện tại của workflow mà không thay đổi nó.
- Timer:
sleep("3 days")không chiếm worker. Workflow được gỡ khỏi bộ nhớ và đánh thức khi tới hạn. - Child workflow, continue-as-new: chia quy trình lớn, hoặc reset history khi workflow sống rất lâu.
Capacity và sizing
Temporal có các giới hạn cụ thể của sản phẩm (kích thước history mỗi workflow, kích thước payload, số shard chọn lúc cài đặt). Thư viện chưa có sách về Temporal nên bài không đưa con số; khi cần, tra tài liệu của phiên bản bạn dùng và load test.2 Các nguyên tắc định tính:
- History của một workflow tăng theo số bước. Workflow sống rất lâu hoặc lặp vô hạn cần cơ chế cắt history (continue-as-new).
- Payload truyền giữa workflow và activity nên nhỏ; dữ liệu lớn để ở blob store và chỉ truyền key.
- Workflow đang chờ (timer, signal) chỉ tốn chỗ lưu trữ, không chiếm worker.
- Mỗi bước thêm vài lần ghi vào persistence, nên thông lượng của cụm bị giới hạn bởi database bên dưới.
Ví dụ (giả định của khoá học): 2 triệu đơn mỗi ngày, mỗi đơn 6 activity, peak gấp 10 lần trung bình.
2.000.000 / 86.400 ≈ 23 workflow/s trung bình, peak ×10 ≈ 230/s
230 × 6 ≈ 1.400 activity/s lúc peak
→ số worker và cỡ database: ước lượng, cần load test
Chia số yêu cầu mỗi ngày cho 86.400 giây là cách ước lượng của Grokking ch.5; hệ số peak ×10 là giả định.6 Worker scale theo độ dài task queue (schedule-to-start latency). Nếu latency tăng, thêm worker.2
Khi nào dùng và khi nào không
| Tình huống | Lựa chọn hợp lý |
|---|---|
| Một job nền đơn lẻ: resize ảnh, gửi email | Queue đơn giản |
| Quy trình 2–3 bước, ít nhánh, đội đã quen | Queue cộng bảng trạng thái saga trong DB |
| Quy trình nhiều bước, bù trừ, chờ dài (giờ, ngày), cần nhìn thấy trạng thái | Temporal |
| Luồng sự kiện khối lượng lớn, nhiều consumer | Kafka |
| Tính toán stream, aggregation theo cửa sổ thời gian | Flink |
| Lập lịch cron đơn giản | Cron hoặc task scheduler |
Queue cộng bảng saga là cách Microservices Patterns ch.4 mô tả cho orchestration saga: một orchestrator lưu trạng thái saga trong DB, gửi command qua message broker, nhận reply và chuyển trạng thái như một state machine. Mỗi bước vừa cập nhật DB vừa gửi message nên phải dùng transactional messaging. Sách khuyên dùng orchestration cho mọi saga trừ loại đơn giản nhất.5 Cách này không cần hạ tầng mới, nhưng bạn phải tự viết state machine, timeout, retry, xử lý message trùng, và công cụ để xem saga nào đang kẹt. Temporal gói tất cả những thứ đó lại, đổi lại bạn có thêm một hệ thống phải vận hành và phải tuân thủ quy tắc determinism.2
Có nhiều workflow engine khác cùng nhóm (dịch vụ managed của cloud, engine mã nguồn mở); thư viện chưa có sách so sánh chúng.2
Vận hành ở quy mô nhỏ và lớn
Nhỏ. Dùng bản managed, bạn chỉ chạy worker. Đây là lựa chọn hợp lý cho hầu hết đội nhỏ. Server chạy local chỉ dùng cho môi trường dev.2
Lớn, tự host. Tự chạy các service Temporal cùng database persistence. Một số tham số phân mảnh phải chọn từ đầu. Theo dõi persistence latency, schedule-to-start latency theo task queue, số workflow lỗi non-determinism. Cần quy trình versioning rõ ràng trước khi deploy code workflow mới.
Thực hành nên có:
- Activity idempotent, dùng workflow id hoặc activity id làm idempotency key khi gọi hệ thống ngoài. Lý do giống bài task scheduler của Grokking: task chạy xong nhưng chưa kịp báo kết quả sẽ bị chạy lại, task không idempotent sẽ cho kết quả sai.7
- Đặt
startToCloseTimeoutcho mọi activity; activity dài dùng heartbeat để phát hiện worker chết sớm. - Task queue riêng cho activity nặng (ví dụ gọi GPU) để scale độc lập.
- Viết test replay: lấy history từ production, chạy với code mới để phát hiện lỗi determinism trước khi deploy.
Điểm nói trong phỏng vấn
- Dùng Temporal khi bài có quy trình nhiều bước, bù trừ, chờ đợi: thanh toán, giao hàng nội thành, đặt vé với thời gian giữ chỗ, job scheduler.
- Giải thích durable execution trong một câu: trạng thái workflow được dựng lại từ event history, nên crash worker không mất tiến độ.
- Phân biệt workflow (deterministic, điều phối) và activity (I/O, retry, idempotent).
- Nhắc giới hạn: history có kích thước tối đa, payload nhỏ, thêm độ trễ mỗi bước, và chi phí vận hành.2
- Đưa phương án thay thế: queue cộng bảng saga nếu quy trình đơn giản.5 Xem quy trình nhiều bước và tác vụ dài.
Trong phỏng vấn: Câu chốt tốt: "Order workflow chạy trên Temporal, id order-{id} để chống trùng, mỗi bước là activity idempotent có retry, lỗi thanh toán thì chạy bù trừ nhả kho, timer 15 phút giữ vé nằm trong workflow nên không cần cron quét bảng."
Nguồn trích dẫn
-
Microservices Patterns (file .zip, tải về giải nén) · ch.4 · §4.1.3 Using the Saga pattern to maintain data consistency (tr. 114–117). ↩ ↩2
-
Chưa có nguồn trong thư viện: thư viện không có sách về Temporal hay durable execution; phần này chỉ nêu khái niệm cơ bản, không đưa số liệu, giới hạn, phiên bản hay giá của sản phẩm. ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8 ↩9 ↩10 ↩11
-
Grokking Modern System Design · ch.23 Distributed Task Scheduler · bài Design, mục "Execution cap" (Point to Ponder: task chạy 90% thì máy chết). ↩
-
Microservices Patterns (file .zip, tải về giải nén) · ch.3 · §3.3.6 Handling duplicate messages, mục "Writing idempotent message handlers" (tr. 95–96). ↩
-
Microservices Patterns (file .zip, tải về giải nén) · ch.4 · §4.2.2 Orchestration-based sagas (tr. 121–125), các mục "Modeling saga orchestrators as state machines", "Saga orchestration and transactional messaging", "Benefits and drawbacks of orchestration-based sagas". ↩ ↩2 ↩3
-
Grokking Modern System Design · ch.5 Back-of-the-envelope Calculations · bài "Examples of Resource Estimation", mục "Number of servers required". ↩
-
Grokking Modern System Design · ch.23 Distributed Task Scheduler · mục "Task idempotency". ↩