База данных на сервере (MariaDB)

Это пятая статья серии «Эксплуатация VPS / DevOps-база». К этому моменту у нас уже живёт стек: сервер на ногах (Анатомия VPS), сервисы заводятся (systemd), nginx отдаёт HTTPS (Nginx), PHP-FPM считает детей и пишет slowlog (PHP-FPM). Но где-то под всем этим лежит то, без чего WordPress не может ни-че-го: таблицы с постами, пользователями, настройками. В Части VI мы увидели, что тормозит PHP, и часто там на верхушке трейса сидел get_posts() → mysqli-фрейм — то есть база держала воркер занятым. Пора спуститься на самый нижний этаж прикладного стека.

Под WordPress на VPS чаще всего стоит MariaDB (форк MySQL, ставится по умолчанию в репозиториях Ubuntu/Debian как mysql-server — не пугайтесь, это MariaDB). Она — программа, которая хранит все ваши данные в структурированном виде и отвечает на SQL-запросы от PHP. На одной машине с гигабайтами памяти она же и самый крупный потребитель RAM, и самое частое узкое место, и самое критичное для бэкапа — потерять базу без копий = потерять весь сайт. В этой статье: поставим и «первично закроем» MariaDB (bind-address, сокет вместо TCP, mysql_secure_installation); создадим пользователя и БД только под один сайт (не root!); разберём движок InnoDB и самый важный параметр — размер буферного пула под вашу память; научимся делать бэкап mysqldump так, чтобы он не встал в лок на живой базе (--single-transaction), и обязательно проверять его восстановлением; и, наконец, будем понимать, что делать когда «база висит на одном запросе» — SHOW PROCESSLIST, зависший SELECT, аккуратный KILL.

Термины, которые введём один раз: СУБД (система управления базами данных) — программа, которая хранит данные в таблицах и даёт к ним доступ через SQL-запросы; MySQL/MariaDB — СУБД. SQL (Structured Query Language) — язык запросов к базе (SELECT, INSERT…). InnoDB — «движок хранения» (storage engine) — тот, кто физически хранит строки на диске и следит за транзакциями; у MariaDB/MySQL есть и другие (MyISAM), но для WordPress годится только InnoDB, см. Часть III. Транзакция — блок изменений, который либо применяется целиком, либо не применяется вовсе (атомарность). Сокет — как у PHP-FPM: путь-файл /var/run/mysqld/mysqld.sock, через который локальный клиент приходит в базу без TCP-порта. Dump (дамп) — выгрузка всей базы (или части) в SQL-текст, чтобы восстановить её на другой машине. Все домены/имена условные; подставьте свои.


Часть I. Ставим MariaDB и закрываем первичный контур

1. Установка и проверка

Bash
# ставим сервер (в Ubuntu/Debian пакет mysql-server фактически ставит MariaDB)
sudo apt update
sudo apt install -y mariadb-server
# убедимся, что юнит systemd живёт (см. Части II про то, как читать systemctl)
sudo systemctl status mariadb --no-pager | head -n 8
# какая версия реально стоит
mariadb --version

Юнит mariadb.service из коробки уже enable и start — база живёт после перезагрузки (вот почему выученный в Части II systemctl enable --now — это не «магия», а нормальный рабочий инструмент).

2. Первый вход и почему пока можно

Клиент mariadb (он же mysql — симлинк) подключается от имени вашего unix-пользователя через сокет и по умолчанию получает полные права root-а базы — на Ubuntu/Debian это устроено через auth_socket (сверяется uid процесса клиента). Так что sudo mariadb заходит «в базу как root» без пароля, и это нормально только для административных действий, не для WordPress:

Bash
# вход как системный root базы
sudo mariadb
# внутри клиента:
MariaDB [(none)]> SELECT VERSION(), USER();   # версия и как вы вошли
MariaDB [(none)]> SHOW DATABASES;             # что уже есть (information_schema, mysql, performance_schema, …)

3. mysql_secure_installation — выключатель «раздать всё всем»

В установочных образах есть тестовые БД и разрешённый анонимный вход. mysql_secure_installation задаст привычные вопросы и закроет их:

