خطای Too many connections در MySQL زمانی ظاهر می‌شود که تعداد اتصال‌های همزمان به سرور از سقف تعیین‌شده در متغیر max_connections عبور کند و سرور، به‌جای پذیرش اتصال جدید، آن را با پیام معروف 1040 رد کند. این خطا برخلاف بسیاری از خطاهای دیتابیس، همیشه نشانهٔ بار زیاد نیست؛ گاهی نشانهٔ نشتی اتصال در کد است و گاهی هم نتیجهٔ تنظیم نادرست متغیرها روی سروری که منابع کافی دارد. در سال‌هایی که روی فروشگاه‌های پربازدید وردپرسی و سیستم‌های تراکنشی کار کرده‌ام، این خطا معمولاً در ساعات اوج فروش سر می‌زند و اگر سریع مدیریت نشود، می‌تواند تجربهٔ کاربر را به‌کل مختل کند.

پیام دقیق این خطا در MySQL و MariaDB به این شکل است:

ERROR 1040 (HY000): Too many connections

نکتهٔ ظریف اینجاست که وقتی این خطا ظاهر می‌شود، حتی ریشهٔ سرور هم گاهی نمی‌تواند اتصال بگیرد؛ چون MySQL رزرو اتصال برای کاربران با دسترسی SUPER را در نظر می‌گیرد و از این طریق، اجازهٔ ورود اضطراری مدیر را فراهم می‌کند. همین جزئیات کوچک، در پروژه‌های بحرانی تفاوت میان چند دقیقه downtime و چند ساعت را می‌سازد. مفهوم connection pool که در ویکی‌پدیای Connection pool توضیح داده شده، دقیقاً همان لایه‌ای است که در سمت اپلیکیشن، فشار اتصال‌ها را کنترل می‌کند و در این مقاله، نقطهٔ اصلی تحلیل ما خواهد بود.

خطای Too many connections دقیقاً چه می‌گوید؟

پیام کد 1040 در MySQL و MariaDB به‌طور دقیق می‌گوید که سرور در حال حاضر نمی‌تواند اتصال جدیدی را بپذیرد، چون تعداد اتصال‌های فعال به سقف مجاز رسیده است. این سقف، توسط متغیر سیستمی max_connections تعیین می‌شود که مقدار پیش‌فرض آن معمولاً ۱۵۱ است. تفاوت این خطا با خطاهای مشابه مثل Lock wait timeout در این است که اینجا مشکل در سطح پذیرش اتصال است، نه در سطح اجرای کوئری. یعنی حتی اگر کوئری شما کاملاً بهینه باشد، اگر سرور نتواند اتصال را بپذیرد، کوئری هم اجرا نمی‌شود.

سه نکتهٔ مهم که در تجربه‌ام کمتر به آن‌ها دقت می‌شود. اول، این خطا در سطح سرور رخ می‌دهد نه در سطح دیتابیس؛ پس تغییر دیتابیس کاری را حل نمی‌کند. دوم، MySQL به‌طور پیش‌فرض یک اتصال اضافی برای کاربران SUPER رزرو می‌کند که از این طریق، مدیر می‌تواند در شرایط بحرانی وارد شود و اتصال‌های بی‌مصرف را kill کند. سوم، این خطا فقط در لحظهٔ اوج بار ظاهر نمی‌شود؛ گاهی نشانهٔ نشتی اتصال در کد است، یعنی اتصال‌هایی که باز می‌مانند و هرگز بسته نمی‌شوند، حتی در ساعات کم‌ترافیک می‌توانند این خطا را فعال نگه دارند.

در دیتابیس، اتصال‌ها گران‌تر از کوئری‌ها هستند؛ بستن یک اتصال رهاشده، در برخی موارد بیشتر از بهینه‌سازی ده کوئری به کار می‌آید.

این خطا در محیط‌های production وقتی ظاهر می‌شود که بار همزمان بالا باشد و تعداد کاربران فعال از سقف مجاز سرور عبور کند. در سایت‌های فروشگاهی، دقیقاً لحظه‌ای که یک کمپین تبلیغاتی یا یک خبر مهم باعث جهش ترافیک می‌شود، احتمال بروز این خطا وجود دارد. اگر با مفهوم پایهٔ ارتباط دیتابیس و اپلیکیشن آشنا نیستید، راهنمای انتخاب هاست مناسب نقطهٔ شروع خوبی برای درک زیربنای این مباحث است، چون انتخاب درست هاست با منابع مناسب، خودش نیمی از این مشکل را حل می‌کند.

چرا MySQL سقف اتصال تعیین می‌کند؟

هر اتصال MySQL یک فرآیند یا ترد اختصاصی است که نیازمند منابع مشخصی از جمله حافظه، فایل هندل و پردازنده است. اگر MySQL اجازه می‌داد اتصال‌ها بی‌نهایت باز شوند، سرور به‌سرعت با کمبود حافظه مواجه می‌شد و به‌جای یک خطای کنترل‌شده، با سقوط کامل (crash) مواجه می‌شدیم. سقف max_connections در واقع یک مکانیزم دفاعی است که سرور را در برابر سرریز بار محافظت می‌کند.

