DDIA·مطالعهSTUDY

نقشه راهRoadmap/ فصل ۱۱Chapter 11

پردازش دسته‌ایBatch Processing

پردازش دسته‌ای روی یک ورودیِ محدود، نتیجه‌ای قابل‌انتشار می‌سازد. این فصل از زنجیرهٔ ابزارهای ساده شروع می‌کند و به تقسیمِ کار، انتقالِ داده و بازیابیِ وظیفهٔ شکست‌خورده روی چند ماشین می‌رسد. From the simple Unix pipeline to the “distributed operating system” (filesystem, scheduler, isolated tasks), from MapReduce and shuffle to dataflow engines — and all-or-nothing publication of output to the outside world.

۱۱11 ۱۲ نمودار + روایت گام‌به‌گامdiagrams with step-by-step narration· محور: batch · ابزارهای Unix · MapReduce · shuffle · dataflow · workflowهاFocus: batch · Unix tools · MapReduce · shuffle · dataflow · workflows· منبع: فصل ۱۱ کتاب DDIA ویرایش ۲Source: DDIA 2nd Edition, Chapter 11

۰0 نقشهٔ راه فصلChapter roadmap

ایدهٔ کلیدی فصلChapter thesis

پردازش دسته‌ای روی یک ورودیِ محدود، نتیجه‌ای قابل‌انتشار می‌سازد. این فصل از زنجیرهٔ ابزارهای ساده شروع می‌کند و به تقسیمِ کار، انتقالِ داده و بازیابیِ وظیفهٔ شکست‌خورده روی چند ماشین می‌رسد.

مثال فنی: بازمحاسبهٔ گزارش از ورودی ثابت

وضعیت اولیه: فایل فروش روزِ ۱۴ مهر بسته شده و نسخهٔ ورودی مشخص است.

اجرای مرحله‌به‌مرحله: job مجموع فروش هر شعبه را در خروجی نسخهٔ v2 می‌نویسد؛ اگر فرمول غلط بود با کد اصلاح‌شده از همان ورودی تکرار می‌شود.

نتیجه و دلیل: ورودی immutable و خروجی نسخه‌دار، خطای محاسبه را قابل‌بازسازی می‌کند؛ بازنویسی مستقیمِ دادهٔ مرجع چنین آزادی‌ای نمی‌دهد.

مثال‌های این فصل سناریوهای آموزشیِ برگرفته از کارهای روزمره‌اند. عددهای انتخاب‌شده برای توضیح‌اند؛ معماریِ داخلیِ یک شرکت مشخص را توصیف نمی‌کنند.

A batch job takes read-only, immutable input and produces derived output — output rebuilt from scratch every run, so you can delete it, fix the logic and rerun. That one simple contract changes everything: retries become safe, rolling back is possible (time travel), even “recovering from human bugs” works (human fault tolerance). The model's price is equally clear: output is visible only after the job ends, and any small input change means reprocessing the whole input.
نگاه اولپردازش آنلاین (online)پردازش دسته‌ای (batch / offline)
نوع ورودیدرخواستِ کاربر / کوئریِ تعاملیdataset محدود و از قبل موجود (immutable)
خروجیپاسخِ سریع به همان درخواستdataset مشتق‌شده، هر بار از صفر بازسازی‌شده
معیارِ عملکردresponse time و در دسترس بودنthroughput (حجمِ داده در واحدِ زمان)
مدت اجرامیلی‌ثانیه تا ثانیهدقیقه، ساعت، گاهی روز؛ اغلب دوره‌ای (مثلاً روزانه)
برخورد با خرابیدر دسترس ماندنِ دائمبازتلاشِ task یا اجرای دوبارهٔ کلِ job
نمونهوب‌سرویس، پایگاه داده، کش، جستجوETL، تحلیل، آموزش مدل، ساختِ dataset مشتق‌شده
First lookOnline processingBatch processing (offline)
Input typeUser request / interactive queryA bounded, pre-existing (immutable) dataset
OutputA fast answer to that same requestA derived dataset, rebuilt from scratch each time
Performance metricresponse time and availabilitythroughput (data volume per unit time)
Run durationMilliseconds to secondsMinutes, hours, sometimes days; usually periodic (e.g. daily)
Handling failureStaying always availableRetrying a task or rerunning the whole job
ExampleWeb service, database, cache, searchETL, analytics, model training, derived-dataset building

معماریِ این فصل را می‌توان در سه لایه دید — و نمودار ۲ همین سه لایه را می‌کشد: ذخیره‌سازی (فایل‌سیستمِ توزیع‌شده یا object store)، هماهنگ‌سازی (scheduler، resource manager و workflow scheduler) و محاسبه (taskهای منزوی که یک shard ورودی می‌خوانند و یک shard خروجی می‌نویسند). مدل‌های پردازش روی این سه لایه می‌نشینند: MapReduce، موتورهای dataflow (مثل Spark و Flink)، زبان‌های کوئری مثل SQL و APIهای DataFrame. و کاربردها در چهار دسته جمع می‌شوند: ETL، تحلیل، یادگیری ماشین و سرویس‌دهیِ دادهٔ مشتق‌شده.

This chapter's architecture reads as three layers — and Diagram 2 draws exactly them: storage (a distributed filesystem or object store), orchestration (scheduler, resource manager and workflow scheduler) and compute (isolated tasks each reading one input shard and writing one output shard). Processing models sit on these layers: MapReduce, dataflow engines (like Spark and Flink), query languages like SQL and DataFrame APIs. And the applications group into four: ETL, analytics, machine learning and serving derived data.

چیدمان فصلChapter layout
بخش ۱ فلسفهٔ batch را جا می‌اندازد؛ بخش ۲ همان فلسفه را با ابزارهای Unix روی یک ماشین نشان می‌دهد (که بهترین راهِ ساختنِ شهود است). بخش ۳ همان ابزارها را به سیستم‌عاملِ توزیع‌شده تعمیم می‌دهد: فایل‌سیستم، object store، هماهنگ‌سازی، تخصیص منابع، workflow و تحملِ خرابی. بخش ۴ مدل‌های پردازش است (MapReduce، dataflow، shuffle، join، SQL، DataFrame) و بخش ۵ کاربردها. اگر فقط وقتِ یک بخش را داری، بخش ۲ و ۳ را بخوان — همه‌چیز از آنجا ریشه می‌گیرد.
Section 1 establishes the batch philosophy; Section 2 shows the same philosophy with Unix tools on one machine (the best way to build intuition). Section 3 generalizes those tools into a distributed operating system: filesystem, object store, orchestration, resource allocation, workflows and fault tolerance. Section 4 is the processing models (MapReduce, dataflow, shuffle, joins, SQL, DataFrames) and Section 5 the applications. If you have time for one section only, read Sections 2 and 3 — everything grows from there.

۱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

نمودار تکمیلی · دو جهان آنلاین و دسته‌ایExtra diagram · the online and batch worlds باز کردن در صفحه جدا ↗Open standalone ↗
روایت مدرس از نمودار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.

نمودار ۱ — pipeline تحلیل لاگ: پنج URL پردرخواستDiagram 1 — the log-analysis pipeline: top 5 URLs باز کردن در صفحه جدا ↗Open standalone ↗
روایت مدرس — نمودار ۱ را با صدای بلند بخوانیم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.
نمودار ۲ — پشتهٔ یک framework دسته‌ای: سه لایهDiagram 2 — a batch framework's stack: three layers باز کردن در صفحه جدا ↗Open standalone ↗
روایت مدرس — نمودار ۲ را با صدای بلند بخوانیم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؛ مقیاسِ مستقل
DimensionLocal filesystem (ext4/XFS)Distributed filesystem (HDFS etc.)Object store (S3 etc.)
Data unitFiles, broken into 4KB blocksFiles, broken into big blocks (128MB / 4MB)Uniquely-keyed objects; immutable after writing
MetadataInodes and directories, on the same diskCentralized metadata service (like NameNode) or a KV storePart of the key; “prefixes” instead of directories
CacheOS page cacheEach data node's page cache + client-side cachesUsually no local cache; S3 Express with single-digit-ms latency
Failure resistanceRAID on the same machineCross-machine replication or erasure coding (Reed–Solomon)Guaranteed inside the service (multi-region)
Atomic opsAtomic rename, links, locksOften atomic rename, links and locksNon-atomic rename (copy+delete), no links or locks
LocalityCompute node = storage nodeRun 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.

نمودار ۳ — درختِ تصمیمِ سیاستِ زمان‌بندیDiagram 3 — the scheduling-policy decision tree باز کردن در صفحه جدا ↗Open standalone ↗
روایت مدرس — نمودار ۳ را با صدای بلند بخوانیم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.
نمودار ۴ — خرابیِ task، preemption و بازتلاشDiagram 4 — task failure, preemption and retry باز کردن در صفحه جدا ↗Open standalone ↗
روایت مدرس — نمودار ۴ را با صدای بلند بخوانیم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).

نمودار ۵ — چرخهٔ زندگیِ یک تلاشِ taskDiagram 5 — the life of one task attempt باز کردن در صفحه جدا ↗Open standalone ↗
روایت مدرس — نمودار ۵ را با صدای بلند بخوانیم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

