Все уроки
ДанныеСредний11 мин чтенияВ моём стеке

MySQL и MariaDB

InnoDB, индексы, транзакции, пользователи, медленные запросы, репликация и бэкапы на практике.

  • MySQL 8 / MariaDB
  • InnoDB
  • Replication
  • Backups
Шаг 1/6
Запрос: найти пользователя с id = 42. Индекса нет, поэтому база читает таблицу строку за строкой. Уже потеет.

За 30 секунд

MySQL — одна из самых распространённых open-source реляционных баз данных и выбор по умолчанию для большинства PHP- и Laravel-хостингов. Её движок InnoDB даёт транзакции, блокировки на уровне строк и восстановление после сбоев. MariaDB — общественный форк, который по-прежнему во многом совместим. Чтобы хорошо эксплуатировать любую из них, нужны правильные индексы, несколько ключевых настроек, аккуратные привилегии пользователей и бэкапы, которые вы действительно восстанавливали.

Почему я это использую

  • Я использую MySQL и MariaDB в большинстве своих Laravel-проектов, потому что они быстры на типичных веб-нагрузках, поддерживаются везде и хорошо знакомы панелям хостинга и командам.
  • Я полагаюсь на транзакции InnoDB при оформлении заказа, операциях с кошельком и складом, где несколько строк должны измениться вместе или не измениться вовсе.
  • Я держу базы данных привязанными к localhost или приватной сети, с отдельным пользователем на каждое приложение, потому что база, открытая в интернет, — одна из самых частых причин взломов.
  • Я ценю slow query log и EXPLAIN, потому что большинство проблем с производительностью, которые я исправлял, были отсутствующим индексом, а не нехваткой железа.

Как это работает

01InnoDB: как MySQL хранит данные

InnoDB — движок хранения по умолчанию и единственный, который стоит использовать для данных приложения. Каждая таблица хранится как кластерный индекс: строки физически упорядочены по первичному ключу внутри B-tree. Вторичные индексы хранят индексируемые столбцы плюс первичный ключ и используют его, чтобы найти полную строку. Отсюда два следствия. Короткий, постоянно растущий первичный ключ (bigint) держит каждый индекс маленьким, а вставки дешёвыми, тогда как случайные UUID вызывают разбиение страниц и раздувание. И поиск через вторичный индекс стоит двух проходов, если индекс не покрывает все нужные столбцы. Кроме того, InnoDB ведёт redo log для восстановления после сбоев, undo log для откатов и согласованного чтения и buffer pool, который кэширует страницы в памяти.

  • innodb_buffer_pool_size: около 50–70% RAM на выделенном сервере базы данных.
  • Каждой таблице нужен явный первичный ключ.
  • Используйте utf8mb4 везде; старая кодировка utf8 не умеет хранить эмодзи и некоторые письменности.

02Индексы и EXPLAIN

Индексы MySQL — это B-tree, и правило левого префикса действует: индекс по (status, created_at) помогает WHERE status = 'paid' и WHERE status = 'paid' AND created_at > ..., но не одному created_at. Покрывающий индекс содержит все столбцы, нужные запросу, поэтому MySQL вообще не обращается к таблице; EXPLAIN показывает 'Using index'. Функции над столбцами, например WHERE DATE(created_at) = ..., мешают использованию индекса; переписывайте их как диапазоны. Ведущие wildcard'ы (LIKE '%term') не могут использовать B-tree, поэтому применяйте FULLTEXT или поисковый движок. Используйте EXPLAIN, чтобы увидеть план, и EXPLAIN ANALYZE (MySQL 8), чтобы увидеть реальные тайминги. Следите за столбцом type: const и ref — хорошо, ALL означает полное сканирование таблицы.

sql
ALTER TABLE orders ADD INDEX idx_status_created (status, created_at);
 
EXPLAIN ANALYZE
SELECT id, total_minor
FROM orders
WHERE status = 'paid'
  AND created_at >= '2026-10-01'
  AND created_at <  '2026-11-01';

03Транзакции и блокировки