سه مکانیزم کلیدی در MySQL وجود دارد که هر کدام در بروز این خطا نقش دارند:

  • سقف max_connections: تعیین‌کنندهٔ حداکثر تعداد اتصال‌های همزمان.
  • نشتی اتصال در اپلیکیشن: اتصال‌هایی که باز می‌مانند و در زمان اوج سرور را اشغال می‌کنند.
  • تراکنش‌های طولانی: هر اتصال با تراکنش طولانی، به‌طور نامتناسب منابع بیشتری مصرف می‌کند.

نکتهٔ مهم اینکه افزایش max_connections به‌تنهایی راه‌حل نیست. اگر سرور شما با ۳۲ گیگابایت رم کار می‌کند، هر اتصال به‌طور متوسط چند مگابایت مصرف می‌کند ولی در کوئری‌های سنگین مثل مرتب‌سازی، مصرف هر اتصال می‌تواند به ده‌ها مگابایت برسد. پس اگر max_connections را بدون توجه به حافظهٔ سرور بالا ببرید، به‌جای حل مشکل، سرور را به سمت از دست دادن حافظه سوق می‌دهید. مبنای این تحلیل، همان چیزی است که در تأثیر دیتابیس بر سرعت سایت هم به آن پرداخته‌ام و اصول کلی را در آنجا باز کرده‌ام.

عامل دوم، نشتی اتصال است که در پروژه‌های واقعی شایع‌تر از آن است که تصور می‌شود. مثال ساده در PHP:

$conn = new mysqli($host, $user, $pass, $db);
$result = $conn->query("SELECT * FROM products");
// فراموش می‌شود: $conn->close();

در این کد، اتصال باز می‌ماند و در حلقه‌ای که این تابع چند بار فراخوانی می‌شود، تعداد اتصال‌ها به‌سرعت بالا می‌رود. همین الگو، در بعضی افزونه‌های وردپرسی هم دیده می‌شود؛ به‌خصوص در افزونه‌هایی که مستقیم با mysqli یا PDO کار می‌کنند و نه با $wpdb. در Python هم به‌طور مشابه، اگر با mysql-connector کار می‌کنید و در بلوک finally اتصال را نمی‌بندید، به همین مشکل دچار می‌شوید. مدیریت اتصال در اپلیکیشن، بخشی از مدیریت تراکنش است که اصول آن را در راهنمای تراکنش‌ها در MySQL توضیح داده‌ام.

پرتکرارترین سناریوها در پروژه‌های واقعی

در تجربه‌ام، خطای Too many connections در پنج سناریوی مشخص رخ می‌دهد. شناخت این سناریوها، مسیر تشخیص را از چند ساعت به چند دقیقه کاهش می‌دهد.

سناریو اول: جهش ترافیک بدون آماده‌سازی

وقتی یک کمپین تبلیغاتی، یک پست وایرال یا یک اعلان عمومی باعث جهش ناگهانی ترافیک می‌شود، تعداد اتصال‌های همزمان به سرعت به سقف می‌رسد. این سناریو در فروشگاه‌های آنلاین که کمپین حراج دارند، بیشترین شیوع را دارد. راه‌حل بلندمدت، افزایش ظرفیت سرور یا استفاده از connection pool با ظرفیت کنترل‌شده است. راه‌حل کوتاه‌مدت، بستن اتصال‌های بی‌مصرف با دستور KILL و بررسی سریع processlist است.

سناریو دوم: نشتی اتصال در کد اپلیکیشن

اگر یک اسکریپت یا افزونه، اتصال‌ها را در پایان به‌درستی نمی‌بندد، حتی در ترافیک کم هم به‌تدریج اتصال‌ها جمع می‌شوند و در نهایت سرور به سقف می‌رسد. تشخیص این سناریو با پایش processlist به‌مرور زمان انجام می‌شود؛ اگر تعداد اتصال‌های غیرفعال (Sleep) به‌طور پیوسته بالا می‌رود، احتمالاً نشتی دارید. برای بررسی، کوئری زیر کمک‌کننده است:

SELECT ID, USER, HOST, DB, COMMAND, TIME, STATE, INFO
FROM information_schema.PROCESSLIST
WHERE COMMAND = 'Sleep' AND TIME > 300
ORDER BY TIME DESC;

این کوئری، اتصال‌هایی را نشان می‌دهد که بیش از پنج دقیقه در حالت Sleep مانده‌اند. اگر تعدادشان زیاد است، یا اپلیکیشن اتصال‌ها را نمی‌بندد یا از connection pool با اندازهٔ بزرگ استفاده می‌کند.

سناریو سوم: تنظیم نامناسب max_connections

گاهی مشکل این است که max_connections روی مقدار پیش‌فرض ۱۵۱ مانده در حالی که سایت پربازدید است. این حالت در پروژه‌هایی که به‌تازگی از هاست اشتراکی به VPS مهاجرت کرده‌اند، بسیار شایع است؛ چون فایل تنظیمات از نصب پیش‌فرض گرفته شده و کسی مقدار را تغییر نداده. راه‌حل، محاسبهٔ دقیق ظرفیت بر اساس منابع سرور و بار واقعی سایت است.

