چرا تعداد اتصالها یک مسئله معماری است؟
یک برنامه وب ممکن است صدها فرایند یا نمونه داشته باشد و هر نمونه چند اتصال به پایگاه داده باز کند. حاصل ضرب این دو عدد گاهی بسیار بزرگتر از تعداد درخواستهایی است که واقعاً همزمان پرسوجو اجرا میکنند. اتصالهای بیکار نیز بدون هزینه نیستند و افزایش نامحدود ظرفیت اتصال راهحل عمومی محسوب نمیشود. PgBouncer میان کلاینت و PostgreSQL قرار میگیرد و اتصالهای سمت سرور را به شکل کنترلشده در اختیار کار قرار میدهد. مسئله اصلی فقط سریعتر بازکردن اتصال نیست؛ باید روشن شود یک کلاینت چه مدت مالک یک اتصال سرور باقی میماند و چه وضعیتهایی به آن اتصال وابستهاند.
در این معماری دو نوع ارتباط داریم: اتصال برنامه به PgBouncer و اتصال PgBouncer به PostgreSQL. تعداد این دو الزاماً برابر نیست. ممکن است تعداد زیادی کلاینت متصل باشند اما تنها بخشی از آنها در یک لحظه اتصال سرور دریافت کنند. این تفاوت امکان مدیریت ازدحام را فراهم میکند، ولی ظرفیت پردازش پایگاه داده را از هیچ نمیسازد. اگر همه درخواستها به طور همزمان کار سنگین داشته باشند، محدودکردن اتصالها تنها محل انتظار را تغییر میدهد. پس ارزیابی باید زمان انتظار، زمان اجرای واقعی و نرخ درخواست را جداگانه ثبت کند تا گلوگاه پنهان نشود.
Session pooling چه قراردادی میسازد؟
در حالت session، اتصال سرور تا پایان نشست کلاینت به آن اختصاص مییابد. این مدل با بسیاری از رفتارهایی که به وضعیت نشست وابستهاند سازگاری بیشتری دارد، زیرا کارهای بعدی همان کلاینت روی همان نشست سرور ادامه پیدا میکنند. در عوض، اگر برنامه اتصال کلاینت را برای مدت طولانی باز و بیکار نگه دارد، اتصال سرور نیز آزاد نمیشود تا به دیگران خدمت دهد. بنابراین وجود PgBouncer در این حالت به خودی خود تضمین نمیکند تعداد اتصالهای مورد نیاز به شدت کاهش یابد. الگوی عمر اتصال در برنامه هنوز عامل تعیینکننده است.
این حالت میتواند برای شروع مهاجرت مفید باشد، اما باید مزیت مورد انتظار دقیق تعریف شود. اگر هدف حفاظت از سرور در برابر موج اتصالهای کوتاه است، رفتار برنامه و زمان نگهداری اتصال باید اندازهگیری شود. اگر هدف اشتراک تعداد محدودی اتصال میان هزاران کلاینت است، مدل نشست ممکن است به محدودیت برسد. مدیر باید بداند کدام قابلیت برنامه واقعاً به نشست وابسته است و کدام وابستگی صرفاً ناشی از انتخاب یک کتابخانه یا تنظیم پیشفرض است. حذف وابستگی غیرضروری گاهی از افزایش اندازه pool مؤثرتر است، ولی چنین تغییری نیازمند آزمون سازگاری برنامه است.
Transaction pooling و مرز پایان تراکنش
در حالت transaction، اتصال سرور پس از پایان تراکنش میتواند به pool بازگردد و برای کلاینت دیگری استفاده شود. از نگاه برنامه، اتصال به PgBouncer همچنان باز است، اما پشت آن ممکن است تراکنش بعدی به نشست دیگری در PostgreSQL برسد. این همان نقطهای است که بسیاری از فرضهای پنهان برنامه آشکار میشوند. تنظیمی که در یک نشست اعمال شده، جدول موقتی که باید میان تراکنشها بماند یا قفلی که به عمر نشست وابسته است، دیگر نباید بدون بررسی به عنوان وضعیت پایدار کلاینت در نظر گرفته شود. مرز تراکنش در این مدل یک قرارداد عملیاتی مهم است.
یک تراکنش باز و بیکار همچنان میتواند اتصال سرور را اشغال کند. بنابراین transaction pooling اثر مخرب تراکنشهایی را که پس از یک پرسوجو باز میمانند از بین نمیبرد. اگر برنامه بین آغاز و پایان تراکنش منتظر شبکه خارجی، پاسخ کاربر یا پردازش طولانی باشد، اتصال درگیر میماند. راهحل تنها افزایش pool نیست؛ باید محدوده تراکنش کوتاه و متناسب با کار پایگاه داده طراحی شود. در عیبیابی، وضعیت idle in transaction و طول عمر تراکنشها باید کنار صف انتظار PgBouncer بررسی شوند. بدون این ارتباط، انتظار ممکن است اشتباهاً به خود پراکسی نسبت داده شود.
قابلیتهای نشست را مورد به مورد بررسی کنیم
عبارت «با transaction pooling سازگار است» نباید فقط بر اساس موفقیت یک پرسوجوی ساده اعلام شود. برنامه باید با مسیرهای واقعی احراز هویت، آغاز تراکنش، خطا، بازگشت تراکنش و استفاده دوباره از اتصال آزمایش شود. متغیرهای نشست، temporary table، فرمانهای مرتبط با notification و قفلهای سطح نشست میتوانند نیازمند توجه باشند. بعضی رفتارها قابل تبدیل به الگوهای محدود به تراکنشاند و بعضی دیگر به اتصال اختصاصی نیاز دارند. فهرست قابلیتهای مستند PgBouncer و نسخه نصبشده باید مبنای این بررسی باشد؛ تجربه یک نسخه قدیمی را نمیتوان بدون بازبینی به همه نسخهها تعمیم داد.
Prepared statement نیز موضوعی است که باید با دقت نسخهای بررسی شود. حمایت از prepared statement در سطح پروتکل و تنظیم مربوط به نگهداری آن با هر شکل استفاده از فرمانهای SQL یکسان نیست. کتابخانههای مختلف رفتار متفاوتی دارند و گاهی پس از چند بار اجرای یک پرسوجو به شکل آمادهشده تغییر حالت میدهند. در نتیجه آزمایشی که تنها یک بار اجرا میشود ممکن است مسئله را نشان ندهد. تست باید از آستانههای خودکار کتابخانه عبور کند، جابهجایی اتصال سرور را تجربه کند و مسیر پاکسازی و خطا را نیز پوشش دهد. مستندات نسخه دقیق، مرجع تصمیم نهایی است.
اندازه pool را با مجموع اتصالها بسنجیم
یک عدد مانند default_pool_size به تنهایی سقف کل اتصالهای سرور را توصیف نمیکند. poolها با ترکیبهای مربوط به پایگاه داده و کاربر شکل میگیرند و تنظیمات ویژه میتوانند رفتار را تغییر دهند. اگر چند برنامه، چند کاربر و چند نام پایگاه داده وجود داشته باشد، مجموع ظرفیت میتواند بیشتر از برداشت اولیه شود. همچنین محدودیت تعداد کلاینتها با محدودیت اتصالهای سمت PostgreSQL یکی نیست. طراحی ظرفیت باید همه poolهای فعال، محدودیتهای مربوط به کاربر و پایگاه داده، و ظرفیت سرور مقصد را در کنار هم محاسبه کند؛ دیدن یک مقدار در فایل برای نتیجهگیری کافی نیست.
بزرگتر کردن pool همیشه توان عملیاتی را بالا نمیبرد. اجرای همزمان بیش از حد میتواند رقابت بر سر پردازنده، حافظه، دیسک و قفلها را افزایش دهد. در چنین حالتی صفی کوتاه در لایه pool ممکن است از ازدحام داخل پایگاه داده قابل مدیریتتر باشد. اما صف طولانی هم تجربه کاربر را خراب میکند. نقطه مناسب باید با آزمون بار و مشاهده زمان پاسخ پیدا شود. reserve pool و زمان انتظار آن نیز باید به عنوان ظرفیت اضطراری با محدودیت روشن بررسی شوند؛ استفاده دائمی از ظرفیت اضطراری نشانهای است که فرضهای عادی ظرفیت یا الگوی درخواست نیازمند بازبینیاند.
Timeoutها هر کدام مرحلهای را محدود میکنند
توقف اتصال، انتظار برای گرفتن سرور و طول اجرای پرسوجو سه مرحله متفاوت هستند. نامهای تنظیمات ممکن است شبیه باشند اما اثر آنها یکسان نیست. برای نمونه query_wait_timeout به زمانی مربوط است که درخواست منتظر اتصال سرور میماند، در حالی که محدودیت اجرای پرسوجو در زمینه دیگری اعمال میشود. پیش از تغییر باید مشخص کنیم کدام مرحله کند است و چه لایهای خطا را تولید کرده است. تنظیم تصادفی همه timeoutها به یک عدد، میتواند درخواستهایی را قطع کند که از دید برنامه هنوز در بازه مجاز هستند و تشخیص علت را دشوار سازد.
هماهنگی با timeout برنامه و پراکسیهای دیگر نیز مهم است. اگر کلاینت زودتر منصرف شود ولی کار پایگاه داده ادامه پیدا کند، منابع ممکن است برای نتیجهای مصرف شوند که دیگر کسی دریافت نمیکند. رفتار لغو درخواست و مدیریت اتصال پس از خطا باید آزمایش شود. از سوی دیگر، مقدار بیش از حد کوچک میتواند در جهش کوتاه بار خطای فراوان تولید کند و سازوکار retry آن را تشدید کند. تکرار خودکار باید محدود و با تأخیر مناسب باشد؛ تکرار یک تراکنش نیز فقط وقتی درست است که برنامه پیامدهای اجرای احتمالی قبلی را مدیریت کند.
احراز هویت و TLS در دو سوی اتصال
PgBouncer یک مرز جدید در مسیر ارتباط ایجاد میکند. هویت برنامه نزد PgBouncer و هویت اتصال PgBouncer نزد PostgreSQL باید جداگانه طراحی شوند. فایل کاربران، پرسوجوی احراز هویت و تنظیمات سمت سرور هر کدام پیامدهایی برای نگهداری راز و اعمال مجوز دارند. این لایه نباید به بهانه سادهسازی، همه برنامهها را بیدلیل با یک نقش پرقدرت به پایگاه داده متصل کند. محدودیتهای SQL همچنان در PostgreSQL اعمال میشوند و نقشها باید با نیاز واقعی برنامه متناسب باشند. حساب مدیریتی خود PgBouncer نیز نباید با حساب عادی برنامه اشتباه گرفته شود.
رمزگذاری مسیر برنامه تا PgBouncer الزاماً مسیر PgBouncer تا پایگاه داده را رمز نمیکند. تنظیم TLS و اعتبارسنجی گواهی در هر دو سمت باید بررسی شود. در شبکه داخلی نیز تصمیم درباره اعتماد به نام میزبان و مرجع گواهی باید صریح باشد. آزمون موفق اتصال با یک گواهی معتبر تنها بخش مثبت آزمایش است؛ باید معلوم شود گواهی نامعتبر یا نام نامنطبق چگونه رد میشود. ثبت تنظیمات بدون ثبت نتیجه اعتبارسنجی، شاهد کافی برای حفاظت مسیر نیست. دسترسی به رابط مدیریتی و فایلهای حاوی اطلاعات احراز هویت نیز باید محدود و قابل ممیزی باشد.
مشاهده صف و برنامه تغییر
دستورهای مدیریتی مانند SHOW POOLS و SHOW STATS برای دیدن وضعیت poolها و رفتار ترافیک کاربرد دارند. شمار کلاینتهای منتظر، اتصالهای فعال و زمانهای اندازهگیریشده باید در کنار متریکهای PostgreSQL خوانده شوند. اگر صف زیاد است ولی پایگاه داده ظرفیت آزاد دارد، محدودیت pool یا توزیع نامتوازن میان poolها ارزش بررسی دارد. اگر سرور همزمان تحت فشار است، افزایش اتصال احتمالاً مسئله را پیچیدهتر میکند. مقایسه پیش و پس از تغییر باید با بار مشابه انجام شود. کاهش لحظهای صف پس از افت ترافیک، موفقیت یک تنظیم جدید را اثبات نمیکند.
برای مهاجرت، ابتدا یک سرویس کمریسک با رفتار شناختهشده انتخاب میشود. حالت pooling، تنظیمات نشست مورد استفاده و مسیر بازگشت ثبت میشوند. سپس آزمایش بار، خطا و قطع اتصال اجرا میشود و تنها پس از مشاهده نتیجه، دامنه استفاده گسترش مییابد. در عملیات روزمره نیز باید تفاوت reload تنظیمات، قطع اتصالهای موجود و بازسازی ارتباطهای سرور روشن باشد. PgBouncer زمانی ارزش واقعی دارد که قرارداد مالکیت اتصال را با رفتار برنامه هماهنگ کنیم؛ یک پراکسی سالم نمیتواند فرض نادرست برنامه درباره ماندگاری نشست یا ظرفیت بینهایت پایگاه داده را اصلاح کند.