بهینه‌سازی وردپرس روی سرور NVMe؛ تنظیماتی که از سخت‌افزار استفاده می‌کنند

بهینه‌سازی وردپرس روی سرور NVMe؛ تنظیماتی که از سخت‌افزار استفاده می‌کنند

سرور تازه را با دیسک NVMe تحویل گرفتی و انتظار داشتی سایت پرواز کند. ولی TTFB هنوز بالای ۸۰۰ میلی‌ثانیه است و پیشخوان وردپرس به‌کندی باز می‌شود. دیسک گناهکار نیست. در یک درخواست وردپرسی معمولی، دیسک فقط چند میلی‌ثانیه کار می‌کند و بقیه‌ی زمان صرف اجرای PHP و رفت‌وبرگشت با دیتابیس می‌شود. تا وقتی این دو را تنظیم نکنی، NVMe نشسته و منتظر است.

این راهنما برای کسی است که سرور دارد و دسترسی root دارد. برای هر تنظیم می‌گویم چه چیزی را قبل و بعد اندازه بگیری، دستورها قابل کپی‌اند، و خروجی هرکدام را هم توضیح می‌دهم. نسخه‌هایی که روی آن‌ها آزموده‌ام: PHP 8.3 و MySQL 8.0 روی Ubuntu 24.04 با Nginx.

اول گلوگاه را اندازه بگیر، بعد دست به تنظیمات بزن

بدون عدد پایه، نمی‌فهمی کدام تغییر کار کرد. قبل از هر چیز TTFB یک صفحه‌ی معمولی را بگیر:

curl -s -o /dev/null -w "TTFB: %{time_starttransfer}s  Total: %{time_total}s\n" https://example.com/

این را سه بار پشت‌سرهم بزن. عدد اول معمولاً بالاتر است چون کش سرد است؛ دو عدد بعدی واقعی‌ترند. اگر TTFB بالای ۰٫۵ ثانیه ماند، مشکل در سمت سرور است نه مرورگر بازدیدکننده.

برای فشار واقعی، چند درخواست هم‌زمان بفرست:

ab -n 200 -c 10 https://example.com/

به دو خط خروجی نگاه کن: Requests per second و Time per request. اگر زیر بار، زمان پاسخ چند برابر شد، یعنی worker ها یا دیتابیس اشباع شده‌اند. این عدد را یادداشت کن. بعد از هر بخش دوباره همین دستور را بزن و مقایسه کن.

NVMe چه چیزی را حل می‌کند و چه چیزی را نه

دیسک سریع جایی دیده می‌شود که عملیات واقعاً به دیسک می‌رسد: نوشتن و خواندن فایل‌های موقت، لاگ، آپلود رسانه، و کوئری‌هایی که در حافظه جا نمی‌شوند و به دیسک سرریز می‌کنند. اگر سایتت رسانه‌محور است یا دیتابیس بزرگ‌تر از رم داری، NVMe تفاوت ملموسی می‌سازد. اینجاست که سخت‌افزار و نرم‌افزار به هم می‌رسند: اگر سرورت هنوز روی دیسک چرخان یا SATA SSD است، سرور مجازی انگلستان در کاونتری با گزینه‌ی NVMe همان سقف I/O را که این تنظیمات به آن تکیه می‌کنند فراهم می‌کند. تفاوت NVMe و SSD را در این مقایسه بیشتر باز کرده‌ایم.

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

مرورگرTLSNginxکش صفحهphp-fpmOPcacheMySQLbuffer poolRedisکش آبجکتدیسکNVMe
مسیر یک درخواست: کش صفحه‌ی Nginx قبل از همه جواب می‌دهد؛ اگر رد شد، php-fpm با OPcache اجرا می‌کند و از buffer pool دیتابیس و کش آبجکت ردیس می‌خواند. دیسک NVMe پشت همه است و فقط وقتی لازم می‌شود که چیزی در حافظه نباشد.

OPcache: PHP را هر بار از نو کامپایل نکن

بدون OPcache، هر درخواست تمام فایل‌های PHP وردپرس و افزونه‌ها را دوباره می‌خواند و کامپایل می‌کند. این سنگین‌ترین کار بی‌فایده‌ی سرور است. OPcache بایت‌کد کامپایل‌شده را در حافظه نگه می‌دارد.

