در این پست بلاگ قصد داریم به بهانه مهاجرت (Migration) پایگاه داده (Database) زمان پخش کاربران یک استارتآپ پادکست کانادایی به سرور جدید، مرور کوتاهی بر راه حل انتخابیمان برای ذخیره این دادهها داشته باشیم و در کنار آن به معرفی اجمالی پایگاه داده ClickHouse و روش اعمال مهاجرتمان بپردازیم.
پس از تشکیل تیم هوشمندسازی و تلاش برای ارائه راهحلهایی برای هوشمندسازی سرویس با استفاده از مدلهای یادگیری عمیق (Deep Learning)، مهم ترین نیازمندی، حجم عظیمی از داده بود که این مدلها بتوانند بر اساسشان آموزش ببینند. دادههای مختلفی از بخشهای مختلف سایت/اپ میتوان استخراج کرد که بتوان برای هوشمندسازی از آنها استفاده کرد؛ از نظرات کاربران برای پادکستها گرفته تا جریان کلیکهای کاربر در صفحه (که در پست دیگری در آینده به آن پرداخته خواهد شد). اما یکی از حیاتیترین این دادهها، زمان پخش کاربران است که میتوان با تخمین خوبی، به میزان رضایت کاربر از پادکست ترجمه شود و برای ارائه پادکستها بر اساس سلیقه کاربر مورد استفاده قرار گیرد.
زمان پخش چند صد هزار نفر کاربر همزمان (Concurrent) سرویس، هر دقیقه به صورت متناوب از گوشیها، کامپیوترها و تلویزیونها و … به سمت سرورهای ما ارسال میشود که دریافت و ذخیرهسازی آن به صورتی که حجم عظیم و سرعت بالای تولید آن، مشکلی برای ذخیرهسازی و کوئری کردن بهینه دادهها ایجاد نکند، یکی از چالشهای این پروژه بود که انتخاب یک پایگاه داده مناسب بخش مهمی از راه حل بود.
ما معتقدیم که یک راهکار نهایی (Silver Bullet) برای ذخیره و بازیابی همه انواع دادهها وجود ندارد و بر اساس شرایط مختلف مانند مورد استفاده (Use Case)، نوع داده، سرعت تولید داده، حجم داده، نسبت نوشتن به خواندن داده، زمان کوئری و … باید راهکار ذخیرهسازی خاصی را انتخاب کرد. ما بر اساس شرایط از تکنولوژیهای مختلفی استفاده میکنیم، از ذخیرهسازی ساده به عنوان فایلهای CSV برای آموزش (Training) برخی از مدلهای هوش مصنوعی گرفته تا Redis برای دادههای کوچک و Cache کردن دادهها، MySQL برای اطلاعات رابطهای، MongoDB برای دادههای بدون ساختار خاص، ElasticSearch برای دادههایی که نیاز به جستجوی تماممتن (Full-Text Search) دارند، Graylog برای ذخیره و مدیریت لاگها، Prometheus و Grafana برای ذخیره و نمایش اطلاعات پایش (Monitoring) سرویسها، Kafka و KSQL برای ذخیره و پردازش جریان رخدادهای سمت کاربر و …
پایگاه داده ClickHouse یک پایگاه داده ساختارمند (Structured) ستونی (Column-oriented) سریع برای پردازش تحلیلی آنلاین (OLAP) است که با دستورات SQL کار میکند و توسط Yandex ساخته و متنباز شده است.
برخی از ویژگیهای ClickHouse:
این تصاویر برای درک بهتر تفاوت پردازش کوئری پایگاه داده ستونی در مقابل ردیفی گویا هستند:
این پایگاه داده تفاوتهایی با پایگاه دادههای سنتی دارد که در ابتدا میتواند گیج کننده باشد. به طور مثال:
دو مورد آخر نیاز به توضیح دارند چون مفاهیم جدیدی نسبت به پایگاه دادههای سنتی هستند. ClickHouse برای ذخیرهسازی جداول، موتورهای مخلفی را معرفی کرده که از خانواده MergeTree هستند که ایده پشت آن، همانند LSM است و به این صورت کار میکند که دادههای جدید در دستههایی (Batch) به پایگاه داده وارد میشوند و در هنگام ورود در Log هایی نگه داشته میشوند. در این مرحله دادهها مرتب شده نیستند ولی ترتیب آنها را زمان ورود دادهها مشخص میکند.
سپس این بخش از دادههای جدید با Sorting Key مرتب میشوند و منتظر ادغام شدن (Merge) میشود.
سپس در زمان نامعلومی* در پسزمینه، این بخشهای با هم ادغام میشوند و تغییرات لازم روی دادهها اعمال میشود و بر اساس Sorting Key مرتب و نهایی میشوند.
در این مرحله دادهها در شکل نهایی ثبت شدند و این عملیات برای دستههای بعدی تکرار میشود.
توسط این مکانیزم، ClickHouse میتواند بدون نیاز به قفل کردن، دادهها را بروز نگه دارد و در هر لحظه به سریعترین حالت ممکن به کوئریها پاسخ دهد. اما برای روشنتر شدن شیوه بروز رسانی دادههای تغییر یافته، باید با سایر اعضای خانواده MergeTree آشنا شویم:
توسط این موتورها، ClickHouse میتواند بدون نیاز به قفل یا ارائه عملیات Update، دادهها را فقط با عملیات Insert بروز نگه دارد.
برای آشنایی بیشتر با پایگاه داده ClickHouse به وبسایت رسمی یا مستندات آن مراجعه کنید.
ما در تیم جوان هوشمندسازی، در اولین روزهای کاری سال ۱۳۹۹ (حدود ۹ ماه پیش از نگارش این پست) پایگاه داده ClickHouse را در سرورهای شرکت راهاندازی کردیم و تا زمان نگارش این پست، بیش از یک میلیارد ردیف ادغام شده از دادههای پخش روزانه کاربران جمعآوری کردیم. این دادهها علاوه بر خوراک رسانی به مدلهای هوش مصنوعی، مصارف آماری دیگری هم برای ما داشتند. به طور مثال برای تستهای A/B، تحلیلهای BI و …
برخی از دلایلی که ما این پایگاه داده را انتخاب کردیم:
اما در کنار نقاط قوت فراوان، هم نقاط ضعفی وجود دارد، هم نکاتی که باید به آنها توجه شود:
Data deduplication occurs only during a merge. Merging occurs in the background at an unknown time, so you can’t plan for it. Some of the data may remain unprocessed. Although you can run an unscheduled merge using theOPTIMIZE
query, don’t count on using it, because theOPTIMIZE
query will read and write a large amount of data.
معماری سیستم ذخیره سازی اطلاعات زمان پخش در حقیقت بسیار ساده است. پخشکننده (Player) هایی که در دستگاههای مختلف داریم به صورت متناوب هر دقیقه مجموع دقایق پخش و ثانیه فعلی پادکستی که کاربران در حال گوش کردن به آن هستند را به سرورهای ما ارسال میکنند که به واسطه یک HA Proxy به دست خوشه ای از یک کد نوشته شده به زبان Go میرسد که وظیفه اعتبارسنجی درخواست را دارد و در صورت معتبر تشخیص دادن، اطلاعات زمان پخش را به یک Redis منتقل میکند و در آن تجمیع میکند. سپس یک خوشه دیگر از کدهای به زبان Go به نام WALL·E دسته دسته اطلاعات پخشهای کاربران را به ClickHouse ارسال میکنند. این اطلاعات در پایگاه داده ClickHouse در یک جدول از نوع SummingMergeTree جمع آوری میشدند که Field مربوط به دقیقه پخش را به صورت روزانه جمع میکرد. بنابراین در این مدت یک میلیارد ردیف داده کاربر-پادکست-روز داشتیم. این ساختار که تمام بخشهای آن به دقت پایش میشود، به خوبی پاسخگوی نیازهای ما بود اما به دلایلی نیاز دیدیم که ساختار بهتری داشته باشیم و دادهها را مهاجرت دهیم.
یکی از دلایل مهاجرت این بود که این پایگاه داده برای ما در فاز تحقیقات و امکانسنجی و آشنایی بود و به همین دلیل روی یک ماشین مجازی (Virtual Machine) بالا آمده بود و حالا که حجم دیتای خوبی دارد و از پس تستهای ما بر آمده، نیاز بود سرور اختصاصی خود را با دیسک قابل انبساط (Expansion) داشته باشد تا بنابر نیاز، افزایش یابد.
از دلایل دیگر، تغییر در Field های داده بود که بعضی دیگر کاربرد نداشتند، بعضی جدید اضافه شده بودند و بعضی نیاز به بهینهسازی بیشتری داشتند. به طور مثال Field هایی که مشخص شد مقدارهای نسبتاً ثابتی دارند را از نوع LowCardinality تعریف کردیم.
همچنین احساس نیاز به جدولی کردیم که تجمیع بیشتری از روی این جدول فعلی انجام دهد و به جای اطلاعات روزانه، اطلاعات پخش کلی کاربر را ثبت کند و Field هایی همچون درصد پخش داشته باشد. برای این مورد از Materialized View استفاده کردیم که به هنگام درج دادهها در جدول اصلی، تجمیع شده آن را در این جدول ثانویه اضافه میکند.
دلیل دیگر، بروزرسانی خود ClickHouse بود که در این مدت امکانات جدیدی اضافه کرده بود و بسیاری از مشکلات آن برطرف شده بود.
حالا که مطمئن بودیم مهاجرتی با این تعداد تغییر داریم، نیاز به سناریویی دقیق برای اجرای آن حس میشد که شامل چند بخش اساسی باشد:
ابتدا جدیدترین نسخه ای از ClickHouse که به اندازه کافی پایدار (Stable) باشد را انتخاب کردیم و تمام سیاهه تغییرات (Changelog) بین نسخه نصب شده قدیمی و نسخه جدید را به دقت مطالعه کردیم تا هر نوع ناسازگاری با دادههای قدیمی را کشف کنیم که خوشبختانه مشکلی وجود نداشت.
در این مرحله در یک ماشین مجازی نسخه جدید را نصب و تنظیم و راهاندازی کردیم و سپس همه راههای موجود را امتحان کردیم تا بهترین را پیدا کنیم. به عنوان نمونه فایلهای داده ClickHouse قدیمی را به ماشین مجازی جدید منتقل کردیم و جای فایلهای قدیم قرار دادیم (با تمام عجیب بودن این روش، در مستندات ClickHouse تقریباً چنین روشی مطرح شده) و همه چیز به درستی کار کرد ولی راه درستی به نظر نمیرسید زیرا باید پایگاه داده سیستمی را هم منتقل میکردیم که انتقال آن بین دو نسخه از نظر ما درست نبود. روشهای دیگر را هم امتحان کردیم ولی در نهایت ClickHouse-Copier را انتخاب کردیم. این کد نوشته شده به زبان Go در کنار مسیر دادههای ClickHouse یک پوشه با نام backup ایجاد میکند و نسخه پشتیبان از دادههای فعلی را به صورت Hard Link به دادههای اصلی در آن ذخیره میکند که هم فضای اضافه نمیگیرد، هم به راحتی میتوان فایلها را به مسیر جدید منتقل کرد که در مورد استفاده ما با rsync انجام شد. اما در تمام مستندات پشتیبانگیری این پایگاه داده اشاره شده که نباید در زمان پشتیبانگیری، داده جدیدی در سیستم درج شود.
To get a consistent copy, the data in the source tables and partitions should not change during the entire process.
در معماری سیستم، سرویس WALL·E میتواند برای مدتی از کار بیافتد و مشکلی برای سیستم پیش نیاید، زیرا دادههای در Redis تجمیع میشوند. اما این زمان نمیتواند زیاد باشد زیرا این Redis گنجایش نگهداری دادههای زیاد را ندارد. پس زمان اجرای مهاجرت باید ساعتی انتخاب شود که سیستم کمترین ترافیک را دارد و از روی نمودار ترافیک، این بازه بین ساعت ۶ تا ۸ صبح است که میتوان در آن سرویس WALL·E را متوقف کرد، مهاجرت را انجام داد و سپس آن را به کار انداخت.
برای انتقال دادهها از جداول با ساختار قدیم به جدید، نیاز بود یک کوئری دقیق INSERT INTO SELECT نوشته شود که تمام ملاحظات ساختار جدید را در نظر بگیرد و Field های اضافی را حذف کند، مقدار معقولی برای Field های جدید در نظر بگیرد و همزمان با اجرای Join روی جداول دیگر، دادههایی که موجود نبودند را پر کند.
اینها تغییرات مربوط به خود ClickHouse بود اما نیاز بود دو سرویس دیگر که با Go نوشته شده بودند هم تغییر کنند که ضمن حفظ سازگاری با حالت قبلی (Backward Compatibility)، بتوانند از پس درخواستها از نوع جدید نیز برآیند تا بتوان مهاجرت نرمی را انجام داد و در صورت بروز هر نوع خطا بتوان به حالت قبل برگشت. اولین بخش واقعی مهاجرت، همین بخش بود که از مدتها قبل از شروع مهاجرت، این دو سرویس به طوری تغییر کردند که از پس هر دو نوع درخواست برآیند و مشکلی برای آنها پیش نیاید.
مرحله آخر هم تغییر آدرس اتصال پایگاه داده سرویسهایی بود که از ClickHouse استفاده میکنند .
این عملیات دو بار به صورت صفر تا صد روی ماشین مجازی انجام شد و زمان بندیها بدست آمد و تمام مشکلات دیده شد و تقریباً برای تمام مراحل راه حل ثانویه (Plan B) و راهکار برگشت به حالت قبل نوشته شد تا در زمان مهاجرت اصلی مشکلی بوجود نیاید که برای آن آمادگی وجود ندارد. همچنین در تمام مراحل باید پایش سیستمها و خواندن دقیق Log ها انجام شود.
ساعت ۰۷:۰۰ - سرویس WALL·E متوقف شد
ساعت ۰۷:۰۱ - ایجاد جداول جدید در ClickHouse جدید
ساعت ۰۷:۰۳ - شروع عملیات پشیبانگیری توسط ClickHouse-Copier
ساعت ۰۷:۰۵ - پایان پشتیبانگیری و شروع کپی دادههای پشتیبانگیری شده به سرور جدید توسط rsync
ساعت ۰۷:۰۷ - پایان کپی گیری
ساعت ۰۷:۰۸ - برگرداندن (Restore) دادههای پشتیبانگیری شده
ساعت ۰۷:۰۹ - شروع انتقال دادهها از جدول قدیم به جدید توسط کوئری INSERT INTO SELECT
ساعت ۰۷:۴۵ - پایان انتقال دادهها
Elapsed: 2194.532 sec. Processed 1.01 billion rows, 101.02 GB (461,970 rows/s., 46.03 MB/s.)
ساعت ۰۷:۴۶ - راهاندازی مجدد سرویس WALL·E و شروع تستها (در Redis هیچ تجمع خاصی رخ نداد)
ساعت ۰۷:۵۰ - پایان تستهای از قبل مشخص شده
ساعت ۰۷:۵۲ - خوشه سرویس Stats Gateway جدید بارگذاری شد و به ساختار جدید انتقال پیدا کرد
ساعت ۰۷:۵۳ - پایان موفقیت آمیز تمام تستهای مشخص شده سیستم از صفر تا صد
ساعت ۰۷:۵۴ - پایش و رصد Log ها برای مشکلات غیر منتظره
ساعت ۰۷:۵۹ - پایان
http://mattturck.com/wp-content/uploads/2020/09/2020-Data-and-AI-Landscape-Matt-Turck-at-FirstMark-v1.pdf
https://clickhouse.tech/docs/en/
https://altinity.com/blog/clickhouse-materialized-views-illuminated-part-1
https://altinity.com/blog/2018/8/22/clickhouse-copier-in-practice