Bash
# интерактивная «первичная гигиена»
sudo mysql_secure_installation

Спрашивает и что отвечать (по-русски смысл):

  • Set root password? Обычно «нет» — у вас на Ubuntu/Debian работает auth_socket, root-базы через пароль нет, вход через sudo mariadb. Пароль root базы — это отдельная тема (например, если вы потом поменяете аутентификацию root). Ставьте только если понимаете, зачем.
  • Remove anonymous users? — Y (анонимный вход = дыра).
  • Disallow root login remotely? — Y (root базы ходит только с localhost).
  • Remove test database? — Y (она ничего не стоит, но доступна всем).
  • Reload privilege tables — Y.

После этой процедуры ваша база уже «не как из коробки», а как «свой сервер под одним хозяином».

4. bind-address и сокет: где именно слушать

Главная «дверь» сервера — в /etc/mysql/mariadb.conf.d/50-server.cnf (или /etc/mysql/mysql.conf.d/mysqld.cnf, в зависимости от дистрибутива — смотрите !includedir в /etc/mysql/my.cnf).

HTML
# /etc/mysql/mariadb.conf.d/50-server.cnf
[mysqld]
bind-address = 127.0.0.1   # слушаем только локально; наружу из интернета не стучаться
# skip-networking          # (экстремальный вариант) — только сокет, вообще без TCP-порта
# socket = /var/run/mysqld/mysqld.sock   # unix-сокет, через него ходит wp на этой же машине

Что выбрать на одной машине:

  • bind-address = 127.0.0.1 — база слушает TCP только на локальном (loopback) интерфейсе. Подключиться с чужой машины нельзя. WordPress на этой же машине ходит через сокет (или через 127.0.0.1 — одинаково безопасно).
  • skip-networking — TCP вообще не открывается. Абсолютно локальный вариант. Удобен, если вы точно уверены, что ничего извне не подключится (и что мониторинг/репликация вам не нужны).

Не открывайте 0.0.0.0 (слушать «со всех интерфейсов») — иначе 3306 влезет в internet, и через день вы увидите в SHOW PROCESSLIST чужие IP с попытками войти. Если же нужен доступ извне (например, GUI с ноутбука) — делайте SSH-туннель (ssh -L 3306:127.0.0.1:3306 deploy@сервер), а не открытие 3306.

Проверка, что мы сделали правильно:

Bash
# пересоздать конфиг и перезапустить юнит
sudo systemctl restart mariadb
# какие адреса реально слушает база
sudo ss -tulpn | grep -E '3306|mysqld'
# ожидаем: 127.0.0.1:3306 (или отсутствие TCP при skip-networking) и файл сокета
sudo ls -l /var/run/mysqld/

5. Куда и как смотрим логи

MariaDB пишет в syslog (в Ubuntu/Debian) или в свой /var/log/mysql/error.log. Читать через journalctl, как учили в Части V про systemd:

Bash
sudo journalctl -u mariadb -n 40 --no-pager   # последние строчки сервера

Чек-лист части

  • Открыли `bind-address = 0.0.0.0` «чтобы работало из IDE» — и получили перебор паролей у 3306; лечится `127.0.0.1` + SSH-туннель.
  • Забыли `mysql_secure_installation` — живут анонимы и test-база.
  • `sudo mariadb` оставили как способ входа WordPress (root базы в `wp-config.php`) — см. Часть II.

Часть II. Пользователь и база под сайт, а не «от root со всем доступом»

WordPress хочет одну базу и одного пользователя. На сервере, где живут несколько сайтов, у каждого — свой пользователь и своя база. Так вы держите границы.

1. Создаём базу и пользователя под один сайт

Bash
# заходим как админ базы
sudo mariadb
-- создаём БД с кириллической/эмодзи-совместимой кодировкой (utf8mb4 для WP — обязательно)
CREATE DATABASE wp_example
  DEFAULT CHARACTER SET utf8mb4
  DEFAULT COLLATE utf8mb4_unicode_ci;

-- создаём пользователя, которому разрешено ходить только через localhost (socket/loopback)
CREATE USER 'wp_example'@'localhost' IDENTIFIED BY 'СВОЙ_длинный_пароль_из_менеджера_паролей';

