مقدمه

سال‌ها پیش، روی یک سایت خبری پربازدید بود که هر چند ساعت یک بار، کاربران با یک صفحه سفید مواجه می‌شدند. لاگ وردپرس پر بود از یک پیام تکراری: «Lost connection to MySQL server during query». مدیر سایت فکر می‌کرد سرور دیتابیس مشکل دارد و مصر بود که باید هاست را عوض کنیم. اما وقتی دقیق‌تر نگاه کردم، ریشه مشکل جای دیگری بود: یکی از افزونه‌ها یک کوئری سنگین را روی جدول wp_postmeta اجرا می‌کرد که با یک max_allowed_packet کوچک، به قطع اتصال منجر می‌شد.

این خطا در تجربه من از آن دسته خطاهایی است که همیشه یک علت واحد ندارد و برای حلش باید تمام لایه‌های بین PHP و MySQL را بررسی کنید. در این مقاله می‌خواهم دقیقاً بگویم این خطا از کجا می‌آید، چه تفاوتی با خطاهای هم‌خانواده مثل Query execution was interrupted و MySQL server has gone away دارد، و چه راهکارهایی برای حل ریشه‌ای آن وجود دارد.

خطای Lost connection دقیقاً چیست؟

پیام «Lost connection to MySQL server during query» (کد خطای 2013 در MySQL) یعنی اتصال بین کلاینت (معمولاً PHP یا یک اپلیکیشن دیگر) و سرور MySQL، در میانه اجرای یک کوئری، به‌طور ناگهانی قطع شده است. این قطع می‌تواند از طرف سرور، از طرف کلاینت، یا از طرف هر لایه‌ای در مسیر ارتباطی بین این دو رخ دهد.

پیام کامل خطا معمولاً به یکی از شکل‌های زیر در لاگ یا خروجی PHP ظاهر می‌شود:

ERROR 2013 (HY000): Lost connection to MySQL server during query

یا شکل دیگری که در آن مسیرِ قطع‌شدن مشخص است:

ERROR 2006 (HY000): MySQL server has gone away

یک اشتباه رایج این است که این دو خطا با هم قاطی شوند. در واقع خطای 2006 وقتی رخ می‌دهد که اتصال هنگام انتظار قطع شده باشد (بدون کوئری فعال)، اما خطای 2013 وقتی رخ می‌دهد که اتصال در میانه اجرای یک کوئری قطع شود. تفاوت این دو در نقطه‌شناسی، به شما کمک می‌کند سریع‌تر به ریشه مشکل برسید. برای مطالعه بیشتر، مقاله MySQL در ویکی‌پدیا نقطه شروع خوبی است.

نکته کلیدی که در سال‌ها تجربه یاد گرفته‌ام این است: این خطا همیشه از طرف سرور نیست. در بیش از نیمی از مواردی که بررسی کرده‌ام، مقصر یکی از این سه بود: یک کوئری با بسته‌بندی بزرگ‌تر از max_allowed_packet، یک timeout شبکه‌ای در لایه پروکسی، یا یک اسکریپت PHP طولانی که قبل از ارسال کوئری بعدی، اتصال را از دست داده است.

Lost connection یعنی یکی از لایه‌های بین PHP و MySQL تصمیم گرفته سیم را بکشد؛ پیدا کردن آن لایه، تمام کار تشخیص است.

شش ریشه اصلی این خطا

در تجربه‌ام، این خطا تقریباً همیشه یکی از شش ریشه زیر را دارد. هر کدام امضای خاص خودش را در لاگ دارد:

۱. max_allowed_packet کوچک

شایع‌ترین علت در محیط‌های وردپرسی. اگر یک کوئری INSERT یا UPDATE بزرگ‌تر از اندازه‌ای باشد که MySQL مجاز می‌داند، MySQL بسته را رد می‌کند و اتصال را می‌بندد. مقدار پیش‌فرض این پارامتر در نسخه‌های قدیمی MySQL فقط ۱ مگابایت است — عددی که در ذخیره کردن یک برگه سنگین، یک پست بلند یا یک گزارش گرفتن روی ووکامرس به‌راحتی رد می‌شود.

SHOW VARIABLES LIKE 'max_allowed_packet';
-- خروجی: 1048576 یعنی فقط ۱ مگابایت

۲. wait_timeout و interactive_timeout کوتاه