Транзакция группирует операторы так, что все они либо выполняются, либо нет (ACID: атомарность, согласованность, изолированность, долговечность). Уровень изоляции InnoDB по умолчанию — REPEATABLE READ: внутри транзакции вы видите согласованный снимок. Обычные SELECT никогда не блокируют писателей благодаря MVCC (многоверсионному управлению конкурентным доступом). Когда нужно прочитать строку, а затем безопасно её обновить, например остаток на складе или баланс кошелька, заблокируйте её через SELECT ... FOR UPDATE или обновите атомарно одним оператором с условием. Держите транзакции короткими: длинные удерживают блокировки, мешают другим и раздувают undo log. Дедлоки случаются, когда две транзакции блокируют строки в противоположном порядке; InnoDB их обнаруживает и откатывает одну из них, поэтому ваш код должен повторять попытку.

sql
START TRANSACTION;
 
SELECT stock FROM variants WHERE id = 42 FOR UPDATE;
 
UPDATE variants SET stock = stock - 2
WHERE id = 42 AND stock >= 2;   -- 0 rows affected = out of stock
 
INSERT INTO order_items (order_id, variant_id, qty, unit_price_minor)
VALUES (1001, 42, 2, 2490);
 
COMMIT;

04Пользователи и привилегии

Учётная запись в MySQL — это пользователь плюс хост: 'app'@'localhost' и 'app'@'%' — разные учётные записи. Следуйте принципу наименьших привилегий. Пользователь приложения получает только права на данные (SELECT, INSERT, UPDATE, DELETE) в своей базе. Отдельный пользователь для миграций получает права на схему (CREATE, ALTER, INDEX, DROP), а для бэкапов заводится своя учётная запись только на чтение. Никогда не позволяйте приложениям подключаться как root. Начиная с MySQL 8.4 старый плагин mysql_native_password по умолчанию отключён; по умолчанию используется caching_sha2_password, и он работает с актуальными драйверами. Привяжите сервер к 127.0.0.1 или адресу приватной сети через bind-address и держите порт 3306 закрытым в файрволе. Если соединения идут через сеть, используйте TLS.

sql
CREATE USER 'shop_app'@'localhost' IDENTIFIED BY 'use-a-long-random-secret';
GRANT SELECT, INSERT, UPDATE, DELETE ON shop.* TO 'shop_app'@'localhost';
 
CREATE USER 'shop_migrate'@'localhost' IDENTIFIED BY 'another-long-secret';
GRANT ALL PRIVILEGES ON shop.* TO 'shop_migrate'@'localhost';
 
SHOW GRANTS FOR 'shop_app'@'localhost';

05Slow query log

Slow query log записывает каждый оператор, который выполняется дольше long_query_time секунд. Это самый быстрый способ найти то, что реально болит в продакшене, вместо того чтобы гадать. Включите его с порогом примерно от половины секунды до секунды, при желании логируйте на staging-копии запросы, которые не используют индексы, а затем сведите файл в отчёт. mysqldumpslow поставляется вместе с MySQL; pt-query-digest из Percona Toolkit даёт отчёт получше, сгруппированный по отпечатку запроса, и показывает, какие запросы потребляют больше всего суммарного времени. Исправьте первые несколько, обычно индексом или переписанным запросом, и повторите. performance_schema и схема sys дают ту же картину в реальном времени, например sys.statements_with_runtimes_in_95th_percentile.

ini
# /etc/mysql/mysql.conf.d/mysqld.cnf
[mysqld]
slow_query_log      = ON
slow_query_log_file = /var/log/mysql/slow.log
long_query_time     = 0.5
 
# then summarise:
# sudo pt-query-digest /var/log/mysql/slow.log | less

06Основы репликации

Репликация копирует изменения с сервера-источника на одну или несколько реплик. Источник записывает каждое изменение в свой бинарный лог (binlog); каждая реплика забирает binlog и воспроизводит его. Используйте построчный формат (по умолчанию) и GTID (глобальные идентификаторы транзакций), которые сильно упрощают failover и перенастройку реплик. По умолчанию репликация асинхронная, поэтому реплика может отставать; чтение с неё может быть слегка устаревшим, что важно для сценариев «прочитай свою запись», например показа заказа сразу после оформления. Реплики полезны для масштабирования чтения, отчётов и как тёплый резерв. Репликация — это не бэкап: DROP TABLE или ошибочный UPDATE реплицируются мгновенно. Начиная с MySQL 8.4 используются термины source и replica, с командами вроде SHOW REPLICA STATUS.

