دسته بندی:

APC، XCache، Memcached و Varnish؛ کدام کش برای سرور شما مناسب است؟

سایت کند شده، مصرف CPU سرور بالا رفته و کسی به شما گفته «یک کش نصب کن». حالا چهار اسم جلوی شماست: XCache، APC، Memcached و Varnish. سؤال درست این نیست که کدام‌یک بهتر است؛ سؤال این است که کندی از کجا می‌آید و کدام لایه از کش آن را حل می‌کند. انتخاب اشتباه در بهترین حالت هیچ تغییری در سرعت نمی‌دهد و در بدترین حالت صفحه‌ی یک کاربر لاگین‌شده را به کاربر دیگری نشان می‌دهد.

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

این چهار ابزار رقیب هم نیستند

مهم‌ترین نکته‌ای که معمولاً در معرفی‌ها گفته نمی‌شود این است: این‌ها سه لایه‌ی متفاوت از کش هستند و در بسیاری از سرورها هر سه لایه هم‌زمان فعال‌اند.

  • کش opcode — XCache، APC و OPcache. نتیجه‌ی کامپایل کدهای PHP را در حافظه نگه می‌دارند تا PHP در هر درخواست دوباره فایل‌ها را پارس و کامپایل نکند. روی مصرف CPU اثر می‌گذارد، نه روی بار پایگاه داده.
  • کش داده یا آبجکت — بخش user cache در APC، و همچنین APCu، Memcached و Redis. نتیجه‌ی کوئری‌ها و محاسبات سنگین را نگه می‌دارند تا بار از روی دیتابیس برداشته شود. برنامه باید صریحاً کد استفاده از آن‌ها را داشته باشد؛ صرف نصب سرویس هیچ اتفاقی نمی‌افتد.
  • کش HTTP — Varnish. پاسخ کامل صفحه را نگه می‌دارد، طوری که درخواست بعدی اصلاً به PHP و دیتابیس نمی‌رسد. بیشترین سود را دارد و بیشترین ریسک را هم.

پس «Varnish یا Memcached؟» سؤال بی‌معنایی است. سؤال درست این است: گلوگاه شما کامپایل PHP است، کوئری دیتابیس است، یا تولید مکرر یک صفحه‌ی یکسان برای بازدیدکننده‌های ناشناس؟

XCache

XCache یک opcode کش open-source است؛ یعنی عملکرد PHP روی سرور را تسریع می‌بخشد. روش بهینه‌سازی این است که زمان compile کدهای PHP را حذف می‌کند، از طریق cache کردن کدهای compile شده‌ی قبلی در RAM. در معرفی‌های رسمی این پروژه گفته می‌شد که این فرایند زمان اجرا را تا حدود ۵ برابر بهبود می‌دهد و باعث کاهش server load می‌شود؛ عدد واقعی به این بستگی دارد که چه سهمی از زمان درخواست شما صرف کامپایل شدن کد می‌شود.

وضعیت امروز XCache

باید صریح باشیم: XCache دیگر نگهداری نمی‌شود و هیچ نسخه‌ی پایداری برای PHP 7 و بالاتر منتشر نکرد. اگر در آموزشی از سال‌های گذشته به آن برخوردید، جایگزین امروزی‌اش Zend OPcache است که از PHP 5.5 به بعد همراه خود PHP عرضه می‌شود و روی اکثر بیلدها به‌صورت پیش‌فرض فعال است. هیچ دلیلی برای نصب XCache روی یک سرور جدید وجود ندارد.

برای بررسی فعال بودن OPcache:

php -v
php -m | grep -i opcache
php -i | grep -E 'opcache.enable|opcache.memory_consumption|opcache.max_accelerated_files'

خروجی php -v باید خطی شبیه with Zend OPcache داشته باشد. یک هشدار مهم: این دستورها وضعیت خط فرمان را نشان می‌دهند، نه وضعیت PHP-FPM. روی Ubuntu و Debian این دو مجموعه تنظیمات کاملاً جدا هستند (/etc/php/8.x/cli/ در برابر /etc/php/8.x/fpm/)، بنابراین برای سایت واقعی باید خروجی یک فایل phpinfo() یا php-fpm -i را ببینید.

فایل تنظیمات معمولاً در /etc/php.d/10-opcache.ini روی AlmaLinux و Rocky Linux و در /etc/php/8.x/fpm/conf.d/10-opcache.ini روی Ubuntu و Debian است. در دایرکت‌ادمین از طریق CustomBuild و در cPanel از طریق EasyApache 4 و MultiPHP INI Editor فعال و تنظیم می‌شود.