-- выдаём права, и только на ЭТУ базу
GRANT ALL PRIVILEGES ON wp_example.* TO 'wp_example'@'localhost';

FLUSH PRIVILEGES;

-- проверяем, что всё именно так
SHOW GRANTS FOR 'wp_example'@'localhost';
EXIT;

Почему именно так:

  • utf8mb4 — WordPress без неё ломается на эмодзи, китайских символах, длинных диакритиках. Ставьте с самого начала: поменять потом — отдельная болезненная операция.
  • 'wp_example'@'localhost' — связывает пользователя и источник подключения. Даже если пароль утечёт, зайти с чужого IP не получится (только через сокет/локальный интерфейс этой машины).
  • GRANT … ON wp_example.* — пользователь имеет права только на эту базу, и не видит остальные (SHOW DATABASES у него покажет только свою, mysql и information_schema). Компрометация wp-config.php (частый путь) = потеря одной базы, а не всего сервера.

2. wp-config.php на стороне WordPress

PHP
// /var/www/example.test/wp-config.php
define('DB_NAME',     'wp_example');
define('DB_USER',     'wp_example');
define('DB_PASSWORD', 'СВОЙ_длинный_пароль');
define('DB_HOST',     'localhost');    // ← сокет; mariadb клиент сам возьмёт socket из my.cnf
// для явного сокета (не обязательно): define('DB_HOST', 'localhost:/var/run/mysqld/mysqld.sock');

Права на wp-config.php: только владелец (site_example или deploy) читает, 640/600. Это не тема этой статьи, но без этого пароль утек через соседний сайт — всё остальное бесполезно. См. Части V про права файлов и authorized_keys.

3. Проверка «под тем пользователем, кто будет жить»

Не верьте на глаз. Проверьте, что wp_example реально входит и видит только свою базу:

# заходим как wp_example (пароль введёт интерактив; в CI/скриптах — MYSQL_PWD env)
mysql -u wp_example -p -e "SHOW DATABASES; USE wp_example; SELECT COUNT(*) FROM wp_posts;"

Если видите wp_example, information_schema и ничего лишнего — границы держатся. Если бы у вас был там mysql и performance_schema с полными правами — вы бы дали пользователю больше, чем нужно.

4. Что делать с правами, когда надо «чуть шире»

Иногда WordPress-плагину нужно CREATE TEMPORARY TABLES или LOCK TABLES. Добавляем точечно, а не «GRANT ALL на все базы»:

GRANT CREATE TEMPORARY TABLES ON wp_example.* TO 'wp_example'@'localhost';

Убираем через REVOKE ... ON wp_example.* FROM .... Иерархия проста: чем меньше прав у боевого пользователя — тем меньше площадь атаки.

Чек-лист части

  • В `wp-config.php` стоит `DB_USER=’root’` — любая утечка конфига = полный контроль над сервером БД.
  • `DB_HOST=’%’` или `’внешний.ip’` — пользователь видит базу извне; ставим `localhost`.
  • Кодировка базы не `utf8mb4` — эмодзи крэшат сохранение постов.
  • `GRANT ALL ON *.*` (на все базы) «для простоты» — убивает изоляцию нескольких сайтов.

Часть III. InnoDB и самый важный параметр — буферный пул

InnoDB — движок хранения, с которым WordPress работает корректно. Если вдруг какая-то таблица стоит на движке MyISAM (старый, без транзакций), база теряет согласованность при падении и хуже кэшируется. Проверим и поправим, а потом займёмся памятью.

1. Убеждаемся, что таблицы — InnoDB

-- движки всех таблиц базы
USE wp_example;
SELECT TABLE_NAME, ENGINE FROM information_schema.TABLES
  WHERE TABLE_SCHEMA='wp_example';
-- глобальный дефолт движка для новых таблиц
SHOW VARIABLES LIKE 'default_storage_engine';

Если увидите MyISAM — переводим на InnoDB: ALTER TABLE wp_posts ENGINE=InnoDB; (на живой базе — только в окно тишины; это перестройка таблицы). Дефолт ставим навсегда в конфиге (см. ниже): default_storage_engine=InnoDB.

