Продвижение

Продающий сайт

партнерские программы

Drupal: Настройка и оптимизация небольшого WEB-сервера

Большое спасибо kayo за хорошую статью
Рано или поздно для каждого, кто трудится в связанной с интернетом теме, наступает необходимость в собственном выделенном реальном или виртуальном сервере. Хорошо, когда до этого момента имеется реальный опыт настройки программного обеспечения WEB-сервера, однако зачастую это редко бывает так, и поставленная задача решается отнюдь не так тривиально, как бы хотелось.
В сегодняшней статье Kayo будет делиться своим успешным и горьким опытом в поставленном вопросе, подскажет кое-какие хитрости и трюки позволяющие гибко настроить конфигурацию не только в рамках решаемой задачи, которую можно рассматривать лишь в качестве примера.

Введение

По оптимизации работы WEB серверов существует не мало материалов в сети. В этой статье хотелось бы обобщить основные моменты и привести некоторые свои наработки по этой теме применительно к конкретной конфигурации.

Задача
Итак, что мы хотим получить:

Хостинг, оптимизированный под работу CMS Drupal
Возможность гибко настраивать конфигурацию
Снижение затрат ресурсов при размещении большого числа сайтов
Унификация настройки размещаемых сайтов
Решение

Программное обеспечение сервера:

ОС: Debian GNU/Linux
Сервер имён: bind9
WEB-сервер: Nginx (Engine X)
PHP-обработчик: PHP-FPM
СУБД: MySQL
Также нам понадобятся следующие вещи:

Кешер данных в оперативной памяти: Memcached
Модуль PHP для работы с Memcached: Memcache
Модуль PHP для кеширования скриптов: Eaccelerator
И некоторые модули к Drupal:

Поддержка механизма кеширования Memcached: memcache
Модуль статической локализации: locale_static
По возможности будем использовать пакеты, предоставляемые дистрибутивом. Отсутствующие или по каким-то причинам не устроившие Kayo собрал сам.

Итак, устанавливаем по возможности последние версии Nginx и MySQL. Автор использует, предоставленные веткой дерева пакетов debian/sid: Nginx 7.x, MySQL 5.2.x, Memcached 1.4.x, хотя, некоторые вещи, из описываемых в статье, будут работать и на более ранних версиях, но не все, так что, если у вас что-то не работает, обновитесь.

Далее нам понадобится PHP5 с поддержкой FPM, а также Eaccelerator.


Настройка

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

WEB-сервер Nginx
Правильная настройка WEB-сервера очень важна, поскольку основная первичная нагрузка будет приходиться именно на него. Здесь нужно уяснить следующие моменты, определяющие правильность подхода к конфигурированию:

Контент клиенту должен по-возможности отдаваться средствами Nginx
Отдаваемый исключительно самим WEB-сервером контент должен иметь правильные атрибуты кеширования в заголовках
Для динамически формируемого содержимого должны быть как можно более просто определены правила его получения
Не оправданное применение правил модуля перезаписи URL-ов mod_rewrite должно быть предано всяческому порицанию
Кроме перечисленного для нашего удобства по части унификации настроек мы применим директиву include для отделения общих шаблонных конфигураций под разные платформы сайтов от собственно настроек сайтов. Другими словами, все оптимизированные под Drupal настройки мы зададим в отдельном файле, который затем подключим во всех конфигах для сайтов на Drupal.

Итак, идём в /etc/nginx и создаём файл drupal6x, его мы будем включать во все проекты, использующие Drupal 6 версии. Для начала запрещаем доступ ко всем скрытым файлам в директории сайта:
<code>
location ~ /\. {
  deny all;
  break;
}
</code>
Определяем индексный файл:
<code>
index index.php;
fastcgi_index index.php;
</code>
Теперь определяем, что будет происходить при обращение к корневой директории сайта:
<code>
location / {
  try_files $uri $uri/ @drupal;
}
</code>
Здесь мы буквально сказали Nginx-у: попробуй отдать $uri, если не получится, то $uri/, и в противном случае, обратись к виртуальному расположению @drupal.

