رفتن به محتوای اصلی
کوهستان
معماری پایگاه داده· 10 دقیقه مطالعه· در فناوری

چگونه برچسب زمانی ترتیبی در UUIDv7 جلوی دو نیم شدن صفحات B-Tree و هدررفت بافر را می‌گیرد

استاندارد UUIDv7 با جایگزین کردن شناسه‌های کاملاً تصادفی با کلیدهای مرتب‌شده بر اساس زمان، معضل تکه‌تکه شدن شاخص‌ها را که موجب افت توان پایگاه داده می‌شود، از بین می‌برد. برچسب زمانی ۴۸ بیتی امکان تزریق منظم داده‌ها را در برگه‌های انتهایی درخت B فراهم کرده و تورم حافظه و بار اضافی لاگ تراکنش‌ها را به حداقل می‌رساند.

به قلم نیما موسوی

در اوت ۲۰۲۶، مهندسان هنگام مهاجرت روی یک کلاستر پرترافیک PostgreSQL 18.4 کلیدهای اصلی جدول‌ها را از مقادیر کاملاً تصادفی UUIDv4 به استاندارد نوین UUIDv7 تغییر دادند. نتیجه بلافاصله خود را نشان داد: افزایش ۲۳ برابری در سرعت ثبت و درج دسته‌ای رکوردها. این جهش عظیم نه به خاطر ارتقا یا تعویض موتور پایگاه داده، بلکه صرفاً ناشی از بازآرایی ساختار بیت‌ها درون شناسه بود.[4]

سال‌هاست توسعه‌دهندگان برای تولید کلیدهای یکتا در سیستم‌های توزیع‌شده بدون نیاز به سرور مرکزی هماهنگ‌کننده، به شناسه‌های یکتای جهانی (UUID) متکی هستند. نسخه پیش‌فرض و غالب یعنی UUIDv4، معادل ۱۲۲ بیت داده کاملاً تصادفی در اختیار می‌گذارد؛ این یعنی در نمونه‌ای شامل ۳۲ کوادریلیون مقدار، ۹۹.۹۹ درصد احتمال دارد حتی یک نمونه تکراری هم یافت نشود. با این حال، دقیقاً همین تصادفی‌بودنِ مطلق است که در خفا عملکرد پایگاه‌های داده رابطه‌ای را نابود می‌کند.[6]

از آنجا که مقادیر UUIDv4 ترتیبی ندارند، بلای جان شاخص‌های B-Tree می‌شوند؛ همان ساختاری که موتورهایی مانند PostgreSQL و MySQL برای ساماندهی و مرتب‌سازی داده‌ها از آن بهره می‌برند. الگوی درهم‌ریخته و غیرقابل پیش‌بینی درج کلیدها، موتور دیتابیس را مجبور به شکافتن پیاپی صفحات شاخص کرده و حجم ذخیره‌سازی را بالا می‌برد. گروه ضربت مهندسی اینترنت (IETF) در مه ۲۰۲۴ با معرفی RFC 9562 گزینه‌ای زمان‌محور و مرتب را برای حل این بحران ارائه داد.[2][6]

کالبدشکافی انشقاق صفحات B-Tree

برای درک چرایی افت عملکرد با شناسه‌های تصادفی، باید نحوه ذخیره شاخص‌ها در پایگاه‌های داده رابطه‌ای را بررسی کرد. در پلتفرم PostgreSQL کلیدهای اصلی درون ساختار درختی مرتب موسوم به B-Tree چیده می‌شوند؛ جایی که پایین‌ترین لایه از صفحاتی به نام برگه با حجم ثابت ۸ کیلوبایت تشکیل شده است. هر برگه شامل آرایه‌ای مرتب از کلیدها و اشاره‌گرهایی به رکوردهای واقعی داخل جدول است.[3]