2. innodb_buffer_pool_size — кэш данных в RAM

Буферный пул — это область памяти, куда InnoDB кладёт «горячие» страницы данных и индексов. Каждый запрос читает не с диска, а из пула, если данные там уже есть. Пул маленький — горячие индексы не влезают → диск под нагрузкой дёргается, WordPress деградирует. Пул слишком большой — RAM съедена, OOM-killer (из Части V) кого-то убьёт.

Сколько ставить на WordPress на одной машине (ориентир, не догма):

  • на VPS 1–2 ГБ RAM: отдайте базе столько, чтобы суммарно PHP-воркеры + пул + ОЗУ оставили запас системе. Часто 60–80% всей машины уходит базе, потому что она одна. Но при 1 ГБ реалистичнее 512M–768M, не «70%».
  • если база влезает в RAM целиком (небольшой WordPress-блог), пул = весь размер данных + 20% на разрастание.
# /etc/mysql/mariadb.conf.d/99-wp.cnf   (свой файл, чтобы apt не затёр дефолты)
[mysqld]
# движок
default_storage_engine = InnoDB
innodb_buffer_pool_size = 768M
innodb_log_file_size    = 128M          # размер журнала (WAL) для записи
innodb_flush_log_at_trx_commit = 1      # 1 = макс. надёжность (конформность с ACID)
innodb_flush_method = O_DIRECT          # обходить ОС-кэш (меньше двойного дублирования)

После изменения — рестарт сервиса и проверка, что параметр принял:

sudo systemctl restart mariadb
mysql -e "SHOW VARIABLES LIKE 'innodb_buffer_pool_size';"

3. Ещё параметры, которые полезно тронуть под WordPress

  • innodb_log_file_size (по умолчанию мал). Больше — быстрее крупные записи (импорты, массовые обновления), дольше восстановление после сбоя. Для одного сайта хватает 128M–256M.
  • innodb_flush_log_at_trx_commit — режим сброса журнала: 1 = писать на диск при каждой транзакции (данные не теряются при отказе), 2 = писать в ОС-кэш раз в секунду (быстрее, но потеря ~1 секунды при отказе самого сервера, при отказе MySQL данные сохранятся). Для WordPress на одной машине оставляйте 1; 2 оправдан, если это неоплачиваемый тестовый сервер и вы готовы потерять секунду изменений.
  • innodb_file_per_table = ON (дефолт в новых версиях) — каждой таблице свой файл .ibd. Управляет размером базы и удалением данных (не остаётся «дыр» в общем табличном пространстве).

4. Как понять, что пул малав

-- индексы, которые реально используются, и их размер
SHOW STATUS LIKE 'Innodb_buffer_pool_read%';
-- «битые» (промахи) кэш-пула на общее число чтений
SHOW GLOBAL STATUS LIKE 'Innodb_buffer_pool_reads';            -- чтения с диска
SHOW GLOBAL STATUS LIKE 'Innodb_buffer_pool_read_requests';    -- попыток читать (из кэша или диска)

Примерный «хит-рейт» = (read_requests - buffer_pool_reads) / read_requests. Здорово — > 99%. Если Innodb_buffer_pool_reads растёт постоянно — горячие данные не влезают, увеличивайте пул (если память позволяет).

Чек-лист части

  • Часть таблиц осталась на MyISAM → WordPress не получает транзакционной целостности и хуже кэшируется.
  • `innodb_buffer_pool_size` выставлен «по учебнику — 70% RAM» на машину, где ещё и 12 воркеров PHP живут рядом → не хватает памяти, OOM.
  • `innodb_log_file_size` изменили без остановки сервера/удаления старых `ib_logfile*` → сервер не поднимется.
  • `innodb_flush_log_at_trx_commit=2` «для скорости» на боевом сайте — потеря последних секунд при жёстком отказе.

Часть IV. Резервная копия mysqldump --single-transaction: снимаем «на лету»