نمودار تکمیلی · فایل‌سیستم محلی، توزیع‌شده و object storeExtra diagram · local and distributed filesystems and object stores باز کردن در صفحه جدا ↗Open standalone ↗
روایت مدرس از نمودار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:

  1. خواندنِ ورودی و شکستن به رکورد: در مثالِ لاگ، هر رکورد یک خط است (جداکنندهٔ رکورد \n). در Hadoop، ورودی در HDFS یا object store مثل S3 است و قالب‌های فایلِ مختلفی داریم: مثل Apache Parquet (قالبِ ستونی) یا Apache Avro (قالبِ سطری).
  2. فراخوانیِ تابعِ mapper برای استخراجِ key و value از هر رکوردِ ورودی. در مثالِ ما mapper همان awk '{print $7}' است که URL را به‌عنوان key برمی‌دارد و value خالی می‌ماند.
  3. مرتب‌سازیِ همهٔ جفت‌های (key, value) بر اساسِ key. در مثال، همان دستورِ اولِ sort.
  4. فراخوانیِ reducer روی جفت‌های مرتب‌شده. چون رکوردهای هم‌کلید با مرتب‌سازی مجاور شده‌اند، ترکیبِ مقادیرشان بدونِ نگه‌داشتنِ state زیاد در حافظه ممکن است. در مثال، reducer همان uniq -c است که تعدادِ رکوردهای مجاورِ هم‌کلید را می‌شمارد.
  1. 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).
  2. 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.
  3. Sort all (key, value) pairs by key. In the example, the first sort command.
  4. 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.
نمودار ۶ — shuffle در MapReduce (شکل ۱۱-۱)Diagram 6 — shuffle in MapReduce (Figure 11-1) باز کردن در صفحه جدا ↗Open standalone ↗
روایت مدرس — نمودار ۶ را با صدای بلند بخوانیم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.

نمودار ۷ — وقتِ اجرا کجا می‌رود: MapReduce در برابر dataflowDiagram 7 — where the time goes: MapReduce vs dataflow باز کردن در صفحه جدا ↗Open standalone ↗
روایت مدرس — نمودار ۷ را با صدای بلند بخوانیم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.

نمودار ۸ — sort-merge join و group by در دو jobDiagram 8 — sort-merge join and group by in two jobs باز کردن در صفحه جدا ↗Open standalone ↗
روایت مدرس — نمودار ۸ را با صدای بلند بخوانیم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).

نمودار ۹ — ساختارِ sort-merge join روی user_id (شکل‌های ۱۱-۲ و ۱۱-۳)Diagram 9 — the sort-merge join on user_id (Figures 11-2 and 11-3) باز کردن در صفحه جدا ↗Open standalone ↗
روایت مدرس — نمودار ۹ را با صدای بلند بخوانیم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؛ بدونِ نگه‌داریِ خوشه
معیارِ انتخابهزینه، راحتی، سادگیِ پیاده‌سازی، در دسترس بودنِ مهارت — و بزرگ‌ترها اغلب هر دو را دارند
DimensionDataflow engine (Spark / Flink)Cloud data warehouse (BigQuery / Snowflake)
Programming modelProgramming API, SQL, DataFrames — general language logicSQL as the core + DataFrame/Snowpark libraries
Data formatAny format (Parquet, Avro, JSON, …) on DFS/object storeUsually 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 codeDifficult or unsupported
Row-wise compute on columnar dataFlexible execution engineInefficient — another API or replacement needed
CostOften cheaper for big jobs (self-managed)Comfortable but usually pricier
OperationsYou run the cluster yourself (or a managed service)Fully managed; no cluster keeping
Selection metricCost, 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”.

نمودار ۱۰ — نقشهٔ مفهومی: مدل‌های پردازش، storage و orchestrationDiagram 10 — the concept map: processing models, storage and orchestration باز کردن در صفحه جدا ↗Open standalone ↗
روایت مدرس — نمودار ۱۰ را با صدای بلند بخوانیم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.

نمودار ۱۱ — درختِ تصمیمِ سرویس‌دهیِ دادهٔ مشتق‌شدهDiagram 11 — the derived-data serving decision tree باز کردن در صفحه جدا ↗Open standalone ↗
روایت مدرس — نمودار ۱۱ را با صدای بلند بخوانیم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”.

نمودار ۱۲ — چرخهٔ انتشارِ خروجیِ job: all-or-nothingDiagram 12 — a job's output-publication cycle: all-or-nothing باز کردن در صفحه جدا ↗Open standalone ↗
روایت مدرس — نمودار ۱۲ را با صدای بلند بخوانیم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

نقشه فصل ۱۱ · پردازش دسته‌ایChapter 11 map · batch processing باز کردن در صفحه جدا ↗Open standalone ↗
روایت مدرس از نمودار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.

۶6 بستهٔ مصاحبهInterview pack

۶.۱. همه‌چیز در یک نگاه6.1. Everything at a glance