07Бэкапы и MySQL vs MariaDB

Бэкапы бывают двух видов. Логические бэкапы, сделанные через mysqldump или утилиты дампа MySQL Shell, экспортируют SQL или файлы данных; они переносимы между версиями и легко восстанавливаются частично, но медленны для больших баз. Используйте --single-transaction для согласованного снимка InnoDB без блокировок. Физические бэкапы копируют сами файлы данных с помощью Percona XtraBackup для MySQL или mariadb-backup для MariaDB; для больших баз их гораздо быстрее делать и восстанавливать, и они поддерживают инкрементальные бэкапы. Храните binlog'и, чтобы можно было восстановиться на момент времени, держите копии вне сервера и регулярно проверяйте восстановление. MariaDB отделилась от MySQL в 2009 году. В повседневном SQL они всё ещё близки, но разошлись: JSON, аутентификация, репликация и возможности оптимизатора различаются, поэтому относитесь к ним как к разным продуктам и тестируйте перед переходом.

bash
# consistent logical backup, compressed, off to another location
mysqldump --single-transaction --routines --triggers --events \
  -u backup_user -p shop | gzip > shop-$(date +%F).sql.gz
 
# restore into a test database to prove the backup works
gunzip < shop-2026-11-05.sql.gz | mysql -u root -p shop_restore_test

Когда применять

  • Типичные веб- и e-commerce нагрузки с большим количеством коротких чтений и записей.
  • Laravel- и PHP-проекты, особенно на shared-хостинге или хостинге с панелью управления.
  • Команды, которым нужна зрелая, хорошо документированная база с простой репликацией.
  • Приложения с преобладанием чтения, которые можно масштабировать репликами.

Когда не стоит

  • Когда нужны продвинутые возможности вроде богатой индексации JSONB, row-level security, exclusion constraints или множества расширений; здесь сильнее PostgreSQL.
  • Тяжёлая аналитика по миллиардам строк, где лучше подходит колоночная база вроде ClickHouse.

Альтернативы

  • PostgreSQL

    Более богатый SQL, JSONB, расширения и row-level security; мой выбор для сложного мультитенантного SaaS.

  • MariaDB

    Общественный форк, распространённый в дистрибутивах Linux и панелях хостинга; в основном совместим, но расходится.

  • Managed MySQL (RDS, Aurora, PlanetScale)

    Бэкапы, failover и обновления берёт на себя провайдер, но дороже.

Лучшие практики

  1. Используйте InnoDB, utf8mb4 и явный короткий первичный ключ в каждой таблице.
  2. Привязывайте сервер к localhost или приватной сети и закрывайте порт 3306 для публичного доступа.
  3. Дайте каждому приложению своего пользователя с минимальными привилегиями; никогда не подключайтесь как root.
  4. Включите slow query log и регулярно его просматривайте.
  5. Держите транзакции короткими и повторяйте попытку при дедлоке.
  6. Сочетайте регулярные бэкапы с binlog'ами для восстановления на момент времени и проверяйте восстановление.
  7. Оставайтесь на поддерживаемой ветке релизов, например MySQL 8.4 LTS.

Частые ошибки

  • Использовать устаревшую кодировку utf8, которая на самом деле является 3-байтовым подмножеством.
  • Оборачивать индексированные столбцы в функции, из-за чего индексы игнорируются.
  • Считать реплику бэкапом.
  • Запускать mysqldump без --single-transaction на нагруженном сервере.
  • Оставлять сервер слушающим публичный интерфейс со слабыми паролями.

Глубже

08Память и надёжность: buffer pool, redo log и настройки сброса