APC

APC (مخفف Alternative PHP Cache) یک framework رایگان و open source است که کدهای PHP را در shared memory ذخیره می‌کند. علاوه بر کش opcode، یک user cache هم برای ذخیره‌ی اطلاعات application فراهم می‌آورد — یعنی برنامه می‌تواند هر مقداری را با کلید در حافظه بگذارد و بعداً بخواند.

APC امروز به چه چیزی تبدیل شده است

APC هم مثل XCache متوقف شده؛ توسعه‌اش روی PHP 5.4 ماند و هرگز نسخه‌ی پایداری برای PHP 5.5 و بالاتر نگرفت. کارکردهایش بین دو پروژه تقسیم شد:

  • بخش opcode به OPcache رسید که همراه خود PHP عرضه می‌شود.
  • بخش user cache به افزونه‌ی مستقلی به نام APCu منتقل شد. APCu فقط کش داده است و هیچ کش opcode ندارد.

در نتیجه توابع قدیمی مثل apc_store() و apc_fetch() روی PHP 7 و ۸ وجود ندارند و کد قدیمی با خطای Call to undefined function apc_store() متوقف می‌شود. راه‌حل، بازنویسی با توابع apcu_store() و apcu_fetch() است؛ اگر کد قدیمی قابل تغییر نیست، افزونه‌ی سازگاری apcu_bc نام‌های قدیمی را برمی‌گرداند، ولی این یک راه‌حل موقت است نه دائمی.

php -m | grep -i apcu
php -r 'var_dump(function_exists("apcu_store"));'

نکته‌ی مهم درباره‌ی APCu: حافظه‌اش محلی و مخصوص همان ماشین است. اگر دو وب‌سرور پشت لودبالانسر دارید، هر کدام کش جدا و ناهماهنگ خودش را دارد. برای همین APCu برای ذخیره‌ی session در حالت چندسروره مناسب نیست، اما برای یک سرور تکی سریع‌ترین گزینه است چون هیچ رفت‌وبرگشت شبکه‌ای ندارد.

Memcached

Memcached یک سیستم high-performance و توزیع‌شده‌ی object caching در حافظه است که برای بالا بردن سرعت برنامه‌های dynamic web application از طریق کاهش بار پایگاه داده استفاده می‌شود. برخلاف APCu، یک سرویس شبکه‌ای مستقل است که پیش‌فرض روی پورت 11211 گوش می‌دهد، بنابراین چند وب‌سرور می‌توانند یک کش مشترک داشته باشند.

یک ابهام نام‌گذاری که وقت زیادی از افراد می‌گیرد: memcached نام سرویس است، اما در PHP دو افزونه‌ی جداگانه با نام‌های memcache و memcached وجود دارد. این‌ها یکی نیستند و API متفاوتی دارند؛ افزونه‌ی memcached که بر پایه‌ی libmemcached است گزینه‌ی امروزی و کامل‌تر به شمار می‌آید. اگر برنامه یکی را می‌خواهد و شما آن یکی را نصب کرده‌اید، کش بی‌سروصدا کار نخواهد کرد.

قبل از بالا آوردن سرویس، اول آن را ببندید

این ترتیب را جابه‌جا نکنید. Memcached هیچ احراز هویتی به‌صورت پیش‌فرض ندارد و روی AlmaLinux و Rocky فایل /etc/sysconfig/memcached به‌صورت پیش‌فرض OPTIONS="" دارد؛ یعنی اگر همین‌طور استارتش کنید روی همه‌ی اینترفیس‌ها — از جمله IP عمومی سرور — گوش می‌دهد. روی Ubuntu و Debian فایل /etc/memcached.conf پیش‌فرض -l 127.0.0.1 دارد که وضعیت بهتری است، ولی باز هم تأییدش کنید. اول این فایل را اصلاح کنید و بعد سرویس را فعال کنید:

# AlmaLinux / Rocky Linux → /etc/sysconfig/memcached
OPTIONS="-l 127.0.0.1 -U 0"

# Ubuntu / Debian → /etc/memcached.conf
-l 127.0.0.1
-U 0