مفهومتعریفِ یک‌خطینکته‌ی مصاحبه
batch jobورودیِ immutable بخوان، خروجیِ derived از صفر بسازمعیار: throughput نه response time
online vs offlineدرخواست/پاسخِ سریع در برابرِ محاسبه‌ی انبوهمرزشان تیز نیست؛ کوئریِ بلند ≈ دسته‌ای
time travelبازگشت به نسخهٔ قبلیِ خروجی/دادهobject store و open table formatها پشتیبانی می‌کنند
human fault toleranceبازیابی از باگِ کد با اجرای دوبارهپایگاه دادهِ تراکنشی این را ندارد
working setمقدارِ حافظه‌ای که job به دسترسیِ تصادفی نیاز دارددر شمارشِ URLها = تعدادِ URLهای متمایز
external sortمرتب‌سازی با spill به دیسک و ادغامِ segmentهاGNU sort خودش spill و موازی‌سازی می‌کند
distributed OS analogyDFS = فایل‌سیستم، scheduler = kernel، task = پروسهسه لایه: ذخیره‌سازی، هماهنگ‌سازی، محاسبه
DFS blockبلاکِ بزرگ (128MB/4MB) در برابرِ 4KB محلیمتادیتای کمتر + seek ارزان‌تر نسبت به read
data nodedaemonِ خواندن/نوشتنِ بلاک روی فایل‌سیستمِ محلیpage cache هر نود = کشِ توزیع‌شده
metadata serviceمحلِ فایل‌ها، مجوزها، متادیتای خوشهNameNode در Hadoop؛ 3FS روی FoundationDB
shared-nothing vs NAS/SANکامپیوترِ معمولی+شبکه در برابرِ appliance متمرکزDFSها shared-nothing‌اند؛ RAID ولی روی شبکه
erasure codingبازیابیِ داده‌ی گم‌شده با هزینه‌ی کمتر از کپیِ کاملReed–Solomon؛ جایگزینِ replication
object storebucket + key؛ get/put؛ immutable بعد از نوشتنrename غیراتمیک؛ بدونِ link/قفل؛ بدونِ دایرکتوریِ خالی
prefix listingفهرستِ objectها با پیشوند، مثلِ ls -Rپیشوند = قراردادِ نام‌گذاری، نه دایرکتوریِ واقعی
data localityاجرای task روی نودِ حاویِ replicaِ ورودیHDFS بله؛ object storeها جداییِ compute/storage
task executordaemonِ هر نود: اجرای task + heartbeatNodeManager / kubelet؛ جداسازی با cgroups
resource managerنمایِ سراسریِ وضعیتِ خوشهYARN: ZooKeeper؛ Kubernetes: etcd — گلوگاهِ بالقوه
gang schedulingنگه‌داشتنِ منابع تا همه‌ی taskها با هم اجرا شوندidle + خطرِ deadlock؛ starvation در صف
preemptionکشتنِ taskِ کم‌اولویت برای jobِ مهم‌ترشایع‌تر از خرابیِ سخت‌افزار؛ spot/preemptible VMs
DRF / FIFO / bin-packingheuristicsِ تخصیصِ منابعمسئله NP-hard است؛ بهینه ناممکن
workflow (DAG)زنجیرهٔ jobها با وابستگیِ ورودی/خروجیاینجا ≠ durable execution فصل ۵
workflow schedulerAirflow/Dagster/Prefect برای کلِ DAGschedulerِ هر job فقط job را می‌فهمد
backpressureصبرِ تولیدکننده وقتی بافر پر شودلوله‌ی Unix؛ بینِ taskهای dataflow
task-level retryبازتلاشِ فقط taskِ شکسته، نه کلِ jobورودی immutable + بدونِ side effect = امن
intermediate dataداده‌ی بینِ taskها در یک workflowDFS در MapReduce؛ حافظه+lineage در Spark؛ checkpoint در Flink
MapReducemap و reduce + sort ضمنیjob دوم برای sort دوم؛ I/O فایلی؛ بدونِ pipelining
dataflow engineکلِ workflow یک job (Spark/Flink)ریشه: Dryad/Nephele؛ operatorهای قابلِ fuse
shuffleمرتب‌سازیِ توزیع‌شده با ورودی/خروجیِ shardشده«تصادفی» نیست! پایهٔ join و تجمیع
hash(key) routingهر رکورد دقیقاً به یک reducerهر mapper یک فایلِ مرتب به‌ازای هر reducer (m1r2)
sort-merge joinادغامِ لیست‌های مرتبِ دو طرفِ joinیک رکوردِ کاربر در حافظه؛ بدونِ lookup شبکه‌ای
secondary sortترتیبِ درونِ هر کلید (مثلاً DOB اول، بعد ts)رکوردِ dimension قبل از factهای مرتب‌شده
star schemaجدولِ fact (رویدادها) + جدولِ dimension (کاربران)join دسته‌ای، denormalize برای تحلیل
cost-based optimizerانتخابِ الگوریتم و ترتیبِ join بر اساسِ costHive/Trino/Spark/Flink؛ تغییرِ ترتیبِ join
data lakehousetable format (Iceberg) + catalog (Unity) روی object storeSQL روی فایل‌های دریاچه‌ی داده
DataFrameجدولِ سطرها با تیپِ یکسان در ستون‌هاایندکس/ترتیبِ محلی در توزیع‌شده نیست → غافلگیری
eager vs lazyاجرای فوری (Pandas) در برابرِ query plan (Spark)Arrow = مدلِ داده‌ی مشترکِ client/server
all-or-nothing outputیا همه‌ی taskها دقیقاً یک بار، یا هیچ خروجیاعلانِ پایانِ job = مرزِ commit
read-committed revealپنهان‌نگه‌داشتنِ داده تا اعلانِ کامل‌شدنِ jobالهام از read committed (فصل ۷)
bulk load / dataset swapساختِ پایگاه دادهِ نو + سوییچِ اتمیکِ نسخهTiDB Lightning، import هادوپ Pinot، bulk-ingest SST در RocksDB
ConceptOne-line definitionInterview point
batch jobRead immutable input, build derived output from scratchMetric: throughput, not response time
online vs offlineFast request/response vs bulk computationThe boundary isn't sharp; a long query ≈ batch
time travelReturn to a previous output/data versionObject stores and open table formats support it
human fault toleranceRecover from code bugs by rerunningTransactional databases lack this
working setMemory a job needs random access toIn URL counting = distinct-URL count
external sortSort with disk spills and segment mergesGNU sort spills and parallelizes by itself
distributed OS analogyDFS = filesystem, scheduler = kernel, tasks = processesThree layers: storage, orchestration, compute
DFS blockBig blocks (128MB/4MB) vs 4KB localLess metadata + cheaper seeks relative to reads
data nodeThe block read/write daemon on local filesystemsEach node's page cache = distributed cache
metadata serviceFile placement, permissions, cluster metadataNameNode in Hadoop; 3FS on FoundationDB
shared-nothing vs NAS/SANCommodity computers+network vs centralized applianceDFSs are shared-nothing; RAID but over the network
erasure codingRecover lost data cheaper than full copiesReed–Solomon; replication's replacement
object storeBucket + key; get/put; immutable after writingNon-atomic rename; no links/locks; no empty dirs
prefix listingList objects by prefix, like ls -RPrefix = naming convention, not a real directory
data localityRun tasks on input-replica-holding nodesHDFS yes; object stores split compute/storage
task executorEach node's daemon: task running + heartbeatsNodeManager / kubelet; cgroups isolation
resource managerThe cluster's global state viewYARN: ZooKeeper; Kubernetes: etcd — potential bottleneck
gang schedulingHold resources until all tasks run togetherIdleness + deadlock risk; queue starvation
preemptionKill low-priority tasks for bigger jobsMore common than hardware faults; spot/preemptible VMs
DRF / FIFO / bin-packingResource-allocation heuristicsThe problem is NP-hard; optimal is impossible
workflow (DAG)A chain of jobs with input/output dependenciesHere ≠ Chapter 5's durable execution
workflow schedulerAirflow/Dagster/Prefect for the whole DAGEach job's scheduler understands only its job
backpressureThe producer waiting when buffers fillUnix pipes; between dataflow tasks
task-level retryRetry only the broken task, not the whole jobImmutable input + no side effects = safe
intermediate dataData between tasks in a workflowDFS in MapReduce; memory+lineage in Spark; checkpoints in Flink
MapReduceMap and reduce + implicit sortSecond sort = second job; file I/O; no pipelining
dataflow engineThe whole workflow as one job (Spark/Flink)Roots: Dryad/Nephele; fusable operators
shuffleDistributed sort with sharded input/outputNot “random”! The basis of joins and aggregation
hash(key) routingEach record to exactly one reducerEach mapper one sorted file per reducer (m1r2)
sort-merge joinMerge both join sides' sorted listsOne user record in memory; no network lookups
secondary sortOrder within each key (e.g. DOB first, then ts)Dimension record before sorted facts
star schemaFact table (events) + dimension table (users)Batch joins denormalize the star for analysis
cost-based optimizerPick join algorithm and order by costHive/Trino/Spark/Flink; join reordering
data lakehouseTable format (Iceberg) + catalog (Unity) on object storeSQL over data-lake files
DataFrameRow tables with same-typed columnsLocal indexing/ordering missing distributed → surprises
eager vs lazyImmediate (Pandas) vs query plan (Spark)Arrow = shared client/server data model
all-or-nothing outputEither every task exactly once, or no outputThe job-done announcement = commit boundary
read-committed revealHide data until the job-done announcementInspired by read committed (Chapter 7)
bulk load / dataset swapBuild a fresh database + atomic version switchTiDB Lightning, Pinot Hadoop import, RocksDB SST bulk-ingest

۶.۲. سوالات پرتکرار مصاحبه6.2. Frequent interview questions

۱. پردازشِ دسته‌ای و آنلاین در یک جمله چه تفاوتی دارند؟1. How do batch and online processing differ in one sentence?

آنلاین برای درخواستِ حاضر پاسخ می‌سازد؛ دسته‌ای روی ورودیِ محدود کار می‌کند و خروجی را پس از پردازش آماده می‌کند. تأخیرِ هدف و روشِ انتشارِ نتیجه متفاوت‌اند.

مثال فنی: پاسخ کاربر یا پایان job

وضعیت اولیه: API وضعیت سفارش باید در چندصد میلی‌ثانیه پاسخ دهد؛ گزارش کل سال ممکن است نیم ساعت طول بکشد.

اجرای مرحله‌به‌مرحله: API درخواست محدود را همان لحظه انجام می‌دهد؛ job دسته‌ای ورودیِ کران‌دار را کامل می‌خواند و خروجی را منتشر می‌کند.

نتیجه و دلیل: معیار آنلاین بیشتر latency هر درخواست و معیار batch بیشتر زمان پایان و هزینهٔ کل پردازش است.

Online: ask, get a fast answer — response time is the metric. Batch: process a bounded immutable dataset from scratch into derived output — throughput is the metric. Batch input never changes and output is rebuilt every time.

۲. منظور از «human fault tolerance» چیست؟2. What does “human fault tolerance” mean?

تحملِ خطای انسانی در این بحث یعنی با حفظِ ورودی و جداسازیِ خروجی، منطقِ اشتباه را اصلاح و محاسبه را دوباره اجرا کنیم. این امکان به تغییرناپذیری و کنترلِ اثرهای جانبی وابسته است.

مثال فنی: بازمحاسبهٔ گزارش از ورودی ثابت

وضعیت اولیه: فایل فروش روزِ ۱۴ مهر بسته شده و نسخهٔ ورودی مشخص است.

اجرای مرحله‌به‌مرحله: job مجموع فروش هر شعبه را در خروجی نسخهٔ v2 می‌نویسد؛ اگر فرمول غلط بود با کد اصلاح‌شده از همان ورودی تکرار می‌شود.

نتیجه و دلیل: ورودی immutable و خروجی نسخه‌دار، خطای محاسبه را قابل‌بازسازی می‌کند؛ بازنویسی مستقیمِ دادهٔ مرجع چنین آزادی‌ای نمی‌دهد.

The ability to recover from human code bugs: since input is immutable and output derived, revert code to the previous version and rerun the job — or switch to the previous output (kept under another path). Transactional databases lack this: if buggy code writes bad data, reverting code never fixes the data.

۳. چرا در مثالِ pipeline، اول sort می‌کنیم و بعد uniq -c؟3. Why sort first and uniq -c after in the pipeline example?

uniq فقط تکرارهای کنارِ هم را می‌شمارد. مرتب‌سازی مقدارهای یکسان را کنارِ هم می‌آورد تا شمارشِ همهٔ رخدادها درست باشد.

مثال فنی: چرا sort قبل از uniq؟

وضعیت اولیه: مسیرهای ورودی /a,/b,/a هستند.

اجرای مرحله‌به‌مرحله: uniq -c فقط تکرارهای مجاور را می‌شمارد و بدون sort سه گروه می‌بیند؛ sort دو /a را کنار هم می‌آورد.

نتیجه و دلیل: مرتب‌سازی در این pipeline برای درستیِ شمارش لازم است، نه فقط برای زیبایی خروجی.