MySQL برای هر اتصال، یک تایمر بیکاری تنظیم می‌کند. اگر اپلیکیشن شما یک اتصال را باز نگه دارد و بعد از مدتی کوئری جدیدی ارسال کند، MySQL ممکن است آن اتصال را بسته باشد. مقدار پیش‌فرض این پارامتر معمولاً ۲۸۸۰۰ ثانیه (۸ ساعت) است، اما بسیاری از هاست‌ها آن را به ۶۰ تا ۳۰۰ ثانیه کاهش می‌دهند. در این حالت، اگر یک اسکریپت طولانی مثل یک Export گزارش یا یک به‌روزرسانی دسته‌ای اجرا کنید، در میانه راه اتصال قطع می‌شود.

SHOW VARIABLES LIKE 'wait_timeout';
SHOW VARIABLES LIKE 'interactive_timeout';

۳. قطع اتصال توسط پروکسی یا فایروال

اگر سایت شما از یک CDN، Load Balancer یا پروکسی مثل ProxySQL عبور می‌کند، ممکن است کوئری شما به خاطر یک idle timeout در لایه پروکسی قطع شود. این نوع قطع معمولاً الگوی مشخصی دارد: کوئری‌ای که مستقیم روی سرور به‌طور کامل اجرا می‌شود، از طریق پروکسی در وسط راه قطع می‌گردد. تفاوت ظریف این است که خطای قطع در این حالت معمولاً همان 2013 است، نه 2006.

۴. Restart یا Failover سرور

اگر سرور MySQL در حال ریستارت باشد یا یک عملیات Failover رخ دهد، تمام اتصال‌های فعال به‌طور ناگهانی قطع می‌شوند. این حالت معمولاً با پیام‌های اضافی در لاگ MySQL همراه است، مثل Server shutdown in progress یا Shutting down MySQL. اگر مکرراً این الگو را می‌بینید، احتمالاً هاست شما روی یک زیرساخت پرنوسان قرار دارد.

۵. محدودیت تعداد اتصال هم‌زمان

پارامتر max_connections سقف تعداد اتصال‌های هم‌زمان را تعیین می‌کند. اگر سایت شما ترافیک پیک داشته باشد و تعداد اتصال‌ها به این سقف برسد، MySQL اتصال‌های جدید را با خطای Too many connections رد می‌کند. اما در برخی سناریوها، این خطا به‌صورت Lost connection نمایش داده می‌شود چون در حین برقراری اتصال قطع شده است. برای درک عمیق‌تر این محدودیت، مقاله رفع خطای Too many connections در MySQL را توصیه می‌کنم.

۶. Timeout شبکه‌ای در مسیر ارتباطی

پارامتر net_read_timeout و connect_timeout، مدت زمانی که MySQL منتظر بسته‌های شبکه می‌ماند را تعیین می‌کنند. اگر شبکه بین PHP و MySQL کند باشد (مثلاً روی یک VPS با شبکه اشتراکی)، ممکن است بسته‌ها دیر برسند و MySQL اتصال را ببندد. این حالت معمولاً با نوسان در زمان پاسخ‌دهی همراه است.

نشانه‌ها و علائم تشخیص

قبل از اینکه این خطا به یک بحران تبدیل شود، معمولاً نشانه‌هایی وجود دارد که اگر به آن‌ها توجه کنید، می‌توانید به‌موقع اقدام کنید:

  • قطعی‌های ناگهانی در ساعات پیک: اگر خطا فقط در ساعات پرترافیک ظاهر می‌شود، احتمالاً به max_connections یا منابع سرور مربوط است.
  • الگوی تکرارشونده در صفحات سنگین: اگر فقط صفحاتی مثل گزارش‌گیری، Export یا به‌روزرسانی دسته‌ای با این خطا مواجه می‌شوند، به max_allowed_packet یا wait_timeout مشکوک شوید.
  • افزایش زمان پاسخ سرور دیتابیس: اگر Query Time متوسط صعودی است، احتمالاً به سقف پارامترها نزدیک می‌شوید.
  • خطاهای همراه در لاگ سرور: پیام‌هایی مثل Got timeout reading communication packets یا Aborted connection سرنخ‌های خوبی هستند.
  • افزایش مصرف CPU دیسک دیتابیس: اگر سرور دیسک در حال پرش است، ممکن است کوئری‌ها کند شده باشند و اتصال‌ها منقضی شوند.