سوئیچ -U 0 پروتکل UDP را می‌بندد؛ UDP باز همان چیزی است که سرورها را به بازتاب‌دهنده‌ی حملات amplification تبدیل می‌کند. نسخه‌های جدید memcached پیش‌فرض UDP را غیرفعال می‌کنند، اما این را خودتان تأیید کنید. اگر چند وب‌سرور دارید، به‌جای 127.0.0.1 فقط IP شبکه‌ی خصوصی داخلی را بگذارید و پورت ۱۱۲۱۱ را روی فایروال از اینترنت ببندید.

systemctl enable --now memcached
systemctl status memcached
ss -lntup | grep 11211
php -m | grep -i memcache

خروجی ss باید 127.0.0.1:11211 را نشان بدهد، نه 0.0.0.0:11211. روی سیستم‌های قدیمی دستور service memcached start یا /etc/init.d/memcached start استفاده می‌شد؛ این روش منسوخ است و روی توزیع‌های امروزی باید از systemctl استفاده کنید.

ماندگاری: Memcached چیزی را نگه نمی‌دارد

Memcached چیزی روی دیسک نمی‌نویسد. با ریستارت سرویس، تمام کش خالی می‌شود و وقتی حافظه پر شود داده‌های قدیمی با الگوریتم LRU حذف می‌شوند. هرگز آن را تنها محل نگهداری داده‌ای که از دست دادنش برایتان مهم است قرار ندهید — به‌خصوص session کاربران، چون یک ریستارت ساده همه را لاگ‌اوت می‌کند. حداکثر اندازه‌ی پیش‌فرض هر آیتم هم ۱ مگابایت است. اگر به ماندگاری روی دیسک یا ساختارهای داده‌ی پیچیده‌تر نیاز دارید، Redis گزینه‌ی مناسب‌تری برای همین لایه است.

Varnish

Varnish یک شتاب‌دهنده‌ی HTTP است که برای وب‌سایت‌های پویا با محتوای سنگین طراحی شده. Varnish داده را روی virtual memory ذخیره می‌کند و فایل‌های استاتیک و anonymous page-viewها را بسیار سریع‌تر و در حجم‌های بالاتر از آپاچی پردازش می‌کند. در معرفی رسمی این فناوری، افزایش سرعت ارائه‌ی اطلاعات تا حدود ۳۰۰ تا ۱۰۰۰ برابر اعلام شده است؛ این عدد مربوط به بهترین حالت ممکن است — صفحه‌ای که کاملاً از کش سرو می‌شود در برابر همان صفحه که هر بار توسط PHP ساخته می‌شود. سود واقعی روی سایت شما به این بستگی دارد که چه نسبتی از ترافیکتان قابل کش شدن است.

Varnish جلوی وب‌سرور می‌نشیند: خودش پورت ۸۰ را می‌گیرد و آپاچی یا Nginx به پورت دیگری (معمولاً ۸۰۸۰) منتقل می‌شود. توجه کنید که بسته‌های آماده‌ی Varnish معمولاً پیش‌فرض روی پورت ۶۰۸۱ تنظیم شده‌اند و این را باید خودتان تغییر دهید. رفتارش با زبانی به نام VCL در فایل /etc/varnish/default.vcl تعریف می‌شود و پارامترهای اجرا (آدرس و پورت گوش دادن و اندازه‌ی حافظه) بسته به توزیع در فایل سرویس systemd یا /etc/varnish/varnish.params تنظیم می‌شوند.

varnishd -V
varnishstat -1 | grep -E 'cache_hit|cache_miss'

برای اعمال تغییرات VCL از systemctl reload varnish استفاده کنید، نه restart. reload فقط VCL جدید را بارگذاری می‌کند و کش موجود سرِ جایش می‌ماند؛ restart کل کش را دور می‌ریزد. اگر بسته‌ی توزیع شما ExecReload ندارد، معادلش این است که VCL را با varnishadm vcl.load و varnishadm vcl.use جابه‌جا کنید.

محدودیتی که باید قبل از نصب بدانید

نسخه‌ی متن‌باز Varnish از HTTPS پشتیبانی نمی‌کند. برای سایت دارای SSL باید یک TLS terminator مثل Nginx، HAProxy یا Hitch جلوی Varnish قرار بگیرد که ترافیک را رمزگشایی کرده و به Varnish بدهد. این یعنی معماری سرور شما پیچیده‌تر می‌شود و روی سرورهای دارای کنترل‌پنل، این تغییر ساختار پورت‌ها می‌تواند با تنظیمات خودکار پنل تداخل پیدا کند. قبل از دست زدن به پورت وب‌سرور روی یک سرور زنده، از کانفیگ فعلی نسخه‌ی پشتیبان بگیرید و بدانید چطور برش می‌گردانید — اگر آپاچی روی ۸۰۸۰ بالا نیاید و Varnish هم بالا نیامده باشد، سایت به‌کلی از دسترس خارج می‌شود.

