Справочник DBA: базы данных изнутри читается по порядку

Статья 9.5 · раздел 9 «Бэкапы и восстановление» · 68 из 109

Бэкапы MySQL: mysqldump, XtraBackup с хвостом redo, prepare, инкремент по LSN и CLONE INSTANCE

опубликовано
читать
≈ 10 мин
автор
среда и версия
MySQL 8.4.13 в контейнерах mysql:8.4 (источник и получатель CLONE, по 1500 МБ памяти, буферный пул 256 МБ, GTID ON); Percona XtraBackup 8.4.0-7 в образе percona/percona-xtrabackup:8.4; таблица lab.t 5,24 млн строк, 699 МБ данных, каталог данных 990 МБ; нагрузка 16 клиентов по одной вставке за транзакцию, около 780 вставок в секунду; опыт выполнен 28 октября 2026
содержание решать задачи SQL на собеседовании

Резервную копию MySQL можно получить тремя способами, и у каждого свой способ быть несогласованным: mysqldump обрывается, если таблицу меняют во время дампа; копия файлов InnoDB, снятая на ходу, непригодна, пока к ней не применили redo; CLONE INSTANCE копирует сервер целиком, но заканчивается перезапуском получателя. Страницы InnoDB и номер LSN описаны в статье про redo и двухфазный коммит; здесь они нужны, чтобы увидеть, как XtraBackup читает redo параллельно с копированием файлов, а --prepare доводит копию до согласованности. Все четыре опыта сделаны на таблице в 5,2 млн строк (699 МБ), стратегия RPO и RTO разобрана в статье про бэкапы PostgreSQL.

DBA / 01

XtraBackup: файлы и redo идут параллельно, prepare сводит их

Полный бэкап XtraBackup: копирование файлов, хвост redo, prepare и восстановлениеСлева вверху два параллельных процесса. Первый копирует файл t.ibd размером 752 МиБ в четыре потока за 0,9 секунды. Второй читает redo с LSN 981089371 до 989457829. Оба ведут в файл xtrabackup_checkpoints: to_lsn 983481288, last_lsn 989457829, файлы сняты в разные моменты, redo доводит их до last_lsn. Ниже prepare за 5,5 секунды применяет 1121 запись redo и ставит backup_type full-prepared. Справа внизу после copy-back и запуска сервера в таблице 5254861 строка, что равно 5242880 строк до нагрузки плюс 11981 транзакций из GTID бэкапа.1 · копия файловt.ibd 752 МиБ, 4 потока: 0,9 сстраницы читаются на ходу2 · хвост redo, параллельноLSN 981089371 → 989457829около 8 МиБ redo за бэкапxtrabackup_checkpointsto_lsn = 983481288last_lsn = 989457829файлы сняты в разные моменты,redo доводит их до last_lsn3 · prepare: 5,5 сприменена 1121 запись redobackup_type = full-prepared4 · copy-back и старт5 254 861 строка в lab.t= 5 242 880 + GTID :1-11981
Файлы копируются, пока сервер пишет, поэтому на выходе они несогласованы; параллельный поток redo до last_lsn даёт всё, что нужно --prepare, чтобы довести копию до единого состояния. Число строк после восстановления равно числу строк на момент координаты GTID из бэкапа.

Стенд

Источник работает с GTID, как реплика в статье про репликацию MySQL. Каталог данных лежит в именованном томе, который XtraBackup монтирует только на чтение. Таблица заполняется удвоением десяти строк, 19 раз, в одной сессии с sql_log_bin=0: GTID потом считают только строки нагрузки.

N=dba-mysql-backups-xtrabackup-clone; D=$N-data
docker network create $N-net; docker volume create $D; docker volume create $N-bk
docker run -d --name $N-src --network $N-net --memory 1500m -v $D:/var/lib/mysql -e MYSQL_ROOT_PASSWORD=pg mysql:8.4 \
  --server-id=1 --gtid-mode=ON --enforce-gtid-consistency=ON --innodb-buffer-pool-size=256M
