همه‌چیز عالیه؛ API با سرعت جواب میده، Queryها سریع اجرا میشن و هیچ Error خاصی هم توی Log دیده نمیشه. چند ساعت بعد اما همون Endpointهایی که قبلاً در چند صد میلی‌ثانیه جواب میدادن، شروع میکنن به کند شدن، بعضی Requestها Timeout میشن و یه سری خطاهای عجیب مربوط به Connection دیتابیس ظاهر میشه.

یکی از اولین چیزهایی که تو چنین شرایطی باید بررسی کنید، Connection Pool دیتابیسه.

مشکل Connection Pool از اون دسته باگ‌هاییه که معمولاً توی محیط Development خودش رو نشون نمیده؛ چون روی سیستم خودتون شاید همزمان دو یا سه Request داشته باشید، ولی وقتی پروژه میره روی سرور و ده‌ها یا صدها کاربر همزمان درخواست میفرستن، تازه مشخص میشه Connectionها درست مدیریت نمیشن.

Connection Pool اصلاً چیه؟

برای اجرای Query روی دیتابیس، برنامه باید یه Connection داشته باشه. ساختن Connection جدید هم رایگان نیست؛ Handshake انجام میشه، احراز هویت صورت میگیره و مقداری از منابع دیتابیس و سرور مصرف میشه.

برای همین به جای اینکه برای هر Request یه Connection جدید بسازیم و بعد نابودش کنیم، معمولاً تعدادی Connection از قبل ساخته میشن و داخل یه Pool قرار میگیرن.

مثلاً Pool شما ۱۰ Connection داره. وقتی یه Request میاد، یکی از Connectionها رو میگیره، Query رو اجرا میکنه و بعد Connection دوباره برمیگرده داخل Pool تا Request بعدی ازش استفاده کنه.

این مدل هم سریع‌تره و هم فشار کمتری به دیتابیس وارد میکنه.

اشتباه اول: ساختن Connection برای هر Request

یکی از بدترین طراحی‌ها اینه که داخل هر Route یه Connection جدید بسازیم.

مثلاً فرض کنید چنین الگویی داشته باشیم:

app.get('/users', async (req, res) => {
  const connection = await mysql.createConnection({
    host: 'localhost',
    user: 'root',
    password: 'password',
    database: 'app'
  });

  const [users] = await connection.query('SELECT * FROM users');

  await connection.end();

  res.json(users);
});

این کد شاید با چند Request کاملاً خوب کار کنه، ولی اگه صدها کاربر همزمان وارد سایت بشن، برنامه مرتب Connection جدید میسازه و میبنده. نتیجه میتونه افزایش Latency، فشار روی دیتابیس و در نهایت رسیدن به محدودیت تعداد Connectionها باشه.

راه بهتر اینه که Pool رو یک بار هنگام بالا اومدن برنامه بسازیم و بعد تمام Routeها از همون Pool استفاده کنن.

Connection Leak؛ قاتل بی‌سروصدا

مشکل خطرناک‌تر زمانی اتفاق میفته که Connection رو از Pool میگیریم ولی فراموش میکنیم برش گردونیم.

فرض کنید Pool فقط ۱۰ Connection داره. Request اول یه Connection میگیره و آزادش نمیکنه، Request دوم هم همین کار رو میکنه و این روند ادامه پیدا میکنه. بعد از ۱۰ Request، دیگه هیچ Connection آزادی باقی نمونده.

Request یازدهم چی میشه؟می‌ایسته.Request دوازدهم هم می‌ایسته.و کم‌کم صفی از Requestها درست میشه که منتظر آزاد شدن Connection هستن.برای همین ممکنه API اول روز فوق‌العاده سریع باشه، ولی بعد از مدتی به شکل عجیبی کند بشه.

نسخه درست‌تر با Pool

در اکثر کتابخونه‌های دیتابیس میشه Pool رو یک بار ساخت و دوباره استفاده کرد. مثلاً با mysql2:

import mysql from 'mysql2/promise';

const pool = mysql.createPool({
  host: 'localhost',
  user: 'root',
  password: 'password',
  database: 'app',
  connectionLimit: 10
});

app.get('/users', async (req, res) => {
  try {
    const [users] = await pool.query('SELECT * FROM users');
    res.json(users);
  } catch (error) {
    res.status(500).json({ error: 'Database error' });
  }
});