نکته‌ی دوم: Varnish به‌صورت پیش‌فرض پاسخ‌هایی که هدر Set-Cookie دارند و درخواست‌هایی که کوکی می‌فرستند را کش نمی‌کند. دقیقاً به همین دلیل است که در توضیح اصلی از «anonymous page-views» صحبت می‌شود: سود Varnish روی بازدیدکننده‌های ناشناس است، نه کاربران لاگین‌شده.

کدام یک برای کدام سناریو

  • مصرف CPU بالا و کندی کلی PHP: OPcache (جانشین XCache و بخش opcode در APC). اولین و کم‌ریسک‌ترین کاری که باید انجام دهید.
  • دیتابیس زیر فشار، کوئری‌های تکراری سنگین: APCu روی یک سرور تکی، یا Memcached و Redis وقتی چند وب‌سرور کش مشترک لازم دارند.
  • ترافیک سنگین ناشناس روی صفحات یکسان (خبری، فروشگاهی، لندینگ): Varnish.
  • پنل اشتراکی بدون دسترسی root: فقط OPcache و در بهترین حالت APCu در دسترس است.

ترتیب درست پیاده‌سازی

ترتیب اهمیت دارد، چون هر لایه ریسک و هزینه‌ی نگهداری بیشتری از قبلی دارد:

  • اول اندازه‌گیری. با top، لاگ کوئری‌های کند MySQL و ابزار profiling مشخص کنید گلوگاه کجاست. نصب کش بدون دانستن گلوگاه، حدس زدن است.
  • OPcache را فعال و تنظیم کنید. رایگان، بدون تغییر کد، کم‌ریسک.
  • کش داده را اضافه کنید. این مرحله نیاز به پشتیبانی برنامه دارد؛ در وردپرس با یک افزونه‌ی object cache و در Laravel با تغییر درایور کش انجام می‌شود.
  • Varnish در آخر. نیاز به root، تغییر پورت وب‌سرور، TLS terminator و نوشتن VCL دارد. روی سایتی که هنوز کوئری‌های کندش را درست نکرده، Varnish فقط مشکل را پنهان می‌کند.

چطور مطمئن شویم کش واقعاً کار می‌کند

برای OPcache، وضعیت زنده را با یک اسکریپت PHP بررسی کنید. این فایل را در مسیر قابل دسترس عموم نگذارید و بعد از تست حتماً حذفش کنید:

<?php
$s = opcache_get_status(false);
if ($s === false) {
    exit("OPcache در این پروسه فعال نیست (یا opcache.restrict_api اجازه نمی‌دهد)n");
}
$st = $s['opcache_statistics'];
echo "hits: {$st['hits']}n";
echo "misses: {$st['misses']}n";
echo "oom_restarts: {$st['oom_restarts']}n";
echo "keys: {$st['num_cached_keys']} / {$st['max_cached_keys']}n";
echo "cache_full: " . var_export($s['cache_full'], true) . "n";

نسبت hits به misses باید به‌سرعت بالا برود. این دو عدد را جدا از هم بخوانید: اگر oom_restarts صفر نیست یعنی حافظه‌ی مشترک کم است و باید opcache.memory_consumption را افزایش دهید؛ اما اگر num_cached_keys به max_cached_keys رسیده و cache_full برابر true است، مشکل تعداد اسلات فایل‌هاست و باید opcache.max_accelerated_files را بالا ببرید. بعد از هر تغییر در این مقادیر، PHP-FPM را reload کنید.

برای Memcached، آمار سرویس را بگیرید و نسبت get_hits به get_misses را ببینید:

printf 'statsrnquitrn' | nc 127.0.0.1 11211 | grep -E 'get_hits|get_misses|curr_items|evictions'

فرستادن quit باعث می‌شود اتصال خودش بسته شود؛ به سوئیچ -q که در آموزش‌های قدیمی می‌بینید نیاز نیست و اتفاقاً روی AlmaLinux و Rocky که nc در آن‌ها همان nmap-ncat است، این سوئیچ اصلاً وجود ندارد و دستور با خطا برمی‌گردد. اگر get_hits همیشه صفر است یعنی برنامه اصلاً با کش حرف نمی‌زند. اگر evictions مدام بالا می‌رود، حافظه‌ی Memcached کم است.

