تغییرات طرحواره Postgres هنوز یک PITA هستند
ما مهندسان نرمافزار در مورد چیزهای زیادی موافق نیستیم، اما در این مورد توافق داریم: تغییرات طرحواره پایگاهداده یک مشکل است.
بخشی از کار من در Xata این است که تا حد امکان با توسعه دهندگان بیشتری صحبت کنم – از فارغ التحصیلان تازه وارد بوت کمپ گرفته تا توسعه دهندگان مستقل و مهندسان اصلی که در تیم های بزرگ کار می کنند. ما در مورد پایگاه های داده به طور کلی صحبت می کنیم، با چه مسائلی روبرو هستند، ابزارهایی که استفاده می کنند و غیره.
از میان افرادی که با آنها صحبت کردیم، تقریباً همه گفتند که تغییرات طرحواره و مدیریت طرحواره یکی از آنهاست. حداقل مورد علاقه بخش هایی از کار با پایگاه های داده در حالی که این احساس تقریباً جهانی است، دلایلی که آنها مطرح می کنند همیشه یکسان نیستند. برای مثال، شرکتهای کوچک یا توسعهدهندگانی که روی پروژههای سرگرمی کار میکنند، باید با رشد برنامههایشان و کشف نیازهای جدید، تغییرات زیادی ایجاد کنند. آنها دوست دارند گردش کار تغییرات طرحوارهشان به همان اندازه درخواستهای کشش در GitHub ساده و مستقیم باشد.
برای شرکتهای بزرگتر، با دادههای بیشتر و جدولهای پربازدید، ممکن است تغییرات طرحواره کمتر اتفاق بیفتد، اما همچنان باید نگران مواردی مانند خرابی ناشی از قفل شدن باشند. آنها به راهنماهای داخلی طولانی برای انجام صحیح تغییرات طرحواره نیاز دارند (مانند GitLab، PayPal)، ابزارهای سفارشی (مثلاً Meta، Square)، و اغلب حوادث یا رویدادهای نزدیک ناشی از مهاجرت طرحواره را مستند می کنند (مانند GitHub، Doctolib، GoCardless).
در سراسر هیئت مدیره، توسعه دهندگان از تغییرات طرحواره که بر آنها تأثیر می گذارد شکایت دارند سرعت: آنها نیاز به ارتباطات بیشتر، مراحل بیشتر، با نگرانی های سازگاری با عقب دارند. در نتیجه، برخی از توسعهدهندگان هرگز ستونها را تغییر نمیدهند و حذف نمیکنند، بلکه فقط ستونهای جدید را اضافه میکنند. این “شما بدهی” را ایجاد می کند – که باگ ایجاد می کند، اعضای جدید تیم را گیج می کند و کد سازگاری را در طول تاریخ استفاده از آن نگه می دارد.
موارد زیر مختص PostgreSQL هستند، زیرا این سیستم پایگاه داده ای است که ما در Xata استفاده می کنیم، اما برخی از آنها برای سایر سیستم های پایگاه داده نیز اعمال می شوند.
قفل کردن گوچاها
PostgreSQL دارای انواع مختلفی از قفل ها و بسیاری از آنها است ALTER TABLE
عبارات (اما نه همه) جدول را می گیرند ACCESS EXCLUSIVE
قفل، که با سایر انواع قفل در تضاد است. این بدان معنی است که جدول اساساً غیرقابل دسترسی است و مهم است که این قفل را برای حداقل مدت زمان ممکن نگه دارید.
حتی زمانی که قفل برای مدت کوتاهی گرفته میشود، باز هم میتواند باعث شود که میز در دسترس نباشد. در طول عملیات عادی، خواندن و نوشتن روی جداول شما به طور همزمان اجرا می شود. با این حال، زمانی که یک ALTER TABLE
پرس و جو نیاز دارد ACCESS EXCLUSIVE
اجرا می شود، Postgres نیاز به ایجاد فضای باز دارد که در آن هیچ پرس و جو یا تراکنش دیگری در حال اجرا نباشد. برای دستیابی به این هدف، تمام نمایش داده شد پس از صادر شده است ALTER TABLE
در یک صف قرار می گیرند تا بعد از آن اجرا شوند ALTER TABLE
کامل می کند. مشکل اینجاست: اگر جدول شما یک کوئری در حال اجرا یا یک تراکنش در حال اجرا داشته باشد، تغییر طرح شما نمی تواند آغاز شود. در حالی که Postgres منتظر اجرای تغییر طرح شما است، همه پرس و جوهایی که فرض میکردید فورا اجرا میشوند اکنون در صف قرار میگیرند. این به میز شما ظاهری می دهد که در دسترس نیست.
ترفند برای جلوگیری از موارد فوق این است که قبل از اجرای قفل به صراحت قفل را بگیرید ALTER TABLE
و a را تنظیم کنید lock_timeout
برای جلوگیری از صف پرس و جو برای مدت طولانی. اگر تایم اوت رسید، به تلاش مجدد ادامه میدهید تا زمانی که قفل موفق شود، و تنها پس از آن این کار را انجام دهید ALTER TABLE
.
گوچاهای دیگری نیز وجود دارد، مانند استفاده از کلمه کلیدی جادویی CONCURRENTLY
هنگام اضافه کردن شاخص ها، اما در داخل تراکنش ها کار نمی کند. هنگام اضافه کردن یک محدودیت مانند NOT NULL
، Postgres ابتدا باید بررسی کند که هیچ NULL در جدول وجود ندارد و باید این کار را در حالی که قفل را نگه داشته است انجام دهد. شما می توانید با اضافه کردن a دور آن کار کنید CHECK CONSTRAINT
با NOT VALID
، به این معنی که محدودیت برای ردیف های جدید اعمال می شود اما برای ردیف های قدیمی اعمال نمی شود. سپس می توانید بدوید VALIDATE
بعد، که نیازی به آن ندارد ACCESS EXCLUSIVE
قفل شود زیرا فرض می کند ردیف های جدید محدودیت را رعایت می کنند.
به طور خلاصه، PostgreSQL راه های خوبی برای کنترل قفل و جلوگیری از نگه داشتن قفل برای مدت طولانی ارائه می دهد، اما منصفانه است که بگوییم این یک میدان مین است. اشتباه کردن آسان است و هر اشتباهی می تواند گران تمام شود. به همین دلیل، تیم ها باید اسناد داخلی در مورد نحوه انجام صحیح آن داشته باشند و در طول بررسی ها بیشتر مراقب باشند. سیستمهای مرحلهبندی برای گرفتن برخی از گوچاها مفید هستند، تا زمانی که آنها دقیقاً همان دادههای پایگاه داده prod و سطوح مشابهی از ترافیک را داشته باشند، که معمولاً اینطور نیست.
استقرار برنامه و 6 مرحله تغییر نام
بخش قبلی بیشتر برای تیم هایی اعمال می شود که میزهای بزرگی دارند، اما این قسمت برای هر تیمی که به شکستن چیزها اهمیت می دهد، اعمال می شود. همچنین مختص Postgres نیست.
تغییرات طرحواره به صورت مجزا اتفاق نمیافتد، بلکه بخشی از یک ویژگی یا اصلاح جدید است که شامل تغییرات کد برنامه است. اگر میتوانستیم تغییر طرح و کد برنامه را دقیقاً در همان لحظه در همه سرورهای برنامه اجرا کنیم، مشکلی وجود نخواهد داشت. با این حال، در عمل، یک پنجره زمانی وجود دارد که در آن کد قدیمی باید با طرحواره جدید کار کند، یا برعکس.
استراتژی انجام درست کارها به تغییری که می خواهید ایجاد کنید بستگی دارد. به عنوان مثال، اگر یک ستون اضافه کنید: ابتدا تغییر طرحواره را انجام می دهید، داده ها را تکمیل می کنید، سپس کد برنامه را اجرا می کنید. پس از آن، شما محدودیت هایی را اعمال می کنید، که خود می تواند یک فرآیند چند مرحله ای باشد.
اگر می خواهید ستونی را حذف کنید، برعکس است. ابتدا مطمئن شوید که هیچ کدی به آن ستون دسترسی ندارد، سپس آن را از طرح حذف می کنید.
در مورد تغییر نام ها چطور؟ شما باید این مراحل را دنبال کنید (نکات کلاه به اسناد PlanetScale):
- یک ستون جدید با نام جدید ایجاد کنید.
- به روز رسانی و استقرار برنامه برای نوشتن داده ها در هر دو ستون.
- پر کردن داده های گمشده از ستون قدیمی به ستون جدید.
- به صورت اختیاری، محدودیت هایی مانند اضافه کنید
NOT NULL
به ستون جدید پس از پر کردن تمام داده ها. - برنامه را بهروزرسانی کنید تا فقط از ستون جدید استفاده کند و هرگونه ارجاع به نام ستون قدیمی را حذف کنید.
- ستون قدیمی را رها کنید.
به طور خلاصه، برای ایجاد تغییرات در طرحواره به درستی، حتی با نادیده گرفتن مسائل قفل، اغلب به یک فرآیند چند مرحله ای نیاز دارید. این هم سرعت شما را کاهش می دهد و هم سطح بالایی برای اشتباهات گران قیمت دارد.
عقبگرد؟ انتظارات خود را برگردانید
اگر یک چیز ترسناک تر از انجام تغییرات طرحواره وجود دارد، این فکر است که ممکن است مجبور شوید آنها را تحت محدودیت زمانی لغو کنید.
اگر تغییرات طرحواره نیاز به بررسی دقیق و یک فرآیند چند مرحله ای دارد، بازگرداندن آنها ساده تر نیست. معمولاً باید همان مراحل را با دقت به ترتیب معکوس اعمال کنید.
از آنجایی که این کار پیچیده است و زمان زیادی می برد، بیشتر ما تمایل داریم قبل از شروع مهاجرت طرحواره، بازگشت به عقب را آزمایش نکنیم. این باعث می شود آن را مضاعف ترسناک; شما در حال اجرای یک حادثه، با پایگاه داده خود در وضعیت نامعلومی هستید، و اکنون باید مجموعه ای از مراحل آزمایش نشده تولید را طی کنید.
برای توسعه دهندگان منضبط موجود در آنجا که بازگردانی های خود را آزمایش کردند، ممکن است هنوز مدت زیادی منتظر تکمیل بازگشت خود باشید. این میتواند شما را مجبور کند برای چند دقیقه یا ساعتها به یک ترمینال خیره شوید و منتظر بمانید تا برنامهتان برای کاربرانتان آنلاین شود.
یکی از بخشهای مورد علاقه من در مورد کار بر روی Xata این است که میتوانیم این نوع مسائل گردش کار را در نظر بگیریم و آنها را از اصول اولیه بازنگری کنیم.
بیایید تصور کنیم که یک عصای جادویی داریم. سیستم مدیریت طرحواره ایده آل شما چگونه خواهد بود؟ این مال منه:
- وجود دارد گردش کار استاندارد که هر بار بدون توجه به نوع طرحواره دنبال می شود.
- تغییرات طرحواره هستند بدون قفل یا قفل میز را برای حداقل زمان مصرف کنید.
- تغییر طرحواره باعث خرابی نشود با شکستن کد برنامه
- شما می توانید انواع تغییرات را در a اعمال کنید یک قدم، یا حداکثر چند قدم.
- تغییرات طرحواره هستند غیر قابل انجامو واگرد سریع است.
به طور خلاصه، سیستمی که تغییرات طرحواره را ایجاد می کند استاندارد شده، بدون توقف، بدون قفل، یک مرحله ای و غیرقابل انجام.
آیا این ممکن است؟
اولین بینش این است که وقتی صحبت از تغییرات طرحواره می شود، برای ساده نگه داشتن پایگاه داده مسئولیت زیادی بر روی کد برنامه می گذاریم. اگر پیچیدگی را در سمت پایگاه داده جابجا کنیم، یک بار آن را پیاده سازی می کنیم و همه برنامه ها از آن بهره مند می شوند.
به جای اینکه کد برنامه کاربردی را با جلو و عقب سازگار کنیم، شمای پایگاه داده را با عقب سازگار می کنیم. پایگاه داده می تواند هم طرح قدیمی (قبل از تغییر) و هم طرح جدید (پس از تغییر) را به طور همزمان ارائه دهد. شما می توانید تغییر طرح را اعمال کنید، و کد قدیمی و کد جدید می توانند به صورت موازی کار کنند تا زمانی که ارتقاء رولینگ کامل شود.
با قرار دادن آن در یک جدول زمانی، به شکل زیر خواهد بود:
- تغییر طرحواره شروع می شود، برای مثال زمانی که PR ادغام می شود. ممکن است مدتی طول بکشد تا طرح جدید در دسترس باشد، بنابراین استقرار برنامه هنوز شروع نشده است.
- هنگامی که طرح جدید آماده شد، استقرار برنامه شروع می شود. این میتواند یک راهاندازی مجدد باشد، بنابراین نسخه قدیمی و جدید برنامه ممکن است برای مدتی همزیستی داشته باشند. این مشکلی ندارد زیرا طرح قدیمی هنوز در دسترس است.
- پس از تکمیل استقرار برنامه، طرح قدیمی را می توان حذف کرد. با این حال، ممکن است بخواهید آن را برای مدتی بیشتر زنده بگذارید، در صورت نیاز به بازگشت.
اگر در زمانی که طرح قدیمی هنوز در دسترس است، نیاز به بازگشت است، میتوانید با خیال راحت کد برنامه را برگردانید. از نقطه نظر آن، طرح واره هرگز اصلاح نشد.
در طول دوره زمانی که هر دو طرحواره قدیمی و جدید معتبر هستند، سیستم ستونهای پنهان موقتی را نگه میدارد و از نماها برای نشان دادن طرحواره قدیمی و جدید استفاده میکند. درجها و بهروزرسانیهای جدید بهطور خودکار بین دو طرحواره «ارتقا» یا «کاهش» میشوند، بنابراین هر دو نسخه قدیمی و جدید برنامه میتوانند به طور عادی کار کنند.
با موارد فوق، عملیات اصلی ستون (افزودن/حذف/تغییر نام) همگی استاندارد و ایمن می شوند. دیگر نیازی به برنامهریزی تغییرات طرحواره، اجتناب از تغییر نام، یا به تعویق انداختن حذف ستونهای استفاده نشده برای هفتهها «فقط برای اطمینان» نیست.
افزودن محدودیت ها برای پیاده سازی چالش برانگیزتر است، زیرا طرح واره قدیمی و جدید می توانند در تضاد باشند. به عنوان مثال، فرض کنید شما در حال اضافه کردن a هستید NOT NULL
محدودیت در یک ستون اگر جدید INSERT
طرحواره قدیمی با a می آید NULL
ارزش، باید پذیرفته شود، زیرا به طرح واره قدیمی احترام می گذارد. با این حال، ردیف حاصل را نمی توان در طرح جدید در معرض دید قرار داد، زیرا محدودیت را رعایت نمی کند. در این حالت، بهتر است ردیف جدید را در طرح جدید مخفی کنید و تا زمانی که این مشکل حل نشود، از تکمیل انتقال جلوگیری کنید.
از آنچه من می دانم، تنها یک پروژه وجود دارد که چیزی نزدیک به این را امتحان می کند: Reshape نسبتاً اخیر. از نماهای Postgres برای افشای دو نسخه طرحواره استفاده می کند و باعث ارتقا/تنزل داده های جدید می شود. بخش محدودیت ها را همانطور که در بالا توضیح داده شد انجام نمی دهد، اما نشان می دهد که این رویکرد امکان پذیر است. در ترکیب با گردش کار مبتنی بر درخواست کشش Xata، فکر می کنم سیستم ایده آلی که در بالا توضیح داده شد امکان پذیر است!
اگر درد تغییرات طرحواره را احساس می کنید و فکر می کنید که باید راه بهتری وجود داشته باشد، مایلیم با شما صحبت کنیم! ما قصد داریم در قالب یک پروژه متن باز برای PostgreSQL روی این موضوع کار کنیم، و اگر میخواهید هنگام انتشار آن به شما اطلاع داده شود، میتوانید ما را دنبال کنید. توییتر، در Discord به ما بپیوندید یا به سادگی در Xata ثبت نام کنید.