- نام درس: آشنایی با Jetflow: راهکار کلودفلر برای جابجایی دیتا در مقیاس بزرگ
- مدت زمان مطالعه: حدود ۱۵ دقیقه
- پیشنیازها: آشنایی اولیه با مفاهیم دیتابیس و دیتا
- اهداف درس:
- یاد میگیری که Jetflow چی هست و چرا ساخته شده.
- با دستاوردها و بهبودهای عملکردی که Jetflow ایجاد کرده آشنا میشی.
- با معماری و بخشهای اصلی این فریمورک آشنا میشی.
- میفهمی که چطور با تکنیکهای خاص، عملکرد انتقال دیتا رو به شدت بالا بردن.
امروز میخوایم بریم سراغ یه مطالعه موردی خیلی جالب از شرکت کلودفلر (Cloudflare). این شرکت با حجم عظیمی از دیتا سر و کار داره و برای مدیریت این دیتا به یه چالش بزرگ خورده بود. ما قراره ببینیم این چالش چی بوده و چطوری با ساختن ابزاری به اسم Jetflow حلش کردن. این درس بهت کمک میکنه بفهمی در دنیای واقعی، شرکتهای بزرگ با چه مشکلاتی روبرو میشن و چطور برای حلشون راهکارهای خلاقانه طراحی میکنن.
بریم که شروع کنیم.
فصل اول: صورت مسئله، یک دریاچه پر از دیتا
قضیه از این قراره که در کلودفلر، یه تیمی به اسم تیم هوش تجاری (Business Intelligence) وجود داره. وظیفه این تیم مدیریت یه چیزی به اسم «دریاچه دیتا» یا Data Lake هست که مقیاسش در حد پتابایته. این تیم هر روز هزاران جدول رو از منابع مختلف جمعآوری و وارد این دریاچه میکنه.
این منابع هم داخلی هستن، مثل دیتابیسهای Postgres و ClickHouse، و هم خارجی، مثل نرمافزارهای خدماتی (SaaS) مثل Salesforce. کار این تیم خیلی پیچیدهاس، چون بعضی از این جدولها هر روز صدها میلیون یا حتی میلیاردها ردیف دیتای جدید دارن. در مجموع، هر روز حدود ۱۴۱ میلیارد ردیف دیتا وارد سیستم میشه. این دیتاها برای تصمیمگیریهای مهم شرکت، مثل برنامهریزی برای رشد، تصمیمات مربوط به محصولات و نظارت داخلی، فوقالعاده حیاتی هستن.
با بزرگتر شدن کلودفلر، حجم دیتا هم روز به روز بیشتر و پیچیدهتر میشد. راهکار قبلی اونها برای انتقال دیتا که بهش میگن ELT (مخفف Extract, Load, Transform)، دیگه نمیتونست نیازهای فنی و تجاری شرکت رو برآورده کنه. اونها راهکارهای رایج دیگه رو هم بررسی کردن اما به این نتیجه رسیدن که عملکرد اونها هم معمولا بهتر از سیستم فعلی خودشون نیست.
اینجا بود که مشخص شد باید یه فریمورک مخصوص خودشون بسازن که با نیازهای منحصر به فردشون سازگار باشه. اینطوری بود که Jetflow متولد شد.
فصل دوم: Jetflow چه کرد؟ نگاهی به دستاوردها
خب، حالا که فهمیدیم Jetflow چرا به وجود اومد، ببینیم نتیجه کار چی شد و چه دستاوردهایی داشت.
- بهبود بهرهوری تا بیش از ۱۰۰ برابر:
برای اینکه بهتر متوجه بشی این عدد یعنی چی، به این مثال دقت کن. طولانیترین کار پردازشی اونها مربوط به یک جدول ۱۹ میلیارد ردیفی بود. این کار قبلا ۴۸ ساعت طول میکشید و ۳۰۰ گیگابایت حافظه مصرف میکرد. اما با Jetflow، همین کار حالا توی ۵.۵ ساعت و فقط با ۴ گیگابایت حافظه انجام میشه. این یک بهبود فوقالعاده در مصرف منابع به حساب میاد. - هزینه خیلی پایین:
تیم کلودفلر تخمین میزنه که انتقال ۵۰ ترابایت دیتا از دیتابیس Postgres با استفاده از Jetflow، بر اساس قیمتهای ارائه شده توسط سرویسدهندههای ابری تجاری، میتونه کمتر از ۱۰۰ دلار هزینه داشته باشه. - بهبود عملکرد تا بیش از ۱۰ برابر:
بزرگترین مجموعه دیتای اونها قبلا با سرعت ۶۰ تا ۸۰ هزار ردیف در ثانیه منتقل میشد. این سرعت با Jetflow به ۲ تا ۵ میلیون ردیف در ثانیه برای هر اتصال دیتابیس رسیده. نکته جالب اینه که این اعداد برای بعضی دیتابیسها با افزایش تعداد اتصالها، بهتر هم میشه و مقیاسپذیری خوبی داره. - توسعهپذیری (Extensibility):
طراحی Jetflow ماژولار هست، یعنی از قطعههای جدا از هم تشکیل شده. این ویژگی باعث میشه اضافه کردن قابلیتهای جدید و تست کردن سیستم خیلی راحت باشه. امروز Jetflow با سیستمهای مختلفی مثل ClickHouse، Postgres، Kafka، خیلی از API های نرمافزارهای خدماتی (SaaS)، Google BigQuery و خیلیهای دیگه کار میکنه و انعطافپذیری خودش رو در کاربردهای جدید هم حفظ کرده.
فصل سوم: نقشه راه ساخت Jetflow، چه چیزهایی مهم بود؟
برای طراحی یه فریمورک جدید، اولین قدم اینه که دقیقا بدونی چه مشکلاتی رو میخوای حل کنی. تیم کلودفلر چند تا نیازمندی اصلی رو مشخص کرد تا مطمئن بشه راهکار جدیدشون مشکلات جدیدی درست نمیکنه.
- عملکرد بالا و بهینه (Performant & Efficient):
اونها باید دیتای بیشتری رو در زمان کمتری جابجا میکردن. بعضی از کارهای انتقال دیتا حدود ۲۴ ساعت طول میکشید و حجم دیتا هم که روز به روز در حال افزایش بود. راهکار جدید باید دیتا رو به صورت جریانی (streaming) منتقل میکرد و حافظه و منابع پردازشی کمتری نسبت به راهکار قبلی مصرف میکرد. - سازگاری با سیستمهای قدیمی (Backwards Compatible):
با توجه به اینکه هزاران جدول هر روز در حال انتقال بودن، نمیشد همه چیز رو یک دفعه عوض کرد. راهکار جدید باید اجازه میداد که جدولها به صورت جداگانه و در صورت نیاز به سیستم جدید منتقل بشن. از طرفی، اونها در مراحل بعدی کار از ابزاری به اسم Spark استفاده میکنن و اسپارک برای ادغام فایلهای Parquet با ساختارهای (schema) متفاوت محدودیتهایی داره. پس راهکار جدید باید این انعطاف رو میداشت که دقیقا همون ساختار مورد نیاز برای سازگاری با سیستم قدیمی رو تولید کنه.
همچنین، باید با سیستم متادیتای سفارشی خودشون که برای چک کردن وابستگیها و وضعیت کارها استفاده میشد، به صورت یکپارچه کار میکرد. - سادگی در استفاده (Ease of Use):
تیم میخواست که تنظیمات در یک فایل کانفیگ قابل مدیریت باشه (چیزی که بهش میگن configuration as code). اینطوری میتونن تاریخچه تغییرات رو دنبال کنن.
یه نیاز دیگه این بود که در اکثر موارد، کار با سیستم بدون نیاز به کدنویسی (no-code) باشه تا افراد با نقشهای مختلف در تیم بتونن ازش استفاده کنن. کاربرها نباید نگران مواردی مثل در دسترس بودن یا تبدیل انواع دیتا بین سیستم مبدا و مقصد باشن یا برای هر انتقال جدید، کد جدیدی بنویسن. تنظیمات هم باید حداقل ممکن باشه. برای مثال، ساختار دیتا (schema) باید به صورت خودکار از سیستم مبدا تشخیص داده بشه و نیازی نباشه کاربر اون رو دستی وارد کنه. - قابلیت شخصیسازی (Customizable):
در کنار ساده بودن، باید این گزینه هم وجود داشت که در صورت نیاز بشه تنظیمات رو تغییر داد و شخصیسازی کرد. مثلا، نوشتن فایلهای Parquet معمولا هزینه بیشتری نسبت به خوندن از دیتابیس داره. پس باید این قابلیت وجود داشته باشه که بشه منابع و همزمانی بیشتری رو به این بخش اختصاص داد.
علاوه بر این، میخواستن کنترل کاملی روی محل اجرای کار داشته باشن. یعنی بتونن کارگرهای (worker) همزمانی رو در تردها، کانتینرها یا حتی ماشینهای مختلف اجرا کنن. اجرای این کارگرها و ارتباط بینشون از طریق یک رابط (interface) مدیریت میشد تا بشه پیادهسازیهای مختلفی رو با تغییر در تنظیمات کار، به سیستم تزریق کرد. - قابلیت تست (Testable):
اونها دنبال راهکاری بودن که بشه اون رو به صورت محلی (locally) در یک محیط کانتینری اجرا کرد. اینطوری میتونستن برای هر مرحله از خط لوله (pipeline) تست بنویسن. در راهکارهای «جعبه سیاه» یا black box، تست کردن معمولا به معنی چک کردن خروجی بعد از یک تغییره که این فرآیند کندی هست و ریسک بالایی داره، چون ممکنه همه حالتهای خاص تست نشن و پیدا کردن مشکل هم خیلی سخت میشه.
فصل چهارم: معماری Jetflow، چطور همه چیز کنار هم کار میکنه؟
برای ساختن یه فریمورک واقعا منعطف، تیم کلودفلر کل فرآیند رو به چند مرحله مشخص تقسیم کرد و بعد یک لایه تنظیمات (config layer) ساخت تا ترکیب این مراحل رو تعریف کنه. بریم ببینیم این معماری چطوریه.
بخش اول: مراحل خط لوله (Pipeline Stages)
ایده اصلی این بود که فرآیند انتقال دیتا به سه دسته اصلی از مراحل تقسیم بشه:
- مصرفکنندهها (Consumers): اینها نقطه شروع خط لوله هستن. وظیفهشون ایجاد یک جریان دیتاست، مثلا با خوندن دیتا از سیستم مبدا.
- تبدیلکنندهها (Transformers): این مراحل یک جریان دیتا رو به عنوان ورودی میگیرن و یک جریان دیتای دیگه رو به عنوان خروجی تحویل میدن. کارشون میتونه تبدیل دیتا، اعتبارسنجی و کارهایی از این قبیل باشه. خوبیشون اینه که میشه اونها رو به صورت زنجیروار پشت سر هم قرار داد.
- بارگذارها (Loaders): اینها هم مثل بقیه، یک جریان دیتا رو به عنوان ورودی میگیرن اما نقطه پایان خط لوله هستن. یعنی جایی که دیتا در یک سیستم خارجی ذخیره میشه و اثر دائمی داره.
کل خط لوله از طریق یک فایل YAML تنظیم میشه. در این فایل باید حداقل یک مصرفکننده، صفر یا چند تبدیلکننده، و حداقل یک بارگذار تعریف بشه. این طراحی ماژولار باعث میشه هر مرحله به صورت مستقل قابل تست باشه و رفتارهای مشترک مثل مدیریت خطا و همزمانی، از کلاسهای پایه به ارث برده بشن. این موضوع زمان توسعه رو خیلی کم میکنه و اطمینان به درستی کد رو بالا میبره.
بخش دوم: تقسیمبندی دیتا برای پردازش موازی
قدم بعدی، طراحی یک سیستم برای تقسیمبندی دیتا بود. این سیستم باید طوری کار میکرد که اگر کل خط لوله یا بخشی از اون به خاطر یک خطای موقتی دوباره اجرا شد، نتیجه نهایی درست باشه و دیتای تکراری ایجاد نشه. به این ویژگی Idempotency میگن. اونها به یک طراحی رسیدن که اجازه پردازش موازی رو میده و در عین حال، تقسیمبندی معناداری از دیتا ارائه میکنه. این تقسیمبندی سه سطح داره:
- RunInstance: این بزرگترین واحد تقسیمبندی هست و به یک واحد کاری برای یک بار اجرای کامل خط لوله اشاره داره (مثلا دیتای یک ماه، یک روز یا یک ساعت).
- Partition: این یک بخش از RunInstance هست. هر ردیف از دیتا به صورت قطعی و بر اساس خود دیتا (بدون نیاز به اطلاعات خارجی) به یک پارتیشن اختصاص داده میشه. این ویژگی باعث میشه در صورت اجرای مجدد، همه چیز درست پیش بره (مثلا پارتیشنی شامل اکانتهای با شناسه ۰ تا ۵۰۰).
- Batch: این یک تقسیمبندی کوچکتر و غیرقطعی از دیتاهای یک پارتیشن هست. فقط برای این استفاده میشه که دیتا به تکههای کوچکتر تقسیم بشه تا پردازش سریعتر و با منابع کمتری انجام بشه (مثلا هر ۱۰ هزار ردیف یا هر ۵۰ مگابایت).
تنظیماتی که کاربر در فایل YAML برای مصرفکننده مشخص میکنه، هم کوئری لازم برای گرفتن دیتا از مبدا رو میسازه و هم معنی این تقسیمبندیها رو به صورت مستقل از سیستم کدگذاری میکنه. اینطوری مراحل بعدی خط لوله میفهمن که مثلا «این پارتیشن شامل دیتای اکانتهای ۰ تا ۵۰۰ هست». این قابلیت بهشون اجازه میده در صورت خطا، فقط همون بخش مشکلدار رو پاکسازی و دوباره پردازش کنن و از ایجاد دیتای تکراری جلوگیری بشه.
بخش سوم: فرمت داخلی استاندارد، زبان مشترک مراحل
یکی از رایجترین کاربردهای Jetflow اینه که دیتا رو از یک دیتابیس بخونه، اون رو به فرمت Parquet تبدیل کنه و بعد در یک فضای ذخیرهسازی ابری (object storage) ذخیره کنه. هر کدوم از این کارها یک مرحله جداگانه هست. با اضافه شدن کاربردهای جدید، باید مطمئن میشدن که اگر کسی یک مرحله جدید نوشت، با بقیه مراحل سازگار باشه. اونها نمیخواستن به وضعیتی برسن که برای هر فرمت خروجی و سیستم مقصد، کد جدیدی بنویسن.
راه حل این مشکل این بود که کلاس استخراجکننده (extractor) دیتا فقط اجازه خروجی دادن دیتا در یک فرمت واحد رو داشته باشه. این فرمت واحد، زبان مشترک بین همه مراحل شد. اینطوری، تا زمانی که مراحل بعدی این فرمت رو به عنوان ورودی پشتیبانی کنن، با بقیه خط لوله سازگار خواهند بود. این ایده شاید الان واضح به نظر برسه، اما تیم کلودفلر میگه این یک تجربه دردناک برای اونها بود، چون در ابتدا یک سیستم تایپ سفارشی ساخته بودن و با مشکلات سازگاری بین مراحل دست و پنجه نرم میکردن.
برای این فرمت داخلی، اونها Arrow رو انتخاب کردن که یک فرمت دیتای ستونیِ در حافظه (in-memory columnar data format) هست. مزایای کلیدی Arrow برای اونها این موارد بود:
- اکوسیستم Arrow: خیلی از پروژههای مرتبط با دیتا الان از Arrow به عنوان فرمت خروجی پشتیبانی میکنن. این یعنی وقتی برای یک منبع دیتای جدید مرحله استخراجکننده مینویسن، تولید خروجی Arrow معمولا کار سادهای هست.
- بدون سربار سریالسازی (No serialisation overhead): این ویژگی باعث میشه انتقال دیتای Arrow بین ماشینهای مختلف یا حتی زبانهای برنامهنویسی متفاوت با حداقل سربار انجام بشه. از اونجایی که Jetflow طوری طراحی شده که بتونه در سیستمهای مختلفی اجرا بشه، این بهینگی در انتقال دیتا خیلی مهمه.
- رزرو حافظه در بستههای بزرگ و با اندازه ثابت: زبان برنامهنویسی Go که Jetflow با اون نوشته شده، یک زبان دارای Garbage Collector (GC) هست. زمان چرخه GC بیشتر به تعداد آبجکتها بستگی داره تا اندازه اونها. اگر آبجکتهای کمتری در حافظه داشته باشیم، حتی اگه حجم کل دیتا یکی باشه، زمان پردازش برای پاکسازی حافظه به شدت کم میشه. بذار اینطوری بگم: فرض کن ۸۱۹۲ ردیف دیتا با ۱۰ ستون داری. در حالت عادی، درایورهای دیتابیس برای هر ردیف یک آبجکت در حافظه میسازن که میشه ۸۱۹۲ آبجکت. اما با Arrow، فقط برای هر ستون یک آبجکت ساخته میشه، یعنی ۱۰ آبجکت. این تفاوت بزرگ باعث میشه زمان چرخه GC خیلی کمتر بشه.
بخش چهارم: تبدیل ردیف به ستون، یک بهینهسازی مهم
یک بهینهسازی عملکردی مهم دیگه، کم کردن تعداد مراحل تبدیل دیتا بود. اکثر فریمورکهای انتقال دیتا، دیتا رو به صورت داخلی به شکل ردیفی (rows) نمایش میدن. اما در مورد Jetflow، دیتا عمدتا در فرمت Parquet نوشته میشه که یک فرمت ستونی (columns) هست.
وقتی دیتا از یک منبع ستونی (مثل ClickHouse) خونده میشه، تبدیل اون به یک نمایش ردیفی در حافظه کار بهینهای نیست. بعدا دوباره این دیتای ردیفی باید به ستونی تبدیل بشه تا فایل Parquet نوشته بشه. این تبدیلهای اضافه تاثیر منفی زیادی روی عملکرد دارن.
Jetflow به جای این کار، دیتا رو از منابع ستونی مستقیما در فرمت ستونی میخونه (مثلا فرمت Block در ClickHouse) و اون رو مستقیما در فرمت ستونی Arrow کپی میکنه. بعد فایلهای Parquet مستقیما از روی ستونهای Arrow نوشته میشن. این سادهسازی فرآیند، عملکرد رو به شدت بهبود میده.
فصل پنجم: پیادهسازی در عمل، دو مطالعه موردی
حالا بریم ببینیم این طراحیها در عمل چطور پیاده شدن. دو تا مثال از دیتابیسهای ClickHouse و Postgres رو بررسی میکنیم.
مطالعه موردی: ClickHouse
وقتی نسخه اولیه Jetflow رو تست میکردن، متوجه شدن که به خاطر معماری ClickHouse، استفاده از اتصالهای بیشتر هیچ فایدهای نداره، چون ClickHouse سریعتر از چیزی که اونها دیتا رو دریافت میکردن، میخوند. این یعنی مشکل از درایور دیتابیس بود. باید میشد با یک درایور بهینهتر، از همون یک اتصال استفاده بهتری کرد و تعداد ردیفهای خیلی بیشتری رو در ثانیه خوند.
اونها اول یک درایور سفارشی برای ClickHouse نوشتن، اما در نهایت به استفاده از کتابخانه سطح پایین و عالی ch-go رو آوردن. این کتابخانه مستقیما Block ها رو از ClickHouse به صورت ستونی میخونه. این تغییر یک تاثیر شگفتانگیز روی عملکرد داشت. با ترکیب این درایور و بهینهسازیهای خود فریمورک، اونها حالا با یک اتصال ClickHouse، میلیونها ردیف در ثانیه رو منتقل میکنن.
یک درس مهمی که گرفتن این بود که درایورهای دیتابیس استاندارد معمولا برای خواندن دستهای و حجیم دیتا بهینه نشدن و برای هر ردیف سربار بالایی دارن.
مطالعه موردی: Postgres
برای Postgres، اونها از درایور عالی jackc/pgx استفاده کردن، اما به جای استفاده از رابط استاندارد `database/sql` در زبان Go، مستقیما بایتهای خام هر ردیف رو دریافت میکنن و از توابع داخلی خود jackc/pgx برای اسکن کردن هر نوع دیتا استفاده میکنن.
رابط استاندارد `database/sql` در Go از تکنیکی به اسم reflection استفاده میکنه تا نوع دیتا رو بفهمه و مقدار ستون رو در فیلد مربوطه قرار بده. این روش در سناریوهای عادی به اندازه کافی سریع و سادهاس، اما برای کاربردهای سنگین کلودفلر از نظر عملکردی ضعیف بود. اما درایور jackc/pgx بایتهای ردیف رو در هر بار درخواست ردیف بعدی، دوباره استفاده میکنه که نتیجهاش صفر تخصیص حافظه برای هر ردیف (zero allocations per row) هست. این طراحی به اونها اجازه داد کدی با عملکرد بالا و تخصیص حافظه کم بنویسن.
با این طراحی، اونها تونستن برای اکثر جدولها به سرعت نزدیک به ۶۰۰ هزار ردیف در ثانیه برای هر اتصال Postgres برسن، اون هم با مصرف حافظه خیلی کم.
وضعیت فعلی و آینده Jetflow
تا اوایل جولای ۲۰۲۵، این تیم روزانه ۷۷ میلیارد رکورد رو از طریق Jetflow منتقل میکنه. بقیه کارها هم در حال انتقال به Jetflow هستن که در نهایت مجموع انتقال روزانه رو به ۱۴۱ میلیارد رکورد میرسونه. این فریمورک به اونها اجازه داده جدولهایی رو منتقل کنن که قبلا امکانش وجود نداشت و به خاطر کاهش زمان اجرا و مصرف کمتر منابع، صرفهجویی قابل توجهی در هزینهها ایجاد کرده.
در آینده، کلودفلر قصد داره این پروژه رو به صورت متنباز (open source) منتشر کنه. اونها همچنین اشاره کردن که اگر کسی علاقهمند به کار روی چنین ابزارهایی هست، میتونه موقعیتهای شغلی باز رو در وبسایتشون به آدرس https://www.cloudflare.com/careers/jobs/ پیدا کنه.
دیدگاهتان را بنویسید