در فایل /etc/php/8.3/fpm/conf.d/10-opcache.ini این مقادیر را برای محیط پروداکشن بگذار. مقدار پیش‌فرض memory_consumption برابر ۱۲۸ مگابایت است که برای وردپرس پر از افزونه کم است:

opcache.enable=1
opcache.memory_consumption=256
opcache.interned_strings_buffer=16
opcache.max_accelerated_files=20000
opcache.validate_timestamps=0
opcache.revalidate_freq=0

نکته‌ی max_accelerated_files: PHP آن را به عدد اول بعدی گرد می‌کند؛ ۲۰۰۰۰ در عمل به ۳۲۵۳۱ می‌رسد. اشکالی ندارد، فقط بدان که چرا در خروجی عدد دیگری می‌بینی.

گزینه‌ی خطرناک validate_timestamps=0 است. با این مقدار، PHP دیگر تغییر فایل‌ها را چک نمی‌کند و سرعت بیشتر می‌شود، ولی بعد از هر به‌روزرسانی قالب یا افزونه باید دستی کش را خالی کنی وگرنه کد قدیمی اجرا می‌شود:

systemctl reload php8.3-fpm

اگر تیم توسعه مدام فایل آپلود می‌کند، فعلاً validate_timestamps=1 بگذار. این تنها جایی است که باید میان سرعت و راحتی یکی را انتخاب کنی.

چطور بفهمی کار کرد

بعد از ری‌لود، وضعیت را بخوان:

php -d opcache.enable_cli=1 -r '$s=opcache_get_status(false); printf("hit rate: %.2f%%  used: %dMB  oom: %d\n", $s["opcache_statistics"]["opcache_hit_rate"], $s["memory_usage"]["used_memory"]/1048576, $s["memory_usage"]["oom_restarts"] ?? 0);'

سه عدد مهم‌اند. hit rate بعد از چند دقیقه ترافیک باید بالای ۹۹ درصد باشد. oom اگر بزرگ‌تر از صفر بود یعنی حافظه کم است و باید memory_consumption را بالا ببری. این سنجه مربوط به پردازه‌ی CLI است؛ برای دیدن آمار خودِ php-fpm یک فایل کوچک با opcache_get_status() بساز یا از افزونه‌ی Query Monitor استفاده کن.

کش آبجکت با ردیس: کوئری تکراری را از دیتابیس نگیر

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

ردیس را نصب و افزونه‌ی Redis Object Cache را فعال کن. در wp-config.php:

define( 'WP_REDIS_HOST', '127.0.0.1' );
define( 'WP_REDIS_PORT', 6379 );

بعد با WP-CLI فعالش کن:

wp redis enable

چطور بفهمی کار کرد

چند صفحه را باز کن، بعد آمار ردیس را بخوان:

redis-cli info stats | grep keyspace

دو عدد می‌بینی: keyspace_hits و keyspace_misses. نسبت hit به کل باید بالا برود و با هر بازدید بالاتر بماند. اگر misses همچنان با سرعت hits بالا می‌رود، یا افزونه وصل نشده یا چیزی مدام کش را پاک می‌کند. برای تأیید اتصال از سمت وردپرس:

wp redis status

باید Status: Connected ببینی.

innodb_buffer_pool_size را با رم موجود تنظیم کن

buffer pool جایی است که InnoDB داده‌ها و ایندکس‌ها را در رم نگه می‌دارد. اگر کوچک باشد، هر کوئری به دیسک می‌رود و اینجاست که سایت روی رم کم کند می‌ماند حتی با NVMe. قاعده‌ی مستند MySQL و MariaDB ساده است: buffer pool باید بخش فعال داده را در خود جا بدهد، ولی نه آن‌قدر بزرگ که سیستم شروع به swap کند. چون swap هر سودی را از بین می‌برد.

قبل از تغییر، ببین الان چقدر است:

mysql -e "SHOW VARIABLES LIKE 'innodb_buffer_pool_size';"

روی سروری که دیتابیس و PHP و Nginx کنار هم‌اند، همه‌ی رم را به دیتابیس نده. یک نقطه‌ی شروع محافظه‌کارانه حدود یک‌چهارم تا نیمی از رم است؛ روی سرور اختصاصیِ دیتابیس می‌شود تا ۷۰ تا ۸۰ درصد رفت، اما این عدد فولکلور است و معیار واقعی همان جا‌شدن داده و نبودِ swap است. در /etc/mysql/mysql.conf.d/mysqld.cnf:

[mysqld]
innodb_buffer_pool_size = 2G

بعد ری‌استارت: systemctl restart mysql

چطور بفهمی اندازه‌اش درست است

نرخ خواندن از دیسک را بسنج:

mysql -e "SHOW GLOBAL STATUS LIKE 'Innodb_buffer_pool_read%';"

دو مقدار مهم‌اند: Innodb_buffer_pool_reads (خواندن از دیسک) و Innodb_buffer_pool_read_requests (کل خواندن‌ها). طبق مستند MariaDB، اگر تغییر reads در یک دقیقه کمتر از یک درصدِ تغییر read_requests باشد، اندازه خوب است. اگر بیشتر بود، داده در رم جا نمی‌شود. هم‌زمان با free -m مطمئن شو ستون swap صفر مانده است.

تعداد worker در php-fpm: نه زیاد، نه کم

هر worker یک درخواست PHP را هم‌زمان اجرا می‌کند. اگر pm.max_children خیلی کم باشد، درخواست‌ها در صف می‌مانند و زیر بار سایت می‌خوابد. اگر خیلی زیاد باشد، رم تمام می‌شود و سرور به swap یا OOM می‌افتد. عدد درست از حافظه‌ی هر پردازه می‌آید.

اول میانگین مصرف هر worker را اندازه بگیر وقتی سایت در حال کار است:

ps --no-headers -o rss -C php-fpm8.3 | awk '{s+=$1} END {printf "%.0f MB avg\n", s/NR/1024}'

فرض کن ۸۰ مگابایت درآمد و یک گیگابایت رم برای PHP کنار گذاشته‌ای. آن‌گاه حدود ۱۲ worker جا می‌شود. محافظه‌کار بگیر. در /etc/php/8.3/fpm/pool.d/www.conf:

pm = dynamic
pm.max_children = 12
pm.start_servers = 4
pm.min_spare_servers = 2
pm.max_spare_servers = 6
pm.max_requests = 500

خط pm.max_requests = 500 مهم است: هر worker بعد از ۵۰۰ درخواست از نو ساخته می‌شود تا نشت حافظه‌ی افزونه‌های بد جمع نشود.

چطور بفهمی کم آورده‌ای

زیر بار، لاگ را نگاه کن:

grep "max_children" /var/log/php8.3-fpm.log

اگر خط server reached pm.max_children دیدی، worker کم است. یا رم بیشتری بده و عدد را بالا ببر، یا با کش صفحه بارِ PHP را کم کن. اگر این خط را ندیدی و رم هم خالی است، همه‌چیز متعادل است.

کش صفحه در سطح وب‌سرور: بزرگ‌ترین برد

همه‌ی تنظیمات بالا PHP را سریع‌تر می‌کنند. کش صفحه کاری می‌کند که برای بازدیدکننده‌ی ناشناس اصلاً PHP اجرا نشود. Nginx خروجی HTML را ذخیره می‌کند و درخواست بعدی را مستقیم از حافظه می‌دهد. برای بازدیدکننده‌ی لاگین‌نکرده، این تفاوت بین اجرای کامل وردپرس و یک فایل آماده است.

در بلاک http:

fastcgi_cache_path /var/cache/nginx levels=1:2 keys_zone=WP:100m inactive=60m;

در بلاک server، لاگین‌شده‌ها و درخواست POST را از کش کنار بگذار:

set $skip 0;
if ($request_method = POST) { set $skip 1; }
if ($http_cookie ~* "wordpress_logged_in") { set $skip 1; }

location ~ \.php$ {
    fastcgi_cache WP;
    fastcgi_cache_valid 200 60m;
    fastcgi_cache_bypass $skip;
    fastcgi_no_cache $skip;
    add_header X-Cache $upstream_cache_status;
    # ... بقیه‌ی تنظیمات fastcgi
}

تست کانفیگ و ری‌لود: nginx -t && systemctl reload nginx

چطور بفهمی HIT می‌خوری

curl -sI https://example.com/ | grep -i x-cache

بار اول X-Cache: MISS و بار دوم X-Cache: HIT. اگر همیشه MISS ماند، احتمالاً یک کوکی یا هدر مانع کش شده. حواست باشد: این کش برای کاربر لاگین‌شده عمداً دور زده می‌شود، پس بهبودِ TTFB را روی یک صفحه‌ی عمومی و بدون لاگین بسنج، نه در پیشخوان.

