MySQL Performance Schema: как мы ускорили WordPress-админку в 8 раз для контент-платформы

Ситуация: WordPress-админка загружается 12 секунд

«КонтентПро» — контент-маркетинговое агентство с собственной WordPress-платформой. 340 сайтов на одной multisite-установке, 2.4 миллиона постов, 18 миллионов записей в wp_postmeta. MySQL 8.0 на сервере 16 vCPU, 64 GB RAM.

Жалобы редакторов:

  • Админка загружается 8-12 секунд — список постов, редактирование, медиабиблиотека. Редакторы теряли по 30-40 минут в день на ожидание.
  • Сохранение поста — 5-7 секунд — WordPress делает десятки UPDATE на wp_postmeta.
  • Периодические фризы — раз в 2-3 часа всё подвисает на 10-15 секунд. Совпадает с cron-задачами WordPress.
  • Поиск по постам — полнотекстовый поиск по 2.4 миллионам постов занимает 3-5 секунд.

Команда «КонтентПро» обратилась к нам в itfresh.ru. Мы начали с диагностики через Performance Schema.

Включаем Performance Schema и sys schema

Performance Schema — встроенный в MySQL инструмент профилирования. Он записывает статистику по каждому запросу, каждой блокировке, каждому файлу I/O. В MySQL 8.0 он включён по умолчанию, но многие инструменты (consumers) отключены.

# Проверяем, включён ли Performance Schema
SHOW VARIABLES LIKE 'performance_schema';
# +--------------------+-------+
# | Variable_name      | Value |
# +--------------------+-------+
# | performance_schema | ON    |
# +--------------------+-------+

# Включаем все нужные consumers
UPDATE performance_schema.setup_consumers
SET ENABLED = 'YES'
WHERE NAME IN (
  'events_statements_history',
  'events_statements_history_long',
  'events_waits_history',
  'events_waits_history_long',
  'events_stages_history',
  'events_stages_history_long'
);

# Включаем инструментирование всех потоков
UPDATE performance_schema.setup_instruments
SET ENABLED = 'YES', TIMED = 'YES'
WHERE NAME LIKE 'statement/%'
   OR NAME LIKE 'wait/%'
   OR NAME LIKE 'stage/%';

Sys schema — набор представлений поверх Performance Schema, которые превращают сырые данные в человекочитаемые отчёты:

# sys schema обычно установлен в MySQL 8.0
# Проверяем:
USE sys;
SHOW TABLES;
# +-----------------------------------------------+
# | Tables_in_sys                                  |
# +-----------------------------------------------+
# | host_summary                                   |
# | host_summary_by_file_io                        |
# | statement_analysis                             |
# | statements_with_full_table_scans               |
# | statements_with_runtimes_in_95th_percentile    |
# | user_summary                                   |
# | waits_global_by_latency                        |
# | ... (50+ views)                                |
# +-----------------------------------------------+

Находим медленные запросы через Performance Schema

Первый шаг — найти самые тяжёлые запросы:

# Топ-20 запросов по суммарному времени
SELECT
    DIGEST_TEXT AS query,
    COUNT_STAR AS exec_count,
    ROUND(SUM_TIMER_WAIT / 1000000000000, 2) AS total_time_sec,
    ROUND(AVG_TIMER_WAIT / 1000000000, 2) AS avg_time_ms,
    ROUND(MAX_TIMER_WAIT / 1000000000, 2) AS max_time_ms,
    SUM_ROWS_EXAMINED AS rows_examined,
    SUM_ROWS_SENT AS rows_sent
FROM performance_schema.events_statements_summary_by_digest
ORDER BY SUM_TIMER_WAIT DESC
LIMIT 20;

# Результат (сокращённо):
# +--------------------------------------------------+--------+-----------+----------+
# | query                                            | count  | total_sec | avg_ms   |
# +--------------------------------------------------+--------+-----------+----------+
# | SELECT ... FROM wp_postmeta WHERE post_id IN ... | 847293 | 4821.3    | 5.69     |
# | SELECT ... FROM wp_posts WHERE post_status = ... | 312456 | 2104.7    | 6.74     |
# | UPDATE wp_options SET option_value = ? WHERE ...  | 98742  | 987.4     | 10.00    |
# | SELECT ... FROM wp_posts LEFT JOIN wp_postmeta...| 45123  | 892.1     | 19.77    |
# +--------------------------------------------------+--------+-----------+----------+

