چگونه دستور DELETE در پاستگرس را مقیاسپذیر کنیم؟ — How Postgres DELETE can actually scale
خلاصه
دستور DELETE در PostgreSQL به دلیل مکانیسم MVCC و نیاز به مدیریت tupleهای مرده و vacuum معمولاً کند است و بسیاری از سیستمها به سراغ حذف پارتیشن میروند. این مقاله بررسی میکند که چگونه تیم DBOS با بهینهسازی کش، حذف کلیدهای خارجی و مدیریت VACUUM توانستند عملکرد حذف دادهها را به شدت بهبود ببخشند.
دستور DELETE در PostgreSQL شهرت خوبی میان توسعهدهندگان سیستمهای با عملکرد بالا ندارد. بسیاری از مهندسان ترجیح میدهند به جای استفاده از DELETE، کل جدول یا پارتیشنها را DROP یا TRUNCATE کنند. با این حال، دستور DELETE انعطافپذیری بسیار بالایی دارد؛ بهویژه زمانی که با دادههای ناهمگن سروکار دارید و مجبورید سیاستهای نگهداری (Retention Policies) متفاوتی را روی رکوردها اعمال کنید. در این مقاله نگاهی داریم به اینکه تیم توسعه DBOS چگونه توانسته است عملکرد DELETE را در یک سیستم مدیریت Workflow به مقیاس بالا برساند.
نحوه کارکرد DELETE و چالش MVCC
برای درک اینکه چرا حذف داده در PostgreSQL پرهزینه است، باید به نحوه کارکرد Multi-Version Concurrency Control یا همان MVCC نگاهی بیندازیم. وقتی رکوردی را حذف میکنید، دیتابیس بلافاصله آن را از روی دیسک پاک نمیکند تا ایزولهبودن تراکنشها حفظ شود. در عوض، آخرین نسخه رکورد با شناسه تراکنش فعلی علامتگذاری میشود تا برای تراکنشهای بعدی نامرئی گردد.
این مکانیسم باعث ایجاد پدیدهای به نام Churn میشود. رکوردهای حذفشده به Dead Tuples تبدیل میشوند که باید توسط پروسه VACUUM (یا Autovacuum) پاکسازی شوند تا فضای دیسک آزاد شود. این تعامل میان کارهای پسزمینه، کش دیسک و ایندکسها باعث کندی فرآیند پاکسازی دادههای حجیم میشود.
مشکل لوکالیتی و حذف کلیدهای خارجی
در سیستمهای Workflow، معمولاً سیاستهای نگهداری دادهها بر اساس زمان اتمام کار اجرا میشوند. این کار در دستههایی با سایز N انجام میگیرد. مشکل اصلی در اینجا حذف خود جدول اصلی نیست، بلکه دادههای مرتبط با آن (مثل خروجی گامها در جدول step_outputs) است که از طریق ON DELETE CASCADE به جدول اصلی وصل بودند.
استفاده از Foreign Key برای یکپارچگی دادهها عالی است، اما از نظر Cache Locality فاجعهبار است. گامهای یک Workflow در طول زمان روی دیسک پخش شدهاند و حذف آبشاری آنها باعث میشود دیتابیس مدام مجبور به خواندن و نوشتن پیجهای مختلف از دیسک شود.
راهحل تیم DBOS این بود که کلیدهای خارجی را حذف کنند و عملیات پاکسازی را در فازهای جداگانه انجام دهند:
- ابتدا رکوردهای Workflow در دستههای مشخص حذف میشوند.
- سپس جداول مرتبط دیگر (مثل
step_outputs) به صورت دستهای و با استفاده از Anti-Join پاکسازی میشوند.
باگ ایندکس ۲۰ دقیقهای و راه حل VACUUM
تیم توسعه با یک مشکل عجیب روبهرو شد: گاهی اوقات یک کوئری ساده برای پیدا کردن رکوردهای قدیمی با استفاده از ایندکس روی فیلد created_at تا ۲۰ دقیقه طول میکشید، در حالی که پلن اجرای کوئری درست بود.
ریشه مشکل این بود که دیتابیس برای پیدا کردن رکوردهای قدیمی، ایندکس را از قدیمیترین به جدیدترین اسکن میکند. از آنجا که رکوردهای قدیمیتر قبلاً حذف شده بودند اما Autovacuum هنوز به آنها نرسیده بود، میلیونها Dead Tuple در ابتدای ایندکس وجود داشت. کوئری مجبور بود تکتک این موارد را بررسی کند تا از مرده بودن آنها مطمئن شود.
راهحل این بود که یک VACUUM دستی قبل و بعد از هر چرخه نگهداری داده اجرا شود تا Dead Tupleها قبل از ایجاد اختلال پاک شوند.
چرا از پارتیشنبندی استفاده نشد؟
بسیاری از سیستمهای با عملکرد بالا ترجیح میدهند جداول را بر اساس زمان پارتیشنبندی کنند و پارتیشنهای قدیمی را یکجا DROP کنند. این روش بسیار سریع است اما انعطافپذیری لازم را ندارد. اگر چند Workflow طولانیمدت وجود داشته باشد که هفتهها یا ماهها طول بکشند، مانع از حذف کل پارتیشن حاوی میلیاردها رکورد کوتاهمدت دیگر میشوند. علاوه بر این، سیاستهای پیچیده (مثل نگهداری طولانیترِ کارهای ناموفق) با پارتیشنبندی پیادهسازی دشواری دارند.
دیدگاه جامعه هاکرنیوز
بحثهای جالب توجهی در هاکرنیوز پیرامون این مقاله شکل گرفت:
- برخی کاربران اشاره کردند که پایگاههای دادهای دیگر مانند سیستمهایی که از Undo Logs استفاده میکنند، مشکلات مربوط به Autovacuum پاستگرس را ندارند.
- گروهی دیگر معتقد بودند حذف کلیدهای خارجی خطرات دادههای یتیم (Orphaned Data) را افزایش میدهد و بهتر است به جای ابزارهای دیتابیس، معماری اپلیکیشن به گونهای دیگر طراحی شود.
این مطلب بهصورت کاملاً خودکار و با هوش مصنوعی از مقالهٔ اصلی و بحث هکرنیوز تهیه شده و ممکن است خطا داشته باشد. حقوق مطلب اصلی متعلق به نویسندهٔ آن است. قوانین و گزارش مشکل