برنامه نویسی

🦦 آنچه OtterTune در PostgreSQL بیش از همه از آن متنفر است🐘 در YugabyteDB حل شده است🚀

داشتم قسمتی از PostgreSQL که ما از همه بیشتر متنفریم، نوشته بوهان ژانگ و اندی پاولو را می خواندم. این مقاله مشکلات را به یادگاری از دهه 1980 نسبت می دهد. بیایید با YugabyteDB مقایسه کنیم که از Postgres برای پردازش SQL استفاده می کند اما با یک فضای ذخیره سازی توزیع شده ساخته شده برای زیرساخت مدرن.

اساساً، مقاله OtterTune برخی از معایب پیاده‌سازی MVCC در PostgreSQL را با توضیحات خوب، اما بدون تفاوت‌های ظریف، توضیح می‌دهد. در این پست به نکات مثبت آن نیز اشاره خواهم کرد.

چالش ها و مسائل

4 مشکل در مقاله شرح داده شده است

مشکل شماره 1: کپی کردن نسخه

وقتی یک بایت را در یک به روز می کنید PostgreSQL ردیف، کل ردیف به جای تغییر فقط کپی می شود. آزمایش این آسان است:

drop table demo;
create table demo as 
 select generate_series(1,10000000) n, 'N' flag, lpad('x',1000,'x') filler;
vacuum demo;

select pg_size_pretty(pg_table_size('demo'));
explain (analyze, wal, buffers, costs off) update demo set flag='Y';

select pg_size_pretty(pg_table_size('demo'));

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

بخش جالب این است که من پرچم را به روز می کنم، WAL تولید شده را بررسی می کنم و اندازه جدول را افزایش می دهم:

postgresql=> select pg_size_pretty(pg_table_size('demo'));
 pg_size_pretty
----------------
 11 GB
(1 row)

postgresql=> explain (analyze, wal, buffers, costs off) 
             update demo set flag='Y';

                                 QUERY PLAN
-----------------------------------------------------------------------------
 Update on demo (actual time=103872.095..103872.097 rows=0 loops=1)
   Buffers: shared hit=34285343 read=1428953 dirtied=2857543 written=1493088
   I/O Timings: read=8671.563 write=259.356
   WAL: records=20000354 fpi=1428940 bytes=11758071248
   ->  Seq Scan on demo (actual time=0.540..13265.632 rows=10000000 loops=1)
         Buffers: shared read=1428572 written=32324
         I/O Timings: read=8669.064 write=127.201
 Planning:
   Buffers: shared hit=7
 Planning Time: 0.061 ms
 Execution Time: 103872.487 ms
(11 rows)

select pg_size_pretty(pg_table_size('demo'));
 pg_size_pretty
----------------
 22 GB
(1 row)
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

اندازه جدول دو برابر شده است (از 11 گیگابایت به 22 گیگابایت) و WAL تولید شده به اندازه کامل اضافی (11758071248 بایت) است. اساساً، همه ردیف‌ها با یک بایت تغییر به نسخه جدیدتر کپی شده‌اند و تمام بلوک‌های درگیر برای اهداف بازیابی در WAL ثبت شده‌اند.

من در حال اجرا همان در یک YugabyteDB خوشه. اندازه WAL در آن گنجانده شده است pg_table_size (برای محاسبه اندازه واقعی به عنوان اولین سطح از LSM-Tree در حافظه):

drop table demo;
create table demo as 
 select generate_series(1,10000000) n
     , 'N' flag, lpad('x',1000,'x') filler;

select pg_size_pretty(pg_table_size('demo'));

explain (analyze, buffers, costs off) update demo set flag='Y';

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

در اینجا نتیجه است:

yugabyte=> select pg_size_pretty(pg_table_size('demo'));

 pg_size_pretty
----------------
 1572 MB
(1 row)

yugabyte=> explain (analyze, buffers, costs off)
           update demo set flag='Y';

                                  QUERY PLAN
-------------------------------------------------------------------------------
 Update on demo (actual time=539216.462..539216.462 rows=0 loops=1)
   ->  Seq Scan on demo (actual time=19.515..514476.322 rows=10000000 loops=1)
 Planning Time: 0.697 ms
 Execution Time: 539264.892 ms
 Peak Memory Usage: 27 kB
(5 rows)

yugabyte=> select pg_size_pretty(pg_table_size('demo'));

 pg_size_pretty
----------------
 1762 MB
(1 row)

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

اندازه جدول زیاد افزایش نیافته است، این همان چیزی است که وقتی فقط یک بایت برای هر ردیف به روز می کنید، می توانید انتظار داشته باشید.

مشکل شماره 2: نفخ روی میز

