Applications: "Nginx", PHP-FPM, "MySQL".
Задача: подготовить несущий web-сервер, удовлетворяющий требованиям системы управления процессом преподавания посредством "виртуальной рабочей среды", реализованной в виде комплексного PHP-приложения "Moodle" (от аббревиатуры "Modular Object-Oriented Dynamic Learning Environment", "модульная объектно-ориентированная динамическая обучающая среда"). Попросту говоря - это CMS (вообще такого рода программное обеспечение уже выделяется в отдельный класс LMS - "Learning Management System"), позволяющая действительно просто развёртывать сайты для онлайн-обучения.
Прикладное администрирование LMS "Moodle" не будет рассматриваться - да я и не знаю о нём практически ничего - здесь лишь об оптимизации системного окружения, позволяющего web-сервису работать быстрее и надёжнее.
Предварительная подготовка файловой системы.
Самому классическому web-серверу более чем достаточно 50GB на все его нужды, но объём данных учебных курсов LMS "Moodle" может возрастать самым непредсказуемым образом - в зависимости от аппетитов преподавателей. Удобнее всего завести отдельный виртуальный диск для данных, начальными размером до 200-250 Гигабайт, который можно будет расширять по мере появления необходимости, не затрагивая при этом операционную систему.
Исходя из того, что системный диск уже размечен и введён в работу, добавляем диск (в нашем случае второй, предположительно именованный как "sdb") и файловую систему для хранения данных (разумеется, мы живём с LVM):
# pvcreate /dev/sdb
# vgcreate vg1 /dev/sdb
# lvcreate --zero y -l 100%FREE -n lvmoodledata vg1
# mkfs.ext4 -L moodle-data /dev/vg1/lvmoodledata
# vgcreate vg1 /dev/sdb
# lvcreate --zero y -l 100%FREE -n lvmoodledata vg1
# mkfs.ext4 -L moodle-data /dev/vg1/lvmoodledata
Защищаем административный вход.
Сразу после инсталляции базовых компонентов операционной системы запрещаем сетевой вход через SSH для суперпользователя.
# vi /etc/ssh/sshd_config
....
# Отключаем возможность удалённого входа для суперпользователя
PermitRootLogin no
# Запрещаем использование "пустых" паролей при подключении
PermitEmptyPasswords no
# Запрещаем передачу пользовательских переменных окружения
PermitUserEnvironment no
# Запрещаем перенаправление портов пользователя и транзит подключений (это конечнй сервер, а не "шлюз")
GatewayPorts no
X11Forwarding no
....
# Отключаем возможность удалённого входа для суперпользователя
PermitRootLogin no
# Запрещаем использование "пустых" паролей при подключении
PermitEmptyPasswords no
# Запрещаем передачу пользовательских переменных окружения
PermitUserEnvironment no
# Запрещаем перенаправление портов пользователя и транзит подключений (это конечнй сервер, а не "шлюз")
GatewayPorts no
X11Forwarding no
....
Предварительно проверяем корректность изменений конфигурационного файла и перезапускаем OpenSSH-сервер:
# sshd -t
# /etc/init.d/ssh reload
# /etc/init.d/ssh reload
Устанавливаем программное обеспечение.
Прежде всего обновим имеющееся ПО и укомплектуем операционную систему утилитами комфорта:
# aptitude update
# aptitude full-upgrade
# aptitude install coreutils sudo acl psmisc net-tools dnsutils host htop iotop tree findutils mc vim pwgen ntpdate dirmngr
# aptitude full-upgrade
# aptitude install coreutils sudo acl psmisc net-tools dnsutils host htop iotop tree findutils mc vim pwgen ntpdate dirmngr
Некоторые современные и потому модные подсистемы чаще всего лишние - иногда их есть смысл удалить, чтобы не путались "под ногами":
# aptitude purge snapd lxd lxcfs
Устанавливаем наборы пакетов приложений требующихся непосредственно для развёртывания web-сервиса. Полезно сделать это заранее, чтобы далее работать уже с набором автоматически созданных инсталляторами необходимых директорий и файлов.
PHP-интерпретатор:
# aptitude install php-fpm php-cgi php-cli php-opcache php-pgsql php-mysqli php-memcache php-mongodb php-mbstring php-pclzip php-xml php-sockets php-json php-gd php-curl php-geoip php-ldap php-zip php-xmlrpc php-soap php-intl php-pspell
Web-сервер:
# aptitude install nginx apache2-utils
Почтовая утилита:
# aptitude install msmtp
СУБД (для ОС до 2020-го):
# aptitude install mysql-server
СУБД (для ОС после 2020-го):
Скачиваем и применяем PGP-ключ, которым подписано содержимое официального APT-репозитория "MySQL", формируем выделенный конфигурационный файл описания подключения к репозиторию и устанавливаем ПО:
# apt-key adv -v --keyserver pgp.mit.edu --recv-keys 5072E1F5
# echo -e "# Official APT-repository MySQL\ndeb [arch=amd64] deb http://repo.mysql.com/apt/$(. /etc/os-release; echo "$ID") $(lsb_release -cs) mysql-8.0" >> /etc/apt/sources.list.d/mysql.list
# aptitude update && aptitude install -y mysql-server
# echo -e "# Official APT-repository MySQL\ndeb [arch=amd64] deb http://repo.mysql.com/apt/$(. /etc/os-release; echo "$ID") $(lsb_release -cs) mysql-8.0" >> /etc/apt/sources.list.d/mysql.list
# aptitude update && aptitude install -y mysql-server
LMS "Moodle" для работы требуются специфичное ПО - устанавливаем его и проверяем, появились ли нужные утилиты по ожидаемым путям:
# aptitude --without-recommends install texlive-extra-utils
# ls /usr/bin/latex
# ls /usr/bin/dvips
# ls /usr/bin/dvisvgm
# ls /usr/bin/latex
# ls /usr/bin/dvips
# ls /usr/bin/dvisvgm
# aptitude --without-recommends install aspell aspell-en aspell-ru
# ls /usr/bin/aspell
# ls /usr/bin/aspell
# aptitude --without-recommends install graphicsmagick-imagemagick-compat
# ls /usr/bin/convert
# ls /usr/bin/convert
# aptitude --without-recommends install ghostscript gsfonts
# ls /usr/bin/gs
# ls /usr/bin/gs
# aptitude --without-recommends install graphviz
# ls /usr/bin/dot
# ls /usr/bin/dot
# aptitude --without-recommends install unoconv
# ls /usr/bin/unoconv
# ls /usr/bin/unoconv
Устанавливаем точное системное время.
Виртуальный сервер синхронизирует свои часы через несущую систему виртуализации, но изначально может потребоваться явно указать желаемый часовой пояс (пример для Новосибирска):
# echo "Asia/Novosibirsk" > /etc/timezone
# rm /etc/localtime
# ln -sf /usr/share/zoneinfo/Asia/Novosibirsk /etc/localtime
# rm /etc/localtime
# ln -sf /usr/share/zoneinfo/Asia/Novosibirsk /etc/localtime
Синхронизируемся с каким-нибудь солидным сервером:
# ntpdate 0.asia.pool.ntp.org
Если это аппаратный сервер, то сохраняем полученные показатели времени в постоянную память BIOS:
# hwclock --systohc --utc
Дополняем системный набор национальных кодировок.
В свежеустановленном Linux-сервере запросто может не оказаться полноценной поддержки "национальной кодировки" (в нашем случае - русской). Обычно на новых серверах я не трачу время на проверки, а сразу перехожу в режим конфигурирования, добавляя нужные кодировки и удаляя невостребованные:
# dpkg-reconfigure locales
Естественно, кроме всего прочего, активируем пункт "ru_RU.utf8". В качестве варианта "по умолчанию", применяемого для приложений не требующих определённой кодировки, я выбираю "en_US.UTF-8".
Удостоверяемся в успешности изменения перечня поддерживаемых кодировок:
# locale -a
C.UTF-8
en_US.utf8
ru_RU.utf8
en_US.utf8
ru_RU.utf8
Файловая структура сайтов.
Исходя из задачи создания выделенного web-сервера, предназначенного для обслуживания одного-двух сайтов, не пытаемся выдумать что-то эксклюзивное в структуре файловых ресурсов, в просто следуем общепринятой идеологии иерархии, лишь слегка дополняя её более точными и удобными определениями прав доступа:
# mkdir -p /var/www
# chown root:root /var/www
# chmod go-w /var/www
# chmod go+rx /var/www
# chown root:root /var/www
# chmod go-w /var/www
# chmod go+rx /var/www
Через расширение POSIX ACL предпишем устанавливать всем создаваемым файлам (и директориям) разрешения полного доступа как для пользователя, так и группы (в отличии от системных установок "umask 0022", разрешающим группе только чтение), при этом полностью убираем доступ всем остальным:
# setfacl --no-mask --set default:user::rwX,default:group::rwX,default:other:--- /var/www
Опция "--no-mask" в команде выше важна - она прямо указывает не использовать какие-либо фильтры при вычислении "итоговых допусков", устанавливая буквально только те, что указаны.
После задания глобальных параметров доступа файловой структуры набора сайтов, для просмотра списка сайтов разрешаем заходить в корень структуры всем (но не глубже - правило не рекурсивное):
# setfacl --no-mask --modify group::rX,other:rX /var/www
Смотреть результаты - так:
# getfacl /var/www
Добавляем директории ресурсов LMS-сайта как такового:
# mkdir -p /var/www/moodle.example.net
# mkdir -p /var/www/moodle.example.net/moodledata
# mkdir -p /var/www/moodle.example.net/tmp
# mkdir -p /var/www/moodle.example.net/www
# mkdir -p /var/www/moodle.example.net/moodledata
# mkdir -p /var/www/moodle.example.net/tmp
# mkdir -p /var/www/moodle.example.net/www
Настройка PHP.
Предварительно создадим или обновим параметры прав доступа директории хранения файлов web-сессий - в дальнейшем мы слегка это оптимизируем:
# mkdir -p /var/lib/php/sessions
# chmod go-r /var/lib/php/sessions
# chmod +t /var/lib/php/sessions
# chmod go-r /var/lib/php/sessions
# chmod +t /var/lib/php/sessions
Рассматривать здесь полную настройку PHP-интерпретатора не вижу смысла и обращу внимание лишь на особенности, необходимые для работы LMS "Moodle":
# vi /etc/php/7.2/fpm/php.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 = 200M
upload_max_filesize = 200M
max_file_uploads = 100
max_input_vars = 10000
....
; Отключаем новый и пока невостребованный функционал PHPv7, некстати перекрывающее привычные настройки PCRE
pcre.jit = 0
....
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
....
session.gc_maxlifetime = 864000
....
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 = 200M
upload_max_filesize = 200M
max_file_uploads = 100
max_input_vars = 10000
....
; Отключаем новый и пока невостребованный функционал PHPv7, некстати перекрывающее привычные настройки PCRE
pcre.jit = 0
....
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
....
session.gc_maxlifetime = 864000
....
Обязательно проверяем корректность конфигурации, простейшим тестированием:
# php -e -c /etc/php/7.2/fpm/php.ini -r 'echo "OK\n";';
В настройках FPM-пула изменим ряд параметров для достижения лучшей производительности:
# vi /etc/php/7.2/fpm/pool.d/www.conf
; Блок описания отдельного инстанса PHP-FPM (их может быть несколько, обслуживающих разные web-сервисы)
[www]
....
; Указываем пользователя, от имени которого PHP-FPM будет запущен
user = www-data
group = www-data
....
; Явно переключаемся на работу через локальный файловый "сокет", существенно снижая задержки при вызовах (открытие файла всегда быстрее сетевой операции), указывая при этом владельца "сокета"
; listen = 127.0.0.1:9000
listen = /run/php/php-fpm.sock
....
listen.owner = www-data
listen.group = www-data
....
; Режим запуска инстанса
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
....
; Блок переопределения в среде исполнения PHP-FPM глобальных параметров PHP-интерпретатора
php_admin_value[sys_temp_dir] = /var/www/moodle.example.net/tmp
php_admin_value[post_max_size] = 300M
php_admin_value[upload_max_filesize] = 300M
php_admin_flag[allow_url_fopen] = On
[www]
....
; Указываем пользователя, от имени которого PHP-FPM будет запущен
user = www-data
group = www-data
....
; Явно переключаемся на работу через локальный файловый "сокет", существенно снижая задержки при вызовах (открытие файла всегда быстрее сетевой операции), указывая при этом владельца "сокета"
; listen = 127.0.0.1:9000
listen = /run/php/php-fpm.sock
....
listen.owner = www-data
listen.group = www-data
....
; Режим запуска инстанса
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
....
; Блок переопределения в среде исполнения PHP-FPM глобальных параметров PHP-интерпретатора
php_admin_value[sys_temp_dir] = /var/www/moodle.example.net/tmp
php_admin_value[post_max_size] = 300M
php_admin_value[upload_max_filesize] = 300M
php_admin_flag[allow_url_fopen] = On
Проверяем корректность конфигурации и перезапускаем FPM-сервис:
# php-fpm7.2 -t --fpm-config /etc/php/7.2/fpm/pool.d/www.conf
# /etc/init.d/php7.2-fpm reload
# /etc/init.d/php7.2-fpm reload
Оптимизация работы PHP с дисковой подсистемой.
Для ускорения операций создания и поддерживания PHP-интерпретатором web-"сессий" удобно вынести директорию файлов хранения таковых в специально созданную директорию, смонтированную в область ОЗУ - на мой взгляд это проще и надёжнее применения в этом качестве "Memcached" и тому подобных дополнительных инструментов.
Место сохранения сессий в 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
....
# Tuning PHP-sessions`s place
tmpfs /var/lib/php/sessions tmpfs rw,nosuid,nodev,size=2045M,uid=root,gid=root,mode=41733 0 0
....
# mount /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 &
....
# 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 &
....
Настраиваем Nginx.
Слегка корректируем глобальную конфигурацию web-сервиса:
# vi /etc/nginx/nginx.conf
# Явно запускаем Nginx в рамках привилегий пользователя "www-data" и такой же группы "www-data"
user www-data www-data;
....
worker_processes 24;
....
events {
worker_connections 1024;
....
}
http {
....
tcp_nodelay on;
tcp_nopush on;
# Запрещаем web-серверу сообщать о себе подробные данные
server_tokens off;
# Запрещаем просмотр содержимого директории, если не указан целевой файл
autoindex off;
# Отключаем проверку размера тела передаваемого PHP-FPM запроса
client_max_body_size 0;
....
user www-data www-data;
....
worker_processes 24;
....
events {
worker_connections 1024;
....
}
http {
....
tcp_nodelay on;
tcp_nopush on;
# Запрещаем web-серверу сообщать о себе подробные данные
server_tokens off;
# Запрещаем просмотр содержимого директории, если не указан целевой файл
autoindex off;
# Отключаем проверку размера тела передаваемого PHP-FPM запроса
client_max_body_size 0;
....
Описываем конфигурацию типового web-сайта под управлением LMS "Moodle":
# vi /etc/nginx/sites-available/moodle.example.net.conf
# Перехватываем все запросы по протоколу без шифрования и перенаправляем их на HTTPS
server {
listen 80;
server_name www.moodle.example.net moodle.example.net;
access_log off;
error_log off;
location / {
rewrite ^ https://moodle.example.net$request_uri permanent;
}
}
# Перехватываем запросы к имени нежелательного формата и перенаправляем к нужному
server {
listen 443 ssl http2;
server_name www.moodle.example.net;
ssl on;
include /etc/nginx/ssl_wild_params;
access_log off;
error_log off;
location / {
rewrite ^ https://moodle.example.net$request_uri permanent;
}
}
# Описывем рабочее окружение web-сайта LMS "Moodle" как такового
server {
listen 443 ssl http2;
server_name moodle.example.net;
# Принудительно переводим сайт на работу только через SSL
ssl on;
include /etc/nginx/ssl_wild_params;
access_log /var/log/nginx/moodle.example.net-access.log;
error_log /var/log/nginx/moodle.example.net-error.log;
# Выключаем невостребованную перекодировку контента
charset off;
# Задаём переменную с многократно используемым параметром PHP-FPM
set $php_pass unix:/var/run/php/php-fpm.sock;
root /var/www/moodle.example.net/www;
index index.html index.htm index.php;
# Подствляем свои страницы обработки ошибок
error_page 403 /403.html;
error_page 404 /404.html;
error_page 500 502 503 504 /50x.html;
# Глобальный обработчик событий отсутствия запрашиваемого файла
location / {
try_files $uri $uri/ =404;
}
# Блокируем доступ к типовым "закрытым" ресурсам
location ~* (/\.ht|/\.hg|/\.svn|/\.git|/\.enabled|/\.config) {
deny all;
log_not_found off;
access_log off;
}
# Блокируем доступ к классически неправильно и опасно именованным файлам (вроде ".php.1")
location ~* \.(phtml|php)(?!(\?|\/|$)) {
deny all;
log_not_found off;
}
# Выносим директорию хранения файлов за пределы сайта
location /moodledata/ {
internal;
# Путь обязательно должен завершаться символом "/"
alias /var/www/moodle.example.net/moodledata/;
}
# Обработчик прямых обращений к PHP-скриптам
location ~ ^(.+\.php)(.*)$ {
fastcgi_split_path_info ^(.+\.php)(.*)$;
include /etc/nginx/fastcgi_params;
fastcgi_pass $php_pass;
fastcgi_param PATH_INFO $fastcgi_path_info;
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;
}
}
server {
listen 80;
server_name www.moodle.example.net moodle.example.net;
access_log off;
error_log off;
location / {
rewrite ^ https://moodle.example.net$request_uri permanent;
}
}
# Перехватываем запросы к имени нежелательного формата и перенаправляем к нужному
server {
listen 443 ssl http2;
server_name www.moodle.example.net;
ssl on;
include /etc/nginx/ssl_wild_params;
access_log off;
error_log off;
location / {
rewrite ^ https://moodle.example.net$request_uri permanent;
}
}
# Описывем рабочее окружение web-сайта LMS "Moodle" как такового
server {
listen 443 ssl http2;
server_name moodle.example.net;
# Принудительно переводим сайт на работу только через SSL
ssl on;
include /etc/nginx/ssl_wild_params;
access_log /var/log/nginx/moodle.example.net-access.log;
error_log /var/log/nginx/moodle.example.net-error.log;
# Выключаем невостребованную перекодировку контента
charset off;
# Задаём переменную с многократно используемым параметром PHP-FPM
set $php_pass unix:/var/run/php/php-fpm.sock;
root /var/www/moodle.example.net/www;
index index.html index.htm index.php;
# Подствляем свои страницы обработки ошибок
error_page 403 /403.html;
error_page 404 /404.html;
error_page 500 502 503 504 /50x.html;
# Глобальный обработчик событий отсутствия запрашиваемого файла
location / {
try_files $uri $uri/ =404;
}
# Блокируем доступ к типовым "закрытым" ресурсам
location ~* (/\.ht|/\.hg|/\.svn|/\.git|/\.enabled|/\.config) {
deny all;
log_not_found off;
access_log off;
}
# Блокируем доступ к классически неправильно и опасно именованным файлам (вроде ".php.1")
location ~* \.(phtml|php)(?!(\?|\/|$)) {
deny all;
log_not_found off;
}
# Выносим директорию хранения файлов за пределы сайта
location /moodledata/ {
internal;
# Путь обязательно должен завершаться символом "/"
alias /var/www/moodle.example.net/moodledata/;
}
# Обработчик прямых обращений к PHP-скриптам
location ~ ^(.+\.php)(.*)$ {
fastcgi_split_path_info ^(.+\.php)(.*)$;
include /etc/nginx/fastcgi_params;
fastcgi_pass $php_pass;
fastcgi_param PATH_INFO $fastcgi_path_info;
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;
}
}
Активируем конфигурацию:
# rm /etc/nginx/sites-enabled/default
# ln -s /etc/nginx/sites-available/moodle.example.net.conf /etc/nginx/sites-enabled/moodle.example.net.conf
# ln -s /etc/nginx/sites-available/moodle.example.net.conf /etc/nginx/sites-enabled/moodle.example.net.conf
Проверяем синтаксическую корректность новой конфигурации и указываем её принять:
# nginx -t
# /etc/init.d/nginx reload
# /etc/init.d/nginx reload
Настройка СУБД.
Предварительно создадим или обновим параметры прав доступа директории хранения временных файлов - она будет затронута при оптимизации работы СУБД:
# mkdir -p /var/lib/mysql/tmp
# chown -R mysql:mysql /var/lib/mysql/tmp
# chmod -R go-rwx /var/lib/mysql/tmp
# chown -R mysql:mysql /var/lib/mysql/tmp
# chmod -R go-rwx /var/lib/mysql/tmp
Основное требование, предъявляемое LMS "Moodle" к "MySQL" - поддержка формата хранения данных в "InnoDB":
# vi /etc/mysql/mysql.conf.d/mysqld.cnf
[mysqld]
....
# Обязательно включаем работу с клиентами через файловый "сокет"
socket = /var/run/mysqld/mysqld.sock
# Разрешаем подключаться по сетевому протоколу только с "localhost"
bind-address = 127.0.0.1
# Явно указываем месторасположение директории временных файлов СУБД (позже оптимизируем её работу)
tmpdir = /var/lib/mysql/tmp
innodb_tmpdir = /var/lib/mysql/tmp
....
# * InnoDB
#
# Включаем поддержку нового формата файлов DB
#innodb_file_format = barracuda # (deprecated)
# Включаем поддержку длинных индексных ключей
#innodb_large_prefix = 1 (deprecated)
# Явно указываем создавать для каждой таблицы отдельные файлы описаний (.frm) и данных (.ibd) на диске, а не сваливать всё в один (ibdataX)
innodb_file_per_table = 1
# Указываем записывать журнал транзакций примерно один раз в секунду, а не немедленно после каждого изменения данных
innodb_flush_log_at_trx_commit = 2
# Увеличиваем количество параллельных потоков чтения/записи (по умолчанию: 4)
innodb_read_io_threads = 16
innodb_write_io_threads = 16
# Слегка расширяем объём буфера данных в ОЗУ
innodb_buffer_pool_size = 4G // ~60% RAM
# Увеличиваем размер журнала транзакций
innodb_log_file_size = 256M
....
....
# Обязательно включаем работу с клиентами через файловый "сокет"
socket = /var/run/mysqld/mysqld.sock
# Разрешаем подключаться по сетевому протоколу только с "localhost"
bind-address = 127.0.0.1
# Явно указываем месторасположение директории временных файлов СУБД (позже оптимизируем её работу)
tmpdir = /var/lib/mysql/tmp
innodb_tmpdir = /var/lib/mysql/tmp
....
# * InnoDB
#
# Включаем поддержку нового формата файлов DB
#innodb_file_format = barracuda # (deprecated)
# Включаем поддержку длинных индексных ключей
#innodb_large_prefix = 1 (deprecated)
# Явно указываем создавать для каждой таблицы отдельные файлы описаний (.frm) и данных (.ibd) на диске, а не сваливать всё в один (ibdataX)
innodb_file_per_table = 1
# Указываем записывать журнал транзакций примерно один раз в секунду, а не немедленно после каждого изменения данных
innodb_flush_log_at_trx_commit = 2
# Увеличиваем количество параллельных потоков чтения/записи (по умолчанию: 4)
innodb_read_io_threads = 16
innodb_write_io_threads = 16
# Слегка расширяем объём буфера данных в ОЗУ
innodb_buffer_pool_size = 4G // ~60% RAM
# Увеличиваем размер журнала транзакций
innodb_log_file_size = 256M
....
LMS "Moodle" сильно хочет, чтобы СУБД предоставляла ей возможность сохранять данные в современной кодировке "utf8mb4", поддерживающей "четырёхбайтные символы" (в частности, для особо детализированных "смайликов"). С точки зрения общей функциональности это непринципиально, но в общем неплохо заранее перейти на более современные форматы хранения данных:
# vi /etc/mysql/mysql.conf.d/mysqld.cnf
[client]
default-character-set = utf8mb4
....
[mysql]
default-character-set = utf8mb4
....
[mysqld]
....
character-set-server = utf8mb4
collation-server = utf8mb4_unicode_ci
skip-character-set-client-handshake
....
default-character-set = utf8mb4
....
[mysql]
default-character-set = utf8mb4
....
[mysqld]
....
character-set-server = utf8mb4
collation-server = utf8mb4_unicode_ci
skip-character-set-client-handshake
....
Проверяем корректность синтаксиса конфигурационных файлов MySQL-сервера и перезапускаем таковой:
# mysqld --verbose --help 1>/dev/null
# /etc/init.d/mysql restart
# /etc/init.d/mysql restart
Проконтролировать фактическое применение параметров можно посредством SQL-запроса:
# mysql
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)
+-------------------+--------------------+
| 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)
Оптимизация работы 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
....
# 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% от всей ОЗУ (она не заблокирует всё заявленное место, а будет выбирать блоки памяти по мере появления необходимости).
Создаём БД для Moodle-сайта.
# mysql
MySQL> CREATE DATABASE `moodle_example_net` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
MySQL> CREATE USER 'moodle_example_net'@'localhost' IDENTIFIED BY 'dbPassword';
MySQL> GRANT ALL PRIVILEGES ON `moodle_example_net`.* TO 'moodle_example_net'@'localhost';
MySQL> FLUSH PRIVILEGES;
MySQL> CREATE USER 'moodle_example_net'@'localhost' IDENTIFIED BY 'dbPassword';
MySQL> GRANT ALL PRIVILEGES ON `moodle_example_net`.* TO 'moodle_example_net'@'localhost';
MySQL> FLUSH PRIVILEGES;
Настраиваем подсистему отправки почты.
В самых простых сайтах функцию "mail()" PHP реализуют через системную утилиту "sendmail" или её заменители в MTA. Однако в сложных проектах со множеством профилей транзитных почтовых серверов удобнее использовать легковесного почтового клиента "mSMTP" - с ним можно как просто пересылать сообщения через прозрачный почтовый шлюз, так и подключаться к почтовым провайдерам вроде "GMail" с многоступенчатой авторизацией.
Журнал регистрации событий утилита сама создавать не умеет, так что помогаем ей в этом:
# 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@moodle.example.net
defaults
auth off
tls off
logfile /var/log/msmtp.log
# Default Account
account default
host mx.example.net
port 25
from www@moodle.example.net
# echo "Test" | msmtp --debug --account=default test@example.net
loaded system configuration file /etc/msmtprc
....
<-- 220 mx.example.net ESMTP Exim Thu, 06 Jul 2017 17:22:36 +0700
--> EHLO localhost
....
<-- 250 OK id=1dT3vg-0002Vv-Q4
--> QUIT
<-- 221 mx.example.net closing connection
....
<-- 220 mx.example.net ESMTP Exim Thu, 06 Jul 2017 17:22:36 +0700
--> EHLO localhost
....
<-- 250 OK id=1dT3vg-0002Vv-Q4
--> QUIT
<-- 221 mx.example.net closing connection
При желании явно указываем PHP отсылать почту через "mSMTP", но на самом деле мы эту возможность уже реализовали ранее посредством символической ссылки к "sendmail":
# vi /etc/php/7.2/fpm/php.ini
....
; sendmail_path = "/usr/sbin/sendmail -t -i"
sendmail_path = "/usr/bin/msmtp -t -i"
....
; sendmail_path = "/usr/sbin/sendmail -t -i"
sendmail_path = "/usr/bin/msmtp -t -i"
....
Проверяем, способен ли PHP-интерпретатор воспользоваться почтовой подсистемой:
# sudo -u www-data /usr/bin/php -r "mail('test@example.net', 'Test', 'Test');"
Загрузка дистрибутива.
LMS "Moodle" крупный проект, который естественным образом разделился на две кодовых ветви: "latest" и условный LTS (Long-term support). В частности, нынешний LTS - ветка "3.4.6", обновления безопасности для которой обещают выпускать до Мая 2019 года. Учитывая то, что в государственных бюджетных организациях процессы протекают чрезвычайно медленно (например попытка обновления существующего портала до "latest" версии "3.3" в НГУ длилась так долго, что разработчики за это время успели выкатить новый релиз "3.4"), то удобнее играть роль консерватора и в работе использовать LTS-релизы:
Загрузить дистрибутив можно следующим образом:
# cd /usr/src && wget https://download.moodle.org/download.php/direct/stable34/moodle-3.4.6.tgz
Развёртывание сайта под управлением LMS "Moodle".
Всё просто - разворачиваем дистрибутивный архив в целевую директорию, отрезав от имён извлекаемых файловых ресурсов предваряющее "moodle/":
# tar -xvf /usr/src/moodle-3.4.6.tgz -C /var/www/moodle.example.net/www --strip-components=1
Переводим свежераспакованные файлы во владение соответствующего пользователя:
# chown -R www-data:www-data /var/www/moodle.example.net
# setfacl --no-mask --modify user::rX,group::rX /var/www/moodle.example.net
# setfacl --no-mask --modify user::rX,group::rX /var/www/moodle.example.net
Весь процесс инсталляции реализован через web-интерфейс:
https://moodle.example.net/install.php
Разработчики LMS "Moodle" реализовали полное разделение данных и средств управления таковыми: абсолютно всё пользовательское сосредоточено в директории "./moodledata", а файлы LMS могут быть заменены совершенно безбоязненно, за исключением несущего ключевые настройки файла "./www/config.php".
Отсюда следует, что если установка LMS "Moodle" пошла "совсем не так", то для её возобновления нужно всего лишь удалить файл "./www/config.php" (он потом будет воссоздан из "./www/config-dist.php").
По завершению установки лучше бы сразу проверить, получилось ли удовлетворить все требования LMS "Moodle" к программному обеспечению:
https://moodle.example.net/admin/environment.php
Корректировка разрешений доступа к пользовательским ресурсам.
Слишком свободные разрешения доступа для создаваемых LMS "Moodle" файлов мне не нравятся - исправим это (запретив доступ всем не членам группы "www-data"), заодно уточнив в конфигурационном файле ресурсов, там ли директория с данными, где предполагается:
# vi /var/www/moodle.example.net/www/config.php
<?php // Moodle configuration file
....
$CFG->dataroot = '/var/www/moodle.example.net/moodledata';
....
$CFG->directorypermissions = 0770;
....
....
$CFG->dataroot = '/var/www/moodle.example.net/moodledata';
....
$CFG->directorypermissions = 0770;
....
# chmod -R o-rwx /var/www/moodle.example.net/moodledata
Вынос файлов web-сессий в общепринятое место.
Ранее мы уже создали условия для хранения, быстрого к ним доступа и управления файлами "PHP sessions". LMS "Moodle" по умолчанию хранит таковые в своих директориях, что неэффективно - исправим это (для режима хранения сессий в файлах):
# vi /var/www/moodle.example.net/www/config.php
<?php // Moodle configuration file
....
$CFG->session_handler_class = '\core\session\file';
$CFG->session_file_save_path = '/var/lib/php/sessions';
$CFG->sessiontimeout = 14400;
....
....
$CFG->session_handler_class = '\core\session\file';
$CFG->session_file_save_path = '/var/lib/php/sessions';
$CFG->sessiontimeout = 14400;
....
Как выяснилось в эксплуатации, LMS "Moodle" почему-то удаляет сессии, исходя из взятого из конфигурации PHP параметра "session.gc_maxlifetime" в 1440 секунд (по умолчанию). При этом переопределение этого параметра из панели администрирования "Moodle -> Администрирование -> Сервер -> Сессии" не срабатывало, без каких-либо предупреждений. Сайт молчком удалял свои "бездействующие" сеансы через 1440 секунд.
Помогло лишь явное указание длительности таймаута бездействия сессии в конфигурационном файле "Moodle". После этого в административной панели сайта у параметра сессии появилась отметка "Значение определено в файле config.php". Похоже, что это мелкий баг, вот таким образом обходящийся.
Вероятно, этого же эффекта глобально можно было добиться изменением параметра PHP "session.gc_maxlifetime" до больших значений.
Вынос хранилища файлов курсов на иную файловую систему.
LMS "Moodle" сам по себе громоздкий web-проект (164 Мегабайта при первичной установке, для v3.4.6), а уж файлы учебных курсов (по умолчанию располагающиеся в директории "./moodledata/filedir/") так быстро отъедают дисковое пространство на сотни Гигабайт, что весьма скоро сервер может стать немобильным. Потому первым делом стоит озаботится явным отделением тяжёлых данных от логики и легковесного контента, вынеся файловое хранилище на отдельную файловую систему. Я подключаю его по NFS или с иного внешнего СХД, в точку вне самого сайта, чтобы не смешивать сущности:
# vi /etc/fstab
....
# Moodle file storage
/dev/vg1/lvmoodledata /var/www/moodle.example.net/mnt ext4 rw,noatime,nodiratime,nosuid,nodev 0 0
....
# Moodle file storage
/dev/vg1/lvmoodledata /var/www/moodle.example.net/mnt ext4 rw,noatime,nodiratime,nosuid,nodev 0 0
....
Создаём точку монтирования и подключаем файловую систему:
# mkdir -p /var/www/moodle.example.net/mnt
# mount /var/www/moodle.example.net/mnt
# mount /var/www/moodle.example.net/mnt
Перемещаем директорию с данными на выделенную файловую систему:
# mkdir -p /var/www/moodle.example.net/mnt/moodledata
# mv /var/www/moodle.example.net/moodledata/filedir /var/www/moodle.example.net/mnt/moodledata/filedir
# mv /var/www/moodle.example.net/moodledata/filedir /var/www/moodle.example.net/mnt/moodledata/filedir
Для выноса web-ресурсов в пределах файловой структуры доступной PHP вполне достаточно "символической ссылки":
# ln -s /var/www/moodle.example.net/mnt/moodledata/filedir /var/www/moodle.example.net/moodledata/filedir
# chown -R www-data:www-data /var/www/moodle.example.net/mnt/moodledata/filedir
# chmod -R g+w /var/www/moodle.example.net/mnt/moodledata/filedir
# chmod -R o-rwx /var/www/moodle.example.net/mnt/moodledata/filedir
# chmod -R g+w /var/www/moodle.example.net/mnt/moodledata/filedir
# chmod -R o-rwx /var/www/moodle.example.net/mnt/moodledata/filedir
Важно понимать, что целиком выносить ресурс "./moodledata/" на внешнюю СХД нежелательно - в этой директории также расположены "кеши" и временные файлы ("./cache/", "./localcache/", "./temp/" и "./lock/"), постоянно запрашиваемые на чтение и изменение web-сервером. А вот большие и редко запрашиваемые файлы директории "./filedir/" вполне можно сложить на медленной файловой системе - при частом использовании они всё равно будут слегка кешироваться и скорость доступа к ресурсам в целом не снизится.
Обеспечиваем возможность запуска заданий по расписанию.
В LMS "Moodle" встроен функционал исполнения заданий по расписанию. Он опирается на вызовы через системный "crontab":
В нашем случае пользовательский "crontab" расположен в "/var/spool/cron/crontabs/www-data", и в него следует внести соответствующее правило:
# sudo -u www-data crontab -e
....
# Moodle schedule initiator
*/5 * * * * /usr/bin/php -q -f /var/www/moodle.example.net/www/admin/cli/cron.php >/dev/null 2>&1 &
# Moodle schedule initiator
*/5 * * * * /usr/bin/php -q -f /var/www/moodle.example.net/www/admin/cli/cron.php >/dev/null 2>&1 &
Включаем антивирусную проверку (опционально).
При желании можно установить антивирус "ClamAV" - в LMS "Moodle" есть плагин, посредством которого можно наладить предварительное сканирование загружаемых пользователями файлов:
# aptitude install clamav
# ls /usr/bin/clamscan
# ls /usr/bin/clamscan
# mkdir -p /var/quarantine
# chown -R www-data:www-data /var/quarantine
# chown -R www-data:www-data /var/quarantine
Управление режимом вывода сообщений об ошибках.
По умолчанию в LMS "Moodle" принята политика подавления любых сообщений об ошибках от PHP-интерпретатора - вероятно из соображений как безопасности (сложнее взломать, основываясь на ответах сервиса), так и производительности (снижение нагрузки на дисковую подсистему). На этапе поиска неполадки можно включить отображение детальной информации следующим набором параметров в конфигурационном файле web-приложения:
# vi /var/www/moodle.example.net/www/config.php
<?php // Moodle configuration file
....
// Force a debugging mode regardless the settings in the site administration
@error_reporting(E_ALL); // NOT FOR PRODUCTION SERVERS!
@ini_set('display_errors', '1'); // NOT FOR PRODUCTION SERVERS!
$CFG->debug = E_ALL; // NOT FOR PRODUCTION SERVERS!
$CFG->debugdisplay = 1; // NOT FOR PRODUCTION SERVERS!
....
....
// Force a debugging mode regardless the settings in the site administration
@error_reporting(E_ALL); // NOT FOR PRODUCTION SERVERS!
@ini_set('display_errors', '1'); // NOT FOR PRODUCTION SERVERS!
$CFG->debug = E_ALL; // NOT FOR PRODUCTION SERVERS!
$CFG->debugdisplay = 1; // NOT FOR PRODUCTION SERVERS!
....
Очень желательно после исправления проблемы снизить детализацию сообщений интерпретатора, или, как минимум, отключить их показ пользователю (опции "display_errors" и "debugdisplay").
Восстановление доступа к учётной записи администратора.
Довольно часто случается, что пароль к учётной записи web-сервиса LMS "Moodle" оказывается забыт или утрачен. В составе сайта имеется соответствующая утилита, посредством которой пароль для известного пользователя сбрасывается в один проход:
# sudo -u www-data php /var/www/moodle.example.net/www/admin/cli/reset_password.php
== Password reset ==
Enter username (manual authentication only)
: admin
....
Enter username (manual authentication only)
: admin
....
На самом дела технически ничто не препятствует заменить "хеш" пароля в БД - но это чуть сложнее и не нужно:
# mysql
MySQL> USE moodle;
MySQL> SELECT * FROM mdl_user WHERE username='%admin%';
MySQL> UPDATE mdl_user SET password=MD5('strongAdminPassword') WHERE username='admin';
MySQL> QUIT;
MySQL> USE moodle;
MySQL> SELECT * FROM mdl_user WHERE username='%admin%';
MySQL> UPDATE mdl_user SET password=MD5('strongAdminPassword') WHERE username='admin';
MySQL> QUIT;
Нагрузочное тестирование.
После запуска web-сайта в работу возможно захочется проверить, как скоро он сдастся под возрастающей нагрузкой. Для этого можно воспользоваться утилитой "Siege".
Вот так очень просто протестировать сервер в сценарии "250 пользователей 10 минут упорно ходят по сайту":
$ siege https://moodle.example.net -c250 -t10m
Lifting the server siege...
Transactions: 188316 hits
Availability: 100.00 %
Elapsed time: 599.28 secs
Data transferred: 6127.82 MB
Response time: 0.76 secs
Transaction rate: 314.24 trans/sec
Throughput: 10.23 MB/sec
Concurrency: 238.28
Successful transactions: 185865
Failed transactions: 0
Longest transaction: 2.93
Shortest transaction: 0.00
Transactions: 188316 hits
Availability: 100.00 %
Elapsed time: 599.28 secs
Data transferred: 6127.82 MB
Response time: 0.76 secs
Transaction rate: 314.24 trans/sec
Throughput: 10.23 MB/sec
Concurrency: 238.28
Successful transactions: 185865
Failed transactions: 0
Longest transaction: 2.93
Shortest transaction: 0.00
Ради интереса я запускал тестирование одновременно с нескольких рабочих станций, суммарно эмулирующих хождение по сайту пары тысяч пользователей - компоненты web-сервиса равномерно задействовали все доступные ресурсы, количество конкурентных запросов к дисковой подсистеме возросло до сотни (!!!), но в целом сервер под нагрузкой спокойно себя чувствовал и отвечал на запросы без потерь таковых, даже будучи под завязку занят.
17 октября 2024 в 19:34