وقتی برنامه یک کلید متوالی — مثل اعداد صحیح ۶۴ بیتی معمولی — درج می‌کند، موتور دیتابیس دقیقاً می‌داند رکورد کجا می‌نشیند. از آنجا که هر عدد جدید از نظر مقداری بزرگ‌تر از قبلی است، همواره در منتهی‌الیه راست‌ترین برگه درخت جای می‌گیرد. همین یک صفحه فعال در حافظه مشترک رم نگه‌داشته می‌شود و تا پیش از پر شدن کامل، هزاران رکورد جدید را بدون اصطکاک تحویل می‌گیرد.[2][3]

اما شناسه تصادفی UUIDv4 درست برعکس عمل می‌کند. هر مقدار جدید با احتمالی برابر ممکن است در هر گوشه‌ای از شاخص سر درآورد؛ بنابراین هر عملیات درج به صفحه‌ای متفاوت و غیرقابل پیش‌بینی برخورد می‌کند. موتور پایگاه داده برای ثبت یک رکورد ساده، پیوسته ناچار می‌شود صفحات سرد و دور از دسترس را از حافظه دیسک فیزیکی فراخوانی کند.[2]

درج مقادیر کاملاً تصادفی صفحات پرشده را از وسط دو نیم می‌کند؛ پدیده‌ای که شاخص را دچار تورم و چگالی ناقص می‌سازد.

خسارت ساختاری زمانی اوج می‌گیرد که کلید تصادفی جدید به صفحه‌ای برخورد کند که از قبل پر شده است. چون داده‌ها باید با ترتیب قطعی حفظ شوند، دیتابیس نمی‌تواند به سادگی مقدار را ته صف بچسباند. به ناچار عملیات انشقاق یا دو نیم شدن صفحه (Page Split) رخ می‌دهد؛ PostgreSQL تقریباً نیمی از داده‌های صفحه قبلی را به یک صفحه خالی ۸ کیلوبایتی جدید منتقل می‌کند تا جا باز شود.[1][3]

پایگاه تحلیلی DBGorilla در این زمینه می‌نویسد: «درج‌های تصادفی، صفحات را در سرتاسر شاخص از وسط نصف می‌کنند؛ این کار صفحاتی نیمه‌خالی به جا می‌گذارد و حجم شاخص برای همان تعداد داده افزایش می‌یابد.» این یک جبر مکانیکی در رفتار موتور دیسک است. با گذشت زمان، یک درخت B که با شناسه‌های تصادفی پر شده باشد، به چگالی متوسطی در حدود تنها ۶۹ درصد رضایت می‌دهد.[1][5]

این تکه‌تکه شدن و هدررفت فضا باعث می‌شود شاخص یک ستون UUIDv4 تقریباً دو برابر یک ستون عدد صحیح ترتیبی با همان تعداد رکورد، حافظه دیسک اشغال کند. پایگاه داده باید درختی بسیار قطورتر و گسترده‌تر را بپیماید تا یک داده را بخواند. این بادکردگی ساختاری، سرعت پرس‌وجوهای مستقیم و جستجوهای بازه‌ای را در سراسر سامانه به وضوح پایین می‌آورد.[1][6]

دومینوهای ناشی از تورم بافر

اتلاف حافظه دیسک تازه شروع ماجراست؛ پیامد مرگبارتر، پدیده‌ای به نام تورم بافر است که حافظه رم سیستم را اشغال و مسدود می‌کند. ابزار PostgreSQL فضایی اختصاصی موسوم به بافرهای مشترک (Shared Buffers) دارد تا صفحات شاخص‌های پرکاربرد را برای پاسخگویی سریع، درون حافظه رم فعال نگه دارد.[1][3]

