Контекст инцидента

Пятница, 22:30. На вторичной реплике MySQL 5.7 график Seconds_Behind_Master внезапно улетает в стратосферу — 14 000 секунд и продолжает расти. Read-only сервисы начинают отдавать устаревшие данные профилей, кэширующие слои прогреваются до критических температур, а мониторинг заливается алертами.

Диагностика и снятие дампов

Первым делом снимаем состояние репликации и треды:

BASH
mysql -e "SHOW SLAVE STATUS\G"
mysql -e "SHOW FULL PROCESSLIST;"

В выводе SHOW SLAVE STATUS:

  • Slave_IO_Running: Yes
  • Slave_SQL_Running: Yes
  • Seconds_Behind_Master: 14210
  • Last_Error: ""

SQL-тред не упал с ошибкой, он просто намертво завис в статусе:

TEXT
System lock | UPDATE account_balances SET last_audit_ts = NOW() WHERE audit_batch = 4821;

Разработчики запустили ночной фоновый джоб без разбивки на чанки. Один транзакционный пакет изменил 2.4 миллиона строк. На мастере стояли быстрые NVMe в RAID10 и многопоточная запись, поэтому мастер проглотил транзакцию за 18 секунд. Но реплика выполняла её строго в один поток (в MySQL 5.7 schema-based multi-threaded replication не распараллеливает транзакции внутри одной базы).

Смотрим дисковую подсистему через iostat -xz 1:

TEXT
Device  r/s  w/s   rMB/s  wMB/s  await  r_await  w_await  %util
nvme0n1 12   450   0.1    85.2   48.2   1.2      49.8     99.4%

Утилизация диска упёрлась в 100%, при этом w_await скакнул до 50мс.

Ловушка параметров sync_binlog и doublewrite

На реплике были включены:

INI
sync_binlog = 1
innodb_flush_log_at_trx_commit = 1
innodb_doublewrite = 1

При попытке сбросить гигантский объем грязных страниц (dirty pages) InnoDB упёрся в сброс буфера doublewrite и бесконечные системные вызовы fsync() на каждый флаш binlog-файла реплики (log_slave_updates был включен для каскадной репликации).

Снимаем стектрейс зависшего потока через gdb -p $(pidof mysqld):

TEXT
#0  0x00007f9c8d10b70b in fsync () from /lib/x86_64-linux-gnu/libc.so.6
#1  0x000055c1e2891f1a in os_file_flush_func ()
#2  0x000055c1e29e012c in fil_flush ()
#3  0x000055c1e27a988d in buf_dblwr_flush_buffered_writes ()

Как погасили пожар

1. На время догона реплики временно снизили строгость записи:

SQL
SET GLOBAL sync_binlog = 0;
SET GLOBAL innodb_flush_log_at_trx_commit = 2;

2. Разблокировали I/O троттлинг:

SQL
SET GLOBAL innodb_io_capacity = 4000;
SET GLOBAL innodb_io_capacity_max = 8000;

Реплика переварила накопленный буфер за 24 минуты и догнала мастер.

Системные выводы для продакшена

  • Пакетная обработка: любые миграции и массовые обновления обязаны быть разбиты на чанки по 2 000 — 5 000 строк с явным sleep(0.05).
  • MySQL 8.0 и Writeset Replication: переход на binlog_transaction_dependency_tracking = WRITESET (сняло ограничение однопоточной репликации по схемам).
  • Репликам без каскада строго отключать log_slave_updates.