Большая часть тюнинга MySQL сводится к нескольким настройкам InnoDB. Buffer pool кэширует страницы данных и индексов; на выделенном сервере базы ему обычно отдают от 50 до 75 процентов RAM, и цель в том, чтобы горячий рабочий набор помещался целиком и чтения редко шли на диск. innodb_dedicated_server = ON автоматически выбирает размеры buffer pool и redo log по памяти машины — хороший вариант по умолчанию для хоста, где работает только MySQL. Redo log записывает изменения до того, как пишутся страницы данных; начиная с MySQL 8.0.30 его размер задаётся одной настройкой, innodb_redo_log_capacity, а слишком маленький журнал под нагрузкой записи вызывает постоянный сброс. Надёжность — это компромисс, который вы выбираете явно. innodb_flush_log_at_trx_commit = 1 и sync_binlog = 1 сбрасывают данные на каждом коммите и переживают отключение питания без потери закоммиченных транзакций; значения 2 и 0 быстрее, но при сбое можно потерять около секунды коммитов. Для всего, что связано с деньгами, оставляйте 1 и 1.

ini
# /etc/mysql/mysql.conf.d/mysqld.cnf  (dedicated DB host, 16 GB RAM)
[mysqld]
innodb_buffer_pool_size        = 10G
innodb_redo_log_capacity       = 4G
innodb_flush_log_at_trx_commit = 1     # full durability
sync_binlog                    = 1
max_connections                = 300   # each connection also uses memory
binlog_expire_logs_seconds     = 604800  # keep 7 days for point-in-time recovery
 
# check how often reads miss the buffer pool:
# SHOW GLOBAL STATUS LIKE 'Innodb_buffer_pool_read%';

09Онлайн-изменения схемы: INSTANT, INPLACE и metadata locks

У ALTER TABLE в MySQL 8 три алгоритма. INSTANT меняет только метаданные и завершается за миллисекунды независимо от размера таблицы; начиная с 8.0.29 он умеет добавлять и удалять колонки в любой позиции, а также переименовывать колонки и менять значения по умолчанию. INPLACE перестраивает или изменяет таблицу, не блокируя чтение и запись для многих операций, например добавления вторичного индекса с LOCK=NONE, но всё равно требует времени и I/O, а на репликах изменение потом проигрывается заново и вызывает отставание. COPY перестраивает всю таблицу и блокирует запись. Указывайте алгоритм явно: тогда MySQL откажется, а не будет молча откатываться на что-то медленнее. Скрытая опасность — metadata lock. Каждому ALTER нужна короткая эксклюзивная блокировка метаданных в начале и в конце. Если на таблице открыта длинная транзакция, ALTER ждёт, а каждый новый запрос к этой таблице становится в очередь за ALTER: сайт выглядит зависшим. Задавайте короткий lock_wait_timeout и повторяйте. Для огромных таблиц или рискованных изменений gh-ost или pt-online-schema-change копируют таблицу в фоне небольшими порциями с троттлингом.

sql
SET SESSION lock_wait_timeout = 5;   -- give up quickly instead of freezing traffic
 
ALTER TABLE orders ADD COLUMN gift_note VARCHAR(255) NULL, ALGORITHM=INSTANT;
 
ALTER TABLE orders ADD INDEX idx_orders_customer (customer_id, created_at),
  ALGORITHM=INPLACE, LOCK=NONE;
 
-- who is holding or waiting for metadata locks?
SELECT * FROM sys.schema_table_lock_waits\G

10Внутреннее устройство блокировок и анализ deadlock

InnoDB блокирует записи индекса, а не строки в абстрактном смысле, и это объясняет большинство сюрпризов. Блокирующее чтение или UPDATE блокирует каждую просканированную запись индекса, а не только изменённые, поэтому UPDATE, чей WHERE не может использовать индекс, может заблокировать большую часть таблицы. При REPEATABLE READ InnoDB также берёт gap- и next-key-блокировки на диапазоны, чтобы предотвратить фантомы, а значит, INSERT может ждать диапазон, который просканировала другая транзакция. Практические правила: убедитесь, что UPDATE и DELETE фильтруют по индексу; трогайте строки в одном и том же порядке, например отсортированными по первичному ключу, чтобы две транзакции не могли блокировать в противоположном порядке; держите транзакции короткими. Когда случается deadlock, InnoDB откатывает более дешёвую транзакцию с ошибкой 1213, и приложению нужно просто повторить всю транзакцию. Чтобы разобраться в конкретном случае, читайте раздел LATEST DETECTED DEADLOCK в SHOW ENGINE INNODB STATUS или включите innodb_print_all_deadlocks = ON, чтобы логировать каждый deadlock. Текущие ожидания видны в performance_schema.data_locks и sys.innodb_lock_waits.