Because uniq only drops adjacent duplicate lines. Sorting makes every n-times-seen URL appear n times in a row; then uniq -c turns each run into one counted line. Exactly MapReduce's implicit-sort role, bringing same-key records together for the reducer.

۴. «مرتب‌سازی» چرا نسبت به «تجمیع در حافظه» مقیاس می‌خورد؟4. Why does “sorting” scale better than “in-memory aggregation”?

تجمیعِ هش‌محور به حافظهٔ متناسب با تعدادِ کلیدهای متفاوت نیاز دارد. مرتب‌سازیِ بیرونی می‌تواند بخش‌ها را روی دیسک مرتب و ادغام کند؛ برای دادهٔ بزرگ‌تر از حافظه راهی قابل‌کنترل می‌دهد.

مثال فنی: تجمیع بیرون از RAM

وضعیت اولیه: ورودی یک میلیارد کلید متفاوت دارد و جدول hashِ شمارنده‌ها در RAM جا نمی‌شود.

اجرای مرحله‌به‌مرحله: external sort تکه‌های جا‌شونده در حافظه را مرتب و روی دیسک می‌نویسد، سپس ادغام می‌کند.

نتیجه و دلیل: مرتب‌سازی می‌تواند با دیسک و حافظهٔ محدود ادامه دهد؛ hash aggregation هم ممکن است spill داشته باشد، پس برتری مطلق مطرح نیست.

Because it uses disk efficiently: chunks sort in memory, spill as segments, and merge via mergesort (sequential access). A hash table instead wants memory per distinct key (the working set); bigger than memory, it breaks. GNU sort spills and parallelizes by itself though; the bottleneck becomes the input read rate.

۵. قیاسِ «framework دسته‌ای = سیستم‌عاملِ توزیع‌شده» یعنی چه؟5. What does the “batch framework = distributed OS” analogy mean?

چارچوبِ دسته‌ای محلِ داده، منابع، اجرای وظیفه و بازیابی را میان چند ماشین مدیریت می‌کند. این نقش‌ها شبیه وظایفِ سیستم‌عامل‌اند، اما هزینهٔ شبکه و خرابیِ جزئی هم وجود دارد.

مثال فنی: سه مسئولیتِ چارچوب

وضعیت اولیه: job به خواندن فایل، اجرای ۱۰۰ task و انتقال دادهٔ بین مرحله‌ها نیاز دارد.

اجرای مرحله‌به‌مرحله: لایهٔ ذخیره فایل را نگه می‌دارد؛ scheduler منابع می‌دهد؛ موتور اجرا task، retry و shuffle را مدیریت می‌کند.

نتیجه و دلیل: قیاس با سیستم‌عامل یعنی برنامه همهٔ جزئیات ماشین‌ها را خودش اداره نکند؛ مرز هر لایه هنوز باید روشن باشد.

The same three Unix-machine parts exist distributed: storage (DFS/object store = filesystem), orchestration (scheduler and resource manager = the kernel deciding what runs where with what resources), and compute (isolated tasks = processes exchanging data via the filesystem).

۶. چرا بلاک‌های DFS این‌قدر بزرگ‌تر از بلاک‌های فایل‌سیستمِ محلی‌اند؟6. Why are DFS blocks so much bigger than local-filesystem blocks?

بلوکِ بزرگ‌تر نسبتِ سربارِ هماهنگی و جست‌وجو به انتقالِ انبوه را کم می‌کند و برای خواندنِ ترتیبی مناسب است. اندازهٔ مناسب به بار و پیاده‌سازی وابسته است.

مثال فنی: بلاک بزرگِ فایل توزیع‌شده

وضعیت اولیه: فایل 1 GB با بلاک فرضی 128 MB به هشت بخش تقسیم شده است.

اجرای مرحله‌به‌مرحله: metadata محل کپی هر بلاک را می‌دهد و worker از یک کپی سالم می‌خواند؛ اندازهٔ بلاک روی تعداد metadata و موازی‌سازی اثر دارد.

نتیجه و دلیل: بلاکِ درشت برای اسکن ترتیبی و کاهش سربار مناسب است؛ با page کوچکِ موتور پایگاه داده هدف یکسانی ندارد.

Two reasons: less metadata to track (a big deal on petabyte datasets), and cheaper seeks relative to block reads. And since a DFS is implemented on the nodes' OS filesystems, the “write whole blocks” requirement vanishes: a 900MB file with 128MB blocks means seven 128MB blocks and one 4MB block.

۷. object store چه تفاوت‌هایی با فایل‌سیستم دارد؟7. How does an object store differ from a filesystem?

مخزنِ شیء با کلید و API داده را نگه می‌دارد؛ فایل‌سیستم قراردادِ فایل و پوشه و عملیاتِ متفاوتی دارد. تغییرِ جزئی، تغییرنام و تضمین‌های سازگاری را باید برای محصولِ مشخص بررسی کرد.

مثال فنی: Object یک فایل POSIX کامل نیست

وضعیت اولیه: خروجی job با کلید reports/run-42/part-1 در object store نوشته می‌شود.

اجرای مرحله‌به‌مرحله: برای انتشارِ نسخهٔ کامل، manifest یا اشاره‌گر نسخهٔ معتبر عوض می‌شود؛ نباید rename اتمیکِ دایرکتوری را بدون پشتیبانی فرض کرد.

نتیجه و دلیل: API و تضمین عملیات object store باید جدا بررسی شود؛ شبیه بودن نام «فایل» رفتار یکسان نمی‌سازد.

Objects are immutable after writing (updates = full puts); no directories exist and paths are just key-naming conventions; prefix listing is recursive like ls -R and empty directories are impossible; no links or locks, and rename is non-atomic (copy+delete). And unlike HDFS, compute and storage usually stay apart — no locality, but CPU/memory scale independently.

۸. resource manager چه می‌کند و چه ریسکی دارد؟8. What does the resource manager do, and what is its risk?

مدیرِ منابع سهمِ حافظه و پردازنده و محلِ اجرای کارها را تعیین می‌کند. تصمیمِ نامناسب می‌تواند گرسنگیِ کار، مصرفِ اضافی یا توقفِ هماهنگی بسازد؛ بازیابیِ خودِ مدیر هم مهم است.

مثال فنی: CPU آزاد، RAM ناکافی

وضعیت اولیه: job برای هر task یک هسته و ۴ گیگابایت RAM درخواست می‌کند؛ ماشین ۸ هسته و ۱۶ گیگابایت RAM دارد.

اجرای مرحله‌به‌مرحله: resource manager با این درخواست‌ها حداکثر چهار task را هم‌زمان جا می‌دهد، هرچند چهار هسته بیکار بماند؛ درخواستِ کمتر از مصرف واقعی می‌تواند OOM بسازد.

نتیجه و دلیل: تخصیص منبع فقط شمردن هسته نیست؛ سهم حافظه، محدودیت هم‌زمانی و بازپس‌گیری منابع در زمان‌بندی اثر دارند.

It keeps each node's metadata (available hardware, task states, network position) and gives the cluster's global view; the scheduler decides from exactly this view. Risk: its centrality can bottleneck scalability and availability; hence state is written to a coordination service like ZooKeeper or etcd.

۹. معمای ۱۶۰ هسته و دو job صدتایی چه چیزی را نشان می‌دهد؟9. What does the 160-cores, two-100-core-jobs puzzle show?

وقتی دو کارِ صد‌هسته‌ای با ۱۶۰ هسته رقابت می‌کنند، منابع برای خواستهٔ کاملِ هر دو کافی نیست. تقسیمِ عادلانه، پایانِ سریعِ یک کار و استفادهٔ کامل از منابع هدف‌های متفاوت‌اند.

مثال فنی: دو job روی ۱۶۰ هسته

وضعیت اولیه: دو job هرکدام ۱۰۰ task تک‌هسته‌ایِ هم‌زمان در مرحلهٔ جاری می‌خواهند.

اجرای مرحله‌به‌مرحله: با تقسیم ۸۰/۸۰ هر دو مرحله منتظر دو موج‌اند؛ تخصیصِ متفاوت می‌تواند یک job را زودتر تمام و منابعش را آزاد کند.

نتیجه و دلیل: انصاف لحظه‌ای و کمینه کردن زمان پایان همیشه یکی نیستند؛ اندازهٔ مرحله و امکان preemption در تصمیم اثر دارند.

The fairness/efficiency balance: an 80-80 split, gang scheduling (idleness and deadlock risk), waiting for 100 free cores (starvation), or preemption (killing tasks and resuming). And at large scale optimal allocation is NP-hard; so schedulers make reasonable-but-suboptimal calls with heuristics like FIFO, DRF, priority queues, quotas and bin-packing.

۱۰. preemption چه تفاوتی با خرابیِ task دارد؟10. How does preemption differ from task failure?

Preemption گرفتنِ برنامه‌ریزی‌شدهٔ منابع از یک وظیفه برای اولویتِ دیگر است؛ خرابی توقفِ ناخواسته است. هر دو ممکن است به بازیابی یا تکرار نیاز داشته باشند، ولی علت و سیاست فرق دارند.