سناریو چهارم: اسکریپت‌های پایش و cron jobs

ابزارهای پایش، اسکریپت‌های بکاپ و cron jobهایی که به دیتابیس وصل می‌شوند، هر کدام یک یا چند اتصال مصرف می‌کنند. اگر این‌ها همزمان با ترافیک اوج اجرا شوند، می‌توانند در مجموع به سقف کمک کنند. در یکی از پروژه‌های واقعی، خطای Too many connections دقیقاً ساعت ۳ بامداد رخ می‌داد و بعد از بررسی مشخص شد که سه اسکریپت پایش همزمان اجرا می‌شده‌اند. تنظیم زمان‌بندی، مشکل را حل کرد.

سناریو پنجم: connection pool با اندازهٔ نامناسب

اگر اپلیکیشن شما از connection pool استفاده می‌کند و اندازهٔ pool بیش از ظرفیت سرور است، در ساعات اوج همهٔ اتصال‌های pool به‌طور همزمان فعال می‌شوند و سقف پر می‌شود. توجه به این نکته که اندازهٔ pool باید بر اساس ظرفیت سرور و نه بار مورد انتظار تنظیم شود، در پروژه‌های بزرگ اهمیت بالایی دارد. توصیه‌های دقیق این بخش، در راهنمای طراحی دیتابیس در MySQL هم آمده است.

سناریونشانهراه‌حل سریع
جهش ترافیکافزایش ناگهانی Threads_connectedافزایش ظرفیت یا pool کنترل‌شده
نشتی اتصالرشد پیوستهٔ Sleep در processlistبستن اتصال در finally block
max_connections پیش‌فرضعدد ثابت ۱۵۱ یا نزدیک به آنمحاسبهٔ ظرفیت واقعی سرور
cron و پایشاوج در ساعات خاصتنظیم زمان‌بندی جداگانه
در MySQL، مانند هر سیستم دیگر، اتصال‌های رهاشده مانند شیر آب بازی هستند که قطره‌قطره مخزن را خالی می‌کنند؛ تفاوت این است که در MySQL، خالی شدن مخزن سریع‌تر است.

Connection pool و نقشش در این خطا

Connection pool یک لایهٔ میانی در سمت اپلیکیشن است که اتصال‌ها را از پیش باز می‌کند و در اختیار درخواست‌های جدید قرار می‌دهد. هدف اصلی آن، کاهش هزینهٔ ساخت اتصال جدید است؛ چون هر اتصال جدید به MySQL نیازمند handshake، احراز هویت و تخصیص حافظه است و این هزینه در سایت‌های پربازدید محسوس است. ولی این لایه، شمشیری دو لبه است: اگر اندازهٔ آن بزرگ باشد، سرور را با اتصال‌های اشغال‌شده تحت فشار می‌گذارد؛ اگر کوچک باشد، درخواست‌ها در صف می‌مانند و کندی سایت را به دنبال دارند.

در PHP، به‌طور سنتی MySQL پشتیبانی از connection pool نداشته است ولی در نسخه‌های جدید با mysqlnd و با استفاده از لایه‌هایی مثل ProxySQL، امکان pool فراهم شده است. در پایتون، کتابخانه‌هایی مثل SQLAlchemy به‌طور پیش‌فرض از pool استفاده می‌کنند و اندازهٔ pool قابل تنظیم است. تنظیم درست این اندازه در هر دو حالت، کلید جلوگیری از خطای Too many connections است.

سه اصل کلی برای تنظیم اندازهٔ pool وجود دارد. اول، اندازهٔ pool باید کمتر از max_connections سرور باشد، با حاشیهٔ امنیت برای اتصال‌های اداری و پایش. دوم، در شرایطی که ترافیک نوسان دارد، می‌توانید از pool داینامیک استفاده کنید که اندازهٔ آن با بار تطبیق پیدا می‌کند. سوم، در هر صورت، امکان بستن اتصال‌های idle را فعال کنید؛ در PDO گزینهٔ PDO::ATTR_TIMEOUT و در کتابخانه‌های مشابه، گزینه‌های مشابهی وجود دارد. اگر پروژه شما از Kubernetes یا Docker استفاده می‌کند، مدیریت pool در سطح سرویس هم اهمیت دارد؛ راهنمای مقایسه هاست لینوکس و ویندوز تفاوت‌های بنیادی محیط‌های اجرا را توضیح می‌دهد.

نکتهٔ ظریف اینکه در MySQL 8، مفهوم thread pool به‌طور اختصاصی بهینه شده است. در نسخه‌های Enterprise و Percona، Thread Pool Plugin می‌تواند تعداد threadهای فعال را کنترل کند و از این طریق، مقیاس‌پذیری سرور را بالا ببرد. اگر پروژه‌تان به این سطح از پیچیدگی رسیده، بررسی این گزینه ارزش وقت را دارد.

