بیدار کردن پایگاهدادهها بدون اتصالهای مرده
اپهای منتشرشده در Mythex و پایگاهدادههایشان وقتی کسی از آنها استفاده نمیکند به خواب میروند. وقتی یک اپ بیدار میشد، میتوانست اولین درخواستش را ۱۰ تا ۱۲ ثانیه نگه دارد، چون سعی میکرد اتصالهای پایگاهدادهای را دوباره استفاده کند که در زمان خواب همهچیز مرده بودند. حالا به اپهای Spring Boot تنظیمات پایگاهدادهای میدهیم که در برابر خواب امناند، و به عامل میگوییم اپهای هر stack را به همین شکل بسازد. در سطح پلتفرم میزبانی، اولین درخواست پس از بیدار شدن از ۱۲٫۷ ثانیه به ۲٫۳ ثانیه رسید.
چرا اپها و پایگاهدادهها میخوابند
یک اپ منتشرشدهی Mythex روی ماشینی اجرا میشود که بعد از چند دقیقه بیکاری معلق میشود، و پایگاهدادهاش یک Postgres سرورلس است که بعد از پنج دقیقه بیکاری میخوابد و با درخواست بعدی بیدار میشود. این همان چیزی است که اپ آرام را ارزان نگه میدارد: برای آنچه پایگاهداده واقعاً استفاده کرده پول میدهید، و اپ خوابیده هیچ هزینهی محاسباتی ندارد. دربارهی جنبهی صورتحساب آن در صورتحساب پایگاهدادهها بر اساس آنچه واقعاً استفاده میکنند نوشتهایم.
مشکل این است که این دو جدا از هم میخوابند و دربارهی اینکه یک اتصال چیست توافق ندارند.
علامت: بازدید اول کند است
اکثر اپهای سروری یک connection pool نگه میدارند: چند اتصال باز پایگاهداده که آماده نگه داشته میشوند، تا هر درخواست هزینهی باز کردن یک اتصال تازه را نپردازد. این طراحی درستی برای اپی است که همیشه روشن است.
با خواب، این اتفاق میافتد:
- اپ در حال اجراست، با چند اتصال بیکار در pool.
- کسی بازدید نمیکند. ماشین اپ معلق میشود — یخزده، با pool دستنخورده.
- پایگاهداده میخوابد و سر دیگر هر اتصال را رها میکند.
- یک بازدیدکننده میرسد. اپ دقیقاً همانطور که بود بیدار میشود، هنوز اتصالهایی را نگه داشته که پایگاهداده دیگر نمیشناسدشان.
- pool یکی از آن اتصالها را میدهد. باید پیش از کنار گذاشتنش بفهمد که مرده است — با امتحان کردن و منتظر ماندن.
در یک اپ Spring Boot، اعتبارسنجی آن اتصالهای مرده اولین درخواست را حدود ۱۰ ثانیه بلوکه میکرد، پیش از اینکه با «Connection is not available» شکست بخورد. برای بازدیدکننده، سایت فقط هنگ میکرد و بعد شاید خطایی نشان میداد.
چرا اپهای Spring بیشتر از همه آسیب دیدند
Spring Boot یک فریمورک محبوب Java است و بهطور پیشفرض از یک connection pool به نام HikariCP استفاده میکند. Spring میتواند آن pool را از طریق تنظیمات پیکربندی کند، از جمله اینکه چند اتصال بیکار نگه دارد.
اما Mythex به هر اپ پایگاهدادهاش را بهصورت یک DATABASE_URL واحد با فرم postgresql://user:pass@host/db میدهد. Spring مستقیم نمیتواند این فرمت را بخواند. پس اپهای Spring که روی Mythex ساخته میشدند، pool خودشان را در کد میساختند و با تجزیهی آن URL این کار را میکردند — و pool ساختهشده با دست، همهی تنظیمات spring.datasource.* Spring را نادیده میگیرد. حتی اگر کسی تنظیمات درست pool را میدانست، جایی نبود که اثر کنند.
رفع، بخش ۱: تنظیماتی که Spring میتواند استفاده کند
وقتی پروژهای پایگاهداده دارد، حالا متغیرهای محیطیای هم میگیرد که Spring بهطور بومی میخواند:
SPRING_DATASOURCE_URL،SPRING_DATASOURCE_USERNAMEوSPRING_DATASOURCE_PASSWORD، که ازDATABASE_URLتبدیل شدهاند (با SSL الزامی، مگر اینکه URL خلاف آن بگوید).- تنظیمات pool که از خواب جان سالم به در میبرند:
| تنظیم | مقدار | چرا |
|---|---|---|
| حداقل اتصال بیکار | ۰ | بین درخواستها هیچ اتصالی باز نگه داشته نمیشود، پس چیزی برای کهنه شدن وجود ندارد |
| زمان بیکاری | ۳۰ ثانیه | اتصالهای بیاستفاده زود بسته شوند |
| حداکثر عمر | ۲ دقیقه | هر اتصال پیش از اینکه پیر شود بازنشسته شود |
| زمان انتظار اتصال | ۵ ثانیه | اگر پایگاهداده کند جواب میدهد، یک درخواست را طولانی نگه ندارد |
وقتی اتصال بیکاری نگه داشته نمیشود، اپ بیدارشده یک اتصال تازه باز میکند، بهجای اینکه از میان اتصالهای مرده عبور کند.
وقتی این متغیرها موجودند، Spring خودش pool را پیکربندی میکند و تنظیمات اعمال میشوند. هر چیزی که خود پروژه تنظیم کرده همیشه برنده است؛ ما فقط آنچه را که جا افتاده پر میکنیم. اپهایی که Spring نیستند این متغیرها را نادیده میگیرند.
جزئیاتی که تقریباً کاری نمیکرد
Spring متغیرهای محیطی را با قاعدهای به تنظیماتش نگاشت میکند که بهراحتی اشتباه میشود. تنظیم minimum-idle باید به شکل SPRING_DATASOURCE_HIKARI_MINIMUMIDLE نوشته شود — خطتیرهها حذف شده و کلمات به هم چسبیده. نوشتنش به شکل SPRING_DATASOURCE_HIKARI_MINIMUM_IDLE، که طبیعیتر به نظر میرسد، به تنظیمی متفاوت و ناموجود (minimum.idle) نگاشت میشود و بیصدا نادیده گرفته میشود.
این از آن اشتباهاتی است که از همهی بازبینیها رد میشود و در production هیچ کاری نمیکند. پس یک تست هم بررسی میکند که مقادیر تنظیم شدهاند و هیچ متغیری از شکل جداشده با زیرخط استفاده نمیکند.
رفع، بخش ۲: هر stack دیگر
Spring تنها فریمورکی نیست که pool دارد، و ما نمیتوانیم هر کتابخانه را در هر زبانی از پیش پیکربندی کنیم. پس عامل Mythex — هوش مصنوعیای که اپ شما را مینویسد — حالا یک دستورالعمل ثابت برای هر stack دارد: اپهای منتشرشده و پایگاهدادههایشان وقتی بیکارند میخوابند، پس اتصالی که باز نگه داشته شده ممکن است در درخواست بعدی مرده باشد. باید:
- هیچ اتصال بیکار پایگاهدادهای را باز نگه ندارد، یا پیش از استفاده هر کدام را بررسی کند،
- یک بار در خطای اتصال دوباره تلاش کند، و
- تنظیمات پایگاهدادهای را که پلتفرم در محیط میدهد استفاده کند، بهجای اینکه خودش یک pool را با تجزیهی
DATABASE_URLبسازد.
نکتهی آخر حلقه را با بخش ۱ میبندد: اپی که روی تنظیمات ارائهشده ساخته شده، پیکربندی امن در برابر خواب را خودکار میگیرد.
نتیجه
روی سکویی که اپهای منتشرشده را روی آن میزبانی میکنیم اندازه گرفتیم: اولین درخواست پس از بیدار شدن از ۱۲٫۷ ثانیه به ۲٫۳ ثانیه رسید.
آنچه از آن برداشتیم
- هر لایهای که میخوابد باید دربارهی آنچه باقی میماند توافق داشته باشد. یک اپ یخزده و یک پایگاهدادهی خوابیده هر کدام انتخاب معقولی کردند. با هم، اپ با وضعیتی بیدار شد که دیگر درست نبود.
- پیشفرضهایی که برای سرورهای همیشهروشن طراحی شدهاند با اپهایی که به صفر میرسند جور نیستند. نگه داشتن اتصالها گرم ایدهی خوبی است، تا وقتی همهچیز اطرافش سرد شود.
- داده را به قالب خود فریمورک بدهید. یک URL برای ما ساده بود، اما هر اپ Spring را مجبور میکرد یک pool را با دست بسازد که تنظیمات خودش را نادیده میگرفت.
- آزمایش کنید که یک تنظیم با املای درستی که فریمورک میخواند نوشته شده باشد. یک مقدار درست با نام اشتباه هیچ کاری نمیکند، بیصدا.
اگر تازه با نحوهی کار پایگاهدادهها در Mythex آشنا میشوید، مستندات پایگاهداده و راهنمای ما دربارهی Postgres چیست را ببینید.