Pull to refresh

Опыт оптимизации окружения VPS за $10 для персональной сети WordPress

Reading time 12 min
Views 19K
Эта небольшая статья посвящена описанию моих действий по приведению VPS за $10/месяц в приемлемое состояния для работы персональной сети сайтов на WordPress в одиночном и сетевом режиме (WordPress Network Mode).

Интро


Итак, дорогие мои хай-тек специалисты и случайные читатели, история эта началась в конце апреля 2012 года, когда мой любимый городской провайдер-монополист наотрез отказал мне в дальнейшем размещении моего трех-ядерного атлона в своей стойке.

Я живу в маленьком курортном городке (3 тыс.мы-местные и 50 тыс.понаехали-тут-летом) и большинство моих персональных проектов посвящено родному и любимому городу. Пара городских вебкамер, погода, справочники и т.д. Кроме того, я ещё чуток борюсь за справедливость и права: организовал небольшой проектик на домене TV — снимаем репортажи про безпредел чиновников и т.д...

Так вот о чем это я… ах да! Стоял, значит, двух-юнитовый серверочек (его и сервером тяжко-то назвать) совершенно бесплатно в стойке у провайдера, у которого стоек было аж 3 и 2 из них стояли пустые. Почему же бесплатно? Потому что этому самому провайдеру я помогал развивать корпоративные сайты. Кроме того, других людей, с аналогичными моим знаниями в нашем маленьком городке просто нет — далекая периферия, умные быстро уезжают в Москву и другие зарубежья.

В общем, крутились на AthlonX3 / 4Gb RAM мои проекты и я совершенно не заморачивался по вопросу оптимизации, наладки — делал себе сайтики на любимом wordpress и даже не думал, что когда-нибудь халява и кончится.

После очередной публикации любительского репортажа-недокомпромата на местного мэра ко мне подошел директор провайдера (из той же партии что и мэр) и порекомендовал в течении суток забрать информацию с сервера. Компроматики-то на нём лежали. Ещё хорошо, что подошел и сказал.

Суток уже не оставалось. А проектики мои уже и в SERP'е хорошо сидели, и позиции терять не хотелось… Суть — за 12 часов был найден первый попавшийся дешевый VPS за $10 в Германии (только лишь бы не в моей стране), софт поставлен из репозиториев какой попало, данные были перелиты, DNS'ы перепрописаны и с горем пополам все более менее работало, правда еле еле шевелилось.

Кстати, ограничение в $10 взялось не просто так — именно такая сумма у меня была на тот момент на webmoney, накапало в Директе. Собственно, этот бюджет поднялся всего на $2 — за дополнительный IP. Сейчас мне этот VPS обходится в $12.

Лишь бы поисковики видели


Параметры VPS: CentOS 5, XEN, 2.8Ghz, 256 Mb RAM, 15 GB SATA, траф — безлимитный. Второпях было установлено nginx, apache+mod_php+eaccelerator, mysql, memcache. Представьте себе — время тикает, надо перелить около 12 гиг из одной страны в другую, а ещё поставить, настроить, и желательно так что бы поисковики ничего не поняли :)

Я вроде как и не программист — так себе, быдлоламо-кодер, и не администратор — знаю yum install, yum erase, make && make install… Но зато я более-менее владею незаменимым навыком гугл-обучения.

Изначально я поставил apache фронт-эндом. Это было нечто :) ab2 со старого сервера показывало не более 1.4 requests per second. Nginx я тогда видел в первый раз. Поставил, почитал (чуток, времени не было) кое-как накопипастил конфигов — завелось. И даже стало быстрее. После установки eaccelerator результат ab2 в 6 r.p.s мне показался просто отличным и я это всё дело так и оставил.

Никакого mysqltuner.pl, никакого выбора/оптимизации apache mpm, даже nginx кушал столько памяти, что 80% забитого свопа мне казалось нормальным. Но видимо это было из-за стресса :) На мой взгляд, сайты не тормозили, ну чуток задумывались. Но зная свою аудиторию — я прекрасно понимал, что мои посетители никуда не уйдут.