Через sys schema ещё проще:

# Запросы с полным сканированием таблиц
SELECT * FROM sys.statements_with_full_table_scans
ORDER BY no_index_used_pct DESC
LIMIT 10;

# Запросы с наибольшей латентностью (95-й перцентиль)
SELECT * FROM sys.statements_with_runtimes_in_95th_percentile
LIMIT 10;

# Неиспользуемые индексы (можно удалить — экономим место и ускоряем INSERT)
SELECT * FROM sys.schema_unused_indexes
WHERE object_schema = 'wordpress';

Мы обнаружили четыре корневые причины:

  1. wp_postmeta без индекса на meta_key — 18M строк, Full Table Scan при каждом запросе метаданных
  2. wp_options autoload — 12 000 строк загружаются при каждом запросе, из них 80% не нужны
  3. Lock contention на wp_options — cron-задачи конкурируют с пользовательскими запросами
  4. InnoDB buffer pool — 128 MB (по умолчанию!) при 64 GB RAM

Проблема 1: wp_postmeta и индексы

wp_postmeta — самая проблемная таблица WordPress. 18 миллионов строк, EAV (Entity-Attribute-Value) паттерн. WordPress создаёт индекс только на (post_id), но запросы часто фильтруют по meta_key и meta_value.

# Анализируем, какие meta_key запрашиваются чаще всего
SELECT
    meta_key,
    COUNT(*) AS cnt
FROM wp_postmeta
GROUP BY meta_key
ORDER BY cnt DESC
LIMIT 20;

# Результат:
# +---------------------------+----------+
# | meta_key                  | cnt      |
# +---------------------------+----------+
# | _edit_last                | 2400000  |
# | _edit_lock                | 2400000  |
# | _wp_old_slug              | 1850000  |
# | _thumbnail_id             | 2100000  |
# | custom_seo_title          | 2400000  |
# | custom_seo_description    | 2400000  |
# +---------------------------+----------+

# Текущий план запроса — Full Table Scan!
EXPLAIN SELECT post_id, meta_value
FROM wp_postmeta
WHERE meta_key = '_thumbnail_id'
  AND post_id IN (1001, 1002, 1003, 1004, 1005);
# type: ALL, rows: 18000000 — катастрофа

Решение — составные индексы:

# Индекс для самых частых запросов
ALTER TABLE wp_postmeta
  ADD INDEX idx_meta_key_value (meta_key(191), meta_value(100)),
  ADD INDEX idx_postid_metakey (post_id, meta_key(191));

# После индексации:
EXPLAIN SELECT post_id, meta_value
FROM wp_postmeta
WHERE meta_key = '_thumbnail_id'
  AND post_id IN (1001, 1002, 1003, 1004, 1005);
# type: ref, rows: 5 — мгновенно

# Проверяем использование индексов через Performance Schema
SELECT
    OBJECT_SCHEMA,
    OBJECT_NAME,
    INDEX_NAME,
    COUNT_STAR AS uses,
    ROUND(SUM_TIMER_WAIT / 1000000000, 2) AS total_wait_ms
FROM performance_schema.table_io_waits_summary_by_index_usage
WHERE OBJECT_SCHEMA = 'wordpress'
  AND OBJECT_NAME = 'wp_postmeta'
ORDER BY COUNT_STAR DESC;

Результат: среднее время запроса к wp_postmeta упало с 5.7 мс до 0.3 мс — ускорение в 19 раз.

Проблема 2: lock contention и InnoDB buffer pool

Lock contention анализируем через Performance Schema:

# Текущие блокировки
SELECT
    OBJECT_SCHEMA,
    OBJECT_NAME,
    LOCK_TYPE,
    LOCK_MODE,
    LOCK_STATUS,
    COUNT(*) AS cnt
FROM performance_schema.data_locks
GROUP BY OBJECT_SCHEMA, OBJECT_NAME, LOCK_TYPE, LOCK_MODE, LOCK_STATUS
ORDER BY cnt DESC;