در کلیدهای ترتیبی، محدوده داده‌های فعال و داغ بسیار فشرده است و نگه داشتن تنها چند برگه راست درخت در کش کفایت می‌کند. اما با کلیدهای تصادفی UUIDv4، کل پیکره شاخص به محدوده فعال تبدیل می‌شود، زیرا ثبت بعدی می‌تواند مربوط به هر صفحه‌ای باشد. وقتی حجم شاخص فرسوده از ظرفیت رم مشترک فراتر می‌رود، نرخ پاسخ‌دهی موفق کش سقوط می‌کند.[1][2]

در چنین وضعیتی، پایگاه داده در باتلاق جابه‌جایی مکرر کش (Cache Thrashing) گرفتار می‌شود؛ صفحات مفید را پاک می‌کند تا یک صفحه دورافتاده را برای ثبت یک رکورد بارگذاری کند، و لحظه‌ای بعد دوباره به همان صفحات حذف‌شده نیاز پیدا می‌کند. این رفت‌وبرگشت مداوم توان پردازنده را می‌سوزاند و دیسک را با عملیات تصادفی خواندن و نوشتن اشباع می‌کند.[6]

شناسه UUIDv4 با دستکاری بی‌نظم صفحات در سرتاسر درخت، حجم لاگ تراکنش‌ها را به شکل سرسام‌آوری بالا می‌برد.

این آشفتگی همچنین حجم فایل‌های لاگ تراکنش (WAL) را — که PostgreSQL برای بازیابی در صورت قطعی یا خرابی استفاده می‌کند — سرسام‌آور افزایش می‌دهد. تمهیدات ایمنی موتور ایجاب می‌کند که با اولین ویرایش در هر برگه پس از ذخیره نقطه‌ای (Checkpoint)، کل بلوک ۸ کیلوبایتی صفحه در لاگ ثبت شود. درج‌های متوالی تنها چند صفحه را دستکاری می‌کنند، در حالی که درج تصادفی تقریباً همه صفحات درخت را دستخوش تغییر می‌کند.[1][5]

مجموعه تخصصی MyDBA.dev تأکید می‌کند: «کلیدهای UUIDv4 در تمام شاخص پراکنده می‌شوند، انشقاق پنجاه درصدی در صفحات می‌اندازند و حجم لاگ تراکنش را منفجر می‌کنند.» این پدیده موسوم به تقویت نوشتن، پایگاه داده را وادار می‌کند تا گیگابایت‌ها تصویر تکراری از صفحات را در لاگ ذخیره کند؛ فرآیندی که استهلاک درایوهای SSD را بالا می‌برد و همگام‌سازی سرورهای پشتیبان را با تأخیر مواجه می‌سازد.[3]

در سامانه‌های پرترافیک، این حجم عظیم از لاگ‌نویسی به تنهایی گلوگاه اصلی کل کلاستر می‌شود. کنترل‌کننده حافظه زیر فشار هجوم مداوم صفحات کم می‌آورد و زمان تأخیر درج رکوردها به شکل نامنظمی بالا می‌رود. پایگاه داده بیش از آنکه به پردازش اطلاعات اصلی کاربران برسد، توان خود را صرف مدیریت مکانیزم‌های پایداری خود می‌کند.[1]

سیر تکامل شناسه‌ها از نسخه ۱ تا نسخه ۷

ایده بهره‌گیری از زمان در ساختار UUID کاملاً جدید نیست. نسخه نخستین یعنی UUIDv1 که در دهه ۱۹۹۰ میلادی شکل گرفت نیز برچسب زمانی داشت. اما بخش‌های زمانی در آن معکوس چیده شده بود و از همه بدتر، آدرس فیزیکی کارت شبکه (MAC) دستگاه را فاش می‌کرد؛ معضلی امنیتی که باعث شد صنعت نرم‌افزار آن را کنار بگذارد.[4]

نسخه UUIDv4 مشکل حریم خصوصی را با تکیه کامل بر اعداد تصادفی مرتفع کرد، اما پیوند و همخوانی فنی با سخت‌افزارهای ذخیره‌سازی داده را به کلی از دست داد. برای سال‌ها، تیم‌های فنی با کدنویسی راهکارهای اختصاصی و شناسه متوالی در صدد حل این مشکل برآمدند؛ اقداماتی که اکوسیستم را تکه‌تکه کرد و نیازمند افزونه‌های اختصاصی در هر موتور دیتابیس بود.[1][6]