Бэкап базы — это mysqldump: выгрузка дампа (SQL-инструкций CREATE+INSERT) в файл. Простая команда бэкапит InnoDB-базу с блокировкой таблиц, то есть сайт висит на всё время выгрузки. Правильный бэкап живой базы — --single-transaction.

1. Что делает --single-transaction и почему это нужно

InnoDB поддерживает «снимок» (snapshot) на старте транзакции: дамп начинается с одной согласованной точки, при этом база продолжает принимать изменения. Без флага — блокировка, сайт «висит»; с ним — согласованная копия без даунтайма. (Дополнение: если в базе есть MyISAM-таблицы — они всё равно блокируются, --lock-tables=false не спасёт; ещё раз аргумент, почему MyISAM убираем, см. Часть III.)

# снимаем согласованную копию всей базы без остановки сайта
mysqldump --single-transaction \
          --routines --triggers --events \
          --default-character-set=utf8mb4 \
          --user=wp_example \
          wp_example > /var/backups/wp_example.sql

Разбор флагов:

  • --single-transaction — снимок без блокировок.
  • --routines --triggers --events — не забыть функции, триггеры и события (WordPress-плагины используют cron-события базы, без этого флага они не попадут в дамп и после восстановления «перестанет часть автоматизации»). Это отдельная грабля.
  • --default-character-set=utf8mb4 — кодировка совпадает с базой.
  • --user=... — пользователь, которому хватит прав SELECT, LOCK TABLES, SHOW VIEW (для --single-transaction не нужен SUPER/EVENT глобально, но для дампа событий нужен EVENT-привилегия — у wp_example её может не быть, см. ниже про бэкап «от админа»).

2. Дамп + gzip, чтобы не засорять диск

# в один конвейер: mysqldump | gzip
mysqldump --single-transaction --routines --triggers --events \
          --default-character-set=utf8mb4 -u wp_example wp_example \
  | gzip > /var/backups/wp_example_$(date +%F).sql.gz

SQL-текст сжимается в 3–5 раз, как любой текст.

3. Автоматизация: cron или systemd-таймер

Копия «раз в сутки» не должна делаться вручную. Здесь работают оба инструмента, которые уже знаем:

  • cron — системный планировщик задач (ставите строку 15 3 * * * deploy /usr/local/bin/backup-mariadb.sh, «в 03:15 ежедневно»). Минимально и надёжно.
  • systemd-таймер — более «нативный» способ (юнит .timer + .service; механика в Части II). Удобно логирование через journalctl.

Пример скрипта (/usr/local/bin/backup-mariadb.sh, права 700, владелец deploy), с ротацией: держать последние N копий, чтобы не завалить диск:

#!/usr/bin/env bash
set -euo pipefail

DATE=$(date +%F)
OUT=/var/backups/mysql/wp_example_${DATE}.sql.gz
KEEP=14

# сокет и пароль под конкретного пользователя базы (пароль — в ~/.my.cnf, НЕ в скрипте!)
# ~/.my.cnf (права 600): [client] user=wp_example password=...

mkdir -p /var/backups/mysql
gzip -c < <(mysqldump --single-transaction --routines --triggers --events \
                     --default-character-set=utf8mb4 wp_example) > "$OUT"

# ротация: удаляем старше KEEP-1
ls -1t /var/backups/mysql/wp_example_*.sql.gz | tail -n +$((KEEP+1)) | xargs -r rm -f

# (опционально) переслать копию на другую машину/облако — rsync/rclone/объект

⛔ Пароль — не в скрипте. Хранить его в открытом скрипте 755, который читает любой пользователь системы, — способ отдать пароль всем. Правильно: ~/.my.cnf (chmod 600, секция [client] с user/password), тогда mysqldump читает автоматически. Отдельный способ — «плагин-аутентификация» или mysql_config_editor. (В этой теме мы в рамках одной машины; в контексте Ветки B это тот же принцип, что «никогда не светить ключ/пароль в коде, только в защищённой форме» — секреты в конфиг, права 600.)

4. Что бэкапить «обязательно», чтобы восстановление было полным