اینجا Pool یه بار ساخته شده و برای هر Request Connection جدید از صفر ایجاد نمیشه. خود Pool Connectionهای موجود رو مدیریت میکنه و بعد از پایان Query دوباره برای Requestهای بعدی آماده میشن.

این تفاوت ظاهراً کوچیک توی پروژه‌های پرترافیک میتونه تأثیر خیلی بزرگی روی Performance داشته باشه.

Pool بزرگ‌تر همیشه بهتر نیست

یه اشتباه رایج دیگه اینه که وقتی Pool پر میشه، سریع connectionLimit رو از ۱۰ ببریم روی ۱۰۰ یا حتی ۵۰۰ و فکر کنیم مشکل حل شده.

ولی Pool بزرگ‌تر همیشه به معنی Performance بهتر نیست.

دیتابیس هم منابع محدودی داره. هر Connection مقداری RAM و CPU مصرف میکنه و اگر تعداد Connectionها بیش از حد زیاد بشه، ممکنه خود دیتابیس کندتر بشه.

مثلاً اگه ۵ Instance از Node.js داشته باشید و روی هرکدوم Pool با ۱۰۰ Connection تعریف کنید، در بدترین حالت ممکنه ۵۰۰ Connection به دیتابیس باز بشه.

پس اندازه Pool باید با ظرفیت واقعی دیتابیس، تعداد Instanceهای برنامه و حجم Queryها هماهنگ باشه.

Query کند هم Pool رو قفل میکنه

گاهی مشکل اصلاً Leak نیست.

فرض کنید Pool شما ۲۰ Connection داره ولی یه Query بد نوشته شده ۱۵ ثانیه طول میکشه. اگر ۲۰ Request همزمان اون Query رو اجرا کنن، هر ۲۰ Connection برای ۱۵ ثانیه اشغال میشن.

در اون مدت Request بعدی باید منتظر بمونه.

اینجاست که خیلی‌ها فکر میکنن Pool کوچیکه، در حالی که مشکل واقعی یه Query کند، Index نامناسب یا Lock داخل دیتابیسه.

پس قبل از بالا بردن اندازه Pool، حتماً Queryهای کند رو هم بررسی کنید.

چه نشونه‌هایی میگن Pool مشکل داره؟

یکی از واضح‌ترین نشونه‌ها اینه که API در شروع کار سریع باشه ولی با افزایش ترافیک کند بشه. همچنین Errorهایی مثل Too many connections، Connection timeout یا Requestهایی که مدت زیادی منتظر دیتابیس میمونن میتونن به Pool مرتبط باشن.

اگه CPU سرور و دیتابیس خیلی بالا نیست ولی Requestها همچنان منتظر میمونن، بررسی تعداد Connectionهای فعال و صف Pool میتونه سرنخ خوبی بده.

همچنین اگه بعد از Restart کردن برنامه ناگهان همه‌چیز دوباره سریع میشه و چند ساعت بعد مشکل برمیگرده، احتمال Connection Leak ارزش بررسی جدی داره.

هر Connection رو واقعاً لازم نیست دستی بگیری

در خیلی از کتابخونه‌ها اگه فقط یه Query ساده دارید، بهتره مستقیماً از pool.query() استفاده کنید تا خود Pool مدیریت گرفتن و پس دادن Connection رو انجام بده.

گرفتن دستی Connection بیشتر زمانی لازمه که چند Query باید روی یه Connection خاص اجرا بشن، مثلاً داخل Transaction.

در اون حالت هم باید مطمئن باشید Connection حتی در صورت Error دوباره آزاد میشه؛ معمولاً با try/finally.

جمع بندی

Connection Pool یه جزئیات کوچیک از معماری بک‌اند نیست؛ مستقیماً روی سرعت و پایداری API تأثیر میذاره. ساختن Connection جدید برای هر Request، آزاد نکردن Connectionها، Queryهای کند و تنظیم Pool بیش از حد بزرگ همگی میتونن باعث بشن پروژه‌ای که اولش خیلی سریع کار میکنه بعد از مدتی تبدیل به یه سیستم پر از Timeout بشه.

قبل از اینکه برای حل کندی API سراغ ارتقای سرور برید، تعداد Connectionهای دیتابیس، وضعیت Pool و Queryهای طولانی رو بررسی کنید. خیلی وقت‌ها مشکل کمبود CPU یا RAM نیست؛ فقط همه Connectionها یه گوشه گیر افتادن و بقیه Requestها دارن پشت در منتظر میمونن.