روش گام‌به‌گام دیباگ یک سرور درگیر

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

گام اول: وضعیت لحظه‌ای اتصال‌ها

ابزار اصلی این مرحله، دستور زیر است:

SHOW STATUS LIKE 'Threads_connected';
SHOW STATUS LIKE 'Threads_running';
SHOW STATUS LIKE 'Max_used_connections';
SHOW VARIABLES LIKE 'max_connections';

اگر Threads_connected نزدیک max_connections بود، سرور در مرز ظرفیت است. اگر Max_used_connections از مقدار max_connections کمتر بود، سرور در لحظهٔ کنونی فشار ندارد ولی در بازه‌های اخیر در اوج بوده. این دو عدد، مسیر دیباگ را روشن می‌کنند.

گام دوم: بررسی processlist

با کوئری زیر، اتصال‌های فعال را با جزئیات ببینید:

SELECT ID, USER, HOST, DB, COMMAND, TIME, STATE, INFO
FROM information_schema.PROCESSLIST
ORDER BY TIME DESC
LIMIT 30;

اگر تعداد زیادی از اتصال‌ها در وضعیت Sleep با زمان طولانی هستند، مشکل نشتی اتصال است. اگر در وضعیت Query یا Sending data هستند، مشکل کوئری سنگین است. تفکیک این دو حالت، در انتخاب راه‌حل اهمیت دارد.

گام سوم: بستن اتصال‌های بی‌مصرف

اگر سرور در وضعیت بحرانی است، با کاربر SUPER وارد شوید و اتصال‌های یتیم را kill کنید:

KILL ;

قبل از kill، حتماً مطمئن شوید اتصال، بخشی از یک تراکنش مالی نیست. راهنمای رفع خطای Lock wait timeout در MySQL توضیحات بیشتری درباره رفتار تراکنش‌های فعال دارد و می‌تواند در تصمیم‌گیری کمک کند.

گام چهارم: بررسی لاگ خطا و لاگ سرور

سرور MySQL هر خطای Too many connections را در error log ثبت می‌کند. با بررسی این لاگ، می‌توانید الگوهای زمانی را ببینید. مثلاً اگر خطا در ساعات خاصی متمرکز است، احتمالاً cron job یا کمپین تبلیغاتی مقصر است. راهنمای بررسی لاگ‌های دیتابیس نکات دقیق‌تری برای فیلترکردن این لاگ‌ها دارد.

گام پنجم: افزایش موقت max_connections

اگر نیاز به تنفس فوری دارید، می‌توانید max_connections را به‌طور موقت افزایش دهید:

SET GLOBAL max_connections = 500;

این تغییر تا ری‌استارت بعدی باقی می‌ماند. برای دائمی کردن، مقدار را در my.cnf یا my.ini بگذارید. افزایش موقت، فقط زمان می‌خرد؛ مشکل ریشه‌ای را حل نمی‌کند و اگر منابع سرور محدود باشد، ممکن است به مشکل بدتری منجر شود.

گام ششم: تحلیل با performance_schema

در MySQL 8، جدول‌های performance_schema اطلاعات دقیق‌تری درباره اتصال‌ها فراهم می‌کنند:

SELECT * FROM performance_schema.threads;
SELECT * FROM performance_schema.events_statements_summary_by_thread_by_event_name;

این کوئری‌ها نشان می‌دهند که هر اتصال، چه کوئری‌هایی را اجرا کرده و چه زمان‌هایی را صرف کرده است. اگر اتصالی با کوئری‌های بی‌مصرف، زمان زیادی از سرور می‌گیرد، مقصر پیدا می‌شود.

در وردپرس و ووکامرس چطور ظاهر می‌شود؟

وردپرس به‌طور پیش‌فرض از $wpdb استفاده می‌کند که در هر درخواست، یک اتصال به دیتابیس باز می‌کند و در پایان درخواست، آن را می‌بندد. این مدل کار می‌کند چون هر درخواست به‌طور مستقل اجرا می‌شود. ولی در برخی شرایط، این مدل شکست می‌خورد و خطای Too many connections ظاهر می‌شود.

سه موقعیت رایج در وردپرس که این خطا را می‌بینید. اول، هنگام اجرای cron jobهای سنگین که همزمان با ترافیک کاربران اجرا می‌شوند. cron در وردپرس، به‌طور پیش‌فرض از همان فرآیند PHP که به سایت پاسخ می‌دهد استفاده می‌کند و اگر تعداد زیادی cron job همزمان اجرا شوند، هر کدام یک اتصال مصرف می‌کنند. راه‌حل، استفاده از cron سیستمی به‌جای wp-cron است که در عیب‌یابی cron در وردپرس توضیح داده‌ام. دوم، هنگام اجرای پلاگین‌هایی که از connection pool سفارشی استفاده می‌کنند و اندازهٔ آن را درست تنظیم نکرده‌اند. سوم، هنگام بارگذاری بالای فروشگاه ووکامرس که به‌طور همزمان چندین تراکنش سفارش و پرداخت را اجرا می‌کند.