فایل‌های ثابت را از دوش سرور بردار

بعد از این تنظیمات، بار باقی‌مانده اغلب از تصویر، CSS و جاوااسکریپت می‌آید. این‌ها را می‌شود از لبه سرو کرد تا سرور اصلی فقط HTML بسازد. سی‌دی‌ان ابریشم محتوای سایت را از نزدیک‌ترین نود به بازدیدکننده سرو می‌کند و بار سرور اصلی را کم می‌کند، با نودهایی در شیراز و تهران و چند مقصد خارج، به‌علاوه‌ی گواهی SSL رایگان. برای سایت ایرانی با بازدیدکننده‌ی داخلی، نودِ نزدیک تفاوت زمان پاسخ را ملموس می‌کند.

جدول قبل و بعد

برای هر تغییر، یک معیار مشخص بسنج. اگر عدد تکان نخورد، آن تنظیم گلوگاه تو نبوده و وقت را جای دیگر بگذار.

تنظیمبا چه اندازه بگیرینشانه‌ی موفقیت
OPcacheopcache_get_statushit rate بالای ۹۹٪، oom صفر
کش آبجکت ردیسredis-cli info statsنسبت hits به کل بالا می‌رود
buffer poolInnodb_buffer_pool_read%reads زیر ۱٪ read_requests
php-fpmلاگ php-fpm + free -mبدون max_children، بدون swap
کش صفحهcurl -I هدر X-CacheHIT در درخواست دوم

کجا این‌ها را اجرا کنیم

این تنظیمات به یک چیز نیاز دارند که هاست اشتراکی نمی‌دهد: دسترسی root تا فایل‌های PHP و MySQL و Nginx را خودت بنویسی. یک سرور مجازی انگلستان در کاونتری با گزینه‌ی NVMe و رم از ۱ تا ۱۲ گیگابایت این کنترل را می‌دهد؛ رم را بر اساس بزرگی دیتابیس و تعداد worker انتخاب کن، نه صرفاً قیمت. برای راهنمای انتخاب رم و CPU و دیسک متناسب با بار، این مطلب را ببین. اگر دیتابیست بزرگ‌تر از رم شد و buffer pool دیگر جا نداد، آن وقت وقت فکر کردن به رم بیشتر یا سرور اختصاصی است.

منابع

پرسش‌های پرتکرار

چرا با وجود دیسک NVMe سایت وردپرسم هنوز کند است؟

چون گلوگاه دیسک نیست. در یک درخواست وردپرسی بیشتر زمان صرف اجرای PHP و کوئری دیتابیس می‌شود، نه خواندن از دیسک. تا OPcache، کش آبجکت، buffer pool و کش صفحه را تنظیم نکنی، سرعت دیسک دیده نمی‌شود.

مهم‌ترین تنظیم برای پایین آوردن TTFB کدام است؟

کش صفحه در سطح Nginx، چون برای بازدیدکننده‌ی ناشناس اصلاً PHP اجرا نمی‌شود و HTML آماده برمی‌گردد. بعد از آن OPcache و کش آبجکت ردیس بیشترین اثر را دارند، به‌خصوص برای کاربران لاگین‌شده.

innodb_buffer_pool_size را چقدر بگذارم؟

به‌اندازه‌ای که بخش فعال داده در رم جا شود و سیستم swap نکند. روی سرور مشترک حدود یک‌چهارم تا نیم رم نقطه‌ی شروع است. درستی‌اش را با نسبت Innodb_buffer_pool_reads به read_requests بسنج؛ زیر یک درصد خوب است.

تعداد worker در php-fpm را چطور حساب کنم؟

میانگین مصرف رم هر پردازه‌ی php-fpm را با ps بگیر، بعد رمِ کنارگذاشته برای PHP را بر آن تقسیم کن و محافظه‌کارانه گرد کن. اگر در لاگ پیام server reached pm.max_children دیدی، worker کم است.

validate_timestamps=0 در OPcache امن است؟

سریع‌تر است چون PHP تغییر فایل‌ها را چک نمی‌کند، ولی بعد از هر به‌روزرسانی قالب یا افزونه باید با reload کردن php-fpm کش را خالی کنی وگرنه کد قدیمی اجرا می‌شود. اگر مدام فایل تغییر می‌دهی فعلاً روی ۱ بگذار.

سفارش سرور مجازی بازگشت به وبلاگ →