وقتی دربارهٔ Apache Spark صحبت میکنیم، معمولاً ذهن ما میرود سمت DataFrameها، RDDها، کوئریهای SQL یا کلاسترهای بزرگ.
اما حقیقت این است که بخش بزرگی از سرعت خارقالعادهٔ اسپارک از دو پروژه نشأت میگیرد که شاید بسیاری از مهندسان داده حتی اسمشان را هم نشنیده باشند:
این مقاله دقیقاً برای توضیح همین دو بخش نوشته شده است—اما نه به صورت یک متن خشک و دانشگاهی؛ بلکه با مثالهای واقعی و روایت مرحلهبهمرحله از اینکه دقیقاً وقتی یک فرمان ساده Spark SQL اجرا میکنید، چه اتفاقی میافتد.
فرض کنید این کوئری را اجرا میکنید:
result = (
df.filter("age > 30")
.groupBy("country")
.agg(sum("salary").alias("total_salary"))
.orderBy("total_salary", ascending=False)
)
در ظاهر، یک فیلتر، یک groupBy و یک مرتبسازی است.
اما پشت صحنه، اسپارک یک فرآیند چندمرحلهای پیچیده انجام میدهد شامل:
همهٔ اینها با دو جزء انجام میشود: Catalyst و Tungsten.
بیایید شروع کنیم.
Catalyst یک Query Optimizer مدرن است که اسپارک برای فهمیدن، تحلیل و بهینهسازی عملیات شما از آن استفاده میکند.
در عمل Catalyst باعث میشود:
بگذارید مراحل آن را از نزدیک ببینیم.
وقتی دستور زیر اجرا میشود:
df.filter("age > 30")
Catalyst نمیداند:
در این مرحله ONLY هدف ثبت میشود:
«تصفیه دادهای به نام age با شرط بیش از ۳۰»
Catalyst یک گراف منطقی خام میسازد که به آن Unresolved Logical Plan میگوییم.
در این مرحله، Catalyst:
اگر همهچیز درست باشد، یک Resolved Logical Plan به دست میآید که حالا قابل بهینهسازی است.
Catalyst در این مرحله شروع میکند به:
فیلتر را تا حد ممکن نزدیک به منبع داده میبرد. البته این امر نیازمند این است که منبع داده هم این مورد را پشتیبانی کند . مثلا فایلهای پارکت، چون آمار و اطلاعات خلاصه ای راجع به دادهها ذخیره می کنند می توانیم از آنها استفاده کنیم و اگر دادهای شرایط مورد نیاز را نداشت، از همان ابتدا اصلا بارگذاری و لود هم نشود.
چرا؟
چون هر چه زودتر داده کم شود، عملیات بعدی سریعتر میشود.
مثلاً:
select name, age
select name
به یک select تبدیل میشود.
اگر sort در یک stage کافی باشد، sort اضافی حذف میشود.
مثلاً:
Catalyst بر اساس اندازه دیتا بهترین join را انتخاب میکند.
Catalyst همیشه تلاش میکند بازتوزیع دادهها (Shuffle) کمتر شود.
حالا Catalyst باید تصمیم بگیرد:
نتیجه:
نمونه خروجی:
== Physical Plan ==
*(۳) Sort [total_salary DESC], true
*(۲) HashAggregate(keys=[country], functions=[sum(salary)])
*(۱) Filter (age > 30)
*(۰) Scan parquet ...
اما این فقط “نقشه” است.
اجرای واقعی توسط Tungsten انجام میشود.