برای کاهش خطای Too many connections در وردپرس، سه توصیهٔ عملی دارم. اول، تعداد افزونه‌های فعال را کم کنید؛ هر افزونه‌ای که در پشت صحنه کوئری می‌زند، ممکن است اتصال مستقیم یا غیرمستقیم مصرف کند. دوم، از کش صفحه و کش آبجکت استفاده کنید تا بار روی دیتابیس کاهش یابد؛ راهنمای بهینه‌سازی کوئری‌های وردپرس با کد نکات عملی این بخش را دارد. سوم، در فروشگاه‌های ووکامرس بزرگ، جداول سفارش و محصولات را به‌طور دوره‌ای بهینه کنید؛ بهینه‌سازی جداول، نه‌فقط کارایی کوئری‌ها را بالا می‌برد، بلکه حجم منابع مصرفی هر کوئری را کاهش می‌دهد. راهنمای بهینه‌سازی دیتابیس ووکامرس این موضوع را با جزئیات بیشتری باز کرده است.

در محیط‌هایی که از MySQL بیرونی استفاده می‌کنند، مثل معماری headless یا Kubernetes، مدیریت اتصال‌ها از خود وردپرس جدا می‌شود و باید در سطح سرویس دیتابیس مدیریت شود. اگر با این معماری‌ها آشنا نیستید، راهنمای CI/CD برای پروژه‌های وردپرس بخشی از این مباحث را پوشش می‌دهد.

در وردپرس، هر افزونه‌ای که غیرفعال می‌کنید، در واقع به‌طور بالقوه یک اتصال کمتر به دیتابیس آزاد می‌کنید؛ نه به این معنا که آن افزونه مقصر است، بلکه به این معنا که هر منبع مصرفی، در نهایت به اتصال‌ها متصل است.

پیشگیری: عادت‌هایی که این خطا را کاهش می‌دهند

پیشگیری از Too many connections، بیش از هر چیز به عادت‌های مدیریت منابع در سرور و اپلیکیشن برمی‌گردد. شش عادت زیر، در پروژه‌های واقعی بیشترین اثر را داشته‌اند.

عادت اول: محاسبهٔ دقیق max_connections

قبل از تعیین مقدار max_connections، منابع سرور را دقیق بشمارید. برای هر اتصال، به‌طور محافظه‌کارانه یک مگابایت در نظر بگیرید و برای کوئری‌های سنگین که نیازمند sort یا join بزرگ هستند، مقدار بیشتری بگذارید. اگر سرور شما هشت گیگابایت رم دارد و می‌خواهید دو گیگابایت برای سیستم‌عامل و سایر سرویس‌ها نگه دارید، حداکثر اتصال منطقی حدود ۲۰۰ تا ۳۰۰ است. الگوی مشابهی در راهنمای بهینه‌سازی کوئری‌های MySQL هم برای تنظیم buffer pool توصیه کرده‌ام.

عادت دوم: استفاده از connection pool با اندازهٔ مناسب

اگر اپلیکیشن شما از connection pool استفاده می‌کند، اندازهٔ آن را کمی کمتر از نصف max_connections بگذارید تا فضای تنفس برای اتصال‌های اداری، پایش و بکاپ باقی بماند. در PHP با PDO، اندازهٔ pool را می‌توان با تنظیم PDO::ATTR_PERSISTENT و سایر گزینه‌ها کنترل کرد. در پایتون با SQLAlchemy، پارامتر pool_size تعیین‌کننده است.

عادت سوم: بستن اتصال در بلوک finally

هرگز اتصال را بدون بستن رها نکنید. در PHP، حتی با mysqli و PDO که در پایان اسکریپت خودکار بسته می‌شوند، در اسکریپت‌های طولانی یا daemon، باید به‌صورت دستی بسته شود. در پایتون، بلوک try/finally را همیشه استفاده کنید. الگوی مشابه در راهنمای نوشتن کد PHP امن برای وردپرس هم توصیه شده است.

عادت چهارم: پایش دوره‌ای وضعیت اتصال

یک اسکریپت ساده که هر پنج دقیقه وضعیت Threads_connected و Max_used_connections را ثبت کند، الگوهای رشد را نشان می‌دهد. اگر روند رشد صعودی است، پیش از بروز بحران می‌توانید ریشه‌یابی کنید. بکاپ منظم دیتابیس، مکمل این پایش است و روش‌های آن در راهنمای پشتیبان‌گیری از MySQL توضیح داده شده است.

عادت پنجم: جداسازی بار خواندن و نوشتن

در پروژه‌های پربار، می‌توانید از replication استفاده کنید و کوئری‌های خواندن را به replica منتقل کنید. این کار فشار اتصال روی سرور اصلی را به‌شدت کاهش می‌دهد. نکتهٔ مهم اینکه replicaها خودشان سقف اتصال دارند و باید به‌طور متناسب تنظیم شوند.

عادت ششم: استفاده از caching در سطح اپلیکیشن

