UMGUM.COM (лучше) 

LNPMM + Multisite ( Web сервис на базе "Nginx", PHP-FPM, "MySQL", "Supervisord", "Memcached", "mSMTP" и "inCron". )

10 октября 2017  (обновлено 10 июня 2019)

OS: "Linux Debian 8/9", "Linux Ubuntu 16/18 LTS".
Applications: "Nginx", PHP-FPM, "MySQL", "Supervisord", "Memcached", "mSMTP" и "inCron".

Задача: подготовить выделенный сервер для простого развёртывания площадок web-сайтов, по возможности автоматизировав процедуры создания таковых.

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

Последовательность дальнейших действий такова:

1. Подготовка системного окружения.
2. Установка прикладного программного обеспечения.
3. Создание файловой структуры сайтов.
4. Обеспечение SFTP-доступа разработчикам web-сайтов.
5. Настройка PHP-интерпретатора и "пулов" PHP-FPM.
6. Настройка web-сервера "Nginx" и создание конфигурации для сайта.
7. Настройка спарки сервисов "Supervisord" и "Memcached".
8. Настройка подсистемы отправки почты.
9. Поверхностная настройка СУБД "MySQL".
10. Настройка спарки сервисов "inCron" и "Crontab".
11. Автоматизация рутинных процедур.


Подготовка системного окружения.

Обновляем программное обеспечение и устанавливаем набор полезных утилит:

# apt-get update && apt-get upgrade
# apt-get install aptitude coreutils sudo acl psmisc net-tools dnsutils host htop iotop tree findutils mc vim pwgen ntpdate dirmngr nfs-client socat

Удаляем ненужные нам подсистемы:

# aptitude purge snapd lxd lxcfs open-iscsi mdadm

Если устанавливается "Linux Ubuntu", то лучше бы отключить автоматическое обновление компонентов системы - неприятно бывает обнаружить, что через полгода директория "/boot" забита новыми версиями ядра, установка которых явно не разрешалась.

Устанавливаем точное системное время:

# echo "Asia/Novosibirsk" > /etc/timezone
# rm /etc/localtime && ln -sf /usr/share/zoneinfo/Asia/Novosibirsk /etc/localtime

Для корректировки отклонений внутренних часов заведём регулярную (через три часа) сверку с общемировым временем:

# vi /etc/crontab

....
0 */3 * * * root ntpdate asia.pool.ntp.org &

Ненужную нам подсистему синхронизации времени посредством "Systemd" отключаем:

# systemctl disable systemd-timesyncd
# systemctl stop systemd-timesyncd

В свежеустановленном Linux-сервере запросто может не оказаться полноценной поддержки "национальной кодировки" (в нашем случае - русской). Обычно на новых серверах я не трачу время на проверки, а сразу перехожу в режим конфигурирования, добавляя нужные кодировки и удаляя невостребованные:

# dpkg-reconfigure locales

Естественно, кроме всего прочего, активируем пункт "ru_RU.utf8". В качестве варианта "по умолчанию", применяемого для приложений не требующих определённой кодировки, я выбираю "en_US.UTF-8".

Удостоверяемся в успешности изменения перечня поддерживаемых кодировок:

# locale -a

C.UTF-8
en_US.utf8
ru_RU.utf8

Защита административного входа.

Сразу после инсталляции базовых компонентов операционной системы запрещаем сетевой вход через SSH для суперпользователя.

# vi /etc/ssh/sshd_config

....
# Отключаем возможность удалённого входа для суперпользователя
PermitRootLogin no

# Запрещаем использование "пустых" паролей при подключении
PermitEmptyPasswords no

# Запрещаем передачу пользовательских переменных окружения
PermitUserEnvironment no

# Запрещаем перенаправление портов пользователя и транзит подключений (это конечный сервер, а не "шлюз")
GatewayPorts no
X11Forwarding no
....

Предварительно проверяем корректность изменений конфигурационного файла:

# sshd -t

Перезапускаем OpenSSH-сервер:

# /etc/init.d/ssh reload

Установка программного обеспечения.

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

Устанавливаем web-сервер "Nginx":

# aptitude install nginx apache2-utils

Устанавливаем PHP-интерпретатор:

# aptitude install php7.2-fpm php7.2-cgi php7.2-cli php7.2-opcache php7.2-pgsql php7.2-mysqli php-memcache php-mongodb php7.2-mbstring php-pclzip php7.2-xml php-sockets php7.2-json php7.2-gd php7.2-curl php-geoip php7.2-ldap php7.2-zip php7.2-xmlrpc php7.2-soap php7.2-intl

Устанавливаем утилиты отправки почты через преднастроенный шлюз:

# aptitude install msmtp

Устанавливаем СУБД "MySQL" (в "Linux Ubuntu" это "Percona MySQL"):

# aptitude install mysql-server

Устанавливаем NoSQL-сервер "Memcached":

# aptitude install memcached

Устанавливаем сервис запуска и контроля статуса приложений "Supervisord":

# aptitude install supervisor

Устанавливаем сервис отслеживания изменений файлов и запланированной реакции на них:

# aptitude install incron

Создание опорной файловой структуры сайтов.

Наверняка корень файловой структуры для web-сайтов уже имеется - но не помешает профилактически его обновить:

# mkdir -p /var/www
# chown root:root /var/www && chmod go+rx /var/www && chmod go-w /var/www

Через "POSIX ACL" предпишем устанавливать всем создаваемым файлам (и директориям) ниже по иерархии разрешения полного доступа как для пользователя, так и группы (в отличии от системных установок "umask 0022", разрешающим на уровне группы только чтение), при этом полностью убираем доступ всем остальным:

# setfacl --set default:user::rwX,default:group::rwX,default:other:--- /var/www

После задания глобальных параметров доступа файловой структуры набора сайтов, для просмотра списка сайтов разрешаем заходить в корень структуры всем (но не глубже - правило не рекурсивное):

# setfacl --modify group::rX,other:rX /var/www

Никогда не мешает проконтролировать результаты предпринятых действий:

# getfacl /var/www

Создание файловой структуры групп сайтов.