s() { docker exec -i $N-src mysql -uroot -ppg -t "$@"; }
xb() { docker run --rm --network $N-net --user root -v $D:/var/lib/mysql:ro -v $N-bk:/bk percona/percona-xtrabackup:8.4 "$@"; }
SQL="SET sql_log_bin=0; CREATE DATABASE lab;
CREATE TABLE lab.t (id bigint AUTO_INCREMENT PRIMARY KEY, v int, pad char(100));
CREATE TABLE lab.u (id int PRIMARY KEY, v int); INSERT INTO lab.u VALUES (1,1),(2,2),(3,3);
INSERT INTO lab.t (v,pad) SELECT seq, REPEAT('p',100) FROM (SELECT 1 AS seq UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 UNION SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9 UNION SELECT 10) x;
DELIMITER //
CREATE PROCEDURE lab.ins(secs int) BEGIN DECLARE e datetime DEFAULT now() + INTERVAL secs SECOND;
  WHILE now() < e DO INSERT INTO lab.t (v, pad) VALUES (0, REPEAT('l', 100)); END WHILE; END//
DELIMITER ;"
for i in $(seq 19); do SQL="$SQL
INSERT INTO lab.t (v,pad) SELECT v,pad FROM lab.t;"; done
s -e "$SQL"
s -e "select count(*), round(data_length/1048576) data_mb from information_schema.tables where table_name='t'; select @@gtid_executed"
xb xtrabackup --version
+----------+---------+
| count(*) | data_mb |
+----------+---------+
|  5242880 |     699 |
+----------+---------+
xtrabackup version 8.4.0-7 based on MySQL server 8.4.0 Linux (x86_64) (revision id: 4093cfd8)

@@gtid_executed пуст, строк 5 242 880: дальше одна нагрузочная вставка равна одному GTID. В Docker Hub самый свежий тег XtraBackup для 8.4 — 8.4.0-7, а сервер 8.4.13; копия с ним сработала, но это пара из разных патч-версий, и на своём сервере берут XtraBackup с версией не ниже версии сервера.

Опыт 1: mysqldump —single-transaction и DDL

--single-transaction открывает транзакцию со снимком и читает таблицы внутри неё. Дамп всех таблиц без конкурентов, вывод считается wc, на диск не пишется:

docker exec $N-src bash -c 'time mysqldump -uroot -ppg --single-transaction lab 2>/dev/null | wc -c'
602585372

real	0m2.856s
user	0m2.079s
sys	0m0.364s

2,9 с и 602 МБ текста: для сравнения с физической копией эта цифра — нижняя граница, потому что на диск дамп не писался. Теперь ALTER во время дампа. Сначала таблица t, которую дамп уже читает, а потом u, до которой он ещё не дошёл:

docker exec $N-src bash -c 'mysqldump -uroot -ppg --single-transaction lab 2>/dev/null | wc -c; echo rc=${PIPESTATUS[0]}' &
sleep 1; docker exec $N-src bash -c "time mysql -uroot -ppg -e 'ALTER TABLE lab.t ADD COLUMN c int'" &
sleep 1; s -e "SELECT ID, TIME, STATE, INFO FROM information_schema.PROCESSLIST WHERE STATE LIKE 'Waiting for table%'"; wait
+----+------+---------------------------------+------------------------------------+
| ID | TIME | STATE                           | INFO                               |
+----+------+---------------------------------+------------------------------------+
| 69 |    1 | Waiting for table metadata lock | ALTER TABLE lab.t ADD COLUMN c int |
+----+------+---------------------------------+------------------------------------+

real	0m1.331s
user	0m0.002s
sys	0m0.002s
613002201
rc=0

ALTER по таблице, которую дамп уже прочитал, не ошибается и не ломает дамп: он ждёт блокировку метаданных до конца транзакции дампа (1,3 с) и только потом применяется. Цена — все новые запросы к t встанут в очередь за ним. Вторая таблица ведёт себя иначе:

docker exec $N-src bash -c 'mysqldump -uroot -ppg --single-transaction lab 2>/tmp/err.txt | tail -c 300 | tail -3; echo rc=${PIPESTATUS[0]}; grep -v Warning /tmp/err.txt' &
sleep 1.5; docker exec $N-src mysql -uroot -ppg -e 'ALTER TABLE lab.u ADD COLUMN c int'; wait
-- Dumping data for table `u`
--

rc=3
mysqldump: Error in field count for table: `u` !  Aborting.

ALTER на u прошёл сразу: дамп ещё не трогал эту таблицу и блокировку метаданных на ней не держал. Когда дамп дошёл до u, число колонок в результате не совпало с определением, и mysqldump вышел с кодом 3 (mysqldump.cc, условие result_num_fields != num_fields, строки 4223–4228). Файл дампа обрывается на заголовке данных u, без итоговой строки. Повтор дал тот же код 3 и ту же ошибку.

Опыт 2: XtraBackup под нагрузкой, prepare и восстановление

Нагрузка — 16 клиентов по 40 секунд, а через 3 секунды стартует полный бэкап в четыре потока. Каталог данных источника в контейнере XtraBackup смонтирован :ro, а к серверу он подключается по сети:

for i in $(seq 16); do docker exec -d $N-src mysql -uroot -ppg -e "CALL lab.ins(40)"; done; sleep 3
xb xtrabackup --backup --host=$N-src --user=root --password=pg --datadir=/var/lib/mysql --target-dir=/bk/full --parallel=4

Главное из вывода (в журнале 315 строк, остальное — копирование .sdi и служебных файлов):

2026-10-28T00:36:19.913419-00:00 0 [Note] [MY-011825] [Xtrabackup] Starting to parse redo log at lsn = 981089371
2026-10-28T00:36:20.148979-00:00 2 [Note] [MY-011825] [Xtrabackup] space_id: 2, Copying ./lab/t.ibd to /bk/full/lab/t.ibd, size 788529152 (752.00 MiB)
2026-10-28T00:36:21.033293-00:00 2 [Note] [MY-011825] [Xtrabackup] space_id: 2, Done: Copying ./lab/t.ibd to /bk/full/lab/t.ibd
2026-10-28T00:36:21.575792-00:00 0 [Note] [MY-011825] [Xtrabackup] Stopping log copying thread at LSN 989457829
2026-10-28T00:36:22.614640-00:00 0 [Note] [MY-011825] [Xtrabackup] MySQL binlog position: filename 'binlog.000002', position '6550', GTID of the last change '160aa11d-d267-11f1-87f6-5ed31fb96cb1:1-11981'
2026-10-28T00:36:23.631914-00:00 0 [Note] [MY-011825] [Xtrabackup] Transaction log of lsn (981089452) to (990245169) was copied.
2026-10-28T00:36:23.838711-00:00 0 [Note] [MY-011825] [Xtrabackup] completed OK!

От первой строки журнала (00:36:19.733) до completed OK! прошло 4,1 с. Файл t.ibd в 752 МиБ скопирован за 0,9 с, а поток redo стартовал раньше файлов, с LSN 981089371, и остановился на 989457829: за бэкап сервер записал около 8 МиБ redo. Копия лежит в /bk/full и содержит xtrabackup_logfile с этим redo и файл xtrabackup_checkpoints:

xb cat /bk/full/xtrabackup_checkpoints
backup_type = full-backuped
from_lsn = 0
to_lsn = 983481288
last_lsn = 989457829
flushed_lsn = 988558676
redo_memory = 0
redo_frames = 0