کش در سطح اپلیکیشن (مثل Redis یا Memcached) باعث می‌شود که بسیاری از درخواست‌ها بدون رسیدن به دیتابیس پاسخ بگیرند. این کار، مصرف اتصال را به‌شدت کاهش می‌دهد. برای وردپرس، راهنمای بهینه‌سازی کوئری‌های وردپرس الگوهای caching را باز کرده است.

پرسش‌های پرتکرار درباره Too many connections

این بخش، پاسخ کوتاه به پرسش‌هایی است که در جلسه‌های پشتیبانی و در دیدگاه‌های همین سایت زیاد تکرار می‌شوند.

آیا افزایش max_connections همیشه راه‌حل است؟

خیر. افزایش این مقدار، خطا را به تأخیر می‌اندازد ولی اگر منابع سرور محدود باشد، به‌جای حل مشکل، سرور به‌سمت کمبود حافظه و کندی سوق پیدا می‌کند. راه‌حل درست، ترکیبی از افزایش هوشمندانه max_connections و کاهش مصرف اتصال در لایهٔ اپلیکیشن است.

تفاوت این خطا با Lock wait timeout چیست؟

در Too many connections، مشکل در سطح پذیرش اتصال است؛ سرور اصلاً اتصال جدید را نمی‌پذیرد. در Lock wait timeout، اتصال پذیرفته شده ولی تراکنش نمی‌تواند قفل موردنیازش را بگیرد. مسیر دیباگ این دو متفاوت است و باید تفکیک شوند. توضیح بیشتر در راهنمای رفع خطای Lock wait timeout در MySQL آمده است.

آیا این خطا در MariaDB هم رخ می‌دهد؟

بله. MariaDB که fork سازگار با MySQL است، همان رفتار را دارد و همان متغیر max_connections را می‌شناسد. تفاوت‌های جزئی در مقادیر پیش‌فرض و نحوهٔ مدیریت thread pool وجود دارد ولی مفاهیم پایه یکسان است.

آیا این خطا می‌تواند از حملهٔ DDoS ناشی شود؟

بله، در حملات DDoS که هدف، اشغال منابع سرور است، یکی از اهداف ممکن، اشغال اتصال‌های دیتابیس است. اگر این خطا با جهش ناگهانی ترافیک همراه است و الگوی آن شبیه حمله است، حتماً لاگ‌های وب سرور و فایروال را بررسی کنید. راهکار مقابله با این نوع حملات، عمدتاً در سطح فایروال و CDN انجام می‌شود نه در سطح MySQL.

آیا MySQL برای کاربر root اتصال رزرو می‌کند؟

بله. MySQL به‌طور پیش‌فرض یک اتصال اضافی برای کاربران SUPER رزرو می‌کند که از طریق آن، مدیر می‌تواند در شرایط بحرانی وارد شود. این قابلیت با متغیر max_connections به‌طور مستقیم کنترل نمی‌شود ولی مقدار آن معمولاً یک اتصال است. برای استفاده از این رزرو، باید کاربری با دسترسی SUPER داشته باشید.

چگونه بفهمم مشکل از اپلیکیشن است یا از ترافیک؟

اگر الگوی مصرف اتصال‌ها در طول شبانه‌روز ثابت است ولی به‌مرور رشد می‌کند، احتمالاً مشکل از نشتی اتصال در اپلیکیشن است. اگر الگوی مصرف در ساعات مشخص اوج می‌گیرد و در ساعات دیگر کاهش می‌یابد، احتمالاً مشکل از ترافیک است. ترکیب این دو، در انتخاب راه‌حل اهمیت دارد.

آیا استفاده از connection pool به‌جای اتصال مستقیم همیشه بهتر است؟

در پروژه‌های پربار بله، چون هزینهٔ ساخت اتصال را کاهش می‌دهد. ولی در پروژه‌های کم‌بار، ممکن است هزینهٔ مدیریت pool بیشتر از فایدهٔ آن باشد. تصمیم درست، بر اساس الگوی بار و منابع سرور گرفته می‌شود. راهنمای تفاوت InnoDB و MyISAM نشان می‌دهد که تفاوت‌های مشابهی در سطح موتور دیتابیس هم وجود دارد و باید انتخاب‌ها با دقت انجام شود.

ابزارها و تکنیک‌های حرفه‌ای پایش اتصال

در پروژه‌های جدی، پایش دستی کافی نیست. چند ابزار و تکنیک وجود دارد که سرعت بررسی اتصال‌ها را چند برابر می‌کند و در تیم‌های بالغ به یک عادت تبدیل شده است.

پایش با اطلاعات information_schema

جدول PROCESSLIST، ساده‌ترین و در دسترس‌ترین ابزار پایش است:

SELECT USER, HOST, COMMAND, COUNT(*) AS connections
FROM information_schema.PROCESSLIST
GROUP BY USER, HOST, COMMAND
ORDER BY connections DESC;

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

پایش با performance_schema در MySQL 8

جدول‌های performance_schema اطلاعات دقیق‌تری دارند:

SELECT * FROM performance_schema.threads;
SELECT * FROM performance_schema.accounts;

این جدول‌ها، تعداد threadهای فعال را با جزئیات کامل نشان می‌دهند و برای دیباگ پیشرفته مناسب‌اند.