Для набора web-сайтов, объединённых какой-то логикой (например особенностями конфигурации PHP, используемыми общими ресурсами или уровнями безопасности), создаём файловые структуры играющие роль сводных площадок (запрещая при этом кому бы то ни было изменять корневую структуру директорий):

# mkdir -p /var/www/group0
# setfacl --modify user::rX,group::rX /var/www/group0
# setfacl --modify user:www-data:X /var/www/group0

Для каждой сводной площадки создаём необходимые служебные директории:

# mkdir -p /var/www/group0/conf
# mkdir -p /var/www/group0/home
# mkdir -p /var/www/group0/log
# mkdir -p /var/www/group0/mnt
# mkdir -p /var/www/group0/tmp

Заранее добавляем опорного пользователя (и одноимённую группу), от имени которого будут работать сайты сводной площадки:

# useradd --system --shell /bin/false --no-create-home --home-dir /var/www/group0/home --user-group www-group0

Передаём файловую структуру "группы сайтов" во владение опорному пользователю:

# chown -R www-group0:www-group0 /var/www/group0

Удовлетворяя требованиям OpenSSH/SFTP-сервера корневую директорию (и только её, не рекурсивно) сводной площадки группы web-ресурсов передаём в собственность суперпользователю:

# chown root:www-group0 /var/www/group0
# chmod g+rx /var/www/group0

Если этого не сделать, то "SSHd" откажется принимать подключения со следующим уведомлением в журнале событий:

fatal: bad ownership or modes for chroot directory "/var/www/group0"

Создание файловой структуры сайтов.

Добавляем директории web-сайтов как таковых:

# mkdir -p /var/www/group0/site.example.net
# mkdir -p /var/www/group0/site.example.net/www

Переводим файловую структуру сайта во владение опорного пользователя (рекурсивно, для всех поддиректорий):

# chown -R www-group0:www-group0 /var/www/group0/site.example.net
# chmod -R ug+rw /var/www/group0/site.example.net
# setfacl --recursive --modify user::rwX,group::rwX,other:--- /var/www/group0/site.example.net

Защищаем от изменений структуры корневую директорию сайта, разрешая всем только чтение содержимого:

# setfacl --modify user::rX,group::rX,other:--- /var/www/group0/site.example.net

Предоставляя доступ к файловой структуре сайтов web-серверу "Nginx", разрешаем пользователю, от имени которого он запускается, чтение содержимого предназначенной к публикации директории:

# setfacl --modify user:www-data:X /var/www/group0/site.example.net
# setfacl --recursive --modify user:www-data:rX /var/www/group0/site.example.net/www
# setfacl --recursive --modify default:user:www-data:rX /var/www/group0/site.example.net/www

Наладка ротации файлов журналов событий web-сайтов.

Как уже понятно из заготавливаемой файловой структуры, где мы при каждой группе сайтов создаём директорию "/var/www/group0/log", журналы событий предполагается хранить где-то здесь, рядом с сайтами как таковыми и в лёгкой доступности web-разработчикам. Дабы журналы не росли бесконтрольно, настроим их автоматическое урезание и ротацию:

# mkdir -p /etc/logrotate.d
# vi /etc/logrotate.d/web