Что-то как-то уж очень медленно


Сайтики росли и ширились — неспешно, но выйдя в TOP-5 Google и Яндекс по запросам относительно моего города-курорта, сайты летом стали часто недоступны (RAM/SWAP — 100%). Поскольку времени заниматься оптимизацией летом реально не было — я задачу отложил на конец года.

С конца апреля до 7 декабря в таком режиме «еле-едет» сайтики мои наработали вот такой статистики:

.SU — это WordPress Network — сетка сайтов, посвященная моему городу. Остальные — «одиночные» WordPress'ы. P.S. Когда всё разнесено на поддомены — удобнее обслуживать. Кстати, я только сегодня прочитал про вынесения статики на поддомен, буду пробовать, но позже.

(Прошу в комментариях не описывать в подробностях, как плох Wordpress, его код и какое у него ресурсопотребление. Сам столкнулся с этим, как головой об стену. Возможно, это суть следующей статьи.)

Кстати, важным будет упомянуть тот факт, что за этот период у меня начали появляться клиенты в городе, которым нужно было сделать сайт. Я особо не заморачивался — штамповал на WP. И в один прекрасный момент, с поднятием очередного сайта, это всё нагромождение WP стало работать из рук вон плохо.

Читаю, ставлю, конфигурю, тюню, катаю, сношу


yum update принес много обновлений. Но они особо не помогли, да и не могли помочь. Я знал, что надо переходить на php-fpm и заводить кеширование. Для моего случая кеширование наиболее актуально, поскольку абсолютное большинство моих сайтов практически статично — expire в пару недель всех устраивает. А те сайты, которые не статичны (а это в первую очередь, тяжелый WordPress Network), можно аккуратненько подкрутить так, что бы кеш обновлялся строго там где надо и в то время в которое надо.

В общем, php-fpm без бубнов ставиться не хотел. Через пяток другой репозиториев нашел скомпиленый пакет, со всеми необходимыми мне опциями. В общем-то можно было из сорцов — но не хотелось. Установил, по быстрому настроил… httpd stop && php-fpm start — ООО! 9 r.p.s! Иду на рекорд! Шутка конечно.

php-frm стоит, а eaccelerator-то пришлось снести — не совместим, нужно новый. Гугл-обучение — оказывается этот акселератор уже давно устарел. yum install php-pecl-apc. Поставилось, опять не без бубнов — поднастроил, завелось. Генерация страниц стала работать как-то очень странно. Первое открывание долгое — потом на какое-то время сайты начинают летать. Потом тупить. Потом опять летать. Долгое гугл-обучение привело меня к описанию использования APC в Facebook. Это оказалось полезным, многое стало понятным.

Настроил php-fpm. Лазаю по сайту — тут вдруг 502, F5, F5, 504. Что за байда! php-fpm перестает откликаться через какое-то время. Опять читаю — памяти мало, поставил больше. Лучше, но ещё через некоторое время опять 502. Читаю, читаю, memleaks, deadlocks, полный комплект.

Сношу APC, ставлю XCache. Вообще не завелось. Ладно. Надо думать и читать. Ставлю обратно APC. Опять настраиваю. С другим конфигом пока вылетать перестало. Да и скорость значительно поднялась — время генерации особенно простых страниц уменьшилось в несколько раз (2,7s -> 0.8s). Да, ребятушки, 2,7s — это у прокачанного WordPress простая страница. А если плагинов понавесить — как у меня на .TV — так там вообще без оптимизации только генерация 19-25s.

И тут вдруг я понял! Сначала нужно настроить весь окружной софт, который обкатан у множества людей и великолепно справляется со своими обязанностями. Я бросил ковырять php-fpm+apc — и начал настраивать с самого «переднего» края — с сетевого стека операционной системы. Рекомендаций в сети много, всё подробно расписано — повторять не буду. Настроил, проверил, nginx стал шевелиться гораздо быстрей, да и mysql чуток ускорился.

