Khi thước đo không còn là độ trễ mà là thông lượngGom lại làm một mẻ thật lớnWhen the measure is no longer latency but throughput
Mở Phần III của cuốn sách, Kleppmann chia thế giới hệ thống thành ba loại1: dịch vụ trực tuyến chờ yêu cầu rồi trả lời càng nhanh càng tốt, nên thước đo là response time; hệ theo lô (batch) nhận một khối dữ liệu lớn đã nằm sẵn, chạy một mạch có thể hàng giờ, nên thước đo là thông lượng — bao nhiêu dữ liệu đi qua trong một đơn vị thời gian; và hệ gần thời gian thực (xử lý luồng) nằm giữa hai loại kia. Chương 10 dành trọn cho loại thứ hai, và bắt đầu từ một nơi ít ai ngờ: dòng lệnh Unix2.
Có ba kiểu hệ thống. Một: trả lời ngay — bạn bấm, nó đáp, càng nhanh càng tốt. Hai: làm cả mẻ — gom một đống dữ liệu thật lớn rồi chạy một lèo, có khi mất mấy tiếng; ở đây điều quan trọng không phải "nhanh mấy giây" mà là "xong được bao nhiêu". Ba: kiểu ở giữa, dữ liệu chảy tới đâu làm tới đó. Chương này nói về kiểu thứ hai. Nhớ nhé: hỏi đúng thước đo trước đã — nhanh mấy giây, hay xong bao nhiêu?
Opening Part III of the book, Kleppmann splits systems into three kinds1: online services wait for requests and answer as fast as they can, so the measure is response time; batch systems take a large body of data that is already sitting there, run for minutes or hours, and are measured by throughput — how much data passes through per unit of time; and near-real-time systems (stream processing) sit between the two. Chapter 10 is devoted to the second kind, and it starts somewhere unexpected: the Unix command line2.
Xử lý theo lô đứng vững nhờ một kỷ luật rất cũ: đầu vào bất biến, đầu ra là một tập dữ liệu hoàn toàn mới, không tác dụng phụ1. Chính kỷ luật ấy — được Kleppmann truy nguyên về triết lý Unix5 rồi phóng đại lên hàng nghìn máy bằng MapReduce8 — cho phép chạy lại một job bao nhiêu lần cũng được, và biến sai sót của con người thành thứ có thể sửa. Khung tư duy này sống lâu hơn chính công cụ đã sinh ra nó.
Bí quyết của "làm cả mẻ" là: không sửa dữ liệu gốc. Đọc vào, tính toán, rồi ghi kết quả ra chỗ mới. Làm sai thì xóa kết quả, chạy lại, dữ liệu gốc vẫn còn nguyên. Nhớ nhé: giữ nguyên bản gốc thì bao giờ cũng làm lại được.
Batch processing rests on a very old discipline: inputs are immutable, the output is an entirely new dataset, and there are no side effects1. That discipline — which Kleppmann traces back to the Unix philosophy5 and then blows up across thousands of machines with MapReduce8 — is what lets you re-run a job as often as you like, and turns human mistakes into something recoverable. The frame outlives the tool that produced it.
HỆ THỐNGTHREE KINDS
OF SYSTEM
Công cụ nhỏ, nối bằng một giao diện duy nhấtSmall tools, joined by a single interface
Kleppmann không mở chương bằng Hadoop mà bằng một đường ống dòng lệnh phân tích log máy chủ web1. Lựa chọn ấy có chủ ý: theo ông, những công cụ Unix ra đời từ thập niên 1970 đã mang sẵn toàn bộ logic của xử lý theo lô hiện đại — đọc đầu vào, biến đổi, ghi đầu ra, không đụng vào dữ liệu gốc. Chỉ có quy mô là khác. Vì thế hiểu vì sao awk nối được với sort là đã hiểu quá nửa vì sao MapReduce nối được job này với job kia.
Chương này không bắt đầu bằng công nghệ to đùng, mà bằng mấy câu lệnh nhỏ trên máy tính. Mỗi câu lệnh làm đúng một việc: cái này lọc, cái kia sắp xếp, cái nọ đếm. Nối chúng lại là ra kết quả. Ý tưởng đó y hệt ý tưởng của những hệ thống khổng lồ sau này — chỉ khác là chạy trên nghìn máy thay vì một máy. Nhớ nhé: việc lớn thường chỉ là việc nhỏ ghép lại cho khéo.
Kleppmann opens the chapter not with Hadoop but with a command-line pipeline that analyses a web-server log1. The choice is deliberate: for him the Unix tools of the 1970s already carry the whole logic of modern batch processing — read input, transform, write output, never touch the original. Only the scale differs. So understanding why awk composes with sort is already understanding most of why MapReduce composes one job with the next.
Cái Kleppmann gọi là "triết lý Unix" được phát biểu thành văn năm 1978, trong lời mở đầu số đặc biệt của The Bell System Technical Journal do McIlroy, Pinson và Tague viết. Nguyên văn hai châm ngôn đầu: “Make each program do one thing well. To do a new job, build afresh rather than complicate old programs by adding new ‘features.’” và “Expect the output of every program to become the input to another, as yet unknown, program.”5 Kleppmann lấy đúng bộ châm ngôn này làm khung cho cả chương1.
Năm 1978, những người làm ra Unix viết xuống nguyên tắc của họ: mỗi chương trình chỉ làm một việc, và làm thật tốt; và kết quả của chương trình này phải dùng được làm đầu vào cho chương trình khác — kể cả chương trình chưa ai viết. Nhớ nhé: làm ít việc nhưng làm giỏi, rồi ghép lại với nhau.
What Kleppmann calls "the Unix philosophy" was written down in 1978, in the foreword to a special issue of The Bell System Technical Journal by McIlroy, Pinson and Tague. The first two maxims, verbatim: “Make each program do one thing well. To do a new job, build afresh rather than complicate old programs by adding new ‘features.’” and “Expect the output of every program to become the input to another, as yet unknown, program.”5 Kleppmann takes exactly this set of maxims as the frame for the whole chapter1.
Đáng chú ý là bản gốc 1978 không hề dùng chữ "triết lý" — các tác giả chỉ nói đó là “a number of maxims [that] have gained currency among the builders and users of the UNIX system”5, tức những câu cửa miệng đã thành nếp, chứ không phải một tuyên ngôn được ban hành. Chi tiết này quan trọng khi đọc Chương 10: Kleppmann không viện dẫn một thẩm quyền, ông chỉ ra một tập quán kỹ thuật đã chứng minh được giá trị qua bốn thập kỷ và rồi được lặp lại gần như nguyên vẹn ở tầng hệ phân tán. Danh mục tham chiếu công khai của chính ông cho Chương 10 dẫn thẳng về bài BSTJ này4.
stdin/stdout để người dùng quyết định.
stdin/stdout leave that to the user.
-
The log-analysis pipeline
Năm câu lệnh thay cho một chương trìnhFive commands instead of a program
Ví dụ mở màn của Kleppmann là tìm năm địa chỉ URL được yêu cầu nhiều nhất trong log của một máy chủ web, bằng một đường ống nối
awk(lấy ra trường URL),sort(gom các dòng giống nhau lại cạnh nhau),uniq -c(đếm), rồisort -rnvàhead1. Ông đối chiếu nó với một chương trình tương đương viết bằng ngôn ngữ kịch bản, dùng bảng băm đếm trong bộ nhớ — cùng ra một kết quả, nhưng khác nhau ở chỗ chịu tải.Muốn biết trang nào được xem nhiều nhất, bạn không cần viết phần mềm. Chỉ cần nối vài câu lệnh có sẵn: một cái cắt ra tên trang, một cái xếp các tên giống nhau nằm cạnh nhau, một cái đếm, rồi lấy năm cái nhiều nhất. Nhớ nhé: nhiều việc lớn chỉ là mấy việc nhỏ ghép lại.
Kleppmann's opening example finds the five most-requested URLs in a web-server log with a pipeline of
awk(extract the URL field),sort(bring identical lines together),uniq -c(count), thensort -rnandhead1. He contrasts it with an equivalent script that counts in an in-memory hash table — same answer, different behaviour under load.Nhận ra ở đâuMỗi lần bạn nối lệnh bằng dấu|để lọc rồi đếm một tệp log, bạn đang chạy một job theo lô thu nhỏ — có đúng cấu trúc map/shuffle/reduce.Thấy ở đâuBất cứ khi nào bạn nối lệnh này sang lệnh kia để lọc và đếm, bạn đang làm y hệt cách hệ lớn làm.Where you see itEvery time you chain commands with|to filter and count a log file, you are running a miniature batch job — with exactly a map/shuffle/reduce shape.Vì sao
sortmới là mấu chốtKleppmann nhấn rằng khác biệt thật giữa hai cách nằm ở giới hạn bộ nhớ: bảng băm trong bộ nhớ vỡ khi tập khóa lớn hơn RAM, còn
sortcủa GNU coreutils thì không — trang man chính thức của nó ghi rõ có tệp tạm trên đĩa (tùy chọn-T), giới hạn bộ đệm bằng-S, và chạy song song nhiều luồng qua--parallel6. Nghĩa là công cụ đã tự làm sẵn phần "tràn ra đĩa và trộn nhiều lượt" — đúng thứ mà tầng shuffle của MapReduce phải làm ở quy mô cụm.Khi một máy thắng cả cụm
Danh mục tham chiếu của Kleppmann cho chương này dẫn bài viết của Adam Drake, trong đó một đường ống dòng lệnh xử lý xong bộ dữ liệu cờ vua nhanh hơn khoảng 235 lần so với một job Hadoop trên cùng bài toán74. Bài học không phải "cụm là vô dụng", mà là chi phí điều phối của một hệ phân tán chỉ đáng trả khi dữ liệu thật sự vượt khỏi một máy.
-
A uniform interface: the byte stream
Ghép được vì ai cũng nói một thứ tiếngThey compose because they speak one language
Điều khiến các công cụ Unix ghép được với nhau, theo Kleppmann, là một giao diện thống nhất: mọi chương trình đều đọc và ghi cùng một thứ — một luồng byte, theo quy ước là văn bản ASCII phân tách bằng ký tự xuống dòng1. Giao diện ấy nghèo nàn và lắm chỗ vụng (tách trường bằng khoảng trắng hay tab là nguồn lỗi kinh niên), nhưng chính sự nghèo nàn ấy khiến bất kỳ công cụ nào cũng cắm được vào bất kỳ công cụ nào.
Vì sao mấy câu lệnh khác nhau lại nối được vào nhau? Vì chúng nói chung một thứ tiếng: cái nào cũng nhận chữ và trả ra chữ, mỗi việc một dòng. Giống như mọi đồ chơi xếp hình dùng chung một kiểu khớp nối. Nhớ nhé: chung một kiểu khớp thì ghép kiểu gì cũng được.
What makes Unix tools compose, for Kleppmann, is a uniform interface: every program reads and writes the same thing — a byte stream, by convention ASCII text separated by newlines1. The interface is poor and often clumsy (splitting fields on whitespace or tabs is a perennial source of bugs), but that poverty is exactly what lets any tool plug into any other.
Nhận ra ở đâuMỗi định dạng trao đổi chung trong hệ của bạn — CSV, JSON theo dòng, Avro trong một thư mục HDFS — đều là một bản sao của ý tưởng này ở tầng khác.Thấy ở đâuMọi "định dạng chung" mà cả nhóm thống nhất dùng đều là cùng một ý tưởng.Where you see itEvery shared interchange format in your system — CSV, newline-delimited JSON, Avro in an HDFS directory — is the same idea at another layer. -
Separation of logic and wiring
Chương trình không tự chọn bạn của nóA program doesn't choose its neighbours
Công cụ Unix mặc định đọc từ
stdinvà ghi rastdout; nó không biết — và không cần biết — dữ liệu tới từ tệp, từ ống dẫn hay từ bàn phím1. Kleppmann gọi đó là tách logic khỏi việc nối dây: việc nối dây thuộc về người dùng, nên cùng một công cụ dùng lại được trong vô số ngữ cảnh mà tác giả nó chưa từng nghĩ tới. Ông cũng chỉ ra giới hạn: mô hình này chỉ có một luồng vào và một luồng ra, nên những chương trình có nhiều đầu vào hoặc nhiều đầu ra thì rất khó ghép theo kiểu ống dẫn.Mỗi công cụ chỉ biết "có chữ đi vào" và "có chữ đi ra" — nó không quan tâm chữ đó từ đâu tới hay sẽ đi đâu. Người dùng mới là người quyết định nối cái nào với cái nào. Nhờ vậy một công cụ dùng được cho trăm việc khác nhau. Nhớ nhé: đừng dính chặt vào một chỗ thì mới dùng lại được nhiều nơi.
A Unix tool reads from
stdinand writes tostdoutby default; it does not know — and need not know — whether the data comes from a file, a pipe or a keyboard1. Kleppmann calls this separating the logic from the wiring: wiring belongs to the user, so one tool is reusable in countless contexts its author never imagined. He also names the limit: the model has one input and one output, so programs with several inputs or outputs are hard to compose as pipes.Nhận ra ở đâuMột job đọc đường dẫn đầu vào/đầu ra từ tham số thay vì ghi cứng trong mã là đang dùng đúng nguyên tắc này — và vì thế mới kiểm thử được.Thấy ở đâuChương trình nào cho bạn tự chọn tệp vào và tệp ra thì dễ dùng lại hơn hẳn.Where you see itA job that takes its input and output paths as arguments instead of hard-coding them is applying this principle — and that is what makes it testable. -
Transparency and experimentation
Đầu vào bất biến nên thử thoải máiImmutable inputs make experiments free
Kleppmann nêu một phẩm chất ít được nói tới của công cụ Unix: chúng minh bạch và khuyến khích thử nghiệm1. Tệp đầu vào thường được coi là bất biến, nên chạy lệnh bao nhiêu lần cũng vô hại; bạn có thể chặn đường ống ở bất kỳ khâu nào để xem kết quả trung gian, đưa nó vào
less, hay ghi ra tệp rồi tiếp tục từ đó. Chính ba tính chất ấy — bất biến, quan sát được, nối lại được — là cái sẽ được nâng lên quy mô cụm ở nửa sau chương.Chạy một câu lệnh không làm hỏng tệp gốc, nên bạn cứ thử thoải mái. Muốn xem giữa chừng ra cái gì thì dừng lại nhìn, rồi chạy tiếp. Nhớ nhé: khi bản gốc luôn còn nguyên, sai cũng không sao — cứ thử lại.
Kleppmann names a rarely-praised quality of Unix tools: they are transparent and invite experimentation1. Input files are normally treated as immutable, so running a command again is harmless; you can stop the pipeline at any stage to inspect intermediate results, pipe them into
less, or write them to a file and resume from there. Those three properties — immutable, observable, resumable — are exactly what the second half of the chapter scales up to a cluster.Nhận ra ở đâuQuy trình nào cho phép bạn xóa kết quả và chạy lại từ dữ liệu gốc mà không sợ hỏng gì, quy trình đó đã thừa hưởng phẩm chất này.Thấy ở đâuViệc nào làm lại được từ đầu mà không mất gì thì việc đó an toàn.Where you see itAny pipeline where you can delete the output and re-run from the original data without fear has inherited this property.Tại sao điều này là nền của cả chương
Bất biến không chỉ là tiện lợi khi gỡ lỗi. Nó là điều kiện để một job có thể được chạy lại — mà chạy lại được thì mới có chuyện tự động thử lại khi máy hỏng, và mới có chuyện sửa sai của con người bằng cách chạy lại với mã đã vá. Toàn bộ mô hình chịu lỗi của MapReduce dựa trên giả định này.
Giới hạn của mô hình một máy
Kleppmann cũng nói thẳng vì sao câu chuyện không dừng ở đây: đường ống Unix chỉ chạy trên một máy. Khi dữ liệu vượt quá một đĩa hoặc thời gian chạy vượt quá một đêm, ta cần cùng những nguyên tắc ấy nhưng trải ra nhiều máy — và đó chính là điều MapReduce làm.
Unix trải trên hàng nghìn máyUnix, spread across thousands of machines
MapReduce là một khung lập trình do Google công bố năm 2004: người dùng chỉ viết hai hàm — map và reduce — còn khung lo phần phân tán, cân bằng tải và chịu lỗi8. Kleppmann đọc nó như một bản dịch của đường ống Unix sang tầng cụm máy1: chỗ của tệp là hệ tệp phân tán, chỗ của công cụ là job, chỗ của dấu | là một thư mục đầu ra dùng làm thư mục đầu vào cho job kế tiếp.
Ở đây có một cách chia việc rất đơn giản. Bạn chỉ cần nói hai điều: với mỗi dòng dữ liệu thì rút ra cái gì, và gom những cái giống nhau lại thì tính ra sao. Phần còn lại — chia việc cho nghìn máy, máy nào hỏng thì làm lại — có hệ thống lo. Nhớ nhé: bạn lo phần "tính cái gì", máy lo phần "chia việc thế nào".
MapReduce is a programming framework Google published in 2004: the user writes just two functions — map and reduce — and the framework handles distribution, load balancing and fault tolerance8. Kleppmann reads it as a translation of the Unix pipeline into the cluster1: files become a distributed filesystem, tools become jobs, and the | becomes one job's output directory serving as the next job's input directory.
Một job đọc các tệp đầu vào, tách thành bản ghi, chạy mapper trên từng bản ghi để sinh ra các cặp khóa–giá trị, sắp xếp mọi cặp theo khóa, rồi giao mỗi nhóm khóa giống nhau cho một reducer18. Kleppmann chỉ ra rằng bước sắp xếp chính là chỗ sort trong đường ống Unix xuất hiện trở lại — chỉ có điều lần này việc sắp xếp bị cắt thành từng mảnh và trải ra khắp cụm.
Một "mẻ việc" chạy như sau: đọc từng dòng, gắn cho mỗi dòng một cái nhãn, rồi xếp tất cả dòng cùng nhãn về một chỗ để tính gộp. Nhớ nhé: gắn nhãn — gom lại — tính.
A job reads its input files, breaks them into records, runs a mapper over each record to emit key–value pairs, sorts every pair by key, then hands each group of identical keys to a reducer18. Kleppmann points out that the sorting step is where the Unix pipeline's sort reappears — except this time the sort is cut into pieces and spread over the cluster.
Bài báo gốc cho thấy quy mô thật của ý tưởng này ngay từ 2004: hệ thống lập chỉ mục tìm kiếm của Google lấy đầu vào là hơn 20 terabyte tài liệu thô và chạy như một chuỗi năm đến mười thao tác MapReduce; một pha của quá trình rút từ khoảng 3.800 dòng C++ xuống còn khoảng 700 dòng khi viết lại bằng MapReduce, và vào thời điểm ấy hơn một nghìn job MapReduce được chạy mỗi ngày trên các cụm của Google8. Đó là những con số của năm 2004, không phải của hôm nay — nhưng chúng giải thích vì sao mô hình này định hình cả một thập kỷ hạ tầng dữ liệu.
-
The distributed filesystem underneath
HDFS: một đĩa khổng lồ ghép từ nghìn đĩa thườngHDFS: one huge disk made of ordinary ones
MapReduce không đứng một mình: nó cần một hệ tệp phân tán bên dưới, mà trong thế giới Hadoop là HDFS — bản hiện thực mã nguồn mở theo kiến trúc Google File System91. Tài liệu chính thức mô tả nó là hệ shared-nothing: mỗi tệp được cắt thành các khối, khối được nhân bản sang nhiều máy với hệ số nhân bản đặt riêng cho từng tệp, và một tiến trình NameNode giữ toàn bộ siêu dữ liệu về việc khối nào nằm ở đâu10.
Muốn chứa lượng dữ liệu quá lớn cho một máy, người ta cắt tệp thành từng khúc rồi rải ra nhiều máy, mỗi khúc chép thành vài bản để lỡ một máy hỏng vẫn còn bản khác. Có một "người quản lý" ghi nhớ khúc nào đang nằm ở máy nào. Nhớ nhé: chia nhỏ, chép thành nhiều bản, và có sổ ghi chỗ để.
MapReduce does not stand alone: it needs a distributed filesystem beneath it, which in the Hadoop world is HDFS — an open-source implementation of the Google File System architecture91. The official documentation describes it as shared-nothing: each file is cut into blocks, blocks are replicated across machines with a per-file replication factor, and a NameNode process holds all the metadata about which block lives where10.
Nhận ra ở đâuBất kỳ kho đối tượng nào bạn đang dùng làm nơi đổ dữ liệu thô đều đóng đúng vai trò này: một không gian tên chung, ghi một lần, đọc nhiều lần.Thấy ở đâuMọi "kho chứa chung" mà cả hệ cùng đọc đều làm việc y như vậy.Where you see itAny object store you use as a raw-data landing zone plays exactly this role: one shared namespace, write once, read many.Đưa việc tới chỗ dữ liệu
Tài liệu HDFS nêu thẳng một nguyên tắc thiết kế mà Kleppmann cũng nhấn mạnh: “Moving Computation is Cheaper than Moving Data”10. Khi tập dữ liệu lớn, chuyển mã nguồn vài trăm kilobyte tới máy đang giữ khối dữ liệu rẻ hơn nhiều so với kéo hàng gigabyte qua mạng. Bộ lập lịch MapReduce vì thế ưu tiên đặt tác vụ map lên đúng máy có bản sao của khối cần đọc8.
Vì sao là shared-nothing
Kleppmann đối lập kiến trúc này với các hệ dùng thiết bị lưu trữ chuyên dụng dùng chung: HDFS chỉ cần máy chủ thường với đĩa thường và mạng thường, nên mở rộng bằng cách mua thêm máy chứ không phải thay bằng máy to hơn. Cái giá phải trả là phần mềm phải tự lo nhân bản, phát hiện máy chết và cân bằng lại dữ liệu — đúng những việc mà tài liệu HDFS liệt kê trong phần độ bền vững10.
-
Mapper, reducer, and the callback contract
Hai hàm nhỏ, phần còn lại là của khungTwo small functions; the rest belongs to the framework
Hợp đồng rất hẹp: mapper được gọi một lần cho mỗi bản ghi đầu vào và không giữ trạng thái giữa các lần gọi; reducer được gọi một lần cho mỗi khóa, kèm theo một bộ lặp đi qua toàn bộ giá trị của khóa đó1. Tài liệu Hadoop mô tả đúng khung ấy: người dùng cài đặt hai giao diện, còn khung lo việc lập lịch, giám sát và chạy lại các tác vụ hỏng11. Sự hẹp ấy không phải thiếu sót — nó chính là thứ cho phép khung tự do chia nhỏ và chạy lại công việc.
Bạn chỉ viết hai việc nhỏ: "với mỗi dòng, ghi ra nhãn và số", và "với mỗi nhãn, cộng các số lại". Bạn không được nhớ gì giữa các dòng — nghe có vẻ bất tiện, nhưng chính vì thế mà máy có thể chia việc ra cho nghìn máy khác nhau. Nhớ nhé: chịu ràng buộc một chút thì được chia việc thoải mái.
The contract is deliberately narrow: the mapper is called once per input record and keeps no state between calls; the reducer is called once per key, with an iterator over all values for that key1. Hadoop's documentation describes exactly this shape: the user implements two interfaces, and the framework handles scheduling, monitoring and re-executing failed tasks11. The narrowness is not a shortcoming — it is precisely what lets the framework split and retry the work freely.
Nhận ra ở đâuMọi API "cho tôi một hàm thuần túy, tôi lo phần chạy" — từmap/reducetrong ngôn ngữ tới hàm serverless — đều đang mua sự tự do điều phối bằng một hợp đồng hẹp.Thấy ở đâuBất cứ chỗ nào bạn chỉ cần đưa một "công thức nhỏ" còn hệ thống lo chạy, đó là cùng một kiểu thỏa thuận.Where you see itEvery API that says "give me a pure function and I'll run it" — from language-levelmap/reduceto serverless functions — buys scheduling freedom with a narrow contract.Chịu lỗi ở mức tác vụ
Vì mapper và reducer là những hàm không tác dụng phụ trên đầu vào bất biến, khung chỉ cần chạy lại tác vụ hỏng trên một máy khác là xong8. Bài báo gốc kể trường hợp cả tám mươi máy bị mất liên lạc vài phút và hệ thống đơn giản là chạy lại phần việc dở dang8. Không có trạng thái nào phải khôi phục vì không có trạng thái nào bị sửa.
Kẻ chậm chân và tác vụ dự phòng
Một tác vụ chạy chậm bất thường — vì đĩa lỗi, vì máy bị chia sẻ — có thể kéo dài cả job. Bài báo mô tả cơ chế backup tasks: khi job gần xong, khung khởi chạy bản sao dự phòng cho những tác vụ còn dở, và lấy kết quả của bản nào xong trước8. Đây là một minh họa đẹp cho việc tính bất biến biến "chạy trùng" từ nguy hiểm thành vô hại.
-
The shuffle, and chaining jobs into workflows
Shuffle: chỗ tốn kém nhất, và cũng là chỗ quyết địnhThe shuffle: the costliest step, and the decisive one
Giữa map và reduce có một bước mà Kleppmann gọi thẳng là "shuffle": mỗi mapper băm khóa để chọn phân vùng, ghi kết quả đã sắp xếp ra đĩa cục bộ, rồi các reducer kéo về những phân vùng thuộc phần của mình và trộn các luồng đã sắp xếp lại111. Đây là nơi dữ liệu thật sự đi qua mạng, nên cũng là nơi hầu hết chi phí và hầu hết vấn đề hiệu năng nằm ở đó.
Giữa hai bước có một khâu ít ai để ý nhưng nặng nhất: chuyển tất cả những dòng cùng nhãn về cùng một máy. Dữ liệu phải chạy qua đường truyền giữa các máy, nên khâu này tốn thời gian nhất. Nhớ nhé: chỗ tốn nhất thường là chỗ phải chuyển đồ, không phải chỗ tính toán.
Between map and reduce sits a step Kleppmann simply calls "the shuffle": each mapper hashes the key to pick a partition, writes its sorted output to local disk, and each reducer pulls the partitions belonging to it and merges those sorted streams111. This is where data actually crosses the network, so it is where most of the cost and most of the performance trouble live.
Nhận ra ở đâuBất kỳ phépGROUP BYhayJOINphân tán nào cũng có một bước shuffle núp bên dưới — và đó là chỗ đầu tiên nên nhìn khi job chạy chậm.Thấy ở đâuMỗi khi phải "gom nhóm" trên nhiều máy, khâu chuyển dữ liệu này lại xuất hiện.Where you see itEvery distributedGROUP BYorJOINhides a shuffle underneath — and it is the first place to look when a job runs slowly.Từ job đơn lẻ tới quy trình
Một job MapReduce hiếm khi đủ. Kleppmann mô tả cách thực tế là nối nhiều job thành workflow: thư mục đầu ra của job này là thư mục đầu vào của job sau, và một bộ lập lịch chịu trách nhiệm chỉ khởi chạy job sau khi job trước đã hoàn tất. Đây là chỗ tương tự Unix bị hụt: dấu
|cho phép hai lệnh chạy đồng thời và truyền dữ liệu ngay, còn MapReduce buộc phải vật chất hóa toàn bộ kết quả trung gian trước khi đi tiếp.Vì sao phải sắp xếp
Sắp xếp không phải trang trí: nó là cách rẻ nhất để đảm bảo mọi giá trị của một khóa nằm liền nhau, nhờ đó reducer chỉ cần quét tuần tự qua một luồng đã trộn thay vì giữ cả tập khóa trong bộ nhớ. Đó cũng chính là lý do
sortđứng giữaawkvàuniqtrong ví dụ dòng lệnh ở phần trước6. -
Outputs: search indexes and key-value stores
Một quy trình theo lô kết thúc bằng cái gì?What does a batch pipeline end with?
Kleppmann nêu hai kiểu đầu ra tiêu biểu1. Thứ nhất là chỉ mục tìm kiếm — đúng công dụng đầu tiên của MapReduce tại Google, nơi chuỗi job dựng lại chỉ mục cho máy tìm kiếm8. Thứ hai là kho khóa–giá trị để phục vụ trực tuyến: job theo lô ghi ra các tệp cơ sở dữ liệu hoàn chỉnh, rồi hệ phục vụ nạp nguyên khối các tệp đó. LinkedIn mô tả đúng kiến trúc này trong bài về Project Voldemort13.
Chạy xong một mẻ thì được cái gì? Thường là một cuốn "mục lục" để tra cứu cho nhanh, hoặc một bảng tra sẵn để app dùng ngay. Điểm quan trọng: kết quả được ghi ra chỗ mới hoàn toàn, xong xuôi rồi mới đem ra dùng. Nhớ nhé: nấu xong cả nồi rồi mới dọn lên bàn.
Kleppmann names two characteristic outputs1. The first is a search index — MapReduce's original use at Google, where a chain of jobs rebuilt the index for the search engine8. The second is a key-value store for online serving: the batch job writes complete database files, and the serving system loads those files wholesale. LinkedIn describes exactly this architecture in its Project Voldemort paper13.
Nhận ra ở đâuMọi bảng tổng hợp, mô hình gợi ý hay chỉ mục tìm kiếm được "dựng lại hằng đêm" trong hệ của bạn đều là đầu ra của một quy trình theo lô.Thấy ở đâuNhững thứ được "làm mới mỗi đêm" trong hệ của bạn chính là kết quả của một mẻ như thế.Where you see itEvery summary table, recommendation model or search index "rebuilt nightly" in your system is the output of a batch pipeline.Vì sao không ghi thẳng vào cơ sở dữ liệu
Kleppmann cảnh báo rằng để job theo lô ghi trực tiếp từng bản ghi vào cơ sở dữ liệu đang phục vụ là một ý tồi: mỗi bản ghi một lần gọi mạng thì cực chậm, job chạy song song có thể nhấn chìm cơ sở dữ liệu, và tệ nhất là kết quả nửa vời sẽ lộ ra ngoài nếu job hỏng giữa chừng. Ghi ra tệp mới rồi chuyển đổi nguyên khối giữ được tính "tất cả hoặc không gì cả".
Bất biến sinh ra khả năng lùi lại
Vì đầu vào không bị sửa và đầu ra nằm ở một chỗ mới, bản cũ vẫn còn nguyên — nên khi mã có lỗi, ta chỉ cần vá rồi chạy lại, hoặc quay về bản đầu ra trước đó. Kleppmann gọi đây là khả năng dung thứ lỗi của con người, và coi nó là một lợi thế thiết kế chứ không phải may mắn. Kiến trúc nạp nguyên khối của Voldemort cũng vì lý do ấy mà giữ lại bản trước để quay lui13.
-
Hadoop versus distributed databases
Cùng ý tưởng cũ, khác nhau ở ba chỗThe same old idea, differing in three places
Xử lý song song trên nhiều máy không mới: các cơ sở dữ liệu song song quy mô lớn đã làm điều đó từ lâu, và một bài phê bình nổi tiếng năm 2008 của DeWitt và Stonebraker thậm chí gọi MapReduce là "một bước lùi lớn"14. Kleppmann đọc lại cuộc tranh luận ấy và rút ra ba khác biệt thật sự1: mô hình lưu trữ (Hadoop cho phép đổ dữ liệu thô vào trước, diễn giải sau, thay vì bắt định nghĩa lược đồ khi ghi), mô hình xử lý (chạy mã tùy ý chứ không chỉ SQL), và giả định về lỗi.
Trước Hadoop, người ta đã có những "cơ sở dữ liệu chạy trên nhiều máy" rồi. Khác nhau ở ba chỗ: Hadoop cho phép cứ đổ dữ liệu vào trước, sắp xếp ý nghĩa sau; cho phép chạy chương trình tùy ý chứ không chỉ các câu truy vấn; và nó coi máy hỏng là chuyện thường ngày. Nhớ nhé: mỗi lựa chọn đều đổi một tiện lợi lấy một tiện lợi khác.
Parallel processing across machines was not new: massively parallel databases had done it for years, and a famous 2008 critique by DeWitt and Stonebraker even called MapReduce "a major step backwards"14. Kleppmann revisits that argument and draws out three real differences1: the storage model (Hadoop lets you dump raw data first and interpret it later, rather than forcing a schema at write time), the processing model (arbitrary code, not only SQL), and the assumptions about failure.
Nhận ra ở đâuCuộc tranh luận "hồ dữ liệu hay kho dữ liệu" trong tổ chức của bạn chính là cuộc tranh luận này, chỉ đổi tên.Thấy ở đâuTranh cãi "đổ hết vào rồi tính sau" hay "sắp xếp gọn từ đầu" chính là chuyện này.Where you see itThe "data lake or data warehouse" argument in your organisation is this same debate under another name.Thiết kế cho lỗi xảy ra thường xuyên
Đây là khác biệt mà Kleppmann coi là sâu nhất. MapReduce chấp nhận ghi trạng thái trung gian xuống đĩa và chạy lại ở mức từng tác vụ, vì nó được thiết kế cho môi trường nơi tác vụ bị giết giữa chừng là chuyện bình thường — cụm dùng chung, tác vụ ưu tiên thấp bị thu hồi tài nguyên. Bài báo gốc mô tả đúng giả định ấy: với cụm hàng trăm đến hàng nghìn máy, hỏng hóc là điều thường gặp8.
Cái giá của sự thận trọng
Chính giả định "hay bị giết" khiến MapReduce ghi nhiều xuống đĩa và dùng bộ nhớ dè dặt. Với job ngắn trên cụm ít tranh chấp, sự thận trọng đó thành lãng phí thuần túy — và đó chính là kẽ hở mà các engine dòng dữ liệu ở phần sau sẽ khai thác15.
Đưa dữ liệu liên quan về cùng một chỗBringing related data to the same place
Trong một cơ sở dữ liệu có chỉ mục, join là chuyện tra cứu từng dòng. Trong một job theo lô đọc toàn bộ tập dữ liệu, tra cứu từng dòng qua mạng là thảm họa về thông lượng1. Kleppmann vì thế đóng khung join theo lô bằng một ý duy nhất: thay vì đi tìm dữ liệu, hãy sắp xếp để dữ liệu liên quan tự về cùng một chỗ. Toàn bộ các thuật toán join trong chương chỉ là những cách khác nhau để đạt được điều đó.
Muốn ghép "ai bấm vào gì" với "người đó là ai", cách ngây thơ là mỗi lần lại chạy đi hỏi một lần — làm thế với hàng tỉ dòng thì không xong nổi. Cách của xử lý theo lô là: xếp mọi thứ liên quan tới cùng một người nằm cạnh nhau, rồi đọc một lượt. Nhớ nhé: sắp xếp trước, rồi đọc một lần cho hết.
In an indexed database, a join is a per-row lookup. In a batch job that scans the whole dataset, per-row lookups over the network are a throughput disaster1. So Kleppmann frames batch joins around a single idea: instead of going out to find the data, sort so that related data arrives in the same place by itself. Every join algorithm in the chapter is a different way of achieving that.
Trong join phía reduce, hai tập dữ liệu được cho qua mapper riêng, cùng phát ra khóa join; tầng shuffle sắp xếp và dồn mọi bản ghi cùng khóa — bất kể chúng đến từ tập nào — về cùng một reducer, nơi chúng nằm liền nhau và được ghép bằng một lượt quét1. Ưu điểm lớn nhất: mã ứng dụng không cần biết gì về cách dữ liệu được tổ chức, nên đây là cách join tổng quát nhất.
Cách ghép phổ biến nhất: cho cả hai bảng cùng gắn nhãn theo mã người dùng, rồi để hệ thống xếp mọi dòng cùng mã về một chỗ. Khi chúng đã nằm cạnh nhau thì ghép rất dễ. Nhớ nhé: xếp cạnh nhau rồi ghép, đừng chạy đi tìm từng cái.
In a reduce-side join, both datasets pass through their own mappers, each emitting the join key; the shuffle sorts and routes every record with the same key — whichever dataset it came from — to the same reducer, where they sit adjacent and are merged in a single scan1. Its great advantage: the application code need assume nothing about how the data is laid out, which makes it the most general join.
Có một chi tiết kỹ thuật mà Kleppmann nêu và rất dễ bỏ qua: reducer cần thấy trước bản ghi hồ sơ người dùng rồi mới tới các sự kiện, để nó giữ hồ sơ trong bộ nhớ và chỉ việc quét qua danh sách sự kiện. Điều đó đạt được bằng secondary sort: khóa sắp xếp không chỉ là mã người dùng mà là một cặp (mã người dùng, mã loại bản ghi). Cùng một ý tưởng ấy — nhét thêm một thành phần vào khóa sắp xếp để điều khiển thứ tự bên trong nhóm — xuất hiện lại trong hầu hết engine theo lô về sau.
-
Reduce-side join & the grouping identity
Join và gom nhóm là cùng một cỗ máyJoins and grouping are the same machine
Kleppmann chỉ ra rằng GROUP BY và join dùng chung một cơ chế, chỉ khác ở chỗ chọn khóa1. Muốn gom nhóm thì cho mapper phát ra khóa nhóm; muốn join thì cho cả hai tập cùng phát ra khóa join. Trong cả hai trường hợp, tầng shuffle làm đúng một việc: đưa mọi bản ghi có cùng khóa tới cùng một reducer. Nhận ra sự đồng nhất đó giúp ta đọc mọi quy trình theo lô bằng một bộ từ vựng duy nhất.
"Gom nhóm" và "ghép bảng" tưởng là hai việc khác nhau, nhưng máy làm y hệt nhau: chọn một cái nhãn, rồi dồn mọi dòng cùng nhãn về một chỗ. Khác nhau chỉ ở chỗ bạn chọn nhãn là gì. Nhớ nhé: một cỗ máy, hai cách dùng.
Kleppmann points out that GROUP BY and joins share one mechanism and differ only in the choice of key1. To group, have the mapper emit the grouping key; to join, have both datasets emit the join key. In both cases the shuffle does exactly one thing: send every record with the same key to the same reducer. Seeing that identity lets you read any batch pipeline with a single vocabulary.
Nhận ra ở đâuPhân tích phiên truy cập, gom sự kiện theo người dùng, tính tổng theo ngày — tất cả đều là cùng một phép "chọn khóa rồi gom".Thấy ở đâuĐếm theo ngày, gom theo người dùng, ghép hai bảng — cùng một cách làm.Where you see itSessionization, grouping events by user, summing by day — all of them are the same "pick a key and gather" operation. -
Skew: when one key carries the job
Khóa nóng và chiếc reducer gánh cả cụmHot keys and the reducer that carries everything
Mô hình "một khóa, một reducer" sụp đổ khi một khóa có lượng bản ghi lớn bất thường — Kleppmann gọi những thực thể ấy là linchpin objects, chẳng hạn một người nổi tiếng có hàng triệu người theo dõi1. Cả job khi ấy phải chờ một reducer duy nhất. Tài liệu Apache Pig mô tả đúng vấn đề và cách chữa: skewed join lấy mẫu đầu vào để dựng một biểu đồ tần suất của không gian khóa, rồi dựa vào đó phân bổ nhiều reducer cho một khóa quá nặng12.
Nếu chia việc theo tên người, mà có một người đông gấp triệu lần mọi người khác, thì máy nào nhận người đó sẽ è cổ ra làm còn các máy khác ngồi chơi. Cách chữa: đếm trước xem ai đông bất thường, rồi chia phần của người đó ra cho nhiều máy, xong mới cộng lại. Nhớ nhé: chia đều theo số việc, đừng chia đều theo số nhãn.
The "one key, one reducer" model collapses when a key has an unusual mass of records — Kleppmann calls such entities linchpin objects, for instance a celebrity with millions of followers1. The whole job then waits on a single reducer. Apache Pig's documentation describes the problem and the cure precisely: a skewed join samples the input to build a histogram of the key space and uses it to allocate several reducers to an over-heavy key12.
Nhận ra ở đâuJob nào có 99% tác vụ xong trong mười phút còn một tác vụ chạy ba tiếng thì gần như chắc chắn đang gặp khóa lệch.Thấy ở đâuKhi gần hết việc đã xong mà còn một phần lê thê mãi, thường là do một nhãn quá đông.Where you see itA job where 99% of tasks finish in ten minutes and one runs for three hours is almost certainly hitting key skew.Lấy mẫu trước, chia sau
Điểm chung của mọi cách chữa là phải biết trước khóa nào nóng — mà muốn biết thì phải chạy một lượt lấy mẫu. Pig mô tả cụ thể: đầu vào bên trái được lấy mẫu để dựng biểu đồ, sau đó khóa nặng được tách ra nhiều reducer còn đầu vào bên phải được phát dòng sang các reducer tương ứng12. Kleppmann ghi nhận rằng các công cụ khác trong hệ sinh thái giải bài toán này theo những cách hơi khác nhau, từ khai báo thủ công tới suy diễn tự động.
Cái giá của việc băm nhỏ
Chia một khóa nóng ra nhiều reducer đồng nghĩa với việc phía bên kia của join phải được nhân bản sang từng reducer ấy, và kết quả cần một bước gộp ở job sau. Nói cách khác, ta đổi thời gian chờ lấy thêm lưu lượng mạng — một đánh đổi chỉ đáng khi độ lệch thật sự lớn.
-
Broadcast hash join
Khi một bên đủ nhỏ để mang theoWhen one side is small enough to carry
Nếu một trong hai tập dữ liệu đủ nhỏ để nằm gọn trong bộ nhớ, ta không cần shuffle chút nào: mỗi mapper nạp toàn bộ tập nhỏ vào một bảng băm rồi quét tập lớn, tra cứu tại chỗ1. Kleppmann gọi đây là broadcast hash join — "phát sóng" vì tập nhỏ được gửi tới mọi mapper. Pig hiện thực đúng ý tưởng này dưới tên replicated join, và tài liệu nói rõ toàn bộ công việc khi ấy diễn ra ở phía map12.
Nếu một bảng nhỏ xíu — ví dụ danh sách 300 tỉnh thành — thì cứ đưa cho mỗi máy một bản, khỏi phải chuyển gì qua lại. Mỗi máy tự tra bảng nhỏ trong lúc đọc bảng lớn. Nhớ nhé: bảng nhỏ thì mang theo, đừng bắt cả hệ chạy qua chạy lại.
If one of the two datasets fits in memory, no shuffle is needed at all: each mapper loads the small dataset into a hash table and scans the large one, looking up as it goes1. Kleppmann calls this a broadcast hash join — "broadcast" because the small side is sent to every mapper. Pig implements exactly this under the name replicated join, and its documentation states plainly that all the work then happens on the map side12.
Nhận ra ở đâuMọi bảng tra cứu nhỏ — danh mục, bảng ánh xạ mã, danh sách cấu hình — đều là ứng viên của kiểu join này, và chuyển sang nó thường là tối ưu rẻ nhất.Thấy ở đâuDanh mục sản phẩm, bảng mã vùng, danh sách nhỏ nào cũng hợp với cách này.Where you see itEvery small lookup table — a catalogue, a code mapping, a config list — is a candidate, and switching to it is often the cheapest optimisation available.Ranh giới "đủ nhỏ"
"Đủ nhỏ" là một ngưỡng thật, không phải cách nói. Tài liệu Pig cảnh báo thẳng rằng nếu các quan hệ nhỏ không nằm vừa bộ nhớ chính thì tiến trình sẽ thất bại và báo lỗi12. Kleppmann cũng nhắc rằng thay vì bảng băm trong bộ nhớ, ta có thể để tập nhỏ dưới dạng một chỉ mục chỉ-đọc trên đĩa cục bộ và dựa vào bộ đệm của hệ điều hành.
-
Partitioned hash join & map-side merge join
Khi dữ liệu đã được sắp sẵn từ trướcWhen the data was laid out in advance
Nếu cả hai tập đã được phân vùng theo cùng một khóa và cùng số phân vùng, mapper thứ n chỉ cần đọc phân vùng thứ n của cả hai bên — bài toán lớn vỡ thành nhiều bài toán nhỏ độc lập, mỗi cái đủ nhỏ để dùng bảng băm1. Nếu các phân vùng ấy còn được sắp xếp sẵn theo khóa, ta thậm chí không cần nạp gì vào bộ nhớ: chỉ việc trộn hai luồng đã sắp xếp. Tài liệu Pig mô tả đúng trường hợp đó và nói rõ lợi ích là bỏ qua được các pha sort và shuffle không cần thiết12.
Nếu hai bảng đã được chia cùng kiểu và xếp cùng thứ tự từ trước, thì việc ghép trở nên nhẹ tênh: máy số 3 chỉ cần đọc phần số 3 của cả hai bảng, đọc song song một lượt là xong. Nhớ nhé: chịu khó sắp xếp từ trước thì về sau đỡ rất nhiều.
If both datasets are already partitioned by the same key into the same number of partitions, mapper n only needs partition n of each side — the big problem shatters into independent small ones, each small enough for a hash table1. And if those partitions are also sorted by key, nothing need be loaded into memory at all: just merge two sorted streams. Pig's documentation describes exactly this case and states the benefit as skipping unneeded sort and shuffle phases12.
Nhận ra ở đâuCác bảng được "bucket" theo cùng một cột trong kho dữ liệu của bạn tồn tại chính là để mở khóa kiểu join này.Thấy ở đâuKhi hai bảng được chia sẵn theo cùng một cột, ghép chúng nhanh hơn hẳn.Where you see itTables bucketed on the same column in your warehouse exist precisely to unlock this join.Hình dạng đầu ra kế thừa hình dạng đầu vào
Kleppmann nêu một hệ quả thực dụng: khi dùng join phía map, cách phân vùng và sắp xếp của đầu ra bị quy định bởi đầu vào, vì không có tầng reduce nào để tổ chức lại. Điều đó biến cách bố trí dữ liệu thành một quyết định kiến trúc: bạn chọn khóa phân vùng một lần, và mọi job phía sau sống với lựa chọn đó.
Ai quyết định dùng thuật toán nào
Trong thực tế, người viết truy vấn hiếm khi chọn tay. Bài báo về Pig mô tả hệ thống này như một tầng dòng dữ liệu mức cao dựng trên MapReduce, nơi các phép toán quan hệ được biên dịch xuống thành job21; còn Hive được giới thiệu như một kho dữ liệu quy mô petabyte với trình biên dịch riêng22. Kleppmann ghi nhận rằng khi đã có lược đồ và thống kê, trình tối ưu có thể tự chọn thuật toán join — đó là lúc xử lý theo lô bắt đầu giống cơ sở dữ liệu trở lại.
Vượt khỏi MapReduce: engine dòng dữ liệuBeyond MapReduce: dataflow engines
Phần cuối chương là một sự tự phê bình có kiểm soát. Kleppmann giữ nguyên các nguyên tắc — đầu vào bất biến, đầu ra mới, chạy lại được — nhưng chỉ ra rằng cách hiện thực của MapReduce không phải cách duy nhất1. Một lớp engine mới, mà ông gọi là dataflow engines, coi cả quy trình là một job duy nhất gồm nhiều toán tử thay vì một chuỗi job độc lập: Spark15, Tez16 và Flink17 là ba đại diện ông nêu tên.
Phần cuối chương thừa nhận: cách làm cũ có chỗ dở. Thay vì chia quy trình thành nhiều mẻ tách rời, mỗi mẻ phải ghi hết ra đĩa rồi mới tới mẻ sau, thế hệ sau coi cả quy trình là một việc liền mạch. Kết quả giữa chừng không cần đổ ra đĩa nữa, nên nhanh hơn nhiều. Nhớ nhé: bớt được bước dọn dẹp giữa chừng là bớt được rất nhiều thời gian.
The last part of the chapter is a controlled self-critique. Kleppmann keeps the principles — immutable inputs, fresh outputs, re-runnability — but shows that MapReduce's implementation is not the only one possible1. A newer class of engines, which he calls dataflow engines, treats the whole pipeline as one job made of several operators rather than a chain of independent jobs: Spark15, Tez16 and Flink17 are the three he names.
Kleppmann gọi tên chính xác chỗ lãng phí: MapReduce vật chất hóa mọi trạng thái trung gian — ghi trọn đầu ra của mỗi job vào hệ tệp phân tán, nhân bản nó, rồi job sau mới đọc lên1. Hệ quả là job sau không thể bắt đầu trước khi job trước kết thúc hoàn toàn, nhiều mapper chỉ làm mỗi việc đọc lại thứ reducer vừa ghi, và dữ liệu tạm bị nhân bản như thể nó quý giá bằng dữ liệu gốc.
Chỗ tốn kém nhất là: mỗi bước xong đều phải chép kết quả ra đĩa, chép thành mấy bản cho chắc, rồi bước sau mới được đọc. Nhưng kết quả giữa chừng thì cần gì phải giữ kỹ đến thế — làm lại là có ngay. Nhớ nhé: đồ dùng tạm thì đừng cất kỹ như đồ quý.
Kleppmann names the waste precisely: MapReduce materialises all intermediate state — writing each job's full output to the distributed filesystem, replicating it, and only then letting the next job read it1. So the next job cannot start before the previous one fully ends, many mappers do nothing but read back what a reducer just wrote, and temporary data is replicated as though it were as precious as the source.
Engine dòng dữ liệu thay chuỗi job bằng một đồ thị có hướng không chu trình gồm các toán tử, và cho phép chọn cách nối giữa hai toán tử: phân vùng lại kèm sắp xếp (đúng bằng shuffle cũ), phân vùng lại mà không sắp xếp, hoặc phát sóng. Trang chủ Tez mô tả đúng mô hình ấy: xây một đồ thị tác vụ phức tạp với API định nghĩa dòng dữ liệu và mô hình Input–Processor–Output linh hoạt, chạy trên YARN16. Nhờ vậy engine bỏ được những lần sắp xếp không cần thiết, giữ trạng thái trung gian trong bộ nhớ hoặc đĩa cục bộ, và khởi động toán tử ngay khi đầu vào của nó sẵn sàng — ba khoản tiết kiệm mà Kleppmann liệt kê rõ.
-
Fault tolerance by recomputation
Không cất giữ thì lấy gì khôi phục?If nothing is stored, what is there to recover?
Bỏ vật chất hóa thì mất luôn chỗ dựa để chạy lại — đó là cái giá phải trả, và lời giải là tính lại từ đầu nguồn1. Spark ghi lại phả hệ của mỗi tập dữ liệu: nó được sinh ra từ tập nào, qua những phép biến đổi nào; mất một mảnh thì dựng lại đúng mảnh ấy. Tài liệu chính thức của Spark phát biểu tính chất này gọn lỏn — RDD là tập dữ liệu phân tán có khả năng phục hồi, và chúng tự động khôi phục sau khi một nút hỏng27.
Nếu không cất kết quả giữa chừng, lỡ một máy hỏng thì sao? Trả lời: làm lại đúng phần đó. Hệ thống ghi nhớ "phần này được làm ra từ đâu, bằng cách nào", nên dựng lại được. Nhớ nhé: nhớ công thức thì không cần cất sẵn món ăn.
Dropping materialisation also drops the thing you would restart from — that is the price, and the answer is recomputation from the source1. Spark records each dataset's lineage: which dataset it came from and through which transformations; lose a piece and it rebuilds exactly that piece. Spark's own documentation states the property tersely — RDDs are resilient distributed datasets, and they automatically recover from node failures27.
Nhận ra ở đâuMỗi khi bạn thấy một engine "tự chạy lại phần hỏng" mà không cần bạn can thiệp, cơ chế bên dưới hoặc là phả hệ, hoặc là điểm kiểm tra định kỳ.Thấy ở đâuKhi hệ tự làm lại phần hỏng mà bạn không phải làm gì, đó là cơ chế này.Where you see itWhenever an engine "just re-runs the broken piece" without your help, the mechanism underneath is either lineage or periodic checkpointing.Tính tất định là điều kiện ngầm
Kleppmann cảnh báo một điều kiện mà người ta hay quên: tính lại chỉ đúng nếu toán tử tất định. Nếu hàm của bạn dùng số ngẫu nhiên, đọc đồng hồ hệ thống, hay duyệt một cấu trúc theo thứ tự địa chỉ bộ nhớ, thì lần chạy lại sẽ cho kết quả khác — và những tác vụ phía sau đã nhận kết quả cũ sẽ trở nên mâu thuẫn. Cách chữa là loại bỏ nguồn bất định, chẳng hạn gieo hạt ngẫu nhiên cố định theo phân vùng.
Khi tính lại đắt hơn cất giữ
Bài báo gốc về RDD đặt nền cho mô hình phả hệ và cho thấy vì sao việc giữ dữ liệu trong bộ nhớ giữa các bước lại cải thiện mạnh các khối lượng công việc lặp15. Nhưng Kleppmann lưu ý phần còn lại của đánh đổi: nếu chuỗi biến đổi quá dài hoặc quá tốn kém, vật chất hóa một điểm ở giữa lại rẻ hơn là dựng lại từ đầu. Không phải "đừng bao giờ ghi ra đĩa", mà là "đừng ghi ra đĩa theo mặc định".
-
Graph processing and the Pregel model
Đồ thị: khi kết quả phải quay lại làm đầu vàoGraphs: when the output must feed back in
Nhiều thuật toán đồ thị — tìm đường, xếp hạng trang — phải lặp đi lặp lại cho tới khi ổn định, điều mà chuỗi job MapReduce làm rất vụng vì mỗi vòng lặp là một job đọc lại toàn bộ đồ thị1. Mô hình Pregel của Google đảo ngược góc nhìn: lập trình theo quan điểm của một đỉnh, mỗi đỉnh gửi thông điệp dọc theo các cạnh, và một vòng lặp là một "siêu bước" đồng bộ18. Kleppmann chỉ rõ gốc gác của mô hình đồng bộ khối này là mô hình bulk synchronous parallel mà Valiant đề xuất năm 199019.
Có những bài toán phải làm đi làm lại nhiều vòng mới ra, ví dụ tìm đường đi trong một mạng lưới bạn bè. Cách hay là: coi mỗi điểm trong mạng như một người, mỗi vòng thì mỗi người nhắn tin cho hàng xóm, rồi cả lớp cùng chờ nhau xong vòng đó mới sang vòng sau. Nhớ nhé: đi từng vòng, cả nhóm cùng nhịp.
Many graph algorithms — shortest paths, page ranking — must iterate until they settle, which a chain of MapReduce jobs handles clumsily because each iteration is a job that re-reads the entire graph1. Google's Pregel model inverts the viewpoint: program from the perspective of one vertex, each vertex sending messages along its edges, with one iteration being a synchronous "superstep"18. Kleppmann traces this bulk-synchronous scheme to the bulk synchronous parallel model Valiant proposed in 199019.
Nhận ra ở đâuMọi thuật toán "lan truyền dần ra hàng xóm rồi lặp lại" — từ gợi ý bạn bè tới phát hiện cộng đồng — đều hợp với khuôn này.Thấy ở đâuGợi ý "người bạn có thể biết" chạy theo đúng kiểu lan dần từng vòng như vậy.Where you see itEvery algorithm that "spreads to neighbours and repeats" — friend suggestions, community detection — fits this shape.Chịu lỗi theo siêu bước
Vì mọi đỉnh cùng bước qua từng vòng một cách đồng bộ, khung chỉ cần lưu điểm kiểm tra ở cuối mỗi siêu bước; hỏng thì quay lại siêu bước gần nhất và chạy lại18. Đây là một biến thể khác của cùng ý tưởng chạy-lại-được: ranh giới đồng bộ tạo ra những điểm nhất quán để lùi về.
Đừng vội cho rằng phân tán là thắng
Kleppmann dẫn một phản biện thẳng thắn với xử lý đồ thị phân tán: bài “Scalability! But at What COST?” đề xuất đo một nền tảng bằng lượng phần cứng cần có để nó vượt được một hiện thực đơn luồng viết tử tế, và cho thấy nhiều hệ thống được công bố có chi phí ấy lên tới hàng trăm lõi — hoặc không bao giờ vượt nổi một luồng ở mọi cấu hình đã báo cáo20. Phân vùng một đồ thị là bài toán khó, nên với đồ thị vừa phải, một máy thường thắng.
-
High-level APIs and declarative queries
Viết ít đi để máy tối ưu được nhiều hơnWrite less so the machine can optimise more
Viết tay mapper và reducer là việc tốn công và dễ sai, nên quanh MapReduce mọc lên một tầng API mức cao mà Kleppmann liệt kê: Hive, Pig, Cascading và Crunch1. Bài báo giới thiệu Pig mô tả nó như một hệ dòng dữ liệu mức cao dựng trên MapReduce21; bài về Hive giới thiệu một kho dữ liệu quy mô petabyte chạy trên Hadoop22; còn Crunch tự mô tả là thư viện Java để viết và chạy các quy trình dữ liệu23. Điểm chung: càng khai báo cái gì thay vì làm thế nào, engine càng có chỗ để tự chọn thuật toán join và thứ tự thực thi.
Viết tay từng bước rất mệt, nên người ta làm ra những cách viết ngắn gọn hơn — gần giống viết câu hỏi cho máy tính thay vì viết chương trình. Bạn nói muốn gì, máy tự nghĩ cách làm nhanh nhất. Nhớ nhé: nói rõ mong muốn thì máy mới giúp bạn tối ưu được.
Writing mappers and reducers by hand is laborious and error-prone, so a layer of high-level APIs grew around MapReduce, which Kleppmann lists: Hive, Pig, Cascading and Crunch1. The paper introducing Pig describes it as a high-level dataflow system built on MapReduce21; the Hive paper presents a petabyte-scale data warehouse on Hadoop22; and Crunch describes itself as a Java library for writing and running data pipelines23. The common thread: the more you declare what instead of how, the more room the engine has to pick join algorithms and execution order itself.
Nhận ra ở đâuMỗi lần bạn viết SQL trên một engine phân tán thay vì viết mã xử lý tay, bạn đang đứng đúng ở tầng này.Thấy ở đâuViết câu truy vấn thay vì viết chương trình chính là ý tưởng này.Where you see itEvery time you write SQL on a distributed engine instead of hand-rolling the processing code, you are standing exactly at this layer.Khai báo một phần, không phải hoàn toàn
Kleppmann nêu một sắc thái quan trọng: các hệ này không thuần khai báo như SQL. Chúng vẫn cho phép nhúng hàm tùy ý vào giữa luồng — thứ mà cơ sở dữ liệu quan hệ thuần túy không làm được — nên vẫn giữ được ưu thế "chạy mã bất kỳ" của Hadoop, trong khi nhường phần join và gom nhóm cho trình tối ưu. Đó là một thỏa hiệp có chủ đích, không phải nửa vời.
Chuyên biệt hóa dần
Ngoài các phép toán quan hệ, Kleppmann ghi nhận xu hướng đóng gói sẵn những họ thuật toán hay lặp lại — học máy thống kê, phân loại, thuật toán không gian, tìm kiếm gần đúng — thành thư viện dùng lại được trên cùng hạ tầng theo lô. Xử lý theo lô vì thế dần trở thành một nền chung, chứ không còn là một kỹ thuật riêng lẻ.
-
The batch/stream boundary begins to blur
Theo lô chỉ là dòng dữ liệu có điểm dừngBatch is just a stream with an end
Ranh giới giữa theo lô và theo luồng mỏng hơn vẻ ngoài. Kleppmann đặt Flink cạnh Spark và Tez trong cùng một họ engine dòng dữ liệu1, và tài liệu chính thức của Flink mô tả nó là khung xử lý phân tán cho các phép tính có trạng thái trên cả luồng dữ liệu vô hạn lẫn hữu hạn17. Nói cách khác, một tập dữ liệu theo lô chỉ là một luồng tình cờ có điểm kết thúc — và đó chính là cây cầu bắc sang Chương 11.
"Làm cả mẻ" và "làm liên tục" thật ra rất giống nhau: một mẻ chỉ là dòng dữ liệu có điểm dừng. Cùng một cỗ máy có thể làm cả hai, chỉ khác chỗ dữ liệu có kết thúc hay không. Nhớ nhé: hữu hạn hay vô hạn, cách nghĩ vẫn thế.
The boundary between batch and stream is thinner than it looks. Kleppmann places Flink alongside Spark and Tez in the same family of dataflow engines1, and Flink's own documentation describes it as a distributed processing framework for stateful computations over both unbounded and bounded data streams17. Put another way, a batch dataset is simply a stream that happens to end — and that is the bridge into Chapter 11.
Nhận ra ở đâuKhi một engine cho bạn chạy cùng một đoạn mã trên dữ liệu lịch sử và trên dữ liệu đang tới, bạn đang thấy chính sự hợp nhất này.Thấy ở đâuCùng một đoạn mã chạy được cho dữ liệu cũ lẫn dữ liệu mới về — đó là điều này.Where you see itWhen one engine lets you run the same code over historical data and over arriving data, you are seeing this unification.
Ba ngộ nhận thường gặp về xử lý theo lôThree common misreadings of batch processing
“Theo lô nghĩa là chậm”"Batch means slow"
Sai ở chỗ nhầm thước đo. Hệ theo lô không tối ưu thời gian phản hồi của một yêu cầu mà tối ưu thông lượng — lượng dữ liệu đi qua trong một đơn vị thời gian1. Một job chạy hai giờ để xử lý mười terabyte không "chậm"; nó đang được đo bằng một cây thước khác.
This confuses the metric. A batch system does not optimise one request's response time but throughput — data processed per unit of time1. A job that takes two hours to chew through ten terabytes is not "slow"; it is being measured with a different ruler.
“Cứ dữ liệu lớn là phải dùng cụm”"Big data always needs a cluster"
Sai: chi phí điều phối của hệ phân tán chỉ đáng trả khi dữ liệu thật sự vượt một máy. Một đường ống dòng lệnh từng xử lý xong một bộ dữ liệu nhanh hơn khoảng 235 lần so với job Hadoop tương ứng7, và với đồ thị vừa phải, một hiện thực đơn luồng viết tử tế thường thắng cả nền tảng phân tán20.
Wrong: a distributed system's coordination cost is only worth paying when the data genuinely exceeds one machine. A command-line pipeline once beat the equivalent Hadoop job by roughly 235×7, and for moderate graphs a competent single-threaded implementation often beats the distributed platform outright20.
“MapReduce lỗi thời nên chương này vô dụng”"MapReduce is obsolete, so this chapter is useless"
Nhầm công cụ với nguyên tắc. Thứ chương này dạy là kỷ luật đầu vào bất biến, đầu ra mới và khả năng chạy lại — và chính các engine thế hệ sau cũng dựng trên đúng kỷ luật ấy, chỉ đổi cách hiện thực1527.
This confuses the tool with the principle. What the chapter teaches is the discipline of immutable inputs, fresh outputs and re-runnability — and the later engines are built on exactly that discipline, changing only the implementation1527.
Soi quy trình theo lô của chính bạnAudit your own batch pipeline
Chương 10 cho bạn một bộ từ vựng để nhìn lại bất kỳ quy trình nào chạy theo lịch — báo cáo hằng đêm, bảng tổng hợp, mô hình huấn luyện lại hằng tuần. Những câu hỏi dưới đây không đo quy trình của bạn "lớn" tới đâu, mà soi xem nó có giữ được ba kỷ luật cốt lõi: bất biến, chạy lại được, và biết mình đang tối ưu cho cái gì.
Thử soi một việc chạy đều đặn trong hệ của bạn — ví dụ báo cáo mỗi đêm — bằng mấy câu dưới. Không phải để khoe nó to, mà để xem nó có làm lại được khi sai hay không.
Chapter 10 gives you a vocabulary for re-reading any scheduled pipeline — a nightly report, a summary table, a weekly retrained model. The questions below don't measure how "big" your pipeline is; they check whether it keeps the three core disciplines: immutability, re-runnability, and knowing what it is optimising for.
Xử lý theo lô không phải một công nghệ mà là một kỷ luật: đọc dữ liệu không được phép sửa, viết kết quả sang một chỗ hoàn toàn mới, và nhờ thế chạy lại bao nhiêu lần cũng được. Triết lý Unix đã phát biểu kỷ luật ấy trên một máy; MapReduce phóng nó lên hàng nghìn máy; các engine dòng dữ liệu bỏ đi những lần ghi thừa mà vẫn giữ nguyên nó. Công cụ đến rồi đi, nhưng lời hứa còn lại thì không đổi: khi đầu vào vẫn còn nguyên vẹn, mọi sai lầm đều có thể sửa bằng cách làm lại."Làm cả mẻ" không phải một cái máy, mà là một thói quen tốt: không sửa bản gốc, ghi kết quả sang chỗ mới, nên sai thì làm lại được. Từ mấy câu lệnh trên một máy cho tới hệ thống nghìn máy, cách nghĩ vẫn y như vậy. Nhớ nhé: giữ nguyên bản gốc thì không có lỗi nào là không sửa được.Batch processing is not a technology but a discipline: read data you may not modify, write results somewhere entirely new, and therefore re-run as often as you like. The Unix philosophy stated that discipline on one machine; MapReduce blew it up to thousands; dataflow engines stripped the redundant writes while keeping it intact. Tools come and go, but the remaining promise does not change: while the input is still untouched, every mistake is fixable by doing it again.