Теперь мы опишем это виртуальное расположение @drupal:
<code>
location @drupal {
  include phpfpm/params;
 
  fastcgi_param QUERY_STRING q=$uri&$args;
  fastcgi_param SCRIPT_NAME /index.php;
  fastcgi_param SCRIPT_FILENAME $document_root/index.php;
  fastcgi_param DOCUMENT_URI $uri;
}
</code>
Вначале мы подключили шаблон настройки бэкэнда PHP-FPM, затем переписали некоторые параметры нужным для данной конфигурации образом.

Первое, что мы сделали, это реализовали поддержку чистых ссылок для Drupal. В основанных на Apache конфигурациях это обычно делается средствами mod_rewrite, однако для Nginx существует более простой и быстрый способ. Нам потребовалось просто установить вид строки запроса для динамического содержимого "q=$uri&$args", чтобы запрос вида:lo/ca/ti/on&a1=vl1&aN=vN преобразовался в вид, понятный движку: q=lo/ca/ti/on&a1=vl1&aN=vN.

Далее мы указали имя обрабатываемого по-умолчанию скрипта (SCRIPT_NAME), расположение скрипта (SCRIPT_FILENAME) и установили URI.

Если хотим сделать возможным выполнение других скриптов в директории сайта (например update.php, cron.php), то включим в конфиг следующее:
<code>
location ~ \.php$ {
  try_files $uri @drupal;
  include phpfpm/params;
}
</code>
Но лучше так не поступать, а дело в том, что глобально позволять выполнение всех файлов с расширением .php не безопасно, поэтому предпочтительнее жестко задать доступные для запуска сервером php-fpm файлы. Можно сделать это например так:
<code>
location /cron.php {
  include allow_self;
  deny all;
  try_files $uri @drupal;
  include phpfpm/params;
}
</code>
Мы позволили вызывать /cron.php только с самого сервера, и запретили все остальные способы обращения к нему посредством http. В файле allow_self у нас перечислены все адреса нашего сервера, с которых возможен доступ, заданные директивой allow:
<code>
allow a.b.c.d;
allow j.k.l.m;
</code>
Общий для всех конфигураций /etc/nginx/phpfpm/params файл определяем следующим образом:
<code>
include phpfpm/service; # Свойства соединения
include phpfpm/common;  # Общие настройки
include phpfpm/options; # Специальные опции
</code>
Файл /etc/nginx/phpfpm/service у меня на самом деле является символической ссылкой, которую можно назначить на один из файлов, определяющих параметры соединения, например, tcp.service:
<code>
fastcgi_pass 127.0.0.1:9000;
<code>
или unix.service:
<code>
fastcgi_pass unix:/var/run/php5-fpm/php5-fpm.sock;
</code>
Общие настройки (phpfpm/common) у меня содержат обязательные параметры, которые нужно передать интерпретатору при FastCGI запросе:
<code>
# Иногда переопределяемые
fastcgi_param QUERY_STRING $query_string;
fastcgi_param SCRIPT_NAME $fastcgi_script_name;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param DOCUMENT_URI $document_uri;
 
# Обычно статические
fastcgi_param REQUEST_METHOD $request_method;
fastcgi_param CONTENT_TYPE $content_type;
fastcgi_param CONTENT_LENGTH $content_length;
 
fastcgi_param REQUEST_URI $request_uri;
fastcgi_param DOCUMENT_ROOT $document_root;
fastcgi_param SERVER_PROTOCOL $server_protocol;
 
fastcgi_param GATEWAY_INTERFACE CGI/1.1;
fastcgi_param SERVER_SOFTWARE nginx/$nginx_version;
 