sql
-- who blocks whom right now
SELECT waiting_pid, waiting_query, blocking_pid, blocking_query, wait_age
FROM sys.innodb_lock_waits;
 
-- last deadlock, with both transactions and the locks they held
SHOW ENGINE INNODB STATUS\G
 
-- log every deadlock to the error log
SET PERSIST innodb_print_all_deadlocks = ON;

11Высокая доступность: параллельная репликация, semi-sync и кластеры

Базовая асинхронная репликация может потерять последние транзакции, если источник упадёт, а реплика может отставать под тяжёлой записью. С этим помогают несколько возможностей. Многопоточная репликация применяет транзакции на реплике параллельно; replica_parallel_workers по умолчанию равен 4 начиная с 8.0.27, а replica_preserve_commit_order сохраняет безопасный порядок коммитов. Полусинхронная репликация заставляет источник ждать, пока хотя бы одна реплика получит каждую транзакцию, прежде чем подтвердить коммит: немного задержки в обмен на то, что подтверждённые записи не теряются при failover. Для автоматического failover MySQL InnoDB Cluster объединяет Group Replication — протокол консенсуса между тремя и более узлами — с MySQL Router, который направляет соединения на текущий primary. Пользователи MariaDB часто выбирают вместо этого Galera Cluster. Что бы вы ни выбрали, мониторьте отставание (SHOW REPLICA STATUS, а лучше heartbeat-таблицу), избегайте огромных одиночных транзакций, которые реплики должны проигрывать последовательно, и отправляйте запросы read-your-own-write на primary.

  • Разбивайте большие UPDATE или DELETE на порции по несколько тысяч строк, чтобы реплики не отставали.
  • Первичный ключ в каждой таблице обязателен для Group Replication и полезен для построчной репликации.
  • Репетируйте failover; кластер, на котором никто не делал переключение, — это непроверенное предположение.

Реальные ситуации

01

Простой ALTER TABLE заморозил весь сайт

Проблема

Во время деплоя миграция добавила индекс в таблицу orders. За секунды все страницы, работающие с orders, зависли, а список процессов заполнился «Waiting for table metadata lock».

Как решить

Сначала восстановитесь: найдите блокирующего через SELECT * FROM sys.schema_table_lock_waits; обычно это долгая или простаивающая открытая транзакция, например забытая консольная сессия или отчётный запрос. Убейте блокирующего или убейте ALTER, чтобы трафик снова пошёл. Затем предотвратите повторение. Запускайте миграции с SET SESSION lock_wait_timeout = 5 и автоматически повторяйте несколько раз, чтобы заблокированный ALTER сдавался, а не выстраивал всех в очередь за собой. Явно указывайте ALGORITHM=INSTANT или INPLACE, LOCK=NONE. Настройте алерт на транзакции, открытые дольше минуты. Для больших таблиц или изменений, требующих копирования, используйте gh-ost или pt-online-schema-change, которые притормаживают по нагрузке и отставанию реплик.

02

Отставание реплик резко растёт каждую ночь

Проблема

Read-реплики отстают на 20–30 минут во время ночной задачи очистки, поэтому клиенты видят устаревшие статусы заказов на страницах, читающих с реплик.

Как решить

Посмотрите на задачу: один DELETE или UPDATE, затрагивающий миллионы строк, — это одна транзакция, которую реплики должны проиграть целиком, и распараллелить её нельзя. Перепишите её на работу порциями, например DELETE ... WHERE created_at < ? ORDER BY id LIMIT 5000 в цикле с короткой паузой и остановкой, когда отставание реплик превышает порог. Убедитесь, что WHERE использует индекс, чтобы каждая порция была быстрой. Проверьте, что replica_parallel_workers больше 1, а у реплик такое же железо, как у источника. Для страниц, показывающих данные сразу после изменения, например статус заказа после оплаты, читайте с primary или используйте sticky reads на несколько секунд после записи.

