Tư duy batch processing — input bất biến, output phái sinh
Batch xử lý một khối dữ liệu cố định thành output mới mà không đụng dữ liệu gốc — mẫu pipe của Unix gieo mầm cho kỷ luật bất biến của batch phân tán.
TL;DR: Batch processing là xử lý một khối dữ liệu kích thước cố định (bounded) thành output mới — chạy tới khi xong rồi dừng, khác stream xử lý dòng sự kiện vô tận. Nguyên tắc nền là input bất biến, output phái sinh: job chỉ đọc dữ liệu gốc, không sửa, và ghi kết quả ra nơi khác. Nhờ vậy chạy lại luôn an toàn (idempotent), lỗi thì dựng lại từ nguồn — không có trạng thái nửa vời để dọn. Mẫu pipe của Unix (mỗi tool một việc, nối nhau qua giao diện đồng nhất là dòng text) chính là hình mẫu thu nhỏ của MapReduce/Spark ở bài sau.
Mỗi đêm, một sàn thương mại điện tử dựng lại bảng "sản phẩm bán chạy 24h" từ khoảng 40 triệu dòng log đơn hàng trong ngày. Google thì chạy pipeline dựng chỉ mục (indexing) xử lý ~20 TB tài liệu đã crawl mỗi lượt để tạo chỉ mục tìm kiếm. Hai việc này có độ trễ cao — tính bằng phút tới giờ — nhưng đó không phải điểm yếu cần sửa: chúng đánh đổi độ trễ lấy throughput khổng lồ và tính đơn giản.
Điều làm chúng đơn giản không phải công cụ, mà là một kỷ luật: dữ liệu gốc (log, HTML) không bao giờ bị sửa, và mọi thứ job sinh ra (bảng xếp hạng, chỉ mục) đều là bản phái sinh có thể vứt đi rồi dựng lại. Bài này giải thích vì sao kỷ luật "input bất biến → output phái sinh" là hạt nhân của mọi hệ batch, và vì sao nó bắt nguồn từ cách Unix nối các lệnh nhỏ qua pipe.
1. Analogy — nhà máy chế biến theo lô
Một nhà máy nước ép nhận vào một lô trái cây đã cân đong sẵn cho ca sản xuất. Trong suốt ca, không ai đổ thêm hay rút bớt trái khỏi lô đó — lô là cố định. Dây chuyền chạy: rửa → ép → lọc → đóng chai, mỗi trạm làm đúng một việc rồi đẩy sang trạm sau. Sản phẩm ra là mẻ nước ép mới; đống vỏ và bã là phụ phẩm — không ai lấy nước ép "trộn ngược" vào lô trái cây ban đầu.
Nếu mẻ nước ép bị lỗi (nêm sai tỉ lệ), bạn không mất gì ở nguyên liệu: lô trái cây vẫn còn nguyên (nếu giữ lạnh), chỉnh công thức rồi ép lại. Đây chính là batch: lô nguyên liệu = input bất biến, dây chuyền = pipeline, mẻ nước = output phái sinh.
| Nhà máy nước ép | Batch processing |
|---|---|
| Lô trái cây cân sẵn cho ca | Input bounded, bất biến |
| Không thêm/bớt trái giữa ca | Job không sửa dữ liệu gốc |
| Rửa → ép → lọc → đóng chai | Pipeline nhiều bước nối tiếp |
| Mỗi trạm làm đúng một việc | Do one thing well (một việc, làm tốt) |
| Băng chuyền chuẩn nối các trạm | Uniform interface — dòng text |
| Mẻ nước ép ra lò | Output phái sinh (report, index) |
| Nêm sai → ép lại từ lô cũ | Reprocess từ input bất biến |
Input là lô nguyên liệu (đừng đụng vào), output là mẻ hàng (hỏng thì làm lại). Batch = chế biến theo lô, không phải phục vụ gọi món từng phần (đó là stream).
2. Batch là gì — bounded input, chạy rồi dừng
Hình dung hai kiểu bài toán. Một: "tính doanh thu tháng 6" — dữ liệu tháng 6 là một khối hữu hạn, đã đóng, bạn biết chính xác điểm bắt đầu và kết thúc. Hai: "hiện số đơn hàng đang tăng theo thời gian thực" — dữ liệu là một dòng không bao giờ dứt.
Kiểu một là batch: input bounded (biết biên), job đọc hết khối đó, tính ra kết quả, rồi kết thúc. Kiểu hai là stream (Module 2): input unbounded, job chạy mãi, xử lý từng sự kiện khi nó tới.
| Tiêu chí | Batch | Stream |
|---|---|---|
| Input | Bounded — khối hữu hạn, biết điểm cuối | Unbounded — dòng sự kiện vô tận |
| Vòng đời job | Chạy → xong → dừng | Chạy liên tục, không dừng |
| Độ trễ | Cao (phút–giờ) | Thấp (mili giây–giây) |
| Throughput | Rất cao — khấu hao overhead trên khối lớn | Cao, nhưng ưu tiên độ trễ thấp từng sự kiện |
| Ví dụ | Báo cáo cuối ngày, dựng lại index | Cảnh báo gian lận, đếm live |
Vì input đã đóng, batch được tự do sắp xếp lại công việc cho hiệu quả: đọc tuần tự cả khối (throughput đĩa cao nhất), sort toàn bộ, chia cho nghìn máy. Đó là lý do batch nuốt được TB–PB dữ liệu mà stream khó theo kịp — đổi lại là độ trễ.
3. Vì sao input phải bất biến?
Đây là hạt nhân của tư duy batch. Một job batch tuân hai quy tắc: (1) chỉ đọc input, không bao giờ sửa nó; (2) ghi kết quả ra một nơi khác.

