بدترین نوع خرابی در وردپرس، خطایی نیست که صفحه را سفید میکند. بدترین نوع، آن است که هیچ اتفاقی نمیافتد: فایل را آپلود میکنید، نسخهٔ تازه را در فایلمنیجر میبینید، sha256sum دو طرف برابر است، افزونهٔ کش هم پیام میدهد که پاسخ از حافظه سرو نشده، اما رفتار سایت دقیقاً همان رفتار قبلی است. در این وضعیت تقریباً همهچیز را متهم میکنید: کش لایتاسپید، کش مرورگر، CDN، نسخهٔ PHP، حتی خودِ وردپرس. ساعتها وقت میگذارید تا بفهمید چیزی که اجرا میشود فایل روی دیسک نیست.
این حالت، اثر جانبی یک تنظیم درست و مشروع است. در ادامه توضیح میدهم OPcache چه میکند، validate_timestamps چه چیزی را خاموش میکند، چطور در پنج دقیقه وضعیت هاست خودتان را بخوانید، و چطور workerهای PHP فقط یک اکانت را بدون قطعی بقیهٔ سایتها ریاستارت کنید.
OPcache دقیقاً چه کاری انجام میدهد
PHP هر بار که یک فایل را اجرا میکند، ابتدا آن را میخواند، تجزیه میکند و به bytecode تبدیل میکند؛ بعد bytecode اجرا میشود. مرحلهٔ تجزیه، هیچ ربطی به منطق برنامهٔ شما ندارد و صرفاً هزینه است: خواندن فایل از دیسک، تبدیل متن به AST، و تولید دستورهای ماشین مجازی. برای یک فایل قالب یا افزونهٔ بزرگ، این بخش میتواند درصد قابلتوجهی از زمان پردازش هر درخواست باشد، آن هم در حالی که محتوا و خروجی هیچ تغییری نکرده است.
OPcache همان مرحلهٔ تجزیه را حذف میکند. اولین باری که یک فایل اجرا میشود، bytecode آن در یک ناحیهٔ حافظهٔ مشترک ذخیره میشود. از آن به بعد، درخواستهای بعدی مستقیم همان bytecode را اجرا میکنند و دیگر فایل از دیسک خوانده و تجزیه نمیشود. نتیجه روی سایتهای پرتراکم محسوس است؛ بهخصوص روی هاستهای اشتراکی که ورودی/خروجی دیسک گرانتر از CPU است.
نکتهای که معمولاً از قلم میافتد این است که کش سطح PHP با کش سطح صفحه متفاوت است. افزونههای کش وردپرس، خروجی نهایی HTML را ذخیره میکنند تا اصلاً PHP اجرا نشود. OPcache هم وقتی به کار میآید که PHP اجرا شود؛ یعنی حتی اگر صفحهای کاملاً کشنشده باشد یا یک درخواست پستباکد به admin-ajax.php بزنید، باز هم OPcache در مسیر است. پس پاکردن کشِ افزونهٔ کش، هیچ اثری روی OPcache ندارد. همین تفکیک، اولترین چیزی است که در این نوع عیبیابی گیجکننده میشود.
validate_timestamps و revalidate_freq یعنی چه
حالا سؤال طبیعی: اگر bytecode در حافظه است، چطور PHP میفهمد فایل عوض شده؟ پاسخش دو گزینه است.
گزینهٔ اول opcache.validate_timestamps است. وقتی روی 1 باشد، PHP پیش از استفاده از نسخهٔ کششده، زمان تغییر فایل روی دیسک را با زمانِ ثبتشده در کش مقایسه میکند. اگر فایل تازهتر باشد، دوباره تجزیه میشود. این رفتاری است که در بیشتر نصبها میبینید و برای محیط توسعه هم الزامی است.
گزینهٔ دوم opcache.revalidate_freq است و میگوید این مقایسهٔ زمانی هر چند ثانیه یک بار انجام شود. مقدار پیشفرض معمولاً شصت است؛ یعنی حتی اگر فایل عوض شود، تا یک دقیقه ممکن است نسخهٔ قدیمی اجرا شود. در یک سایتی که روزی چند بار بهروزرسانی میشود، این تأخیر عادی و بیخطر است.
مسئله از جایی شروع میشود که validate_timestamps را روی 0 میگذارند. این کار کاملاً عمدی و برای عملکرد انجام میشود: اگر مقایسهٔ زمانی حذف شود، در هر درخواست یک stat() از دیسک کم میشود و دیگر هیچ فایل تازهای هرگز خودبهخود کش را بازنویسی نمیکند. روی سرورهای پرترافیک، تفاوت اندازهگیریشدنی است. بهای آن هم روشن است: هیچ تغییر فایلی، تا زمانی که خودتان کش را خالی نکنید، اثر نمیکند. آپلود فایل، جایگزینی افزونه، ویرایش قالب، حتی بازگشت از نسخهٔ پشتیبان؛ هیچکدام دیده نمیشود.
بدترین ویژگی این تنظیم، بیصدا بودن آن است. نه خطایی در لاگ ثبت میشود، نه هشداری در پستان نمایش داده میشود، نه صفحهای خراب میشود. سایت درست کار میکند؛ فقط نسخهای را که دیگر روی دیسک نیست.
نشانهها: فایل درست است، خروجی قدیمی
علامتهایی که مجموعهشان تقریباً همیشه به همین مورد اشاره میکند:
- فایل را دوباره آپلود میکنید و باز هم تغییری نمیبینید؛ حتی اگر کل پوشه را پاک کنید و از نو بگذارید.
- مجموعهی هش فایل روی سرور با هش همان فایل در بستهٔ آپلودی شما برابر است.
- در هدر پاسخ، سروکنندهٔ کش میگوید این پاسخ از کش نبوده؛ ولی باز هم رفتار قدیمی است.
- صفحهای که منطقش در فایل PHP نیست، مثلاً خروجی یک تابع تغییرکرده، همانطور قبلی است.
- در فضای سرور، فایلهای
.phpبا تاریخ و اندازهٔ درست دیده میشوند. - اگر همان فایل را با دستور خط فرمان اجرا کنید، نتیجهٔ جدید را میگیرید.
مورد آخر، مهمترین آزمون تشخیصی است. وقتی php -l یا اجرای CLI یک فایل، نتیجهٔ تازه میدهد اما صفحهٔ وب نتیجهٔ قدیمی، آنگاه فایل و مسیر و کاربر و دسترسیها سالماند؛ تنها چیزی که فرق میکند این است که فرآیند وب، bytecode قدیمی را در حافظه دارد. روی همان سروری که این حالت را تجربه کردیم، نسخهٔ PHP یک اکانت opcache.validate_timestamps=1 داشت و نسخهٔ PHP اکانت دیگر، همان گزینه را 0. یعنی رفتار بین دو سایت روی یک سرور متفاوت بود، و دقیقاً همین باعث شد حدس اول اشتباه باشد: فکر میکردیم تنظیم سرور یکسان است.
چطور وضعیت OPcache را روی هاست خودتان بخوانید
سریعترین راه، فایل phpinfo() نیست؛ آن هم کار میکند ولی در محیط اشتراکی ریسک افشای اطلاعات دارد. راه دقیقتر، پرسش از خودِ PHP است.
از خط فرمان، با همان باینری که اکانت شما استفاده میکند:
php -i | grep -E 'opcache.enable|validate_timestamps|revalidate_freq|memory_consumption|max_memory'اگر خروجی چیزی نشانتان نداد، یعنی OPcache برای آن CLI روشن نیست؛ که خودش خبر مهمی است، چون یعنی CLI و وب از دو پیکربندی متفاوت اجرا میشوند.
روی سیپنل، نسخهٔ PHP هر اکانت جداگانه انتخاب میشود، پس فایل php.ini هر نسخه جای خودش را دارد. مسیرها معمولاً به این شکلاند:
/opt/cpanel/ea-php82/root/etc/php.d/10-opcache.ini
/opt/cpanel/ea-php84/root/etc/php.d/10-opcache.iniفایل را باز کنید و این سه خط را ببینید. اگر کنار opcache.enable=1 مقدار opcache.validate_timestamps=0 دیدید، رفتار سایت شما توضیح پیدا کرده است. روی همان سرور، نسخهٔ 84 مقدار 1 و revalidate_freq=60 داشت و نسخهٔ 83 مقدار 0. تفاوت همین یک خط، دو سایت را از نظر رفتار دیپلوی کاملاً متفاوت میکرد.
یک نکتهٔ عملی: تنظیمات OPcache را از طریق پنل وردپرس یا افزونهٔ کش نمیشود عوض کرد. هر افزونهای که ادعای «پاک کردن کش OPcache» با یک دکمه دارد، یا در واقع چیز دیگری را پاک میکند یا به opcache_reset() نیاز دارد که روی بسیاری از هاستهای اشتراکی غیرفعال است. پس اگر قرار است این کش را مدیریت کنید، باید دسترسی به فایل php.ini همان نسخهٔ PHP داشته باشید.
چطور مطمئن شوید کدام نسخه از PHP در حال اجراست
قبل از اینکه نتیجه بگیرید، باید مطمئن شوید همان باینری را میخوانید که صفحه را سرو میکند. روی LiteSpeed، پروسهٔ اجراکنندهٔ PHP معمولاً با نام کاربری خودِ اکانت اجرا میشود. پیدا کردنش یک ترفند دارد:
pgrep -u YOUR_ACCOUNT lsphp
ls -l /proc/$(pgrep -n -u YOUR_ACCOUNT lsphp)/exeخط دوم، باینری واقعی را نشان میدهد؛ چیزی مثل /opt/cpanel/ea-php83/root/usr/bin/lsphp. چرا این کار لازم است؟ چون ps روی این پروسهها فقط عبارت lsphp را چاپ میکند، چون آرگومانهای خط فرمان بازنویسی میشوند. اگر سعی کنید با فیلتر روی نام کامل پروسه آنها را پیدا کنید، فهرست خالی میبینید و نتیجه میگیرید که اصلاً workerی در کار نیست؛ در حالی که هست و با نام کاربری درست هم اجرا میشود.
وقتی باینری را شناختید، همان را مستقیم بپرسید:
/opt/cpanel/ea-php83/root/usr/bin/php -i | grep validate_timestampsاین، همان تنظیمی است که صفحهٔ شما تحت تأثیر آن است. اگر بین این مقدار و مقدار فایل php.ini تفاوت دیدید، یک php.ini محلی در پوشهٔ سایت یا تنظیم surfaces در پنل، مقدار را override میکند.
ریاستارت workerهای یک اکانت، بدون قطع بقیه
وقتی فهمیدید کش bytecode قدیمی است، راهحل استاندارد این است که workerهای PHP را طوری ببندید که LiteSpeed در درخواست بعدی آنها را از نو بالا بیاورد؛ و اینبار bytecode از فایل نو ساخته میشود.
pkill -u YOUR_ACCOUNT lsphpاین، دقیقترین عملیاتی است که میتوان انجام داد: فقط پروسههای متعلق به یک نامکاربری را میبندد، بقیهٔ اکانتهای روی سرور را دست نمیزند، و چون وبسرور خودش worker را دوباره میسازد، هیچ قطعی معناداری برای کاربر ایجاد نمیشود. بعد از آن، یک بار صفحه را باز کنید و رفتار تازه را ببینید.
دو احتیاط لازم است. اول، این دستور باید با همان نامکاربری اکانت یا از دسترسی مدیریتی سرور اجرا شود؛ اگر روی سرور دیگری باشید که آن نامکاربری را نمیشناسد، با «invalid user name» مواجه میشوید و هیچ اتفاقی نمیافتد. علامت این اشتباه معمولاً این است که دستور اجرا میشود، چیزی چاپ نمیکند، و رفتار سایت هم عوض نمیشود. پس پیش از هر چیز، مسیر فعلی و فهرست اکانتهای سرور را ببینید و از درستی میزبان مطمئن شوید.
دوم، اگر چند سرور با یک نام میزبان دارید، قضاوت بر اساس hostname گمراهکننده است. اولین خط هر مجموعهدستور باید چیزی مثل این باشد:
hostname; pwd; ls -d /home/*/ | headاگر اکانت هدف در فهرست نبود، همانجا توقف کنید. اجازه ندهید هر دستور بهطور جداگانه شکست بخورد و شما در پایان با شش خط خطای نامفهوم مواجه شوید.
چرا نباید کل وبسرور را ریاستارت کنید
وسوسهٔ معمول این است که راحتترین کار را بکنند: سرویس وبسرور را restart کنند تا همهچیز تازه شود. روی سروری که فقط یک سایت دارد، این قابلقبول است. روی سرور اشتراک یا سروری که چند سایت شما رویش است، نه.
ریاستارت کامل وبسرور، کش صفحات همهٔ سایتها را میزند، اتصالهای باز را میبندد، و برای چند ثانیه همهٔ سایتها را بدون پاسخ میگذارد. اگر آن سرور میزبان چند سرویس مهم شماست، این هزینهٔ بیمورد است؛ چون مشکل شما با بستن workerهای یک کاربر حل میشد.
یک تلهٔ عملیاتی هم اینجا هست: روی نصبهای استاندارد لایتاسپید، نام سرویس lshttpd است و پروسهٔ اصلی ممکن است با نام litespeed اجرا شود. اگراز روی عادت با pidof lsws بپرسید، خروجی خالی میگیرید و ممکن است نتیجه بگیرید وبسرور از کار افتاده، در حالی که سالم است و فقط نام را اشتباه پرسیدهاید. برای دیدن وضعیت:
systemctl status lshttpdپس قانون ساده: برای تازهکردن کد PHP یک اکانت، pkill -u <user> lsphp؛ و restart سرویس وب را فقط وقتی انجام دهید که واقعاً تنظیمات وبسرور عوض شده است.
تنظیم درست: پروداکشن، استیجینگ و محیط توسعه
بعد از حل مشکل، سؤال این است که مقدار درست چیست. گزینهای که باعث این درد شد، تنظیم بدی نیست؛ فقط برای محیط اشتباهی نگه داشته شده.
روی پروداکشن با ترافیک جدی، validate_timestamps=0 منطقی است، به شرطی که فرآیند استقرار شما «خالیکردن کش PHP» را بهعنوان یک مرحلهٔ رسمی داشته باشد. یعنی دیپلوی تمام نمیشود مگر با بستن workerهای همان اکانت و یک تأیید پس از استقرار. اگر چنین مرحلهای ندارید، این تنظیم برای شما خطرناکتر از سودش است و بهتر است validate_timestamps=1 با revalidate_freq=60 نگه دارید: هزینهاش یک بررسی زمانی در هر دقیقه است، و از نوع خطایی که ساعتها وقت میخورد ارزانتر است.
روی استیجینگ و محیط توسعه همیشه 1 با revalidate_freq=0. جایی که مدام فایل را عوض میکنید، کش bytecode با تأخیر، فقط مانع کار است. اگر روی یک سرور هر دو محیط وجود دارد، پیکربندی جدا برای هر نسخهٔ PHP یا دستکم یک فایل php.ini محلی در پوشهٔ استیجینگ، تفاوت را مشخص میکند.
روی هاست اشتراکی هم واقعبینانه باشید: شما کنترل php.ini اصلی را ندارید. اگر رفتار مشکوک دیدید، اول با پشتیبانی میزبان بررسی کنید که این گزینه روی چه مقدار تنظیم شده، و بعد استراتژی دیپلوی خودتان را بر پایهٔ همان واقعیت بچینید.
روش قطعی: بعد از دیپلوی، از اجرا شدن کد نو مطمئن شوید
مهمترین چیزی که این تجربه به من یاد داد، این نبود که pkill را یاد بگیرم؛ این بود که هیچ ادعای دیپلوی را بدون نشانهٔ اجراپذیر باور نکنم. «فایل آپلود شد» با «کد اجرا شد» دو چیز متفاوت است.
یک الگوی قابلاتکا این است: در تغییر خودتان، یک نشانهٔ مختصر و بیضرر بگذارید که فقط کد نو آن را تولید میکند. مثلاً یک کلاس یا شناسهٔ دادهای روی یک عنصر که در HTML ظاهر میشود، یا یک هدر پاسخ سفارشی که فقط در مسیر جدید افزوده میشود. بعد از استقرار، جستجوی همان نشانه در خروجی صفحه، پاسخ قطعی را میدهد:
- نشانه هست: کد نو در حال اجراست. اگر رفتار عوض نشده، مشکل منطق است، نه کش.
- نشانه نیست:فایل، یا مسیر اجرا، یا کش bytecode، یا نسخهٔ PHP، یا یک لایهٔ کش دیگر.
برای بررسی کش bytecode هم ابزار مستقیمی وجود دارد؛ اگر روی همان باینری PHP دسترسی داشته باشید:
php -r 'var_dump(function_exists("opcache_get_status") ? opcache_get_status(false)["opcache_enabled"] : "no opcache");'پیشنیاز دیپلویِ بیخطر هم روشن است: پیش از هر بازنویسی درجا، همان بازنویسی را روی یک کپی اجرا کنید و خروجی را ببینید. یکی از همان ابزارهای جایگزینی متنی، چون الگوی جایگزینیاش بد تفسیر شده بود، یک فایل Blade را خراب کرد؛ و چون فایل از نظر نحوی هنوز درست بود، بررسی متنی و lint از آن عبور کردند. این دقیقاً همان نوع آسیبی است که با «چکلیست ظاهری» دیده نمیشود: باید ساختار فایل تغییریافته را با نسخهٔ سالم مقایسه کنید، نه فقط قرائت چشمی.
اشتباههایی که در این مسیر مرتکب میشویم
تجربه نشان میدهد تقریباً همه در همین چند خط لغزنده میافتند.
اول، پاککردن کش صفحه بهعنوان اولین واکنش. بیضرر است ولی بیفایده؛ چون کش صفحه و کش bytecode دو لایهٔ جدااند. تا وقتی نشانهٔ اجرای کد را نگیرید، هر لایهای که پاک میکنید فقط وقت میگیرد.
دوم، نتیجهگیری از نبود خطا. در این حالت هیچ خطایی ثبت نمیشود. نبود خطا در لاگ، نشانهٔ سلامت نیست؛ نشانهٔ این است که سیستم دارد کاری را که از آن خواسته شده انجام میدهد — و آن کار، اجرای bytecode قدیمی است.
سوم، فرض یکسانبودن تنظیمات روی یک سرور. هر اکانت سیپنل میتواند نسخهٔ PHP خودش را داشته باشد و هر نسخه، فایل php.ini خودش را. اینکه یک سایت با آپلود فوراً تازه میشود و سایت دیگر نه، لزوماً نشانهٔ خرابی نیست؛ نشانهٔ دو پیکربندی متفاوت است.
چهارم، بیاعتنایی به تفاوت CLI و وب. وقتی اجرای خط فرمان نتیجهٔ نو میدهد، خیلیها همان را دلیلِ سالمبودن کل سیستم میگیرند؛ در حالی که دقیقاً برعکس، همان تفاوت، تشخیص را روشن میکند.
پنجم، ریاستارتهای زنجیرهای. اگر بعد از pkill -u رفتار عوض نشد، احتمال زیاد در سرور اشتباهی هستید یا نامکاربری را غلط نوشتهاید، نه اینکه دستور اثر نکرده باشد. تکرار کردنش با دستورهای بزرگتر، مشکل را حل نمیکند.
ششم، واکر کردن ادعاهای ابزار. اگر افزونهای میگوید «کاملاً کش را بهروز کرد»، یعنی احتمالاً چیزی را بهروز کرده که شما لازم ندارید. سؤال همیشه باید این باشد: نشانهٔ اجرای کد نو در خروجی کجاست.
جمعبندی و چکلیست
اگر فایل را آپلود کردهاید و سایت هنوز رفتار قبلی را نشان میدهد، در بیشتر موارد مشکل از استقرار نیست؛ از این است که PHP نسخهٔ تجزیهشدهٔ قدیمی را در حافظه نگه داشته، چون مقایسهٔ زمانی فایل خاموش شده است. راهحل، روشن و کمهزینه است:خواندن تنظیم، شناساندن باینری واقعی، بستن workerهای همان اکانت، و گرفتن نشانهٔ اجرا.
چکلیستی که بهجای حدسزدن، ترتیب دارد:
۱. از درستی میزبان و اکانت مطمئن شوید: نام میزبان، مسیر فعلی، فهرست اکانتها. ۲. فایل را تأیید کنید: هش فایل روی سرور با هش فایل بستهٔ آپلودی باید برابر باشد. ۳. باینری واقعی را پیدا کنید: pgrep -u <user> lsphp و خواندن /proc/<pid>/exe. ۴. تنظیم را بخوانید: validate_timestamps و revalidate_freq برای همان نسخهٔ PHP. ۵. اگر 0 است: pkill -u <user> lsphp؛ نه restart کل وبسرور. ۶. نشانهٔ اجرا را در خروجی صفحه ببینید؛ بدون نشانه، دیپلوی را تأییدشده حساب نکنید. ۷. برای پروداکشن، خالیکردن کش PHP را بهعنوان مرحلهٔ رسمی در اسکریپت استقرار بگذارید. ۸. برای استیجینگ، validate_timestamps=1 و revalidate_freq=0.
این چند خط، تفاوت بین ساعتها شککردن در ابزارها و دو دقیقه کار واقعی است. اگر همین حالا با چنین وضعیتی روبهرو هستید، از بند یک شروع کنید و فقط وقتی به بند بعد بروید که خروجی بند قبلی را دیدهاید.