چند سال پیش در پروژه‌ای که یک فروشگاه ووکامرس با چند هزار محصول داشت، هر شب حدود ساعت دو بامداد یک ایمیل خطا برایم می‌آمد: «MySQL server has gone away». سایت تا صبح سالم بود، کرون شبانه اجرا می‌شد و ایمیل‌ها روی هم تلنبار می‌شدند. اولین واکنشم این بود که سرور را عوض کنم. چند روز خوب بود و دوباره شروع شد. آن روز یاد گرفتم این پیام، خطای یک نقطه نیست؛ علامت یک زنجیره است. در این مقاله همان مسیری را می‌روم که آن شب تا صبح طی کردم.

این پیام واقعاً چه می‌گوید؟

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

نکتهٔ مهمی که در همان شب پروژه یاد گرفتم: این خطا در سه سناریوی متفاوت ظاهر می‌شود و اگر سناریو را تشخیص ندهید، تنظیمات سرور فقط علامت را جابه‌جا می‌کند:

  • کوئری سنگین یا بستهٔ بزرگ: مثلاً درج یک بلوک متنی حجیم یا ایمپورت دیتابیس، بزرگ‌تر از بستهٔ مجاز MySQL.
  • اتصال بی‌کار طولانی: اسکریپت PHP بعد از باز کردن اتصال، مدت‌ها چیزی نمی‌فرستد و سرور با wait_timeout آن را می‌بندد.
  • مرگ سرور یا restart شدن آن: سرویس MySQL ری‌استارت شده یا حافظه‌اش سرریز کرده و همهٔ اتصال‌های باز از بین رفته‌اند.
این خطا اسم یکی از طرفین را صدا می‌زند، ولی تقریباً همیشه باید دو طرف را با هم بررسی کرد: هم MySQL، هم PHP.

ریشه‌های اصلی خطا در MySQL و PHP

فهرست زیر نتیجهٔ همان پروژه و پروژه‌های بعدی است. هر مورد را در جای خودش می‌بینم:

ریشهنشانهٔ تشخیصیمحل تنظیم
کوچک بودن max_allowed_packetخطا در ایمپورت یا درج داده‌های حجیمmy.cnf و متغیر سرور
کوتاه بودن wait_timeoutخطا بعد از چند دقیقه سکوت کدmy.cnf یا پنل هاست
مرگ MySQL به‌دلیل حافظهخطا در ساعات ترافیک بالاسطح سرور و منابع هاست
اتصال‌های بازِ فراموش‌شده در کدهر بار جای دیگری قطع می‌شودکد PHP
رفتار نادرست PDO در reconnectخطای مکرر بدون الگوی زمانیتنظیمات PDO

در پست‌های جداگانه‌ای که نوشته‌ام، هم خطای Can't connect to MySQL server را باز کرده‌ام و هم خطای اتصال به دیتابیس در وردپرس را. تفاوت این سه دقیقاً همان چیزی است که خیلی‌ها با هم قاطی می‌کنند: «connect» یعنی هرگز وصل نشدی، «gone away» یعنی وصل بودی و وسط کار قطع شد.

تشخیص: قبل از هر تغییر، مقصر را پیدا کنید

عادت من در پروژه‌ها این است: پیش از عوض کردن هر تنظیمی، سه چیز را ثبت می‌کنم.

  1. الگوی زمانی: خطا فقط در ساعت خاصی می‌آید؟ یا در نقطهٔ خاصی از کد؟ یا بعد از ایمپورت‌های بزرگ؟
  2. منابع سرور: در لحظهٔ خطا، مصرف RAM و CPU سرویس MySQL چه شکلی بوده؟
  3. لاگ MySQL: فایل mysql_error.log تقریباً همیشه حرف آخر را می‌زند.

یک ابزار خانگی و بسیار ساده که کمکم کرد: یک فایل probe.php بسازید که هر پنج دقیقه یک SELECT 1 بزند و زمان و پاسخ را در فایل ذخیره کند. اگر خطا در همان بازه ظاهر شد، حالا می‌دانید دقیقاً کِی اتفاق افتاده و چه چیزی همزمان در سایت اجرا می‌شده. اگر خطا با کندی سایت همراه است، نگاهی هم به عیب‌یابی سرعت سایت بیندازید؛ این دو مشکل اغلب با هم می‌آیند.

در عیب‌یابی MySQL، لاگ سرور حقیقت را می‌گوید؛ پیام خطای PHP فقط سرنخ است.

راه‌حل اول: تنظیم max_allowed_packet

max_allowed_packet بزرگ‌ترین بسته‌ای است که MySQL قبول می‌کند. مقدار پیش‌فرض در بعضی سرورها فقط ۴ مگابایت است؛ یک ایمپورت دیتابیس یا درج یک فایل SQL حجیم همین‌جا می‌ترکد. اگر نشانهٔ شما «خطا در حین ایمپورت یا درج دادهٔ حجیم» است، اینجا شروع کنید:

[mysqld]
max_allowed_packet = 64M

در سرورهایی که به my.cnf دسترسی ندارید، با یک کوئری سراسری هم می‌توان مقدار را بالا برد:

SET GLOBAL max_allowed_packet = 67108864;

نکته: این تغییر تا ری‌استارت بعدی می‌ماند مگر در my.cnf ثبتش کنید. یادآوری می‌کنم که کوئری‌های حجیم روی جدول‌های درهم‌ریخته بیشتر دردسر می‌سازند؛ مقایسه‌شان را در بهینه‌سازی جدول‌های MySQL آورده‌ام.

راه‌حل دوم: wait_timeout و اتصال‌های بی‌کار