fastcgi_param REMOTE_ADDR $remote_addr;
fastcgi_param REMOTE_PORT $remote_port;
fastcgi_param SERVER_ADDR $server_addr;
fastcgi_param SERVER_PORT $server_port;
fastcgi_param SERVER_NAME $server_name;
</code>
Дополнительные опции (phpfpm/options) у меня доступны для глобального изменения при необходимости:
<code>
fastcgi_connect_timeout 300;
fastcgi_send_timeout 300;
fastcgi_read_timeout 300;
fastcgi_buffer_size 32k;
fastcgi_buffers 16 32k;
fastcgi_busy_buffers_size 64k;
fastcgi_temp_file_write_size 64k;
fastcgi_intercept_errors on;
 
fastcgi_pass_request_body off;
client_body_in_file_only clean;
fastcgi_param REQUEST_BODY_FILE $request_body_file;
</code>
Осталось определить правила отдачи статического контента:
<code>
location ~* ^.+\.(jpg|jpeg|gif|css|png|js|ico|html|txt)$ {
  try_files $uri @drupal;
  access_log off;
  expires 31d;
  add_header Last-Modified: $date_gmt;
}
</code>
Не забываем передавать в качестве заголовка время последней модификации файла, чтобы браузер при наличии файла в своём кеше запрашивал только заголовок и не запрашивал сам файл каждый аз заново пока время, получаемое в заголовке, не больше времени в кеше. Также нас не интересует статистика отдачи статического контента, поэтому отключаем логгиинг. Если файл не будет найден по соответствующему пути, передача управления снова переходит к псевтолокейшну @drupal.

PHP-FPM
Здесь вся настройка сводится к редактированию файла /etc/php5/fpm/php5-fpm.conf, содержащего параметры работы собственно менеджера процессов PHP, обрабатывающих входящие запросы в режиме FastCGI. А также переопределению некоторых специфических настроек, отвечающих за работу интерпретатора PHP в файле /etc/php5/fpm/php.ini.

php5-fpm.conf представляет собой структурированный XML файл, в котором представлена секция глобальных настроек и секция настройки worker-ов. Worker-ы представляют собой экземпляры менеджера процессов, которые могут быть сконфигурированны независимо друг от друга. Так каждый может работать под своим пользователем, должен иметь свой адрес, может иметь отличные параметры конфигурации в зависимости от наших целей.

Основные настройки
По-умолчанию настроен только один worker с именем pool, он слушает 9000 порт на локальном интерфейсе и работает (в моём дистрибутиве) под пользователем www-data (<value name="listen_address">127.0.0.1:9000</value>). Если требуется использовать unix socket в качестве интерфейса с веб-сервером для ускорения обмена трафиком минуя сетевой стек, можно указать его как /var/run/php5-fpm/php5-fpm.sock, главное, чтобы директория /var/run/php5-fpm/ существовала и пользователь, под которым запускается worker, имел право создать там файл сокета.

php.ini:memory_limit и php-fpm.conf:max_children
Итак, для начала нам необходимо определиться с мерой выделения физических ресурсов памяти и процессорного времени, которые мы готовы отдать процессам PHP. К примеру, если максимальная память, выделяемая каждому процессу (определяется директивой memory_limit в php.ini), составляет 32Mb (считается оптимальным на сегодняшний день, однако будет оптимальнее впоследствии определить минимально необходимую), то если мы FPM максимально создавать 6 одновременно работающих процессов (директива <value name="max_children">6</value>), то суммарная память, выделенная под PHP будет составлять 32x6=192 Mb. Если мы, ввиду физических ограничений сервера, не готовы отвести столько, то попытаемся сперва уменьшить выделяемую на каждый процесс память. Если это сделать уже нельзя, то уменьшаем число одновременно загруженных процесса, однако следует помнить, что это обязательно приведёт к такому же снижению числа потенциально возможных одновременно обрабатываемых запросов.

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

php-fpm.conf:max_requests
PHP-FPM обеспечивает жизнь каждого запускаемого им подпроцесса интерпретатора PHP до достижении некоторого числа обработанных им запросов (определяется, например, так <value name="max_requests">200</value>). Путём изменения этого значения можно влиять на два несовместимых свойства такой системы. Чем выше это значение, тем больше запросов будет обработано и, соответственно, меньше времени затрачено на перезапуск интерпретатора и перезагрузку скриптов в него. На стабильной системе (когда обслуживаемый PHP код не вызывает серьёзных проблем при длительной работе) следует выставить это значение как можно больше, но в пределах разумного разумеется, и наоборот, уменьшить при возникновении проблем.