تشخیص دقیق: از کجا شروع کنیم؟

برای تشخیص دقیق، ترتیب زیر را در تجربه‌ام مفید یافته‌ام:

گام اول: بررسی پارامترهای جاری

اولین کاری که می‌کنم این است که ببینم چه تنظیماتی روی سرور فعال است:

SHOW VARIABLES LIKE 'max_allowed_packet';
SHOW VARIABLES LIKE 'wait_timeout';
SHOW VARIABLES LIKE 'interactive_timeout';
SHOW VARIABLES LIKE 'max_connections';
SHOW VARIABLES LIKE 'connect_timeout';
SHOW VARIABLES LIKE 'net_read_timeout';

هر کدام از این پارامترها می‌تواند مقصر باشد. اگر هر یک از مقادیر زیر از حد انتظار کمتر است، آن را در ذهن نگه دارید:

  • max_allowed_packet کمتر از ۱۶ مگابایت
  • wait_timeout کمتر از ۶۰ ثانیه
  • max_connections کمتر از ۱۰۰
  • connect_timeout کمتر از ۵ ثانیه

گام دوم: بررسی لاگ MySQL

فایل لاگ MySQL (معمولاً در /var/log/mysql/error.log) را بررسی کنید. به دنبال خطاهایی مثل زیر بگردید:

[Note] Aborted connection 42 to db: 'wordpress' user: 'root' host: 'localhost' (Got timeout reading communication packets)
[Warning] Got an error reading communication packets

این خطوط نشان می‌دهند که MySQL، به دلیل انقضای timeout، اتصال را بسته است. اگر پیام Got an error reading communication packets می‌بینید، احتمالاً یک بسته ناقص رسیده و MySQL اتصال را قطع کرده است.

گام سوم: بررسی لاگ PHP

لاگ PHP و لاگ وردپرس (debug.log) را بررسی کنید. پیام‌های MySQL server has gone away که توسط افزونه‌ها ثبت می‌شوند، در اینجا ظاهر می‌شوند. اگر با Query Monitor کار می‌کنید، می‌توانید کوئری‌های سنگین را در زمان واقعی ببینید — همان‌طور که در بخش مانیتورینگ توضیح خواهم داد.

گام چهارم: تست اتصال مستقیم

با یک کلاینت مثل mysql CLI یا phpMyAdmin، مستقیماً به سرور وصل شوید و یک کوئری سنگین اجرا کنید. اگر خطا اینجا هم رخ دهد، مشکل در سطح سرور است. اگر فقط در PHP رخ دهد، مشکل در لایه PHP یا شبکه است:

mysql -u root -p -h 127.0.0.1 -e "SELECT BENCHMARK(100000000, MD5('test'));"

گام پنجم: بررسی پارامترهای شبکه و پروکسی

اگر سایت شما از یک CDN یا پروکسی عبور می‌کند، تنظیمات آن را بررسی کنید. معمولاً ابزارهای پروکسی مثل Nginx یا HAProxy یک پارامتر proxy_read_timeout دارند که اگر از wait_timeout MySQL کوتاه‌تر باشد، این خطا ایجاد می‌شود.

تفاوت با خطاهای مشابه

این جدول به شما کمک می‌کند سریع تشخیص دهید کدام خطا را در دست دارید:

خطاعلت اصلینشانه کلیدی
Lost connection to MySQL (2013)قطع اتصال در میانه کوئریلاگ همراه با timeout reading packets
MySQL server has gone away (2006)قطع اتصال در حالت بیکاریاتصال قبلاً timeout شده بود
Query execution was interrupted (1317)قطع عمدی کوئریKILL QUERY یا max_execution_time
Lock wait timeout exceededانتظار طولانی برای قفلتراکنش قربانی، بدون قطع اتصال
Deadlock foundچرخه انتظار قفلRollback خودکار تراکنش
Too many connectionsسقف اتصال هم‌زمانmax_connections رد شده

دو نکته ظریف در این جدول وجود دارد. اول، خطای Lost connection و MySQL server has gone away اغلب با هم اشتباه گرفته می‌شوند، اما نقطه وقوعشان متفاوت است: اولی در میانه کوئری، دومی در حالت بیکاری. دوم، خطای Query execution was interrupted ناشی از یک تصمیم عمدی (KILL یا timeout) است، در حالی که Lost connection ناشی از یک رخداد فیزیکی (قطع شبکه، بسته شدن اتصال، یا Restart) است. تشخیص درست این تفاوت‌ها، اولین قدم در حل صحیح است.