مثال فنی: Preemption یعنی پس‌گرفتن منابع

وضعیت اولیه: scheduler برای job فوری، task کم‌اولویت را متوقف می‌کند.

اجرای مرحله‌به‌مرحله: task متوقف‌شده بعداً از checkpoint یا ابتدا ادامه می‌یابد؛ مانند crash باید اثر خروجی نیمه‌کاره کنترل شود.

نتیجه و دلیل: علت توقف سیاست زمان‌بندی است، نه خرابی ماشین؛ اما قرارداد retry و پاک‌سازی همچنان لازم است.

Both take a task out of Running, but preemption is no error: the preempted task requeues penalty-free and eats no retry budget; a Failed task (crash or lost heartbeat) has its partial output deleted, spends budget, and with attempts exhausted reaches Failed Permanently. And in practice preemption (via spot capacity and priorities) is far more common than hardware faults.

۱۱. چرا بازتلاش در سطحِ task کافی است و کلِ job را دوباره اجرا نمی‌کنیم؟11. Why is task-level retry enough instead of rerunning the whole job?

اگر ورودی و خروجیِ وظیفه‌ها جدا و قابل‌بازیابی باشند، قسمتِ شکست‌خورده دوباره اجرا می‌شود. وابستگی‌ها ممکن است بخشی از کارِ بالادستی را هم تکرار کنند؛ همیشه فقط یک وظیفه نیست.

مثال فنی: Task دوباره زمان‌بندی می‌شود

وضعیت اولیه: از ۱۰۰ task، worker اجرای task ۳۷ خاموش می‌شود.

اجرای مرحله‌به‌مرحله: scheduler task ۳۷ را روی worker دیگری اجرا می‌کند و خروجیِ attempt موفق را می‌پذیرد.

نتیجه و دلیل: تمام job لازم نیست از صفر تکرار شود، اگر ورودی و وابستگی‌های لازم برای همین task در دسترس باشند.

Because tasks read immutable input and carry no outside side effects; rerunning them is safe. Only the broken task is rescheduled on another machine, rebuilding its output from scratch — so results never duplicate and the previous attempt's partial output was deleted.

۱۲. سه استراتژیِ مدیریتِ داده‌ی intermediate کدام‌اند؟12. What are the three intermediate-data strategies?

سه راهِ کلی: پایدار کردنِ خروجیِ مراحل، نگه داشتنِ موقتِ داده همراه با امکانِ محاسبهٔ مجدد، یا ثبتِ نقطهٔ بازیابی. دادهٔ میانیِ mapper در Hadoop لزوماً در HDFS نیست؛ خروجیِ job و دادهٔ shuffle فرق دارند.

مثال فنی: نگهداری دادهٔ میانی

وضعیت اولیه: مرحلهٔ B به خروجی A نیاز دارد و workerِ A ممکن است خاموش شود.

اجرای مرحله‌به‌مرحله: راه اول خروجی پایدار روی storage است؛ راه دوم کپی روی چند worker؛ راه سوم بازساخت A از lineage.

نتیجه و دلیل: اولی I/O بیشتر، دومی هزینهٔ کپی و سومی هزینهٔ محاسبهٔ دوباره دارد؛ انتخاب به هزینه و نرخ خرابی بستگی دارد.

منبعِ توضیحِ فنی: راهنمایِ اجرا و خروجیِ میانیِ Hadoop

MapReduce: write all intermediate data to DFS and wait for the write to complete — resilient but I/O-heavy. Spark: in memory (spilling to local disk) + only the final result in DFS; recovery from lineage. Flink: periodic task-state checkpoints instead of materializing every intermediate record.

۱۳. MapReduce چهار گامش چیست و نقشِ mapper و reducer چیست؟13. What are MapReduce's four steps, and what do mapper and reducer do?

چهار گامِ اصلی: خواندنِ ورودی، map به کلید–مقدار، توزیع و گروه‌بندی بر کلید، و reduce برای خروجیِ هر گروه. چارچوب زمان‌بندی و انتقال را مدیریت می‌کند.

مثال فنی: Map، shuffle، reduce

وضعیت اولیه: متن‌های red blue و red ورودی‌اند.

اجرای مرحله‌به‌مرحله: map جفت‌های (red,1),(blue,1),(red,1) می‌دهد؛ shuffle redها را کنار هم می‌فرستد؛ reduce جمع red=2,blue=1 را می‌سازد.

نتیجه و دلیل: shuffle تصادفی نیست؛ گروه‌بندیِ کلید تعیین می‌کند همهٔ مقدارهای یک کلید به محاسبهٔ مشترک برسند.

1) Read files and break into records; 2) mapper: extract (key, value) per record, stateless, parallel; 3) implicit sort by key; 4) reducer: one call per key with an iterator over its values. The mapper prepares data for sorting; the reducer processes sorted data. A second sort = a second job.

۱۴. «shuffle» چیست و چرا نامش گمراه‌کننده است؟14. What is “shuffle” and why is the name misleading?

Shuffle تصادفی‌کردنِ داده نیست؛ توزیعِ دوباره بر اساسِ کلید است تا رکوردهای مرتبط به پردازشگرِ مناسب برسند. معمولاً شبکه و گاهی دیسک را مصرف می‌کند.

مثال فنی: Shuffle بر کلید مشتری

وضعیت اولیه: سفارش‌های مشتری ۱۷ در چند فایل و چند worker پراکنده‌اند.

اجرای مرحله‌به‌مرحله: partitioner همهٔ جفت‌های کلید ۱۷ را به reducer همان کلید می‌فرستد؛ داده ممکن است روی شبکه منتقل و sort شود.

نتیجه و دلیل: تمرکزِ یک کلید برای تجمیع لازم است؛ مشتری بسیار بزرگ می‌تواند reducer داغ و زمان پایان طولانی بسازد.

The distributed-sort algorithm with sharded input and output, for petabyte data. Misleading because shuffle output is sorted, not random (unlike shuffling cards). In MapReduce: each mapper writes one sorted file per reducer (m1r2), routing by hash(key), and reducers mergesort their share. Newer engines do it in memory or with an external-sort service (with replication).

۱۵. sort-merge join چگونه کار می‌کند و چه زمانی مناسب است؟15. How does sort-merge join work, and why prefer it to hash join?

در sort-merge، دو ورودی بر کلید مرتب و سپس هماهنگ پیمایش می‌شوند. برای ورودیِ مرتب یا دادهٔ بزرگ مناسب است؛ hash join هم بسته به اندازه، حافظه و طرحِ اجرا می‌تواند بهتر باشد.

مثال فنی: Sort-merge join روی دو جدول بزرگ

وضعیت اولیه: فروش و مشتری هر دو بر customer_id تقسیم و مرتب شده‌اند.

اجرای مرحله‌به‌مرحله: دو جریان مرتب هم‌قدم خوانده می‌شوند؛ همهٔ فروش‌های کلید ۱۷ کنار ردیف مشتری ۱۷ قرار می‌گیرند.

نتیجه و دلیل: جدول hashِ کامل لازم نیست، ولی هزینهٔ sort/shuffle و کلیدهای بسیار پرتکرار هنوز وجود دارد.

Both mappers publish user_id keys; shuffle brings one user's every record to one reducer, with secondary sort putting the profile (DOB) record first and events timestamp-ordered after. The reducer holds just one user record in memory and makes one streaming pass — no network lookups. Hash join must hold one whole side in a hash table; but shuffle output arrives pre-sorted, so merging is free and sort-join is the on-disk default.

۱۶. شش مزیتِ موتورِ dataflow بر MapReduce چیست؟16. What are dataflow's six advantages over MapReduce?

شش امکانِ مهم: مرتب‌سازیِ فقطِ لازم، ترکیبِ عملگرها، برنامه‌ریزیِ نزدیکِ داده، نگهداریِ مناسبِ دادهٔ میانی، عبورِ جریانیِ خروجی به مرحلهٔ بعد و استفادهٔ مجدد از فرایندها. بهرهٔ واقعی به موتور و طرح بستگی دارد.

مثال فنی: ادغام عملگرهای متوالی

وضعیت اولیه: مراحل filter → map → groupBy داریم.

اجرای مرحله‌به‌مرحله: موتور می‌تواند filter و map را در همان task اجرا کند و پیش از groupBy داده را shuffle کند.

نتیجه و دلیل: حذف مرزِ غیرضروریِ job، فایل میانی و راه‌اندازی را کم می‌کند؛ مرحلهٔ نیازمند توزیعِ دوباره همچنان شبکه می‌خواهد.

Sort only where needed; fusing same-sharding operators; locality optimization since the whole DAG is known; keeping intermediate state in memory/local disk; starting each operator as soon as its input is ready (pipelining); and reusing processes (MapReduce booted a fresh JVM per task).

۱۷. چرا SQL به زبانِ مشترکِ پردازشِ دسته‌ای تبدیل شد و optimizer چه می‌کند؟17. Why did SQL become batch processing's common language, and what does the optimizer do?