php-fpm.conf:slowlog и php-fpm.conf:request_slowlog_timeout
Эти важные параметры для общего анализа производительности скриптов с целью выявления узких мест часто бывают полезны. Просто выставляем время выполнения скриптов отличным от нуля, и строго говоря, такое, больше которого работа скрипта для вас считается неприемлемой (например как <value name="request_slowlog_timeout">5s</value>). В файле, указанном в качестве slowlog-а (обычно <value name="slowlog">/var/log/php5-fpm.log.slow</value>) у вас появятся скрипты, превысившие это время, с некоторой информацией, полезной для анализа. Если файл, спустя какое-то время остался пуст, поздравляю, всё в порядке ^_.

Пара слов об использовании окружения chroot
Вообще говоря одним из достоинств PHP-FPM из коробки является возможность конфигурировать разные экземпляры worker-ов для различных окружений chroot. Это позволяет сделать работу различных сайтов полностью независимой друг от друга, так, как если бы они физически работали в разных экземплярах операционной системы. В следующих статьях мы, возможно, затронем эту тему в отношении PHP-FPM, однако сейчас ограничимся общей конфигурацией.

Отделение собственных настроек php.ini
Не рекомендую править файлы конфигурации непосредственно, гораздо более изящно использовать modern way, который обеспечит минимум работы при обновлении, это conf.d. Просто создаём файл, например limits.ini и определяем свои ограничения и аналогично.

Несколько слов о eaccelerator
Если сервер обладает достаточными ресурсами оперативной памяти, чтобы вместить все скомпилированные скрипты, рекомендуется отключить дисковый кеш в eaccelerator. Также, если скрипты обновляются редко, рекомендую отключить проверку времени последней модификации файлов скриптов, чтобы eaccelerator по завершении загрузки, байткод компиляции и оптимизации всех скриптов вообще забыл о файловой системе. В последнем случае при обновлении скриптов вам придётся вручную удалить их экземпляры из кеша через admin-скрипт или программно через API.

Скрипт для администрирования eaccelerator в моём дистрибутиве располагается в /usr/share/php5-eaccelerator/control.php Вам потребуется скопировать его в директорию, где он может быть доступен веб-серверу и поправить данные для логина в начеле него. Также, чтобы работал этот скрипт, следует указать eaccelerator-у путь по которому разрешено выполнять административные действия.

Вот фрагмент моего конфига -eaccelerator.ini:
<code>
eaccelerator.enable = "1"
eaccelerator.optimizer = "1"
eaccelerator.check_mtime = "0"
eaccelerator.compress = "1"
eaccelerator.compress_level = "9"
eaccelerator.shm_size = "64"
eaccelerator.shm_only = "1"
eaccelerator.allowed_admin_path = "/var/www/nginx-default/"
</code>
Также необходимо проверить текущие системные ограничения, в частности максимальный объём разделяемой памяти, выделяемый системой, ибо если он окажется меньшим нежели указанный в конфиге, eaccelerator вызовет ошибку. Проверяем так:
<code>
:~$ cat /proc/sys/kernel/shmmax
</code>
В моём случае параметр должен быть равен 67108864 (байт), если он меньше, увеличиваем лимиты:
<code>
sysctl -w kernel.shmmax=67108864
</code>
И дописываем в /etc/sysctl.conf, чтобы значение автоматически применялось после каждой загрузке системы:
<code>
kernel.shmmax = 67108864
</code>
И, наконец, последняя настоятельная рекомендация. Если ваши проекты используют одну базу кода, постарайтесь обеспечить условия, при которых они будут обращаться к единому экземпляру этого кода. Это можно сделать, например, символическими ссылками. Может быть, напишу потом статью, как это сделано у меня.

Memcached