برای Varnish، ساده‌ترین راه اضافه کردن یک هدر تشخیص hit/miss به انتهای /etc/varnish/default.vcl است (فایل از قبل با خط vcl 4.1; شروع شده؛ فقط این بلوک را به آن اضافه کنید):

sub vcl_deliver {
    if (obj.hits > 0) {
        set resp.http.X-Cache = "HIT";
    } else {
        set resp.http.X-Cache = "MISS";
    }
}

بعد systemctl reload varnish بزنید و یک URL را دو بار با curl -I صدا بزنید. بار دوم باید X-Cache: HIT و مقدار Age بزرگ‌تر از صفر ببینید. varnishlog و varnishstat هم برای دیدن دلیل کش نشدن یک درخواست به کار می‌آیند. این هدر را روی سایت زنده دائمی نگذارید؛ ساختار کش شما را به بیرون لو می‌دهد.

خطاهای رایج

  • کد جدید بالا نمی‌آید. اگر opcache.validate_timestamps=0 گذاشته‌اید، OPcache تغییر فایل‌ها را نمی‌بیند. بعد از هر انتشار باید systemctl reload php-fpm بزنید یا opcache_reset() را صدا کنید. روی Ubuntu و Debian نام سرویس نسخه‌دار است، مثل php8.3-fpm؛ نام دقیق را با systemctl list-units | grep fpm پیدا کنید.
  • دو کش opcode هم‌زمان. فعال کردن XCache یا APC در کنار OPcache باعث رفتار غیرقابل پیش‌بینی و کرش می‌شود. فقط یکی باید فعال باشد.
  • نصب کردم ولی هیچ فرقی نکرد. Memcached و APCu تا وقتی کد برنامه از آن‌ها استفاده نکند هیچ کاری نمی‌کنند. نصب سرویس تنها نیمی از کار است.
  • صفحه‌ی یک کاربر به کاربر دیگر نشان داده می‌شود. خطرناک‌ترین اشتباه Varnish: پیکربندی VCL که کوکی‌ها را بدون شرط حذف می‌کند، صفحه‌ی حساب کاربری یک نفر را کش کرده و به بقیه می‌دهد. مسیرهای لاگین، سبد خرید، پرداخت و پنل کاربری را صریحاً از کش خارج کنید و قبل از انتشار با دو مرورگر جداگانه تست کنید.
  • ریستارت Varnish کل کش را پاک می‌کند و همه‌ی ترافیک یک‌باره به بک‌اند می‌ریزد. روی سایت پرترافیک این کار را در ساعت اوج انجام ندهید.
  • سنجیدن با CLI. کش opcode در حالت CLI عملاً بی‌فایده است چون هر اجرا پروسه‌ی جداست. تست را روی php-fpm انجام دهید.

چه زمانی سراغ این ابزارها نروید

کش راه‌حل کندی تکرارشونده است، نه کندی ذاتی. اگر یک کوئری بدون ایندکس چند ثانیه طول می‌کشد، کش فقط اولین کاربر را قربانی می‌کند و مشکل را از دید شما پنهان می‌کند؛ اول کوئری را درست کنید.

همچنین اگر ترافیک سایت پایین است و سرور بیکار، Varnish فقط یک لایه‌ی اضافه برای خراب شدن اضافه می‌کند. اگر تمام صفحات برای هر کاربر شخصی‌سازی شده‌اند، Varnish تقریباً هیچ چیزی برای کش کردن ندارد. و اگر مشکل شما تحویل فایل‌های استاتیک است، یک CDN یا تنظیم درست هدرهای Cache-Control ساده‌تر و کم‌دردسرتر از راه‌اندازی Varnish است.

جمع‌بندی

از این چهار اسم، دوتایشان تاریخی شده‌اند: XCache و APC متوقف‌اند و روی PHP امروزی اجرا نمی‌شوند — جای اولی را OPcache و جای دومی را OPcache به‌علاوه‌ی APCu گرفته است. دو اسم دیگر همچنان زنده و پرکاربردند: Memcached برای کش داده در محیط چندسروره و Varnish برای کش کامل صفحات ناشناس. ترتیب درست هم همیشه یکسان است: اول گلوگاه را پیدا کنید، بعد OPcache، بعد کش داده، و Varnish را برای آخر بگذارید.