ابزارهای گرافیکی و سرویس‌های پایش

ابزارهایی مثل Percona Monitoring and Management، MySQL Workbench و DBeaver، نمای گرافیکی از اتصال‌ها و تراکنش‌ها ارائه می‌دهند. سرویس‌های ابری مثل Datadog و New Relic نیز قابلیت پایش MySQL را دارند و در قالب داشبورد، الگوهای مصرف را نشان می‌دهند. اگر پروژه‌تان پیچیده است، سرمایه‌گذاری روی یکی از این ابزارها سرعت تشخیص را چند برابر می‌کند.

پایش با اسکریپت‌های سبک و cron

یک اسکریپت bash که هر پنج دقیقه اجرا شود و در صورت عبور از آستانه، هشدار بدهد، برای اکثر پروژه‌ها کافی است:

#!/bin/bash
CONN=$(mysql -u root -pXXX -e "SHOW STATUS LIKE 'Threads_connected'" | awk 'NR==2 {print $2}')
if [ "$CONN" -gt 200 ]; then
  echo "Too many connections: $CONN" | mail -s "Warning" admin@example.com
fi

این نوع اسکریپت، ساده ولی مؤثر است و در پروژه‌های کوچک جایگزین ابزارهای سنگین می‌شود.

تست بارگذاری هدفمند

یکی از تکنیک‌های حرفه‌ای، اجرای تست بار روی محیط staging با الگوهایی است که احتمال پر شدن سقف اتصال را بالا می‌برند. ابزارهایی مثل sysbench، mysqlslap و JMeter برای این کار مناسب‌اند. با این تست‌ها می‌توان پیش از production، ظرفیت واقعی سرور و نقطهٔ شکست آن را پیدا کرد.

پشت صحنه MySQL: نگاهی مهندسی به مدیریت اتصال

برای توسعه‌دهندگانی که در سطح معماری کار می‌کنند، درک رفتار MySQL در سطح مدیریت اتصال، تفاوت‌های ظریفی را آشکار می‌کند که در پروژه‌های پرترافیک حیاتی می‌شوند.

MySQL از یک مدل thread-per-connection استفاده می‌کند؛ یعنی برای هر اتصال، یک thread اختصاصی ساخته می‌شود. این مدل برخلاف مدل event-driven که در سرورهای NoSQL رایج است، هزینهٔ حافظهٔ بالاتری دارد ولی در عوض، پیچیدگی کمتری در سطح کد دارد. همین مدل، دلیل اصلی وجود سقف max_connections است؛ چون هر thread، به‌طور پیش‌فرض چند مگابایت پشته (stack) و بافر اختصاصی مصرف می‌کند.

نکتهٔ ظریف اینکه MySQL هر thread را در حافظهٔ اشتراکی نگه می‌دارد و در سرورهای پربار، همین ساختار می‌تواند به گلوگاه تبدیل شود. برای رفع این محدودیت، نسخه‌های Enterprise و Percona از Thread Pool Plugin استفاده می‌کنند که در آن، تعداد threadهای واقعی کمتر از تعداد اتصال‌های منطقی است. این رویکرد، شبیه مفهوم connection pool در سطح اپلیکیشن است، ولی در سطح خود سرور پیاده می‌شود. برای پروژه‌هایی با ده‌ها هزار اتصال همزمان، این گزینه ارزش بررسی دارد.

در سطح مدیریت حافظه، هر اتصال MySQL به‌طور پیش‌فرض از سه بافر اصلی استفاده می‌کند: read buffer، sort buffer و join buffer. اندازهٔ این بافرها توسط متغیرهای سراسری تعیین می‌شود ولی هر اتصال می‌تواند آن‌ها را برای session خودش تغییر دهد. اگر max_connections را بدون توجه به این بافرها بالا ببرید، در لحظهٔ اوج، مجموع مصرف حافظه می‌تواند از کل رم سرور بیشتر شود و این باعث فراخوانی‌های مکرر swap و کندی شدید می‌شود. محاسبهٔ دقیق مصرف حافظهٔ هر اتصال، بخشی از تنظیمات پیشرفته MySQL است که در مستندات رسمی هم توصیه شده است.

نکتهٔ دیگری که در سطح معماری اهمیت دارد، رفتار MySQL در برخورد با اتصال‌های بی‌مصرف است. متغیر wait_timeout و interactive_timeout تعیین می‌کنند که سرور چند ثانیه اتصال بی‌استفاده را نگه دارد. اگر این متغیرها روی مقادیر بسیار بالا تنظیم شده باشند، اتصال‌های یتیم، منابع سرور را اشغال می‌کنند و ظرفیت مفید کاهش می‌یابد. در تجربه‌ام، تنظیم wait_timeout روی ۶۰ تا ۳۰۰ ثانیه، تعادل خوبی بین آزادسازی سریع منابع و اجتناب از ساخت مکرر اتصال است. نکتهٔ ظریف اینکه این مقدار باید با تنظیمات اپلیکیشن هماهنگ باشد؛ اگر اپلیکیشن اتصال‌ها را برای طولانی‌تری نگه می‌دارد، تنظیم سرور بی‌اثر است.