راه‌حل‌های عملی و گام‌به‌گام

حالا که تشخیص دادید، وقت درمان است. راه‌حل‌ها را به سه سطح تفکیک کرده‌ام:

راه‌حل فوری: افزایش پارامترهای سرور

اگر مطمئن هستید که کوئری شما به‌درستی نوشته شده اما به‌خاطر سقف پارامترها قطع می‌شود، می‌توانید این پارامترها را موقتاً افزایش دهید:

SET GLOBAL max_allowed_packet = 67108864;  -- ۶۴ مگابایت
SET GLOBAL wait_timeout = 600;
SET GLOBAL interactive_timeout = 600;
SET GLOBAL max_connections = 200;

این تغییرات موقتی هستند و با ریستارت MySQL از بین می‌روند. برای اعمال دائمی، باید در فایل my.cnf یا my.ini تنظیم شوند:

[mysqld]
max_allowed_packet = 64M
wait_timeout = 600
interactive_timeout = 600
max_connections = 200

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

راه‌حل کوتاه‌مدت: Auto-Reconnect در PHP

در بعضی از کتابخانه‌های اتصال PHP (مثل PDO و MySQLi)، یک پارامتر به نام Auto-Reconnect یا MYSQL_OPT_RECONNECT وجود دارد که اگر اتصال قطع شود، به‌طور خودکار مجدداً وصل می‌شود. اما استفاده از این قابلیت با احتیاط توصیه می‌شود:

$mysqli = new mysqli(
    'localhost',
    'user',
    'pass',
    'db',
    3306,
    null,
    MYSQLI_OPT_RECONNECT  // استفاده با احتیاط
);

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

راه‌حل ساختاری: کاهش اندازه بسته‌ها

به‌جای افزایش max_allowed_packet، می‌توانید اندازه بسته‌ها را در کوئری‌های خود کاهش دهید. سه تکنیک عملی:

تقسیم کوئری‌های INSERT بزرگ: به‌جای یک INSERT با ۱۰٫۰۰۰ ردیف، آن را به ۱۰ INSERT با ۱٫۰۰۰ ردیف تقسیم کنید.

استفاده از LOAD DATA INFILE: برای وارد کردن داده‌های انبوه، به‌جای INSERT، از LOAD DATA INFILE استفاده کنید که بسیار سریع‌تر و کم‌حجم‌تر است.

محدودسازی Export: در گزارش‌گیری، همیشه با LIMIT کار کنید و از Export یک‌باره پرهیز کنید. این تکنیک در تأثیر دیتابیس بر سرعت سایت بیشتر توضیح داده شده است.

راه‌حل پیشرفته: Persistent Connection با مدیریت صحیح

در برنامه‌های پربازدید، استفاده از Persistent Connection می‌تواند تفاوت چشمگیری در عملکرد ایجاد کند، اما نیاز به مدیریت دقیق دارد. در PHP، با استفاده از p:localhost در PDO یا p: در MySQLi، اتصال بین رکوئست‌ها به‌اشتراک گذاشته می‌شود:

$pdo = new PDO(
    'mysql:host=p:localhost;dbname=wordpress',
    'user',
    'pass'
);

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

راه‌حل مقطعی: کش کردن کوئری‌های سنگین

اگر کوئری‌ای ذاتاً سنگین است و به‌طور مرتب اجرا می‌شود، به‌جای اجرای هر بار، نتیجه را در یک کش (مثلاً Redis یا Memcached) ذخیره کنید. برای گزارش‌های ووکامرس، افزونه‌هایی مثل WooCommerce Analytics از همین روش استفاده می‌کنند. اگر می‌خواهید روش اصولی را یاد بگیرید، مقاله بهینه‌سازی دیتابیس ووکامرس را ببینید.

استراتژی‌های پیشگیری

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

۱. تحلیل منظم کوئری‌های سنگین

پارامتر slow_query_log را فعال کنید و هر هفته کوئری‌های کند را بررسی کنید. این کار قبل از اینکه کوئری‌ها به سقف پارامترها برسند، آن‌ها را شناسایی می‌کند:

SET GLOBAL slow_query_log = 'ON';
SET GLOBAL long_query_time = 2;
SET GLOBAL slow_query_log_file = '/var/log/mysql/slow.log';

برای مطالعه جامع‌تر درباره عیب‌یابی سیستماتیک، مقاله بهینه‌سازی کوئری‌های MySQL نقطه شروع خوبی است.

۲. ایندکس‌گذاری درست

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

۳. پایش مستمر خطا

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

grep -E "Lost connection|has gone away" /var/log/mysql/error.log | tail -5

۴. تنظیم پارامترهای پروکسی و وب‌سرور

اگر از Nginx یا Apache استفاده می‌کنید، مطمئن شوید که پارامترهای timeout آن‌ها با wait_timeout MySQL سازگار است. معمولاً این تنظیم در Nginx به‌شکل زیر است:

proxy_read_timeout 300s;
proxy_send_timeout 300s;
fastcgi_read_timeout 300s;

۵. مانیتورینگ منابع هاست

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

هر عددی که در تنظیمات MySQL بالاتر می‌برید، در واقع دارید علامت را می‌پوشانید؛ کوئری سبک‌تر، خودِ بیماری را درمان می‌کند.

پرسش‌های پرتکرار درباره Lost Connection

آیا این خطا باعث از دست رفتن داده می‌شود؟

در کوئری‌های SELECT، خیر؛ چون فقط یک عملیات خواندن نیمه‌کاره رها می‌شود و هیچ تغییری روی داده‌ها اعمال نمی‌شود. اما در کوئری‌های INSERT، UPDATE یا DELETE، اگر تراکنش در میانه راه قطع شود و به‌درستی مدیریت نشود، ممکن است داده‌ها نیمه‌کاره تغییر کنند. حتماً از تراکنش‌ها به‌درستی استفاده کنید.

تفاوت این خطا با MySQL server has gone away چیست؟

خطای Lost connection در میانه کوئری رخ می‌دهد، اما MySQL server has gone away در حالت بیکاری. در عمل، هر دو نشانه قطع اتصال هستند، اما نقطه وقوعشان متفاوت است و این تفاوت در تشخیص ریشه‌ای مشکل اهمیت دارد.

آیا افزایش max_allowed_packet روی هاست اشتراکی ممکن است؟

در سطح session، بله. اما در سطح سرور، نیاز به دسترسی ادمین دارید که روی هاست اشتراکی معمولاً در دسترس نیست. اگر هاست شما این امکان را نمی‌دهد، باید به سراغ کاهش اندازه بسته‌ها در کوئری‌های خود بروید.

آیا KILL QUERY باعث Lost connection می‌شود؟

نه لزوماً. KILL QUERY فقط کوئری جاری را متوقف می‌کند و اتصال باز می‌ماند، اما KILL CONNECTION کل اتصال را قطع می‌کند و می‌تواند منجر به Lost connection شود. برای مطالعه بیشتر، مقاله خطای Query execution was interrupted را ببینید.

آیا این خطا فقط در ووکامرس دیده می‌شود؟

خیر. این خطا در هر سیستم مبتنی بر MySQL می‌تواند رخ دهد. اما در ووکامرس چون گزارش‌های سنگین روی جداول بزرگ (wp_postmeta و wp_wc_order_stats) اجرا می‌شوند، شایع‌تر است.

آیا استفاده از Auto-Reconnect در PDO امن است؟

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

آیا این خطا در MySQL 8 تفاوت کرده است؟

در MySQL 8، مقادیر پیش‌فرض برخی از پارامترها بهتر شده است (مثلاً max_allowed_packet پیش‌فرض 64M است)، اما مکانیزم بروز خطا تفاوت نکرده. اگر با این خطا مواجه می‌شوید، بررسی پارامترها و بهینه‌سازی همچنان اولین قدم است.

کالبدشکافی فنی: درون موتور MySQL چه می‌گذرد؟

برای درک عمیق این خطا، باید بدانید MySQL چطور اتصال‌ها را مدیریت می‌کند. هر اتصال به MySQL، یک Thread (نخ) اختصاصی در فضای کاربری سرور دارد. این نخ از لحظه اتصال، یک تایمر داخلی شروع می‌کند که با هر کوئری فعال، بازنشانی می‌شود.