Memcached в целом неплохой кешер, однако у него есть ряд недостатков, которые снижают удобства его использования на серверах с множеством сайтов. Один из таких недостатков - особенность работы одного экземпляра с одним пулом кеша. Все ключи в этом пуле должны быть уникальны, поэтому приходится использовать префиксы ключей, чтобы разделять пул между несколькими проектами или использовать несколько экземпляров процесса, по одному-два на каждый проект. Для небольших серверов предпочтителен средний фариант - использовать 1-2 экземпляра и префиксы ключей.

Memcached в Debian из коробки запускается init.d скриптом, который не позволяет сконфигурировать более одного экземпляра, поэтому пришлось написать другой скрипт. Для того, чтобы он не пересекался с штатным я назвал его /etc/inid.d/memcache:
<code>
#! /bin/sh
#
 
USER=nobody
MAXCONN=1024
OPTIONS=""
DAEMON=/usr/bin/memcached
INSTANCES=/etc/default/memcache
 
RETVAL=0
prog="memcached"
 
list_instances() {
    cat $INSTANCES | grep -v '^[[:space:]]*#' | grep -v '^[[:space:]]*$'
}
 
filter_inst() {
    if [ -n "$*" ]; then
        grep '^\('`echo $* | sed -r 's/[[:space:]]+/\\\\|/g'`'\)'
    else
        cat
    fi
}
 
start_instance() {
        echo -n "Starting $prog ($1): "
        start-stop-daemon --start --quiet --pidfile /var/run/memcached/memcached.$1.pid --exec $DAEMON \
        -- -d -p $2 -u $USER -m $3 -c $MAXCONN -P /var/run/memcached/memcached.$1.pid $OPTIONS
        RETVAL=$?
        echo
        [ $RETVAL -eq 0 ] && touch /var/lock/memcached.$1
}
 
stop_instance() {
        echo -n "Stopping $prog ($1): "
        start-stop-daemon --stop --quiet --oknodo --pidfile /var/run/memcached/memcached.$1.pid --exec $DAEMON
        RETVAL=$?
        echo
        if [ $RETVAL -eq 0 ] ; then
            rm -f /var/lock/memcached.$1
            rm -f /var/run/memcached/memcached.$1.pid
        fi
}
 
start () {
        # insure that /var/run/memcached has proper permissions
        mkdir -p /var/run/memcached
        if [ "`stat -c %U /var/run/memcached`" != "$USER" ]; then
                chown $USER /var/run/memcached
        fi
 
        list_instances | filter_inst $@ | while read inst; do
            start_instance $inst;
    done
}
 
stop () {
    list_instances | filter_inst $@ | while read inst; do
                stop_instance $inst;
        done
}
 
status () {
    cat /var/run/memcached/memcached.*.pid | while read pid; do
        cat /proc/$pid/cmdline | tr -s '\0' ' '
        echo
    done
}
 
restart () {
        stop $@
        start $@
}
 
action="$1"
shift
 
# See how we were called.
case "$action" in
  start)
        start $@
        ;;
  stop)
        stop $@
        ;;
  status)
        status memcached
        ;;
  restart|reload|force-reload)
        restart $@
        ;;
  *)
        echo "Usage: $0 {start|stop|status|restart|reload|force-reload} [instances]"
    echo "Instances: " `list_instances | filter_inst $@ | sed 's/^\([^[:space:]]*\).*$/\1/g'`
        exit 1
esac
 