در معماری‌های distributed که چند اپلیکیشن به‌طور همزمان به یک MySQL وصل می‌شوند، شمارش دقیق اتصال‌ها اهمیت بالایی دارد. هر اپلیکیشن، ممکن است از ارتباط مستقیم، از pool محلی، یا از proxy استفاده کند و در هر سه حالت، نحوهٔ شمارش اتصال‌ها متفاوت است. برای کنترل دقیق، معمولاً لایهٔ پروکسی مثل ProxySQL یا MaxScale استفاده می‌شود که در آن، تعداد اتصال‌های واقعی به MySQL کنترل‌شده است و اپلیکیشن‌ها به‌جای وصل شدن به MySQL، به پروکسی وصل می‌شوند. این رویکرد، در معماری‌های میکروسرویس به‌طور گسترده استفاده می‌شود و مدیریت اتصال‌ها را از اپلیکیشن جدا می‌کند.

یک نکتهٔ آکادمیک که در کار روزمره هم به کار می‌آید: در نظریهٔ سیستم‌های توزیع‌شده، مفهوم backpressure به‌عنوان یک اصل طراحی مطرح می‌شود. وقتی یک سیستم نمی‌تواند بار بیشتر را پردازش کند، باید به‌جای پذیرش سرریز، درخواست‌ها را در نقطه‌ای رد کند. خطای Too many connections دقیقاً یک نمونهٔ عملی از backpressure است که در سطح MySQL پیاده شده. این مفهوم، در طراحی سیستم‌های مقیاس‌پذیر جایگاه محوری دارد و درک آن، در انتخاب استراتژی‌های مقابله اهمیت دارد.

در نهایت، یک نکتهٔ مهم درباره همکاری با تیم DevOps: هیچ تصمیمی درباره max_connections و تنظیمات اتصال، نباید جدا از ظرفیت کل سیستم گرفته شود. اگر سرور MySQL در یک محیط مجازی کار می‌کند، منابع اختصاص‌یافته به آن، محدود است و تصمیم درباره اتصال‌ها باید با توجه به این محدودیت باشد. عدم هماهنگی این لایه‌ها، در پروژه‌های بزرگ یکی از شایع‌ترین دلایل بروز خطاهای اتصال است.

برای درک عمیق‌تر این مفاهیم، مطالعهٔ مستندات رسمی MySQL درباره performance tuning توصیه می‌شود که در آن، نحوهٔ محاسبهٔ دقیق مصرف حافظهٔ هر اتصال توضیح داده شده است. ابزارهای پایش حرفه‌ای مثل Percona PMM نیز می‌توانند در تحلیل دقیق‌تر کمک کنند.

خط پایان و توصیه‌های آخر

خطای Too many connections، بیش از آنکه نشانهٔ ضعف سرور باشد، نشانهٔ عدم تعادل بین بار و منابع است. این جمله را عمداً تکرار می‌کنم؛ چون در تجربه‌ام دیدم که تیم‌های تازه‌کار همیشه سراغ تنظیمات سرور می‌روند در حالی که مقصر اصلی، الگوهای مصرف در اپلیکیشن است. یکی از پروژه‌های فروشگاهی که این خطا را روزانه چند بار می‌دید، بعد از افزودن connection pool با اندازهٔ مناسب و کاهش تعداد اتصال‌های همزمان در اسکریپت پایش، برای همیشه از این خطا خلاص شد؛ بدون هیچ تغییر در max_connections.

سه توصیهٔ پایانی من به تیم‌های فنی این است. اول، پیش از هر تصمیمی درباره تنظیمات سرور، الگوهای مصرف اتصال را در طول شبانه‌روز و هفته اندازه‌گیری کنید؛ بدون داده، تصمیم‌ها فقط حدس‌اند. دوم، نشتی اتصال در کد را جدی بگیرید و اتصال‌ها را همیشه در بلوک finally ببندید. سوم، پایش اتصال را به یک عادت دوره‌ای تبدیل کنید. اگر این سه را رعایت کنید، خطای Too many connections از یک بحران تکراری به یک سیگنال نادر تبدیل می‌شود که با کمی دقت، همیشه سریع ریشه‌یابی می‌شود.

اگر در پروژه‌ای با یک مورد نادر از این خطا روبه‌رو شده‌اید که در هیچ‌کدام از سناریوهای این مقاله جا نمی‌گیرد، تجربه‌تان را در دیدگاه بنویسید؛ به‌خصوص اگر خروجی SHOW STATUS یا الگوی مصرف اتصال‌ها را ذکر کنید، می‌توانیم با هم به ریشه برسیم. همچنین اگر ترفند یا ابزار خاصی دارید که در پروژه‌های خودتان برای پایش اتصال‌ها استفاده می‌کنید، همان را به اشتراک بگذارید؛ برای خواننده بعدی که با همین خطا درگیر است، تجربهٔ شما ارزشمندتر از هر مستند رسمی است. 🔗