۱1 مفاهیم پایهCore concepts
۱.۱. آنلاین در برابر دستهای1.1. Online vs batch
پردازش آنلاین برای پاسخ به درخواستِ حاضر کار میکند؛ پردازش دستهای روی مجموعهای با پایان مشخص اجرا میشود و خروجی را بعداً آماده میکند. معیار موفقیتِ این دو الزاماً یکسان نیست.
مثال فنی: پاسخ کاربر یا پایان job
وضعیت اولیه: API وضعیت سفارش باید در چندصد میلیثانیه پاسخ دهد؛ گزارش کل سال ممکن است نیم ساعت طول بکشد.
اجرای مرحلهبهمرحله: API درخواست محدود را همان لحظه انجام میدهد؛ job دستهای ورودیِ کراندار را کامل میخواند و خروجی را منتشر میکند.
نتیجه و دلیل: معیار آنلاین بیشتر latency هر درخواست و معیار batch بیشتر زمان پایان و هزینهٔ کل پردازش است.
Most things built so far were “ask, get an answer”: the browser wants a page, a remote service calls an API, the database, cache and search answer. These are online systems and their core metric is response time. But sometimes you hold a computation that fits no interactive request: training a model, transforming a huge data volume, or analyzing a large dataset. These are batch jobs, and the systems running them are offline systems.
قراردادِ یک job دستهای ساده است: ورودیِ read-only بخوان، خروجی را از صفر بساز. مثلِ تراکنشِ read/write نیست
که داده را mutate کند؛ خروجی از ورودی derived است. اگر از خروجی خوشت نیامد، پاکش کن، منطقِ job را درست کن،
دوباره اجرا کن. همین «immutable بودنِ ورودی + نداشتنِ side effect» چند مزیتِ بزرگ میدهد:
A batch job's contract is simple: read read-only input, build output from scratch. Unlike a read/write transaction it never mutates data; output is derived from input. Dislike the output? Delete it, fix the job logic, rerun. This “immutable input + no side effects” buys several big wins:
-
بازگشت از باگِ کد: اگر باگ بزنی و خروجی خراب شود، کافی است به نسخهٔ قبلیِ کد برگردی و job را دوباره اجرا کنی —
یا حتی سادهتر: خروجیِ قبلی را در مسیرِ دیگری نگه داشتهای و به آن سوییچ میکنی. بیشترِ object storeها و
open table formatها این را پشتیبانی میکنند و به آن time travel میگویند.
پایگاههای داده تراکنشی معمولاً این خاصیت را ندارند: اگر کدِ باگی دادهٔ بد بنویسد، برگرداندنِ کد چیزی را درست نمیکند.
این تواناییِ بازیابی از خطای کد را human fault tolerance نامیدهاند.
-
توسعهٔ سریعتر: وقتی اشتباه یعنی «آسیبِ برگشتناپذیر» نیست، تیمها سریعتر feature میسازند.
این همان اصلِ «کمینهسازیِ برگشتناپذیری» است که برای توسعهٔ چابک هم خوب است.
-
استفادهٔ مجدد از یک مجموعه فایل: همان ورودی میتواند ورودیِ jobهای مختلف باشد، از جمله jobهای مانیتورینگی
که خروجیِ job را با اجرای قبلی مقایسه و اختلاف را اندازه میگیرند.
-
بهرهوری از منابع: frameworkهای دستهای منابع را کارآمد مصرف میکنند؛ پردازشِ دستهای با سیستمهای آنلاین
(مثل پایگاه دادهِ OLTP یا اپسرور) ممکن است ولی از نظرِ منابع بسیار گرانتر است.
-
Recovering from code bugs: if you ship a bug and corrupt the output, just revert to the previous code version and rerun the job — or simpler: you kept the previous output under another path and switch back to it. Most object stores and open table formats support this, calling it time travel. Transactional databases usually lack this property: if buggy code writes bad data, reverting the code fixes nothing. This recovery-from-code-errors ability is called human fault tolerance.
-
Faster development: when a mistake means no “irreversible damage”, teams ship features faster. This is the same “minimize irreversibility” principle that serves agile development well.
-
Reusing one file set: the same input can feed different jobs, including monitoring jobs that compare a job's output with the previous run and measure the drift.
-
Resource efficiency: batch frameworks consume resources efficiently; batch work on online systems (like an OLTP database or app server) is possible but far costlier in resources.
بهای این مدلThe price of this model
در بیشترِ frameworkها، خروجیِ job فقط بعد از پایانِ کلِ job قابلِ استفاده توسط jobهای دیگر است.
و کارایی هم میتواند افت کند: هر تغییر در ورودی — حتی یک بایت — یعنی پردازشِ دوبارهٔ کلِ dataset ورودی.
یک job ممکن است دقیقهها، ساعتها یا حتی روزها طول بکشد و معمولاً بهصورتِ دورهای (مثلاً روزانه) زمانبندی شود؛
پس معیارِ اصلی throughput است، نه سرعتِ پاسخ.
In most frameworks a job's output is usable by other jobs only after the whole job ends. And performance can suffer: any input change — even one byte — means reprocessing the entire input dataset. A job may take minutes, hours or even days and is usually scheduled periodically (e.g. daily); so the core metric is throughput, not response speed.
بعضی سیستمهای دستهای با خطای task با «کشتن و اجرای دوبارهٔ کلِ job» کنار میآیند؛ بعضی دیگر
fault tolerance دارند: job با وجودِ مردنِ چند نود باز هم کامل میشود.
مرزِ بینِ آنلاین و دستهای همیشه تیز نیست — یک کوئریِ بلندِ پایگاه داده خیلی شبیهِ پردازشِ دستهای است —
ولی همین مدلِ دستهای است که آن را به بلوکِ ساختِ سیستمهای قابلاتکا، مقیاسپذیر و قابلنگهداری تبدیل میکند؛
مثلاً در data integration و ETL.
(جایگزینِ دستهای، پردازش جریانی است: job بعد از پایانِ ورودی تمام نمیشود و تغییرات را همان لحظه پردازش میکند — فصل ۱۲.)
Some batch systems handle a task failure by “killing and rerunning the whole job”; others have fault tolerance: the job still completes though several nodes died. The online/batch boundary isn't always sharp — a long database query looks a lot like batch processing — but this batch model is what turns it into a building block of dependable, scalable, maintainable systems; e.g. in data integration and ETL. (Batch's alternative is stream processing: the job never ends after the input runs out and processes changes as they happen — Chapter 12.)
۱.۲. از MapReduce تا امروز1.2. From MapReduce to today
MapReduce روشی برای شکستنِ پردازش بزرگ به کارهای کوچک و جمع کردنِ نتیجههاست. موتورهای جدیدتر میتوانند زنجیرههای پیچیدهتری از تبدیلها را با سربار کمتر اجرا کنند.
مثال فنی: از چند job تا یک DAG
وضعیت اولیه: ابتدا داده پاکسازی میشود، سپس join و در آخر دو گزارش ساخته میشود.
اجرای مرحلهبهمرحله: MapReduce میتواند این مراحل را چند job جدا کند؛ موتور dataflow آنها را به گراف عملگرها تبدیل و بعضی مرحلهها را ادغام میکند.
نتیجه و دلیل: مدل اجرا هزینهٔ فایل میانی و زمانبندی را عوض میکند؛ هدف هنوز ساخت خروجی درست از ورودی مشخص است.
Modern batch processing is deeply shaped by MapReduce — the algorithm Google published in 2004, later implemented in Hadoop, CouchDB and MongoDB. MapReduce is a fairly low-level programming model; less capable than the parallel execution engines of analytical databases. Freshly arrived, it was a leap in processable scale on commodity hardware, but today it is largely obsolete and no longer used at Google.
پردازشِ دستهایِ امروز معمولاً با Spark یا Flink یا موتورهای کوئریِ
data warehouse انجام میشود. اینها مثلِ MapReduce به sharding (فصل ۷) و اجرای موازی تکیه دارند،
ولی استراتژیهای کشینگ و اجرای بسیار پیچیدهتری دارند. با بالغشدنِ این سیستمها، دغدغههای عملیاتی تا حدِ زیادی حل شد
و تمرکز به قابلیتِ استفاده رفت: مدلهای پردازشِ جدید مثل dataflow APIها،
زبانهای کوئری و DataFrame APIها حالا همهجا پشتیبانی میشوند.
ابزارهای orchestrations هم بالغ شدند: schedulerهای Hadoop-محور مثل Oozie و
Azkaban جای خود را به راهحلهای عمومیتر مثل Airflow و
Dagster و Prefect دادهاند که طیفِ وسیعی از frameworkها و
warehouseهای ابری را پشتیبانی میکنند.
Today's batch processing usually runs on Spark or Flink or data-warehouse query engines. Like MapReduce they rely on sharding (Chapter 7) and parallel execution, but with far more sophisticated caching and execution strategies. As these systems matured, operational concerns were largely solved and focus moved to usability: new processing models like dataflow APIs, query languages and DataFrame APIs are now supported everywhere. Orchestration tooling matured too: Hadoop-centric schedulers like Oozie and Azkaban gave way to general solutions like Airflow, Dagster and Prefect supporting a wide range of frameworks and cloud warehouses.
و البته ابر: لایهٔ ذخیرهسازیِ دستهای دارد از فایلسیستمهای توزیعشده مثل HDFS و
GlusterFS و CephFS به object storeهایی مثل S3
منتقل میشود؛ و data warehouseهای ابریِ مقیاسپذیر مثل BigQuery و Snowflake
مرزِ بینِ warehouse و پردازشِ دستهای را محو کردهاند. نتیجه: «همگرایی» — که در §۴.۶ دقیقتر میبینیمش.
And of course the cloud: batch storage is moving from distributed filesystems like HDFS, GlusterFS and CephFS to object stores like S3; and scalable cloud data warehouses like BigQuery and Snowflake blurred the line between warehouse and batch processing. Result: “convergence” — seen precisely in §4.6.
مفهوم مکمل — آنلاین در برابر دستهایExtra concept — online vs batch
روایت مدرس از نمودارTeacher narration
مثال فنی برای خواندنِ نمودار: پاسخ کاربر یا پایان job
وضعیت اولیه: API وضعیت سفارش باید در چندصد میلیثانیه پاسخ دهد؛ گزارش کل سال ممکن است نیم ساعت طول بکشد.
اجرای مرحلهبهمرحله: API درخواست محدود را همان لحظه انجام میدهد؛ job دستهای ورودیِ کراندار را کامل میخواند و خروجی را منتشر میکند.
نتیجه و دلیل: معیار آنلاین بیشتر latency هر درخواست و معیار batch بیشتر زمان پایان و هزینهٔ کل پردازش است.
دو ستون دو جهاناند: آنلاین (بپرس، سریع جواب بگیر؛ معیار تأخیر پاسخ؛ داده زنده mutate میشود) و دستهای (ورودی immutable، خروجی derived؛ معیار throughput؛ خروجی هر بار از صفر). نوار پایین سه هدیه همان قرارداد را میگوید: سفر در زمان به خروجیهای قدیمی، تحمل خطای انسانی و بازتلاش امن taskها.
The two columns are two worlds: online (ask, get a fast answer; response-time metric; live data gets mutated) and batch (immutable input, derived output; throughput metric; output rebuilt from scratch). The bottom strip names the same contract's three gifts: time travel to older outputs, human fault tolerance and safe task retries.
۲2 پردازش دستهای با ابزارهای UnixBatch processing with Unix tools
ابزارهای Unix را میتوان به یک زنجیره تبدیل کرد: هر ابزار خروجیِ مشخصی میدهد و ابزار بعدی آن را میخواند. قراردادِ سادهٔ ورودی و خروجی، ترکیبپذیری را ممکن میکند.
مثال فنی: Pipe فایلِ ثابت
وضعیت اولیه: فایل لاگ شامل مسیرهای /a,/b,/a است.
اجرای مرحلهبهمرحله: استخراج مسیر، مرتبسازی و uniq -c خروجی /a:2,/b:1 میدهد؛ هر ابزار خروجی ابزار قبلی را ورودی میگیرد.
نتیجه و دلیل: اتصال ابزارهای ساده یک جریان پردازش میسازد؛ ترتیب ورودی برای بعضی ابزارها، از جمله uniq، جزئی از قرارداد است.
The best intuition for batch processing is what already works on one machine: a chain of Unix tools joined by pipes. Later we generalize the same example to distributed.
۲.۱. تحلیل لاگ با pipeline2.1. Log analysis with a pipeline
لاگ دسترسی، ردِ درخواستهای وب را نگه میدارد. برای گزارش باید ابتدا فیلدِ موردنظر را جدا کنیم و بعد آن را به شکل مناسبِ شمارش یا گروهبندی درآوریم.
مثال فنی: چرا sort قبل از uniq؟
وضعیت اولیه: مسیرهای ورودی /a,/b,/a هستند.
اجرای مرحلهبهمرحله: uniq -c فقط تکرارهای مجاور را میشمارد و بدون sort سه گروه میبیند؛ sort دو /a را کنار هم میآورد.
نتیجه و دلیل: مرتبسازی در این pipeline برای درستیِ شمارش لازم است، نه فقط برای زیبایی خروجی.
Suppose a web server appending one line to a log file per request. A standard NGINX access line looks like this:
216.58.210.78 - - [27/Jun/2025:17:55:11 +0000] "GET /css/typography.css HTTP/1.1"
200 3377 "https://example.com/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) ..."
(این در واقع یک خط است؛ برای خوانایی دو تکه شده.) قالبِ لاگ اینطور تعریف میشود:
(This is really one line; split for readability.) The log format is defined as:
$remote_addr - $remote_user [$time_local] "$request"
$status $body_bytes_sent "$http_referer" "$http_user_agent"
یعنی این یک خط میگوید: در تاریخِ مشخص، سرور درخواستِ فایلِ /css/typography.css را از IPِ
216.58.210.78 گرفته؛ کاربر احراز هویت نشده (پس $remote_user خط تیره است)؛
پاسخِ 200 بوده و 3,377 بایت حجم داشته. حالا بیا «پنج صفحهٔ پربازدید» را با ابزارهای پایه پیدا کنیم:
So this line says: at the given date the server got a request for /css/typography.css from IP 216.58.210.78; the user wasn't authenticated (so $remote_user is a dash); the response was 200 with 3,377 bytes. Now let's find the “top five pages” with basic tools:
cat /var/log/nginx/access.log |
awk '{print $7}' |
sort |
uniq -c |
sort -r -n |
head -n 5
cat /var/log/nginx/access.log — فایلِ لاگ را میخواند. (راستش cat اینجا لازم نیست؛
میشد فایل را مستقیم به awk داد. ولی خطِ لولهایِ خطی با این شکلِ نوشتن واضحتر میشود.)
awk '{print $7}' — هر خط را بر اساسِ فاصلهٔ خالی به فیلدها تقسیم میکند و فقط فیلدِ هفتم را چاپ میکند
که همان URL درخواستشده است؛ در خطِ نمونه یعنی /css/typography.css.
sort — فهرستِ URLها را الفبایی مرتب میکند. دلیلش این است: اگر URLای n بار
درخواست شده باشد، در خروجیِ مرتبشده، همان URL n بار پشتِ سرِ هم میآید.
uniq -c — خطوطِ تکراریِ مجاور را حذف میکند؛ گزینهٔ -c یک شمارنده هم اضافه میکند:
برای هر URLِ متمایز، تعدادِ دفعاتِ ظهورش را مینویسد.
sort -r -n — بارِ دوم مرتبسازی، اینبار بر اساسِ عددِ ابتدای خط (-n) که همان
تعدادِ درخواستهاست، و بهصورتِ معکوس (-r) تا بزرگترین اول بیاید.
head -n 5 — فقط پنج خطِ اول را نگه میدارد و بقیه را دور میریزد.
cat /var/log/nginx/access.log — reads the log file. (Honestly cat is unneeded here; the file could go to awk directly. But the linear-pipeline style reads clearer.)
awk '{print $7}' — splits each line on whitespace and prints only the seventh field, the requested URL; in the sample line, /css/typography.css.
sort — sorts the URL list alphabetically. Why: if a URL was requested n times, the sorted output holds that same URL n times in a row.
uniq -c — drops adjacent duplicate lines; the -c flag adds a counter: one line with its count per distinct URL.
sort -r -n — sorts a second time, now by the leading number (-n), the request count, in reverse (-r) so the biggest comes first.
head -n 5 — keeps only the first five lines and discards the rest.
خروجیِ این زنجیره چیزی شبیهِ این است:
That pipeline's output looks like this:
4189 /favicon.ico
3631 /2016/02/08/how-to-do-distributed-locking.html
2124 /2020/11/18/distributed-systems-and-elliptic-curves.html
1369 /
915 /css/typography.css
این خطِ فرمان شاید مبهم به نظر برسد، اما فوقالعاده قدرتمند است: گیگابایت لاگ را در چند ثانیه پردازش میکند
و تغییرش هم راحت است. اگر بخواهی فایلهای CSS از گزارش حذف شوند، آرگومانِ awk را
به $7 !~ /\.css$/ {print $7} تغییر میدهی؛ و اگر بهجای صفحات، پرترافیکترین IPهای کلاینت را بخواهی،
کافی است {print $1} بگذاری — چون فیلدِ اول همان IP است.
The command line may look cryptic, but it is remarkably powerful: it chews through gigabytes of logs in seconds and is easy to change. To drop CSS files from the report, change awk's argument to $7 !~ /\.css$/ {print $7}; and for busiest client IPs instead of pages, just put {print $1} — the first field is the IP.
روایت مدرس — نمودار ۱ را با صدای بلند بخوانیمTeacher narration
مثال فنی برای خواندنِ نمودار: چرا sort قبل از uniq؟
وضعیت اولیه: مسیرهای ورودی /a,/b,/a هستند.
اجرای مرحلهبهمرحله: uniq -c فقط تکرارهای مجاور را میشمارد و بدون sort سه گروه میبیند؛ sort دو /a را کنار هم میآورد.
نتیجه و دلیل: مرتبسازی در این pipeline برای درستیِ شمارش لازم است، نه فقط برای زیبایی خروجی.
این یک dataflow با پنج ستون است که بالایشان نامگذاری شده:
Source، Extract، External Sort،
Count و Rank & Report. از سمتِ راستِ نمودار (در RTL یعنی ابتدا)
شروع کن: جعبهی access.log با زیرنویسِ NGINX log lines —
همان فایلِ لاگی که در متن دیدی.
فلشِ اول «log lines» نام دارد و زیرش نوشته
«e.g. GET /css/typography.css 200»: یک خطِ نمونه از لاگ. این فلش به جعبهی
awk {print $7} با زیرنویسِ extract URL field میرسد —
دقیقاً همان گامِ «استخراجِ فیلدِ هفتم».
فلشِ دوم «URL stream» است و به sort میرود
(زیرنویس: group duplicate URLs) — اینجا یادداشتِ مهمی روی فلشِ بعدی است:
«sorted URLs» با توضیحِ «spills to disk, merges sorted segments»؛
یعنی همان چیزی که در بخشِ بعدی دربارهاش حرف میزنیم: sort به دیسک سرازیر میشود و segmentهای مرتب را ادغام میکند.
بعد به uniq -c میرسیم (زیرنویس: count per URL)، فلشِ
«counted pairs» به sort -r -n
(زیرنویس: rank by request count)، و در آخر فلشِ «ranked pairs»
که زیرش نوشته «top 5 kept, rest discarded» و از پایینِ نمودار به head -n 5
میرسد (زیرنویس: top-5 report). یعنی مسیرِ داده دقیقاً همان خطِ فرمانِ ششتایی است.
سه کارتِ پایینِ نمودار را هم مرور کن. کارتِ اول «Pipeline, Step by Step» همان توضیحِ
خطبهخط است: هر خطِ لاگ یک درخواستِ سروشده است، awk '{print $7}' فیلدِ هفتم را برمیدارد،
sort URLهای تکراری را کنارِ هم میآورد، uniq -c هر اجرا را به یک خط با شمارنده تبدیل میکند،
و sort -r -n و head -n 5 رتبهبندی و گزینش میکنند.
کارتِ دوم «Why Sorting Scales Past RAM» درسِ مهندسی را میگوید:
GNU sort به دیسک spill میکند، با mergesortِ segmentهای مرتب را ادغام میکند،
پس به جدولِ hashی که همهٔ URLها در حافظه بگندد نیازی نیست؛ گلوگاه نرخِ خواندنِ ورودی از دیسک میشود؛
و البته ابزارهای Unix روی یک ماشین میچرخند — datasetهای بزرگتر از یک دیسک به پردازشِ دستهایِ توزیعشده نیاز دارند.
و کارتِ سوم «Example Output — Top 5 Requested URLs» همان پنج خطِ خروجی است
(4189 /favicon.ico تا 915 /css/typography.css) —
همان چیزی که در متن دیدی.
درسِ این نمودار: هر فلش یک «جریانِ رکورد» است نه یک «درخواست/پاسخ»؛ و مرتبسازی همان کاری را میکند که بعداً
در مقیاسِ توزیعشده به آن shuffle میگوییم: کنارِ هم آوردنِ رکوردهای همکلید.
This dataflow has five labelled columns: Source, Extract, External Sort, Count and Rank & Report. Start at the access.log box (“NGINX log lines”) — the same log file from the text. The first arrow, “log lines” (“e.g. GET /css/typography.css 200”), reaches awk {print $7} (“extract URL field”) — exactly the seventh-field extraction step.
The “URL stream” arrow runs to sort (“group duplicate URLs”), and the next arrow's note matters: “sorted URLs” (“spills to disk, merges sorted segments”) — the same disk-spilling sort discussed in the next section. Then uniq -c (“count per URL”), the “counted pairs” arrow to sort -r -n (“rank by request count”), and finally “ranked pairs” (“top 5 kept, rest discarded”) down to head -n 5 (“top-5 report”) — the data path is exactly the six-command line.
The three bottom cards complete the lesson. “Pipeline, Step by Step” narrates line by line: each log line is one served request, awk '{print $7}' takes the seventh field, sort brings duplicate URLs together, uniq -c turns each run into one counted line, and sort -r -n plus head -n 5 rank and select. “Why Sorting Scales Past RAM” gives the engineering lesson: GNU sort spills to disk and merges sorted segments, so no hash table holding every URL in memory is needed; the bottleneck becomes the input file's disk read rate; and Unix tools run on one machine — datasets bigger than one disk need distributed batch processing. “Example Output — Top 5 Requested URLs” shows the same five output lines as the text (4189 /favicon.ico down to 915 /css/typography.css). The diagram's moral: each arrow is a “record stream”, not a “request/response”; and sorting does what we later call shuffle at distributed scale: bringing same-key records together.
۲.۲. sort در برابر تجمیع در حافظه2.2. Sort vs in-memory aggregation
شمارش با جدول هش معمولاً به حافظهٔ متناسب با تعداد کلیدهای متفاوت نیاز دارد. مرتبسازیِ بیرونی میتواند از دیسک کمک بگیرد و وقتی داده در حافظه جا نمیشود هم ادامه دهد.
مثال فنی: تجمیع بیرون از RAM
وضعیت اولیه: ورودی یک میلیارد کلید متفاوت دارد و جدول hashِ شمارندهها در RAM جا نمیشود.
اجرای مرحلهبهمرحله: external sort تکههای جاشونده در حافظه را مرتب و روی دیسک مینویسد، سپس ادغام میکند.
نتیجه و دلیل: مرتبسازی میتواند با دیسک و حافظهٔ محدود ادامه دهد؛ hash aggregation هم ممکن است spill داشته باشد، پس برتری مطلق مطرح نیست.
The same job could be done with a simple program instead of the Unix chain. E.g. in Python:
from collections import defaultdict
counts = defaultdict(int)
with open('/var/log/nginx/access.log', 'r') as file:
for line in file:
url = line.split()[6]
counts[url] += 1
top5 = sorted(((count, url) for url, count in counts.items()),
reverse=True)[:5]
for count, url in top5:
print(f"{count} {url}")
counts = defaultdict(int) — یک جدولِ hash که برای هر URL یک شمارنده نگه میدارد؛ مقدارِ اولیهٔ هر شمارنده صفر است.
line.split()[6] — URL درخواستشده را از هر خط برمیدارد؛ فیلدِ هفتمِ جداشده با فاصله
(شاخصِ ۶ چون آرایههای پایتون از صفر شمارهگذاری میشوند).
counts[url] += 1 — شمارندهٔ URLِ همان خط را یکی زیاد میکند.
sorted(... reverse=True)[:5] — محتوای جدولِ hash را بر اساسِ شمارنده (نزولی) مرتب و پنج موردِ اول را برمیدارد.
print(...) — همان پنج مورد را چاپ میکند.
counts = defaultdict(int) — a hash table keeping one counter per URL; each counter starts at zero.
line.split()[6] — takes the requested URL from each line; the seventh whitespace-separated field (index 6, since Python arrays count from zero).
counts[url] += 1 — increments that line's URL counter.
sorted(... reverse=True)[:5] — sorts the hash table's contents by counter (descending) and takes the top five.
print(...) — prints those five.
این برنامه به فشردگیِ زنجیرهٔ Unix نیست ولی خواناست و انتخاب بینشان تا حدی سلیقهای است.
ولی یک تفاوتِ بزرگ در جریانِ اجرا هست که روی فایلِ بزرگ خودش را نشان میدهد:
This program is less compact than the Unix chain but readable, and the choice between them is partly taste. But one big execution-flow difference shows on large files:
-
تجمیع در حافظه (اسکریپتِ پایتون): جدولِ hashِ URLها در حافظه میماند. کارِ حافظهایِ job
(یعنی مقدارِ حافظهای که job به دسترسیِ تصادفی نیاز دارد — به آن working set میگویند)
فقط به تعدادِ URLهای متمایز بستگی دارد: اگر یک URL یک میلیون بار آمده باشد، باز هم فقط یک URL + یک شمارنده جا میگیرد.
اگر این working set کوچک باشد (مثلاً برای اکثرِ وبسایتها در ۱ گیگابایت جا میشود)، همین روش حتی روی لپتاپ عالی است.
-
مرتبسازی (pipeline): اگر working set از حافظه بزرگتر شود، رویکردِ مرتبسازی برنده است چون دیسک را کارآمد
استفاده میکند — دقیقاً همان اصلِ «ذخیرهسازیِ log-structured» (فصل ۴): تکههای داده در حافظه مرتب و بهصورتِ
segment file روی دیسک نوشته میشوند، بعد چند segmentِ مرتبِ کوچک در یک فایلِ مرتبِ بزرگتر
ادغام میشوند. mergesort الگوی دسترسیِ ترتیبی دارد که روی دیسک (و SSD) بسیار بهتر از
دسترسیِ تصادفی عمل میکند.
-
و نکتهٔ عملی: دستورِ
sort در GNU Coreutils خودش datasetهای بزرگتر از حافظه را
با spill به دیسک مدیریت میکند و مرتبسازی را روی چند هستهٔ CPU موازی میکند. یعنی همان خطِ فرمانِ ساده
بدونِ تمامشدنِ حافظه به datasetهای بزرگ مقیاس میخورد؛ گلوگاه احتمالاً نرخِ خواندنِ فایلِ ورودی از دیسک است.
-
In-memory aggregation (the Python script): the URL hash table lives in memory. The job's memory footprint (the memory needing random access — called the working set) depends only on the distinct URL count: a URL seen a million times still takes one URL + one counter. When this working set is small (e.g. most websites fit in 1 GB), this method is excellent even on a laptop.
-
Sorting (the pipeline): when the working set outgrows memory, sorting wins because it uses disk efficiently — exactly the “log-structured storage” principle (Chapter 4): data chunks are sorted in memory and written as segment files, then several small sorted segments merge into a bigger sorted file. Mergesort's sequential access pattern performs far better on disk (and SSD) than random access.
-
And the practical point: GNU Coreutils
sort itself handles bigger-than-memory datasets by spilling to disk and parallelizes sorting across CPU cores. So the same simple command line scales to large datasets without running out of memory; the bottleneck is likely the input file's read rate.
۲.۳. محدودیتِ تکماشین و گذار به توزیعشده2.3. The single-machine limit and the move to distributed
وقتی زمان اجرا، ظرفیت دیسک یا توانِ یک ماشین کافی نیست، میتوان داده و کار را تقسیم کرد. توزیع، هزینهٔ شبکه و هماهنگی را هم اضافه میکند و همیشه قدم اول نیست.
مثال فنی: ورودی بزرگتر از یک ماشین
وضعیت اولیه: 10 TB لاگ داریم و یک ماشین روزانه فقط 1 TB را در زمان مجاز پردازش میکند.
اجرای مرحلهبهمرحله: فایلها به taskهای مستقل تقسیم میشوند؛ چند worker بخشها را میخوانند و خروجی تجمیعی در مرحلهٔ بعد ادغام میشود.
نتیجه و دلیل: توزیع ظرفیت را زیاد میکند، اما شبکه، skew و هماهنگیِ taskها بخشی از زمان پایان میشوند.
Unix tools' limit is running on one machine. Datasets that fit neither in memory nor on local disk are the problem — and that is where distributed batch frameworks enter. The same Unix example had three cooperating parts: storage devices reachable through the OS filesystem interface; a scheduler deciding when each process runs and how much CPU it gets; and a set of Unix programs whose stdin and stdout are piped together. The same three parts exist in distributed frameworks — the key analogy of the next section.
۳3 سیستمعاملِ توزیعشدهThe distributed operating system
۳.۱. قیاس با سیستمعامل و سه لایه3.1. The OS analogy and the three layers
چارچوبِ دستهای مانند یک سیستمعامل، محل داده، سهم منابع و اجرای کارها را مدیریت میکند؛ با این تفاوت که این مسئولیتها میان چند ماشین پخش شدهاند.
مثال فنی: سه مسئولیتِ چارچوب
وضعیت اولیه: job به خواندن فایل، اجرای ۱۰۰ task و انتقال دادهٔ بین مرحلهها نیاز دارد.
اجرای مرحلهبهمرحله: لایهٔ ذخیره فایل را نگه میدارد؛ scheduler منابع میدهد؛ موتور اجرا task، retry و shuffle را مدیریت میکند.
نتیجه و دلیل: قیاس با سیستمعامل یعنی برنامه همهٔ جزئیات ماشینها را خودش اداره نکند؛ مرز هر لایه هنوز باید روشن باشد.
As seen at the previous section's end, a distributed batch framework can be viewed as a distributed operating system: it has a filesystem, a job scheduler, and programs passing data through the filesystem or other channels. Three main layers:
- ذخیرهسازی (Storage): بلاکها، متادیتا و یک پروتکلِ دسترسیِ قابلِ تعویض — نقشِ فایلسیستم.
- هماهنگسازی (Orchestration): اینکه چه چیزی کجا و با چقدر حافظه و CPU اجرا شود — نقشِ kernel.
- محاسبه (Compute): taskهای منزوی که یک shard ورودی میخوانند و یک shard خروجی مینویسند — نقشِ پروسهها.
- Storage: blocks, metadata and a swappable access protocol — the filesystem role.
- Orchestration: what runs where with how much memory and CPU — the kernel role.
- Compute: isolated tasks each reading one input shard and writing one output shard — the processes role.
روایت مدرس — نمودار ۲ را با صدای بلند بخوانیمTeacher narration
مثال فنی برای خواندنِ نمودار: سه مسئولیتِ چارچوب
وضعیت اولیه: job به خواندن فایل، اجرای ۱۰۰ task و انتقال دادهٔ بین مرحلهها نیاز دارد.
اجرای مرحلهبهمرحله: لایهٔ ذخیره فایل را نگه میدارد؛ scheduler منابع میدهد؛ موتور اجرا task، retry و shuffle را مدیریت میکند.
نتیجه و دلیل: قیاس با سیستمعامل یعنی برنامه همهٔ جزئیات ماشینها را خودش اداره نکند؛ مرز هر لایه هنوز باید روشن باشد.
نمودارِ معماری سه ناحیهی رنگی دارد که بالایشان نوشته: Orchestration layer،
Compute layer و Storage layer — دقیقاً همان سه لایهی متن.
از بیرونِ خوشه شروع کن: جعبهی Job client (زیرنویس: submits the workflow
با تگِ outside the cluster). فلشِ «submit workflow (DAG of jobs)»
آن را به Workflow scheduler (زیرنویس: DAG of jobs، تگِ
Airflow-style) در لایهٔ هماهنگسازی میرساند.
از workflow scheduler فلشِ «submit job (N tasks, resources, code)» به
Scheduler میرود (زیرنویس: decides which task runs where) و
از scheduler یک فلشِ خطچینِ «query cluster state» به
Resource manager میرود (زیرنویس: global cluster state،
تگِ ZooKeeper / etcd) — یعنی scheduler تصمیمش را بر اساسِ نمایِ سراسریِ خوشه میگیرد.
بعد فلشِ «assign task, prefer data locality» از scheduler به لایهٔ محاسبه میرود:
Executor on each node (زیرنویس: runs tasks, cgroups isolation،
تگِ on every node) و از آن فلشِ «runs each task» به
Batch task با زیرنویسِ input shard → output shard.
یک فلشِ خطچینِ برگشتی هم هست: «heartbeats + task status» از executor به resource manager —
همان heartbeat که زندهبودن را خبر میدهد.
حالا لایهٔ پایین: از Batch task سه فلش به لایهٔ ذخیرهسازی میرود:
«read input shard / write output shard» به Data nodes
(زیرنویس: blocks 128MB / 4MB، تگِ replicated or erasure-coded)؛
خطِ خطچینِ «resolve block locations» به Metadata service
(زیرنویس: file locations, permissions، تگِ NameNode)؛
و «read input / write output through pluggable protocol (S3 API)» به
Object store (زیرنویس: bucket + key, immutable، تگِ
S3-style API). همین سه مسیر میگوید: هم فایلسیستمِ توزیعشده و هم object store
پشتِ یک پروتکلِ قابلِ تعویض قرار میگیرند.
کارتها را هم بخوان تا درس جا بیفتد: کارتِ «The distributed operating system analogy»
قیاس را جمع میکند (Storage = فایلسیستم، Orchestration = kernel، Compute = پروسههای منزوی)؛
کارتِ «Storage layer» میگوید بلاکها خیلی بزرگترند
(128MB در HDFS یا 4MB در JuiceFS و بسیاری از object storeها،
در برابرِ 4,096 bytes در ext4) و replicates یا erasure coding
(Reed–Solomon) دارند و پروتکلِ دسترسی pluggable است؛ کارتِ «Data locality»
میگوید task میتواند روی هر نودی که replicaِ ورودی را دارد اجرا شود و object storeها ذخیره و محاسبه را جدا نگه میدارند؛
و کارتِ «Orchestration layer» فهرستِ متادیتای درخواستِ اجرا را مینویسد
(تعدادِ taskها، منابعِ هر task، job id، credentialها، پارامترها، سختافزارِ لازم و جای کد).
The architecture diagram has three colored zones: Orchestration layer, Compute layer and Storage layer — exactly the text's three layers. Start outside the cluster: the Job client box (“submits the workflow”, tagged “outside the cluster”). Its “submit workflow (DAG of jobs)” arrow reaches the Workflow scheduler (“DAG of jobs”, “Airflow-style”) in the orchestration layer, whose “submit job (N tasks, resources, code)” arrow runs to the Scheduler (“decides which task runs where”), with a dashed “query cluster state” arrow to the Resource manager (“global cluster state”, “ZooKeeper / etcd”) — the scheduler decides from the cluster's global view.
Then “assign task, prefer data locality” runs from the scheduler to the compute layer: an Executor on each node (“runs tasks, cgroups isolation”, “on every node”), and “runs each task” to a Batch task (“input shard → output shard”), with a dashed return arrow — “heartbeats + task status” from executor to resource manager. Three arrows run from the batch task to storage: “read input shard / write output shard” to Data nodes (“blocks 128MB / 4MB”, “replicated or erasure-coded”); the dashed “resolve block locations” to the Metadata service (“file locations, permissions”, “NameNode”); and “read input / write output through pluggable protocol (S3 API)” to the Object store (“bucket + key, immutable”, “S3-style API”) — both the distributed filesystem and object stores sit behind one swappable protocol. The cards seal the lesson: the OS analogy (storage = filesystem, orchestration = kernel, compute = isolated processes), the storage layer (huge blocks — 128MB in HDFS, 4MB in JuiceFS and many object stores, vs 4,096 bytes in ext4 — with replication or Reed–Solomon erasure coding and a pluggable access protocol), data locality (a task may run on any node holding an input replica; object stores keep storage and compute apart), and the orchestration layer (the run request's metadata: task count, per-task resources, job id, credentials, parameters, hardware needs and code location).
۳.۲. فایلسیستمهای توزیعشده3.2. Distributed filesystems
فایلسیستم توزیعشده فایل بزرگ را به بخشهایی روی چند ماشین تقسیم میکند و معمولاً نسخههای اضافی نگه میدارد. برنامه باید بتواند فایل را بخواند، بدون اینکه محلِ تکتک بخشها را مدیریت کند.
مثال فنی: بلاک بزرگِ فایل توزیعشده
وضعیت اولیه: فایل 1 GB با بلاک فرضی 128 MB به هشت بخش تقسیم شده است.
اجرای مرحلهبهمرحله: metadata محل کپی هر بلاک را میدهد و worker از یک کپی سالم میخواند؛ اندازهٔ بلاک روی تعداد metadata و موازیسازی اثر دارد.
نتیجه و دلیل: بلاکِ درشت برای اسکن ترتیبی و کاهش سربار مناسب است؛ با page کوچکِ موتور پایگاه داده هدف یکسانی ندارد.
Your own OS filesystem has layers: at the bottom the block device driver talks to the disk directly, enabling raw block reads/writes; above it the page cache keeps recently-touched blocks in memory; then the filesystem layer (like ext4 or XFS) breaks big files into blocks and tracks metadata like inodes and directories; and finally the OS exposes filesystems to programs through one shared API called VFS.
فایلسیستمهای توزیعشده (DFS) هم همینطور کار میکنند: فایلها به بلاک شکسته و بینِ ماشینها پخش میشوند؛
ولی بلاکهای DFS معمولاً خیلی بزرگترند: HDFS پیشفرضِ 128MB دارد،
JuiceFS و بسیاری از object storeها 4MB — در برابرِ 4,096 بایتِ ext4.
بلاکِ بزرگتر یعنی متادیتای کمتر (که روی datasetهای petabyteی فرقِ بزرگی میکند) و هزینهٔ seek نسبت به خواندنِ کمتر.
یک تفاوتِ ظریف: دستگاههای فیزیکی نمیتوانند «بخشی از بلاک» بنویسند و سیستمعامل مجبور به نوشتنِ کلِ بلاک است،
ولی چون DFS روی فایلسیستمِ سیستمعاملِ نودها پیادهسازی میشود این الزام را ندارد:
فایلِ 900MB با بلاکِ 128MB یعنی هفت بلاکِ 128MB و یک بلاکِ 4MB.
Distributed filesystems (DFS) work the same way: files break into blocks spread across machines; but DFS blocks are usually far bigger: HDFS defaults to 128MB, JuiceFS and many object stores to 4MB — vs ext4's 4,096 bytes. Bigger blocks mean less metadata (a big deal on petabyte datasets) and cheaper seeks relative to reads. One subtle difference: physical devices cannot write “part of a block” so the OS must write whole blocks, but since a DFS is implemented on the nodes' OS filesystems it has no such requirement: a 900MB file with 128MB blocks means seven 128MB blocks and one 4MB block.
بلاکهای DFS با درخواستِ شبکهای به ماشینی که بلاک را نگه داشته خوانده میشوند. روی هر ماشین یک daemon مینشیند که
API برای خواندن/نوشتنِ بلاکها بهصورتِ فایل روی فایلسیستمِ محلیاش میدهد: Hadoop به آنها
DataNode میگوید و GlusterFS فرآیندهای glusterfsd.
معادلِ توزیعشدهٔ page cache هم هست: چون بلاکها بهصورتِ فایل روی data nodeها هستند، هر خواندن از
page cacheِ سیستمعاملِ همان نود میگذرد؛ بعضی DFSها لایههای کشِ بیشتر هم دارند (مثل کشِ سمتِ کلاینت و دیسکِ محلی در JuiceFS).
DFS blocks are read with network requests to the machine holding the block. Each machine runs a daemon offering a block read/write API as files on its local filesystem: Hadoop calls them DataNodes, GlusterFS calls them glusterfsd processes. The distributed equivalent of the page cache exists too: since blocks sit as files on data nodes, every read passes through that node's OS page cache; some DFSs add more cache layers (like client-side caches and local disk in JuiceFS).
متادیتا هم مسئلهٔ خودش را دارد: ext4/XFS فضای خالی، جای بلاکهای فایل، ساختارِ دایرکتوری و مجوزها را نگه میدارند؛
DFS هم به راهی برای پیگیریِ جای فایلها در ماشینهای مختلف نیاز دارد. Hadoop سرویسی به نام
NameNode دارد؛ 3FS (پروژهٔ DeepSeek) سرویسِ متادیتایی دارد
که دادهاش را در یک KV store مثل FoundationDB پایدار میکند. و روی فایلسیستم،
نقشِ VFS را پروتکلِ DFS بازی میکند: هر DFSای که آن پروتکل را پیاده کند قابلِ استفاده است.
APIِ S3 دقیقاً به همین شکل فراگیر شده: MinIO، Cloudflare R2، Tigris، Backblaze B2 و بسیاری دیگر
آن را پیادهسازی میکنند. بعضی DFSها کاملاً POSIX-سازگارند و با FUSE یا
NFS به VFS میچسبند — NFS معروفترین پروتکلِ فایلسیستمِ توزیعشده است.
Metadata has its own problem: ext4/XFS track free space, file-block placement, directory structure and permissions; a DFS likewise needs a way to track file placement across machines. Hadoop has a service called NameNode; 3FS (DeepSeek's project) has a metadata service persisting its data in a KV store like FoundationDB. And the VFS role is played by the DFS protocol: any DFS implementing that protocol is usable. The S3 API became ubiquitous exactly this way: MinIO, Cloudflare R2, Tigris, Backblaze B2 and many more implement it. Some DFSs are fully POSIX-compatible and attach to VFS via FUSE or NFS — NFS being the most famous distributed-filesystem protocol.
| بُعد | فایلسیستمِ محلی (ext4/XFS) | فایلسیستمِ توزیعشده (HDFS و…) | object store (S3 و…) |
| واحدِ داده | فایل، به بلاکِ ۴KB شکسته | فایل، به بلاکِ بزرگ (128MB / 4MB) | object با کلیدِ یکتا؛ immutable بعد از نوشتن |
| متادیتا | inode و دایرکتوری، در همان دیسک | سرویسِ متادیتای متمرکز (مثل NameNode) یا KV store | بخشی از key؛ «پیشوند» بهجای دایرکتوری |
| کش | page cache سیستمعامل | page cache هر data node + کشِ سمت کلاینت | معمولاً کشِ محلی ندارد؛ S3 Express تأخیرِ تکمیلیثانیهای |
| مقاومت در برابر خرابی | RAID روی همان ماشین | replication بینِ ماشینها یا erasure coding (Reed–Solomon) | داخلِ سرویس تضمین میشود (چندمنطقهای) |
| عملیاتِ اتمی | rename اتمی، link، قفل | اغلب rename اتمی، link و قفل دارد | rename غیراتمیک (کپی+حذف)، بدونِ link و قفل |
| locality | نودِ محاسبه = نودِ ذخیرهسازی | اجرا نزدیکِ داده (task روی نودِ حاویِ replica) | جداییِ compute و storage؛ مقیاسِ مستقل |
| Dimension | Local filesystem (ext4/XFS) | Distributed filesystem (HDFS etc.) | Object store (S3 etc.) |
| Data unit | Files, broken into 4KB blocks | Files, broken into big blocks (128MB / 4MB) | Uniquely-keyed objects; immutable after writing |
| Metadata | Inodes and directories, on the same disk | Centralized metadata service (like NameNode) or a KV store | Part of the key; “prefixes” instead of directories |
| Cache | OS page cache | Each data node's page cache + client-side caches | Usually no local cache; S3 Express with single-digit-ms latency |
| Failure resistance | RAID on the same machine | Cross-machine replication or erasure coding (Reed–Solomon) | Guaranteed inside the service (multi-region) |
| Atomic ops | Atomic rename, links, locks | Often atomic rename, links and locks | Non-atomic rename (copy+delete), no links or locks |
| Locality | Compute node = storage node | Run near the data (tasks on replica-holding nodes) | Compute/storage separation; independent scaling |
در نهایت یک بُعدِ معماریِ مهم: DFSها روی اصلِ shared-nothing ساخته شدهاند —
در برابرِ رویکردِ shared-disk در NAS و SAN
که یک applianceِ متمرکز (گاهی با سختافزارِ خاص و Fibre Channel) هستند.
shared-nothing فقط کامپیوترِ معمولی + شبکهٔ دیتاسنترِ عادی میخواهد. سختافزارِ commodity ارزانتر است
ولی نرخِ خرابیِ بیشتری دارد؛ پس بلاکها روی چند ماشین replicate میشوند (مثل فصل ۶) یا با
erasure coding مثل Reed–Solomon محافظت میشوند که هزینهٔ ذخیرهسازیِ کمتری از کپیِ کامل دارد —
همان ایدهی RAID، ولی روی شبکهٔ دیتاسنتر و بدونِ سختافزارِ خاص.
One final architectural dimension: DFSs are built on the shared-nothing principle — vs the shared-disk approach of NAS and SAN, centralized appliances (sometimes with special hardware and Fibre Channel). Shared-nothing wants only commodity computers + an ordinary datacenter network. Commodity hardware is cheaper but fails more often; so blocks are replicated across machines (like Chapter 6) or protected with erasure coding like Reed–Solomon, cheaper in storage than full copies — the RAID idea, but over the datacenter network and without special hardware.
۳.۳. object store3.3. Object stores
مخزن شیء، داده را معمولاً بهشکل اشیای نامدار با API ذخیره میکند. رفتارِ آن الزاماً مثل فایلسیستم محلی نیست؛ برای مثال، تغییر یک بخش از فایل ممکن است به شکل دیگری انجام شود.
مثال فنی: Object یک فایل POSIX کامل نیست
وضعیت اولیه: خروجی job با کلید reports/run-42/part-1 در object store نوشته میشود.
اجرای مرحلهبهمرحله: برای انتشارِ نسخهٔ کامل، manifest یا اشارهگر نسخهٔ معتبر عوض میشود؛ نباید rename اتمیکِ دایرکتوری را بدون پشتیبانی فرض کرد.
نتیجه و دلیل: API و تضمین عملیات object store باید جدا بررسی شود؛ شبیه بودن نام «فایل» رفتار یکسان نمیسازد.
Object storage services like Amazon S3, Google Cloud Storage, Azure Blob Storage and OpenStack Swift became the popular DFS replacement for batch jobs. The boundary between the two is blurring: FUSE drivers let you see an object store as a filesystem, and some DFS implementations like JuiceFS and Ceph offer both APIs (object and filesystem). But their APIs, performance and consistency guarantees differ a lot — adopt carefully and verify they really behave as you expect.
-
ساختارِ URL: هر object آدرسی مثل
s3://my-photo-bucket/2025/04/01/birthday.png دارد. بخشِ host
(my-photo-bucket) نامِ bucket است (در سطحِ جهانی یکتا) و بقیه، key شیء است
که درونِ bucket باید یکتا باشد.
-
get/put و immutability: خواندن با
get و نوشتن با put است. برخلافِ فایلها،
object بعد از نوشتن immutable است: برای تغییر باید کلش را دوباره بنویسی — مثلِ key-value store.
(Azure Blob و S3 Express One Zone append دارند، بقیه معمولاً نه.) هیچ file handleای مثل
fopen/fseek در کار نیست.
-
پیشوند بهجای دایرکتوری: مسیرها شبیهِ دایرکتوری به نظر میرسند ولی object store اصلاً مفهومِ دایرکتوری ندارد؛
اسلشها بخشی از key هستند. فهرستگیری با یک پیشوند انجام میشود و دو تفاوت با
ls دارد:
مثلِ ls -R بازگشتی است (همهٔ objectهای زیرمجموعه را میآورد) و
دایرکتوریِ خالی ممکن نیست — اگر همهٔ objectهای زیرِ یک مسیر را حذف کنی، دیگر آن مسیر در list ظاهر نمیشود؛
رسمِ رایج ساختنِ یک objectِ صفربایتی بهعنوانِ نمایندهٔ دایرکتوریِ خالی است.
-
نبودِ link و قفل، rename غیراتمیک: DFSها اغلب hard link، symbolic link، قفلِ فایل و renameِ اتمی دارند؛
object storeها ندارند. rename یعنی کپی به key جدید و بعد حذفِ کلیدِ قدیمی؛ و برای تغییرِ نامِ یک «دایرکتوری»
باید تکتک objectها را تغییر نام دهی چون نامِ دایرکتوری بخشی از key است.
-
تفاوت با KV store (فصل ۴): KV storeها برای مقدارهای کوچک (کیلوبایت) و خواندن/نوشتنِ مکررِ کمتأخیر بهینهاند؛
DFS و object store برای objectهای بزرگ (مگابایت تا گیگابایت) و خواندنهای بزرگتر و کمدفعهتر.
البته object storeها دارند این مرز را جابهجا میکنند (مثلاً S3 Express One Zone با تأخیرِ تکمیلیثانیهای).
-
locality: DFS مثل HDFS اجازه میدهد task روی ماشینی اجرا شود که نسخهای از فایل را دارد — پس فایل از شبکه نمیگذرد
و اگر کدِ task از دادهای که میخواند کوچکتر باشد، پهنای باند صرفهجویی میشود. object storeها معمولاً
ذخیرهسازی و محاسبه را جدا نگه میدارند: پهنای باندِ بیشتری مصرف میشود (که با شبکههای سریعِ امروزی قابلِ قبول است)،
ولی CPU و حافظه مستقل از ذخیرهسازی مقیاس میگیرند.
-
URL structure: each object has an address like
s3://my-photo-bucket/2025/04/01/birthday.png. The host part
(my-photo-bucket) is the bucket name (globally unique); the rest is the object's key,
unique within the bucket.
-
Get/put and immutability: reading is
get, writing is put. Unlike files,
an object is immutable after writing: changing it means rewriting it whole — like a key-value store.
(Azure Blob and S3 Express One Zone have appends; most others don't.) No file handles like
fopen/fseek exist.
-
Prefixes instead of directories: paths look directory-like but object stores have no directory concept;
slashes are part of the key. Listing happens by prefix and differs from
ls in two ways:
it is recursive like ls -R (returning all objects below), and
empty directories are impossible — deleting every object under a path makes the path vanish from listings;
the common convention is creating a zero-byte object as the empty-directory stand-in.
-
No links or locks; non-atomic rename: DFSs often have hard links, symbolic links, file locks and atomic rename;
object stores don't. Rename means copy-to-new-key then delete-old-key; and renaming a “directory”
means renaming every object one by one, since the directory name is part of the key.
-
Difference from KV stores (Chapter 4): KV stores optimize for small values (kilobytes) and frequent low-latency reads/writes;
DFS and object stores for big objects (megabytes to gigabytes) with bigger, rarer reads.
Object stores are shifting this boundary though (e.g. S3 Express One Zone with single-digit-ms latency).
-
Locality: a DFS like HDFS lets a task run on a machine holding a copy of the file — so the file never crosses the network,
and if the task code is smaller than the data it reads, bandwidth is saved. Object stores usually
keep storage and compute apart: more bandwidth is consumed (acceptable on today's fast networks),
but CPU and memory scale independently of storage.
۳.۴. هماهنگسازیِ job3.4. Job orchestration
هماهنگکنندهٔ کار مشخص میکند هر وظیفه کجا و چه زمانی اجرا شود، نتیجهاش کجاست و پس از شکست چه کاری لازم است. این با خودِ منطقِ جمعزدنِ فروش فرق دارد.
مثال فنی: Task دوباره زمانبندی میشود
وضعیت اولیه: از ۱۰۰ task، worker اجرای task ۳۷ خاموش میشود.
اجرای مرحلهبهمرحله: scheduler task ۳۷ را روی worker دیگری اجرا میکند و خروجیِ attempt موفق را میپذیرد.
نتیجه و دلیل: تمام job لازم نیست از صفر تکرار شود، اگر ورودی و وابستگیهای لازم برای همین task در دسترس باشند.
The same OS analogy holds in orchestration: running a Unix job means someone must actually launch the awk, sort and uniq processes, carry data from one's output to the next one's input, allocate memory and guarantee fair execution. On one machine that is the kernel's job; in a distributed setting it is the job orchestrator's. A job-run request carries metadata: task count, each task's memory/CPU/disk, the job id, access credentials, parameters (like input and output), needed hardware (like GPUs) and the runnable code's location.
-
task executor: یک daemon مثل NodeManager در YARN یا
kubelet در Kubernetes روی هر نود مینشیند: taskها را اجرا میکند،
heartbeat میفرستد تا زندهبودنش معلوم شود، و وضعیتِ task و تخصیصِ منابعِ نود را پیگیری میکند.
وقتی درخواستِ شروعِ task میرسد، کدِ job را میگیرد و فرمانِ اجرا را میزند. بسیاری از executorها با سیستمعامل
جداسازیِ امنیتی و عملکردی هم میدهند (مثلاً cgroups در YARN و Kubernetes)
تا taskی نتواند بدونِ مجوز به داده برسد یا با مصرفِ بیرویه، taskهای دیگرِ نود را کند کند.
-
resource manager: متادیتای هر نود را نگه میدارد: سختافزارِ در دسترس (CPU، GPU، حافظه، دیسک)، وضعیتِ taskها،
موقعیتِ شبکهای و وضعیتِ نود — یعنی نمایِ سراسری از وضعیتِ فعلیِ خوشه. متمرکزبودنش هم میتواند
گلوگاهِ مقیاسپذیری و در دسترسپذیری شود؛ YARN وضعیت را در ZooKeeper و Kubernetes در
etcd مینویسد.
-
scheduler: معمولاً یک زیرسیستمِ متمرکز که درخواستِ شروع/توقف/وضعیتِ job را میگیرد — مثلاً
«یک job با ۱۰ task و فلان imageِ Docker روی نودهایی با فلان نوعِ GPU». scheduler با ترکیبِ درخواست و وضعیتِ
resource manager تصمیم میگیرد هر task کجا اجرا شود و executorها را خبر میکند. بعضی تصمیمها به
schedulerهای اختصاصیِ برنامه نیاز دارند (مثلاً scale کردنِ replicaهای خواندن وقتی تعدادِ کوئری از آستانه گذشت)؛
schedulerِ متمرکز و اینها با هم کار میکنند — YARN به آنها ApplicationMaster و
Kubernetes به آنها operator میگوید.
-
Task executor: a daemon like YARN's NodeManager or
Kubernetes' kubelet sits on each node: it runs tasks,
sends heartbeats to prove liveness, and tracks task state and the node's resource allocation.
When a task-start request arrives it fetches the job code and fires the run command. Many executors also give OS-level
security and performance isolation (e.g. cgroups in YARN and Kubernetes)
so no task can reach data unauthorized or slow its node's other tasks by overconsuming.
-
Resource manager: keeps each node's metadata: available hardware (CPU, GPU, memory, disk), task states,
network position and node health — a global view of the cluster's current state. Its centrality can
become a scalability and availability bottleneck; YARN writes state to ZooKeeper and Kubernetes to
etcd.
-
Scheduler: usually a centralized subsystem taking job start/stop/state requests — e.g.
“a job with 10 tasks and such Docker image on nodes with such GPU type”. The scheduler combines the request with the
resource manager's state to decide where each task runs and notifies the executors. Some decisions need
application-specific schedulers (e.g. scaling read replicas when query counts pass a threshold);
the centralized scheduler and these work together — YARN calls them ApplicationMasters,
Kubernetes calls them operators.
۳.۵. تخصیص منابع: انصاف در برابر کارایی3.5. Resource allocation: fairness vs efficiency
زمانبند میان کارها منابع تقسیم میکند. انصاف، زمانِ پایانِ یک کار و استفادهٔ کامل از خوشه ممکن است همزمان به بهترین مقدار نرسند.
مثال فنی: دو job روی ۱۶۰ هسته
وضعیت اولیه: دو job هرکدام ۱۰۰ task تکهستهایِ همزمان در مرحلهٔ جاری میخواهند.
اجرای مرحلهبهمرحله: با تقسیم ۸۰/۸۰ هر دو مرحله منتظر دو موجاند؛ تخصیصِ متفاوت میتواند یک job را زودتر تمام و منابعش را آزاد کند.
نتیجه و دلیل: انصاف لحظهای و کمینه کردن زمان پایان همیشه یکی نیستند؛ اندازهٔ مرحله و امکان preemption در تصمیم اثر دارند.
The scheduler's job is genuinely hard: it must distribute the cluster's limited resources “optimally” among competing jobs, and these decisions inherently balance fairness against efficiency. See the book's puzzle: a 5-node cluster with 160 CPU cores total, and two job requests each wanting 100 cores. What to do?
- میتوانی ۸۰ task از هر job را اجرا کنی و ۲۰ تای باقیماندهٔ هر کدام را با آزادشدنِ taskهای قبلی شروع کنی.
- میتوانی همهٔ taskهای یکی را اجرا کنی و job دوم فقط وقتی شروع شود که ۱۰۰ هسته آزاد باشد — استراتژیای که به آن
gang scheduling میگویند.
- اگر درخواستِ دوم خیلی دیرتر برسد، scheduler اطلاعاتِ ناقص دارد: باید حدس بزند همهٔ ۱۰۰ هسته را به job اول بدهد
یا مقداری را نگه دارد برای jobی که شاید هرگز نیاید.
- Run 80 tasks of each job and start each one's remaining 20 as earlier tasks free up.
- Run all of one's tasks and start the second only when 100 cores are free — a strategy called gang scheduling.
- If the second request arrives much later, the scheduler holds incomplete information: it must guess whether to give all 100 cores to the first job or hold some back for a job that may never come.
همین مثالِ ساده پر از معاوضهی دشوار است. در gang scheduling اگر تا آزادشدنِ همزمانِ ۱۰۰ هسته صبر کنی،
نودها بیکار مینشینند، استفاده از منابع پایین میآید، و اگر jobهای دیگر هم بخواهند رزرو کنند ممکن است
deadlock رخ دهد. اگر هم بیتکلیف منتظرِ ۱۰۰ هسته بمانی، ممکن است jobهای دیگر هستهها را بربایند و
خوشه مدتِ بسیار طولانی ۱۰۰ هستهٔ آزاد نداشته باشد — یعنی starvation. راهِ سوم این است که
preemption کنی: بعضی taskهای job اول را بکُشی تا جا برای job دوم باز شود؛ ولی taskهای کشتهشده بعداً
باید دوباره اجرا شوند و باز هم کاراییِ خوشه میافتد.
Even this simple example overflows with hard trade-offs. In gang scheduling, waiting for 100 simultaneously-free cores idles nodes, drops utilization, and if other jobs also want reservations a deadlock may strike. Waiting aimlessly for 100 free cores, other jobs may snatch cores and the cluster may lack 100 free cores for very long — starvation. The third way is preemption: kill some of the first job's tasks to make room for the second; but killed tasks must rerun later and cluster efficiency drops again.
روایت مدرس — نمودار ۳ را با صدای بلند بخوانیمTeacher narration
مثال فنی برای خواندنِ نمودار: دو job روی ۱۶۰ هسته
وضعیت اولیه: دو job هرکدام ۱۰۰ task تکهستهایِ همزمان در مرحلهٔ جاری میخواهند.
اجرای مرحلهبهمرحله: با تقسیم ۸۰/۸۰ هر دو مرحله منتظر دو موجاند؛ تخصیصِ متفاوت میتواند یک job را زودتر تمام و منابعش را آزاد کند.
نتیجه و دلیل: انصاف لحظهای و کمینه کردن زمان پایان همیشه یکی نیستند؛ اندازهٔ مرحله و امکان preemption در تصمیم اثر دارند.
این یک درختِ تصمیم با چهار lane است: Decision (سوالها)،
Gang Scheduling، Preemption و
Heuristic Policies. از جعبهٔ شروعِ وسطِ laneِ اول بخوان:
5 Nodes, 160 Cores با زیرنویسِ two jobs want 100 cores each
و تگِ fairness vs efficiency — دقیقاً همان معمای متن.
فلشِ «schedule» به اولین سوال میرسد:
Must Tasks Run Together? (زیرنویس: gang requirement).
جوابِ «yes» تو را به Gang Scheduling میاندازد
(زیرنویس: hold cores until all free، تگِ
idle cores, deadlock risk) — یعنی نگهداشتنِ هستهها تا همه با هم آزاد شوند؛
بهایش هستههای بیکار و خطرِ deadlock. جوابِ «no» تو را به سوالِ دوم میبرد:
New Job Higher Priority? (زیرنویس: priority of the request).
جوابِ «yes» یعنی مسیرِ Preempt Low-Priority
(زیرنویس: killed work restarts later، تگِ spot VMs) —
کشتنِ کارِ کماولویت برای بازکردنِ جا؛ کارِ کشتهشده بعداً دوباره اجرا میشود و در دنیای واقعی همین instanceهای ارزانِ
spotاند. جوابِ «no» به سوالِ سوم میرسد:
Fair-Share or Quota Set? (زیرنویس: fair-share policy available).
جوابِ «yes» یعنی DRF / Priority / Quota
(زیرنویس: fairness over optimality، تگِ heuristic policy)؛
و جوابِ «no» مسیرِ اصلیِ این درخت را به مقصدِ نهایی میرساند:
FIFO / Bin-Packing با زیرنویسِ starvation risk if jobs queue.
و یک شاخهٔ جدا از جعبهٔ شروع، با فلشِ خطچینِ «the hard limit» پایین میرود به
Optimal Is NP-Hard (زیرنویس: schedulers use heuristics instead،
تگِ FIFO, DRF, quotas, bin packing). این حکمِ کلیِ نمودار است:
در مقیاسِ صدها یا میلیونها درخواست، یافتنِ تخصیصِ بهینه NP-hard است،
پس schedulerها با heuristics تصمیمهای «غیربهینه ولی معقول» میگیرند.
کارتِ «The Dilemma in Numbers» اعداد را جمع میکند (۸۰+۸۰ و بقیه بعداً)،
کارتِ «The Cost of Each Answer» بهای هر گزینه را (idle، deadlock، starvation، ازسرگیریِ کشتهشدگان)،
و کارتِ «Why Schedulers Use Heuristics» میگوید schedulerِ متمرکز با
subschedulerهای اختصاصیِ برنامه کار میکند — YARN به آنها ApplicationMasters و
Kubernetes به آنها operators میگوید.
This decision tree has four lanes: Decision (the questions), Gang Scheduling, Preemption and Heuristic Policies. Read from the middle start box of lane one: 5 Nodes, 160 Cores (“two jobs want 100 cores each”, “fairness vs efficiency”) — exactly the text's puzzle. The “schedule” arrow reaches question one: Must Tasks Run Together? (“gang requirement”). “Yes” drops you into Gang Scheduling (“hold cores until all free”, “idle cores, deadlock risk”); “no” moves to question two: New Job Higher Priority? (“priority of the request”).
“Yes” means the Preempt Low-Priority path (“killed work restarts later”, “spot VMs”) — killing low-priority work to make room; killed work reruns later, and in the real world these are the cheap spot instances. “No” reaches question three: Fair-Share or Quota Set? (“fair-share policy available”). “Yes” means DRF / Priority / Quota (“fairness over optimality”, “heuristic policy”); “no” lands the tree's main path at its destination: FIFO / Bin-Packing (“starvation risk if jobs queue”). A separate dashed branch, “the hard limit”, runs from the start box down to Optimal Is NP-Hard (“schedulers use heuristics instead”; “FIFO, DRF, quotas, bin packing”): at hundreds or millions of requests, finding the optimal allocation is NP-hard, so schedulers make “suboptimal but reasonable” heuristic calls. The cards tally the numbers (“The Dilemma in Numbers”: 80+80 with the rest later), each answer's price (“The Cost of Each Answer”: idleness, deadlock, starvation, resuming the killed), and why heuristics (“Why Schedulers Use Heuristics”: the centralized scheduler works with application-specific subschedulers — YARN's ApplicationMasters, Kubernetes' operators).
۳.۶. workflow و DAG3.6. Workflows and DAGs
گردشکار، وابستگیِ کارها را مشخص میکند. DAG نموداری بدون چرخه است که نشان میدهد کدام مرحله باید پیش از مرحلهٔ دیگر تمام شود.
مثال فنی: وابستگی در DAG
وضعیت اولیه: پاکسازی A ورودی مشترک گزارشهای B و C است.
اجرای مرحلهبهمرحله: پس از موفقیت A، دو شاخه B و C میتوانند موازی اجرا شوند؛ اگر A شکست بخورد، خروجی نهاییِ وابسته منتشر نمیشود.
نتیجه و دلیل: DAG ترتیب وابستگی را میگوید، نه یک فهرستِ لزوماً سریالی از دستورها.
In the Unix example each command's output reached the next command; distributed processing has the same pattern: one job's output becomes one or more other jobs' input. This is called a workflow, or a DAG (directed acyclic graph) of jobs.
«workflow» اینجا یعنی چه؟What does “workflow” mean here?
در فصل ۵ از موتورهای durable execution حرف زدیم: اجرای پایدارِ زنجیرهای از گامها،
معمولاً با فراخوانیِ RPC. در پردازشِ دستهای، «workflow» معنای دیگری دارد:
زنجیرهای از jobهای دستهای که هر کدام داده میخوانند و داده مینویسند، ولی معمولاً به سرویسِ بیرونی RPC نمیزنند.
موتورهای durable execution معمولاً دادهی کمتری بهازای هر درخواست پردازش میکنند — هرچند مرز تا حدی مبهم است.
In Chapter 5 we discussed durable execution engines: durable execution of a chain of steps, usually via RPC calls. In batch processing “workflow” means something else: a chain of batch jobs each reading and writing data, but usually never RPC-ing an outside service. Durable execution engines usually process less data per request — though the boundary is somewhat blurry.
سه دلیلِ رایج برای زنجیرهشدنِ jobها:
Three common reasons jobs chain:
- اگر خروجیِ یک job ورودیِ چند jobِ دیگر است که تیمهای مختلفی نگهداریشان میکنند، بهتر است job اول خروجیاش را
جایی بنویسد که همه بتوانند بخوانند؛ jobهای مصرفکننده هر بار که داده بهروز شد (یا با برنامهٔ خودشان) اجرا شوند.
- گاهی میخواهی داده را بینِ ابزارهای پردازش جابهجا کنی: مثلاً یک jobِ Spark روی HDFS خروجی بدهد،
یک اسکریپتِ پایتون یک کوئریِ Trino را روی همان فایلها تریگر کند و نتیجه به S3 برود.
- گاهی خودِ pipeline چند مرحله میخواهد: اگر مرحلهای داده را با یک کلید shard کند و مرحلهٔ بعدی به shard با کلیدِ
دیگری نیاز داشته باشد، مرحلهٔ اول خروجی را به همان شکلی آماده میکند که مرحلهٔ دوم میخواهد.
- If one job's output feeds several jobs maintained by different teams, the first job should write its output where all can read it; consuming jobs run whenever data refreshes (or on their own schedule).
- Sometimes you want to move data between processing tools: e.g. a Spark job outputs on HDFS, a Python script triggers a Trino query over the same files, and the result lands on S3.
- Sometimes the pipeline itself needs stages: if one stage shards data by one key and the next needs sharding by another, the first stage prepares output exactly as the second wants it.
نکتهٔ مهندسیِ جالب: در مثالِ Unix، لوله فقط یک بافرِ کوچکِ درونِ حافظه است و چیزی فایل نمیشود؛
اگر بافر پر شود، تولیدکننده صبر میکند تا مصرفکننده بخواند — یک جور backpressure.
Spark و Flink هم مدلِ مشابهی دارند: خروجیِ یک task مستقیم به taskِ بعدی میرود (اگر روی ماشینهای مختلفاند، از شبکه).
اما معمولتر این است که jobها با نوشتنِ میانی در DFS/object store از هم جدا شوند:
این جداسازیِ زمانی میدهد و اگر jobی چند ورودی داشته باشد، workflow scheduler صبر میکند تا همهٔ jobهای تولیدکننده
با موفقیت تمام شوند. و توجه کن: schedulerهای داخلیِ YARN یا Spark روی job به job کار میکنند و
کلِ workflow را نمیفهمند؛ برای وابستگیها، workflow schedulerهایی مثل
Airflow و Dagster و Prefect ساخته شدهاند.
workflowهای ۵۰ تا ۱۰۰ jobی در pipelineهای داده عادیاند و در سازمانِ بزرگ، تیمهای زیادی روی خروجیِ هم job میزنند —
پس ابزارِ مدیریتی واقعاً لازم است.
The interesting engineering note: in the Unix example the pipe is just a small in-memory buffer with nothing filed; when the buffer fills, the producer waits for the consumer to read — a kind of backpressure. Spark and Flink have a similar model: one task's output goes straight to the next task (over the network if on different machines). But more common is separating jobs by writing intermediate output to DFS/object store: this gives time-decoupling, and when a job has several inputs the workflow scheduler waits for all producing jobs to succeed. And note: YARN's or Spark's internal schedulers work job-by-job and never grasp the whole workflow; for dependencies, workflow schedulers like Airflow, Dagster and Prefect were built. 50-to-100-job workflows are ordinary in data pipelines, and in a big organization many teams run jobs on each other's outputs — so real management tooling is needed.
۳.۷. تحملِ خرابی3.7. Fault tolerance
اگر ورودی تغییرناپذیر باشد و خروجیِ تلاشِ ناقص جدا بماند، میتوان وظیفهٔ شکستخورده را دوباره اجرا کرد. مدیریتِ خروجیِ میانی تعیین میکند چه مقدار کار تکرار شود.
مثال فنی: خروجی attempt ناموفق را منتشر نکن
وضعیت اولیه: task قسمتی از فایل را نوشته و پیش از پایان crash کرده است.
اجرای مرحلهبهمرحله: خروجی در مسیر موقتِ attempt میماند؛ سازوکار commit فقط خروجی موفق را در نسخهٔ job وارد میکند.
نتیجه و دلیل: retry نباید فایل ناقص و فایل کامل را با هم بهعنوان خروجی معتبر نشان دهد؛ I/O مستقیم به مقصد بیرونی سختتر است.
Batch jobs run long; a long job with many parallel tasks will almost surely meet at least one task failure on its way — hardware faults (especially on commodity hardware) or network cuts. But a more common cause exists: the scheduler deliberately preempts (kills) the task. This pays off with priority levels: cheap low-priority tasks run whenever spare capacity exists, at the risk of being killed the moment a higher-priority job arrives. These cheap VMs are called spot instances on EC2, spot VMs on Azure and preemptible instances on Google Cloud. Since batch jobs are rarely urgent, using this cheap capacity is economical (and raises cluster use); but it means preemption happens far more than hardware failure.
چون jobهای دستهای خروجی را هر بار از صفر بازسازی میکنند، مدیریتِ خرابیِ task سادهتر از سیستمهای آنلاین است:
خروجیِ ناقصِ اجرای شکستخورده را حذف میکنی و task را دوباره روی ماشینِ دیگری زمانبندی میکنی.
ولی اجرای دوبارهٔ کلِ job بهخاطرِ یک taskِ شکسته، اسراف است؛ پس MapReduce و جانشینانش اجرای taskهای موازی را
از هم مستقل نگه میدارند تا بتوان در سطحِ تکtask بازتلاش کرد.
Because batch jobs rebuild output from scratch every time, task-failure handling is simpler than in online systems: delete the failed run's partial output and reschedule the task on another machine. But rerunning the whole job for one broken task is wasteful; so MapReduce and its successors keep parallel tasks' execution independent, enabling single-task-level retries.
کار وقتی سخت میشود که خروجیِ یک task ورودیِ taskِ دیگر شود (در workflow). سه استراتژیِ معروف برای دادهی
intermediate:
Things get hard when one task's output feeds another's input (in a workflow). Three famous strategies for intermediate data:
- MapReduce: همهٔ دادهی میانی را در DFS مینویسد و صبر میکند تا نوشتنِ task کامل شود؛ حتی در محیطِ
پر از preemption کار میکند، ولی یعنی نوشتنهای بسیار زیاد در DFS و کاراییِ پایین.
- Spark: دادهی میانی را در حافظه نگه میدارد (و در صورتِ نیاز به دیسکِ محلی spill میکند)
و فقط نتیجهٔ نهایی را به DFS مینویسد؛ چون میداند داده چطور محاسبه شده (lineage)، در صورتِ ازدسترفتن دوباره محاسبهاش میکند.
- Flink: رویکردِ checkpoint: بهصورتِ دورهای از وضعیتِ taskها اسنپشات میگیرد و به ذخیرهسازیِ پایدار مینویسد،
بهجای مادیسازیِ هر رکوردِ میانی.
- MapReduce: writes all intermediate data to DFS and waits for the task's write to complete; works even in preemption-heavy settings, but means enormous DFS writes and low performance.
- Spark: keeps intermediate data in memory (spilling to local disk when needed) and writes only the final result to DFS; knowing how data was computed (lineage), it recomputes on loss.
- Flink: the checkpoint approach: periodically snapshots task state to durable storage instead of materializing every intermediate record.
روایت مدرس — نمودار ۴ را با صدای بلند بخوانیمTeacher narration
مثال فنی برای خواندنِ نمودار: خروجی attempt ناموفق را منتشر نکن
وضعیت اولیه: task قسمتی از فایل را نوشته و پیش از پایان crash کرده است.
اجرای مرحلهبهمرحله: خروجی در مسیر موقتِ attempt میماند؛ سازوکار commit فقط خروجی موفق را در نسخهٔ job وارد میکند.
نتیجه و دلیل: retry نباید فایل ناقص و فایل کامل را با هم بهعنوان خروجی معتبر نشان دهد؛ I/O مستقیم به مقصد بیرونی سختتر است.
این یک sequence با پنج شرکتکننده از چپ به راست است:
Job Client (زیرنویس: workflow scheduler)،
Resource Manager (task scheduler + cluster state)،
Executor A (Node 1)، Executor B
(Node 2) و Distributed FS
(زیرنویس: object store + intermediate data). بالای نمودار سه فاز شمارهگذاری شده:
«1 · Job submission and task allocation»،
«2 · Intermediate data strategies» و
«3 · Failure, task-level retry, all-or-nothing commit».
فازِ ۱: فلشِ «submit job — N tasks + request metadata» از client به resource manager میرود
و یادداشتش همان متادیتای متن است: «job id, CPU / RAM / disk, GPU need, credentials, params, code location».
بعد دو فلشِ تخصیص: «allocate tasks — data locality preferred» به Executor A و
«allocate remaining tasks» به Executor B.
فازِ ۲ سه استراتژیِ دادهی میانی را روی همان نمودار کنارِ هم میگذارد (سه فلش به Distributed FS):
«MapReduce: intermediate written to DFS» با یادداشتِ
«task waits for the write: fault-tolerant, heavy I/O»؛
«Spark: intermediate in memory, spill to disk» با یادداشتِ
«only the final result reaches DFS; loss recovered from lineage»؛
و «Flink: periodic checkpoint snapshot» با یادداشتِ
«task state snapshotted to durable storage».
فازِ ۳ ماجرا را جذاب میکند: فلشِ خطچینِ «heartbeats stop — crash, network cut, or preemption»
از Executor A به resource manager میرود — یادداشت میگوید «preemption: spot capacity reclaimed or a higher-priority job arrives».
resource manager فوراً «task declared incomplete — partial output deleted» را به DFS میفرستد
(فلشِ برجسته) و بعد «reschedule the task — retry at task level» را به Executor B،
با یادداشتِ کلیدیِ «immutable input, no side effects: re-running is safe».
Executor B با «recompute from immutable input; write output» خروجی را از نو میسازد،
«replacement task complete» را خبر میدهد، و بالاخره فلشِ برگشتیِ
«publish job output — all-or-nothing» از resource manager به client میرود.
چهار کارتِ پایین، چهار درس را جمع میکنند: «Preemption Dominates Hardware Failure»
(در عمل preemption شایعترین علتِ ازدسترفتنِ task است، نه خرابیِ سختافزاری)؛
«Three Intermediate Data Strategies» (همان سه گزینهٔ فازِ ۲)؛
«Why Retry at Task Level Is Enough» (ورودی immutable و task بدونِ side effect است؛
فقط taskِ شکسته دوباره جای دیگری اجرا میشود نه کلِ job؛ هر تلاش خروجی را از صفر میسازد، پس retry هرگز نتیجه را تکراری نمیکند)؛
و «All-or-Nothing Output» (خروجیِ ناقصِ هر تلاشِ شکسته حذف میشود؛ خروجیِ job فقط وقتی
همهٔ taskها موفق شدند commit میشود؛ اگر کلِ job شکست بخورد هیچ خروجیای برای خواننده دیده نمیشود).
This sequence has five participants left to right: Job Client (“workflow scheduler”), Resource Manager (“task scheduler + cluster state”), Executor A (“Node 1”), Executor B (“Node 2”) and the Distributed FS (“object store + intermediate data”), in three numbered phases: “1 · Job submission and task allocation”, “2 · Intermediate data strategies” and “3 · Failure, task-level retry, all-or-nothing commit”. Phase 1: “submit job — N tasks + request metadata” runs from client to resource manager (the note lists the text's metadata: job id, CPU/RAM/disk, GPU need, credentials, params, code location), then two allocation arrows: “allocate tasks — data locality preferred” to Executor A and “allocate remaining tasks” to Executor B.
Phase 2 lays the three intermediate-data strategies side by side as arrows to the Distributed FS: “MapReduce: intermediate written to DFS” (“task waits for the write: fault-tolerant, heavy I/O”); “Spark: intermediate in memory, spill to disk” (“only the final result reaches DFS; loss recovered from lineage”); and “Flink: periodic checkpoint snapshot” (“task state snapshotted to durable storage”).
Phase 3 brings the drama: the dashed “heartbeats stop — crash, network cut, or preemption” from Executor A to the resource manager (“preemption: spot capacity reclaimed or a higher-priority job arrives”). The resource manager immediately sends “task declared incomplete — partial output deleted” to DFS (the bold arrow), then “reschedule the task — retry at task level” to Executor B, with the key note “immutable input, no side effects: re-running is safe”. Executor B rebuilds output via “recompute from immutable input; write output”, reports “replacement task complete”, and finally “publish job output — all-or-nothing” returns from resource manager to client. The four bottom cards collect four lessons: preemption dominates hardware failure as the usual task-loss cause; the three intermediate-data options of phase 2; why task-level retry suffices (immutable input, side-effect-free tasks — only the broken task reruns elsewhere, each attempt builds output from scratch, so retries never duplicate results); and all-or-nothing output (each broken attempt's partial output is deleted; job output commits only when all tasks succeed; a fully failed job shows readers no output at all).
روایت مدرس — نمودار ۵ را با صدای بلند بخوانیمTeacher narration
مثال فنی برای خواندنِ نمودار: Task دوباره زمانبندی میشود
وضعیت اولیه: از ۱۰۰ task، worker اجرای task ۳۷ خاموش میشود.
اجرای مرحلهبهمرحله: scheduler task ۳۷ را روی worker دیگری اجرا میکند و خروجیِ attempt موفق را میپذیرد.
نتیجه و دلیل: تمام job لازم نیست از صفر تکرار شود، اگر ورودی و وابستگیهای لازم برای همین task در دسترس باشند.
این نمودار یک ماشینِ وضعیت با چهار lane است: Task attempt (مسیرِ اصلی)،
Preemption — not a fault، Faults and retries و
Terminal outcomes. مسیرِ اصلی چهار گامِ شمارهدار دارد.
01 Queued (زیرنویس: waiting in the job queue، تگِ
no resources yet) — با فلشِ «scheduler assigns task» به
02 Assigned (executor picked on a node، تگِ
locality preferred)، بعد با «fetch code, start process» به
03 Running (intermediate output written، تگِ
heartbeat flowing)، و با «all records processed» به
04 Succeeded (task output written، تگِ
all records processed).
حالا شاخهها را دنبال کن. از Running فلشِ خطچینِ «spot reclaim or priority job»
به وضعیتِ Preempted میرود (زیرنویس: not a fault; requeued،
تگِ no penalty) و فلشِ برگشتیِ «requeue on spare capacity»
آن را دوباره به Queued میبرد — یعنی preemption فقط صف را تغییر میدهد، جریمهای در کار نیست.
در laneِ خطاها، فلشِ قرمزِ «crash or lost heartbeat» از Running به
Failed میرود (node crash or lost heartbeat، تگِ
partial output deleted): اینجا خروجیِ ناقص دور ریخته میشود.
سرنوشتِ Failed دو راه دارد: «retry: attempts left» که به Queued برمیگرداند،
یا «attempts exhausted» که به Failed Permanently
میرساند (retry budget spent، تگِ or job aborted).
از Queued هم فلشِ «job cancelled by client» مستقیم به
Cancelled میرود (job cancelled by the client، تگِ terminal).
و مسیرِ خوشیمنا: از Succeeded با «all tasks done: commit» به
Committed میرسیم (job output committed atomically، تگِ
all-or-nothing output) — همان all-or-nothingِ نمودارِ قبل.
کارتها مرزهای ظریف را تیز میکنند: «Failed vs Preempted» میگوید هر دو از Running
خارج و به Queued برمیگردند، ولی فقط Failed خطا حساب میشود و از budgetِ بازتلاش میخورد؛
«Preemption Is Normal, Not a Failure» یادآوری میکند preemption از خرابیِ واقعیِ سختافزار
خیلی رایجتر است و برای jobهای غیرفوری این معاوضه اقتصادی است؛ و «Why Retrying a Task Is Safe»
میگوید ورودی immutable و task بدونِ side effect بیرونی است — پس دوباره اجرا کردنش هیچچیز را خراب نمیکند.
This state machine has four lanes: Task attempt (the main path), Preemption — not a fault, Faults and retries and Terminal outcomes. The main path has four numbered steps: 01 Queued (“waiting in the job queue”, “no resources yet”) → “scheduler assigns task” → 02 Assigned (“executor picked on a node”, “locality preferred”) → “fetch code, start process” → 03 Running (“intermediate output written”, “heartbeat flowing”) → “all records processed” → 04 Succeeded (“task output written”, “all records processed”).
The branches: from Running the dashed “spot reclaim or priority job” reaches Preempted (“not a fault; requeued”, “no penalty”), and “requeue on spare capacity” returns it to Queued — preemption only reshuffles the queue, no penalty involved. In the fault lane the red “crash or lost heartbeat” runs from Running to Failed (“node crash or lost heartbeat”, “partial output deleted”): partial output is discarded here. A Failed fate forks two ways: “retry: attempts left” back to Queued, or “attempts exhausted” to Failed Permanently (“retry budget spent”, “or job aborted”). From Queued, “job cancelled by client” goes straight to Cancelled (“job cancelled by the client”, “terminal”); and the happy path runs from Succeeded via “all tasks done: commit” to Committed (“job output committed atomically”, “all-or-nothing output”) — the previous diagram's all-or-nothing. The cards sharpen the fine lines: Failed vs Preempted (both leave Running for Queued, but only Failed counts as an error eating the retry budget); preemption is normal, not failure (far more common than real hardware faults, and economical for non-urgent jobs); and retrying a task is safe because input is immutable and the task has no outside side effects.
مفهوم مکمل — سه خانه دادهExtra concept — three homes for bytes
روایت مدرس از نمودارTeacher narration
مثال فنی برای خواندنِ نمودار: Object یک فایل POSIX کامل نیست
وضعیت اولیه: خروجی job با کلید reports/run-42/part-1 در object store نوشته میشود.
اجرای مرحلهبهمرحله: برای انتشارِ نسخهٔ کامل، manifest یا اشارهگر نسخهٔ معتبر عوض میشود؛ نباید rename اتمیکِ دایرکتوری را بدون پشتیبانی فرض کرد.
نتیجه و دلیل: API و تضمین عملیات object store باید جدا بررسی شود؛ شبیه بودن نام «فایل» رفتار یکسان نمیسازد.
سه ستون سه خانهاند: فایلسیستم محلی (بلاک 4KB، rename اتمی و قفل، فقط یک ماشین)، فایلسیستم توزیعشده (بلاک 128MB، متادیتای متمرکز، اجرای task نزدیک داده) و object store (bucket و key، immutable، بدون قفل و link، مقیاس مستقل compute). نوار پایین ریزنکتهها را هشدار میدهد: rename یعنی کپی و حذف غیراتمیک، و دایرکتوری خالی ناممکن است.
The three columns are three homes: the local filesystem (4KB blocks, atomic rename and locks, one machine only), the distributed filesystem (128MB blocks, centralized metadata, tasks near data) and the object store (bucket and key, immutable, no locks or links, independently-scaling compute). The bottom strip warns the fine print: rename means non-atomic copy plus delete, and empty directories are impossible.
۴4 مدلهای پردازشProcessing models
مدل پردازش مشخص میکند منطق برنامه چگونه به وظیفههای اجرایی شکسته شود. MapReduce الگوی محدودتری دارد و dataflow زنجیرهٔ متنوعتری از تبدیلها را بیان میکند.
مثال فنی: یک محاسبه، چند مدل اجرا
وضعیت اولیه: ورودی فروش باید فیلتر، با کالا join و بر دسته تجمیع شود.
اجرای مرحلهبهمرحله: میتوان آن را به jobهای MapReduce، DAG عملگرها یا SQL روی موتور توزیعشده بیان کرد.
نتیجه و دلیل: اختلاف در کنترل و بهینهسازی اجراست؛ برای مقایسه باید همان semanticsِ join و null و نوع عدد حفظ شوند.
So far we saw how jobs are scheduled and run; now how frameworks process data. Two common models: MapReduce and dataflow engines. Though dataflow has in practice replaced MapReduce, understanding MapReduce is still required — it shaped many modern frameworks. Both models opened over time to multiple programming models: low-level programming APIs, relational query languages and DataFrame APIs.
۴.۱. MapReduce4.1. MapReduce
در MapReduce، نگاشت برای هر ورودی جفتِ کلید–مقدار میسازد، جابهجایی داده کلیدهای یکسان را جمع میکند و کاهش نتیجهٔ هر کلید را محاسبه میکند.
مثال فنی: Map، shuffle، reduce
وضعیت اولیه: متنهای red blue و red ورودیاند.
اجرای مرحلهبهمرحله: map جفتهای (red,1),(blue,1),(red,1) میدهد؛ shuffle redها را کنار هم میفرستد؛ reduce جمع red=2,blue=1 را میسازد.
نتیجه و دلیل: shuffle تصادفی نیست؛ گروهبندیِ کلید تعیین میکند همهٔ مقدارهای یک کلید به محاسبهٔ مشترک برسند.
MapReduce's processing pattern closely resembles Section 2's log analysis — four steps:
- خواندنِ ورودی و شکستن به رکورد: در مثالِ لاگ، هر رکورد یک خط است (جداکنندهٔ رکورد
\n).
در Hadoop، ورودی در HDFS یا object store مثل S3 است و قالبهای فایلِ مختلفی داریم: مثل
Apache Parquet (قالبِ ستونی) یا Apache Avro (قالبِ سطری).
- فراخوانیِ تابعِ mapper برای استخراجِ key و value از هر رکوردِ ورودی. در مثالِ ما mapper همان
awk '{print $7}' است که URL را بهعنوان key برمیدارد و value خالی میماند.
- مرتبسازیِ همهٔ جفتهای (key, value) بر اساسِ key. در مثال، همان دستورِ اولِ
sort.
- فراخوانیِ reducer روی جفتهای مرتبشده. چون رکوردهای همکلید با مرتبسازی مجاور شدهاند،
ترکیبِ مقادیرشان بدونِ نگهداشتنِ state زیاد در حافظه ممکن است. در مثال، reducer همان
uniq -c است که تعدادِ رکوردهای مجاورِ همکلید را میشمارد.
- Read input and break into records: in the log example each record is one line (record separator
\n).
In Hadoop, input sits in HDFS or an object store like S3, with various file formats: e.g.
Apache Parquet (columnar) or Apache Avro (row-based).
- Call the mapper to extract key and value from each input record. In our example the mapper is
awk '{print $7}', taking the URL as key with an empty value.
- Sort all (key, value) pairs by key. In the example, the first
sort command.
- Call the reducer on the sorted pairs. Since same-key records became adjacent through sorting,
combining their values needs little in-memory state. In the example the reducer is
uniq -c, counting adjacent same-key records.
این چهار گام را میتوان در یک jobِ MapReduce انجام داد. گامهای ۲ (map) و ۴ (reduce) جاییاند که
کدِ سفارشیِ خودت را مینویسی؛ گامِ ۱ با parserِ ورودی حل است؛ و گامِ ۳ یعنی sort در MapReduce
ضمنی است — لازم نیست بنویسیاش، چون خروجیِ mapper همیشه قبل از رسیدن به reducer مرتب میشود.
These four steps fit in one MapReduce job. Steps 2 (map) and 4 (reduce) are where you write custom code; step 1 is solved by the input parser; and step 3, sorting, is implicit in MapReduce — you never write it, because mapper output is always sorted before reaching the reducer.
-
Mapper: برای هر رکوردِ ورودی دقیقاً یک بار صدا زده میشود و کارش استخراجِ key و value است؛
میتواند بهازای هر ورودی هر تعداد جفت (حتی صفر) تولید کند. هیچ stateای از رکوردِ قبلی نگه نمیدارد —
هر رکورد مستقل پردازش میشود — و میتواند mapperهای زیادی موازی روی بخشهای مختلفِ ورودی اجرا شوند.
-
Reducer: framework جفتهای mapperها را جمع میکند، همهٔ مقادیرِ یک key را کنارِ هم میآورد و
reducer را با یک iterator روی آن مجموعه صدا میزند. reducer میتواند رکوردِ خروجی بسازد
(مثل تعدادِ ظهورِ هر URL). reducerهای کلیدهای مختلف هم موازی اجرا میشوند.
-
Mapper: called exactly once per input record, extracting key and value; it may emit any number of pairs (even zero) per input. It keeps no state from the previous record — each record is processed independently — and many mappers can run in parallel over different input parts.
-
Reducer: the framework collects the mappers' pairs, brings all values of one key together and calls the reducer with an iterator over that set. The reducer may build output records (like each URL's occurrence count). Reducers of different keys run in parallel too.
در مثالِ لاگ، دستورِ دومِ sort برای رتبهبندی بود. در MapReduce اگر به مرحلهٔ مرتبسازیِ دوم نیاز داشته باشی،
یک jobِ MapReduceِ دوم مینویسی و خروجیِ job اول ورودیاش میشود. از همین زاویه نقشها را ببین:
mapper داده را برای مرتبسازی آماده میکند و reducer دادهی مرتبشده را پردازش میکند.
In the log example the second sort was for ranking. In MapReduce, needing a second sort stage means writing a second MapReduce job with the first job's output as its input. Roles from this angle: the mapper prepares data for sorting, and the reducer processes sorted data.
ریشه در برنامهنویسیِ تابعیRoots in functional programming
اگرچه MapReduce برای پردازشِ دستهای به کار میرود، مدلِ برنامهنویسیاش از برنامهنویسیِ تابعی میآید:
لیسپ map و reduce (یا fold) را بهعنوانِ توابعِ مرتبهبالا روی لیستها معرفی کرد.
اصلِ «پرهیز از state تغییرپذیر» دقیقاً همان چیزی است که اجرای موازی را ممکن میکند:
چون هر فراخوانیِ mapper/reducer فقط به دادهای که framework صریحاً به تابع میدهد وابسته است،
framework آزاد است فراخوانیهای مستقل را روی نودهای مختلف موازی اجرا کند — و اگر taskی شکست بخورد،
همان mapper/reducer را با همان ورودی دوباره روی نودِ دیگری صدا بزند.
Though MapReduce serves batch processing, its programming model comes from functional programming: Lisp introduced map and reduce (or fold) as higher-order list functions. The “avoid mutable state” principle is exactly what enables parallel execution: since each mapper/reducer call depends only on data the framework explicitly passes in, the framework is free to run independent calls on different nodes — and if a task fails, to recall the same mapper/reducer with the same input on another node.
زحمت و کندیِ MapReduceMapReduce's toil and slowness
پیادهسازیِ jobِ پیچیده با API خامِ MapReduce واقعاً طاقتفرساست — مثلاً هر الگوریتمِ join را باید از صفر بنویسی.
MapReduce در مقایسه با پردازندههای مدرن کُند هم هست؛ یک دلیلش I/O فایلیاش است که
pipelining را ناممکن میکند: نمیشود خروجی را در jobِ پاییندستی پردازش کرد
قبل از آنکه jobِ بالادستی تمام شود.
Implementing a complex job with raw MapReduce API is truly grueling — e.g. every join algorithm must be written from scratch. MapReduce is also slow next to modern processors; one reason is its file I/O, which makes pipelining impossible: output cannot be processed in the downstream job before the upstream job finishes.
روایت مدرس — نمودار ۶ را با صدای بلند بخوانیمTeacher narration
مثال فنی برای خواندنِ نمودار: Map، shuffle، reduce
وضعیت اولیه: متنهای red blue و red ورودیاند.
اجرای مرحلهبهمرحله: map جفتهای (red,1),(blue,1),(red,1) میدهد؛ shuffle redها را کنار هم میفرستد؛ reduce جمع red=2,blue=1 را میسازد.
نتیجه و دلیل: shuffle تصادفی نیست؛ گروهبندیِ کلید تعیین میکند همهٔ مقدارهای یک کلید به محاسبهٔ مشترک برسند.
این dataflow پنج ستون دارد: Input Shards، Map Tasks،
Map Output، Shuffle + Reduce و
Output Dataset — همان شکلِ ۱۱-۱ کتاب با سه mapper و سه reducer.
از راست شروع کن: سه جعبهی Shard m1 و Shard m2 و
Shard m3 با زیرنویسِ HDFS file/object و تگِ
input dataset — یعنی ورودی از قبل shard شده.
فلشهای «m1 records» و «m2 records» و «m3 records»
با توضیحِ «record by record» به سه Map Task میرسند
(زیرنویسِ هر کدام: emits (key,value)، تگِ one per shard).
حالا قلبِ ماجرا: فلشهای «key-value» با توضیحِ «hash(key) routing»
از هر mapper به یک جعبهی Sorted Runs میروند:
m1 Sorted Runs با زیرنویسِ m1r1 m1r2 m1r3 و تگِ
local disk (و به همین ترتیب m2 و m3).
یعنی هر mapper بهازای هر reducer یک فایلِ مرتبِ جدا روی دیسکِ محلی مینویسد: فایلِ m1r2
همان فایلی است که mapper ۱ برای reducer ۲ ساخته. همینجاست که مرتبسازیِ درونِ هر فایل هم انجام میشود —
با همان تکنیکِ log-structured: جفتها در یک ساختارِ مرتبِ درونِ حافظه جمع، بهصورتِ segment مرتب نوشته،
و segmentهای کوچک بهمرور ادغام میشوند.
بعد فلشهای shuffle copy را ببین: «m1r2» و «m2r2»
و «m3r2» هر سه به Reduce Task 2 میروند؛ و «m1r1»
و «m3r3» (با توضیحِ sorted run) به reducer ۱ و ۳.
هر reducer وقتی سهمش را از همهٔ mapperها گرفت، فایلها را با حفظِ ترتیبِ مرتب، سبکِ mergesort
ادغام میکند — به همین دلیل زیرنویسِ reducerها «mergesort shares» و تگشان
«group by key» است: رکوردهای همکلید حالا مجاورند، حتی اگر از mapperهای مختلف آمده باشند.
و خروجی: فلشهای «sorted file» با توضیحِ «one file per task»
به سه جعبهی Output r1 و r2 و r3
(زیرنویس: sorted records، تگِ output shard) —
این سه فایل، shardهای datasetِ خروجی میشوند و به DFS/object store برمیگردند.
کارتها را هم بخوان: «Shuffle Is Not Random» میگوید
hash(key) هر رکورد را دقیقاً به یک reducer میفرستد و هر mapper یک فایلِ مرتب بهازای هر reducer
مینویسد (m1r1 ... m3r3)؛ «Sort Is the Foundation»
میگوید مرتبسازی پایهٔ grouping و join است و reducer یک بار بهازای هر key با یک iterator روی مقادیرش اجرا میشود؛
«Job Sizing» میگوید تعدادِ map taskها = تعدادِ shardهای ورودی، ولی تعدادِ reduce taskها را
نویسندهٔ job تعیین میکند نه اندازهٔ ورودی؛ و «Modern Shuffle Engines»
اشاره میکند موتورهای جدید دادهی shuffle را در حافظه نگه میدارند یا مرتبسازی را به یک سرویسِ external sort میسپارند
و دادهی shuffleشده را replicate میکنند تا گمشدنِ یک نود job را متوقف نکند.
This dataflow has five columns — Input Shards, Map Tasks, Map Output, Shuffle + Reduce and Output Dataset — the book's Figure 11-1 with three mappers and three reducers. Three boxes — Shard m1, m2, m3 (“HDFS file/object”, “input dataset”) — i.e. pre-sharded input. The “m1 records” / “m2 records” / “m3 records” arrows (“record by record”) reach three Map Tasks (each “emits (key,value)”, “one per shard”).
Now the heart: “key-value” arrows (“hash(key) routing”) run from each mapper to a Sorted Runs box: m1 Sorted Runs (“m1r1 m1r2 m1r3”, “local disk”), and likewise m2, m3. Each mapper writes one separate sorted file per reducer on local disk: file m1r2 is what mapper 1 built for reducer 2. In-file sorting happens here too — with the same log-structured technique: pairs gather in an in-memory sorted structure, spill as sorted segments, and small segments gradually merge.
Then the shuffle copy arrows: m1r2, m2r2 and m3r2 all reach Reduce Task 2; m1r1 and m3r3 (“sorted run”) reach reducers 1 and 3. Each reducer, having collected its share from every mapper, merges the files mergesort-style keeping sorted order — hence the reducers' “mergesort shares” caption and “group by key” tag: same-key records are now adjacent even from different mappers. Output: “sorted file” arrows (“one file per task”) to Output r1, r2, r3 (“sorted records”, “output shard”) — the output dataset's shards, returning to DFS/object store. The cards add: “Shuffle Is Not Random” (hash(key) sends each record to exactly one reducer; each mapper writes one sorted file per reducer); “Sort Is the Foundation” (sorting underpins grouping and joins; the reducer runs once per key with an iterator over its values); “Job Sizing” (map-task count = input-shard count, but reduce-task count is the job author's call, not the input's size); and “Modern Shuffle Engines” (new engines keep shuffle data in memory or hand sorting to a dedicated external-sort service, replicating shuffled data so one lost node never stops the job).
۴.۲. موتورهای dataflow4.2. Dataflow engines
موتور dataflow کلِ زنجیرهٔ تبدیل را میبیند و میتواند مراحل را ترکیب کند یا دادهٔ میانی را مناسبتر نگه دارد. همهٔ مراحل الزاماً نباید به چند job مستقل شکسته شوند.
مثال فنی: ادغام عملگرهای متوالی
وضعیت اولیه: مراحل filter → map → groupBy داریم.
اجرای مرحلهبهمرحله: موتور میتواند filter و map را در همان task اجرا کند و پیش از groupBy داده را shuffle کند.
نتیجه و دلیل: حذف مرزِ غیرضروریِ job، فایل میانی و راهاندازی را کم میکند؛ مرحلهٔ نیازمند توزیعِ دوباره همچنان شبکه میخواهد.
To fix MapReduce's problems, several new execution engines were built, most famously Spark and Flink. Their designs differ but share one thing: they see the whole workflow as one job, not a set of independent subjobs. Since they explicitly model data flow across processing stages, they are called dataflow engines. Like MapReduce they have a low-level API calling user functions record by record, but also higher-level operators like join and group by; they parallelize by sharding input and carry each task's output over the network to the next task's input. Unlike MapReduce, operators needn't alternate in a rigid map/reduce pattern and compose more flexibly. The style's roots are the Dryad and Nephele research systems.
شش مزیتِ این مدل نسبت به MapReduce:
Six advantages of this model over MapReduce:
- کارِ گرانی مثل مرتبسازی فقط جایی انجام میشود که واقعاً لازم است، نه همیشه بینِ هر map و reduce.
- اگر چند operatorِ پشتِ سرِ هم shardingِ dataset را عوض نکنند (مثل map یا filter)، میتوانند در یک task
ادغام (fuse) شوند و هزینهٔ کپیِ داده کم شود.
- چون همهٔ joinها و وابستگیهای داده صریحاً اعلام میشوند، scheduler تصویرِ کلی دارد و میتواند
locality را بهینه کند: مثلاً taskِ مصرفکننده را روی همان ماشینِ تولیدکننده بگذارد تا داده
از بافرِ حافظهٔ مشترک رد شود، نه از شبکه.
- معمولاً state میانیِ بینِ operatorها را میتوان در حافظه یا دیسکِ محلی نگه داشت — I/O بسیار کمتر از نوشتن
در DFS/object store (که باید چند نسخه و روی چند دیسک نوشته شود). MapReduce این بهینهسازی را برای خروجیِ mapper داشت؛
dataflow آن را به همهٔ stateهای میانی تعمیم میدهد.
- operatorها میتوانند بهمحضِ آمادهشدنِ ورودیشان شروع کنند؛ نیازی نیست کلِ مرحلهٔ قبل تمام شود.
- پروسههای موجود برای اجرای operatorهای جدید دوباره استفاده میشوند و هزینهٔ راهاندازی کم میشود
(MapReduce برای هر task یک JVM تازه بالا میآورد).
- Expensive work like sorting happens only where truly needed, not always between every map and reduce.
- Consecutive operators that don't change the dataset's sharding (like map or filter) can fuse into one task, cutting data-copy costs.
- Since all joins and data dependencies are declared explicitly, the scheduler sees the whole picture and can optimize locality: e.g. placing the consuming task on the producer's machine so data passes through shared memory buffers, not the network.
- Intermediate state between operators can usually live in memory or local disk — far less I/O than writing to DFS/object store (which must be written several times over several disks). MapReduce had this optimization for mapper output; dataflow generalizes it to all intermediate state.
- Operators can start as soon as their input is ready; no need for the whole previous stage to finish.
- Existing processes are reused for running new operators, cutting startup costs (MapReduce boots a fresh JVM per task).
با dataflow میتوان همان محاسبههای workflowهای MapReduce را نوشت و معمولاً بهخاطرِ همین بهینهسازیها
قابلِ ملاحظهای سریعتر اجرا میشوند.
Dataflow can express the same computations as MapReduce workflows, and usually runs them observably faster thanks to these optimizations.
روایت مدرس — نمودار ۷ را با صدای بلند بخوانیمTeacher narration
مثال فنی برای خواندنِ نمودار: ادغام عملگرهای متوالی
وضعیت اولیه: مراحل filter → map → groupBy داریم.
اجرای مرحلهبهمرحله: موتور میتواند filter و map را در همان task اجرا کند و پیش از groupBy داده را shuffle کند.
نتیجه و دلیل: حذف مرزِ غیرضروریِ job، فایل میانی و راهاندازی را کم میکند؛ مرحلهٔ نیازمند توزیعِ دوباره همچنان شبکه میخواهد.
این یک تایملاینِ مشترک است: «Where the Time Goes — MapReduce vs a Dataflow Engine»
و زیرنویسش میگوید «one shared time axis · green bars are compute tasks · grey bars are durable writes to the filesystem».
راهنمای رنگها بالای نمودار است: compute task (نوارِ سبز)،
durable write to DFS / object store (نوارِ خاکستری) و
stage barrier (خطِ مانعِ مرحله). محورِ زمان با فلشِ «time →»
به سمتِ راست میرود و برچسبِ «not to scale» میگوید مقیاس دقیق نیست.
بندِ اول با عنوانِ «① MapReduce — every stage finishes by writing files to the distributed filesystem»
شروع میشود: سه نوارِ سبزِ map 1 و map 2 و
map 3 (هر کدام «read shard → map()») موازی میدَوَند،
بعد یک stage barrier میآید، بعد نوارِ خاکستریِ DFS write
با توضیحِ «write intermediate to DFS (replicated, durable)» — و بعد دوباره barrier،
بعد نوارهای reduce 1/2/3، barrier دیگر، و نوارِ output
با «write output». جمعِ بندِ اول زیرش نوشته:
«file-based I/O between every stage · no pipelining · new JVM per task · sort happens between every map and reduce».
بندِ دوم «② Dataflow engine (Spark / Flink) — one job, one DAG: operators start as soon as their input is ready»
است: نوارِ map + filter با توضیحِ «map+filter fused into one task»
(یادت باشد فیوژنِ operatorها)، map 2 («read shard → map()»)،
و بعد نوارهای reduce 1 و reduce 2 که کنارِ
reduce 1 نوشته «reduce 1 · starts as soon as input is ready» —
یعنی barrier نیست؛ هر کدام بهمحضِ آمادهشدنِ ورودی شروع میشوند. نوارِ intermediate
با توضیحِ «intermediate state: memory / local disk» نشان میدهد دادهی میانی
به DFS نمیرود؛ و فقط نوارِ پایانیِ output با «final output → DFS / object store»
نوشتنِ پایدار دارد. جمعِ این بند: «sort only where needed · whole DAG visible to the scheduler → data locality ·
reuse worker processes · lineage recompute / checkpoints instead of DFS writes».
درسِ نمودار را با چشم ببین: در بندِ اول، فاصلهی بینِ نوارهای سبز را نوارهای خاکستری و barrierها پر کردهاند —
یعنی وقتِ مصرفشده برای I/O فایلی و انتظار؛ در بندِ دوم، نوارهای سبز روی هم سوارند و فقط یک نوشتنِ پایدارِ نهایی هست.
همان تفاوتِ «pipelining نداریم» با «pipelining داریم».
This shared-timeline diagram — “Where the Time Goes — MapReduce vs a Dataflow Engine” (“one shared time axis · green bars are compute tasks · grey bars are durable writes to the filesystem”) — has its color key on top: compute task (green bar), durable write to DFS / object store (grey bar) and stage barrier (barrier line), with time running right (“time →”, “not to scale”). Lane one, “① MapReduce — every stage finishes by writing files to the distributed filesystem”: three green map 1/2/3 bars (“read shard → map()”) run in parallel, then a stage barrier, then the grey DFS write (“write intermediate to DFS (replicated, durable)”) — then another barrier, the reduce 1/2/3 bars, another barrier, and the output bar (“write output”). Lane one's total reads: “file-based I/O between every stage · no pipelining · new JVM per task · sort happens between every map and reduce”.
Lane two, “② Dataflow engine (Spark / Flink) — one job, one DAG: operators start as soon as their input is ready”: a map + filter bar (“map+filter fused into one task” — operator fusion), map 2 (“read shard → map()”), then reduce 1 and reduce 2 — reduce 1 tagged “starts as soon as input is ready”: no barriers, each starts the moment its input exists. The intermediate bar (“intermediate state: memory / local disk”) shows intermediate data skipping DFS; only the final output bar (“final output → DFS / object store”) writes durably. Lane two's total: “sort only where needed · whole DAG visible to the scheduler → data locality · reuse worker processes · lineage recompute / checkpoints instead of DFS writes”. See the lesson with your eyes: in lane one the gaps between green bars are filled with grey bars and barriers — time spent on file I/O and waiting; in lane two green bars overlap with a single durable write at the end. That is “no pipelining” vs “pipelining”.
۴.۳. Shuffle4.3. Shuffle
Shuffle یعنی داده دوباره میان وظیفهها توزیع شود تا رکوردهای مرتبط کنار هم قرار بگیرند. این جابهجایی معمولاً شبکه، مرتبسازی و گاهی دیسک را درگیر میکند.
مثال فنی: Shuffle بر کلید مشتری
وضعیت اولیه: سفارشهای مشتری ۱۷ در چند فایل و چند worker پراکندهاند.
اجرای مرحلهبهمرحله: partitioner همهٔ جفتهای کلید ۱۷ را به reducer همان کلید میفرستد؛ داده ممکن است روی شبکه منتقل و sort شود.
نتیجه و دلیل: تمرکزِ یک کلید برای تجمیع لازم است؛ مشتری بسیار بزرگ میتواند reducer داغ و زمان پایان طولانی بسازد.
As seen, both the Unix example and MapReduce are built on sorting. Batch processors must sort petabyte datasets — data fitting no single machine — so they need a distributed sort whose input and output are both sharded. That algorithm is called shuffle.
shuffle تصادفی نیست!Shuffle is not random!
وقتی دستهکارت را «shuffle» میکنی، ترتیبِ تصادفی درمیآید. ولی shuffle اینجا دقیقاً برعکس است:
خروجیِ مرتب میدهد، بدونِ هیچ تصادفی. این الگوریتمِ بنیادیِ پردازندههای دستهای است و برای
join و تجمیع به کار میرود: MapReduce و Spark و
Flink و Daft و Dataflow و
BigQuery همگی shuffleهای مقیاسپذیر و کارآمد پیاده کردهاند تا datasetهای بزرگ را اداره کنند.
When you shuffle playing cards, random order comes out. But shuffle here is exactly the opposite: it yields sorted output, with zero randomness. This fundamental batch-processor algorithm serves joins and aggregation: MapReduce, Spark, Flink, Daft, Dataflow and BigQuery all implement scalable, efficient shuffles to handle big datasets.
مکانیزمِ هادوپی (نمودار ۶ و شکلِ ۱۱-۱) را دیدیم: هر mapper بهازای هر reducer یک فایلِ مرتب مینویسد،
مسیریابی با hash(key) انجام میشود و reducer سهمش را با mergesort ادغام میکند.
ولی موتورهای dataflow و warehouseهای ابری پیشرفتهترند: سیستمهایی مثل BigQuery الگوریتمِ shuffle را
بهینه کردهاند تا داده را در حافظه نگه دارند، یا مرتبسازی را به سرویسِ external sort اختصاصی بسپارند؛
چنین سرویسهایی shuffle را سریعتر میکنند و دادهی shuffleشده را replicate میکنند تا از دست رفتنِ یک نود
کلِ job را متوقف نکند.
We saw the Hadoop mechanism (Diagram 6, Figure 11-1): each mapper writes one sorted file per reducer, routing by hash(key), and the reducer merges its share mergesort-style. But dataflow engines and cloud warehouses are more advanced: systems like BigQuery optimized the shuffle algorithm to hold data in memory, or hand sorting to a dedicated external sort service; such services speed shuffle and replicate shuffled data so losing one node never stops the whole job.
۴.۴. Join و Grouping4.4. Joins and grouping
Join رکوردهای مرتبطِ دو مجموعه را ترکیب میکند و grouping رکوردهای یک مجموعه را بر اساس کلید کنار هم میگذارد. روش اجرا به اندازهٔ داده و نحوهٔ توزیع و ترتیب آن بستگی دارد.
مثال فنی: دو راهِ join
وضعیت اولیه: فروش یک میلیارد ردیف دارد و جدول دستهبندی محصول فقط 5 MB است.
اجرای مرحلهبهمرحله: جدول کوچک را میتوان به workerها broadcast کرد؛ اگر هر دو طرف بزرگ باشند، تقسیم و sort-merge روی کلید مطرح میشود.
نتیجه و دلیل: اندازه و توزیع کلید تعیینکنندهاند؛ broadcast جدول بزرگ میتواند حافظهٔ همهٔ workerها را پر کند.
Now see how sorted data simplifies distributed joins and aggregation. The classic example: on the left, the logged-in users' event log (called activity events or clickstream) and on the right, the user database. Think of this as part of a star schema: the event log is the fact table, the user database one dimension. To analyze with user info included (e.g. whether certain pages are more popular among younger or older users — via the birth-date field), you must join the two tables. Assuming both are big enough to shard, how?
جواب در همان خاصیتِ shuffle است: همهٔ جفتهای همکلید را، از هر shardی که آمده باشند، به یک reducer میرساند.
پس شناسهٔ کاربر را کلید میگیریم: یک mapper روی رویدادها میرود و URLهای بازدیدشده را با کلیدِ
user_id منتشر میکند؛ mapper دیگری روی پایگاه دادهِ کاربران سطر به سطر میرود،
شناسهٔ کاربر را بهعنوانِ key و تاریخِ تولد را بهعنوانِ value استخراج میکند.
shuffle تضمین میکند reducer هم تاریخِ تولدِ یک کاربرِ خاص و هم همهٔ رویدادهای او را همزمان داشته باشد.
MapReduce حتی میتواند رکوردها را طوری مرتب کند که reducer همیشه اول رکوردِ پایگاه دادهِ کاربر را ببیند و بعد
رویدادها را به ترتیبِ timestamp — تکنیکی که به آن secondary sort میگویند.
The answer is that same shuffle property: it delivers every same-key pair, from whichever shard, to one reducer. So take user id as key: one mapper scans the events and publishes visited URLs keyed by user_id; another mapper scans the user database row by row, extracting user id as key and birth date as value. Shuffle guarantees the reducer holds one user's birth date and all their events at once. MapReduce can even order records so the reducer always sees the user-database record first, then events in timestamp order — a technique called secondary sort.
حالا منطقِ join ساده است: اولین مقدار باید تاریخِ تولد باشد که reducer در یک متغیرِ محلی نگه میدارد؛
بعد روی رویدادهای همان user_id پیمایش میکند و هر URLِ بازدیدشده را بههمراهِ تاریخِ تولدِ بیننده خروجی میدهد.
چون reducer همهٔ رکوردهای یک user_id را یکجا پردازش میکند، در هر لحظه فقط یک رکوردِ کاربر در حافظه لازم دارد
و هرگز نیازی به درخواستِ شبکهای نیست. نامِ این الگوریتم sort-merge join است:
خروجیِ mapperها بر اساسِ key مرتب است و reducer لیستهای مرتبِ دو طرفِ join را با هم ادغام میکند.
Now the join logic is simple: the first value must be the birth date, which the reducer keeps in a local variable; then it walks the same user_id's events, emitting each visited URL alongside the viewer's birth date. Since the reducer processes one user_id's records together, it needs only one user record in memory at a time and never a network request. This algorithm is the sort-merge join: mapper output is key-sorted and the reducer merges both join sides' sorted lists.
و jobِ بعدیِ workflow میتواند توزیعِ سنِ بینندهها را بهازای هر URL حساب کند: داده را با کلیدِ URL
shuffle میکند، و reducerها روی همهٔ بازدیدهای یک URL میچرخند، بهازای هر گروهِ سنی یک شمارنده نگه میدارند
و با هر بازدید شمارندهٔ مربوط را یکی زیاد میکنند — یعنی همان group by و aggregation.
And the workflow's next job can compute viewer-age distribution per URL: shuffle data keyed by URL, and reducers walk all of one URL's visits, keeping one counter per age group and incrementing on each visit — i.e. group by and aggregation.
روایت مدرس — نمودار ۸ را با صدای بلند بخوانیمTeacher narration
مثال فنی برای خواندنِ نمودار: دو راهِ join
وضعیت اولیه: فروش یک میلیارد ردیف دارد و جدول دستهبندی محصول فقط 5 MB است.
اجرای مرحلهبهمرحله: جدول کوچک را میتوان به workerها broadcast کرد؛ اگر هر دو طرف بزرگ باشند، تقسیم و sort-merge روی کلید مطرح میشود.
نتیجه و دلیل: اندازه و توزیع کلید تعیینکنندهاند؛ broadcast جدول بزرگ میتواند حافظهٔ همهٔ workerها را پر کند.
این dataflow پنج ستون دارد: Sharded Inputs، Map Phase،
Shuffle + Sort، Join Reduce و Group-By Job.
از راست: دو جعبهی ورودی — Clickstream (زیرنویس: fact table،
تگِ user_id, ts, URL) و User Profiles
(زیرنویس: dimension table، تگِ user_id, DOB) —
دقیقاً نقشهای star schema از متن. فلشِ «clickstream» به
Event Mapper (زیرنویس: key: user_id، تگِ
URL + timestamp) و فلشِ «profile records» به
Profile Mapper (زیرنویس: key: user_id، تگِ
date of birth) میرود.
دو فلشِ برجسته — «mapped events» و «mapped profiles» —
به قلبِ نمودار میرسند: User-ID Shuffle با زیرنویسِ group by user_id
و تگِ DOB first, then ts — یعنی همان secondary sort: اول رکوردِ تاریخِ تولد، بعد رویدادها به ترتیبِ timestamp.
فلشِ خروجی «one user's data» با توضیحِ «secondary sort» به
Join Reducer (زیرنویس: sort-merge join، تگِ
one user in RAM) میرود.
خروجیِ join با فلشِ «joined pairs» (توضیح: URL + DOB)
به jobِ دوم میرود: URL Shuffle (زیرنویس: group by URL،
تگِ job 2 starts here)، بعد «grouped views» به
Age Counter (زیرنویس: per-URL counters، تگِ
increment per view)، و بالاخره فلشِ خطچینِ «age counts»
به Page Age Stats (زیرنویس: age distribution، تگِ
output dataset). یعنی نیمهٔ راستِ نمودار همان jobِ دومِ متن است: shuffle با کلیدِ URL
و شمارندهی بهازای هر گروهِ سنی.
شش کارتِ نمودار، شش درس میدهند: «Join Key: user_id» (هر دو mapper شناسهٔ کاربر را
بهعنوانِ کلیدِ shuffle منتشر میکنند و join یک پیمایشِ گروهی روی همین کلید است)؛
«Why Secondary Sort» (ترتیبِ مرتبسازی رکوردِ DOB را قبل از بازدیدها میگذارد؛
reducer یک عبورِ stream دارد بدونِ اسکنِ مجدد یا بافر)؛ «Sort-Merge Join vs Hash Join»
(hash join باید یک طرف را کامل در جدولِ hash نگه دارد، ولی خروجیِ shuffle از قبل مرتب است پس ادغام رایگان است)؛
«Memory Footprint» (فقط یک رکوردِ کاربر در حافظه است و lookupِ شبکهای نداریم)؛
«Star Schema Roles» (کلیکاستریم = fact، پروفایلها = dimension، و join دستهای star را برای تحلیل denormalize میکند)؛
و «Second Job: Group By» (جفتهای (URL, DOB) دوباره با کلیدِ URL shuffle میشوند و هر URL یک شمارنده بهازای هر گروهِ سنی نگه میدارد).
This dataflow has five columns — Sharded Inputs, Map Phase, Shuffle + Sort, Join Reduce and Group-By Job — with two input boxes: Clickstream (“fact table”, “user_id, ts, URL”) and User Profiles (“dimension table”, “user_id, DOB”) — exactly the text's star-schema roles. The “clickstream” arrow reaches the Event Mapper (“key: user_id”, “URL + timestamp”) and “profile records” reaches the Profile Mapper (“key: user_id”, “date of birth”).
The two bold arrows — “mapped events” and “mapped profiles” — reach the diagram's heart: User-ID Shuffle (“group by user_id”, “DOB first, then ts”) — the secondary sort: birth-date record first, then timestamp-ordered events. Its “one user's data” output (“secondary sort”) runs to the Join Reducer (“sort-merge join”, “one user in RAM”). The join's “joined pairs” output (“URL + DOB”) enters job two: URL Shuffle (“group by URL”, “job 2 starts here”), then “grouped views” to the Age Counter (“per-URL counters”, “increment per view”), and finally the dashed “age counts” to Page Age Stats (“age distribution”, “output dataset”) — the right half is the text's second job: URL-keyed shuffle with a per-age-group counter. The six cards teach six lessons: the join key is user_id; why secondary sort (DOB record before the visits; the reducer makes one streaming pass with no rescan or buffering); sort-merge vs hash join (hash join must hold one whole side in a hash table, but shuffle output arrives pre-sorted so merging is free); memory footprint (one user record in memory, no network lookups); star-schema roles (clickstream = fact, profiles = dimension, and the batch join denormalizes the star for analysis); and the second job's group by ((URL, DOB) pairs reshuffle by URL key with one counter per age group per URL).
روایت مدرس — نمودار ۹ را با صدای بلند بخوانیمTeacher narration
مثال فنی برای خواندنِ نمودار: Sort-merge join روی دو جدول بزرگ
وضعیت اولیه: فروش و مشتری هر دو بر customer_id تقسیم و مرتب شدهاند.
اجرای مرحلهبهمرحله: دو جریان مرتب همقدم خوانده میشوند؛ همهٔ فروشهای کلید ۱۷ کنار ردیف مشتری ۱۷ قرار میگیرند.
نتیجه و دلیل: جدول hashِ کامل لازم نیست، ولی هزینهٔ sort/shuffle و کلیدهای بسیار پرتکرار هنوز وجود دارد.
این نمودارِ ساختاری (شکلهای ۱۱-۲ و ۱۱-۳) سه ناحیه دارد و زیرنویسش میگوید:
«left: the fact and dimension tables · middle: the shuffle gathers one user's records · right: the reducer and the next job».
از چپ شروع کن: جدولِ اول «activity events (clickstream)» است با ستونهای
user_id url timestamp و چند سطرِ نمونه:
42 /news 10-01 09:12، 7 /blog 10-01 11:40،
42 /docs 10-01 10:05، 42 /news 10-02 08:20 — و یادداشتِ زیرش:
«fact table of the star schema — sharded into multiple files, so many mappers process it in parallel».
جدولِ دوم «user profiles (dimension)» با ستونهای
user_id date_of_birth و سطرهای 42 1990-07-14 و
7 1985-02-03؛ یادداشت: «dimension table of the star schema — also sharded, read row by row by its own mapper».
زیرِ جدولها دو mapper نشسته: «mapper over events: emit (key=user_id, value=url)» و
«mapper over profiles: emit (key=user_id, value=DOB)» — دقیقاً قراردادِ map.
وسطِ نمودار، «shuffle by user_id — sorted reducer input for user_id = 42» است:
یک ستون از رکوردهای مرتب: اول «(42, dob=1990-07-14)» با برچسبِ
profile، بعد سه رویداد: «(42, url=/news, 10-01 09:12)»،
«(42, url=/docs, 10-01 10:05)» و «(42, url=/news, 10-02 08:20)» —
و یادداشتِ کنارش: «secondary sort: profile record first, then the user's events in timestamp order».
سمتِ راست، جعبهی «reducer — sort-merge join» با کدِ کوتاه است:
یعنی مقدارِ اول = تاریخِ تولد (همان رکوردِ پروفایل)، بعد پیمایشِ رویدادهای مرتبشده و انتشارِ
(url, تاریخِ تولد) بهازای هر بازدید. زیرِ کد، یادداشتِ «Why it is a sort-merge join»:
«one user's record in memory at a time; no network calls during the join; mapper output is sorted by key; reducers merge the sorted lists from both sides».
و نوارِ پایینی نمودار، jobِ بعدی را نشان میدهد: «next MapReduce job — group by + aggregation»
با «shuffle by url: the reducer sees all page views for one URL»،
«it keeps a counter of views per age group, one URL at a time»،
خروجیِ «emit (url, age_group, view_count)» و جمعبندیِ
«group by + aggregation — viewer age comes from date_of_birth».
و راهنمای پایینِ تصویر را ببین: «pill = pipeline stage · box = record or code · dashed = why the join works · arrow = data flow».
درسِ این نمودار این است که joinِ «وحشتناک» بهظاهر، فقط یک پیمایشِ خطی روی ورودیِ مرتب است:
بهجای lookupِ شبکهای بهازای هر رکورد، داده را طوری میچینیم که هرآنچه لازم است کنارِ هم برسد.
dob = first value # the profile record
for e in events: # sorted by timestamp
emit(e.url, dob)
out: (url, date_of_birth) per page view
This structural diagram (Figures 11-2 and 11-3) has three zones: “left: the fact and dimension tables · middle: the shuffle gathers one user's records · right: the reducer and the next job”. Left: the “activity events (clickstream)” table with user_id url timestamp columns and sample rows (42 /news 10-01 09:12, 7 /blog 10-01 11:40, 42 /docs 10-01 10:05, 42 /news 10-02 08:20) — “fact table of the star schema — sharded into multiple files, so many mappers process it in parallel” — plus the “user profiles (dimension)” table (user_id date_of_birth; rows 42 1990-07-14, 7 1985-02-03) — “dimension table of the star schema — also sharded, read row by row by its own mapper”. Below the tables sit the two mappers: “mapper over events: emit (key=user_id, value=url)” and “mapper over profiles: emit (key=user_id, value=DOB)” — exactly the map contract.
Middle: “shuffle by user_id — sorted reducer input for user_id = 42”: one sorted column — first “(42, dob=1990-07-14)” (the profile), then three events in timestamp order — with the note “secondary sort: profile record first, then the user's events in timestamp order”. Right: the “reducer — sort-merge join” box with its short pseudocode (first value = birth date; walk the sorted events emitting (url, birth date) per visit), the “Why it is a sort-merge join” note (one user's record in memory at a time; no network calls during the join; mapper output sorted by key; reducers merge both sides' sorted lists), and the bottom strip with the next job — “next MapReduce job — group by + aggregation” (shuffle by url so the reducer sees one URL's every view; one view counter per age group, one URL at a time; emit (url, age_group, view_count)). The legend reads “pill = pipeline stage · box = record or code · dashed = why the join works · arrow = data flow”. The diagram's moral: the “terrifying” join is just a linear walk over sorted input — instead of a network lookup per record, data is arranged so everything needed arrives together.
۴.۵. زبانهای کوئری4.5. Query languages
زبانهایی مثل SQL نتیجهٔ موردنیاز را بیان میکنند و بهینهساز روشِ اجرا را انتخاب میکند. انتخابِ طرح اجرا میتواند مقدارِ دادهٔ خواندهشده یا جابهجاشده را بسیار تغییر دهد.
مثال فنی: Optimizer چه تصمیمی میگیرد؟
وضعیت اولیه: SQL فروش را به کالا join و فقط دستهٔ book را نگه میدارد.
اجرای مرحلهبهمرحله: optimizer میتواند فیلترِ دسته را زودتر اعمال و با آمار اندازه، نوع join را انتخاب کند.
نتیجه و دلیل: اعلانی بودن مسیر بهینه را قابلانتخاب میکند؛ آمار قدیمی یا skew میتواند طرح نامناسب بسازد.
Distributed execution engines matured: infrastructure is now strong enough to store and process tens of petabytes on 10,000+-machine clusters. With operations solved, focus moved to the programming model. MapReduce, dataflow engines and cloud warehouses all adopted SQL as batch processing's lingua franca — a natural pick: old warehouses had SQL, analytics and ETL tools already supported it, and every developer and analyst knows it.
-
تعاملیبودن: بهجای jobهای MapReduceِ دستنویسِ طولانی، کوئریِ کوتاه مینویسی و از terminal یا GUI اجرا میکنی.
این سبکِ کوئریِ تعاملی برای تحلیلگرانِ کسبوکار، مدیرانِ محصول، تیمهای فروش و مالی راهِ طبیعیِ کاوش در داده است؛
و SQL، سیستمهای دستهای را برای کوئریهای اکتشافی هم مناسب کرده.
-
بهینهسازیِ ماشینی: زبانِ سطحبالا فقط انسانها را بهرهورتر نمیکند؛ کاراییِ اجرا را هم بهتر میکند.
موتورِ کوئری، SQL را به درختِ syntax و بعد operatorهای فیزیکی تبدیل میکند و همین ترجمه جای
optimizer مبتنی بر cost است: Hive، Trino، Spark و Flink میتوانند خواصِ ورودیهای join را تحلیل و
مناسبترین الگوریتم را انتخاب کنند — حتی ترتیبِ joinها را عوض کنند تا state میانی کمینه شود.
-
Interactivity: instead of long hand-written MapReduce jobs, write a short query and run it from a terminal or GUI. This interactive query style is the natural way for business analysts, product managers, sales and finance teams to explore data; and SQL made batch systems fit exploratory queries too.
-
Machine optimization: a high-level language doesn't just make humans more productive; it executes more efficiently too. The query engine turns SQL into a syntax tree and then physical operators, and that translation is where the cost-based optimizer lives: Hive, Trino, Spark and Flink can analyze join inputs' properties and pick the fittest algorithm — even reorder joins to minimize intermediate state.
البته SQL تنها زبانِ عمومی نیست و زبانهای دیگر جای نیچ دارند:
Apache Pig زبانی مبتنی بر operatorهای رابطهای بود که pipeline را
گامبهگام تعریف میکرد، نه بهشکلِ یک کوئریِ بزرگِ SQL — Morel زبانِ مدرنتری
است که از Pig الهام گرفته؛ DataFrameها (بخشِ بعد) هم ویژگیِ مشابهی دارند.
بعضیها سراغِ زبانهای کوئریِ JSON رفتهاند: jq، JMESPath،
JSONPath. و برای دادههای گرافی (فصلِ مدلهای داده)، frameworkهای پردازشِ گراف از
زبانهایی مثل Gremlin (Apache TinkerPop) برای محاسبهٔ دستهای پشتیبانی میکنند.
But SQL isn't the only general language, and other languages hold niches: Apache Pig was a relational-operator language defining the pipeline step by step instead of one big SQL query — Morel is a more modern Pig-inspired language; DataFrames (next section) share a similar trait. Some turned to JSON query languages: jq, JMESPath, JSONPath. And for graph data (the data-models chapter), graph-processing frameworks support languages like Gremlin (Apache TinkerPop) for batch computation.
۴.۶. همگرایی با cloud data warehouse4.6. Convergence with the cloud data warehouse
انبارهای داده و چارچوبهای پردازش، قابلیتهای مشترک بیشتری پیدا کردهاند: ذخیرهسازی جدا، SQL و اجرای موازی. با این حال محدودیتها و هزینهٔ عملیاتیِ ابزارها همچنان متفاوت است.
مثال فنی: انبار ابری هم پردازش توزیعشده دارد
وضعیت اولیه: query روی صدها فایل ستونی، فروش روزانه را جمع میزند.
اجرای مرحلهبهمرحله: سرویس، اسکن فایلها، shuffle و aggregation را بین workerها تقسیم میکند و منابع اجرا را مدیریت میکند.
نتیجه و دلیل: رابط SQL بسیاری از جزئیات را پنهان میکند؛ هزینهٔ اسکن و جابهجایی داده حذف نشده است.
Historically two separate streams existed: data warehouses on dedicated hardware running analytical SQL over relational data; and batch frameworks like MapReduce arriving for bigger scale and flexibility — with logic in general-purpose languages and reads/writes of arbitrary formats. Over time the two grew very alike:
- Frameworkهای دستهایِ مدرن از SQL پشتیبانی میکنند و با قالبهای ستونیِ مثل
Parquet و موتورهای اجرایِ بهینهشده، روی کوئریهای رابطهای هم عملکردِ خوب میگیرند.
- Warehouseها با ابریشدن مقیاسپذیر شدند و همان تکنیکهای scheduling، تحملِ خرابی و shuffle را
پیاده کردند که frameworkهای دستهای داشتند؛ بسیاری از آنها از فایلسیستمِ توزیعشده هم استفاده میکنند.
- و برعکس: warehouseهای ابری مدلهای پردازشِ جایگزین را هم گرفتند — BigQuery کتابخانهی
DataFrames دارد و Snowpark در Snowflake با Pandas یکپارچه میشود.
ابزارهای orchestrations مثل Airflow و Prefect و Dagster هم به warehouseهای ابری وصل میشوند.
- Modern batch frameworks support SQL and, with columnar formats like Parquet and optimized execution engines, perform well on relational queries too.
- Warehouses became scalable in the cloud, implementing the same scheduling, fault-tolerance and shuffle techniques batch frameworks had; many also use distributed filesystems.
- And vice versa: cloud warehouses adopted alternative processing models — BigQuery has a DataFrames library and Snowpark in Snowflake integrates with Pandas. Orchestration tools like Airflow, Prefect and Dagster also plug into cloud warehouses.
اما همهی jobهای دستهای بهسادگی در SQL بیان نمیشوند: الگوریتمهای تکراریِ گراف مثل
PageRank، کارهای پیچیدهٔ ML، و پردازشِ دادههای غیررابطهای و multimodalِ AI
(تصویر، ویدیو، صدا). Warehouseهای ابری در بعضی بارها هم بهدرد نمیخورند: محاسبهی
سطربهسطر روی فرمتهای ستونی ناکارآمد است (آنجا API دیگرِ warehouse یا سیستمِ دستهای بهتر است)،
و معمولاً گرانترند — برای jobهای بزرگ اجرای Spark یا Flink میتواند بهصرفهتر باشد.
معیارِ نهاییِ انتخاب: هزینه، راحتی، سادگیِ پیادهسازی و در دسترس بودن. شرکتهای بزرگ معمولاً
چند سیستمِ پردازشی دارند (و انعطافِ انتخاب)، شرکتهای کوچکتر اغلب با یکی کارشان راه میافتد.
But not every batch job expresses easily in SQL: iterative graph algorithms like PageRank, complex ML work, and non-relational, multimodal AI data processing (images, video, audio). Cloud warehouses also misfit some loads: row-by-row computation over columnar formats is inefficient (there another warehouse API or a batch system does better), and they are usually pricier — running Spark or Flink can be cheaper for big jobs. The final selection metric: cost, comfort, implementation simplicity and skill availability. Big companies usually run several processing systems (and the flexibility to choose); smaller ones often get by with one.
| بُعد | موتورِ dataflow (Spark / Flink) | cloud data warehouse (BigQuery / Snowflake) |
| مدلِ برنامهنویسی | API برنامهنویسی، SQL، DataFrame — منطقِ عمومیِ زبانِ برنامهنویسی | SQL بهعنوانِ هسته + کتابخانههای DataFrame/Snowpark |
| قالبِ داده | هر قالبی (Parquet، Avro، JSON، …) روی DFS/object store | معمولاً جدولهای داخلیِ خودش؛ گاهی اتصال به object store |
| الگوریتمهای تکراری (مثل PageRank) | مناسب (API سطحپایین + حلقه) | بهسختی بیان میشود |
| دادهی multimodal (صوت/تصویر) | قابلِ پردازش با کدِ سفارشی | دشوار یا پشتیبانینشده |
| محاسبهی سطری روی دادهی ستونی | موتورِ اجرای منعطف | ناکارآمد — API دیگر یا جایگزین لازم است |
| هزینه | برای jobهای بزرگ اغلب ارزانتر (خودمدیریتی) | راحت ولی معمولاً گرانتر |
| عملیات | خودت خوشه را اداره میکنی (یا managed service) | کاملاً managed؛ بدونِ نگهداریِ خوشه |
| معیارِ انتخاب | هزینه، راحتی، سادگیِ پیادهسازی، در دسترس بودنِ مهارت — و بزرگترها اغلب هر دو را دارند |
| Dimension | Dataflow engine (Spark / Flink) | Cloud data warehouse (BigQuery / Snowflake) |
| Programming model | Programming API, SQL, DataFrames — general language logic | SQL as the core + DataFrame/Snowpark libraries |
| Data format | Any format (Parquet, Avro, JSON, …) on DFS/object store | Usually its own internal tables; sometimes object-store links |
| Iterative algorithms (e.g. PageRank) | Suitable (low-level API + loops) | Hard to express |
| Multimodal data (audio/video) | Processable with custom code | Difficult or unsupported |
| Row-wise compute on columnar data | Flexible execution engine | Inefficient — another API or replacement needed |
| Cost | Often cheaper for big jobs (self-managed) | Comfortable but usually pricier |
| Operations | You run the cluster yourself (or a managed service) | Fully managed; no cluster keeping |
| Selection metric | Cost, comfort, implementation simplicity, skill availability — and bigger shops often keep both |
۴.۷. DataFrameها4.7. DataFrames
DataFrame نمایی جدولی برای تبدیل داده است. در اجرای توزیعشده، ترتیب ردیفها و زمانِ واقعیِ اجرای عملیات ممکن است با جدولِ داخل حافظهٔ یک برنامه فرق داشته باشد.
مثال فنی: DataFrame محلی یا توزیعشده
وضعیت اولیه: دستور df.filter(...).groupBy(...).sum() نوشته شده است.
اجرای مرحلهبهمرحله: در موتور توزیعشده ممکن است فقط plan ساخته شود و action اجرا را شروع کند؛ collect() تمام خروجی را به driver میآورد.
نتیجه و دلیل: ظاهراً مشابه بودن API با جدول محلی بهمعنی یکسان بودن هزینه نیست؛ خروجی بزرگ میتواند حافظهٔ driver را پر کند.
Data scientists and statisticians usually live in the DataFrame data model of R and Pandas: something like a relational table — a set of rows with same-typed values per column — except instead of one big SQL query you call relational-operator functions: filter, join, sort, aggregation and the rest.
ابتدا دستکاریِ DataFrameها بهصورتِ محلی و درونِ حافظه بود و محدود به datasetهایی که در یک ماشین جا میشدند.
دانشمندانِ داده میخواستند با همان API آشنا به datasetهای بزرگِ محیطهای دستهای دسترسی داشته باشند
(چون SQL و MapReduce برای نیازهایشان مناسب نبود)؛ پس frameworkهای توزیعشده مثل
Spark و Flink و Daft
APIهای DataFrame را پذیرفتند. ولی پیادهسازی کمی متفاوت رفتار میکند و چند تفاوتِ semantic مهم هست:
DataFrame manipulation started local and in-memory, limited to datasets fitting one machine. Data scientists wanted the same familiar API on batch environments' big datasets (SQL and MapReduce didn't fit their needs); so distributed frameworks like Spark, Flink and Daft adopted DataFrame APIs. But the implementation behaves a bit differently, with a few important semantic differences:
-
DataFrameهای محلی معمولاً ایندکسشده و مرتباند؛ DataFrameهای توزیعشده معمولاً نه.
مهاجرت به frameworkهای دستهای میتواند بهخاطرِ همین، «غافلگیریِ عملکرد» بیاورد — کد اجرا میشود
ولی نتیجه یا ترتیب، آن چیزی نیست که انتظار داشتی.
-
eager در برابر lazy: Pandas وقتی متدِ DataFrame صدا زده میشود، فوراً اجرا میکند؛
Spark اول همهی فراخوانیهای API را به یک query plan ترجمه میکند، بهینهسازی را اجرا میکند،
و بعد workflow را روی موتورِ dataflowِ توزیعشدهاش میدَوَراند.
-
client/server: frameworkهایی مثل Daft هر دو را دارند: عملیاتِ کوچکِ درونحافظهای روی
client اجرا میشوند و datasetهای بزرگ روی server. قالبهای ستونیِ مثل
Apache Arrow یک مدلِ دادهی مشترک میدهند که هم موتورِ اجرایِ سمتِ client
و هم سمتِ server میتوانند به اشتراک بگذارند.
-
Local DataFrames are usually indexed and ordered; distributed DataFrames usually aren't.
Migrating to batch frameworks can therefore bring “performance surprises” — code runs, but results or ordering aren't what you expected.
-
Eager vs lazy: Pandas executes the moment a DataFrame method is called; Spark first translates all API calls into a query plan, runs optimization, then executes the workflow on its distributed dataflow engine.
-
Client/server: frameworks like Daft have both: small in-memory ops run on the client, big datasets on the server. Columnar formats like Apache Arrow give a shared data model both the client-side and server-side engines can share.
۴.۸. نقشهٔ مفهومیِ مدلهای پردازش4.8. The processing-models concept map
هفت زیربخشِ قبلی را یکییکی جلو رفتیم: از MapReduce و
shuffle تا موتورهای dataflow، زبانهای کوئری و
DataFrame. بد نیست یک بار دیگر همه را در یک تصویر ببینیم — نه بهشکلِ
«مسیرِ یک درخواست»، بلکه بهشکلِ نمودار کلاسی: چه مفهومی زیرمجموعهی چه مفهومی است،
چه چیزی به چه چیزی وابسته است، و کدام ایدهها بینِ چند مدل مشترکاند.
We walked the previous seven subsections one by one: from MapReduce and shuffle to dataflow engines, query languages and DataFrames. Worth seeing them all once more in one picture — not as “one request's path” but as a class diagram: which concept subclasses which, what depends on what, and which ideas are shared across models.
پیامِ اصلیِ نقشه این است: آنچه در بازار «پردازندهی دستهای» نامیده میشود، چند فناوریِ بیارتباط نیست؛
یک پشته است. بالای پشته، مدلهای برنامهنویسی قرار دارند (لولههای Unix، MapReduce،
dataflow، SQL، DataFrame) که فقط شیوهی بیانِ محاسبه را تعیین میکنند. وسطِ پشته، موتورِ اجرا و
Shuffle است. و زیرِ آن، Storage و
Orchestrator قرار دارند که از مدلِ برنامهنویسی جدایند و با protocolهای
قابلِ تعویض به هم وصل میشوند — همان ایدهای که در «سیستمعاملِ توزیعشده» دیدیم.
The map's core message: what the market calls “batch processors” isn't several unrelated technologies; it is a stack. Atop the stack sit the programming models (Unix pipes, MapReduce, dataflow, SQL, DataFrames) which only set how computation is expressed. Mid-stack sit the execution engine and Shuffle. And below, Storage and the Orchestrator, separate from the programming model and joined by swappable protocols — the same idea as the “distributed operating system”.
روایت مدرس — نمودار ۱۰ را با صدای بلند بخوانیمTeacher narration
مثال فنی برای خواندنِ نمودار: یک محاسبه، چند مدل اجرا
وضعیت اولیه: ورودی فروش باید فیلتر، با کالا join و بر دسته تجمیع شود.
اجرای مرحلهبهمرحله: میتوان آن را به jobهای MapReduce، DAG عملگرها یا SQL روی موتور توزیعشده بیان کرد.
نتیجه و دلیل: اختلاف در کنترل و بهینهسازی اجراست؛ برای مقایسه باید همان semanticsِ join و null و نوع عدد حفظ شوند.
این نمودار یک نمودار کلاسِ UML است با ۱۳ جعبه، که با سه chip شمارهگذاری شدهاند:
«1 · processing models» بالای صفحه، «2 · storage»
پایینچپ و «3 · orchestration» پایینراست. کنارِ صفحه هم legend سه نوع پیکان را
جدا میکند: inheritance (مثلثِ توخالی)، association (پیکانِ ساده) و
«compile to» (پیکانِ خطچین).
در خوشهی ۱، جعبهی مادر ProcessingModel است با توضیحِ
«abstract · bounded input → derived output» و دو خصیصه:
input : immutable dataset, read once و
output : derived dataset · terminates — دقیقاً همان تعریفِ فصل.
سه فرزند با پیکانِ inheritance به او وصلاند:
UnixToolPipeline («sort-based · single machine»، لولههای stdin/stdout)،
MapReduce («map, implicit sort, reduce»، I/O فایلی بین stageها، بازتلاشِ per-task)
و DataflowEngine («whole workflow = one job · operators fused»، pipelined،
دادهی میانی در حافظه).
کنارِ اینها دو جعبهی دیگر هست که فرزند نیستند؛ با پیکانِ خطچینِ «compile to» به
DataflowEngine وصلاند: SqlQueryEngine
(«SQL → physical plan»، optimizer مبتنی بر هزینه، ترتیبِ join عوض میشود) و
DataFrameApi («relational ops as methods»، API سبکِ Pandas روی موتورِ توزیعشده).
یعنی این دو «زبان»اند که به لایهی اجرا کامپایل میشوند، نه مدلِ اجرایِ جدا.
جعبهی چهارمِ خوشهی ۱، Shuffle است («shared helper · repartition»،
hash(key) → one sorted file per reducer · merge-sorted) که دو پیکان به آن
برمیگردد: MapReduce «always uses» و DataflowEngine
«uses only where needed». این همان درسِ بخشِ ۴.۲ است: مرتبسازی فقط جایی که لازم است.
خوشهی ۲ از جعبهی مادرِ Storage شروع میشود («pluggable read/write protocol»،
«S3 API is the de-facto standard») با دو فرزندِ DistributedFS
(بلوکهای بزرگِ ۱۲۸MB روی data nodeها، metadata service، replication یا erasure coding، اشارهی locality)
و ObjectStore (bucket + key، آبجکتهای immutable، بدونِ lock و بدونِ rename اتمیک،
جداییِ storage و compute). یک پیکانِ association از ProcessingModel به
Storage میرود با برچسبِ «reads input / writes output through pluggable S3 API».
و خوشهی ۳: جعبهی مادرِ Orchestrator («abstract · runs batch jobs»،
تصمیمِ «کجا و کی» اجرا شود) با دو فرزندِ JobScheduler («one job»،
task executor + resource manager، FIFO/DRF/priority/quotas) و WorkflowScheduler
(«DAG of jobs»، منتظرِ خروجیِ upstream، سبکِ Airflow). دو association جعبهها را به هم میدوزند:
«submits jobs to» از WorkflowScheduler به JobScheduler، و «runs under» از ProcessingModel به Orchestrator.
حالا یک قدم به عقب برگرد و شکلِ کلی را ببین: سه ستونِ عمودی — مدل، storage، orchestration — که فقط با
چند پیکانِ کمتعداد به هم وصلاند. اگر هر مربع را یک «تصمیمِ معماری» بگیریم، میبینی که انتخابِ
مدلِ برنامهنویسی، انتخابِ storage و انتخابِ orchestrator مستقل از هماند؛ و این دقیقاً همان
چیزی است که سیستمهای دستهی مدرن را قابلِ ترکیب میکند.
This UML class diagram holds 13 boxes under three numbered chips: “1 · processing models” at top, “2 · storage” bottom-left and “3 · orchestration” bottom-right, with a legend separating three arrow kinds: inheritance (hollow triangle), association (plain arrow) and “compile to” (dashed arrow). In cluster 1 the parent box ProcessingModel reads “abstract · bounded input → derived output” with two attributes — input : immutable dataset, read once and output : derived dataset · terminates — exactly the chapter's definition — with three inheritance children: UnixToolPipeline (“sort-based · single machine”, stdin/stdout pipes), MapReduce (“map, implicit sort, reduce”; file I/O between stages; per-task retry) and DataflowEngine (“whole workflow = one job · operators fused”; pipelined; in-memory intermediate data).
Beside them sit two more boxes that are not children; dashed “compile to” arrows join them to DataflowEngine: SqlQueryEngine (“SQL → physical plan”; cost-based optimizer; join order changes) and DataFrameApi (“relational ops as methods”; Pandas-style API on a distributed engine). These two are “languages” that compile to the execution layer, not separate execution models. Cluster 1's fourth box, Shuffle (“shared helper · repartition”; hash(key) → one sorted file per reducer · merge-sorted), takes two inbound arrows: MapReduce “always uses”, DataflowEngine “uses only where needed” — Section 4.2's lesson: sort only where needed.
Cluster 2 starts at the Storage parent (“pluggable read/write protocol”; “S3 API is the de-facto standard”) with two children — DistributedFS (big 128MB blocks on data nodes, a metadata service, replication or erasure coding, the locality note) and ObjectStore (bucket + key, immutable objects, no locks, no atomic rename, storage/compute separation) — plus an association from ProcessingModel to Storage (“reads input / writes output through pluggable S3 API”). Cluster 3: the Orchestrator parent (“abstract · runs batch jobs”; the “where and when” decider) with JobScheduler (“one job”; task executor + resource manager; FIFO/DRF/priority/quotas) and WorkflowScheduler (“DAG of jobs”; waits for upstream output; Airflow-style) children, stitched by two associations: “submits jobs to” (WorkflowScheduler → JobScheduler) and “runs under” (ProcessingModel → Orchestrator). Step back and see the overall shape: three vertical columns — model, storage, orchestration — joined by only a few arrows. Reading each square as one “architecture decision”, the programming-model, storage and orchestrator choices are independent — exactly what makes modern batch systems composable.
درسِ جمعبندیِ بخش ۴Section 4 takeaway
اگر از تو در مصاحبه بپرسند «MapReduce، Spark و SQL چه ربطی به هم دارند؟»، جوابِ این نقشه است:
SQL و DataFrame به موتورِ dataflow کامپایل میشوند؛ خودِ موتورِ dataflow تعمیمِ MapReduce است
(با اپراتورهای قابلِ ترکیب و بدونِ I/O فایلیِ اجباری بینِ هر مرحله)؛ و همهی اینها روی storage و
orchestratorِ قابلِ تعویض مینشینند. انتخابِ درست هم یعنی تطبیقِ مدل با بارِ کاری، نه دنبالِ
«بهترین ابزارِ مطلق» رفتن.
If an interviewer asks “how do MapReduce, Spark and SQL relate?”, this map is the answer: SQL and DataFrames compile to the dataflow engine; the dataflow engine itself generalizes MapReduce (composable operators, no forced file I/O between stages); and all of it sits on swappable storage and orchestration. And the right choice means matching the model to the workload, not chasing the “absolute best tool”.
۵5 کاربردهاApplications
پردازش دستهای برای کارِ انبوه و تکرارپذیر مناسب است: تبدیل داده، تحلیل و ساختِ خروجیهای مشتقشده. معمولاً باید میان زمانِ آمادهشدن نتیجه و هزینهٔ پردازش تعادل بگذاریم.
مثال فنی: سه خروجی از ورودی مشترک
وضعیت اولیه: از فایل سفارش، گزارش مالی، ویژگی مدل و ایندکس جستوجو ساخته میشوند.
اجرای مرحلهبهمرحله: هر job نسخهٔ ورودی و کدِ خودش را ثبت میکند و خروجی جدا میسازد.
نتیجه و دلیل: شکستِ ساخت یک خروجی نباید مرجع را خراب کند؛ رابطهٔ نسخهٔ داده و محاسبه امکان تطبیق و تکرار را میدهد.
Now that we know the mechanism, see where it is used in practice. Batch jobs excel at crunching massive datasets but misfit low latency; so you find them wherever data is plenty and data freshness doesn't matter. It may sound limited, but a large share of data processing fits this model: accounting and warehouse reconciliation, demand forecasting in manufacturing, recommender-model training in e-commerce and media, and many financial systems (the US banking network is almost entirely batch).
۵.۱. ETL5.1. ETL
در ETL، داده استخراج و تبدیل میشود و سپس در مقصد قرار میگیرد. تبدیل باید قواعد کیفیت و معنای فیلدها را هم پوشش دهد، نه فقط تغییر قالبِ فایل.
مثال فنی: پاکسازی پیش از بارگذاری
وضعیت اولیه: فایل فروش ردیف تکراری با order_id=42 و مبلغ با دو واحد دارد.
اجرای مرحلهبهمرحله: job با کلید مناسب deduplicate و واحدها را یکسان میکند؛ رکورد نامعتبر به مسیر خطا میرود.
نتیجه و دلیل: ETL فقط جابهجایی فایل نیست؛ قواعد تکرار و واحد در درستی گزارش نقش دارند.
ETL (Extract–Transform–Load) and ELT mean a data-processing pipeline extracting data from the operational database, transforming it and loading the result into the downstream system (usually a warehouse). Batch jobs are perfect for this workload, especially with a warehouse downstream.
-
embarrassingly parallel: ماهیتِ موازیِ jobهای دستهای برای تبدیلِ داده عالی است؛ بیشترِ تبدیلهای رایجِ
warehouse — فیلتر کردن، project کردنِ فیلدها — کارهاییاند که بهراحتی موازی میشوند.
-
workflow schedulerها: محیطهای دستهای schedulerهای workflowِ قوی دارند که زمانبندی و اشکالزداییِ
pipelineها را ساده میکنند. هنگامِ خطا، schedulerها اغلب job را بازتلاش میکنند تا مشکلاتِ گذرا جبران شوند؛
jobی که مدام شکست بخورد بهعنوانِ failed علامت میخورد و توسعهدهنده راحت میبیند کدام jobِ pipeline ایستاده.
Airflow حتی operatorهای آماده برای MySQL، PostgreSQL، Snowflake، Spark، Flink و دهها سیستمِ دیگر دارد.
-
دیباگپذیری: وقتی اوضاع خراب میشود، jobهای دستهای راحت عیبیابی و ترمیم میشوند: فایلهای خراب را
میتوان بازرسی کرد تا معلوم شود چه شده؛ مثلاً اگر ورودی فیلدِ موردِ نیازِ تبدیل را نداشته باشد،
مهندسِ داده راحت میبیند فیلد غایب است و منطقِ تبدیل یا jobِ تولیدکننده را درست میکند.
-
مالکیتِ توزیعشده: قبلاً pipelineهای داده را یک تیمِ واحدِ مهندسیِ داده اداره میکرد؛
حالا بهبودِ مدلهای پردازش و مدیریتِ متادیتا باعث شده مهندسانِ هر تیمی بتوانند pipelineِ خودشان را بنویسند.
رویههایی مثل data mesh و data contract و
data fabric استانداردها و ابزارهایی میدهند تا تیمها بتوانند دادهشان را
با امنیت برای مصرفِ بقیهٔ سازمان منتشر کنند.
-
مشترکشدنِ موتورِ اجرا: pipelineهای داده و کوئریهای تحلیلی حالا نهتنها مدلِ پردازش،
بلکه موتورِ اجرا را هم مشترک دارند — دیدنِ تبدیلهای pipeline و کوئریهای تحلیلی بهشکلِ کوئریهای
SparkSQL یا Trino یا DuckDB
عادی است؛ و این مرزِ بینِ مهندسیِ اپلیکیشن، مهندسیِ داده، مهندسیِ تحلیل و تحلیلِ کسبوکار را بیشتر محو میکند.
-
Embarrassingly parallel: batch jobs' parallel nature suits data transformation perfectly; most common warehouse transforms — filtering, projecting fields — parallelize easily.
-
Workflow schedulers: batch environments have strong workflow schedulers simplifying pipeline scheduling and debugging. On error, schedulers often retry the job to absorb transient faults; a persistently failing job is marked failed so developers easily see which pipeline job stalled. Airflow even ships ready operators for MySQL, PostgreSQL, Snowflake, Spark, Flink and dozens of other systems.
-
Debuggability: when things break, batch jobs are easy to diagnose and repair: broken files can be inspected to learn what happened; e.g. if input lacks a transform's needed field, the data engineer plainly sees the missing field and fixes the transform logic or the producing job.
-
Distributed ownership: data pipelines used to be run by one data-engineering team; improved processing models and metadata management now let each team's engineers write their own pipelines. Practices like data mesh, data contracts and data fabric give standards and tooling for teams to publish their data securely for the rest of the organization.
-
Shared execution engine: data pipelines and analytical queries now share not just the processing model but the execution engine too — seeing pipeline transforms and analytical queries as SparkSQL, Trino or DuckDB queries is ordinary; and this further blurs the line between application, data, analytics engineering and business analysis.
۵.۲. تحلیل5.2. Analytics
تحلیل دستهای با اسکن و تجمیعِ دادهٔ تاریخی، پاسخِ سؤالهای مدیریتی یا پژوهشی را میسازد. اهمیتِ تازگیِ نتیجه باید متناسب با همان سؤال تعیین شود.
مثال فنی: تحلیل روی مجموعهٔ مشخص
وضعیت اولیه: گزارشِ روزانه، سفارشهای تأییدشده تا مرز زمانی مشخص را لازم دارد.
اجرای مرحلهبهمرحله: job همان ورودی نسخهدار را فیلتر و مجموع را محاسبه میکند؛ زمان cutoff در خروجی ثبت میشود.
نتیجه و دلیل: بدون مرز ورودی، دو اجرای ظاهراً یکسان میتوانند صرفاً بهخاطر رسیدن دادهٔ تازه عدد متفاوت بدهند.
Analytical (OLAP) queries often scan huge record counts and group and aggregate. Such a workload can run on the batch system alongside the other batch jobs: the analyst writes SQL, the query engine executes, reading and writing DFS/object store. Table metadata (mapping tables to files, names, types) is managed with a table format like Apache Iceberg and a catalog like Unity — this architecture is called data lakehouse.
کوئریهای تحلیلی روی frameworkهای دستهای معمولاً دو سبک دارند:
Analytical queries on batch frameworks usually take two styles:
-
Pre-aggregation (پیشتجمیع): داده در OLAP cube یا
data mart جمعبندی میشود تا کوئریها سریع شوند. دادهی پیشتجمیعشده
یا در warehouse کوئری میشود یا به سیستمهای OLAPِ بلادرنگِ اختصاصی مثل
Apache Druid یا Apache Pinot فشار داده میشود.
پیشتجمیع معمولاً با فاصلهی زمانیِ مشخص انجام میشود و همان workflow schedulerها مدیریتش میکنند.
-
Ad hoc: کاربر برای جواب به یک سوالِ مشخصِ کسبوکار، بررسیِ رفتارِ کاربر یا اشکالزداییِ عملیاتی اجرا میکند.
اینجا زمانِ پاسخ مهم است: تحلیلگر تکراری کوئری میزند و با هر جواب بیشتر یاد میگیرد؛
frameworkهای دستهای با اجرای سریعِ کوئری، زمانِ انتظار را کم میکنند.
-
Pre-aggregation: data is summarized in an OLAP cube or data mart to speed queries. Pre-aggregated data is either queried in the warehouse or pushed to dedicated real-time OLAP systems like Apache Druid or Apache Pinot. Pre-aggregation usually runs on a fixed cadence, managed by those same workflow schedulers.
-
Ad hoc: run by a user answering a specific business question, studying user behavior or debugging operations. Here response time matters: the analyst queries iteratively, learning more with each answer; batch frameworks cut waiting time by executing queries fast.
و پشتیبانیِ SQL یعنی یکپارچگی با صفحهگستردهها و ابزارهای مصورسازی: Tableau،
Power BI، Looker و Apache Superset —
مثلاً Superset از Trino، Hive، Spark SQL و Presto پشتیبانی میکند که در نهایت jobهای دستهایاند.
And SQL support means integration with spreadsheets and visualization tools: Tableau, Power BI, Looker and Apache Superset — e.g. Superset supports Trino, Hive, Spark SQL and Presto, which are batch jobs underneath.
۵.۳. یادگیری ماشین5.3. Machine learning
پردازش دستهای در آمادهسازی داده، ساخت ویژگی و آموزش مدل استفاده میشود. بازتولیدِ نتیجه به نسخهٔ ورودی، کد و تنظیمات وابسته است.
مثال فنی: ویژگی مدل بدون نشت آینده
وضعیت اولیه: برای پیشبینیِ روز ۱۰، مجموع خریدِ هفت روز قبل لازم است.
اجرای مرحلهبهمرحله: job فقط رخدادهای پیش از روز ۱۰ را join و aggregate میکند؛ خرید روز ۱۱ نباید وارد ویژگیِ آن نمونه شود.
نتیجه و دلیل: pipeline درست باید مرز زمانی را حفظ کند؛ استفاده از snapshot امروز ممکن است اطلاعات آینده را به آموزش نشت دهد.
ML uses batch processing heavily: data scientists and ML/AI engineers discovering data patterns, transforming data and training models. Three common uses:
- Feature engineering: دادهی خام فیلتر و تبدیل میشود تا دادهای شود که مدل را میتوان رویش آموزش داد؛
مدلهای پیشبین معمولاً دادهی عددی میخواهند، پس فرمهای دیگر (متن یا مقادیرِ گسسته) باید تبدیل شوند.
- آموزشِ مدل: دادهی آموزشی ورودیِ پردازشِ دستهای است و وزنهای مدلِ آموزشدیده، خروجی.
- Batch inference: اگر dataset بزرگ باشد و نتیجهی بلادرنگ لازم نباشد، مدلِ آموزشدیده
پیشبینیها را بهصورتِ انبوه انجام میدهد — از جمله ارزیابیِ پیشبینیها روی datasetِ تست.
- Feature engineering: raw data is filtered and transformed into trainable data; predictive models usually want numeric data, so other forms (text or discrete values) must convert.
- Model training: training data is the batch input; the trained model's weights are the output.
- Batch inference: when the dataset is big and no real-time result is needed, the trained model predicts in bulk — including evaluating predictions on the test dataset.
frameworkها ابزارِ اختصاصیِ اینها را دارند: MLlib در Spark و
FlinkML در Flink با مجموعهای از ابزارهای feature engineering، توابعِ آماری و classifierها.
کاربردهایی مثل موتورهای پیشنهاددهنده و رتبهبندی به پردازشِ گراف هم سنگیناند: بسیاری از الگوریتمهای گراف
با پیمایشِ یک یال در هر گام بیان میشوند — یک رأس را به رأسِ مجاور join میکنی تا اطلاعاتی را منتشر کنی،
و تکرار تا رسیدن به شرط (مثلاً تا وقتی یالی برای پیمایش نمانده یا متریک همگرا شود).
مدلِ bulk synchronous parallel (BSP) برای پردازشِ گراف محبوب شده —
در Apache Giraph، APIی GraphX در Spark و
Gelly در Flink — و به مدلِ Pregel هم معروف است.
Frameworks ship dedicated tooling for these: MLlib in Spark and FlinkML in Flink with feature-engineering tools, statistics functions and classifiers. Uses like recommenders and ranking also lean hard on graph processing: many graph algorithms express as traversing one edge per step — joining a vertex to neighbors to propagate information, iterating to a condition (e.g. until no edges remain or a metric converges). The bulk synchronous parallel (BSP) model grew popular for graph processing — in Apache Giraph, Spark's GraphX API and Flink's Gelly — also known as the Pregel model.
پردازشِ دستهای بخشِ جدانشدنیِ آمادهسازی و آموزشِ مدلهای زبانیِ بزرگ (LLM) هم هست. دادهی متنِ خام
(مثل محتوای وبسایتها) معمولاً در DFS/object store است و باید پیشپردازش شود:
استخراجِ متنِ ساده از HTML و اصلاحِ متنِ خراب، تشخیص و حذفِ اسنادِ بیکیفیت/نامرتبط/تکراری،
و tokenize (تقسیم به کلمات) و تبدیل به embedding.
frameworkهایی مثل Kubeflow و Flyte و Ray
برای همین بارها ساخته شدهاند و یکپارچگیِ آماده با PyTorch و TensorFlow و XGBoost دارند
(و پشتیبانیِ داخلی از feature engineering، آموزش، batch inference و fine-tuning).
و بالاخره، دانشمندانِ داده اغلب در notebookهای تعاملی مثل Jupyter یا Hex تجربه میکنند:
سلولهای کوچکِ Markdown/Python/SQL که بهترتیب اجرا میشوند و خروجیشان جدول و نمودار است —
و بسیاری از notebookها از DataFrame API یا SQL بهعنوانِ پردازشِ دستهای استفاده میکنند.
Batch processing is inseparable from preparing and training large language models (LLMs) too. Raw text data (like website contents) usually sits in DFS/object store and must be preprocessed: extracting plain text from HTML and fixing broken text, detecting and dropping low-quality/irrelevant/duplicate documents, and tokenizing (splitting into words) plus embedding. Frameworks like Kubeflow, Flyte and Ray were built for exactly these loads, with ready PyTorch, TensorFlow and XGBoost integration (plus built-in feature engineering, training, batch inference and fine-tuning). And finally, data scientists often experiment in interactive notebooks like Jupyter or Hex: small Markdown/Python/SQL cells executed in order with tables and charts as output — and many notebooks use the DataFrame API or SQL as their batch processing.
۵.۴. سرویسدهیِ دادهٔ مشتقشده5.4. Serving derived data
خروجیِ دستهای بهتر است ابتدا کامل و اعتبارسنجی شود و بعد در مسیرِ خواندن قرار بگیرد. انتشارِ تدریجیِ فایلهای نیمهکاره میتواند به کاربران نتیجهٔ مخلوط بدهد.
مثال فنی: انتشار ایندکس کامل
وضعیت اولیه: job نسخهٔ search-v2 را در مسیر تازه میسازد و تعداد و کیفیت رکوردها را میسنجد.
اجرای مرحلهبهمرحله: پس از موفقیتِ همهٔ بخشها، اشارهگر خواندن از v1 به v2 عوض میشود؛ v1 برای بازگشت موقتاً میماند.
نتیجه و دلیل: کاربر مخلوطِ خروجی ناقص و قدیمی نمیبیند؛ نوشتن مستقیمِ هر task روی ایندکس فعال این مزیت را سختتر میکند.
Batch jobs often build precomputed or derived datasets: product recommendations, user-facing reports and ML-model features. These are usually served from a production database, key-value store or search engine; so data must travel from the batch processor's DFS/object store to the database answering live traffic.
چرا نوشتنِ مستقیم به پایگاه داده، ایدهٔ بدی استWhy writing directly to the database is a bad idea
وسوسهانگیز است: کلاینتِ پایگاه دادهِ محبوبت را داخلِ job بردار و رکوردرکورد مستقیم بنویس. کار میکند
(اگر firewall اجازه دهد!) ولی به چند دلیل بد است:
- هر رکورد یک درخواستِ شبکهای — چند مرتبه کندتر از توانِ عادیِ یک taskِ دستهای؛ حتی با batching هم عملکرد ضعیف میماند.
- frameworkهای دستهای taskهای زیادی موازی دارند؛ اگر همه با نرخِ موردِ انتظارِ دستهای بنویسند،
پایگاه دادهِ خروجی بهراحتی غرق میشود و عملکردِ کوئریهایش میافتد — و مشکل به بقیهٔ سیستم هم سرایت میکند.
- jobهای دستهای معمولاً تضمینِ تمیزِ all-or-nothing دارند: اگر job موفق شود، نتیجه
«اجرای هر task دقیقاً یک بار» است — حتی اگر وسط راه taskها شکست و بازتلاش شده باشند؛ و اگر کلِ job شکست بخورد،
هیچ خروجیای تولید نمیشود. نوشتنِ مستقیم به سیستمِ بیرونی یعنی side effectِ قابلِ دیدنِ بیرونی که دیگر
نمیشود پنهانش کرد: باید نگرانِ دیدهشدنِ نتایجِ jobِ نیمهکاره باشی، و taskی که شکست و restart شد
ممکن است خروجیِ اجرای شکسته را تکراری تولید کند.
Tempting: pick up your favorite database client inside the job and write record by record, directly. It works (if the firewall allows!) but is bad for several reasons:
- Each record is one network request — several times slower than a normal batch task's pace; even with batching, performance stays poor.
- Batch frameworks run many tasks in parallel; if all write at the expected batch rate, the output database easily drowns and its query performance drops — and the trouble spreads to the rest of the system.
- Batch jobs usually carry a clean all-or-nothing guarantee: a successful job means “each task ran exactly once” — even with mid-way task failures and retries; and a fully failed job produces no output at all. Writing directly to an outside system means an externally-visible side effect that can no longer be hidden: you must worry about half-done jobs' results being seen, and a failed-then-restarted task may duplicate the broken run's output.
راهِ بهتر این است: jobهای دستهای datasetهای آماده را به جریانها مثل topicهای Kafka
فشار دهند. موتورهای جستجو مثل Elasticsearch، سیستمهای OLAPِ بلادرنگ مثل Pinot و Druid،
data storeهای مشتقشده مثل Venice و warehouseهای ابری مثل ClickHouse
همگی قابلیتِ ingest از Kafka را دارند. فشاردادن از طریقِ سیستمِ جریانی چند مشکل را حل میکند:
The better way: batch jobs push ready datasets to streams like Kafka topics. Search engines like Elasticsearch, real-time OLAP systems like Pinot and Druid, derived data stores like Venice and cloud warehouses like ClickHouse can all ingest from Kafka. Pushing through a streaming system fixes several problems:
- سیستمهای جریانی برای نوشتنِ ترتیبی بهینهاند — مناسبِ بارِ نوشتنِ انبوهِ یک jobِ دستهای.
- میتوانند بهعنوانِ بافر بینِ jobِ دستهای و پایگاههای داده تولیدی عمل کنند: سیستمهای پاییندستی
نرخِ خواندنشان را throttle میکنند تا بتوانند همزمان ترافیکِ تولیدی را هم راحت سرو کنند.
- خروجیِ یک jobِ دستهای را میتواند چند سیستمِ پاییندستی مصرف کنند.
- سیستمهای جریانی میتوانند مرزِ امنیتی بینِ محیطِ پردازشِ دستهای و شبکهٔ تولیدی باشند:
در شبکهای موسوم به DMZ (demilitarized zone) بینِ این دو مستقر میشوند.
- Streaming systems optimize for sequential writes — fitting a batch job's bulk write load.
- They can act as a buffer between the batch job and production databases: downstream systems throttle their read rate so they can comfortably serve production traffic at the same time.
- One batch job's output can feed several downstream systems.
- Streaming systems can be a security boundary between the batch environment and the production network: deployed in a network called a DMZ (demilitarized zone) between the two.
اما یک چیز را جریان ذاتاً حل نمیکند: تضمینِ all-or-nothing. برای این کار، job در پایانِ کارش باید به
سیستمهای پاییندستی اعلام کند job تمام شده و داده حالا قابلِ سرو است. مصرفکنندههای جریان باید
دادهای که میگیرند تا اطلاعِ ثانوی از کوئریها پنهان نگه دارند — دقیقاً مثلِ یک تراکنشِ
uncommitted با ایزولهسازیِ read committed (فصل ۷) — تا خبرِ اتمامِ job برسد.
But one thing streams don't inherently fix: the all-or-nothing guarantee. For that, the job must announce completion to downstream systems at its end, and the data is servable only then. Stream consumers must keep received data hidden from queries until further notice — exactly like an uncommitted transaction under read committed isolation (Chapter 7) — until the job-done news arrives.
الگوی دیگر، مخصوصاً هنگامِ bootstrap کردنِ پایگاه دادهها: ساختنِ یک پایگاه دادهِ کاملاً نو داخلِ jobِ دستهای
و bulk-load مستقیمِ فایلها از DFS/object store/دیسکِ محلی به پایگاه داده. ابزارهای bulk import زیادند:
TiDB Lightning، jobهای Hadoop import در Pinot،
و APIی bulk-import فایلهای SST در RocksDB. ساختِ پایگاه داده در دسته و bulk-load
خیلی سریع است و سوییچِ اتمیکِ بینِ نسخههای dataset را ساده میکند؛ ولی بهروزرسانیِ
incremental روی همین پایگاههای داده نوساخته چالشبرانگیز است. وقتی هم bootstrap لازم است و هم loadِ تدریجی،
رویکردِ ترکیبی رایج است — مثلاً Venice با «hybrid store» هم بهروزرسانیِ سطریِ دستهای دارد
و هم تعویضِ کاملِ dataset.
Another pattern, especially when bootstrapping databases: building a brand-new database inside the batch job and bulk-loading files straight from DFS/object store/local disk into the database. Bulk-import tools abound: TiDB Lightning, Hadoop import jobs in Pinot, and the bulk-import API for SST files in RocksDB. Building the database in batch and bulk-loading is very fast and simplifies the atomic dataset-version switch; but incremental updates on these freshly-built databases are challenging. When both bootstrapping and gradual loading are needed, a hybrid approach is common — e.g. Venice with its “hybrid store” has both batched row updates and full dataset replacement.
با همهی این حرفها، تصمیمِ نهایی ساده بیان میشود: خروجیِ job از کدام مسیر به سیستمِ سروِ تولیدی برسد؟
درختِ تصمیمِ زیر همان پرسشهایی را مرتب میکند که در عمل باید یکییکی از خودت بپرسی —
و اولین شاخهاش همان «وسوسهی نوشتنِ مستقیم» را بهعنوانِ بنبست نشان میدهد.
With all that said, the final decision states simply: by which path should a job's output reach the production serving system? The decision tree below orders the questions you must ask yourself one by one in practice — and its first branch shows that very “direct-write temptation” as a dead end.
روایت مدرس — نمودار ۱۱ را با صدای بلند بخوانیمTeacher narration
مثال فنی برای خواندنِ نمودار: انتشار ایندکس کامل
وضعیت اولیه: job نسخهٔ search-v2 را در مسیر تازه میسازد و تعداد و کیفیت رکوردها را میسنجد.
اجرای مرحلهبهمرحله: پس از موفقیتِ همهٔ بخشها، اشارهگر خواندن از v1 به v2 عوض میشود؛ v1 برای بازگشت موقتاً میماند.
نتیجه و دلیل: کاربر مخلوطِ خروجی ناقص و قدیمی نمیبیند؛ نوشتن مستقیمِ هر task روی ایندکس فعال این مزیت را سختتر میکند.
این نمودار یک درختِ تصمیم است که از چپ به راست و از بالا به پایین خوانده میشود.
ریشه، جعبهی 01 Batch Output Ready است (زیرنویس:
derived dataset on disk، تگِ serve it to prod) —
یعنی dataset مشتقشده آماده روی دیسک است و حالا باید به دستِ کاربرانِ واقعی برسد.
فلشِ «assess» به اولین سوال میرود.
سوالِ اول: Write Direct to Prod DB? (زیرنویس:
per-record network calls). اگر «yes» بدهی، مستقیم میافتی به برگِ
Direct DB Writes با زیرنویسِ overloads DB, leaks output
و تگِ duplicates on retry. این یکی برگِ «قرمز» درخت است —
دقیقاً همان سه مشکلی که در کادرِ بالا خواندی: غرقشدنِ پایگاه داده، نشتِ خروجیِ نیمهکاره و رکوردهای تکراری.
پس شاخهی درست «no» است و به سوالِ دوم میرسیم: Rebuild or Bootstrap?
(زیرنویس: full dataset from scratch). اگر بله — یعنی داری کلِ dataset را
از نو میسازی — شاخه به سوالِ سوم میرود: Bulk-Import Tool?.
اگر سیستمِ مقصد ابزارِ bulk-load دارد، برگِ Bulk-Load Files + Swap
(«then atomic version swap»، تگِ «fastest, hard to update») همان مسیرِ فایلهای SST و
Lightning است. اگر ابزار نداشت، برگِ Publish Full Snapshot
(«to a stream topic»، تگِ «ingest at own pace») — انتشارِ snapshot کامل در جریان.
و اگر جوابِ سوالِ دوم «no» بود — یعنی بهروزرسانیِ تدریجی داری نه بازسازیِ کامل — به سوالِ چهارم میرسی:
All-or-Nothing per Run? (زیرنویس: «no partial output visible»).
«yes» یعنی به تضمینِ انتشارِ اتمیک نیاز داری و برگِ Stream + Completion Notice
را انتخاب میکنی («notify when the job is done»، تگِ «invisible until notified»).
«no» هم برگِ Stream Incremental Rows است («updates as they are produced»،
تگِ «consumers throttle reads»).
نکتهی ظریفِ درخت این است: هیچ مسیری به «نوشتنِ مستقیمِ رکوردرکورد» ختم نمیشود. اگر یک مصاحبهکننده
بپرسد «چرا job دستهای را مستقیم به پایگاه داده وصل نمیکنی؟»، جواب دقیقاً همان یک برگِ اول است —
و اگر بپرسد «خب چه کنیم؟»، سه برگِ دیگرِ درخت جوابهایِ معتبرند که باید بسته به «بازسازی در برابر
تدریجی» و «نیاز به all-or-nothing» انتخاب شوند.
This decision tree reads left to right, top to bottom. The root is 01 Batch Output Ready (“derived dataset on disk”, “serve it to prod”) — the derived dataset sits ready on disk and must now reach real users. The “assess” arrow leads to question one: Write Direct to Prod DB? (“per-record network calls”). Answering “yes” drops straight into the Direct DB Writes leaf (“overloads DB, leaks output”, “duplicates on retry”) — the tree's “red” leaf: exactly the three problems from the box above (drowning the database, leaking half-done output, duplicate records).
So the right branch is “no”, reaching question two: Rebuild or Bootstrap? (“full dataset from scratch”). If yes — rebuilding the whole dataset — the branch runs to question three: Bulk-Import Tool?. If the target system has a bulk-load tool, the Bulk-Load Files + Swap leaf (“then atomic version swap”, “fastest, hard to update”) is the SST-files-and-Lightning path. Without the tool, the Publish Full Snapshot leaf (“to a stream topic”, “ingest at own pace”) — publishing the full snapshot to a stream. A “no” to question two — gradual updates, not full rebuild — reaches question four: All-or-Nothing per Run? (“no partial output visible”). “Yes” wants atomic-publish guarantees and picks Stream + Completion Notice (“notify when the job is done”, “invisible until notified”); “no” is Stream Incremental Rows (“updates as they are produced”, “consumers throttle reads”). The tree's subtle point: no path ends at “record-by-record direct writes”. Asked “why not connect the batch job straight to the database?”, the answer is exactly that first leaf — and “then what?” has the tree's three other valid leaves, chosen by “rebuild vs gradual” and “all-or-nothing needed or not”.
روایت مدرس — نمودار ۱۲ را با صدای بلند بخوانیمTeacher narration
مثال فنی برای خواندنِ نمودار: انتشار ایندکس کامل
وضعیت اولیه: job نسخهٔ search-v2 را در مسیر تازه میسازد و تعداد و کیفیت رکوردها را میسنجد.
اجرای مرحلهبهمرحله: پس از موفقیتِ همهٔ بخشها، اشارهگر خواندن از v1 به v2 عوض میشود؛ v1 برای بازگشت موقتاً میماند.
نتیجه و دلیل: کاربر مخلوطِ خروجی ناقص و قدیمی نمیبیند؛ نوشتن مستقیمِ هر task روی ایندکس فعال این مزیت را سختتر میکند.
این نمودار هم یک ماشینِ وضعیت است با سه lane: Job output (مسیرِ اصلی)،
Failure handling و Terminal exits.
مسیرِ اصلی چهار گامِ شمارهدار دارد. 01 Writing Shards
(زیرنویس: tasks writing shards، تگِ output hidden) —
یعنی taskها دارند shardهای خروجی را مینویسند ولی خروجی هنوز پنهان است.
فلشِ «last task succeeds» به 02 Ready, Unpublished
میرود (all shards written، تگِ invisible) —
همهی shardها نوشته شدهاند ولی هنوز هیچکس نمیبیندشان.
گامِ کلیدی فلشِ «job-complete notification» است با یادداشتِ
«read-committed style reveal» به 03 Published
(consumers notified، تگِ serving queries):
اعلانِ پایانِ job، کلِ dataset را یکجا آشکار میکند — دقیقاً همان «افشایِ read-committed» که در متن دیدی.
بعد فلشِ «next scheduled run publishes» وضعیت را به
04 Superseded میبرد (replaced by newer run، تگِ
kept for rollback) — نسخهی جایگزینشده برای rollback و time travel نگه داشته میشود.
حالا مسیرِ شکست: از Writing Shards فلشِ قرمزِ «task fails / preempted» به
Task Failed میرود (partial output deleted، تگِ
retryable) و فلشِ برگشتیِ «retry from input» دوباره به
Writing Shards میرساند — یعنی خروجیِ ناقص حذف و از ورودیِ immutable از نو ساخته میشود.
و از Ready, Unpublished فلشِ «job aborted» به
Discarded میرود (deleted or never announced، تگِ
terminal): اجرای لغوشده یا حذف میشود یا هرگز به پاییندستی اعلام نمیشود —
و چون اعلام نشده، هیچکس چیزی ندیده است.
سه کارت جمعبندی میکنند: «All-or-Nothing Output» (موفقیتِ job یعنی هر task دقیقاً یک بار
اجرا شده، حتی با شکست و بازتلاش؛ اگر کلِ job شکست بخورد اصلاً خروجی تولید نمیشود؛ shardهای ناقص تا پایانِ نوشتن از کوئری پنهاناند)،
«Read-Committed Reveal» (مصرفکنندهها داده را تا اعلانِ پایانِ job نامرئی نگه میدارند؛
این آشکارسازی مثلِ commit یک تراکنش در سطحِ read committed است) و
«Recovery and Time Travel» (خروجیِ ناقصِ taskِ شکسته حذف و task از ورودیِ immutable بازتلاش میشود؛
خروجیهای superseded برای rollback و time travel نگه داشته میشوند؛ خروجیِ اجرای لغوشده حذف یا هرگز اعلام نمیشود).
درسِ کلیِ نمودار: مرزِ commit در پردازشِ دستهای همان «اعلامِ پایانِ job» است.
تا قبل از آن، هر چیزی قابلِ حذف و بازسازی است؛ بعد از آن، کلِ dataset یکجا برای خوانندهها واقعی میشود.
This state machine has three lanes: Job output (the main path), Failure handling and Terminal exits. The main path has four numbered steps: 01 Writing Shards (“tasks writing shards”, “output hidden”) — tasks are writing output shards but output stays hidden — then “last task succeeds” to 02 Ready, Unpublished (“all shards written”, “invisible”): all shards written, yet nobody sees them.
The key step is the “job-complete notification” arrow (“read-committed style reveal”) to 03 Published (“consumers notified”, “serving queries”): the job-done announcement reveals the whole dataset at once — exactly the text's “read-committed reveal”. Then “next scheduled run publishes” moves to 04 Superseded (“replaced by newer run”, “kept for rollback”) — the replaced version kept for rollback and time travel. The failure path: from Writing Shards the red “task fails / preempted” reaches Task Failed (“partial output deleted”, “retryable”), and “retry from input” loops back to Writing Shards — partial output deleted, rebuilt from immutable input. From Ready, Unpublished, “job aborted” goes to Discarded (“deleted or never announced”, “terminal”): an aborted run is deleted or never announced downstream — and since never announced, nobody saw a thing.
Three cards conclude: “All-or-Nothing Output” (job success means every task ran exactly once, even with failures and retries; a fully failed job produces no output at all; incomplete shards stay hidden from queries until writing ends), “Read-Committed Reveal” (consumers keep data invisible until the job-done announcement; this reveal is like committing a transaction at read-committed level) and “Recovery and Time Travel” (a broken task's partial output is deleted and the task retried from immutable input; superseded outputs are kept for rollback and time travel; an aborted run's output is deleted or never announced). The diagram's overall lesson: the commit boundary in batch processing is exactly the “job-done announcement”. Before it everything is deletable and rebuildable; after it the whole dataset becomes real for readers at once.
نقشه فصل — همه مفاهیم در یک نگاهChapter map — all concepts at a glance
روایت مدرس از نمودارTeacher narration
مثال فنی برای خواندنِ نمودار: بازمحاسبهٔ گزارش از ورودی ثابت
وضعیت اولیه: فایل فروش روزِ ۱۴ مهر بسته شده و نسخهٔ ورودی مشخص است.
اجرای مرحلهبهمرحله: job مجموع فروش هر شعبه را در خروجی نسخهٔ v2 مینویسد؛ اگر فرمول غلط بود با کد اصلاحشده از همان ورودی تکرار میشود.
نتیجه و دلیل: ورودی immutable و خروجی نسخهدار، خطای محاسبه را قابلبازسازی میکند؛ بازنویسی مستقیمِ دادهٔ مرجع چنین آزادیای نمیدهد.
گره مرکزی قرارداد فصل است: ورودی immutable، خروجی derived. سه مفهوم بالا ریشهها و موتورها هستند: pipeline یونیکس (لوله روی یک ماشین)، سیستمعامل توزیعشده (ذخیرهسازی، زمانبندها، taskها) و MapReduce (map، sort ضمنی، reduce). سه مفهوم پایین مقیاس و مصرفاند: shuffle (مرتبسازی توزیعشده)، dataflow و SQL (یک job با pipelining) و سرویسدهی خروجی (جریانها، all-or-nothing).
درس یکخطی نقشه: همهچیز از همان قرارداد ساده میآید — خروجی را از صفر بساز تا بازتلاش امن، بازیابی ممکن و انتشار اتمیک شود؛ و برای مقیاس، کار را با shuffle بین taskهای مستقل پخش کن.
The center node is the chapter's contract: immutable in, derived out. The three top concepts are the roots and engines: the Unix pipeline (pipes on one machine), the distributed OS (storage, schedulers, tasks) and MapReduce (map, implicit sort, reduce). The three bottom concepts are scale and consumption: shuffle (the distributed sort), dataflow and SQL (one pipelined job) and serving the output (streams, all-or-nothing).
The map's one-line lesson: everything flows from the same simple contract — build output from scratch so retries stay safe, recovery possible and publication atomic; and for scale, spread the work with shuffle across independent tasks.