03

MySQL убивает OOM killer под нагрузкой

Проблема

На сервере с 8 GB RAM, где работают MySQL и приложение, MySQL перезапускается в пиках трафика. Лог ядра показывает, что OOM killer выбрал mysqld.

Как решить

Честно посчитайте память. Buffer pool был выставлен на 6 GB, как будто MySQL на сервере один, а max_connections был 1,000, при том что каждое соединение может выделять буферы для сортировки, join и временных таблиц. Уменьшите buffer pool, чтобы оставить место приложению и операционной системе, например до 3–4 GB на этом общем хосте. Ограничьте max_connections тем, что реально нужно приложению, и используйте пул соединений в приложении, чтобы пики вставали в очередь, а не открывали сотни сессий. Оставьте сессионные буферы вроде sort_buffer_size по умолчанию. Следите за памятью по компонентам через memory-представления схемы sys. Если нагрузка продолжает расти, вынесите MySQL на отдельный сервер, где innodb_dedicated_server сможет безопасно подобрать размеры.

Шпаргалка

Мгновенно добавить колонку
ALTER TABLE orders ADD COLUMN note VARCHAR(255) NULL, ALGORITHM=INSTANT;
Построить индекс без блокировки записи
ALTER TABLE orders ADD INDEX idx_c (customer_id), ALGORITHM=INPLACE, LOCK=NONE;
Быстро сдаваться на metadata lock
SET SESSION lock_wait_timeout = 5;
Посмотреть ожидания metadata lock
SELECT * FROM sys.schema_table_lock_waits;
Посмотреть ожидания блокировок строк
SELECT * FROM sys.innodb_lock_waits;
Прочитать последний deadlock
SHOW ENGINE INNODB STATUS\G
Проверить здоровье и отставание реплики
SHOW REPLICA STATUS\G
Найти долго открытые транзакции
SELECT * FROM information_schema.innodb_trx ORDER BY trx_started LIMIT 10;
Автоподбор размеров для выделенного хоста БД
innodb_dedicated_server = ON
Онлайн-изменение огромной таблицы
gh-ost --database=shop --table=orders --alter="ADD INDEX idx_s (status)" --execute # plus connection flags

В моих реальных проектах

  • Несколько продакшен-серверов, которые я веду, хостят Laravel-сайты на MariaDB/MySQL на стеках под управлением Plesk.
  • Мой базовый стандарт безопасности для каждого сервера держит базы данных привязанными к localhost или приватной сети, никогда — к публичной.
  • Я проверил бэкапы панели на этих серверах, и мой стандарт — снимки плюс проверенное восстановление, а бэкапы вне сервера — часть текущего плана консолидации.
  • В Laravel-бэкендах, которыми я руковожу, например в платформе заказов для ресторанов с кэшбэк-кошельком и магазине зоотоваров со складскими движениями, оформление заказа и изменения баланса — это именно те сценарии, где транзакции важнее всего.

Вопросы и ответы

Q1Почему выбор первичного ключа так важен в InnoDB?

Потому что InnoDB хранит таблицу как кластерный индекс, упорядоченный по первичному ключу, а каждый вторичный индекс хранит значение первичного ключа как указатель на строку. Короткий последовательный ключ, например bigint, означает, что новые строки добавляются в конец B-tree, страницы остаются заполненными, а каждый вторичный индекс — маленьким. Случайный UUID, сохранённый строкой, заставляет каждую вставку попадать на случайную страницу, вызывая разбиение страниц, фрагментацию и больше I/O, и раздувает каждый вторичный индекс. Если UUID нужны, я храню их как BINARY(16), предпочитаю упорядоченные по времени UUIDv7 или оставляю ключ bigint и отдельный уникальный публичный id.

Q2Как найти и исправить медленный запрос в MySQL?