wait_timeout می‌گوید سرور چند ثانیه به یک اتصال بی‌کار فرصت می‌دهد. اگر اسکریپت PHP شما اتصال را باز می‌کند و بعد ده دقیقه کار می‌کند بدون اینکه کوئری بزند، MySQL در میانهٔ این سکوت، اتصال را می‌بندد. مقادیر پیش‌فرض در سرورهای اشتراکی گاهی ۳۰ یا ۶۰ ثانیه است؛ درحالی‌که اسکریپت شما انتظار دارد اتصال برای همیشه بماند.

[mysqld]
wait_timeout = 600
interactive_timeout = 600

اما این فقط نصف راه است. نصف مهم‌تر در کد شماست: هیچ اسکریپتی نباید انتظار داشته باشد اتصالی که باز کرده، همیشه باز بماند. برای پروژه‌های طولانی (کرون‌ها، ایمپورت‌ها، اسکریپت‌های گزارش‌گیری)، پیش از هر عملیات سنگین یک «ضربان» ساده بزنید: SELECT 1. همین یک کوئری، تایمر سرور را ریست می‌کند. اگر خطا با تراکم اتصال‌ها همراه است، بررسی خطای Too many connections هم بی‌فایده نیست.

راه‌حل سوم: اصلاح کد PHP و PDO

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

  1. اتصال‌ها را کوتاه‌عمر نگه دارید. برای اسکریپت‌های وب، هر درخواست یک اتصال تازه بگیرد و در پایان ببندد.
  2. reconnect در PDO را آگاهانه مدیریت کنید. PDO به‌صورت پیش‌فرض دوباره وصل نمی‌شود؛ اگر لازم دارید، در catch خطای از دست رفتن اتصال، یک اتصال تازه بسازید و همان یک کوئری را دوباره بزنید.
  3. تراکنش‌های طولانی را بشکنید. هر BEGIN که هزاران ردیف درگیر می‌کند، هم قفل می‌سازد هم احتمال برخورد با timeout را بالا می‌برد. مسیر دیدن قفل‌ها را در خطای Lock wait timeout نوشته‌ام.
try {
    $row = $pdo->query("SELECT 1")->fetchColumn();
} catch (PDOException $e) {
    if (str_contains($e->getMessage(), "gone away")) {
        $pdo = new PDO($dsn, $user, $pass, $options);
        $row = $pdo->query("SELECT 1")->fetchColumn();
    }
}

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

در وردپرس و ووکامرس چه کنیم؟

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

  • ایمپورت‌های بزرگ ووکامرس: با افزونه‌های ایمپورت، Batch را کوچک کنید (۱۰۰ ردیف در هر اجرا) تا بسته‌ها کوچک بمانند.
  • کرون‌های شبانه: اگر کار سنگین است، پیش از شروع هر بخش، یک SELECT 1 بزنید تا اتصال تازه شود.
  • افزونه‌های بکاپ: بعضی افزونه‌های بکاپ دیتابیس را در یک تراکنش می‌خوانند و روی دیتابیس‌های سنگین به این خطا می‌خورند؛ بکاپ را مرحله‌ای بگیرید. در پست رفع خطای اتصال به دیتابیس در وردپرس این سناریو را باز کرده‌ام.

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

اشتباهات رایج در برخورد با این خطا

  • بالا بردن wait_timeout بدون تغییر کد: فقط علامت را عقب می‌اندازد. اسکریپتی که اتصال را بی‌دلیل ساعت‌ها باز نگه می‌دارد، باز هم شکستن را یاد می‌گیرد.
  • تغییر max_allowed_packet روی همه چیز: این متغیر به ازای هر اتصال در RAM نگه داشته می‌شود؛ عددهای نجومی روی سرور اشتراکی، فاجعه است.
  • ایمپورت دیتابیس با phpMyAdmin: ابزار خوبی است ولی برای دیتابیس‌های چندصد مگابایتی، بهتر است با mysql در CLI انجام دهید.
  • نصب افزونهٔ «درمان MySQL»: این نوع افزونه‌ها معمولاً هر ساعت کوئری اضافه می‌زنند و مشکل را بدتر می‌کنند. اگر به دنبال کاهش فشار هستید، مسیرش همان است که در خطاهای رایج MySQL گفته‌ام.
هر تنظیم MySQL فقط زمانی جواب می‌دهد که ریشهٔ خطا را درست تشخیص داده باشید؛ بدون تشخیص، تنظیم فقط پنهان‌کاری است.

حرف آخر

آن شب، بعد از چند ساعت، ریشه را پیدا کردم: یک افزونهٔ ایمپورت که کل دیتابیس را در یک تراکنش می‌خواند، در کنار wait_timeout شصت ثانیه‌ای. سه تغییر کوچک — تقسیم ایمپورت به بخش‌های کوچک، ضربان SELECT 1 قبل از هر بخش، و افزایش max_allowed_packet تا ۳۲ مگابایت — کافی بود که آن ایمیل شبانه برای همیشه قطع شود. اگر امروز با این خطا درگیرید، توصیهٔ عملی من سه چیز است: لاگ سرور را جدی بگیرید، الگوی زمانی خطا را ثبت کنید، و پیش از هر تنظیمی یک بار از خودتان بپرسید «اتصال کجا و چرا می‌شکند؟». همین سوال، نصف راه است.

اگر این خطا را در یک پروژهٔ واقعی دیده‌اید و ریشه‌اش جایی بود که در این مقاله مطرح نشده، در دیدگاه‌ها برایم بنویسید. تجربهٔ هر کسی که با دیتابیس‌های سنگین کار کرده، برای خوانندهٔ بعدی ارزشمند است. 🛠️