База — это только строки. Для полного восстановления сайта вместе с БД держите ещё: файлы wp-content/ (тема, плагины, загрузки) и wp-config.php. mysqldump их не берёт — их отдельно архивируют (tar -czf). Это тема разовой статьи про резервное копирование, здесь фиксируем: бэкап сайта = дамп базы плюс файлы; без файлов вы восстановите пустой каркас WordPress.

Чек-лист части

  • Бэкап `mysqldump` без `—single-transaction` на живой базе → таблицы блокируются, сайт висит на время дампа.
  • Забыли `—routines —triggers —events` → после восстановления не хватает триггеров/событий, «что-то перестало работать само».
  • Пароль базы зашит в скрипт `755` → любой процесс в системе его прочитает.
  • Ротация копий забыта → дамп-на-дамп → диск кончается → сервер падает (см. финальную статью про «диск»).
  • Бэкап хранится **на том же диске**, что база → сбой диска = потеряли и базу, и копию. Копию — на другую машину/объект.

Часть V. Проверка бэкапа восстановлением: единственная вещь, которая спасает

Это скучная, но обязательная часть. Дамп лежит, а уверенности, что он рабочий, нет до того дня, когда он реально нужен. Тогда выясняется, что он битый. Проверяем регулярным восстановлением, а не верой.

1. Тестовое восстановление в «песочную» БД

Никогда не проверяем бэкап на боевой базе. Создаём отдельную песочную и разворачиваем дамп туда:

# локально или на другой машине (песочница) — разворачиваем дамп
sudo -u mysql sh -c "gunzip -c /var/backups/mysql/wp_example_$(date +%F).sql.gz | \
    mysql -e 'CREATE DATABASE IF NOT EXISTS wp_example_restore' ; mysql wp_example_restore"
# или, если дамп распакован:
mysql -e "CREATE DATABASE wp_example_restore CHARACTER SET utf8mb4"
gunzip -c wp_example.sql.gz | mysql --database=wp_example_restore

2. Сверка счётчиков

USE wp_example_restore;
SELECT COUNT(*) FROM wp_posts;        -- сверить с боевой
SELECT COUNT(*) FROM wp_options;      -- ключевые таблицы
SELECT COUNT(*) FROM wp_users;
-- структура в порядке? таблицы есть? ключи на месте?
SHOW TABLES;

Совпадение COUNT(*) с боевой базой (или с той датой, на которую дамп) — минимальная проверка целостности. Для более строго — прогнать CHECKSUM TABLE wp_posts, wp_options на боевой и восстановленной за один и тот же срез (учтите: CHECKSUM чувствителен к порядку, но с --single-transaction на тихой базе совпадёт).

3. А что реально стоит проверить

  • Восстанавливается без ошибок (нет ERROR 1064 синтаксис в середине — иногда дамп обрывается из-за кончившегося диска).
  • wp_options содержит настройки сайта (если пусто — дамп не захватил часть).
  • Есть AUTO_INCREMENT, триггеры, --routines (SHOW CREATE TABLE wp_posts на восстановленной покажет триггеры; SHOW EVENTS — события).
  • Кодировка: кириллица/эмодзи не «кракозябрами».

4. Дрессировка «вручную» и автоматизация

  • Минимум раз в неделю — прогон gunzip | mysql <песочница> + сверка COUNT. У вас уже есть cron/systemd-таймер из Части IV: добавьте второй шаг-проверку в тот же скрипт.
  • Раз в квартал — полноценный «fire drill»: на тестовом хосте поднимите WordPress из дампа + файлов и пройдитесь по сайту руками. Это единственная проверка, которая ловит «технически дамп валиден, но сайт не поднимается» (например, потерянные таблицы миграции).

Практическое правило: бэкап без проверки восстановления — это не бэкап, а файл, который когда-то был бэкапом. Проверка — часть процедуры, не «если успею».

Чек-лист части

  • Восстановление на боевую базу «на пробу» → перезаписали живой сайт дампом. Проверка — **только** на песочную.
  • Дамп проверяется только визуально (`ls -la` есть файл) → содержимое не проверялось.
  • Нет проверки «дамп на другой машине разворачивается» (зависимость от версии mysql, от прав) → проверка на копии-на-копии не ловит реального переноса.