exit $?
</code>
Также нам потребуется создать конфиг /etc/default/memcache с описанием экземпляров. Вот пример конфига, который создаёт два экземпляра: один, используемый по-умолчанию, для кеширования данных, другой - сессий:
<code>
# Memcache Instances
# name port memory
default 11211 32
session 11411 16
</code>
Нам необходимо позаботиться об отключении запуска memcached сервера родным скриптом и включении запуска созданного нами memcache:
<code>
invoke-rc.d memcached stop # Останавливаем штатный скрипт
invoke-rc.d memcache start # Запускаем новый
update-rc.d -f memcached remove # Отключаем запуск штатного скрипта
update-rc.d memcache defaults # Включаем запуск нового
</code>
Теперь мы можем настроить сайты на Drupal с установленным модулем memcache, добавив следующий шаблонный код в файл sites/<имя>/settings.php:
<code>
function memcache_settings($key_prefix = NULL, $default_pass = 'localhost:11211', $session_pass = 'localhost:11411'){
  global $conf;
 
  if($key_prefix) $conf['memcache_key_prefix'] = $key_prefix.'__';
 
  $conf['cache_inc'] = './sites/all/modules/memcache/memcache.inc';
 
  $conf['memcache_servers'] = array($default_pass => 'default');
 
  $conf['memcache_bins'] = array(
    'cache' => 'default',
    'cache_page' => 'default',
    'cache_menu' => 'default',
    'cache_filter' => 'default',
    'cache_form' => 'default',
    'cache_block' => 'default',
  );
 
  if($session_pass){
    $conf['session_inc'] = './sites/all/modules/memcache/memcache-session.inc';
 
    $session_bin = 'default';
 
    if($session_pass != $default_pass){
      $conf['memcache_servers'][$session_pass] = 'session';
 
      $session_bin = 'session';
    }
 
    $conf['memcache_bins']['session'] = 'session';
    $conf['memcache_bins']['users'] = 'session';
  }
}
memcache_settings('префикс ключей для этого сайта');
</code>

И вот мы подобрались наконец-таки к оптимизации MySQL. Для небольших серверов с целью экономии требуется совершенно другая конфигурация сервера БД, нежели для серьёзных, обслуживающих большую нагрузку. Следует по максимуму урезать число подключаемых внешних модулей, отключив все не нужные. Например, если не требуется использовать движок таблиц innodb, обеспечивающих блокировки на уровне записей таблицы, отключаем его (skip-innodb или innodb = OFF). Аналогично поступаем со всеми остальными возможностями. Отключаем сетевые возможности (skip-networking и skip-name-resolve), если MySQL физически работает в одном экземпляре OS с сервером PHP-FPM. Можно отключить также поддержку кластеризации (skip-ndbcluster).

Здесь также не рекомендую править общий конфиг /etc/mysql/my.cnf и /etc/mysql/debian.cnf, лучше создать файл типа optimize.conf в директории /etc/mysql/conf.d и сосредоточить все свои подкрутки там, например:

<code>
[mysqld]
skip-networking
skip-name-resolve
skip-innodb
skip-ndbcluster
</code>
А крутить MySQL мы будем много и часто, потому как не всегда бывает просто сразу подобрать оптимальное соотношение параметров для работы при конкретном характере нагрузки. Заранее оговоримся, что мы будем оптимизировать работу MySQL с таблицами, использующими движок myisam.

Буфер ключей
Самый важный, на мой взгляд, параметр, с которым нужно определиться прежде всего, это размер буфера ключей (key_buffer). Желательно, чтобы буфер ключей был способен вместить в себя ключи всех таблиц во всех используемых в данных момент баз данных. Узнать суммарный объём ключей можно, например, так:
<code>
du -cskh $(find /var/lib/mysql/ -name '*.MYI') | tail -n1
key_buffer в идеале должен быть больше полученного значения, желательно с некоторым запасом.
</code>
Подбор оптимальных значений параметров
Далее следует ограничить максимальное число одновременных соединений (max_connections) и подобрать оптимальные значения других параметров в соотношении друг с другом и с возможностями сервера, для чего в общем случае следует анализировать текущие характеристики работы сервера под нагрузкой. Однако не умудрённым опытом настройки MySQL администраторам зачастую бывает очень трудно это сделать ибо оптимизация - довольно сложная задача, которая требует глубокого понимания, как всё на самом деле работает.

Существует perl скрипт mysqltuner, который анализирует статистику работы сервера и выдаёт предложения, чего и как нам не помешало бы подкрутить. Первый раз скрипт получает начальные значения и ничего не предлагает, что-то ценное от него можем получить спустя несколько  часов после первого запуска, время, за которое накопятся статистические данные, на основе которых можно судить об оптимальности текущей конфигурации.