Я начинаю с данных, а не с догадок: slow query log или performance_schema, сведённые через pt-query-digest, чтобы найти запросы с наибольшим суммарным временем, а не просто самый медленный одиночный запуск. Для главного запроса я запускаю EXPLAIN ANALYZE и проверяю тип доступа, соотношение просмотренных и возвращённых строк и наличие filesort или временной таблицы. Типичные исправления: составной индекс с правильным порядком столбцов, покрывающий индекс, переписывание функции над столбцом в диапазон, замена пагинации через OFFSET на keyset-пагинацию или устранение паттерна N+1 в ORM. Потом я измеряю снова.

Q3Какой уровень изоляции использует InnoDB и что это значит на практике?

По умолчанию REPEATABLE READ. Внутри транзакции обычные SELECT читают из согласованного снимка, сделанного при первом чтении, поэтому повторные чтения возвращают те же данные, даже если другие закоммитили изменения, а читатели никогда не блокируют писателей благодаря MVCC. Для блокирующих чтений, например SELECT ... FOR UPDATE, InnoDB использует next-key locks, которые блокируют ещё и промежутки, чтобы не появлялись фантомные строки, и это может вызывать больше ожиданий блокировок и дедлоков, чем люди ожидают. Многие команды переходят на READ COMMITTED ради меньшего количества блокировок. В любом случае для логики «прочитать, затем записать», как с остатками или балансами, я использую явные блокировки или атомарные условные обновления.

Q4mysqldump или физический бэкап?

Для маленьких и средних баз, до нескольких десятков гигабайт, mysqldump с --single-transaction прост, переносим между версиями и позволяет восстановить одну таблицу. Его слабость — скорость: дамп делается медленно, а восстановление ещё намного медленнее, потому что каждый индекс перестраивается. Для больших баз я использую физические бэкапы через Percona XtraBackup или mariadb-backup, которые копируют файлы данных на работающем сервере, быстро восстанавливаются и поддерживают инкрементальные копии. В обоих случаях я храню бинарные логи для восстановления на момент времени, шифрую и храню копии вне сервера и регулярно восстанавливаю их в тестовый экземпляр, потому что непроверенный бэкап — это только надежда.

Q5MySQL или MariaDB: это одно и то же?

У них общая история и большая часть повседневного SQL, поэтому типичное Laravel-приложение работает на любой из них. Но после форка 2009 года они разошлись. У MySQL, которую разрабатывает Oracle, есть нативный бинарный тип JSON, собственные возможности оптимизатора и InnoDB Cluster; MariaDB хранит JSON как текст с валидацией, у неё свои возможности, такие как system-versioned tables, последовательности и другие движки хранения, а также свой формат репликации и GTID. Номера версий больше не совпадают, а репликация или физические бэкапы между ними не поддерживаются. Я выбираю одну на проект, использую её же в разработке и CI и тщательно тестирую перед миграцией с одной на другую.

Q6Как защитить сервер MySQL?

Сначала сеть: привяжите его к localhost или приватному интерфейсу и держите порт 3306 закрытым и в файрволе сервера, и в файрволе провайдера, чтобы он никогда не был доступен из интернета. Затем учётные записи: удалите анонимных пользователей и удалённый root, дайте каждому приложению своего пользователя, ограниченного его базой и операторами работы с данными, используйте отдельных пользователей для миграций и бэкапов и сильные сгенерированные пароли, хранящиеся как секреты. Используйте TLS, когда трафик идёт через сеть. Держите сервер на поддерживаемой версии с установленными патчами и шифруйте бэкапы. Наконец, ограничьте то, что может делать приложение, чтобы SQL-инъекция не могла удалить таблицы.

Вопросы уровня senior

S1Как бы вы изменили схему таблицы MySQL на 500 миллионов строк без простоя?