Далее nginx — уменьшил кол-во воркеров до 1 и конекшенов до 8196. Включил sendfile и aio, проверил величину directio. Подкрутил output_buffers согласно мануалу. И вот! Я получил на статике чуть больше 60 r.p.s. Я подумал, что первая победа есть и нужно продолжать. Аналогичным образом прошелся по php-fpm (пока не трогая apc). Конекшены, лимиты. Затем mysql. В этом мне помог mysqltuner.pl и конечно оф.мануал. После увеличения кешей, и уменьшения кол-ва конекшенов вся работа VPS значительно ускорилась, load average упал, освободилось треть памяти.

Однако, php-fpm продолжал периодически падать. Много читал ссылки в гугле. APC бажный — но он и такой хорош. Вернемся к нему чуть позже.

Хочется спать, а сайты падучие


В качестве промежуточного решения я решил попробовать поставить на все WordPress плагин кеширования — W3-Total-Cache. На будущее хотелось использовать nginx fastcgi cache с модулем cache purge (попробовал — не понравилось, ниже напишу почему). W3TC было выбрано как “быстро-поставил-перед-сном», да и в моем случае – это было оптимальное решение. Если бэк-энд упадет – фронт-энд будет кормить людей статикой (но не все так гладко).

Готовим W3TC следующим образом — Page cache: Disk Enchanced, Database cache — APC, Object cache — APC. Expire — по вкусу. На кодексе вордпресса нашел полурабочий конфиг для раздачи page cache из W3TC напрямую с фронт-энда (через nginx не запуская php-fpm), доработал его. Рестарт и только тут я начинаю понимать, что значит, когда сайты работают быстро. Статика улетает из-под nginx'а ну очень быстро. На чистой статике я получал в ab2 до 800 (!!!) r.p.s.

Конечно, когда я сел писать этот текст — я уже знаю, что ab2 это плохо, а siege — это хорошо. Но на тот момент я этого не знал. И всё же! Сравните начальный показать 1.4 и промежуточный 800! Но на этом всё не закончилось, а только началось! Но только на следующее утро.

Утро вечера мудрЁнее


Проснулся утром от очередной СМСки Яндекса, что сайты давненько в ауте. php-fpm не отвечал на запросы фронт-энда. В результате долго ковыряния, перезапусков и перенастроек выяснилось, что чистка user cache в apc вызывает какой-то мертвый лок в результате чего, php-fpm перестает обрабатывать запросы. И вот тут я подошел к тому, что на VPS стояло, но не использовалось должным образом. Memcached и соответствующее расширение для php — memcache. Database cache и Object cache в W3TC был срочно перенастроен на Memcached. Я конечно читал, что memcached на локалхосте не даёт прироста производительности по сравнению с disk cache, однако в моём случае прирост был очень ощутим. Без тестов — визуально. Видимо это связано со значительным IO на VPS.

php-fpm ПОЧТИ перестал лочиться, но это почти. Меня не устраивало — я же не могу постоянно сидеть и проверять, работает он или нет. Да и как так — вроде продакшн решение (малюсенькое, но все же), а бэк-энд падает. Плохо. Скачал сорцы последнего APC, компилил с разными опциями. Наибольшую производительность показали Spinlocks, но они и самые падучие (экспериментальные всё ж таки). В общем, на дэдлоки при чистке user cache (db, object в контексте wp+w3tс) это никак не повлияло, как висло так и виснет. В итоге, user cache лежит на memcached, и ведёт себя шустро.

Что я только не пробовал. Остановился по итогу на APC с mmap memory и pthread mutex Locks locking. Написал небольшой скриптик для крона, который раз минуту проверяет живость php-fpm. И поставил fastcgi timeout в nginx в 2 минуты. Пока вдруг тупняк – php-fpm упал – пользователь чуточек пождет (с учетом моей аудитории). Другого решения пока не нашел — если кто сталкивался или знает — предлагайте.