وقتی یک کوئری از کلاینت ارسال می‌شود، این فرآیند طی می‌شود:

  1. Packet Read: MySQL منتظر دریافت بسته‌های شبکه می‌ماند. زمان انتظار توسط net_read_timeout کنترل می‌شود.
  2. Packet Validate: اگر بسته بزرگ‌تر از max_allowed_packet باشد، MySQL آن را رد می‌کند و اتصال را می‌بندد.
  3. Query Parse: تجزیه SQL و تبدیل به یک درخت منطقی.
  4. Execute: اجرای پلن روی موتور ذخیره‌سازی.
  5. Result Send: ارسال نتایج به کلاینت. زمان انتظار توسط net_write_timeout کنترل می‌شود.
  6. Idle: اگر کلاینت کوئری جدیدی نفرستد، تایمر بیکاری فعال می‌شود و پس از wait_timeout ثانیه، اتصال بسته می‌شود.

خطای 2013 معمولاً در مراحل ۱، ۲ یا ۵ رخ می‌دهد. خطای 2006 معمولاً در مرحله ۶ (بیکاری) رخ می‌دهد. تفاوت این دو، در محل وقوع و در نکات تشخیصی است.

نکته مهم و کمتر شناخته‌شده: در تراکنش‌های InnoDB، اگر اتصال به‌طور ناگهانی قطع شود، تراکنش به‌طور خودکار Rollback می‌شود، اما قفل‌ها ممکن است برای مدتی باز نمانند تا MySQL فرآیند پاکسازی را کامل کند. این موضوع می‌تواند به خطاهای Deadlock یا Lock Wait Timeout بعدی منجر شود. به همین دلیل، اگر برنامه شما با تراکنش‌ها کار می‌کند، پس از یک Lost connection، باید وضعیت تراکنش‌ها را با SHOW ENGINE INNODB STATUS بررسی کنید.

برای مطالعه بیشتر درباره تفاوت‌های تراکنشی موتورها، مقاله تفاوت InnoDB و MyISAM را توصیه می‌کنم.

نقش هاست اشتراکی در بروز این خطا

بیشتر مواردی که این خطا را در هاست اشتراکی دیده‌ام، سه علت مشترک داشته‌اند:

سقف منابع CPU و I/O

هاست‌های اشتراکی معمولاً سقف CPU و I/O دارند. اگر کوئری شما به‌طور غیرعادی سنگین باشد، ممکن است هاست به‌طور خودکار آن را قطع کند. این کار معمولاً در لاگ به‌عنوان Resource limit reached همراه است. راهکار اصولی این است که کوئری را بهینه کنید — همان‌طور که در مقاله کاهش مصرف منابع هاست توضیح دادم.

max_connections اجباری کوچک

بعضی هاست‌های اشتراکی، max_connections را روی یک مقدار کوچک (مثلاً ۲۵ یا ۵۰) تنظیم می‌کنند. در سایت‌های پربازدید، این مقدار می‌تواند به‌راحتی اشباع شود و اتصال‌های جدید با Lost connection رد شوند. برای بررسی، از دستور SHOW VARIABLES LIKE 'max_connections' استفاده کنید. اگر مقدار کمتر از ۵۰ بود و سایت شما ترافیک دارد، این پارامتر مقصر اصلی است.

Statement Timeout اجباری

بعضی هاست‌های اشتراکی یک max_statement_time یا max_execution_time اجباری دارند که با ابزارهای پنل قابل تغییر نیست. برای بررسی، از دستور SHOW VARIABLES استفاده کنید. اگر مقدار صفر نبود و امکان تغییر نداشتید، دو گزینه پیش روی شماست: بهینه‌سازی کوئری یا مهاجرت به هاست حرفه‌ای‌تر. روش اصولی مهاجرت را در مهاجرت سایت به هاست جدید نوشته‌ام.

یک نکته مهم که در مشاوره‌ها زیاد دیده‌ام: بعد از مهاجرت، ممکن است مدتی همان خطا تکرار شود. اگر این‌طور شد، بدانید که مشکل فقط هاست نبوده و ریشه در کوئری هم وجود داشته. به همین دلیل توصیه می‌کنم مهاجرت را با بهینه‌سازی همراه کنید، نه به‌جای آن.

مطالعه موردی: نجات یک سایت خبری