را PostgreSQL autovacuum فضا را از فایل سیستم پس نمی گیرد. این نیاز به یک سازماندهی مجدد کامل (VACUUM FULL) در طول یک پنجره تعمیر و نگهداری دارد.

postgres=> vacuum demo;
VACUUM

postgres=> select pg_size_pretty(pg_table_size('demo'));

 pg_size_pretty
----------------
 22 GB
(1 row)

postgres=> select pg_size_pretty(pg_table_size('demo'));
 pg_size_pretty
----------------
 11 GB
(1 row)

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

که در YugabyteDB، هیچ سوراخی در فایل ها وجود ندارد. برای بازیابی فضا از نسخه‌های میانی بالای ذخیره (به‌طور پیش‌فرض 15 دقیقه)، فایل‌های فعلی با فشرده‌سازی پس‌زمینه با فایل‌های جدید ادغام می‌شوند. این همان اثر VACUUM FULL را دارد، اما، چون فایل‌های SSD غیرقابل تغییر هستند، با تراکنش‌های در حال انجام تضاد ندارد (به جز منابعی که هنگام اجرا توسط آن گرفته می‌شود، یا تقویت فضا/خواندن در صورت اجرا نشدن) و نیازی به بازسازی نمایه ها نیست.

می‌توانم میز را با فشردگی کامل به اندازه اصلی‌اش برگردانم، اما نیازی به آن نیست. فشرده سازی در برخی از آستانه هایی که در پست قبلی توضیح داده ام اتفاق می افتد.

مشکل شماره 3: نگهداری شاخص ثانویه

PostgreSQL ورودی های فهرست را برای نسخه های جدید بدون حذف ورودی های قبلی اضافه می کند، تا زمانی که autovacuum بتواند این کار را انجام دهد. این با فعالیت برنامه رقابت می کند و زمانی که تراکنش های طولانی در حال اجرا هستند به تعویق می افتد. ایندکس ها اطلاعات قابل مشاهده را ندارند. پیامد آن، که در مقاله OtterTune ذکر نشده است، این است که حتی در مورد Index Only Scan، جدول باید برای مشاهده MVCC خوانده شود. این می تواند روی یک میز تازه جاروبرقی سریع باشد (زیرا یک بیت مپ دید به روز شده است) اما اگر نه طولانی است. توجه داشته باشید که نگهداری شاخص را می توان با رزرو فضای خالی کافی (تنظیم شده با FILLFACTOR) کاهش داد تا نسخه جدید ردیف در همان بلوک قرار گیرد (بهینه سازی HOT).

جداول PostgreSQL جداول Heap هستند و کلید اصلی یک شاخص ثانویه است. افزودن یک کلید اصلی به جدول من ایندکس را ایجاد می کند و دسترسی به کلید اصلی نیاز به خواندن های زیادی دارد:

postgres=> alter table demo add primary key (n);
ALTER TABLE

postgres=> select pg_size_pretty(pg_table_size('demo'));
 pg_size_pretty
----------------
 11 GB
(1 row)

postgres=> select pg_size_pretty(pg_table_size('demo_pkey'));
 pg_size_pretty
----------------
 214 MB
(1 row)

postgres=> explain (analyze, buffers, costs off) select * from demo where n=42;
                                  QUERY PLAN
------------------------------------------------------------------------------
 Index Scan using demo_pkey on demo (actual time=0.861..0.862 rows=1 loops=1)
   Index Cond: (n = 42)
   Buffers: shared read=4
   I/O Timings: read=0.843
 Planning:
   Buffers: shared hit=13 read=1 dirtied=3
   I/O Timings: read=1.390
 Planning Time: 2.056 ms
 Execution Time: 0.889 ms
(9 rows)

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

برای یک ردیف، 4 بافر از شاخص (سطوح B-Tree) و جدول خوانده شده است.

YugabyteDB جدول را مانند بسیاری از پایگاه داده های دیگر در کلید اصلی خود ذخیره می کند. همه (جدول و نمایه ها) به عنوان LSM-Tree با اطلاعات MVCC ذخیره می شوند. بدون نیاز به FILLFACTOR، بدون تکراری از ردیف ها هنگام به روز رسانی وجود دارد. حتی زمانی که یک ردیف جدول به صورت فیزیکی حرکت می‌کند (مثلاً در حین اشتراک‌گذاری مجدد خودکار)، شاخص هیچ‌گونه سربار تعمیر و نگهداری اضافه نمی‌کند، زیرا به کلید اصلی اشاره می‌کند و نه یک مکان فیزیکی.

یک Index Scan از یک نمایه ثانویه باید از طریق کلید اصلی خود به جدول برود، که در یک LSM-Tree سریع است، و همچنین می توان با Index Only Scan که هرگز مجبور به خواندن جدول نیست، از این کار صرفنظر کرد.