# Ожидания блокировок
SELECT * FROM sys.innodb_lock_waits\G
# Результат:
# wait_started: 2026-04-04 14:23:15
# wait_age_secs: 3
# locked_table: wordpress.wp_options
# waiting_query: UPDATE wp_options SET option_value='...' WHERE option_name='cron'
# blocking_query: SELECT option_name, option_value FROM wp_options WHERE autoload='yes'

# wp_options — главный bottleneck!
# Cron UPDATE блокирует SELECT, который выполняется при каждом запросе

Решение для wp_options:

# 1. Убираем autoload у ненужных options
UPDATE wp_options SET autoload = 'no'
WHERE option_name LIKE '_transient_%'
   OR option_name LIKE '_site_transient_%';
# Affected rows: 9847 — почти 10 000 transients грузились на каждый запрос!

# 2. Добавляем индекс на autoload
ALTER TABLE wp_options ADD INDEX idx_autoload (autoload);

# 3. Очищаем expired transients
DELETE FROM wp_options
WHERE option_name LIKE '_transient_timeout_%'
  AND option_value < UNIX_TIMESTAMP();
# Deleted: 7234 rows

InnoDB buffer pool — самый важный параметр производительности MySQL:

# Текущее значение — 128 MB (по умолчанию!)
SHOW VARIABLES LIKE 'innodb_buffer_pool_size';
# 134217728 (128 MB)

# Анализируем hit rate
SHOW GLOBAL STATUS LIKE 'Innodb_buffer_pool_read%';
# Innodb_buffer_pool_read_requests: 847293847  (запросы к buffer pool)
# Innodb_buffer_pool_reads: 24789123           (чтения с диска — промахи)
# Hit rate = 1 - (24789123 / 847293847) = 97.1% — должно быть >99.5%

# Рекомендация: 70-80% от RAM для dedicated MySQL server
# 64 GB * 0.75 = 48 GB

# Изменяем онлайн (MySQL 8.0)
SET GLOBAL innodb_buffer_pool_size = 51539607552;  # 48 GB

# Делаем постоянным
# /etc/mysql/mysql.conf.d/performance.cnf
[mysqld]
innodb_buffer_pool_size = 48G
innodb_buffer_pool_instances = 8
innodb_log_file_size = 2G
innodb_log_buffer_size = 64M
innodb_flush_method = O_DIRECT
innodb_io_capacity = 2000
innodb_io_capacity_max = 4000
innodb_read_io_threads = 8
innodb_write_io_threads = 8
innodb_flush_neighbors = 0

# После изменения buffer pool hit rate:
# 97.1% → 99.8%

Slow query log и pt-query-digest

Performance Schema показывает агрегированную статистику. Для анализа конкретных запросов нужен slow query log:

# Включаем slow query log
SET GLOBAL slow_query_log = ON;
SET GLOBAL long_query_time = 0.5;  # логируем запросы дольше 500 мс
SET GLOBAL log_queries_not_using_indexes = ON;
SET GLOBAL min_examined_row_limit = 1000;
SET GLOBAL slow_query_log_file = '/var/log/mysql/slow.log';

Анализируем через pt-query-digest (Percona Toolkit):

# Устанавливаем Percona Toolkit
sudo apt install percona-toolkit

# Анализируем slow query log
pt-query-digest /var/log/mysql/slow.log --limit 20

# Результат:
# Profile
# Rank Query ID                     Response time   Calls  R/Call
# ==== ============================ =============== ====== ======
#    1 0xABC123DEF456789012345678   412.3s  38.2%   8742   0.0472
#      SELECT wp_postmeta JOIN wp_posts
#    2 0xDEF456789012345678ABC123   198.7s  18.4%   3421   0.0581
#      SELECT wp_posts WHERE post_type
#    3 0x789012345678ABC123DEF456   156.2s  14.5%   45123  0.0035
#      UPDATE wp_options SET option_value

# Детальный анализ конкретного запроса
pt-query-digest /var/log/mysql/slow.log \
  --filter '$event->{fingerprint} =~ m/wp_postmeta/' \
  --limit 5

Optimizer hints для сложных запросов:

-- WordPress генерирует неоптимальные запросы для WP_Query
-- Пример: поиск постов с определённым meta_value
-- Без hint: MySQL выбирает неправильный индекс