معیار فنی RFC 9562 این تلاش‌های پراکنده را ذیل یک استاندارد جهانی و مستقل از زبان برنامه‌نویسی گرد آورده است. این مشخصات، تصادفی‌بودنِ صِرف در نسخه ۴ را با یک معماری منظم و زمان‌محور تعویض می‌کند، در حالی که طول نهایی شناسه همان ۱۲۸ بیت باقی می‌ماند و نیازی به تغییر نوع ستون در جداول داده نیست.[2][5]

معماری استاندارد UUIDv7 برچسب زمانی ۴۸ بیتی را در صدر بیت‌ها قرار می‌دهد تا ترتیب زمانی داده‌ها همواره رعایت شود.

نوآوری کلیدی در ۴۸ بیت نخست قرار دارد که زمان یونیکس را بر حسب میلی‌ثانیه ذخیره می‌کند. از آنجا که این زمان در باارزش‌ترین بیت‌ها (Most Significant Bits) جا خوش کرده، هر UUIDv7 جدید به صورت طبیعی مقداری بزرگ‌تر از شناسه‌های قبلی خواهد داشت. این ویژگی تضمین می‌کند که مرتب‌سازی الفبایی پیش‌فرض هم کلیدها را دقیقاً بر اساس توالی زمانی ردیف کند.[2][6]

چگونه برچسب زمانی گره‌های درخت را التیام می‌دهد

تیم مهندسی pgEdge در توصیف این مکانیسم می‌گوید: «کلیدهای جدید دقیقاً در لبه سمت راست درخت قرار می‌گیرند؛ همان جایی که پیش‌تر فیلدهای ترتیبی BIGINT می‌نشستند. همین تغییر، تمام آنچه را نسخه ۴ خراب کرده بود ترمیم کرد: همجواری ترتیبی، متمرکز ماندن داده‌های فعال در حافظه و انشقاق‌های تمیز صفحات.» موتور پایگاه داده سرانجام به دوران ثبت پرسرعت بازمی‌گردد.[2]

پس از برچسب زمانی، ۴ بیت برای مشخص کردن نسخه شناسه به کار می‌رود و ۷۴ بیت باقیمانده به داده‌های کاملاً تصادفی و امن از نظر رمزنگاری اختصاص می‌یابد. این دنباله تصادفی تضمین می‌کند که اگر ده‌ها سرور مختلف در کسری از یک میلی‌ثانیه به طور هم‌زمان شناسه تولید کنند، کلیدها در سطح شبکه تکراری نشوند؛ آن هم بدون نیاز به هیچ‌گونه هماهنگی بر بستر شبکه.[2]

این ۷۴ بیت فضای تصادفی بیش از ۱۸ سکستیلیون ترکیب در هر میلی‌ثانیه تولید می‌کند. بدین ترتیب، حتی اگر ارتشی از سرورها میلیون‌ها کلید در لحظه صادر کنند، احتمال تصادم به صفر میل می‌کند؛ مزیتی که استقلال و تمرکززدایی نسل قبلی را تمام و کمال حفظ می‌کند.[2][6]

افزون بر این، استاندارد مذکور اجازه می‌دهد تا در صورت نیاز، ۱۲ بیت از بخش تصادفی به عنوان یک شمارنده صعودی پیوسته استفاده شود. اگر یک ماشین خاص چندین کلید را در یک میلی‌ثانیه صادر کند، این شمارنده پشت سر هم بالا می‌رود. این گزینه اختیاری حتی در مقیاس‌های میکروثانیه‌ای هم نظم ترتیبی دقیق را حفظ کرده و درخت B را منسجم نگه می‌دارد.[2]