یک سایت خبری با حدود ۴۵۰۰۰ پست و ۳۰۰۰ بازدید هم‌زمان در ساعات پیک، با مشکل جدی مواجه شد: هر چند ساعت یک بار، کاربران با خطای «Lost connection to MySQL server during query» مواجه می‌شدند. مدیر سایت چند بار هاست را عوض کرده بود، اما مشکل حل نشده بود.

علائم:

  • خطای Lost connection فقط در ساعات ۲۰ تا ۲۳
  • مصرف CPU سرور طبیعی بود
  • در لاگ MySQL خطاهای Aborted connection مکرر دیده می‌شد

تشخیص:

با اجرای SHOW VARIABLES LIKE 'max_connections'، مقدار ۵۰ را دیدم. سپس با SHOW PROCESSLIST در ساعت پیک، دیدم که تعداد اتصال‌ها روی ۴۵ تا ۵۰ نوسان می‌کند. این یعنی سایت در ساعات پیک، در حال اشباع سقف اتصال بود. سپس با بررسی slow query log، متوجه شدم یک افزونه نظرات، یک کوئری SELECT روی wp_comments اجرا می‌کند که به‌خاطر نبود ایندکس مناسب، ۴ تا ۵ ثانیه طول می‌کشد. این کوئری‌های کند، اتصال‌ها را اشغال می‌کردند و ظرفیت را پر می‌کردند.

درمان:

  1. ابتدا یک ایندکس ترکیبی روی wp_comments.comment_post_ID, comment_approved اضافه کردم. زمان کوئری از ۴.۸ ثانیه به ۰.۰۲ ثانیه کاهش یافت.
  2. سپس در سطح session، با کش کردن نتایج سنگین در Redis، بار کوئری‌های تکراری را کم کردم.
  3. در نهایت، مقدار max_connections را از ۵۰ به ۲۰۰ افزایش دادم، اما فقط به‌عنوان پشتیبان.

درس‌آموخته:

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

حرف آخر: همکاری پایدار، نه مذاکره مقطعی

خطای «Lost connection to MySQL server» در نگاه اول ترسناک است، اما در واقع یک پیام گفت‌وگویی است: یکی از لایه‌های بین PHP و MySQL تصمیم گرفته ارتباط را قطع کند، چون آن را غیرقابل اعتماد یا پرهزینه تشخیص داده است. سؤال درست این نیست «چطور این لایه را مجبور به ادامه کنم»، بلکه این است «چرا این لایه تصمیم گرفته قطع کند».

از تجربه‌ام، چهار اصل عملی بیشترین بازدهی را داشته‌اند: اول، پارامترهای max_allowed_packet، wait_timeout و max_connections را در همان روز راه‌اندازی بررسی و تنظیم کنید. دوم، لاگ کندی کوئری را فعال کنید تا قبل از قطع شدن، کوئری‌های مشکوک را بشناسید. سوم، ایندکس‌گذاری اصولی روی ستون‌های فیلتر را جدی بگیرید. چهارم، اندازه بسته‌های ارسالی به MySQL را کوچک نگه دارید و کوئری‌های بزرگ را به قطعات کوچک تقسیم کنید.

در نهایت، اگر روی هاست اشتراکی هستید، باید بپذیرید که یک سقف سخت‌افزاری وجود دارد که نمی‌توانید آن را تغییر دهید. در این حالت، تنها راه‌حل واقعی، بهینه‌سازی کوئری یا مهاجرت به هاست حرفه‌ای‌تر است. اگر هم خودتان سرور اختصاصی دارید، باید بین دو گزینه تصمیم بگیرید: افزایش پارامترها (با ریسک مصرف منابع) یا بهینه‌سازی کوئری (با ریسک زمان توسعه). در ۹۰٪ پروژه‌هایی که دیده‌ام، گزینه دوم جواب داده است.

اگر روی پروژه‌ای با این خطا مواجه شده‌اید و روش خاصی برای حلش پیدا کرده‌اید — به‌خصوص اگر با پروکسی‌ها، Load Balancerها یا سیستم‌های پربازدید سر و کار داشته‌اید — خوشحال می‌شوم تجربه‌تان را بشنوم. بگویید در آن پروژه، مقصر اصلی چه بود: تنظیم هاست، بسته‌های بزرگ، یا نبود ایندکس؟ همین گفت‌وگو برای خواننده بعدی، ارزش یک روز عیب‌یابی را دارد.