/var/www/*/log/*.log {
  # обрезаем файл по достижению заданного размера
  size 30M
  # отсутствие файла не должно вызывать ошибку
  missingok
  # не отрабатываем пустые файлы
  notifempty
  # количество хранимых отработанных резервных копий
  rotate 5
  # сжимаем отрабатываемые резервные копии для экономии места
  compress
  # не сжимаем первую резервную копию, делая это при повторном проходе
  delaycompress
  # указываем копировать данные журнала в архивный и зачищать действующий (что не потребует перезапуска приложения для перехода на новый файл)
  copytruncate
  # указываем пользователя, от имени которого мы манипулируем файлами журналов (при этом сохраняются разрешения исходного файла журнала)
  su root root
}

Проверяем корректность конфигурационного файла:

# logrotate -d /etc/logrotate.d/web

Обеспечение доступа к файловым ресурсам разработчикам web-сайтов.

Добавляем пользователя, от имени которого будет подключаться web-разработчик сайта (таких может быть несколько, разумеется), обязательно делая его членом опорной группы пользователей целевого сайта:

# useradd --shell /bin/bash --no-create-home --home-dir /var/www/group0/home --gid www-group0 developer0-group0

Для корректного назначения разрешений в файловой системе очень важно, чтобы у подключающегося к серверу для редактирования файлов сайта пользователя основной (первичной, указанной в "/etc/passwd") группой была выделенная для этого сайта. Мы это задали при создании пользователя, но, перестраховываясь, явно указываем пользователю нужную группу:

# usermod --gid www-group0 developer0-group0

Чтобы дать пользователю возможность читать журналы событий web-сервера, вводим его в группу такового:

# usermod --append --groups www-data developer0-group0

Доступ web-разработчиков к ресурсам сайтов будем обеспечивать с помощью SFTP, функции сервера "OpenSSH". Как правило, в абсолютном большинстве случаев, web-разработчикам достаточно возможности правки файлов только их web-ресурса. Мало того, на моей практике программисты PHP/Python способны лишь сломать выстроенную работающую структуру, но никогда не могут её починить или улучшить. Потому, во избежание, замыкаем их в "chroot"-е, предоставляя доступ только к файловой системе их web-ресурсов.

Исходя из того, что сервер "OpenSSH" уже работает и настроен, вносим в конфигурацию лишь необходимые изменения:

# vi /etc/ssh/sshd_config

....

# Запрещаем пользователю передачу своих переменных окружения
PermitUserEnvironment no

# Устанавливаем режим проверки права доступа пользователя к целевым объектам
StrictModes yes

# Включаем дополнительную функциональность SFTP-сервиса в OpenSSH (отключаем перевод на внешнее приложение, активируя встроенные возможности)
# Subsystem  sftp  /usr/lib/openssh/sftp-server
Subsystem  sftp  internal-sftp
....

# Запираем пользователей группы "www-group0" в пределах директории их web-ресурсов
Match Group www-group0 User *,!spec0-group0
  AllowTcpForwarding no
  ChrootDirectory /var/www/group0
  ForceCommand internal-sftp -u 0007

Набором параметров "Match" все пользователи "группы сайтов" (в примере "www-group0") изолируются с доступом только посредством SFTP, но для ряда специфичных задач могут быть созданы особые пользователи (вроде "spec0-group0"), на которых правило изоляции не должно распространяться - их мы исключением выводим из под действия конструкции "Match".

Важный параметр "-u 0007" команды "ForceCommand" в групповых настройках переопределяет системные установки "umask 0022", разрешающие членам группы только чтение общих файловых ресурсов, расширяя привилегии группы до уровня владельца файла. Этим мы добиваемся гарантированного доступа к разделяемым файлам web-ресурса как для PHP-интерпретатора, так и для web-разработчиков, могущих в разное время перехватывать владение файлами внутри структуры, не ломая при этом логики разрешений доступа, явно запрещая при этом доступ всем остальным (полностью снимая разрешения для "other").

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

Проверяем синтаксическую корректность внесённых изменений и применяем их:

# sshd -t && /etc/init.d/ssh reload

Обеспечение разработчикам возможности исполнения команд.

Если вдруг мы решили всё-таки предоставить web-разработчикам помимо доступа к файловым ресурсам (посредством SFTP/FTPS) ещё и возможность работы в командной строке web-сервера - а это плохая идея! - то понадобиться включить для пользователя возможность через SUDO запускать на исполнение команды в окружении привилегий и переменных сводной площадки.

Используя функционал расширения конфигурации SUDO добавляем правило в отдельный файл конфигурации:

# vi /etc/sudoers.d/web-shell-users

....
user1-group0 ALL=(www-group0:www-group0) NOPASSWD: ALL
user2-group0 ALL=(www-group0:www-group0) NOPASSWD: ALL

Обязательно проверяем синтаксическую корректность вносимых изменений (правила как таковые применяются немедленно без явного на то указания):

# visudo -cf /etc/sudoers.d/web-shell-users

Ограничиваем доступ посторонних к файлу конфигурации:

# chown root:root /etc/sudoers.d/web-shell-users
# chmod go-rwx /etc/sudoers.d/web-shell-users

Таким образом, например, можно будет тестировать PHP-скрипты их прямым запуском:

user1-group0:$ sudo -u www-group0 php -q -f ./script.php

Создание служебного пользователя для CI/CD-процедур.

Заранее добавляем в группу "сводной площадки" web-сайтов специализированного пользователя, от имени которого в дальнейшем будут осуществляться автоматизированные запросы к Git-репозитариями хранения, разработки, тестирования и публикации:

# useradd --shell /usr/bin/git-shell --create-home --home-dir /home/git-group0 --gid www-group0 git-group0

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

# mkdir -p /home/git-group0/.ssh
# touch /home/git-group0/.ssh/authorized_keys

Обязательно полностью закрываем директорию пользователя от доступа посторонних:

# chown -R git-group0:www-group0 /home/git-group0
# chmod -R go-rwx /home/git-group0

Более в этой заметке о пользователе для фоновых задач "git-group0" упоминать не будем - это заготовка для отдельно рассматриваемой реализации простейшего функционала CI/CD.

Обобщённая настройка PHP.

Настройка PHP-интерпретатора двухэтапна - вначале подкорректируем ряд общих параметров, а потом добавим индивидуальности в описании экземпляров PHP-FPM, запускаемого для каждой группы сайтов отдельно.

В современном "Linux Debian/Ubuntu" пакет PHP-интерпретатора поставляется в виде трёх идеологически разделённых компонентов: PHP-FPM, PHP-CGI и PHP-CLI - каждый из которых настраивается индивидуальными конфигурационными файлами, изначально идентичными. Мне представляется самым простым сконфигурировать один набор параметров и распространить их на все компоненты PHP перезаписью файла (одно время я пытался делать это через указание на "основной" файл настроек символическими ссылками, но практика показала, что отдельные файлы лучше вписываются в идеологию дистрибуции и обновления программного обеспечения):

Копированием произвольного создаём условно "главный" файл конфигурации:

# cp /etc/php/7.2/fpm/php.ini /etc/php/7.2/php-main.ini

Рассматривать здесь полную настройку не вижу смысла и обращу внимание лишь на особенности, необходимые для работы типичного PHP-сайта:

# vi /etc/php/7.2/php-main.ini

....
cgi.fix_pathinfo = 0
allow_url_fopen = Off
....
short_open_tag = On
....
date.timezone = Asia/Novosibirsk
....
memory_limit = 1024M
....
max_execution_time = 300
max_input_time = 300
post_max_size = 64M
upload_max_filesize = 64M
max_file_uploads = 20
max_input_vars = 1000
....
; Отключаем новый и пока невостребованный функционал PHPv7, некстати перекрывающий привычные настройки PCRE
pcre.jit = 0
....
pcre.backtrack_limit = 100000
pcre.recursion_limit = 100000
....
mbstring.default_charset = UTF-8
mbstring.internal_encoding = UTF-8
mbstring.detect_order = "UTF-8"
mbstring.encoding_translation = on
mbstring.strict_detection = on
....
opcache.enable = 1
opcache.memory_consumption = 256
opcache.max_accelerated_files = 500000
opcache.validate_timestamps = 1
opcache.use_cwd = 1
opcache.revalidate_path = 1
opcache.revalidate_freq = 0
....

Всегда есть смысл проверять, какие появились изменения в конфигурационных файлах - вдруг кто-то уже внёс точечные правки:

# diff /etc/php/7.2/php-main.ini /etc/php/7.2/cgi/php.ini

...или:

# vimdiff /etc/php/7.2/php-main.ini /etc/php/7.2/cgi/php.ini

После того, как мы удостоверились, что перезапись конфигурационных файлов сервисам не повредит, делаем это:

# cp /etc/php/7.2/php-main.ini /etc/php/7.2/cgi/php.ini
# cp /etc/php/7.2/php-main.ini /etc/php/7.2/cli/php.ini
# cp /etc/php/7.2/php-main.ini /etc/php/7.2/fpm/php.ini

Если всё-таки потребуется перекрыть какой-то параметр индивидуально компоненту PHP, то иногда это возможно сделать через директивы "php_admin_value" и "php_admin_flag" в настройках "пула" PHP-FPM.

Настройка "пула" PHP-FPM.

Прежде всего удаляем дистрибутивный конфигурационный файл с настройками "пула" PHP-FPM:

# rm /etc/php/7.2/fpm/pool.d/www.conf

Каждой группе сайтов "сводной площадки" будем заводить отдельный экземпляр ("пул") PHP-FPM (их может быть несколько, обслуживающих разные web-сервисы):

# vi /etc/php/7.2/fpm/pool.d/group0.conf

; Блок описания отдельного инстанса PHP-FPM
[group0]

; Указываем запускать инстанс от имени опорного пользователя "сводной площадки"
user = www-group0
group = www-group0

; Задаём точку приёма FCGI-запросов от вышестоящего web-прокси (Nginx в нашем случае)
;listen = 127.0.0.1:9001
;listen.allowed_clients = 127.0.0.1
listen = /var/run/php/php-fpm-group0.sock

; Явно задаём владельца точки входа FCGI-запросов и разрешения для доступа к ней web-прокси
listen.owner = www-data
listen.group = www-data
listen.mode = 0600

; Режим запуска инстанса
pm = dynamic
; Количество процессов, запускаемых при старте PHP-FPM
pm.start_servers = 20
; Максимальное количество процессов, которые могут быть запущены для обработки запросов
pm.max_children = 512
; Параметры количества запущенных неактивных процессов (находящихся в ожидании запросов)
pm.min_spare_servers = 5
pm.max_spare_servers = 20
; Количество запросов, после которого процесс будет перезапущен (для компенсации "утечек памяти" в скриптах)
pm.max_requests = 4096

; Журнал событий доступа к ресурсам (если не объявлен, то журналирование не активизируется)
;access.log = /var/www/group0/log/php-fcgi.access.log

; Очень полезный журнал запросов, блокирующих ресурсы среднестатистически длительное время
slowlog = /var/www/group0/log/php-fcgi.slow.log
; Порог реакции на время (три секунды) исполнения PHP-скрипта, после которого событие запишется в журнал "slowlog"
request_slowlog_timeout = 3s

; Блок переопределения в среде исполнения PHP-FPM глобальных параметров PHP-интерпретатора
php_admin_value[sys_temp_dir] = /var/www/group0/tmp
php_admin_value[post_max_size] = 300M
php_admin_value[upload_max_filesize] = 300M
php_admin_flag[allow_url_fopen] = On

Следует иметь в виду, что не все параметры PHP-интерпретатора можно переопределить динамически на уровне "пула" PHP-FPM или командами самого PHP - нужно сверяться с документацией.

Проверяем синтаксическую корректность внесённых изменений и применяем таковые:

# php -e -c /etc/php/7.2/fpm/php.ini -r 'echo "OK\n";';
# php-fpm7.2 -t --fpm-config /etc/php/7.2/fpm/pool.d/group0.conf
# /etc/init.d/php7.2-fpm reload

Оптимизация работы PHP с дисковой подсистемой.

Для интерпретатора PHP, обслуживающего сайты, активно создающие и читающие файлы сессий, выгодно вынести (параметром "tmpdir") эту работу в файловую систему, смонтированную в область памяти ОЗУ.

Место сохранения сессий в PHP определяется параметров "session.save_path" и по умолчанию оно располагается в директории "/var/lib/php/sessions". Точнее всего это выявляется через вывод функции "php_info()". Мне представляется самым простым смонтировать поверх этой директории кусочек "tmpfs":

# vi /etc/fstab

....
# Tuning PHP-sessions`s place
tmpfs /var/lib/php/sessions tmpfs rw,nosuid,nodev,size=2045M,uid=root,gid=root,mode=41733 0 0
....

Я бы выделил под эту файловую систему до 10-15% от всей ОЗУ (она не заблокирует всё заявленное место, а будет выбирать блоки памяти по мере появления необходимости).

Учитывая то, что в общей куче сохраняются сессий всех пользователей, ресурсы которых тем не менее не должны смешиваться, необходимо задать директории особый набор разрешений доступа, работающий по следующему принципу:

1. Полный доступ в директорию только у суперпользователя.
2. Все могут только создать файл внутри ("-wx"), но удаление файлов явно запрещено ("--t").
3. PHP-интерпретатор при создании файла разрешает доступ к нему только владельцу.
4. Таким образом, впоследствии PHP-интерпретатор сможет прочитать только файл с известным ему именем, в контексте только того пользователя, который его создал.

Заготавливаем файловую структуру и задаём разрешения доступа к таковой:

# mkdir -p /var/lib/php/sessions
# chown root:root /var/lib/php/sessions
# chmod go-r /var/lib/php/sessions
# chmod g+wx /var/lib/php/sessions
# chmod o+wt /var/lib/php/sessions

Сразу настраиваем простейший "сборщик мусора", удаляющий устаревшие файлы сессий - ибо их нарастает действительно много:

# vi /etc/crontab

....
# Garbage collection for old files sessions of PHP (lifetime 31 days; in minutes)
03 */3 * * * root [ -d /var/lib/php/sessions ] && nice find /var/lib/php/sessions -type f -cmin +44640 -exec rm -f {} \; 1>/dev/null &
....