Отдельная песня — это nginx fastcgi cache. После его инициализации nginx cache manager скушал 60Мб памяти из общих 256, показал производительность ниже, чем просто nginx + w3tc (pagecache disk enhanced) и был благополучно отключен.

Итоги оптимизационных процедур


На сейчас я остановился на следующем софте: nginx 1.3.8, php 5.3.19 (+php-fpm), apc 3.1.19, memcached 1.4.15, mysql 5.0.95 обслуживают несколько одиночных WordPress и одну WordPress Network c активированным плагином W3 Total Cache

PHP-сессии находятся в memcached, user cache (db, object) -> memcached, apc.XXXXXX в /dev/shm/, apc работает исключительно как оптимизатор и кешер опкода php.

ab2 на полностью кэшированных сайтах выдает около 800 r.p.s. на блочно кешированных около 200 r.p.s. (округлено в меньшую сторону).

Обновление от сегодня, 12.12.2012

Меня реально утомил постоянно падающий PHP-FPM. Но сегодня, всю ночь у меня был потрясающий сексразбор ключей конфигурации перед компиляцией PHP 5.4.9. Пока я разобрался как собирать его, и его модули из исходников — думал, крыша съедет. Еле собрал, запустил. Сайты работают. User cache в APC не вызывает дэдлок. На момент апдейта публикации — PHP-FPM Uptime 9 hours and 15 minutes без единного нарекания. Однако, я так и не смог понять, как можно скомпилировать PHP в минимальной рабочей сборке под WordPress. Сейчас все модули вкомпилены в сам PHP и я не знаю, как правильно их выключить. Уважаемое сообщество, подскажите пожалуйста, как это можно сделать? Какие реально модули нужны для оптимальной работы WordPress с максимальной минимизацией бинарников PHP?

вывод php -m
# php -m
[PHP Modules]
apc
bcmath
calendar
Core
ctype
curl
date
dom
ereg
exif
fileinfo
filter
ftp
gd
gettext
hash
iconv
intl
json
libxml
mbstring
mcrypt
memcache
mhash
mysql
mysqli
mysqlnd
openssl
pcre
posix
Reflection
session
SimpleXML
sockets
SPL
standard
sysvmsg
sysvsem
sysvshm
tokenizer
xml
zip
zlib


Вечернее дополнение того же дня:

Благодаря указанию aktuba, я обратил внимание на важность реакции сервера на заголовоки If-Modified-Since или If-None-Match. У меня ни nginx ни W3TC почему-то не отдавали в соответствующих случаях 304 Not Modified. По итогу, я добавил в контекст server конфигурации nginx для одного WP-сайта директивы: if_modified_since before; etag on; Это весьма положительно повлияло на снижение количества запросов к серверу. Посмотрю по статистике запросов и включу это на всех сайтах.

Кроме того, в контекст events конфигурации nginx была добавлена директива worker_aio_requests 128; для увеличения производительности aio.

На синтетике ab были получены следующие результаты:
ab -n 10000 -c 1000 http://*.org/
This is ApacheBench, Version 2.3 <$Revision: 655654 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/

Benchmarking *.org (be patient)
Completed 1000 requests
Completed 2000 requests
Completed 3000 requests
Completed 4000 requests
Completed 5000 requests
Completed 6000 requests
Completed 7000 requests
Completed 8000 requests
Completed 9000 requests
Completed 10000 requests
Finished 10000 requests


Server Software:        nginx
Server Hostname:        *.org
Server Port:            80

Document Path:          /
Document Length:        22307 bytes