mysqltuner может выдать что-то типа:
<code>
-------- General Statistics --------------------------------------------------
[--] Skipped version check for MySQLTuner script
[OK] Currently running supported MySQL version 5.1.41-3
[OK] Operating on 64-bit architecture
 
-------- Storage Engine Statistics -------------------------------------------
[--] Status: -Archive -BDB -Federated -InnoDB -ISAM -NDBCluster
[--] Data in MyISAM tables: 196M (Tables: 848)
[--] Data in MEMORY tables: 0B (Tables: 1)
[!!] Total fragmented tables: 7
 
-------- Performance Metrics -------------------------------------------------
[--] Up for: 3d 3h 16m 28s (11M q [41.624 qps], 48K conn, TX: 7B, RX: 1B)
[--] Reads / Writes: 92% / 8%
[--] Total buffers: 144.0M global + 6.5M per thread (30 max threads)
[OK] Maximum possible memory usage: 339.0M (66% of installed RAM)
[OK] Slow queries: 0% (28/11M)
[OK] Highest usage of available connections: 36% (11/30)
[OK] Key buffer size / total MyISAM indexes: 64.0M/47.4M
[OK] Key buffer hit rate: 99.7% (596M cached / 1M reads)
[OK] Query cache efficiency: 78.5% (8M cached / 10M selects)
[OK] Query cache prunes per day: 0
[OK] Sorts requiring temporary tables: 0% (0 temp sorts / 703K sorts)
[!!] Joins performed without indexes: 151
[OK] Temporary tables created on disk: 22% (186K on disk / 847K total)
[OK] Thread cache hit rate: 99% (13 created / 48K connections)
[!!] Table cache hit rate: 0% (171 open / 71K opened)
[OK] Open file limit used: 15% (326/2K)
[OK] Table locks acquired immediately: 99% (4M immediate / 4M locks)
 
-------- Recommendations -----------------------------------------------------
General recommendations:
    Run OPTIMIZE TABLE to defragment tables for better performance
    Enable the slow query log to troubleshoot bad queries
    Adjust your join queries to always utilize indexes
    Increase table_cache gradually to avoid file descriptor limits
Variables to adjust:
    join_buffer_size (> 4.0M, or always use indexes with joins)
    table_cache (> 1024)
</code>
Менять нам потребуется скорее всего следующие параметры:

table_cache сделать больше 4.0Mb
join_buffer_size увеличить больше 1024
Чтобы обнаружить объединения (JOIN-ы), выполняемые без использования индексов, нужно включить лог медленных запросов в конфиге сервера MySQL, затем добавить опцию логгирования запросов, при выполнении которых СУБД не смогла использовать индексы:
<code>
log_slow_queries = /var/log/mysql/mysql-slow.log
long_query_time = 2
log-queries-not-using-indexes
</code>
Теперь смотрим в mysql-slow.log, пробуем выполнить их в сеансе mysql с инструкцией EXPLAIN в начале и смотрим, что говорит СУБД. Скорее всего нам потребуется одно из следующих решений (от простого к сложному):

Добавить в таблицу недостающие индексы либо исправить существующие
Указать MySQL явно какие индексы требуется использовать (USE INDEX(имя индекса) после имени соответствующей таблицы в объединении)
Модифицировать запрос (разбить сложный запрос на 2-3 более простых, использовать подзапросы и т.п. на что фантазии хватит)
В любом случае использование индексов обязательно для максимальной производительности и подобные вещи нельзя обходить вниманием.

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

Заключение

Мы рассмотрели общие и некоторые частные методики настройки и оптимизации программного обеспечения небольших серверов, но многое мы упустили из рассмотрения ввиду ограниченности всего и вся в этой жизни. Кое-что обязательно будет восполнено в будущих статьях, или внесено в эту. Но всегда нужно помнить, что оптимальная настройка ПО сервера - это лишь небольшая часть работы, которую может проделать администратор для улучшения общей производительности системы. Гораздо более обширная и важная часть работы - это оптимизация структур данных и кода приложений, так что, когда сервер уже настроен и работает, самое время заняться выявлением и устранением узких мест в производительности конечных пользовательских приложений.

 