به دلیل همبستگی زمانی مقادیر، عملیات درج به شکلی سازمان‌یافته به سمت برگه‌های راست درخت هدایت می‌شود. صفحات برگه تا انتها پر شده و بدون تکه‌تکه شدن شکافته می‌شوند که این امر ضریب پرشدگی شاخص را به مرز ۹۰ تا ۹۵ درصد می‌رساند. مجموعه صفحات داغ دوباره در حافظه رم مهار شده و پدیده تورم بافر ناپدید می‌شود.[5]

با حجیم شدن شاخص، توان درج رکورد در UUIDv4 به دلیل کمبود رم افت می‌کند، در حالی که UUIDv7 پایداری خود را حفظ می‌کند.

کاهش مداوم برگه‌های دستکاری‌شده، بار ثبت لاگ تراکنش را هم می‌شکند. نتایج بررسی‌های مهندسان نشان می‌دهد جابه‌جایی به شناسه‌های زمان‌محور می‌تواند حجم نوشتن صفحات در لاگ WAL را هنگام درج‌های فله‌ای تا ۹۹.۹۷ درصد کاهش دهد. دیسک‌های ذخیره‌سازی ذخیره آزاد می‌شوند تا توان خود را به نوشتن داده‌های واقعی تخصیص دهند.[5]

مسیر مهاجرت در بسترهای نوین

جامعه دیتابیس با سرعتی بالا به سمت استانداردسازی این فرمت حرکت می‌کند. در این راستا PostgreSQL 18 با تابع بومی تولید این شناسه درون هسته اصلی عرضه می‌شود. بدین ترتیب مهندسان بدون نیاز به توابع سفارشی SQL یا افزونه‌های جانبی، می‌توانند از این فرمت به عنوان کلید اصلی استفاده کنند.[1][3]

برای سیستم‌هایی که از نسخه‌های قدیمی‌تر استفاده می‌کنند هم مسیر تغییر هموار است. از آنجا که UUIDv7 در سطح لایه اپلیکیشن فرمت‌بندی می‌شود، کتابخانه‌های نرم‌افزاری در جاوا، پایتون یا گو می‌توانند شناسه زمان‌محور را پیش از ارسال دستور ثبت بسازند. دیتابیس صرفاً یک رشته باینری ۱۶ بایتی دریافت می‌کند، بی‌آنکه نیازی به دانستن شیوه ساخت آن داشته باشد.[1]

برای سیستم‌هایی که از نسخه‌های قدیمی‌تر استفاده می‌کنند هم مسیر تغییر هموار است.

مهاجرت داده‌های قدیمی از نسخه ۴ به نسخه ۷ نیازی به بازنویسی دردسرساز ساختار ستون ندارد. هر دو فرمت ظرفیت باینری کاملاً یکسانی دارند و قفل‌گذاری روی جدول اتفاق نمی‌افتد. کافی است منطق پیش‌فرض تولید کلید برای رکوردهای جدید تغییر یابد تا شاخص به مرور زمان انسجام خود را بازپس گیرد.[1][5]

البته رکوردهای قدیمی با فرمت نامنظم UUIDv4 همچنان در بخش‌های کهنه درخت B باقی خواهند ماند. برای بازپس‌گیری کامل فضای اشغال‌شده و رفع چندپارگی پیشین، مدیران پایگاه داده باید در نهایت شاخص را بازسازی کنند؛ کاری که با دستور concurrent reindex در پنجره‌های زمانی نگهداری سامانه بدون قطعی سرویس انجام می‌پذیرد.[1]