Concurrency Level:      1000
Time taken for tests:   4.812 seconds
Complete requests:      10000
Failed requests:        0
Write errors:           0
Total transferred:      229199440 bytes
HTML transferred:       226639120 bytes
Requests per second:    2078.22 [#/sec] (mean)
Time per request:       481.181 [ms] (mean)
Time per request:       0.481 [ms] (mean, across all concurrent requests)
Transfer rate:          46516.29 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        6  150  69.8    147     309
Processing:   105  288  70.8    276     600
Waiting:        5  144  50.0    145     511
Total:        215  439  93.6    448     783

Percentage of the requests served within a certain time (ms)
  50%    448
  66%    467
  75%    477
  80%    498
  90%    541
  95%    629
  98%    634
  99%    774
 100%    783 (longest request)


Как видно, значительно ускорилась отдача статики. Это отлично, даже более чем. Хотя, в данном случае я ещё грешу на тот факт, что это может быть был разгружен IO в ноде в эти 4 секунды, но это наврядли. Цикл аналогичных тестов подтвердил оную картину.


Аттач:

crontab bash-скрипт для проверки 'адекватности' php-fpm с дефолтным пулом www
#!/bin/sh
POOL=`curl --retry 3 --retry-delay 1 --connect-timeout 2 --max-time 2 --silent http://127.0.0.1/fpmstatus | grep pool | awk '{ print $2 }'`
if [ "$POOL" != "www" ]
then
    echo -------------------------------------------------------------------------- >>/var/log/php-fpm/restarts.log
    echo `date` PHP-FPM $host:$port is DOWN >>/var/log/php-fpm/restarts.log
    echo `date` first try sigterm PHP-FPM... >>/var/log/php-fpm/restarts.log
    killall -9 php-fpm >>/dev/null
    sleep 5
    echo `date` second try sigterm PHP-FPM... >>/var/log/php-fpm/restarts.log
    killall -9 php-fpm >>/dev/null
    sleep 5
    echo `date` restarting PHP-FPM NOW >>/var/log/php-fpm/restarts.log
    /etc/init.d/php-fpm restart >>/dev/null
    echo -------------------------------------------------------------------------- >>/var/log/php-fpm/restarts.log
else
    sleep 1
fi
exit

Два килла используется потому что бывает так повиснет, что только со второго раза сносится.
Внимание! php-fpm и nginx должен быть настроен для корректной работы location /fpmstatus

apc.ini
extension = apc.so
apc.enabled=1
apc.shm_size=128M
apc.num_files_hint=0
apc.user_entries_hint=0
apc.ttl=0
apc.use_request_time=1
apc.user_ttl=0
apc.gc_ttl=3600
apc.cache_by_default=1
apc.filters
apc.mmap_file_mask=/apc.shm.XXXXXX
apc.file_update_protection=10
apc.enable_cli=0
apc.max_file_size=1M
apc.stat=1
apc.stat_ctime=0
apc.canonicalize=1
apc.write_lock=0
apc.rfc1867=0
apc.rfc1867_prefix =upload_
apc.rfc1867_name=APC_UPLOAD_PROGRESS
apc.rfc1867_freq=0
apc.rfc1867_ttl=3600
apc.include_once_override=0
apc.lazy_classes=0
apc.lazy_functions=0
apc.coredump_unmap=0
apc.file_md5=0
apc.localcache=1
apc.localcache.size=1024


выдержки из nginx.conf (которые оч.глубоко в гугле...)
worker_processes  1;
timer_resolution  100ms;
worker_rlimit_nofile 4096;
worker_priority  -5;
events {
    worker_connections  8196;
    use epoll;
}
sendfile on;
aio on;
directio 5m;
output_buffers 8 128k;
tcp_nopush      on;
tcp_nodelay     on;
server_tokens   off;
reset_timedout_connection  on;
open_file_cache max=8192 inactive=60s;
open_file_cache_valid 60m;
open_file_cache_min_uses 2;
open_file_cache_errors on;


wp-net.conf (для работы Wordpress Network с w3tc page disk cache, конф подключается к nginx)
map $host $sid {
                wp-net.su             -999;
                forum.wp-net.su       2;
                about.wp-net.su      4;
                info.wp-net.su        5;
                test.wp-net.su      6;
}
server {
listen IP.IP.IP.IP:80;
        server_name wp-net.su *.wp-net.su;
        access_log /var/log/nginx/nginx_($host)_access.log;
        error_log /var/log/nginx/nginx_(ALL.wp-net.su)_error.log error;
        root /var/www/vhosts/wp-net.su;
        index index.php;
location = /favicon.ico { log_not_found off; access_log off; }
location = /robots.txt { allow all; log_not_found off; access_log off; }
location ~ /\. { deny all; }
location ~* /(?:uploads|files)/.*\.php$ { deny all; }
if ($request_uri ~* "\/files\/(.*)"){ set $rtfile $1; }
location ^~ /files {
                try_files /wp-content/blogs.dir/$sid/$uri /wp-includes/ms-files.php?file=$rtfile;
                expires 7d;
}
location ^~ /blogs.dir {
                internal;
                alias /var/www/vhosts/wp-net.su/wp-content/blogs.dir;
                access_log off;
                log_not_found off;
                expires 7d;
}
location ~* ^.+\.(js|css)$ { expires 2d; }
location ~* ^.+\.(ogg|ogv|svg|svgz|eot|otf|woff|mp4|ttf|rss|atom|jpg|jpeg|gif|png|ico|zip|tgz|gz|rar|bz2|doc|xls|exe|ppt|tar|mid|midi|wav|bmp|rtf)$ {
                expires 7d;
}

set $cache_uri $request_uri;
if ($request_method = POST) { set $cache_uri 'null cache'; }
if ($query_string != "") { set $cache_uri 'null cache'; }
if ($request_uri ~* "(/wp-admin/|/xmlrpc.php|/wp-(app|cron|login|register|mail).php|wp-.*.php|/feed/|index.php|wp-comments-popup.php|wp-links-opml.php|wp-locations.php|sitemap(_index)?.xml|[a-z0-9_-]+-sitemap([0-9]+)?.xml)") { set $cache_uri 'null cache'; }
if ($http_cookie ~* "comment_author|wordpress_[a-f0-9]+|wp-postpass|wordpress_logged_in") { set $cache_uri 'null cache'; }

location / {
                try_files /wp-content/w3tc-$host/pgcache/$cache_uri/_index.html $uri $uri/ /index.php?$args;
}

location ~ \.php$ {
                try_files $uri =404;
                fastcgi_split_path_info ^(.+\.php)(/.+)$;
                include /etc/nginx/fastcgi.conf;
                fastcgi_connect_timeout 1800s;
                fastcgi_read_timeout 1800s;
                fastcgi_send_timeout 1800s;
                fastcgi_index index.php;
                fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
                fastcgi_intercept_errors off;
                fastcgi_pass phpfpm;
        }
}


важное из /etc/my.cnf
[mysqld_safe]
key_buffer=8M
max_allowed_packet=1M
max_heap_table_size=24M
table_cache=1024
sort_buffer_size=512K
read_buffer_size=512K
read_rnd_buffer_size=2M
net_buffer_length=20K
thread_stack=640K
thread_cache=4
tmp_table_size=8M
query_cache_limit=8M
query_cache_size=16M
skip-locking
skip-networking
skip-bdb
skip-innodb
skip-thread-priority
old-passwords=0
thread_concurrency=1
max_connections=16



Господа специалисты, я понимаю, критиковать часто приятно, однако я не специалист, и делюсь даже не столько своими наработками с новичками, а показываю, как можно нащупать путь, удобный каждому. Хотя конечно — критикуйте! И особенно полезны будут дельные рекомендации сообщества — хочу попробовать, потестировать на этом VPS, и дополнить статью если будет прирост.

Хорошего вам настроения
Tags:
Hubs:
+16
Comments 79
Comments Comments 79

Articles