Часть VI. Когда «база висит» на одном запросе: PROCESSLIST, KILL и связка с PHP-FPM

Самый частый «жареный» случай на одном VPS: PHP-FPM тормозит, slowlog (см. Часть VI) показывает, что воркер завис в get_posts()/mysqli. Смотрим, что делает база.

1. SHOW FULL PROCESSLIST — кто чем занят прямо сейчас

SHOW FULL PROCESSLIST;
Критичная колонкаЧто значит
Idid коннекта — этим снимаете зависший запрос через KILL id
Userкакой пользователь БД; видите wp_example и «завис» — это ваш сайт
Hostоткуда коннект (localhost = ваш WP; незнакомый IP — см. Часть II)
dbкакая база
Commandсостояние; Sleep = ждут, Query/Execute = работают
Timeсколько секунд в текущем состоянии — главный индикатор зависания
Stateфаза; Sending data (долго) = скорее всего полный скан таблицы / нет индекса
Infoсам SQL-запрос (обрезан без FULL)

Как читать: сортируйте глазами по Time. Один запрос с Command=Query, State крутится долго и Info — какой-то «непонятный» SELECT из плагина — вот он, виновник «тормозов сайта и PHP».

2. Аккуратно снимаем зависший запрос — KILL

-- снимаем КОНКРЕТНЫЙ зависший коннект, не трогая сервер
KILL 8123;   -- тот Id из PROCESSLIST

Это не рестарт mariadb.service, а вырывание одного зависшего соединения. Сервер остаётся живым, остальные запросы обслуживаются. Киллите тот процесс, Info у которого выглядит как «этот жуткий SELECT» и Time уже минуты.

⚠️ Килл vs причина: сняли зависание — сайт вздохнул, но если запрос вернётся (плагины снова «клик»), проблема вернётся. KILL — это «открыть закупорку», а чинить надо на уровне запроса/индекса (см. Оптимизация баз данных и Метрики и мониторинг из серии про архитектуру).

3. SHOW ENGINE INNODB STATUS — глубокий снимок

Когда висит не «запрос» в смысле SQL-текста, а что-то внутри движка (блокировки, длинная транзакция), смотрим:

SHOW ENGINE INNODB STATUS\G

В разделе TRANSACTIONS ищете «lock wait» / самую длинную живую транзакцию. На WordPress частая причина блокировки — не сама база, а зависший UPDATE wp_options ... autoload (много плагинов) или «длинная» транзакция, оставленная кривым скриптом. Ищите «самую старую» транзакцию — она держит замок и остальные ждут.

4. max_connections и связка с числом PHP-воркеров

Есть ловушка, которая проявляется именно на одном VPS: PHP-FPM открыл столько соединений к базе, сколько у него детей (каждый воркер держит свой коннект). Если одновременно и соседний сайт тоже ходит, вы упрётесь в потолок.

SHOW VARIABLES LIKE 'max_connections';
SHOW GLOBAL STATUS LIKE 'Threads_connected';   -- сколько сейчас занято
SHOW GLOBAL STATUS LIKE 'Max_used_connections'; -- сколько когда-либо было занято (пиковый за всю жизнь сервера)

Дефолт max_connections обычно 151. У вас pm.max_children (Часть IV про PHP-FPM) из, допустим, 12 (основной) + 5 + 5 по пулам = 22. Но при DDoS на второй сайт и их воркеры разгоняются — Max_used_connections упрётся. Лечится: поднимите max_connections под ваш суммарный максимум детей + запас, или держите pm.* в узде (не больше, чем реально нужно, см. Части IV–V). Связь: PHP-FPM-дети ↔ Threads_connected ↔ max_connections должны сходиться в бюджете, иначе «база отвергает клиентов» (ERROR 1040: Too many connections) — и это не база сломалась, а коннектов открыли больше, чем потолок.

# /etc/mysql/mariadb.conf.d/99-wp.cnf (дополнение)
[mysqld]
max_connections = 80           # суммарно детей по всем пулам + запас
wait_timeout = 120             # сколько секунд коннект в состоянии Sleep ждут перед закрытием
interactive_timeout = 120