من در اینجا نمایه های پوششی را توضیح داده ام.

وقتی یک کلید اصلی اضافه می کنم، اندازه یکسان است (هیچ نمایه اضافی وجود ندارد) و دسترسی توسط کلید اصلی فقط یک بار خوانده می شود:

yugabyte=> alter table demo add primary key (n);
ALTER TABLE

yugabyte=> select pg_size_pretty(pg_table_size('demo'));
 pg_size_pretty
----------------
 1314 MB
(1 row)

yugabyte=> select pg_size_pretty(pg_table_size('demo_pkey'));
 pg_size_pretty
----------------

(1 row)

yugabyte=> explain (analyze, dist, costs off) select * from demo where n=42;
                                  QUERY PLAN
------------------------------------------------------------------------------
 Index Scan using demo_pkey on demo (actual time=1.206..1.208 rows=1 loops=1)
   Index Cond: (n = 42)
   Storage Index Read Requests: 1
   Storage Index Execution Time: 2.000 ms
 Planning Time: 0.054 ms
 Execution Time: 1.242 ms
 Storage Read Requests: 1
 Storage Write Requests: 0
 Storage Execution Time: 2.000 ms
 Peak Memory Usage: 0 kB
(10 rows)

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

اندازه در واقع حتی کوچکتر است زیرا فشردگی اتفاق افتاده است. دسترسی به صورت نمایش داده می شود Index Scan اما، در کلید اصلی، این همان an است Index Only Scan.

مشکل شماره 4: مدیریت خلاء

به دلیل نفخ و همچنین به دلیل PostgreSQL بسته شدن شناسه تراکنش، خلاء باید به طور مکرر اجرا شود و توسط تراکنش های طولانی مسدود نشود. این می تواند با نرخ بالای DML دشوار باشد و نکته اصلی مقاله OtterTune ذکر ابزار آنها است که نظارت بر آن را آسان تر می کند.

YugabyteDB این مشکل نفخ را ندارد تقویت فضای فایل‌های SST با فشرده‌سازی پس‌زمینه حل می‌شود که با تراکنش‌های در حال انجام تضادی ندارد زیرا آنها فقط فایل‌های SST تغییرناپذیر را می‌خوانند و می‌نویسند. همچنین هیچ شناسایی شناسه تراکنش وجود ندارد زیرا تراکنش ها با ساعت منطقی ترکیبی که همیشه در حال افزایش است، توالی می شوند.

به طور خلاصه، VACUUM یک No-op در YugabyteDB است و برای سازگاری با اسکریپت های ساخته شده برای PostgreSQL وجود دارد:

yugabyte=# vacuum demo;
WARNING:  VACUUM will be ignored
VACUUM
yugabyte=#

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

مزایای

در مقاله OtterTune هیچ مزیتی از پیاده سازی PostgreSQL MVCC ذکر نشده است. همیشه برخی معاوضه ها وجود دارد.

برگشت سریع

من بسیار با پایگاه داده Oracle کار کرده ام، که همیشه آن را بهترین پیاده سازی MVCC برای Heap Tables و B-Trees می دانستم. با این حال، یک مورد وجود دارد که PostgreSQL بهتر است. بازگشت مجدد در PostgreSQL زمان نمی برد:

postgres=> \timing on
Timing is on.

postgres=> begin transaction;
BEGIN
Time: 31.719 ms

postgres=*> delete from demo;
DELETE 10000000
Time: 88588.840 ms (01:28.589)

postgres=*> rollback;
ROLLBACK
Time: 31.856 ms

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

حتی اگر YugabyteDB دارای پیاده سازی متفاوتی از MVCC است، سوابق موقت تراکنش به IntentsDB (یکی دیگر از LSM-Tree) می روند تا در پس زمینه با RegularDB در commit ادغام شوند.

YugabyteDB از همین رفتار سود می برد: بازگشت سریع:

yugabyte=> begin transaction;
BEGIN
Time: 31.178 ms
yugabyte=*> delete from demo;
DELETE 10000000
Time: 376758.463 ms (06:16.758)
yugabyte=*> rollback;
ROLLBACK
Time: 31.061 ms
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

این نه تنها برای بازگشت کاربر مفید است، بلکه برای Recovery Time Objective نیز حیاتی است. در صورت بازیابی، تراکنش در حال انجام باید قبل از اینکه جداول در دسترس باشد، برگشت داده شود.

