Hệ thống Dữ liệu Phái sinh/Tư duy batch processing — input bất biến, output phái sinh
2/21
Bài 2 / 21~12 phútXử lý batch (batch processing)Miễn phí lượt xem

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ồ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 épBatch processing
Lô trái cây cân sẵn cho caInput bounded, bất biến
Không thêm/bớt trái giữa caJob không sửa dữ liệu gốc
Rửa → ép → lọc → đóng chaiPipeline nhiều bước nối tiếp
Mỗi trạm làm đúng một việcDo one thing well (một việc, làm tốt)
Băng chuyền chuẩn nối các trạmUniform 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
💡 Cách nhớ

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íBatchStream
InputBounded — khối hữu hạn, biết điểm cuốiUnbounded — dòng sự kiện vô tận
Vòng đời jobChạy → xong → dừngChạy liên tục, không dừng
Độ trễCao (phút–giờ)Thấp (mili giây–giây)
ThroughputRất cao — khấu hao overhead trên khối lớnCao, 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 indexCả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.

flowchart LR
  SRC["Source of truth<br/>(input BAT BIEN)"] -->|"chi doc"| JOB["Batch job"]
  JOB -->|"ghi ra noi khac"| OUT["Derived data<br/>(output PHAI SINH)"]
  OUT -.->|"hong? xoa sach"| REBUILD["Chay lai job"]
  SRC -->|"input van nguyen"| REBUILD
  REBUILD --> OUT

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.
⚠️ 'Chạy lại an toàn' không tự nhiên mà 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:

Thử đoán trước khi đọc tiếp

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. sort chỉ 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.log khô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 truthDerived data
Vai tròSự thật gốc, không tái tạoBả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ấtMất vĩnh viễn — thảm hoạChạy lại batch để dựng lại
Sửa được khôngChỉ 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_byshuffle (gom theo key), countreduce — khung map → shuffle → reduce của bài sau, chỉ chưa phân tán.

7. Pitfall — ghi đè dữ liệu gốc

Pitfall — sửa tại chỗ (in-place update) trên dữ liệu nguồn

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

📚 Deep Dive — Batch & triết lý Unix

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 → reduce chính là dòng lệnh cut | sort | uniq ở bài này phóng to ra cả cluster — shuffle là bước sort chạ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 (khối hữu hạn, biết điểm cuối): chạy → xong → dừng. Đổi độ trễ cao lấy throughput khổng lồ. Stream (Module 2) xử lý input unbounded.
  • Hạt nhân: input bất biến, output phái sinh. Job chỉ đọc dữ liệu 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: mỗi tool một việc + nối qua pipe + giao diện đồng nhất (dòng text). cut | sort | uniq -c | sort -rn | head là một pipeline batch thu nhỏ — và là khuôn của MapReduce.
  • Derived data (index, cache, report) luôn dựng lại được từ nguồn → thử nghiệm/đổi cấu trúc mà không rủi ro sự thật gốc.
  • 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

Tự kiểm tra
Q1
Vì 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 đó?

Vì job chỉ đọc input và ghi output ra nơi khác, nên mỗi lần chạy đều bắt đầu từ *đúng cùng một* dữ liệu gốc và không để lại dấu vết lên nó. Lần chạy hỏng không làm bẩn input, cũng không để trạng thái nửa vời trong input để lần sau vấp phải.

Kết quả: chạy lại = tính lại từ đầu trên dữ liệu y hệt → ra kết quả y hệt. Nếu job lén sửa input (vd đánh dấu record "đã xử lý"), tính chất này vỡ ngay: lần hai thấy dữ liệu đã khác lần một.

Q2
Batch 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?
  • Batch: input bounded — khối hữu hạn, biết điểm cuối; job chạy tới hết rồi dừng.
  • Stream: input unbounded — dòng sự kiện vô tận; job chạy mãi, xử lý từng sự kiện khi tới.

Vì batch biết trước cả khối, nó được sắp xếp lại công việc cho hiệu quả: đọc tuần tự (throughput đĩa cao nhất), sort toàn bộ, chia cho nghìn máy — nên nuốt được TB–PB. Cái giá là phải *chờ gom đủ khối rồi mới chạy*, nên kết quả tới chậm (phút–giờ). Stream đổi ngược lại: phản hồi tức thời cho từng sự kiện, nhưng phải xử lý sự kiện ngay khi tới nên không gom-tối-ưu cả khối như batch (throughput cao vẫn đạt được, chỉ theo cách khác).

Q3
Trong 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?

sort lần đầu gom các IP giống nhau về cạnh nhau, vì uniq -c chỉ đếm được các dòng trùng liền kề — nó không nhớ toàn cục. Bỏ lần sort này thì các IP giống nhau nằm rải rác, uniq đếm sai (mỗi cụm rời một số riêng).

sort -rn lần hai xếp kết quả đã đếm theo số giảm dần để head -5 lấy đúng top 5. Hai lần sort phục vụ hai mục đích khác nhau: gom-để-đếm, rồi xếp-để-chọn.

Q4
Triế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?

Mọi công cụ Unix đọc dòng text từ đầu vào và ghi dòng text ra đầu ra — một giao diện chung duy nhất. Vì input/output đều là cùng một "hình dạng" (dòng text), output của tool này ghép thẳng làm input của tool kia mà không tool nào cần biết về tool kia.

Nhờ đó bạn lắp ghép tự do cut | sort | uniq như lego. MapReduce kế thừa đúng ý này: mọi bước trao đổi cặp key-value, nên `map`/`reduce` của bài toán khác nhau vẫn ghép được vào cùng khung chạy phân tán.

Q5
Search 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?

Vì derived data theo định nghĩa là bản dẫn xuất tính ra từ source of truth (dữ liệu gốc bất biến). Search index không chứa thông tin nào mà dữ liệu gốc không có — nó chỉ là dữ liệu gốc được *tổ chức lại* cho tra cứu nhanh.

Nên index hỏng = chạy lại batch job dựng index từ nguồn → có lại. Thảm hoạ thật sự chỉ xảy ra nếu mất chính source of truth (log/sự kiện gốc), vì cái đó không tái tạo từ đâu được. Đây là lý do ta bảo vệ nguồn nghiêm ngặt (append-only) và thoải mái với phái sinh.

Q6
Mộ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.

Hai hậu quả: (1) mất khả năng reprocess — phát hiện công thức sai từ tháng trước thì không còn dữ liệu gốc để chạy lại, output sai vĩnh viễn; (2) mất idempotent + hỏng một nửa — job chết giữa chừng để lại bảng gốc bị sửa một phần, không có điểm sạch để quay về, và chạy lại lần hai đọc phải dữ liệu đã khác.

Cách đúng: coi input read-only tuyệt đối, ghi output ra bảng/thư mục/bucket tách biệt. Dung lượng là thứ rẻ; khả năng dựng-lại đắt hơn nhiều. Đây cũng là lý do HDFS chọn mô hình write-once-read-many, còn GFS tối ưu cho record-append.

Bài tiếp theo: MapReduce — map, shuffle, reduce trên nghìn máy

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

Đặt 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