Сначала проверяю, может ли изменение быть INSTANT: добавление или удаление колонок, переименование или смена значений по умолчанию обычно может на MySQL 8.0.29 и новее, и тогда размер таблицы не важен. Если нет, INPLACE-изменение с LOCK=NONE на таблице такого размера всё равно вызывает тяжёлый I/O и большое отставание реплик, потому что реплики потом проигрывают его заново. Поэтому я использую gh-ost или pt-online-schema-change: они создают теневую таблицу с новой схемой, копируют строки маленькими порциями, применяют текущие изменения (gh-ost читает binlog, pt-osc использует триггеры), притормаживают по нагрузке и отставанию реплик и завершают быстрым атомарным переименованием. Обоим всё равно нужен короткий metadata lock при переключении, поэтому я задаю lock_wait_timeout, планирую переключение на время низкого трафика и убеждаюсь, что нет открытых длинных транзакций. А само изменение следует схеме expand and contract, чтобы работали и старый, и новый код.

S2READ COMMITTED или REPEATABLE READ для нагруженного OLTP-приложения?

REPEATABLE READ — уровень по умолчанию, он даёт каждой транзакции стабильный снимок, но для блокирующих чтений, UPDATE и DELETE берёт gap- и next-key-блокировки на просканированные диапазоны. При высокой конкурентности это даёт больше ожиданий блокировок и deadlock, особенно при вставках в нагруженные диапазоны. READ COMMITTED для большинства операций берёт только блокировки записей и рано отпускает блокировки на неподходящих строках, поэтому конкуренции меньше; цена — неповторяемые чтения, то есть два одинаковых SELECT в одной транзакции могут увидеть разные закоммиченные данные, и он требует построчного бинарного журнала, который и так используется по умолчанию. Многие высоконагруженные инсталляции MySQL работают на READ COMMITTED, и он совпадает с уровнем по умолчанию в PostgreSQL. Моё мнение: корректность в любом случае не должна зависеть от уровня изоляции. Код, который читает, а потом пишет, должен использовать явные блокировки или атомарные условные обновления, и тогда READ COMMITTED обычно оказывается лучшим операционным выбором.

S3Как бы вы спроектировали высокую доступность MySQL при скромном бюджете?

Я подгоняю дизайн под реальные требования к простою и потере данных. Для многих бизнес-приложений один primary с одной репликой на отдельном хосте, репликация на GTID, semi-sync, чтобы не терять подтверждённые коммиты, и заскриптованный, отрепетированный ручной failover дают восстановление за минуты при небольших затратах. Бэкапы с binlog остаются защитой от человеческих ошибок, которые репликация копирует мгновенно. Когда минуты — это слишком долго, InnoDB Cluster из трёх узлов с MySQL Router даёт автоматический failover ценой большего объёма операционных знаний, первичного ключа в каждой таблице и чувствительности к сетевой задержке между узлами. Managed-сервис перекладывает эту работу на провайдера за более высокую ежемесячную плату. Компромисс всегда в деньгах и сложности против времени восстановления, поэтому я сначала записываю целевое время восстановления и допустимую потерю данных, а потом выбираю самую простую конфигурацию, которая им соответствует.

Глоссарий

Buffer pool
Основной кэш InnoDB в памяти для страниц данных и индексов; самая важная настройка памяти MySQL.
Redo log
Журнал упреждающей записи изменений, который InnoDB проигрывает после сбоя, чтобы не потерять закоммиченные транзакции.
Metadata lock
Блокировка определения таблицы; DDL нужна она эксклюзивно, и он ждёт открытые транзакции на таблице.
ALGORITHM=INSTANT
Режим ALTER TABLE, который меняет только метаданные и завершается почти мгновенно независимо от размера таблицы.
Next-key lock
Блокировка записи индекса вместе с промежутком перед ней; используется для предотвращения фантомных строк.
Полусинхронная репликация
Источник ждёт, пока реплика подтвердит получение транзакции, прежде чем подтвердить коммит.
Group Replication
Репликация MySQL на основе консенсуса между несколькими узлами, основа автоматического failover в InnoDB Cluster.
MySQL Router
Лёгкий прокси, который направляет соединения приложения на текущий primary или реплики кластера.
gh-ost
Инструмент онлайн-изменения схемы, копирующий таблицу порциями с троттлингом через binlog, без триггеров.

Есть идея продукта?Давайте создадим его.

Расскажите о своей идее, продукте или команде. Обычно отвечаю в течение одного рабочего дня.