wait_timeout чистит «уснувшие» коннекты от PHP-FPM, если те не держат connection pool. Ставьте выше, чем типичная «пауза между запросами» вашего движка, чтобы не рвать живые; ниже — чтобы не копить мёртвых.

5. Быстрый порядок действий при «БД висит»

# 1) что вообще сейчас в базе и сколько коннектов занято
mysql -e "SHOW GLOBAL STATUS LIKE 'Threads_connected'; SHOW GLOBAL STATUS LIKE 'Max_used_connections';"
# 2) список процессов, глазами на самый долгий
mysql -e "SELECT ID, USER, HOST, DB, COMMAND, TIME, STATE, LEFT(INFO,40) q FROM information_schema.PROCESSLIST WHERE COMMAND='Query' ORDER BY TIME DESC LIMIT 10;"
# 3) сняли конкретный зависший коннект
mysql -e "KILL 8123;"
# 4) если сервер совсем не отвечает и коннекты не снять (заблокирован и вход) — крайний шаг
sudo systemctl restart mariadb

Рестарт сервиса — крайний шаг, он рвёт все запросы (включая ваши бэкапы/админку). До него всегда пробуйте снять Id.

Чек-лист части

  • Панический `systemctl restart mariadb` вместо `KILL <Id>` → потеряли все текущие коннекты и не поняли, что было.
  • Подняли `max_connections`, но не подумали о памяти: каждый коннект = буферные потоки; 80 коннектов на 1 ГБ VPS = не хватит.
  • `ERROR 1040: Too many connections` — это не «сбой базы», а исчерпание `max_connections`; смотрите `Threads_connected` и `pm.*` (детей PHP-FPM).
  • `KILL` «зависшего» запроса из плагина, не разобравшись, что он делал → потеряли транзакцию плагина (например, half-finished импорт). Смотрите `Info` перед тем, как киллить.

Инструмент под задачу

Соберите корректный запрос под свой префикс и проверьте, ложится ли он на нужный индекс, — с учётом ловушек prepare() и ORDER BY. Попробуйте инструмент прямо здесь, на своих данных:

ИИ SQL-Генератор
Гость
Токены: 0 / 50 000
Гостевой лимит — 50 000 токенов. Зарегистрируйтесь, для пользователей лимит + 200 000 токенов.

Чек-лист граблей этой части

  • В `wp-config.php` стоит `DB_USER=’root’`.
  • `mysqldump` без `—single-transaction` блокирует живую базу.
  • Бэкап без `—routines —triggers —events` не захватывает триггеры/события.
  • Бэкап хранится на том же диске, что база.
  • Проверка восстановления никогда не прогонялась → «дамп есть, но он точно битый?»
  • «БД висит» лечится рестартом сервиса вместо `KILL <Id>`.
  • `innodb_buffer_pool_size` «по учебнику», без учёта PHP-детей на той же машине → OOM.

Материал написан простым языком; любой англоязычный термин (MariaDB, СУБД, SQL, InnoDB, storage engine, транзакция, unix-сокет, дамп, --single-transaction, PROCESSLIST, KILL, buffer pool, max_connections, wait_timeout) объясняется при первом употреблении. Конфиги — для Ubuntu/Debian с MariaDB из apt; конкретные пути (/etc/mysql/mariadb.conf.d/ против /etc/mysql/mysql.conf.d/), имена юнитов (mariadb.service против mysql.service) и версия (10.x/11.x) сознательно условны — проверяйте фактические через mariadb --version, SHOW VARIABLES LIKE .... Домены/пароли/пути условные (example.test, wp_example); подставляйте свои. Секреты (пароль базы) — только в ~/.my.cnf с правами 600, не в открытом скрипте. Тонкие механизмы (бинарный лог binlog и point-in-time recovery, горячее резервное копирование через LVM-снапшоты, репликация мастер-реплика, --all-databases для всего сервера) упомянуты вскользь или не раскрыты — это следующий уровень, когда базовый контур уже живёт и стабильно бэкапится.

👤 Зарегистрируйтесь, чтобы добавить материал в закладки.