برای انجام همین کار، اوراکل (یا MySQL InnoDB) باید قبل از اینکه جدول دوباره در دسترس قرار گیرد، از زنجیره‌ای از بخش‌های بازگشتی عبور کند تا همه تغییرات را یکی یکی خنثی کند. من این را با تراکنش‌هایی که هرگز پایان نمی‌یابند و کاربر درخواست می‌کند آن را بکشد، دیده‌ام. اگر آن را بکشید، و حتی اگر بتوانید پایگاه داده را مجددا راه اندازی کنید، بازگردانی باید انجام شود. همه DBAهای Oracle به خاطر دارند V$TRANSACTION.USED_UREC را نظارت کنند تا ببینند چگونه کاهش می یابد و تخمین می زنند که چه زمانی دوباره در دسترس خواهد بود.

DEMO@o21c_tp> set timing on

DEMO@o21c_tp> create table demo compress as select rownum n, 'N' flag, lpad('x',1000,'x') filler from xmltable('1 to 10000000');

Table DEMO created.

Elapsed: 00:00:24.971

DEMO@o21c_tp> alter table demo add primary key (n);

Table DEMO altered.

Elapsed: 00:00:07.714
DEMO@o21c_tp> delete from demo;

10,000,000 rows deleted.

Elapsed: 00:01:30.074
DEMO@o21c_tp> rollback;

Rollback complete.

Elapsed: 00:02:53.114
DEMO@o21c_tp>
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

بازگشت به عقب طولانی تر از خود عملیات در این پایگاه داده اوراکل خودکار بود.

انواع شاخص

مزیت دیگر از PostgreSQL پیاده سازی MVCC به این صورت است که پیچیدگی مدیریت تراکنش را به شاخص ها فشار نمی دهد. این برای توسعه پذیری خوب است. 6 نوع فهرست با PostgreSQL وجود دارد که برای موارد استفاده زیادی بهینه می شوند و قابل توسعه است.

YugabyteDB، به دلیل شاردینگ و قابلیت‌های زیاد LSM-Trees، مانند اسکن ترکیبی، ممکن است به انواع شاخص زیادی نیاز نداشته باشد. سخت‌افزار مدرن و توانایی کوچک‌سازی، Seq Scan را برای موارد بیشتر کارآمد می‌کند، مخصوصاً با فیلتر از راه دور پایین‌رفته. در حال حاضر دو نوع شاخص (LSM و GIN) وجود دارد. GiST در نقشه راه برای پشتیبانی کامل از PostGIS است. نمایه‌های LSM همچنین ویژگی‌های دیگری را ارائه می‌کنند که در PostgreSQL وجود ندارد: اسکن شاخص شل، کوچک‌سازی، …

به طور خلاصه

همیشه در IT معاوضه هایی وجود دارد. برخی از انتخاب‌ها در زمینه‌های مختلف انجام شده‌اند و پایگاه‌های داده سنتی ممکن است همچنان از همان پیاده‌سازی روی سخت‌افزار مدرن استفاده کنند. پایگاه های اطلاعاتی جدید امکان انتخاب های مختلف را دارند. من بسیار مشتاق هر تلاشی برای بهبود فضای ذخیره سازی PostgreSQL هستم: پروژه رها شده zHeap، موتور Oriole مدرن، نئون انشعاب، و البته YugabyteDB توزیع شده.

همچنین خوب است به یاد داشته باشید که PostgreSQL برای بسیاری از موارد نیز به اندازه کافی خوب است و @ryanbooz مقاله خوبی برای متعادل کردن موارد منفی بسیاری در مورد PostgreSQL MVCC نوشته است:

آنچه OtterTune در PostgreSQL بیش از همه از آن متنفر

فاویکون
softwareandbooz.com

من YugabyteDB را به عنوان حل کننده همه چیز در مورد MVCC در اینجا ارائه کرده ام اما برخی معاوضه ها نیز وجود دارد. من PostgreSQL و YugabyteDB را در اندازه‌های نمونه مشابه (4 vCPU 16 Gib RAM در AWS) اجرا کرده‌ام. مشاهده کرده اید که برخی از عملیات ها در PostgreSQL سریعتر هستند. دلیل آن این است که PostgreSQL یکپارچه است و در حافظه مشترک کار می کند. سریع، اما در صورت خرابی یا تعمیر متوقف می شود. خوشه YugabyteDB یک نمونه در هر منطقه دسترسی دارد و اگر یکی از AZ خاموش باشد، برنامه ادامه می‌یابد. این قابلیت دسترسی بالا را فراهم می کند اما مقداری تأخیر اضافه می کند. مهم‌ترین، قبل از مقایسه پیاده‌سازی‌های مختلف، درک نحوه عملکرد آن و داشتن آزادی انتخاب است. با متن باز، PostgreSQL یا سازگار با PostgreSQL بمانید، چندین منبع را از دیدگاه های مختلف بخوانید. و همه چیز را تست کنید.

نوشته های مشابه

دیدگاهتان را بنویسید

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *

دکمه بازگشت به بالا