SQL نتیجه را به زبانِ مشترکِ بسیاری از تحلیلگران بیان می‌کند. بهینه‌ساز ترتیب و روشِ اجرا را با توجه به آمار و هزینه انتخاب می‌کند، اما کیفیتِ طرح به اطلاعاتِ موجود وابسته است.

مثال فنی: Optimizer چه تصمیمی می‌گیرد؟

وضعیت اولیه: SQL فروش را به کالا join و فقط دستهٔ book را نگه می‌دارد.

اجرای مرحله‌به‌مرحله: optimizer می‌تواند فیلترِ دسته را زودتر اعمال و با آمار اندازه، نوع join را انتخاب کند.

نتیجه و دلیل: اعلانی بودن مسیر بهینه را قابل‌انتخاب می‌کند؛ آمار قدیمی یا skew می‌تواند طرح نامناسب بسازد.

Because old warehouses had SQL, analytics and ETL tools supported it and everybody knew it — plus it enabled interactive querying. The cost-based optimizer translates queries into physical operators, picks the best join algorithm and even reorders joins to minimize intermediate state (Hive/Trino/Spark/Flink).

۱۸. تفاوت‌های semantic بینِ DataFrame محلی و توزیع‌شده چیست؟18. What semantic differences separate local and distributed DataFrames?

در DataFrame توزیع‌شده، ترتیبِ سراسری، محلِ داده و زمانِ اجرا ممکن است ضمنی نباشند. عملیاتِ ظاهراً مشابه می‌تواند تنبل، موازی یا نیازمندِ shuffle باشد.

مثال فنی: DataFrame محلی یا توزیع‌شده

وضعیت اولیه: دستور df.filter(...).groupBy(...).sum() نوشته شده است.

اجرای مرحله‌به‌مرحله: در موتور توزیع‌شده ممکن است فقط plan ساخته شود و action اجرا را شروع کند؛ collect() تمام خروجی را به driver می‌آورد.

نتیجه و دلیل: ظاهراً مشابه بودن API با جدول محلی به‌معنی یکسان بودن هزینه نیست؛ خروجی بزرگ می‌تواند حافظهٔ driver را پر کند.

The local version (R/Pandas) is usually indexed and ordered; the distributed one usually isn't — so migration can surprise ordering and performance. Plus Pandas is eager (immediate execution) while Spark is lazy (query plan + optimizer), and Daft has both: small ops on the client, big datasets on the server, with Arrow as the shared data model.

۱۹. چرا نوشتنِ مستقیمِ خروجیِ job به پایگاه دادهِ تولیدی بد است و راهِ بهتر چیست؟19. Why is writing job output straight to the production database bad, and what is better?

نوشتنِ مستقیم می‌تواند خروجیِ نیمه‌کاره، اثرِ تکراری و بارِ زیاد بسازد. نتیجه را جدا می‌سازیم، اعتبارسنجی می‌کنیم و سپس نسخهٔ کامل را در مسیرِ خواندن قرار می‌دهیم.

مثال فنی: انتشار ایندکس کامل

وضعیت اولیه: job نسخهٔ search-v2 را در مسیر تازه می‌سازد و تعداد و کیفیت رکوردها را می‌سنجد.

اجرای مرحله‌به‌مرحله: پس از موفقیتِ همهٔ بخش‌ها، اشاره‌گر خواندن از v1 به v2 عوض می‌شود؛ v1 برای بازگشت موقتاً می‌ماند.

نتیجه و دلیل: کاربر مخلوطِ خروجی ناقص و قدیمی نمی‌بیند؛ نوشتن مستقیمِ هر task روی ایندکس فعال این مزیت را سخت‌تر می‌کند.

One network request per record (slow), drowning the database with parallel tasks, and destroying the all-or-nothing guarantee (outside side effects + retried-output duplication). Better: push to Kafka — sequential writes, buffering/rate control, multi-system consumption, a DMZ security boundary — plus a job-done announcement so consumers hide data like a read-committed transaction until completion is announced. The other pattern: bulk-load a fresh database + an atomic dataset-version switch.

۲۰. اگر قرار باشد فقط یک جمله از این فصل بگویی، کدام است؟20. If you had one sentence from this chapter, which?

پردازشِ دسته‌ای ورودیِ مشخص را به خروجیِ قابل‌بازسازی تبدیل می‌کند. جداسازیِ تلاش‌های ناقص و انتشارِ نسخهٔ کامل، به تحملِ خرابی و اصلاحِ منطق کمک می‌کند.

مثال فنی: بازمحاسبهٔ گزارش از ورودی ثابت

وضعیت اولیه: فایل فروش روزِ ۱۴ مهر بسته شده و نسخهٔ ورودی مشخص است.

اجرای مرحله‌به‌مرحله: job مجموع فروش هر شعبه را در خروجی نسخهٔ v2 می‌نویسد؛ اگر فرمول غلط بود با کد اصلاح‌شده از همان ورودی تکرار می‌شود.

نتیجه و دلیل: ورودی immutable و خروجی نسخه‌دار، خطای محاسبه را قابل‌بازسازی می‌کند؛ بازنویسی مستقیمِ دادهٔ مرجع چنین آزادی‌ای نمی‌دهد.

“Batch processing means turning immutable input into derived output from scratch; that contract makes task retries safe, errors recoverable and publication all-or-nothing — and for scale, spread the work with shuffle (distributed sorting) across independent tasks.”

۶.۳. اشتباهات رایج در جواب‌های مصاحبه6.3. Common mistakes in interview answers

  • ❌ «batch یعنی داده را دسته‌ای آپدیت می‌کنیم.» → ✅ نه؛ ورودیِ job immutable است و خروجی از صفر ساخته می‌شود؛ job به دادهٔ موجود دست نمی‌زند.
  • ❌ «shuffle یعنی داده را تصادفی پخش می‌کنیم.» → ✅ shuffle خروجیِ مرتب می‌دهد؛ پایهٔ join و grouping است، نه تصادف.
  • ❌ «برای هر تغییرِ ورودی، فقط بخشِ تغییرکرده را پردازش می‌کنیم.» → ✅ در مدلِ کلاسیکِ دسته‌ای، حتی یک بایت تغییر یعنی پردازشِ دوبارهٔ کلِ ورودی.
  • ❌ «task که شکست خورد، job را از اول اجرا می‌کنیم.» → ✅ بازتلاش در سطحِ task است؛ کلِ job فقط وقتی شکست می‌خورد که taskها با budgetِ تمام‌شده بمانند.
  • ❌ «preempted یعنی failed.» → ✅ preemption خطا نیست: task بدونِ جریمه دوباره صف می‌شود؛ فقط Failed از budgetِ بازتلاش می‌خورد.
  • ❌ «خروجیِ job همزمان با نوشتنِ shardها دیده می‌شود.» → ✅ خروجی تا اعلانِ پایانِ job پنهان است (all-or-nothing، read-committed reveal).
  • ❌ «برای انتشار، مستقیم رکوردرکورد به پایگاه دادهِ تولیدی می‌نویسیم.» → ✅ کند، غرق‌کننده و ناقص‌کنندهٔ all-or-nothing؛ راهِ درست push به جریان یا bulk-load + سوییچِ اتمیک است.
  • ❌ «object store هم فایل‌سیستم است؛ rename و قفل هم دارد.» → ✅ rename غیراتمیک است (کپی+حذف)، link و قفل ندارد و دایرکتوریِ خالی ممکن نیست.
  • ❌ «scheduler می‌تواند تخصیصِ بهینهٔ منابع را حساب کند.» → ✅ مسئله NP-hard است؛ فقط heuristics (FIFO، DRF، quota، bin-packing) داریم.
  • ❌ «workflow این‌جا همان durable execution فصل ۵ است.» → ✅ این‌جا یعنی DAG از jobهای دسته‌ای با داده‌ی ورودی/خروجی، معمولاً بدونِ RPC.
  • ❌ «DataFrame توزیع‌شده دقیقاً مثل Pandas رفتار می‌کند.» → ✅ ایندکس و ترتیبِ نسخه‌ی محلی را ندارد و lazy است (query plan) — مهاجرت غافلگیری دارد.
  • ❌ «HDFS و S3 از نظرِ معماری یکی‌اند.» → ✅ HDFS اجرای نزدیکِ داده (locality) می‌دهد؛ object storeها compute و storage را جدا می‌کنند و تضمین‌های متفاوتی دارند.
  • ❌ “Batch means updating data in batches.” → ✅ No; job input is immutable and output is built from scratch; the job never touches existing data.
  • ❌ “Shuffle means scattering data randomly.” → ✅ Shuffle yields sorted output; it underpins joins and grouping, not randomness.
  • ❌ “For each input change, we only process the changed part.” → ✅ In the classic batch model, even one changed byte means reprocessing the whole input.
  • ❌ “When a task fails, we rerun the job from the start.” → ✅ Retries are task-level; the whole job fails only when tasks remain with budgets spent.
  • ❌ “Preempted means failed.” → ✅ Preemption is no error: the task requeues penalty-free; only Failed eats retry budget.
  • ❌ “Job output is visible while shards are being written.” → ✅ Output stays hidden until the job-done announcement (all-or-nothing, read-committed reveal).
  • ❌ “For publication we write record by record straight to the production DB.” → ✅ Slow, drowning, and all-or-nothing-breaking; push to a stream or bulk-load + atomic switch instead.
  • ❌ “An object store is a filesystem too; it has rename and locks.” → ✅ Rename is non-atomic (copy+delete), no links or locks exist, and empty directories are impossible.
  • ❌ “The scheduler can compute the optimal resource allocation.” → ✅ The problem is NP-hard; only heuristics (FIFO, DRF, quotas, bin-packing) exist.
  • ❌ “Workflow here is Chapter 5's durable execution.” → ✅ Here it means a DAG of batch jobs with input/output data, usually no RPC.
  • ❌ “A distributed DataFrame behaves exactly like Pandas.” → ✅ It lacks the local version's indexing and ordering and is lazy (query plan) — migration surprises.
  • ❌ “HDFS and S3 are architecturally one thing.” → ✅ HDFS gives near-data execution (locality); object stores separate compute and storage with different guarantees.