А теперь два небольших бонуса для тех, кто дочитал до конца и всё ещё с нами :-)

Бонус первый. Красивый путь к загруженным файлам для Drupal
В Drupal два способа отдачи загруженных пользователем файлов:

Статический - средствами сервера
Динамический - через Drupal
Первый способ более оптимален в плане производительности, однако его недостатком является не очень красивый путь к файлу (в общем случае: /sites/<домен_сайта>/files/<имя_файла>)

При втором способе отдача происходит выполнением скрипта, в этом случае можно контролировать доступ к файлам, но для загрузки задействуется бэкэнд PHP-FPM, что тоже не очень хорошо однако путь при этом более удобочитаем (/system/files/<имя_файла>)

Таким образом, если нет надобности в ограничении доступа к файлам, предлагаю воспользоваться третьим комбинированным способом: отдавать файлы мы будем напрямую средствами WEB-сервера, а путь к ним будет как при отдаче через Drupal (/system/files/<имя_файла>)

Для этого в конфиг для Drupal включим следующее правило перезаписи путей:

</ode>
location ^~ /system/files/ {
  rewrite ^(/system/files/)([^?]*) /sites/$host/files/$2 last;
}
</code>
Говорим серверу: если путь начинается с /system/files/ то преобразовать его к виду /sites/<домен_сайта>/files/<имя_файла>. Не забываем при этом обеспечить существование директории /sites/<домен_сайта> либо сделать ссылку на /sites/default, если сайт один или наоборот.

Последнее, что нужно сделать, переключить в админке Drupal способ отдачи файлов на второй (то есть через Drupal).

Бонус второй. Перегрузка (в смысле переопределения) контента
Задача: Хотим дать пользователю возможность виртуально переопределять (добавлять, заменять) некоторые файлы на сайте начиная от его корня, но так, чтобы не давать ему никаких привилегий, которые могли бы позволить ему как-то повлиять на работу движка сайта. Это часто бывает нужно для выполнения некоторых действий, связанных с операциями подтверждения владения доменом, например, при подключении сервисов google, при поисковой оптимизации и тд.

Решение: Создаём каталог .override в корне сайта, в нём мы будет создавать скелет структуры каталогов и файлов относительно корня сайта. Добавляем в правило отдачи статического контента, а конкретно, в директиву try_files первым аргументом /.override$uri, должно получиться так:
<code>
location ~* ^.+\.(jpg|jpeg|gif|css|png|js|ico|html|txt)$ {
  try_files /.override$uri $uri @drupal;
  access_log off;
  expires 31d;
  add_header Last-Modified: $date_gmt;
}
</code>
Бонус третий. Простой способ дать chrooted SFTP доступ пользователям
Задача: Дать пользователям возможность просматривать и загружать некоторое содержимое в каталог на сервере по защищённому протоколу, не позволяя им выполнять каких-либо команд в среде удалённой системы, а также скрывая от них всё, что вне этого их каталога.

Решение: В последнем openssh есть встроенный sftp сервер, использование этой возможности позволит нам без лишних заморочек создать полностью изолированное chroot окружение для пользователей. Прописываем в файле /etc/ssh/sshd_config следующее:
<code>
Subsystem sftp internal-sftp
 
UsePAM yes
 
Match Group sftpchroot
    ChrootDirectory /home/%u/sftp-root
    AllowTCPForwarding no
    X11Forwarding no
    ForceCommand internal-sftp
</code>
Теперь все пользователи, состоящие в группе sftpchroot, заходя на сервер будут наглухо замуровываться в свои каталоги/home/<имя_пользователя>/sftp-root. Все остальные штуки реализуем символьными ссылками, например можем сделать каталог .override некоторого сайта символической ссылкой на sftp-root некоторого пользователя, дав ему тем самым возможность перегружать статический контент на сайте.

P.S.

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

Подписка

RSS-материал


Яндекс.Метрика