Nghe hiển nhiên, nhưng bốn hệ quả của nó là toàn bộ sức mạnh của batch:
- Chạy lại an toàn (idempotent). Job chết giữa chừng? Chạy lại từ đầu — vì input không đổi, lần chạy lại cho đúng kết quả như chưa từng lỗi. Điều kiện đi kèm: output của lần hỏng phải bị vứt/ghi đè toàn phần khi chạy lại (framework batch tự bỏ output của task lỗi), không được append thêm — nếu không sẽ ra kết quả nhân đôi. Nhờ đó không còn "trạng thái nửa vời" để dọn dẹp.
- Reprocess cả lịch sử. Phát hiện công thức tính sai từ ba tháng trước? Vì log gốc còn nguyên, sửa code rồi chạy lại trên đúng dữ liệu cũ — dựng lại toàn bộ output đúng. Nếu bạn đã ghi đè input, dữ liệu cũ mất, không sửa lại được.
- Debug bằng tái lập. Output tháng trước trông lạ? Chạy lại job trên input y hệt, quan sát. Input bất biến biến "bug production" thành thứ tái lập được — thứ xa xỉ trong hệ mutable.
- Tách nguồn-sự-thật khỏi phái sinh. Input là source of truth (sự thật gốc); mọi output là derived data (phái sinh) — bản sao dẫn xuất, luôn dựng lại được. Ranh giới này cho phép thử nghiệm thoải mái: chạy job mới, ghi ra output mới, so sánh, mà không rủi ro gì cho dữ liệu gốc.
Idempotent chỉ đúng khi job thật sự không side-effect lên input. Nếu bên trong job bạn lén sửa một bản ghi gốc (đánh dấu "đã xử lý" ngay trên record nguồn), bạn vừa phá vỡ tính bất biến — chạy lại lần hai sẽ thấy dữ liệu đã khác lần một. Giữ input read-only tuyệt đối.
4. Triết lý Unix — pipe và giao diện đồng nhất
Tư duy này không mới. Nó có từ Unix thập niên 1970, trong một dòng lệnh mà mọi kỹ sư đều từng gõ. Giả sử bạn có file log web access.log, muốn tìm 5 địa chỉ IP gọi nhiều nhất:
Dòng lệnh dưới nối 5 công cụ nhỏ qua dấu |. Trước khi đọc giải thích, thử đoán: mỗi công cụ nhận gì và trả ra gì? Vì sao phải sort hai lần?
# Tim 5 IP goi nhieu nhat trong access.log
cut -d' ' -f1 access.log | sort | uniq -c | sort -rn | head -5
Chuỗi này chạy như một pipeline batch thu nhỏ:
cut -d' ' -f1— lấy cột đầu (IP) của mỗi dòng. Đọc từ input, ghi ra output.sort— gom các IP giống nhau về cạnh nhau (chuẩn bị cho đếm).uniq -c— đếm số lần lặp mỗi IP liền kề.sort -rn— sắp giảm dần theo số đếm.head -5— lấy 5 dòng đầu.
Access.log (BAT BIEN)
|
v cut -> stream cac IP
v sort -> IP da gom nhom
v uniq -c -> "<count> <IP>"
v sort -rn-> xep giam dan
v head -5 -> top 5
v
Ket qua (PHAI SINH) — access.log KHONG doi
Hai đặc điểm ở đây là toàn bộ DNA của batch:
- Mỗi công cụ làm đúng một việc và làm tốt.
sortchỉ biết sắp xếp — kể cả sắp file lớn hơn RAM (nó tự chia chunk, sort, trộn — chính là external sort bạn sẽ recall ở bài sau). - Giao diện đồng nhất (uniform interface): mọi công cụ đọc dòng text từ đầu vào, ghi dòng text ra đầu ra. Nhờ một giao diện chung, bất kỳ tool nào cũng nối được với bất kỳ tool nào qua
|(pipe) — không cần biết nhau.access.logkhông hề bị sửa; kết quả là dữ liệu mới.
Doug McIlroy, cha đẻ Unix pipe, gói triết lý này thành: "viết chương trình làm một việc và làm tốt; viết chương trình để phối hợp với nhau; xử lý dòng text, vì đó là giao diện phổ quát." MapReduce ở bài sau chỉ là công thức này phóng to: map giống cut (biến đổi từng dòng), shuffle giống sort (gom theo key), reduce giống uniq -c (tổng hợp mỗi nhóm) — nhưng chạy trên nghìn máy thay vì một terminal.
5. Derived data — thứ luôn dựng lại được
Khi input là source of truth bất biến, mọi thứ khác trong hệ trở thành derived data: chỉ mục tìm kiếm, cache, bảng tổng hợp, báo cáo, materialized view. Đặc tính chung của chúng: mất cũng không sao — vì luôn tính lại được từ nguồn.
| Source of truth | Derived data | |
|---|---|---|
| Vai trò | Sự thật gốc, không tái tạo | Bản dẫn xuất, luôn dựng lại được |
| Ví dụ | Log đơn hàng, sự kiện thô | Search index, cache, báo cáo |
| Nếu mất | Mất vĩnh viễn — thảm hoạ | Chạy lại batch để dựng lại |
| Sửa được không | Chỉ append thêm, không sửa cũ | Xoá sạch, dựng lại tuỳ ý |
Phân biệt này cởi trói cho thiết kế. Muốn đổi cấu trúc chỉ mục tìm kiếm? Đừng migrate tại chỗ đầy rủi ro — chạy một batch job dựng chỉ mục mới hoàn toàn từ dữ liệu gốc, kiểm tra, rồi chuyển sang. Muốn thử một cách tính báo cáo khác? Ghi ra một bảng output khác, so sánh song song. Vì derived data rẻ và tái tạo được, bạn thử nghiệm mà không sợ hỏng sự thật gốc.
6. Áp dụng — hình dạng một batch job
Gộp cả ba nguyên tắc — đọc nguồn read-only, ghi ra nơi khác, tính lại được — vào khung một job cụ thể. Đây là hình dạng bạn sẽ lặp lại ở mọi bài sau:
# Batch job "top san pham ban chay theo ngay"
job DailyTopProducts(date):
src <- read_only("orders/" + date + "/") # NGUON: bat bien, chi doc
ranked <- src
.filter(o -> o.status = "PAID") # loc
.group_by(o -> o.product_id) # gom theo key
.count() # tong hop moi nhom
.sort_desc() # xep giam dan
write("reports/top_products/" + date + "/", ranked) # OUTPUT: noi KHAC
# orders/<date> khong doi -> chay lai bao nhieu lan cung ra dung ket qua
Ba dòng khớp đúng ba nguyên tắc: read_only trên orders/ (input bất biến), write ra reports/ tách biệt (output phái sinh), và vì hai điều đó, chạy lại DailyTopProducts("2026-07-08") bao nhiêu lần cũng ra cùng kết quả (idempotent). Đổi công thức count() thành tổng tiền? Sửa code rồi chạy lại trên đúng orders/ cũ — không đụng gì tới nguồn. Để ý cả luồng: filter + trích key product_id chính là map, group_by là shuffle (gom theo key), count là reduce — khung map → shuffle → reduce của bài sau, chỉ chưa phân tán.
7. Pitfall — ghi đè dữ liệu gốc
Cám dỗ phổ biến: để "tiết kiệm chỗ", job xử lý xong thì ghi đè kết quả ngay lên dữ liệu gốc, hoặc đánh dấu record nguồn là "đã xử lý".
❌ SAI — pipeline mutable
doc orders -> tinh toan -> GHI DE ket qua len chinh bang orders
=> du lieu goc mat. Sua cong thuc? Khong con input cu de chay lai.
=> job chet giua chung? Mot phan bang da bi sua, phan khac chua -> hong.
✅ DUNG — input bat bien
doc orders (read-only) -> tinh toan -> ghi ra bang orders_summary MOI
=> orders con nguyen. Chay lai bao nhieu lan cung ra dung ket qua.
Hệ quả của bản sai: mất khả năng reprocess (sửa bug lịch sử là bất khả), mất tính idempotent (chạy lại lần hai thấy dữ liệu đã khác), và một lần job chết giữa chừng để lại dữ liệu hỏng một nửa không có điểm sạch để quay về.
Hướng đúng: luôn coi input là read-only, ghi output ra nơi tách biệt (bảng/thư mục/bucket khác). Dung lượng là thứ rẻ nhất trong ba thứ; khả năng dựng-lại đắt hơn nhiều. Đây cũng là nền để hiểu vì sao HDFS chọn mô hình write-once-read-many — cố tình làm việc sửa dữ liệu cũ trở nên khó, ép cả hệ theo kỷ luật bất biến; còn GFS thì tối ưu cho đường ghi record-append (ghi đè tại offset vẫn được, nhưng hiếm dùng).
8. 📚 Deep Dive
- MapReduce: Simplified Data Processing on Large Clusters — Dean & Ghemawat (OSDI 2004) — paper gốc cho thấy
map/shuffle/reducelà Unix pipe phóng to ra nghìn máy; xem mục "Large-Scale Indexing" (phần Experience) để thấy con số ~20 TB tài liệu đã crawl đẻ ra mô hình này. - The Google File System — Ghemawat, Gobioff, Leung (SOSP 2003) — filesystem cho batch tối ưu cho ghi append (record-append), hiếm khi sửa tại chỗ: kỷ luật thiên-về-bất-biến ở tầng lưu trữ.
- HDFS Architecture Guide (Apache Hadoop) — bản mã nguồn mở của ý tưởng GFS; xem mục "Simple Coherency Model" (write-once) để thấy bất biến được đưa vào thiết kế.
Ghi chú: Designing Data-Intensive Applications (Kleppmann) Chương 10 là nguồn agnostic chuẩn cho batch — mục "Batch Processing with Unix Tools" dựng đúng trực giác pipe → MapReduce như bài này. Điểm cốt lõi để mang theo: batch mạnh không nhờ công cụ xịn, mà nhờ kỷ luật bất biến + phái sinh.
9. Liên hệ các bài khác
- Bài 02 — MapReduce: mô hình
map → shuffle → reducechính là dòng lệnhcut | sort | uniqở bài này phóng to ra cả cluster —shufflelà bướcsortchạy phân tán. - Bài 03 — Join trong batch: khi cần ghép hai khối dữ liệu bất biến, ta lại dựa vào sort — nối thẳng tư duy pipeline ở đây.
- Bài 05 — Thiết kế batch pipeline: lab tổng hợp, nơi bạn tự dựng chuỗi nhiều job trên nguyên tắc input bất biến → output phái sinh.
- Module 2 — Xử lý stream (bài sau): mặt còn lại — khi input là dòng vô tận thay vì khối cố định, kỷ luật bất biến biến thành "log sự kiện chỉ append".
10. Tóm tắt
- Batch xử lý input bounded — job chạy rồi dừng; hạt nhân là input bất biến, output phái sinh: job chỉ đọc gốc, ghi kết quả ra nơi khác.
- Bốn hệ quả: chạy lại an toàn (idempotent), reprocess cả lịch sử khi sửa bug, debug bằng tái lập, và tách source of truth khỏi derived data.
- Triết lý Unix:
cut | sort | uniq -c | sort -rn | headlà pipeline batch thu nhỏ — khuôn của MapReduce. Derived data luôn dựng lại được từ nguồn. - Anti-pattern: ghi đè input (in-place update) → mất reprocess, mất idempotent, để lại dữ liệu hỏng-một-nửa khi job chết. Luôn ghi output ra nơi tách biệt.
11. Tự kiểm tra
- Q1Vì sao 'input bất biến' làm cho một batch job trở nên idempotent (chạy lại bao nhiêu lần cũng cho cùng kết quả)? Điểm nào trong quy tắc tạo ra tính chất đó?
- Q2Batch và stream khác nhau căn bản ở đâu, và vì sao khác biệt đó cho batch tối ưu throughput trên khối lớn nhưng trả kết quả chậm hơn?
- Q3Trong dòng lệnh `cut -d' ' -f1 access.log | sort | uniq -c | sort -rn | head -5`, vì sao phải gọi `sort` hai lần? Bỏ lần `sort` đầu thì hỏng chỗ nào?
- Q4Triết lý 'giao diện đồng nhất' (uniform interface) của Unix là gì, và vì sao nó cho phép nối bất kỳ công cụ nào với nhau qua pipe?
- Q5Search index và cache là 'derived data'. Nếu một sáng search index của bạn bị hỏng hoàn toàn, vì sao đó là sự cố phục hồi được chứ không phải thảm hoạ mất dữ liệu?
- Q6Một đồng nghiệp đề xuất: 'để tiết kiệm dung lượng, batch job xử lý xong ghi đè kết quả ngay lên bảng gốc'. Nêu hai hậu quả cụ thể và cách làm đúng.
Bài tiếp theo: MapReduce — map, shuffle, reduce trên nghìn máy
Bài này đáng gửi cho bạn học cùng?
Copy link đã gắn nguồn — dán group, chat, hoặc LinkedIn.
Bài này có giúp bạn hiểu bản chất không?
Hỏi đáp về bài này
Chưa có câu hỏi
Có gì chưa rõ trong bài? Đặt câu hỏi đầu tiên — câu trả lời từ cộng đồng giúp bạn (và người sau).
Đặt câu hỏi đầu tiên