بررسی یک سناریوی دنیای واقعی از یک نمونه بکارگرفته شده از راست
ساعت ۳ صبح، خط لوله دادهی (Data Pipeline) ما از کار افتاد. پایتون مقصر نبود، اما راست ناجی شد.
تیمی از مهندسان داده در یک استارتاپ فینتک، هر شب میلیونها تراکنش را با پایتون پردازش میکنند. یک شب، حجم تراکنشها ۱۰ برابر میشود (تخفیف نوروزی و کمپینهای پر سر و صدا) و خط لولهای که قبلاً ۴۵ دقیقه زمان میبرد، حالا ۸ ساعت طول میکشد. درست زمانی که گزارشهای صبحگاهی باید آماده باشند. تیم مجبور میشود در بحبوحهی شب، به دنبال راهحلی باشد. اینجا بود که یکی از اعضای تیم پیشنهاد داد: «چرا بخش بحرانی را با راســــت بازنویســــی نکنیــــــم؟»
لحظه تشخیص مشکل (2:45 صبح)
ساعت ۲:۴۵ بامداد بود که زنگهای هشدار در داشبورد تیم به صدا درآمد. سارا، lead data engineer تیم با چشمانی که از خواب آلودگی قرمز شده بود، به مانیتور زل زده بود:
Lineage Dashboard: دادههای تراکنشهای روز هنوز پردازش نشده است Estimated completion: 8 hours from now
“این چی بود؟!” با خودش گفت. خط لولهای که همیشه ۴۵ دقیقه زمان میبرد، حالا برآورد ۸ ساعت داشت. آن هم درست شبی که فردا صبح جلسه گزارشدهی ماهانه با مدیران بود.
با چند کلیک، لاگها را بررسی کرد. حجم تراکنشها از ۲ میلیون به ۲۲ میلیون رسیده بود. تخفیف ۷۰ درصدی نوروزی کار خودش را کرده بود.
کد پایتون که از Pandas استفاده میکرد، اینگونه شروع میشد:
import pandas as pd
def process_transactions(df):
# پاکسازی داده
df = df.dropna(subset=['transaction_id'])
df = df[df['amount'] > 0]
# محاسبات سنگین
df['risk_score'] = df.apply(
lambda row: calculate_risk(row['user_id'], row['amount'], row['merchant']),
axis=1
)
# گروهبندی و جمعآوری
result = df.groupby('user_id').agg({
'amount': ['sum', 'count'],
'risk_score': 'max'
}).reset_index()
return resultاین کد روی ۲ میلیون رکورد خوب کار میکرد. اما حالا با ۲۲ میلیون، حافظه از حد فراتر رفته بود و swap به شدت در حال استفاده بود.
جلسه فوری (۳:۱۵ صبح)
تیم در Google Meet جمع شدند. رضا، مهندس ارشد بکاند، اولین پیشنهاد را مطرح کرد: “بیایید کد را با Dask موازی کنیم.”
سارا مخالفت کرد: “الان وقتش نیست. باید تنظیمات کلاستر رو عوض کنیم و احتمالاً با باگهای جدید مواجه بشیم.”
سپهر، که تازه به تیم اضافه شده بود و سابقه کار با راست داشت، با احتیاط گفت: “میدونم پیشنهاد عجیبیه… ولی بخش محاسبه risk_score رو با راست بنویسم؟ اون بخش ۷۰٪ زمان رو میگیره.”
سکوت سنگینی در جلسه افتاد. همه میدانستند که اضافه کردن زبان جدید در بحبوحه بحران ریسک بالایی دارد.
سپهر ادامه داد: “فقط همون تابع رو بازنویسی میکنیم. از PyO3 استفاده میکنیم تا تابع راست رو مستقیم توی پایتون صدا بزنیم. ریسکش پایینه.”
تابع راست (ساعت ۳:۴۵)
سپهر شروع به نوشتن کرد. میدانست که باید از Rayon برای پردازش موازی استفاده کند:
use pyo3::prelude::*;
use rayon::prelude::*;
#[pyfunction]
fn calculate_risk_batch(transactions: Vec<(String, f64, String)>) -> Vec<f64> {
transactions
.par_iter() // پردازش موازی با Rayon
.map(|(user_id, amount, merchant)| {
let mut risk = 0.0;
// قوانین ریسک - بدون هزینه اضافی پایتون
if amount > &1_000_000.0 {
risk += 0.4;
}
if is_high_risk_merchant(merchant) {
risk += 0.3;
}
if get_user_velocity(user_id) > 10.0 {
risk += 0.2;
}
risk.min(1.0)
})
.collect()
}
#[pymodule]
fn risk_engine(_py: Python, m: &PyModule) -> PyResult<()> {
m.add_function(wrap_pyfunction!(calculate_risk_batch, m)?)?;
Ok(())
}نکته مهم: این تابع بدون garbage collector کار میکرد و حافظه را به صورت خطی و قابل پیشبینی مدیریت مینمود.
لحظه حقیقت (ساعت ۴:۳۰)
کامپایل شد. تستها پاس شدند. سپهر تابع را در کد اصلی جایگزین کرد:
import risk_engine # تابع راست که حالا به صورت ماژول پایتون درآمده
def process_transactions(df):
df = df.dropna(subset=['transaction_id'])
df = df[df['amount'] > 0]
# آمادهسازی داده برای راست
transactions = list(zip(df['user_id'], df['amount'], df['merchant']))
# فراخوانی تابع راست - اینجا معجزه اتفاق میافتد
df['risk_score'] = risk_engine.calculate_risk_batch(transactions)
result = df.groupby('user_id').agg({
'amount': ['sum', 'count'],
'risk_score': 'max'
}).reset_index()
return resultبا لرزش دست، اسکریپت را اجرا کرد.
نتیجه (ساعت ۴:۳۳)
Processing 22,431,287 transactions... Risk score calculation completed in 47 seconds Total pipeline time: 4 minutes 12 seconds
سارا با شگفتی و تعجب به مانیتور خیره شده بود. ۸ ساعت به ۴ دقیقه تبدیل شده بود. مگه میشه؟ مگه داریم؟
پروفایلر را باز کرد:
- نسخه پایتون خالص: ۲۲ میلیون رکورد → ۳۲ دقیقه فقط برای بخش risk (و ۸ ساعت کل pipeline به دلیل swap)
- نسخه راست: ۲۲ میلیون رکورد → ۴۷ ثانیه برای بخش risk
چرا این اتفاق افتاد؟
سپهر بعداً در لاگها تحلیل کرد:
۱. عدم وجود GIL: تابع راست از همه هستههای CPU استفاده کرد (۱۶ هسته موازی).
۲. مدیریت حافظه: بدون GC pauses و بدون مصرف حافظه اضافی، swap اتفاق نیفتاد.
۳. نوعگذاری: کامپایلر راست کد را بهینهتر از پایتون dynamic type checking کامپایل کرد.
۴. Rayon: توزیع خودکار بار بین هستهها بدون نیاز به دستکاری manual threading.
پایانی برای آن شب پر از استرس
تا ساعت ۴:۴۵ صبح، همه گزارشها آماده و در دشبورد بارگذاری شده بودند.
سارا در گروه نوشت: “جلسه فردا ساعت ۹ هست. تا ۸ بخوابید.”
سپهر اما خوابش نمیبرد. داشت مستندات راست را مرور میکرد و به این فکر میکرد که چه بخشهای دیگری از خط لوله را میتوان با راست بازنویسی کرد.
در پایین صفحه، نگاهی به مصرف حافظه انداخت:
Python: ۱۴.۲ GB RAM (swap: 8.1 GB) Rust: ۱.۸ GB RAM (swap: 0 MB)
لبخندی زد و چراغ را خاموش کرد.
راست در یک نگاه (قبل از پرداختن به نکات فنی داستان)
راست (Rust) یک زبان برنامهنویسی سیستمی است که توسط موزیلا توسعه داده شد و حالا یکی از محبوبترین زبانهای جهان است.
سه ویژگی کلیدی که راست را متمایز میکند بشرح ذیل میباشد:
۱. سرعت سطح C/C++: چون مستقیماً به کد ماشین کامپایل میشود و ماشین مجازی یا اینترپریتر (تفسیر کننده) ندارد.
۲. امنیت حافظه بدون garbage collector: با سیستمی به نام borrow checker، در زمان کامپایل تضمین میکند که هیچ حافظهای به اشتباه دسترسی پیدا نمیکند یا دو بار آزاد نمیشود. این یعنی بدون هزینه اضافی GC، امنیت حافظه دارید.
۳. همروندی بدون رقابت (fearless concurrency): کامپایلر راست جلوی بسیاری از باگهای چندنخی (race conditions) را در زمان کامپایل میگیرد.
در مهندسی داده چرا مهم است؟
کتابخانههایی مثل Polars (جایگزین سریع Pandas) و DataFusion (query engine) در راست نوشته شدهاند. خطوط لوله داده معمولاً با پایتون نوشته میشوند (توسعه سریع) اما در مقیاس بالا با محدودیتهای عملکردی و حافظه مواجه میشوند. اما زبان راست این امکان را میدهد که بخشهای بحرانی را بدون تغییر کل stack، بهینه کنیم.
بخش تحلیل فنی: چرا راست اینگونه معجزه کرد؟
ساعت ۱۰ صبح روز بعد، سپهر با چشمانی که هنوز بوی کافئین میداد، پشت میز سارا نشست. روی مانیتور، نمودارهای مانیتورینگ شب قبل باز بود.
سارا با تعجب گفت: “هنوز باورم نمیشه. ۴۷ ثانیه در برابر ۳۲ دقیقه. راست چطور این کار رو میکنه؟ این عادلانه نیست”.
سپهر قهوهاش را برداشت و لبخندی زد. “بذار دقیقاً نشونت بدم عزیزووووم”.
ماجرای GIL در پایتون (Global Interpreter Lock)
سپهر ابتدا نمودار مصرف CPU را نشان داد: “ببین. پایتون توی ۳۲ دقیقه فقط از یک هسته CPU استفاده میکرد.”
سارا با تعجب گفت: “اما ما ۱۶ هسته داریم!”
“دقیقاً. GIL اجازه نمیده چند ترد پایتونی همزمان روی هستههای مختلف اجرا بشن. تابع map ما با apply توی پایتون، توی یک ترد اجرا شد. اما راست…”
نمودار راست را باز کرد. همه ۱۶ هسته به صورت موازی کار میکردند.
“Rayon که من استفاده کردم، خودکار کارها رو بین همه هستهها پخش میکنه. بدون اینکه من یه خط کد threading بنویسم.”
داستان حافظه و GC (همان جمعکنندهی زبالهها)
سپهر نمودار مصرف حافظه را باز کرد: “اینجا جالبتر میشه.”
Python: [=====RAM 14GB=====][----SWAP 8GB----] Each 2 second: (GC pause: 0.3-0.7 sec) Rust: [==RAM 1.8GB==] Never pause
“پایتون هر چند ثانیه یه بار GC رو اجرا میکرد تا حافظه رو پاک کنه. این pauseها جمعاً حدود ۴ دقیقه شدن. بدتر از اون، وقتی حافظه از ۱۶ گیگ گذشت، سیستم افتاد رو swap که مثل این میمونه که بخوای با کفشهای گلی بدوی.”
“اما راست GC نداره. borrow checker توی زمان کامپایل مشخص میکنه کی حافظه آزاد بشه. این یعنی:”
- هیچ pause غیرمنتظرهای نخواهیم داشت.
- مصرف حافظه دقیقاً همون مقداری هست که نیاز داره، نه بیشتر.
هزینه صفر abstraction
سپهر برگه کدها را کنار هم گذاشت:
# Python
def calculate_risk(row):
# هر بار که این تابع صدا زده میشه:
# 1. بررسی نوع متغیرها در رانتایم
# 2. boxing و unboxing شیءها
# 3. مدیریت reference counting
return complex_calculation(row)// Rust
fn calculate_risk(transaction: Transaction) -> f64 {
// بعد از کامپایل:
// 1. هیچ بررسی نوعی در رانتایم نیست
// 2. دادهها مستقیماً روی استک قرار میگیرن
// 3. معادل کد سی نوشته شده
}“توی راست، abstractionها هزینه ندارن. کدی که مینویسی، بعد از کامپایل تقریباً معادل همون کد دستی C هست. توی پایتون، هر خط کد چندین لایه abstraction بالای خودش داره.”
نوعگذاری ایستا و بهینهسازی
سپهر ادامه داد: “یه نکته دیگه. توی پایتون، موقع اجرا نمیدونم amount از چه نوعیه. شاید int باشه، شاید float، شاید یه کلاس عجیب. هر بار چک میکنه.”
“اما راست از قبل میدونه f64 هست. کامپایلر میتونه از SIMD instructionها استفاده کنه، حلقهها رو بهینه کنه، حتی گاهی کل حلقه رو باز کنه (loop unrolling).”
بطور کلی وقتی در راست کد میزنیم چون از همون ابتدا باید به نوعگذاری یا همون Type annotation دقت کنیم خیلی ذهنمون رو برای برنامهنویسی قویتر میکنه.
نتیجه: ترکیب بهترینهای دو جهان
سپهر جمعبندی کرد: “ما از پایتون برای چسبوندن قطعات استفاده کردیم؛ خوندن CSV، گروهبندی، دشبورد. اینجا پایتون عالیه.”
“اما برای محاسبات سنگین، از راست استفاده کردیم. ترکیب این دو شد:”
- توسعهی سریع (پایتون برای ۸۰٪ کد)
- عملکرد بالا (راست برای ۲۰٪ بحرانی)
سارا با تعجب گفت: “پس ما نیازی نداریم کل پروژه رو با راست بازنویسی کنیم؟”
“نه. فقط همون بخشهایی که واقعاً به عملکرد نیاز دارن. PyO3 بهمون اجازه میده مثل پل بینشون کار کنیم.”
حرف آخر: هزینه واقعی
سپهر کمی جدیتر شد: “البته راست یه هزینه داره.”
“چه هزینهای؟”
“زمان یادگیری. borrow checker اولش سخته. ممکنه ۲-۳ هفته طول بکشه تا راحت بشی. ولی…”
نمودار مصرف حافظه شب قبل را نشان داد: “…وقتی ببینی یه خط لوله که ۸ ساعت طول میکشیده به ۴ دقیقه رسیده، اون هزینه خیلی کوچیک به نظر میاد.”
سارا به نمودارها نگاه کرد، سپس به سپهر گفت: “باشه. توی اسپرینت بعدی، یه تسک داریم: ‘آموزش راست برای کل تیم’. خودت مربی باش.”
سپهر قهوه را تا ته کشید و گفت: “قبول. ولی من یه شرط دارم.”
“چه شرطی؟”
“پیتزا رو تیم تأمین کنه. راست با پیتزا بهتر یاد گرفته میشه و اینکه دو روز گذشت اِفهی راست دولوپری نگیریدها 😁”.
معرفی چند منبع (کتاب و ویدیو)
کتاب:
Rust Programming Language, Third Edition
دورههای ویدیویی:
(Free Download from downloadly.ir) Udemy – Learn to Code with Rust by Boris Paskhaver