أرسل العملاء رسائل تقول إن الموقع لا يعمل. فتحناه فوجدنا بدل الصفحة الرئيسية صفحة بيضاء جافة تتوسطها جملة واحدة: Resource Limit Is Reached. لا شعار، ولا صفحة الخطأ الخاصة بنا، ولا حتى أثر للارافيل. وكان رمز الحالة 508، رقمًا لم نره قبل ذلك اليوم إلا في جدول رموز HTTP.
أول ما فعلناه كان خطأ: ذهبنا إلى storage/logs/laravel.log. فوجدناه نظيفًا تمامًا. لا استثناء ولا سطر جديد. بقينا حائرين دقائق حتى فهمنا أن الطلب لم يصل إلى لارافيل أصلًا ليسجّل شيئًا. في مكان ما قبلنا، وقبل أن يبدأ PHP بالعمل، أغلق أحدهم الباب.
هذه المقالة هي المسار الذي سلكناه بعد ذلك: ما هو الخطأ 508 ومن يصنعه، وما الحدود الأربعة التي تُحسب بشكل منفصل على الاستضافة المشتركة، وكيف نرى في cPanel أي حد قد امتلأ، والأهم من ذلك: ما الذي يلتهم الحصة فعلًا في متجر إلكتروني للملفات.
العَرَض: صفحة لا تأتي من لارافيل#
الأعراض عادة على هذا الشكل:
- صفحة بسيطة بلا تنسيق فيها النص
Resource Limit Is ReachedأوError 508. - الخطأ غير مستمر. تحديث واحد يعطي الخطأ، والتحديث التالي يعرض الموقع، وبعد دقائق يعود الخطأ.
- الملف
storage/logs/laravel.logلا يحتوي على أي سطر جديد. - أحيانًا تصبح لوحة cPanel نفسها بطيئة، ويتأخر اتصال SSH أو FTP في الاستجابة.
أول ما عليك التأكد منه هو أن ما تحصل عليه فعلًا هو 508 وليس شيئًا آخر:
curl -sI https://example.com/ | head -n 1إذا كان الناتج HTTP/1.1 508 أو HTTP/2 508 فقد انتهى الأمر. لا تبحث بعد الآن عن خطأ في شيفرتك. قد يوجد خطأ بالفعل، وقد يكون هو نفسه ما ملأ الحد، لكن ما صنع هذه الصفحة ليس شيفرتك.
خطأ 508 Resource Limit Reached مصدره CloudLinux وليس لارافيل#
تعمل معظم خوادم الاستضافة المشتركة المزوّدة بـ cPanel على نظام CloudLinux، وليس كلها؛ فإن لم تكن متأكدًا فاسأل دعم الاستضافة. يضع CloudLinux كل حساب cPanel داخل حاوية خفيفة تسمى LVE، وهي اختصار Lightweight Virtual Environment. هذه الحاوية جدار حول حسابك: كل ما تستهلكه عملياتك يُسجَّل على حسابك، وعندما يتجاوز الاستهلاك الحد، ترفض وحدة خادم الويب (mod_hostinglimits) الطلب الجديد وتعيد تلك الصفحة الثابتة.
وهناك نقطة دقيقة تسبب الالتباس: في معيار HTTP يعني الرمز 508 «اكتُشفت حلقة» وهو خاص بـ WebDAV. أما CloudLinux وcPanel فقد استعارا هذا الرمز ليقولا «لقد بلغت الحد». فإذا بحثت عن معنى 508 في توثيق HTTP فستصل إلى مكان مضلِّل.
والنتيجة العملية أن هذا الخطأ يُنشأ قبل PHP. لا try/catch الخاص بك، ولا معالج أخطاء لارافيل، ولا الـ Middleware؛ لا شيء من ذلك يعمل. ولهذا السبب يكون السجل فارغًا.
ونقطة أهم: الحد مفروض على الحساب كله وليس على موقع واحد أو سكربت واحد. فإذا كان على الحساب نفسه ووردبريس قديم وسكربت نسخ احتياطي ومتجر لارافيل جنبًا إلى جنب، فإن الثلاثة يسحبون من الكيس نفسه. وخفة تطبيقك وحدها لا تضمن شيئًا.
أربعة حدود تُحسب بشكل منفصل#
الخطأ الشائع أن نظن أن «الموارد» رقم واحد. ليس كذلك. لدى LVE أربعة عدّادات مستقلة على الأقل، ويكفي أن تبلغ أحدها:
| الحد | ما الذي يعدّه | كيف يظهر عادة |
|---|---|---|
| المعالج (CPU) | النسبة التي تستهلكها عملياتك من نواة واحدة أو أكثر | يبطؤ الموقع وتتأخر الاستجابات |
| الذاكرة الفعلية (PMEM) | مجموع الذاكرة الحقيقية (RAM) لكل عمليات الحساب | تُقتل عملية فجأة، وتظهر صفحة بيضاء أو خطأ 500 |
| العمليات الداخلة (EP) | عدد الطلبات التي تُنفَّذ في وقت واحد | صفحة 508 |
| الإدخال/الإخراج (I/O) | سرعة القراءة والكتابة على القرص | يبطؤ كل شيء وتتأخر الملفات |
بحسب إصدار CloudLinux وإعدادات الاستضافة، قد ترى عدّادات أخرى أيضًا: IOPS (عدد عمليات القرص في الثانية) وNPROC (العدد الكلي للعمليات). وهذه أيضًا مستقلة.
والآن النقطة التي كثيرًا ما لا تُذكر: الحد الذي يصنع صفحة 508 مباشرة هو عادة Entry Process. أما بقية الحدود فتملؤه بشكل غير مباشر.
السلسلة كالتالي: استعلام بطيء أو معالجة ثقيلة تجعل كل طلب يستغرق عدة ثوانٍ بدل مئتي مللي ثانية. تتراكم الطلبات فوق بعضها. يتجاوز عدد الطلبات المتزامنة حد EP. الطلب التالي يحصل على 508. أي أن ما تراه «ازدحام»، لكن ما صنعه هو «البطء». وإن اكتفيت بالبحث عن تقليل الزيارات فلم تحل المشكلة.
قراءة قسم Resource Usage في cPanel#
في cPanel، تحت قسم Metrics، يوجد خيار اسمه Resource Usage (ويظهر في بعض القوالب باسم CloudLinux LVE Manager). تعرض هذه الصفحة العدّادات نفسها المذكورة أعلاه. يختلف المسار والمظهر بحسب الإصدار، لكن ثلاثة أشياء موجودة دائمًا:
- ملخص في الأعلى يخبرك هل اصطدمت بحد ما خلال الـ 24 ساعة الماضية أم لا.
- جدول أو رسوم بيانية مقسمة حسب المورد: CPU وPhysical Memory وEntry Processes وI/O، وأحيانًا IOPS وNumber of Processes.
- عمود اسمه Faults — وهو أهم عمود في الصفحة كلها.
كل «fault» تعني اصطدامًا واحدًا بالحد. فإذا كان في عمود Faults رقم أمام Entry Processes وصفر لبقية الموارد، فقد حصلت على الجواب: المشكلة في التزامن. وإذا كانت Faults أمام Physical Memory فابحث عن معالجة ثقيلة على الذاكرة. قد يخدعك رقم الاستهلاك اللحظي لأن كل شيء يبدو هادئًا في اللحظة التي تنظر فيها؛ أما Faults فهي تاريخ، والتاريخ لا يكذب.
وللرسوم البيانية محور زمني أيضًا. خذ هذا المحور بجدية. فإذا تكررت القمم عند رأس الساعة بالضبط أو كل خمس دقائق، فمن شبه المؤكد أن مهمة Cron متورطة. وإذا كانت القمم غير منتظمة وممتدة، فالأرجح أنها زيارات أو بوتات.
الأمر lveinfo الذي يتكرر في توثيق CloudLinux لا يعمل لك على الاستضافة المشتركة، فهو يتطلب صلاحية root. وصفحة Resource Usage تعرض المعلومات نفسها بالقدر الذي يخصّك.
السبب الأول: مهمة Cron تعمل منها عدة نسخ في وقت واحد#
هذا أشيع سبب وقعنا فيه بأنفسنا. مهمة Cron تستدعي schedule:run كل دقيقة. تصبح إحدى المهام داخل المجدول ثقيلة وتستغرق أربع دقائق. في الدقيقة الثانية يعمل Cron مجددًا فتبدأ النسخة الثانية. وفي الدقيقة الرابعة تكون أربع نسخ متزامنة من المهمة نفسها جاثمة على قاعدة البيانات. فيمتلئ حدا CPU وEP معًا.
التشخيص. عدّ عملياتك. على LVE يعرض الأمر ps عملياتِ حسابك فقط، وهذا بالضبط ما نريده:
ps -u $USER -o pid,etime,pcpu,rss,args --sort=-pcpu | head -n 30انظر إلى عمود etime. فإذا رأيت عدة أسطر artisan مدة تشغيلها عدة دقائق فهذا هو الجواب. وللعدّ السريع:
ps -u $USER -o args= | grep -c '[a]rtisan schedule:run'وإذا أردت إجراء العدّ نفسه من داخل PHP فمن المرجح أن تصطدم بجدار: في أغلب الاستضافات المشتركة تكون الدالتان exec وshell_exec معطلتين. وقد كتبنا عن هذا القيد وطرق التعامل معه في مقالة disable_functions على الاستضافة المشتركة.
العلاج. ضع طبقتين. الطبقة الأولى داخل لارافيل:
$schedule->command('reports:build')
->hourly()
->withoutOverlapping(10);الرقم 10 يعني أن القفل يتحرر تلقائيًا بعد عشر دقائق، حتى إذا قُتلت عملية في منتصف عملها لا تبقى المهمة مقفلة إلى الأبد.
الطبقة الثانية خارج لارافيل، على Cron نفسه. withoutOverlapping يفيد بعد أن يكون PHP قد بدأ؛ أما إذا كان إقلاع PHP نفسه مكلفًا فستبدأ عدة نسخ مع ذلك. وهنا يحل flock المشكلة:
flock -n /home/USER/locks/schedule.lock -c '/usr/local/bin/ea-php84 /home/USER/app/artisan schedule:run' >> /home/USER/logs/schedule.log 2>&1الخيار -n يعني «إذا كان القفل محجوزًا فلا تنتظر إطلاقًا وعُد فورًا». أنشئ المجلد locks مسبقًا. واكتب مسار ملف PHP التنفيذي كاملًا؛ أما لماذا تكون كتابة php وحدها في Cron على cPanel فخًّا فقد شرحناه في مقالة PHP 8.4 في مهام Cron على cPanel.
السبب الثاني: بوتات الزحف والطلبات المتزامنة#
للمتجر الإلكتروني للملفات عادة صفحات تصفية: تصنيف ووسم وفرز وترقيم صفحات. يصنع اجتماعها فضاءً لا نهائيًا من العناوين، والبوتات تعشق الفضاء اللانهائي. يمكن لزاحف تجاري أن يرسل عدة طلبات متزامنة، وهذا وحده كفيل بملء حد EP.
التشخيص. اقرأ سجل الوصول (access log) في خادم الويب. يختلف مساره بحسب إصدار cPanel؛ وعادة هو أحد هذين: ~/access-logs/ أو ~/logs/. أكثر عناوين IP استهلاكًا:
awk '{print $1}' ~/access-logs/example.com | sort | uniq -c | sort -rn | head -n 20وأكثر وكلاء المستخدم (User-Agent) استهلاكًا:
awk -F'"' '{print $6}' ~/access-logs/example.com | sort | uniq -c | sort -rn | head -n 20وأكثر الدقائق ازدحامًا، وهذا يخبرك متى كانت القمة ويمكنك مقارنتها برسم cPanel البياني:
awk '{print substr($4,2,17)}' ~/access-logs/example.com | uniq -c | sort -rn | head -n 15إذا وجدت عنوان IP واحدًا أو بوتًا بعينه أرسل آلاف الطلبات في دقائق، فهذا هو الجواب.
العلاج. ثلاثة أمور، بهذا الترتيب:
- أغلق الفضاء اللانهائي. امنع عناوين التصفية والفرز في
robots.txtوضعcanonicalصحيحًا في الصفحات المتكررة. هذا هو الحل الجذري. - حدّد المعدل. للمسارات الثقيلة مثل البحث ضع الـ Middleware المسمى
throttleفي لارافيل:
Route::get('/search', SearchController::class)
->middleware('throttle:20,1');- إذا كان بوت معين لا قيمة له بالنسبة لك فاحجبه في
.htaccess:
RewriteEngine On
RewriteCond %{HTTP_USER_AGENT} (AhrefsBot|SemrushBot|MJ12bot|DotBot) [NC]
RewriteRule ^ - [F,L]لا تتساهل في الحجب. وكيل المستخدم قابل للتزوير، وإذا حجبت Googlebot فستخسر خسارة أغلى بكثير من ساعة 508.
السبب الثالث: استعلام بلا فهرس#
استعلام يمسح عشرة آلاف صف مسحًا كاملًا يستهلك على الاستضافة المشتركة CPU وI/O معًا. والأسوأ أن عملية PHP تنتظر طوال الوقت وتبقي مكانها محجوزًا في حد EP.
التشخيص. سجل الاستعلامات البطيئة في MySQL عادة غير متاح لك على الاستضافة المشتركة. فأنشئه بنفسك. في AppServiceProvider ولفترة قصيرة فقط لأغراض تتبع الأخطاء:
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
DB::listen(function ($query) {
if ($query->time > 300) {
Log::warning('slow query', [
'ms' => $query->time,
'sql' => $query->sql,
]);
}
});القيمة $query->time بالمللي ثانية. بعد عدة ساعات اقرأ السجل وافرز الاستعلامات المتكررة. ثم نفّذ EXPLAIN على الاستعلام نفسه:
EXPLAIN SELECT * FROM orders WHERE user_id = 42 ORDER BY created_at DESC LIMIT 20;إذا رأيت في عمود type القيمة ALL وكان key فارغًا فلا فهرس لديك.
العلاج.
ALTER TABLE orders ADD INDEX orders_user_created_idx (user_id, created_at);ترتيب الأعمدة مهم: أولًا العمود الذي تصفّي به، ثم العمود الذي تفرز به. وابحث بجانب ذلك عن مشكلة N+1؛ فحلقة foreach تُستدعى داخلها علاقة يمكنها وحدها أن تولّد مئة استعلام في طلب واحد. ولا تنسَ with().
وللصفحات التي لا تتغير بياناتها كل ثانية، يفعل التخزين المؤقت القصير العجائب:
$featured = Cache::remember('home:featured', 300, function () {
return Product::query()->featured()->limit(12)->get();
});خمس دقائق من التخزين المؤقت على الصفحة الرئيسية تعني أنه مهما بلغ عدد الزيارات في تلك الدقائق الخمس فلن تصل إلى قاعدة البيانات إلا مرة واحدة.
السبب الرابع: معالجة الصور وحد الذاكرة الفعلية#
هنا فخ دقيق. الإعداد memory_limit في PHP هو حد عملية واحدة. أما PMEM فهو حد مجموع كل عمليات الحساب. أي أن خمس عمليات رفع متزامنة، كل منها دون memory_limit بكثير، يمكنها معًا أن تملأ حد ذاكرة الحساب. لم تخالف أيٌّ منها منفردة، لكن مجموعها فعل ذلك.
والصور أكثر الأعمال المعتادة شراهةً. عندما تفتح صورة JPEG بمكتبة GD يخرج الملف من حالته المضغوطة ويتمدد في الذاكرة بمقدار «العرض × الارتفاع × أربعة بايتات» تقريبًا. صورة بأبعاد أربعة آلاف في ثلاثة آلاف بكسل، حتى لو كان ملفها ميغابايتين، تشغل في الذاكرة عشرات الميغابايتات — والضعف إن احتفظت في الوقت نفسه بالنسخة المعدَّلة الحجم.
العلاج.
- اقرأ الأبعاد بالدالة
getimagesize()قبل المعالجة، وإذا تجاوزت الحد المسموح لديك فارفض الصورة بلباقة. - أخرج المعالجة من دورة الطلب. احفظ الملف المرفوع، وأنشئ مهمة في الطابور (Queue Job)، وأعد الرد للمستخدم فورًا.
- تخلَّ عن مرجع الصورة مبكرًا (
unset($img)) لتتحرر ذاكرتها في الحال. منذ PHP 8 لم تعدimagedestroy()تفعل شيئًا، وصار التحرير موكولًا إلى عدّ المراجع. - اجعل التزامن في الطابور واحدًا. مهمتا تغيير حجم متوازيتان على استضافة مشتركة تعنيان ضعف الذاكرة.
وإذا لم تكن ImageMagick مثبتة على استضافتك واضطررت إلى بناء كل شيء بمكتبة GD، فقد ذكرنا التفاصيل والقيود في مقالة معالجة الصور بمكتبة GD.
السبب الخامس: طابور بلا قفل وجلسات في ملفات وسجل ضخم#
ثلاثة أشياء يلتهم كل منها حد الإدخال/الإخراج بصمت:
طابور بلا قفل. إذا كنت تشغّل queue:work عبر Cron دون قفل فستتكرر قصة الفقرة الأولى؛ مع فارق أن كل عامل إضافي يظل يستطلع قاعدة البيانات باستمرار. هذا النمط يعمل جيدًا على الاستضافة المشتركة:
flock -n /home/USER/locks/queue.lock -c '/usr/local/bin/ea-php84 /home/USER/app/artisan queue:work --stop-when-empty --max-time=50 --tries=3'الخيار --max-time=50 يعني أن العامل ينصرف بنفسه بعد خمسين ثانية ليأتي Cron الدقيقة التالية بعامل جديد. فالعملية الدائمة على الاستضافة المشتركة تُقتل عاجلًا أو آجلًا.
جلسات في ملفات. مع SESSION_DRIVER=file يُنشئ كل زائر ملفًا صغيرًا في storage/framework/sessions. ومع زيارات البوتات يصل هذا المجلد إلى عشرات الآلاف من الملفات، وعلى كل طلب أن يبحث في المجلد نفسه. انظر كم بلغ:
find ~/app/storage/framework/sessions -type f | wc -l
du -sh ~/app/storage/framework/sessionsإذا كان الرقم كبيرًا فانقل المشغّل (driver) إلى database (الجدول بحسب إصدار لارافيل إما موجود مسبقًا وإما يُنشأ بـ Migration) وأفرغ المجلد.
سجل ضخم. LOG_CHANNEL=single يعني أن كل شيء يذهب إلى ملف laravel.log واحد يكبر باستمرار. ملف بحجم مئات الميغابايتات يجعل كل عملية كتابة مكلفة. بدّل إلى daily وحدّد عدد أيام الاحتفاظ. وانظر إلى الحجم الحالي:
ls -lh ~/app/storage/logs/الفرق بين 508 و503 و500#
كثيرًا ما يُخلط بين هذه الثلاثة. جدول موجز:
| الرمز | من يصنعه | أول مكان تنظر فيه |
|---|---|---|
| 500 | شيفرتك — استثناء أو خطأ في البنية أو إعداد خاطئ | storage/logs/laravel.log وسجل أخطاء خادم الويب |
| 503 | وضع الصيانة في لارافيل أو عدم عمل الخلفية | وجود ملف الصيانة وحالة خدمة PHP |
| 508 | LVE على حساب cPanel الخاص بك | قسم Resource Usage في cPanel وعمود Faults |
قاعدة تشخيصية بسيطة: إذا كان سجل لارافيل فارغًا فهو ليس 500. فالخطأ 500 يترك أثرًا في الغالب دائمًا. وإذا لم تنفّذ php artisan down ولا يوجد ملف صيانة فالخطأ 503 أيضًا مستبعد.
وتحذير أخير: عندما يعطي الموقع خطأ تغريك رغبة في تشغيل APP_DEBUG لترى ما يحدث. لا فائدة منه مع 508 إطلاقًا — لأن PHP لا يعمل — وإبقاؤه مفعّلًا على موقع حي خطر أمني.
الوقاية: عادات أبقتنا تحت الحد#
- كل مهمة Cron قد تطول يجب أن تكون محمية بـ
flock. لا تضع استثناءات. - كل عمل ثقيل (صور أو تقارير أو بريد جماعي أو ملف مضغوط) يخرج من دورة الطلب.
- يُشغَّل الطابور بتزامن واحد، مع
--max-timeو--stop-when-empty. - المسارات المكلفة مثل البحث والتصفية يكون لها
throttle. - يغلق
robots.txtالفضاء اللانهائي الناتج عن التصفية. - السجل على
dailyمع احتفاظ محدود؛ والجلسات والتخزين المؤقت في قاعدة البيانات أو الذاكرة، لا في الملفات. - انظر إلى صفحة Resource Usage وعمود Faults مرة في الشهر، لا فقط عندما يتوقف الموقع.
- قبل إطلاق مشروع على استضافة مشتركة، راجع مرة واحدة قائمة فحص الاستضافة المشتركة للارافيل؛ فنصف هذه الفخاخ تُغلق هناك.
الدرس الأساسي#
الخطأ 508 ليس خطأ برنامج، بل إشعار محاسبي: الخادم يقول إن حصتك انتهت. ولهذا فالبحث عن الخطأ في سجل التطبيق عديم الجدوى، ويجب أن تبدأ من مكان آخر — من عمود Faults في cPanel الذي يخبرك أي حد من الحدود الأربعة قد امتلأ.
وعندما تعرف أي حد هو، تراجع خطوة إلى الوراء. امتلاء حد Entry Process غالبًا نتيجة لا سبب؛ والسبب هو البطء. تسريع استعلام واحد أو إخراج معالجة ثقيلة من دورة الطلب يُبعد عنك الحد أكثر من أي إعداد آخر.