Обобщённая настройка web-сервера "Nginx".

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

# vi /etc/sysctl.d/30-sl-nginx.conf

# Увеличиваем ограничение на количество открытых соединений к файловым сокетам (default: 128)
net.core.somaxconn=4096

# Увеличиваем ограничение на количество ожидающих запросов открытия соединений к файловым сокетам (default: 1000)
net.core.netdev_max_backlog=2048

Применяем здесь и сейчас параметры конфигурации:

# sysctl -p -f /etc/sysctl.d/30-sl-nginx.conf

Заготовим место для размещения файлов SSL-сертификатов сайтов за "Nginx":

# mkdir -p /etc/ssl/nginx && chmod -R go-rwx /etc/ssl/nginx

Для последующего включения в "Nginx" современного HTTPv2 генерируем DH-сертификат:

# openssl dhparam -out /etc/ssl/nginx/dhparam.2048.pem 2048

Для очень старых операционных систем и браузеров (Windows XP IE6, Java 6) заготовим DH-сертификат попроще:

# openssl dhparam -out /etc/ssl/nginx/dhparam.1024.pem 1024

Слегка дополняем глобальную конфигурацию web-сервера:

# vi /etc/nginx/nginx.conf

# Явно запускаем Nginx в рамках привbлегий пользователя "www-data" и группы "www-data"
user www-data www-data;
....