full-backuped означает «не готова»: страницы файлов сняты в разные моменты, а last_lsn — до какой точки redo их надо довести. Механика в коде — два независимых цикла. Файлы раздаёт потокам data_copy_thread_func (xtrabackup.cc, строки 3542–3549). А отдельный поток Redo_Log_Data_Manager::copy_func читает redo, пока не дойдёт до stop_lsn; о его выставлении сообщает строка «Stopping log copying thread» в журнале выше:

  while (!aborted && (stop_lsn == 0 || stop_lsn > reader.get_scanned_lsn())) {
    xtrabackup_io_throttling();

    if (!copy_once(false, &finished)) {
      error = true;
      return;
    }

Цикл в redo_log.cc, строки 1195–1201. Теперь --prepare: он запускает встроенный InnoDB и проигрывает собранный redo над скопированными файлами. Это обычный crash recovery InnoDB, по сути тот же REDO, который в PostgreSQL выполняет стартующий сервер после kill -9.

time xb xtrabackup --prepare --target-dir=/bk/full
2026-10-28T00:36:53.896082-00:00 0 [Note] [MY-011825] [Xtrabackup] xtrabackup_logfile detected: size=10289152, start_lsn=(981089452)
2026-10-28T00:36:53.928783-00:00 0 [Note] [MY-012551] [InnoDB] Database was not shutdown normally!
2026-10-28T00:36:53.928788-00:00 0 [Note] [MY-012552] [InnoDB] Starting crash recovery.
2026-10-28T00:36:53.929061-00:00 0 [Note] [MY-013086] [InnoDB] Starting to parse redo log at lsn = 981089371, whereas checkpoint_lsn = 981089452 and start_lsn = 981089280
2026-10-28T00:36:53.969594-00:00 0 [Note] [MY-012550] [InnoDB] Doing recovery: scanned up to log sequence number 989457829
2026-10-28T00:36:54.023405-00:00 0 [Note] [MY-012532] [InnoDB] Applying a batch of 1121 redo log records ...
2026-10-28T00:36:54.603943-00:00 0 [Note] [MY-012535] [InnoDB] Apply batch completed!

Redo применяется с LSN 981089371 до 989457829: тот же номер, на котором остановился поток redo при бэкапе. Применение 1121 записи заняло 0,6 с, весь prepare вместе с запуском контейнера — 5,5 с, а xtrabackup_checkpoints теперь показывает backup_type = full-prepared. Восстановление: --copy-back в пустой том (3,9 с вместе с chown), запуск mysql:8.4 на этом томе:

docker volume create $N-restore
docker run --rm --user root -v $N-bk:/bk -v $N-restore:/restore percona/percona-xtrabackup:8.4 \
  sh -c 'xtrabackup --copy-back --target-dir=/bk/full --datadir=/restore; chown -R 999:999 /restore'
docker run -d --name $N-rst --network $N-net --memory 1500m -v $N-restore:/var/lib/mysql mysql:8.4 \
  --server-id=3 --gtid-mode=ON --enforce-gtid-consistency=ON --innodb-buffer-pool-size=256M
docker exec $N-rst mysql -uroot -ppg -t -e "select @@gtid_executed; select count(*) from lab.t"
+----------------------------------------------+
| @@gtid_executed                              |
+----------------------------------------------+
| 160aa11d-d267-11f1-87f6-5ed31fb96cb1:1-11981 |
+----------------------------------------------+
+----------+
| count(*) |
+----------+
|  5254861 |
+----------+

В таблице ровно 5 242 880 строк до нагрузки плюс 11 981 транзакция из xtrabackup_binlog_info: ни одной строки сверху, хотя нагрузка на источнике не прекращалась.

Опыт 3: инкремент по LSN

Новый полный бэкап base без нагрузки, затем UPDATE первых 60 000 строк (около 1 % таблицы), 20 секунд вставок и инкремент от base:

xb xtrabackup --backup --host=$N-src --user=root --password=pg --datadir=/var/lib/mysql --target-dir=/bk/base --parallel=4
s -e "UPDATE lab.t SET v = v + 1 WHERE id <= 60000"
for i in $(seq 16); do docker exec -d $N-src mysql -uroot -ppg -e "CALL lab.ins(20)"; done; sleep 3
xb xtrabackup --backup --host=$N-src --user=root --password=pg --datadir=/var/lib/mysql --target-dir=/bk/inc1 --incremental-basedir=/bk/base --parallel=4
xb sh -c 'du -sm /bk/base /bk/inc1; cat /bk/inc1/xtrabackup_checkpoints; ls -l /bk/inc1/lab/t.ibd.delta'
921	/bk/base
29	/bk/inc1
backup_type = incremental
from_lsn = 1009388925
to_lsn = 1011858197
last_lsn = 1018380429
flushed_lsn = 1016244224
-rw-r-----. 1 root root  6160384 Oct 28 00:37 t.ibd.delta

Инкремент весит 29 МиБ против 921 МиБ полной копии, а from_lsn = 1009388925 равен to_lsn базы: XtraBackup берёт из файла только страницы с LSN новее этой границы. t.ibd.delta — 6,2 МБ вместо 800 МиБ таблицы, хотя UPDATE и вставки изменили десятки тысяч строк: считаются страницы, а не строки. Но журнал инкремента содержит строку using the full scan for incremental backup: файл читается целиком, чтобы найти новые страницы, и инкремент занял 6,3 с против 6,1 с базы. Места на диске он экономит, времени без отслеживания изменённых страниц — нет.

Цепочку готовят так: xb xtrabackup --prepare --apply-log-only --target-dir=/bk/base (без отката незавершённых транзакций, пока впереди инкременты), затем --prepare --target-dir=/bk/base --incremental-dir=/bk/inc1. После --copy-back и запуска в восстановленной копии gtid_executed равен 160aa11d-d267-11f1-87f6-5ed31fb96cb1:1-44486, а в таблице 5 287 365 строк: 5 242 880 + 44 486 − 1, потому что один GTID занял сам UPDATE.

Опыт 4: CLONE INSTANCE

Второй пустой контейнер mysql:8.4 ($N-dst) клонирует первый по сети. Плагин ставится на обоих серверах, получателю разрешают донора:

s -e "INSTALL PLUGIN clone SONAME 'mysql_clone.so'"
docker exec -i $N-dst mysql -uroot -ppg -e "INSTALL PLUGIN clone SONAME 'mysql_clone.so'; SET GLOBAL clone_valid_donor_list='$N-src:3306'"
docker exec -i $N-dst mysql -uroot -ppg -e "CLONE INSTANCE FROM 'root'@'$N-src':3306 IDENTIFIED BY 'pg'"
docker start $N-dst
ERROR 3707 (HY000) at line 1: Restart server failed (mysqld is not managed by supervisor process).

Клон перезапускает получателя сам. В контейнере нет процесса, который поднял бы mysqld обратно, поэтому ошибка 3707 ожидаема, а контейнер остановился и его запускают вручную. Донорская сторона — цикл команд: клиент присылает COM_EXECUTE, сервер отдаёт данные движку:

      Server_Cbk clone_callback(this);

      err = hton_clone_copy(get_thd(), get_storage_vector(), m_tasks,
                            &clone_callback);

Это ветка COM_EXECUTE в Server::parse_command_buffer (clone_server.cc, строки 263–266); файлы и redo отправляет InnoDB через hton_clone_copy. Результат на получателе после запуска:

docker exec -i $N-dst mysql -uroot -ppg -t -e "select @@gtid_executed, count(*) from lab.t; select STATE, BEGIN_TIME, END_TIME from performance_schema.clone_status; select STAGE, STATE, BEGIN_TIME, END_TIME, ESTIMATE, DATA, NETWORK from performance_schema.clone_progress"
+----------------------------------------------+----------+
| @@gtid_executed                              | count(*) |
+----------------------------------------------+----------+
| 160aa11d-d267-11f1-87f6-5ed31fb96cb1:1-90542 |  5333421 |
+----------------------------------------------+----------+
+-----------+-------------------------+-------------------------+
| STATE     | BEGIN_TIME              | END_TIME                |
+-----------+-------------------------+-------------------------+
| Completed | 2026-10-28 00:39:14.874 | 2026-10-28 00:39:38.594 |
+-----------+-------------------------+-------------------------+
+-----------+-----------+----------------------------+----------------------------+------------+------------+------------+
| STAGE     | STATE     | BEGIN_TIME                 | END_TIME                   | ESTIMATE   | DATA       | NETWORK    |
+-----------+-----------+----------------------------+----------------------------+------------+------------+------------+
| DROP DATA | Completed | 2026-10-28 00:39:14.945827 | 2026-10-28 00:39:15.008639 |          0 |          0 |          0 |
| FILE COPY | Completed | 2026-10-28 00:39:15.008705 | 2026-10-28 00:39:16.358907 | 1014208074 | 1014208074 | 1014270904 |
| PAGE COPY | Completed | 2026-10-28 00:39:16.358991 | 2026-10-28 00:39:16.390549 |          0 |          0 |         99 |
| REDO COPY | Completed | 2026-10-28 00:39:16.390672 | 2026-10-28 00:39:16.425969 |      10752 |      10752 |      11149 |
| FILE SYNC | Completed | 2026-10-28 00:39:16.426064 | 2026-10-28 00:39:16.492195 |          0 |          0 |          0 |
| RESTART   | Completed | 2026-10-28 00:39:16.492195 | 2026-10-28 00:39:37.868034 |          0 |          0 |          0 |
| RECOVERY  | Completed | 2026-10-28 00:39:37.868034 | 2026-10-28 00:39:38.593603 |          0 |          0 |          0 |
+-----------+-----------+----------------------------+----------------------------+------------+------------+------------+

Копирование 1 014 208 074 байт (FILE COPY) заняло 1,35 с, весь перенос до FILE SYNC занял 1,6 с, а RECOVERY после запуска — 0,7 с. Стадия RESTART в 21 с — это время, пока контейнер ждал ручного docker start, а не работа клона. Данные те же, что на доноре: gtid_executed 1-90542 и 5 333 421 строка. Условия не сопоставимы с XtraBackup: клон шёл без нагрузки, а бэкап — под 16 писателями, поэтому сравнивать 1,6 с и 4,1 с как скорость двух инструментов нельзя.

Что из этого следует

  • Дамп годится, пока схема неподвижна. ALTER по таблице, которую дамп уже читает, ждёт блокировку; по таблице, до которой дамп ещё не дошёл, проходит, и дамп падает с кодом 3 и неполным файлом. На время дампа DDL запрещают, а код возврата проверяют.
  • Копия XtraBackup без --prepare не копия. В ней страницы разных моментов, backup_type = full-backuped; согласованной она становится после применения redo до last_lsn. Проверка готовности — full-prepared в xtrabackup_checkpoints.
  • Координата GTID из бэкапа — точка пересборки реплики. Восстановленный сервер сразу имеет gtid_executed 1-11981 и ровно столько строк; дальше им пользуются так же, как в статье про репликацию и GTID, но догонку источника этот опыт не проверял.
  • Инкремент экономит место, а не время. 29 МиБ против 921 МиБ, но таблица читалась целиком (6,3 с против 6,1 с). Для PostgreSQL та же задача решается иначе, как в базовом бэкапе с архивом WAL.
  • CLONE заменяет данные получателя целиком. Без супервизора его перезапуск превращается в ручной шаг, а clone_progress показывает, на какой стадии он остановился.

Чек-лист

  1. Если делаешь дамп, запрети DDL на время работы и проверяй код возврата mysqldump; ненулевой код означает неполный файл.
  2. После xtrabackup --backup не считай каталог бэкапом: выполни --prepare и проверь backup_type = full-prepared в xtrabackup_checkpoints.
  3. Сохраняй xtrabackup_binlog_info вместе с копией: это GTID, до которого копия догнана; версия XtraBackup не ниже версии сервера.
  4. Убери стенд: docker rm -f -v $N-src $N-dst $N-rst; docker volume rm $N-data $N-bk $N-restore; docker network rm $N-net.

Источники

Выжимки статей в Telegram: одна ловушка, код с выводом и ссылка на полный разбор. t.me/koddo_ru