-- Через хук в functions.php добавляем optimizer hint
SELECT /*+ INDEX(pm idx_postid_metakey) NO_BNL(p, pm) */
    p.ID, p.post_title, pm.meta_value
FROM wp_posts p
INNER JOIN wp_postmeta pm ON p.ID = pm.post_id
WHERE p.post_type = 'post'
    AND p.post_status = 'publish'
    AND pm.meta_key = 'custom_seo_title'
ORDER BY p.post_date DESC
LIMIT 20;

-- Проверяем, что hint применился
EXPLAIN FORMAT=TREE SELECT /*+ INDEX(pm idx_postid_metakey) */ ...

Результаты оптимизации

Все оптимизации заняли 2 дня работы, без изменений в коде WordPress (только конфигурация MySQL и SQL-индексы):

МетрикаДоПосле
Загрузка списка постов8-12 сек1.0-1.5 сек
Сохранение поста5-7 сек0.6-0.8 сек
Поиск по постам3-5 сек0.4-0.7 сек
Фризы (cron contention)10-15 сек каждые 2-3 часаИсчезли
InnoDB buffer pool hit rate97.1%99.8%
Autoloaded options12 000 строк2 153 строки
wp_postmeta avg query time5.7 мс0.3 мс

Ключевые выводы:

  • Performance Schema — бесплатная диагностика. Не нужны сторонние инструменты, чтобы найти 80% проблем. Включите events_statements_summary и sys schema.
  • InnoDB buffer pool — первое, что нужно проверить. 128 MB по умолчанию на сервере с 64 GB RAM — преступление.
  • wp_postmeta и wp_options — две главные проблемы WordPress на MySQL. Индексы и очистка transients решают 90% проблем с производительностью.
  • pt-query-digest — must-have для регулярного аудита. Запускайте раз в неделю на slow query log.

Если ваш WordPress (или любое приложение на MySQL) тормозит — обращайтесь к нам в itfresh.ru, мы найдём и устраним узкие места через Performance Schema.

Часто задаваемые вопросы

Влияние минимально — 2-5% overhead при полном включении всех consumers. В MySQL 8.0 Performance Schema включён по умолчанию с основными consumers. Дополнительные consumers (events_waits_history_long) можно включать только на время диагностики и потом отключать. Для продакшена держите включёнными events_statements_summary и sys schema — это практически бесплатно.
Query cache удалён из MySQL 8.0 полностью. В MySQL 5.7 его рекомендовали отключать для нагрузки с частыми записями — каждый INSERT/UPDATE инвалидировал весь кэш для таблицы, создавая lock contention. Вместо query cache используйте Redis или Memcached на уровне приложения, а в MySQL настройте InnoDB buffer pool — это гораздо эффективнее.
Для InnoDB OPTIMIZE TABLE пересоздаёт таблицу и перестраивает индексы. Это полезно после массового DELETE (освобождает неиспользуемое место), но блокирует таблицу на время выполнения. Для WordPress wp_postmeta рекомендуем запускать раз в месяц в maintenance window. Для большинства таблиц OPTIMIZE не нужен — InnoDB эффективно переиспользует пространство.
Для dedicated MySQL-сервера — 70-80% от RAM. Для shared-сервера (MySQL + веб-сервер) — 50-60%. Если buffer pool hit rate ниже 99% — увеличивайте. В MySQL 8.0 можно менять размер онлайн через SET GLOBAL. Важно: innodb_buffer_pool_instances должен быть 8 при buffer pool > 1 GB для параллельного доступа.
Они дополняют друг друга. Performance Schema (events_statements_summary_by_digest) показывает агрегированную статистику по всем запросам в реальном времени, без настройки. pt-query-digest анализирует slow query log и даёт более детальный отчёт: распределение времени, EXPLAIN для каждого запроса, рекомендации по индексам. Используйте Performance Schema для оперативного мониторинга, pt-query-digest — для еженедельного аудита.

Нужна помощь с проектом?

Специалисты АйТи Фреш помогут с архитектурой, DevOps, безопасностью и разработкой — 15+ лет опыта

📞 Связаться с нами
#MySQL#Performance Schema#sys schema#slow query#InnoDB buffer pool#lock contention#optimizer hints#pt-query-digest
Комментарии 0

Оставить комментарий

загрузка...