اگر Catalyst “مغز” باشد، Tungsten «عضله» است.
Tungsten به این علت معرفی شد که JVM و Garbage Collector در پردازشهای سنگین Big Data کارآمد نیستند.
سه ستون Tungsten:
به جای اینکه اسپارک مجموعههای عظیم داده را داخل حافظه JVM نگه دارد، آنها را خارج از JVM و در حافظه خام نگه میدارد.
مزایا:
Tungsten تصمیم میگیرد:
به همین دلیل Spark SQL اغلب سریعتر از RDD است—چون RDD همیشه row-based است.
اینجا جادو آغاز میشود.
به جای اینکه برای هر مرحله یک اپراتور جدا اجرا شود،
Spark کل pipeline را تبدیل میکند به یک بلاک کد جاوا بسیار سریع:
for (InternalRow row : input) {
if (row.age > 30) {
hash_aggregate(row.country, row.salary);
}
}
نتیجه:
کوئری ما:
result = (
df.filter("age > 30")
.groupBy("country")
.agg(sum("salary"))
.orderBy("total_salary")
)
مسیر کامل اجرای آن:
Catalyst یک گراف منطقی خام میسازد.
Catalyst ستون age، salary، country را resolve میکند.
قوانین زیر اعمال میشود:
Catalyst بهترین پلان اجرا را انتخاب میکند:
Catalyst و Tungsten دلیل کلیدی این هستند که Spark به یک موتور پردازش داده مدرن تبدیل شده:
این دو بخش پنهانترین – اما مهمترین – دلایلی هستند که Spark SQL را به یکی از سریعترین موتورهای پردازش داده در جهان تبدیل کردهاند.
اما بیایید کمی عمیقتر بررسی کنیم که چرا این بهینهسازی ها روی دادههای ساختیافته، واقعا کار میکند و چرا توصیه میکنیم به جای استفاده از RDD ها برای پردازش داده، حتما از دیتافریمها یا SQL استفاده کنیم ؟ چرا RDD ها بسیار کندتر از دیتافریمها یا دستورات SQL هستند ؟

RDDها “شیمحور” و غیرقابل تجزیه هستند. اسپارک نمیتواند داخل یک فانکشن پایتون یا جاوا را درک کند؛ فقط میداند شما یک تابع map دادهاید.
اما برای DataFrame/Dataset:
Catalyst چند کار انجام میدهد:
Column و relation و expression ها را بررسی و resolve میکند.
مثل:
Catalyst انتخاب میکند از چه الگوریتمهایی استفاده شود:
بههمین دلیل DataFrame/Dataset از RDD همیشه سریعتر هستند.
(قلب تنگستن / Tungsten)
بعد از اینکه Catalyst بهترین پلن را تولید کرد، Tungsten وارد بازی میشود.
DataFrame/Dataset دادهها را مثل RDD در قالب آبجکتهای JVM ذخیره نمیکنند.
بلکه از یک binary row format استفاده میکنند.
بهجای اینکه دادهها این شکلی باشند:
Row(id=1, name="A", price=10.5)
Row(id=2, name="B", price=20.0)
آنها در یک آرایه فشردهی باینری نگهداری میشوند:
[ ۱ | ۰۳ ۴۱ ... | ۱۰.۵ ]
[ ۲ | ۰۳ ۴۲ ... | ۲۰.۰ ]
✔ حذف کامل object overhead (هر Row دیگر ۲۴–۷۲ بایت اضافه مثل RDD ندارد)
✔ بسیار مناسب CPU cache
✔ امکان اجرای work را با سرعت نزدیک C++
✔ عملیاتهایی مثل sort، join و groupBy بسیار سریعتر میشوند
✔ انتقال داده بین نودها کوچکتر است
در JVM معمولاً دادهها داخل heap ذخیره میشوند.
اما Tungsten:
✔ کاهش محسوس GC (Garbage Collection)
✔ مدیریت حافظه دقیقتر
✔ عملکرد پیوستهتر تحت بار سنگین
✔ قابلیت shared memory و memory mapping
به همین دلیل Spark SQL توانسته است به سرعتهای بسیار بالاتر از نسخههای قبل برسد.
SQL:
SELECT category, SUM(price)
FROM sales
GROUP BY category
Catalyst:
Tungsten:
| Component | نقش |
|---|---|
| Catalyst | تبدیل و بهینهسازی query (logical → physical) |
| Tungsten | اجرای سریع با binary format و off-heap memory |
| DataFrame/Dataset | لایهای ساختاریافته که از هر دو استفاده میکند |
| Row-Oriented Off-Heap Representation | کاهش GC، استفاده مؤثر از CPU، افزایش سرعت |