گرچه برخی شرکت‌های نرم‌افزاری شناسه UUIDv7 را به عنوان معجزه‌ای جادویی تبلیغ می‌کنند، ماجرا در عمل کاملاً مبتنی بر منطق مکانیکی ذخیره‌سازی است. این فرمت موتور پایگاه داده را ذاتاً سریع‌تر نمی‌کند؛ فقط دست سیستم را از تخریب مکرر مدیریت حافظه کوتاه می‌سازد. از یاد نبریم که حجم ۱۶ بایتی این شناسه کماکان دو برابر یک عدد صحیح ۶۴ بیتی معمولی است.[1][3]

طرحواره: شاخص UUIDv7 با متمرکز نگه‌داشتن صفحات فعال در رم، پهنای باند دیسک را برای انجام عملیات اصلی برنامه آزاد می‌سازد.

برای زیرساخت‌هایی که نیازی به تولید نامتمرکز کلید ندارند، همان فیلدهای ترتیبی عددی همچنان بهینه‌ترین کارایی را دارند. اما در سیستم‌های توزیع‌شده، UUIDv7 تعادلی طلایی است؛ استانداردی که با تطبیق فرمت شناسه با واقعیت‌های فیزیکی دیسک، پایانی بر یک دام ساختاری ده‌ساله می‌گذارد.[3][6]

نکات کلیدی

  • ماهیت تصادفی UUIDv4 شاخص‌های B-Tree دیتابیس را به انشقاق مداوم صفحات، تکه‌تکه شدن ساختار و اشغال بی‌رویه رم می‌کشاند.
  • استاندارد RFC 9562 فرمت UUIDv7 را با درج برچسب زمانی ۴۸ بیتی معرفی کرد تا داده‌ها با توالی زمانی و بدون آشفتگی در شاخص ثبت شوند.
  • مهاجرت به این شناسه‌های زمان‌محور می‌تواند بار تولید فایل‌های لاگ تراکنش را تا ۹۹.۹۷ درصد کاهش داده و سرعت درج را بدون تغییر نوع ستون‌ها متحول کند.
بهینه‌سازی ذخیره‌سازی 50%معماری سیستم‌ها 50%
بهینه‌سازی ذخیره‌سازی
تمرکز بر سازگاری میان ساختار داده‌ها و ماهیت فیزیکی حافظه‌های ذخیره‌سازی برای دستیابی به حداکثر کارایی سخت‌افزاری.
معماری سیستم‌ها
تأکید بر کاربرد عملیاتی شناسه‌های نامتمرکز جهت سهولت توسعه مستقل میکروسرویس‌ها بدون وابستگی به هسته دیتابیس.

دیدگاه‌هایی که این گزارش پوشش نداده

  • توسعه‌دهندگان و نگهدارندگان سامانه‌های قدیمی

منابع

پوشش منابع

6 منبع

2 دیدگاه شناسایی‌شده

بهینه‌سازی ذخیره‌سازی 50%معماری سیستم‌ها 50%
  1. [1]DBGorillaبهینه‌سازی ذخیره‌سازی

    The fourth, subtler effect: B-tree page splits

    مطالعه در DBGorilla →
  2. [2]pgEdgeمعماری سیستم‌ها

    UUID version 7 comes in (as standardized in RFC 9562)

    مطالعه در pgEdge →
  3. [3]MyDBA.devبهینه‌سازی ذخیره‌سازی

    UUID vs BIGINT Primary Key in Postgres: Which Wins?

    مطالعه در MyDBA.dev →
  4. [4]Daily.devمعماری سیستم‌ها

    A production migration from UUID v1/v4 to UUID v7 primary keys

    مطالعه در Daily.dev →
  5. [5]Mediumبهینه‌سازی ذخیره‌سازی

    Time clustering keeps recent values in the same region of the index

    مطالعه در Medium →
  6. [6]Dev.toمعماری سیستم‌ها

    Benchmarking UUIDv4 and UUIDv7 in PostgreSQL

    مطالعه در Dev.to →

نظرات

همیشه در جریان باشید

هر زاویه. هر روز.

اخبار فناوری با پوشش کامل منابع و تحلیل دیدگاه‌ها، هر روز و رایگان.