۶.۴. چک‌لیستِ ۵ دقیقه‌ای قبل از مصاحبه6.4. Five-minute checklist before the interview

  1. تعریفِ jobِ دسته‌ای: ورودیِ immutable، خروجیِ derived از صفر، throughput به‌جای response time.
  2. سه مزیتِ مدل (time travel، human fault tolerance، توسعهٔ سریع‌تر) و دو بهایش (خروجی تا پایانِ job، بازپردازشِ کلِ ورودی).
  3. توضیحِ pipelineِ لاگ: awk، sort، uniq -c، sort -r -n، head — و اینکه چرا sort لازم است.
  4. تفاوتِ sort با تجمیعِ hash در حافظه: working set، spill، mergesort و دسترسیِ ترتیبی.
  5. سه لایهٔ پلتفرم (ذخیره‌سازی/هماهنگ‌سازی/محاسبه) و قیاس با سیستم‌عامل.
  6. سه تفاوتِ کلیدیِ object store با فایل‌سیستم: immutability، پیشوند/فهرستِ بازگشتی، نبودِ rename اتمی و قفل.
  7. معمای تخصیصِ منابع: انصاف/کارایی، gang scheduling، preemption، NP-hard و heuristics.
  8. سه استراتژیِ داده‌ی intermediate و اینکه چرا بازتلاش در سطحِ task امن است.
  9. مکانیزمِ shuffle (hash(key)، فایلِ per-reducer مثل m1r2، mergesort سمتِ reduce) و sort-merge join با secondary sort.
  10. شش مزیتِ dataflow بر MapReduce و ماجرای انتشارِ all-or-nothing (push به Kafka + اعلانِ پایانِ job).
  1. The batch-job definition: immutable input, from-scratch derived output, throughput over response time.
  2. The model's three wins (time travel, human fault tolerance, faster development) and two prices (output only at job end, full-input reprocessing).
  3. The log pipeline explained: awk, sort, uniq -c, sort -r -n, head — and why sort is needed.
  4. Sort vs in-memory hash aggregation: working set, spills, mergesort and sequential access.
  5. The platform's three layers (storage/orchestration/compute) and the OS analogy.
  6. Object store's three key filesystem differences: immutability, prefix/recursive listing, no atomic rename or locks.
  7. The allocation puzzle: fairness/efficiency, gang scheduling, preemption, NP-hardness and heuristics.
  8. The three intermediate-data strategies and why task-level retry is safe.
  9. The shuffle mechanism (hash(key), per-reducer files like m1r2, reduce-side mergesort) and sort-merge join with secondary sort.
  10. Dataflow's six wins over MapReduce and the all-or-nothing publication story (Kafka push + job-done announcement).

۷7 تمرین‌های مصاحبه و آزمایشگاهInterview exercises and hands-on labs

این تمرین‌ها برای مسیر Backend / Distributed Systems در ۱۴ اکتبر ۲۰۲۶ بازبینی شدند. سطح مربوط به نسخهٔ مصاحبه‌ای است؛ زمان آزمایشگاه جداست.Reviewed on 14 October 2026 for Backend / Distributed Systems. Level refers to the interview brief; full-lab time is separate. روش ارزیابی و مسیر مطالعهMethod and study path

تمرین ۱ — pipeline در برابرِ تجمیعِ hashExercise 1 — pipeline vs hash aggregation

L2 · SeniorL2 · Seniorوابسته به نقشRole-specific30 دقیقه بحث30 min discussion

memory-bounded aggregation برای data/platform کاربرد دارد؛ pipeline یونیکس ابزار آموزشی است و آستانه URL عدد عمومی ندارد.Useful data/platform work; no universal distinct-URL threshold exists.

نسخهٔ بازبینی‌شده برای مصاحبهReviewed interview brief

top URLها را برای لاگی بزرگ‌تر از RAM محاسبه کن. hash aggregation و external sort را با cardinality، RAM، spill، I/O و approximate counting مقایسه کن. benchmark را با ورودی، memory cap و خروجی برابر طراحی کن؛ آستانه شکست را برای همان محیط گزارش کن.Compute top URLs for logs larger than RAM. Compare hash aggregation, external sort, spilling and approximate counts. Benchmark equal outputs under a stated memory cap and report environment-specific limits.

معیار پاسخ خوبWhat a good answer demonstrates

  • working set به distinct keys و overhead وابسته استCardinality and object overhead
  • OOM را به کندی عادی تعبیر نمی‌کندOOM versus latency
  • sort spill و hash spill/partition دیده شده‌اندSpill and partition strategies

پیگیری مصاحبه:Interview follow-up: فقط top-100 تقریبی لازم باشد، کدام قید را می‌توان سبک‌تر کرد؟If only an approximate top-100 is required, which constraint can be relaxed?

آزمایشگاه تکمیلی · زمان تقریبی ۲–۴ ساعتExtended hands-on laboratory · estimated 2–4 hours

یک فایلِ لاگِ نمونه (چند صد هزار خط) بساز یا از لاگِ واقعیِ وب‌سرور استفاده کن. pipelineِ کاملِ cat | awk | sort | uniq -c | sort -r -n | head را با دستورهای واقعی اجرا کن و زمان بگیر. بعد همان کار را با تجمیعِ hash در پایتون پیاده کن و مقایسه کن. حالا حجمِ ورودی را آن‌قدر زیاد کن که working set از حافظه بگذرد و ببین چه اتفاقی می‌افتد. سوال: دقیقاً در چه نقطه‌ای (چند URL متمایز؟) روشِ hash از sort کُندتر/ناممکن می‌شود و چرا؟

Build a sample log file (a few hundred thousand lines) or use a real web-server log. Run the full cat | awk | sort | uniq -c | sort -r -n | head pipeline with real commands and time it. Then do the same with hash aggregation in Python and compare. Now grow the input until the working set exceeds memory and watch what happens. Question: at exactly what point (how many distinct URLs?) does hashing get slower/impossible than sort, and why?

برای یادگیریِ عمیق است؛ انجام کاملش در زمان بحثِ مصاحبه انتظار نمی‌رود.The full laboratory is optional depth work and is not expected within the discussion time.

ارزیابی، سطح و شواهد این تمرینAssessment, level and evidence · حل آموزشی این تمرینWorked solution · Microsoft · technical interviewing · Atlassian · engineering interview handbook

تمرین ۲ — sort-merge join روی دو فایلِ بزرگ‌تر از حافظهExercise 2 — sort-merge join on two bigger-than-memory files

L2 · SeniorL2 · Seniorوابسته به نقشRole-specific40 دقیقه بحث40 min discussion

دو جریان جدا فقط با user_id مرتب می‌شوند و یک پروفایل یکتا به‌ازای کاربر دارند؛ mixed reducer به profile-first یا buffering نیاز دارد.Two separate streams need user_id sorting and one unique profile per user; a mixed reducer needs profile-first ordering or buffering.

نسخهٔ بازبینی‌شده برای مصاحبهReviewed interview brief

کلیک‌های بزرگ و یک پروفایل یکتا به‌ازای user را با external sort و streaming merge join ترکیب کن. null/missing profile و skew را تعریف کن. سپس ورودی mixed reducer را مقایسه کن که در آن profile-first secondary sort یا buffering لازم است؛ تفاوت دو قرارداد را بگو.Externally sort and merge clicks with one unique profile per user. Define missing-profile behavior and skew. Contrast this with a mixed reducer stream that needs profile-first ordering or buffering; explain the different contracts.

معیار پاسخ خوبWhat a good answer demonstrates

  • sort بزرگ‌تر از حافظه spill واقعی داردReal external spill
  • uniqueness پروفایل و join type مشخص استProfile uniqueness and join type
  • دو sorted stream را با mixed reducer اشتباه نمی‌گیردSeparate streams versus mixed reducer

پیگیری مصاحبه:Interview follow-up: اگر برای هر user چند نسخهٔ پروفایل تاریخی باشد، کدام نسخه باید به کلیک وصل شود؟With multiple historical profiles per user, which version belongs to a click?

آزمایشگاه تکمیلی · زمان تقریبی ۳–۶ ساعتExtended hands-on laboratory · estimated 3–6 hours