# Задаём число рабочих процессов (для начала отталкиваемся от количества ядер CPU; default: 1)
worker_processes 24;

# Указываем максимальное число открытых файлов (RLIMIT_NOFILE) для рабочих процессов (default: "ulimit -n" ~ 1024)
# (на каждое обрабатываемое соединение выделяется по два файловых дескриптора)
# (worker_rlimit_nofile = worker_processes * worker_connections * 2)
worker_rlimit_nofile 50000;
....

events {
  # Задаём максимальное число соединений, которые одновременно может открыть рабочий процесс (default: 512)
  worker_connections 1024;
  ....
}

http {
  ....
  tcp_nodelay on;
  tcp_nopush on;

  # Запрещаем web-серверу сообщать о себе подробные данные
  server_tokens off;

  # Запрещаем просмотр содержимого директории, если не указан целевой файл
  autoindex off;

  # Слегка увеличиваем размер области памяти для хранения перечня обслуживаемых имён сайтов (default: 32)
  server_names_hash_bucket_size 64;

  # Отключаем проверку размера тела передаваемого PHP-FPM запроса (default: 1m)
  client_max_body_size 0;

  # Увеличиваем размер блока данных, обрабатываемого в памяти без сохранения на диск (default: 16K)
  client_body_buffer_size 4M;

  # Подгоняем под параметры PHP время ожидания ответа от скриптов
  fastcgi_send_timeout 300;
  fastcgi_read_timeout 300;
....

Если для большого количества сайтов используется один "wildcard" SSL-сертификат, то есть смысл вынести блок описания его настроек в файл и подключать его только там, где потребуется:

# vi /etc/nginx/ssl_wildcard.conf

# SSL/TLS Settings
ssl_protocols SSLv3 TLSv1 TLSv1.1 TLSv1.2;
# Old backward compatibility (Windows XP IE6, Java 6)
ssl_ciphers HIGH:SEED:AES128-SHA:AES256-SHA:DES-CBC3-SHA:RC4-SHA:RC4-MD5:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!RSAPSK:!aDH:!aECDH;
ssl_prefer_server_ciphers on;
ssl_dhparam /etc/ssl/nginx/dhparam.2048.pem;
ssl_certificate /etc/ssl/nginx/wildcard.example.net.crt;
ssl_certificate_key /etc/ssl/nginx/wildcard.example.net.key.decrypt;
# SSL Caching
ssl_session_cache shared:SSL:30m;
ssl_session_timeout 1h;
# SSL Strict Optional
add_header Strict-Transport-Security max-age=15768000;
# SSL Verify Optional
ssl_stapling on;

Создание в "Nginx" конфигурации web-сайта.

Прежде всего удаляем дистрибутивный конфигурационный файл с настройками web-сайта "по умолчанию":

# rm /etc/nginx/sites-enabled/default

Создаём конфигурацию нашего первого web-сайта:

# vi /etc/nginx/sites-available/site.example.net.conf

# Блок отлова HTTP-обращений и перенаправления их на HTTPS
server {
  listen 80;
  server_name www.site.example.net site.example.net;
  location / {
    rewrite ^ https://site.example.net$request_uri permanent;
  }
}

# Блок отлова обращений по префиксу "www." и перенаправления их на "Canonical URL"
server {
  listen 443 ssl http2;
  server_name www.site.example.net;
  ssl on;
  include /etc/nginx/ssl_wildcard.conf;
  location / {
    rewrite ^ https://site.example.net$request_uri permanent;
  }
}

# Описание параметров web-сайта как такового
server {

  # Указываем сетевые порты и протоколы для приёма клиентских подключений
  listen 443 ssl http2;

  # Перечисляем обслуживаемые FQDN сайта
  server_name site.example.net;

  # Принудительно переводим сайт на работу только через SSL
  ssl on;
  include /etc/nginx/ssl_wildcard.conf;

  # Месторасположение файлов журналов событий сайта
  access_log /var/www/group0/log/nginx-site.example.net-access.log;
  error_log /var/www/group0/log/nginx-site.example.net-error.log;

  # Выключаем невостребованную перекодировку контента
  charset off;

  # Задаём переменную с многократно используемым параметром PHP-FPM
  set $php_pass unix:/var/run/php/php-fpm-group0.sock;

  root /var/www/group0/site.example.net/www;
  index index.html index.htm index.phtml index.php;

  # Подставляем свои страницы обработки ошибок
  error_page 403 /403.html;
  error_page 404 /404.html;
  error_page 500 502 503 504 /50x.html;

  # ACL фильтрующий клиентские IP
  #allow 10.0.0.0/12;
  #allow 172.16.0.0/12;
  #allow 192.168.0.0/16;
  #deny all;

  # Глобальный обработчик запросов
  location / {
    # Обработчик событий отсутствия запрашиваемого файла
    try_files $uri $uri/ =404;
  }

  # Блокируем доступ к типовым "закрытым" ресурсам
  location ~* (/\.ht|/\.hg|/\.git|/\.svn|/\.subversion|/\.inc|/\.sys|/\.local|/\.env|/\.enabled|/\.config|/\.profile) {
    deny all;
    log_not_found off;
    access_log off;
  }

  # Блокируем доступ к ресурсам, которые часто забывают спрятать
  location ~* (/conf|/cnf|/inc|/log/|/tmp/|/temp/|/runtime|/back|/bkp|/bak|/old|/test|/protected|/base|/database|/exchange|/phpshell|/cli|/bin|\.zip|\.gzip|\.gz|\.sql|\.py|\.perl|\.tpl|\.sh|\.bash|\.dist|\.orig|\.back|\.bak|\.conf|phpinfo.php) {
    deny all;
    log_not_found off;
  }

  # Блокируем доступ к классически неправильно и опасно именованным файлам (вроде ".php.1")
  location ~* \.(phtml|php)(?!(\?|\/|$)) {
    deny all;
    log_not_found off;
  }

  # Обработчик прямых обращений к PHP-скриптам
  location ~* \.(phtml|php)$ {
    try_files $uri =404;
    include /etc/nginx/fastcgi_params;
    fastcgi_pass $php_pass;
    fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
  }

  # Не реагируем на неинтересные события загрузок
  location = /(favicon.ico|robots.txt|sitemap.xml) {
    log_not_found off;
    access_log off;
  }

  # Напрямую отдаём "статические" данные, предлагая браузеру сохранить их в своём "кеше", и не фиксируем эти события
  location ~* ^.+\.(jpg|jpeg|gif|png|svg|js|css|mp3|ogg|mpe?g|avi|zip|gz|bz2?|rar|swf|xml|txt)$ {
    try_files $uri =404;
    expires 30d;
    access_log off;
  }
}

Включаем конфигурационный файл, проверяем его синтаксическую корректность и применяем:

# ln -s /etc/nginx/sites-available/site.example.net.conf /etc/nginx/sites-enabled/site.example.net.conf
# nginx -t
# /etc/init.d/nginx reload

Укладываем в корне сайта текстовый файл (если таковой отсутствует) и пробуем его получить через браузер:

# echo "site.example.net" > /var/www/group0/site.example.net/www/index.html
$ wget http://site.example.net/

У "Nginx" на мой взгляд недоработана подсистема журналирования событий - он сам не создаёт файлы, а ожидает их наличия. Хорошо хоть в случае их отсутствия сервер продолжает работу, просто не сохраняя никуда сообщения о событиях. Файлы журналов создаются скриптовой обёрткой "SysV" или "Systemd" на этапе запуска или перезагрузки конфигурации сервера, но сделано это неудобно - несмотря на то, что рабочие потоки "Nginx" могут действовать от имени специфичного пользователя, журнальные файлы всё равно скорее всего (в зависимости от реализации серверного окружения) будут отданы во владении суперпользователя "root".

Приходится после создания и активации каждого нового сайта переопределять права доступа к журнальным файлам "Nginx":

# chown www-group0:www-group0 /var/www/group0/log/*
# chmod g+rw /var/www/group0/log/*

Обобщённая настройка "Memcached".

"Memcached" представляет собой сервис, хранящий в оперативной памяти некоторые данные с заданным временем жизни. Доступ к данным осуществляется по ключу (имени). По сути это "хэш"-таблица. В самом простом случае в нем хранят сессии пользователей, коды "капч" или накапливают перед сбросом в СУБД долговременного хранения сведения о текущих процессах.

Дистрибутивный скрипт запуска "Memcached" откровенно нефункциональный, да ещё и неработающий после перехода на "Systemd", так что его проще заблокировать и воспользоваться для запуска сконфигурированных в нужном нам виде инстансов "Memcached" сервисом "Supervisord" (написан на Python, и проверен на высоконагруженных сервисах).

Блокируем автоматический запуск "Memcached" посредством подсистемы "SysV":

# /etc/init.d/memcached stop && update-rc.d memcached disable

Блокируем автоматический запуск "Memcached" посредством подсистемы "Systemd":

# systemctl stop memcached && systemctl disable memcached

Удаляем дистрибутивный файл конфигурации по умолчанию:

# rm /etc/memcached.conf

По идее унификации локальные файловые "сокеты" надо бы располагать примерно здесь: "/var/run/memcached/*.sock" - однако "Memcached" не умеет вначале стартовать, строить себе окружение, а уже потом уходить в работу от непривилегированного пользователя, так что создать себе ресурс в доступной только суперпользователю директории "/run" он не может. Приходится для простоты посредством "Supervisord" запускать предварительно, с максимальным приоритетом, процедуру создания нужной директории:

# vi /etc/supervisor/conf.d/memcached-preset.conf

[program:memcached-preset]
umask=0000
command=/bin/bash -c 'mkdir -p /var/run/memcached && chmod ugo+rwX /var/run/memcached'
stdout_logfile=/var/log/supervisor/memcached-preset.log
redirect_stderr=true
priority=0
autostart=true
autorestart=false
startsecs=0
startretries=1

Настройка инстанса "Memcached" в "Supervisord".

Каждой группе сайтов "group0" будем создавать по отдельному инстансу "Memcached", доступному через локальный файловый "сокет" только соответствующему пользователю "www-group0" (так проще всего ограничить доступ):

# vi /etc/supervisor/conf.d/memcached-group0.conf

[program:memcached-group0]
user=www-group0
command=/usr/bin/memcached -m 128 -c 1024 -t 4 -s /var/run/memcached/memcached-group0.sock -a 770 -v
stdout_logfile=/var/log/supervisor/memcached-groupo.log
redirect_stderr=true
autostart=true
autorestart=true
stopsignal=KILL
numprocs=1

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

# supervisorctl reread
# supervisorctl update

Просматриваем статус запущенных в "Supervisord" приложений:

# supervisorctl status

memcached-preset EXITED ...
memcached-group0 RUNNING ...

Простейшим способом проверяем доступность для выделенного опорного пользователя запущенного инстанса "Memcached":

# echo 'stats' | sudo -u www-group0 nc.openbsd -U /var/run/memcached/memcached-group0.sock

...или:

# echo 'stats' | sudo -u www-group0 socat - UNIX-CONNECT:/var/run/memcached/memcached-group0.sock

Ротация файлов журналов "супервизора" и его дочерних сервисов изначально не настроена вообще - исправляем упущение:

# vi /etc/logrotate.d/supervisor && logrotate -d /etc/logrotate.d/supervisor

/var/log/supervisor/*.log {
  size 2M
  missingok
  notifempty
  rotate 3
  compress
  delaycompress
  copytruncate
  su root root
}

Настройка подсистемы отправки почты.

В самых простых web-сайтах на PHP функцию "mail()" реализуют через системную утилиту "sendmail" или её заменители в MTA. Однако в сложных проектах со множеством профилей транзитных почтовых серверов удобнее использовать легковесного почтового клиента "mSMTP" - с ним можно как просто пересылать сообщения через прозрачный почтовый шлюз, так и подключаться к почтовым провайдерам вроде "GMail" с многоступенчатой авторизацией.

Простенький "mSMTP" не умеет создавать файл журнала событий, так что делаем это за него:

# touch /var/log/msmtp.log && chmod ugo+rw /var/log/msmtp.log

Приложение "mSMTP" способно полностью эмулировать классический "sendmail", так что для совместимости с настроеным по умолчанию программным обеспечением есть смысл сделать символическую ссылку, направляющую обращения в нужное место:

# ln -s /usr/bin/msmtp /usr/sbin/sendmail

Для начала настроим один профиль, используемый по умолчанию:

# vi /etc/msmtprc

# Set default values for all following accounts.
defaults
auth off
tls off
logfile /var/log/msmtp.log

# Default Account
account default
host mx.example.net
port 25
from www-site@example.net

Тестируем работоспособность схемы путём отправки почтового сообщения:

# echo "Test" | msmtp --debug --account=default test@site.example.net

loaded system configuration file /etc/msmtprc
....
<-- 220 mx.example.net ESMTP Exim ...
--> EHLO localhost
....
<-- 250 OK id=...
--> QUIT
<-- 221 mx.example.net closing connection

Пробуем посредством PHP-интерпретатора отсылать почту через "mSMTP":

# sudo -u www-group0 /usr/bin/php -r "mail('test@site.example.net', 'Test', 'Test');"

Настройка СУБД "MySQL".

Предварительно создадим или обновим параметры прав доступа директории хранения временных файлов - она будет затронута при оптимизации работы СУБД:

# mkdir -p /var/lib/mysql/tmp
# chown -R mysql:mysql /var/lib/mysql/tmp
# chmod -R go-rwx /var/lib/mysql/tmp

Слегка дополняем глобальную конфигурацию СУБД:

# vi /etc/mysql/mysql.cnf

....
[mysql]

# Явно указываем предпочтительную "кодировку" СУБД
default_character_set = utf8
....

[mysqld]

# Для совместимости с грязно-кодными CMS (вроде "Bitrix") выключаем строгое следование SQL-стандартам
sql_mode = ''

# Явно указываем предпочтительную "кодировку" данных при приёме подключений к серверу
character_set_server = utf8
collation_server = utf8_unicode_ci
#
init_connect='SET collation_connection = utf8_general_ci'
init_connect='SET NAMES utf8'

# Выносим временные файлы в удобное нам место
tmpdir = /var/lib/mysql/tmp
innodb_tmpdir = /var/lib/mysql/tmp

# Включаем работу с клиентами через локальный файловый "сокет"
socket = /var/run/mysqld/mysqld.sock

# Разрешаем подключаться только с "localhost"
bind-address = 127.0.0.1

# Увеличиваем лимит размера временной таблицы, обрабатываемой в ОЗУ, до сброса её в файловую систему
tmp_table_size = 512M

# Увеличиваем лимит размера создаваемых пользователем MEMORY-таблицы, обрабатываемой в ОЗУ
max_heap_table_size = 512M

# Чуть увеличиваем лимиты буферов, до которых блоки данных будут кешироваться
query_cache_limit = 10M
query_cache_size = 256M
key_buffer_size = 512M # (default: 8M; optimal: 20% RAM)

# Увеличиваем размер буфера запросов и журнала транзакций
innodb_buffer_pool_size = 2G # (default: 128MB; optimal: 60% RAM)
innodb_log_file_size = 512M # (default: 5MB)
innodb_log_buffer_size = 256M # (default: 8MB)

# Указываем записывать журнал транзакций примерно один раз в секунду, а не немедленно после каждого изменения данных (default: 1)
innodb_flush_log_at_trx_commit = 2

# Увеличиваем количество параллельных потоков чтения/записи (default: 4)
innodb_read_io_threads = 16
innodb_write_io_threads = 16

# Включаем поддержку нового формата файлов DB
innodb_file_format = barracuda

# Включаем поддержку длинных индексных ключей
innodb_large_prefix = 1

# Явно указываем создавать для каждой таблицы отдельные файлы описаний (.frm) и данных (.ibd) на диске, а не сваливать всё в один (ibdataX)
innodb_file_per_table = 1
....

[client]

# Явно указываем предпочтительную "кодировку" данных при подключении к серверу
default_character_set = utf8
....

Параметры СУБД "MySQL" раскиданы по нескольким файлам в директории "/etc/mysql", так что есть смысл убедится, не переопределяются ли они где-то в "/etc/mysql/mysql.conf.d/".

Проверить корректность синтаксиса конфигурационных файлов MySQL-сервера можно следующим способом:

# mysqld --verbose --help 1>/dev/null

... [ERROR] unknown variable 'table_cache=512'
... [ERROR] Aborting

После проверки синтаксической корректности перезапускаем СУБД для применения конфигурации:

# /etc/init.d/mysql restart

Проконтролировать фактическое применение параметров можно посредством SQL-запроса:

# mysql -u root -p

mysql> SHOW VARIABLES WHERE `Variable_name` LIKE '%tmpdir%';
+-------------------+--------------------+
| Variable_name     | Value              |
+-------------------+--------------------+
| innodb_tmpdir     | /var/lib/mysql/tmp |
| slave_load_tmpdir | /var/lib/mysql/tmp |
| tmpdir            | /var/lib/mysql/tmp |
+-------------------+--------------------+
3 rows in set (0.00 sec)

Практически все ныне эксплуатируемые сайты хранят данные к кодировке "UTF-8", так что обязательно добиваемся этого для всех параметров:

mysql> SHOW VARIABLES LIKE 'char%';
+--------------------------+--------+
| Variable_name            | Value  |
+--------------------------+--------+
| character_set_client     | utf8   |
| character_set_connection | utf8   |
| character_set_database   | utf8   |
| character_set_filesystem | binary |
| character_set_results    | utf8   |
| character_set_server     | utf8   |
| character_set_system     | utf8   |
+--------------------------+--------+

Аналогично есть смысл проверить параметры сортировки и сопоставления данных: "SHOW VARIABLES LIKE 'collation%';".

Оптимизация работы "MySQL" с дисковой подсистемой.

Для СУБД, активно создающей и уничтожающей файлы временных таблиц, выгодно вынести (параметром "tmpdir") эту работу в файловую систему, смонтированную в область памяти ОЗУ:

# vi /etc/fstab

....
# Tuning the location of MySQL temporary files
tmpfs /var/lib/mysql/tmp tmpfs rw,nosuid,nodev,size=2G,uid=mysql,gid=mysql,mode=0750 0 0
....

# mount /var/lib/mysql/tmp

Я бы выделил под эту файловую систему до 25% от всей ОЗУ (она не заблокирует всё заявленное место, а будет выбирать блоки памяти по мере появления необходимости).

Понижение уровня параноидальности "MySQL".

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

# mysql -u root -p

mysql> SHOW GLOBAL VARIABLES LIKE 'validate_password%';
+--------------------------------------+--------+
| Variable_name                        | Value  |
+--------------------------------------+--------+
| validate_password_check_user_name    | OFF    |
| validate_password_dictionary_file    |        |
| validate_password_length             | 8      |
| validate_password_mixed_case_count   | 1      |
| validate_password_number_count       | 1      |
| validate_password_policy             | MEDIUM |
| validate_password_special_char_count | 1      |
+--------------------------------------+--------+
mysql>
mysql> uninstall plugin validate_password;
mysql>
mysql> SHOW GLOBAL VARIABLES LIKE 'validate_password%';
Empty set (0.00 sec)

Настройка "Crontab" в произвольном месте файловой системы.

Учитывая то, что подключающиеся к web-площадке по SFTP и замкнутые в в "chroot" web-разработчики не имеют доступа к содержимому директории "/var/spool/cron/crontabs/", придётся сделать небольшой финт ушами, предлагая редактировать условный "crontab" в доступном им месте, а по событию изменения этого файла синхронизировать его его содержимое с системным.

Осуществим это через компонент мониторинга изменений файлов "Inotify", входящий в ядро "Linux" начиная с версии "2.6.13".

Включаем мониторинг событий появления конфигурационных файлов в директории "./conf/" и переинициализации при этом правил наблюдения "incrond", а также изменения регламентировано именуемых "./conf/crontab" файлов подставных "crontab"-ов с запуском экспорта содержимого таковых в обслуживаемое системой пользовательское окружение:

# vi /etc/incron.d/web-crontab-export

#InCron_table_does_not_support_comments_with_several_(more_than_one)_words!
#<directory>_<file_change_mask>_<command_or_action>_<options>

/var/www/group0/conf IN_CREATE /etc/init.d/incron reload &
/var/www/group0/conf/crontab IN_MODIFY,IN_CLOSE_WRITE,IN_MOVE crontab -u www-group0 $@

Параметр "$@" в конфигурации InCron указывает подставить сюда полный путь к объекту файловой системы, событие которого обрабатывается.

Сервис "incrond" мониторит состояние своих конфигурационных файлов и немедленно применяет изменения:

# tail -100 /var/log/syslog

....
... incrond[...]: system table web-crontab-export changed, reloading

Создаём подставной "crontab"-файл из потенциально уже имеющегося системного:

# crontab -u www-group0 -l > /var/www/group0/conf/crontab
# chown www-group0:www-group0 /var/www/group0/conf/crontab
# chmod g+w /var/www/group0/conf/crontab && chmod o-rwx /var/www/group0/conf/crontab

Если файл "/var/www/group0/conf/crontab" отсутствует или случайно удалён, то достаточно его создать снова, как функциональность будет восстановлена.

Настройка простейшего резервного копирования данных.

Подключаем внешнее файловое хранилище:

# vi /etc/fstab

....
# Full backup file storage
10.20.30.40:/volume1/backup_example.net/ /mnt/backup nfs _netdev,rw 0 0
....

Создаём точку монтирования и подключаем внешнее хранилище в файловую систему:

# mkdir -p /mnt/backup
# mount /mnt/backup
# mkdir -p /mnt/backup/web
# chown -R root:root /mnt/backup/web
# chmod -R go-rwx /mnt/backup/web

Пишем простейший скрипт, копирующуи и упаковывающий файлы web-сайтов, а также снимающий "бинарный дамп" БД "MySQL":

# mkdir -p /usr/local/etc/backup
# vi /usr/local/etc/backup/backup-web-full.sh && chmod ugo+x /usr/local/etc/backup/backup-web-full.sh

#!/bin/bash

# Месторасположение директорий для резервных копий.
DPATH="/var/backups/web"

# Копируем содержимое директорий web-сайтов как таковых, без служебной и дополнительной пользовательской информации.
tar -c -z -f "${DPATH}/example.www.$(date +%Y-%m-%dT%H:%M).tar.gz" /var/www/*/*/www/

# Полный срез баз данных MySQL посредством XtraBackup.
xtrabackup --backup --stream=tar --target-dir="${DPATH}/" | gzip - > "${DPATH}/example.mysql-xtra.$(date +%Y-%m-%dT%H:%M).tar.gz"

exit ${?}

Настраиваем автоматический запуск процедуры, например дважды в неделю, в Понедельник и Четверг:

# vi /etc/crontab

....
# Full web-sites backup
0 2 * * 0,3 root /usr/local/etc/backup/backup-web-full.sh &

Автоматизация рутинных процедур.

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


Заметки и комментарии к публикации:


Оставьте свой комментарий ( выразите мнение относительно публикации, поделитесь дополнительными сведениями или укажите на ошибку )