دو CSV کلیک (user_id,url,ts) و پروفایل یکتا (user_id,dob) بساز که از سقف chunk مرتب‌سازی بزرگ‌تر باشند. هر دو را با spill به دیسک برحسب user_id مرتب کن و با دو cursor، left join بده؛ پروفایل گمشده NULL و metric دارد. توضیح بده چرا join به O(1) state پروفایل نیاز دارد ولی حافظهٔ sort و merge fan-in جدا محاسبه می‌شود. سپس یک جریان mixed reducer بساز که در آن کلیک پیش از پروفایل می‌رسد و هزینهٔ buffering را با profile-first secondary sort مقایسه کن. ترتیب ts فقط اگر خروجی آن را بخواهد لازم است. skew و پروفایل تکراری را آزمایش و سیاست رد یا تجمیع آن را صریح تعیین کن.

Create clicks (user_id,url,ts) and unique profiles (user_id,dob) CSVs larger than the sort chunk limit. Externally sort both by user_id with real disk spills, then left-join using two cursors; emit NULL and a metric for missing profiles. Explain O(1) profile state while accounting separately for sort memory and merge fan-in. Contrast a mixed reducer receiving clicks before profiles: compare buffering with profile-first secondary sort. Timestamp order is required only if the output contract demands it. Test skew and duplicate profiles with an explicit reject/aggregation policy.

برای یادگیریِ عمیق است؛ انجام کاملش در زمان بحثِ مصاحبه انتظار نمی‌رود.The full laboratory is optional depth work and is not expected within the discussion time.

ارزیابی، سطح و شواهد این تمرینAssessment, level and evidence · حل آموزشی این تمرینWorked solution · Microsoft · technical interviewing · Atlassian · engineering interview handbook

تمرین ۳ — یک shuffle سادهExercise 3 — a mini shuffle

L3 · Staff / PrincipalL3 · Staff / Principalوابسته به نقشRole-specific40 دقیقه بحث40 min discussion

shuffle و skew برای data infra مفیدند؛ نوشتن همهٔ فایل‌ها از صفر برای backend عمومی بازده زمانی پایینی دارد.Useful for data infrastructure; building every file is an optional extended lab.

نسخهٔ بازبینی‌شده برای مصاحبهReviewed interview brief

shuffle برای group-by را طراحی کن: partition یکسان، sort محلی و merge در reducer. hot key، حجم شبکه، combiner و checksum فایل میانی را تحلیل کن. با خرابی خروجی یک mapper تعیین کن چه چیزی و بر اساس کدام lineage دوباره اجرا می‌شود.Design partitioning, mapper-local sorting and reducer merge for group-by. Analyze hot keys, network volume, combiners and checksums. Recompute lost intermediate output from immutable lineage using isolated attempts.

معیار پاسخ خوبWhat a good answer demonstrates

  • hash در workerها ثابت و مشترک استStable worker partitioning
  • retry mapper خروجی attempt جدا داردAttempt isolation
  • side effect بیرونی قرارداد replay را نقض می‌کندDeterministic side-effect-free retry

پیگیری مصاحبه:Interview follow-up: یک key نصف داده را دارد؛ چگونه reducer را از hotspot نجات می‌دهی؟One key accounts for half the records. How do you prevent a reducer hotspot?

آزمایشگاه تکمیلی · زمان تقریبی ۴–۸ ساعتExtended hands-on laboratory · estimated 4–8 hours

یک shuffleِ چند-worker بنویس: N کارگر که رکوردها را بر اساسِ hash(key) % N partition می‌کنند، هر worker به‌ازای هر partition یک فایلِ مرتب می‌نویسد (مثلِ m1r2)، و مرحلهٔ reduce فایل‌های هر partition را mergesort می‌کند. روی داده‌ی نمونه تست کن که همه‌ی رکوردهای هم‌کلید کنارِ هم برسند. سوال: اگر یکی از فایل‌های میانیِ یک worker خراب شود، برای ادامهٔ job چه چیزی را باید دوباره اجرا کنی — و چرا اجرای دوباره‌اش امن است؟

Write a multi-worker shuffle: N workers partitioning records by hash(key) % N, each worker writing one sorted file per partition (like m1r2), with the reduce stage mergesorting each partition's files. Test on sample data that same-key records all land together. Question: if one worker's intermediate file corrupts, what must you rerun to continue the job — and why is rerunning it safe?

برای یادگیریِ عمیق است؛ انجام کاملش در زمان بحثِ مصاحبه انتظار نمی‌رود.The full laboratory is optional depth work and is not expected within the discussion time.

ارزیابی، سطح و شواهد این تمرینAssessment, level and evidence · حل آموزشی این تمرینWorked solution · Microsoft · technical interviewing · Atlassian · engineering interview handbook

تمرین ۴ — شبیه‌سازیِ بازتلاش و all-or-nothingExercise 4 — simulating retry and all-or-nothing

L3 · Staff / PrincipalL3 · Staff / Principalوابسته به نقشRole-specific40 دقیقه بحث40 min discussion

atomic publication کاربرد جدی دارد؛ rename فایل‌سیستم محلی را نباید بی‌قید به object storage تعمیم داد.Atomic publication is valuable; local rename semantics do not automatically transfer to object storage.

نسخهٔ بازبینی‌شده برای مصاحبهReviewed interview brief

job چندtaskی روی ورودی immutable را با attempt output و manifest نسخه‌دار طراحی کن. crash وسط write و پیش از publish تزریق کن. خواننده باید یک نسخهٔ کامل ثابت ببیند؛ CAS/atomic pointer، cleanup و retry را متناسب با storage انتخاب کن.Design versioned attempt outputs and a job manifest. Inject mid-write and pre-publication crashes. Readers pin one complete manifest; select a storage-appropriate atomic pointer or CAS and safe garbage collection.

معیار پاسخ خوبWhat a good answer demonstrates

  • task commit با job publish یکی نیستTask completion versus job publication
  • reader manifest را یک بار pin می‌کندPinned reader manifest
  • partial attempt و نسخه قدیم امن مدیریت می‌شوندSafe incomplete output cleanup

پیگیری مصاحبه:Interview follow-up: دو job هم‌زمان روی یک نام خروجی publish کنند؛ کدام نسخه برنده می‌شود؟Two jobs concurrently publish to the same output name. Which version wins?

آزمایشگاه تکمیلی · زمان تقریبی ۳–۶ ساعتExtended hands-on laboratory · estimated 3–6 hours

یک jobِ چندtaskی بساز که shardهای ورودیِ immutable را به shardهای خروجی تبدیل می‌کند. سپس یک «خراب‌کننده» اضافه کن: با احتمالِ مشخص، وسطِ نوشتنِ خروجیِ یک task بمیرد. منطقِ بازتلاش را پیاده کن: حذفِ خروجیِ ناقص، اجرای دوبارهٔ task، و commit فقط بعد از موفقیتِ همه. در پایان تست کن: خواننده یا «کلِ نتیجه» می‌بیند یا هیچ‌چیز، حتی با چندین خرابیِ وسطِ راه. سوال: اگر به‌جای حذفِ خروجیِ ناقص، آن را نگه داری و task دوباره اجرا شود، چه نتیجه‌ای ممکن است تولید شود؟

Build a multi-task job turning immutable input shards into output shards. Then add a “killer”: with some probability, die mid-way through writing one task's output. Implement retry logic: delete partial output, rerun the task, commit only after all succeed. Finally test: readers see either “the whole result” or nothing, even with several mid-way failures. Question: keeping partial output instead of deleting it, then rerunning the task — what could that produce?

برای یادگیریِ عمیق است؛ انجام کاملش در زمان بحثِ مصاحبه انتظار نمی‌رود.The full laboratory is optional depth work and is not expected within the discussion time.

ارزیابی، سطح و شواهد این تمرینAssessment, level and evidence · حل آموزشی این تمرینWorked solution · Atlassian · engineering interview handbook · Jepsen · consistency models

نکته‌ی پایانیClosing note
این فصل نشان داد یک قراردادِ ساده — «ورودیِ immutable، خروجیِ از صفر» — چطور کلِ معماری را شکل می‌دهد: بازتلاشِ task امن می‌شود، خطاها قابلِ بازیابی‌اند، و انتشارِ نتیجه all-or-nothing می‌ماند. و در مقیاس، همه‌چیز به یک الگوریتمِ بنیادی برمی‌گردد: shuffle — مرتب‌سازیِ توزیع‌شده‌ای که join و تجمیع را ممکن می‌کند. در فصل ۱۲ همین ایده‌ها را با یک تغییرِ اساسی می‌بینیم: ورودی دیگر محدود نیست و job هرگز «تمام» نمی‌شود.
This chapter showed how one simple contract — “immutable input, from-scratch output” — shapes the whole architecture: task retries become safe, errors recoverable, and result publication stays all-or-nothing. And at scale everything reduces to one fundamental algorithm: shuffle — the distributed sort enabling joins and aggregation. Chapter 12 revisits the same ideas with one fundamental change: input is no longer bounded and the job never “finishes”.