5. Репликация в MySQL
Преодолевая пределы одного сервера
В предыдущей главе (4. Транзакции в MySQL) мы детально исследовали, как движок InnoDB гарантирует свойства ACID и управляет сложнейшими блокировками на уровне одного физического узла. Однако в реальной жизни любой одиночный сервер базы данных — даже вооруженный десятками терабайт NVMe и терабайтами RAM в Buffer Pool — быстро упирается в непреложные законы физики: лимиты тактовой частоты CPU, пропускную способность шины памяти и потолок дисковых IOPS.
Хуже того, одиночный сервер — это классическая единичная точка отказа (SPOF — Single Point of Failure). Сгоревшая материнская плата, обесточенная серверная стойка или авария в стойке ЦОД способны мгновенно парализовать весь бизнес.
Для преодоления аппаратных ограничений и построения надежных высоконагруженных систем применяется Репликация (Replication) — механизм непрерывного, детерминированного копирования потока изменений с основного узла (Master / Source) на один или несколько подчиненных серверов (Replica / Replica Nodes). В экосистеме MySQL репликация исторически является фундаментальным фундаментом масштабирования операций чтения (Read Scaling), создания резервных копий и обеспечения катастрофоустойчивости (Disaster Recovery).
Бинарный лог (Binlog): Источник истины
Прежде чем данные полетят по сетевому кабелю между дата-центрами, нам нужно ответить на фундаментальный вопрос: в каком именно виде изменения фиксируются на диске для передачи?
Чтобы активировать репликацию, на мастере должен быть включен флаг log_bin. Любая транзакция, модифицирующая данные (INSERT, UPDATE, DELETE, ALTER), в момент коммита атомарно регистрируется в файлах бинарного журнала.
Форматы Binlog: Эволюция и компромиссы
- Statement-Based Replication (SBR):
В лог записывается исходный текст выполненного SQL-запроса (например:UPDATE users SET status = 1 WHERE age > 18).- Плюс: Предельная компактность лога. Сетевой трафик и дисковые операции минимальны: даже если запрос обновил миллион строк, в лог пишется одна короткая строчка.
- Минус: Проблема недетерминированности. Если SQL-запрос содержит недетерминированные функции вроде
NOW(),UUID(),RAND()или подзапросы без строгогоORDER BY, то результат выполнения на мастере и реплике неизбежно разойдется. В данных возникнет тихий, катастрофический рассинхрон.
- Row-Based Replication (RBR):
В лог записывается не SQL-запрос, а физические образы затронутых строк (Before-Image и After-Image).- Плюс: 100% математическая гарантия идентичности данных между репликами. Никакие системные функции или условия гонки не могут вызвать рассинхрон.
- Минус: Рост объема лога (Write Amplification). Если один запрос обновил 500 000 записей, в Binlog будет записано 500 000 полных образов строк, что создаст всплеск нагрузки на сеть и дисковый ввод-вывод.
- Mixed (Смешанный режим):
MySQL по умолчанию использует Statement-формат, но если парсер встречает недетерминированную конструкцию (вызовы функцийUUID(), использование триггеров или временных таблиц), он автоматически переключает текущую запись в Row-формат.
Архитектура и потоки репликации: Механика под капотом
Классическая репликация MySQL опирается на модель Publish/Subscribe (Push/Pull) и реализуется согласованной работой трех системных потоков операционной системы:
flowchart LR
subgraph Master ["Master Node (Source)"]
Binlog["Binlog<br>(Файл на диске)"]
DumpThread["Binlog Dump Thread<br>(Ядерный поток OS)"]
Binlog -->|"Чтение событий"| DumpThread
end
subgraph Replica ["Replica Node (Target)"]
IOThread["IO Thread (Receiver)"]
RelayLog["Relay Log<br>(Файл на диске)"]
SQLThread["Applier Coordinator<br>(SQL Thread)"]
Workers["Worker Threads<br>(MTS / Parallel Applier)"]
Data["InnoDB Buffer Pool & Data"]
IOThread -->|"Последовательная запись"| RelayLog
RelayLog -->|"Чтение событий"| SQLThread
SQLThread -->|"Диспетчеризация"| Workers
Workers -->|"Параллельное исполнение"| Data
end
DumpThread -->|"Передача по TCP-сокету"| IOThread
style Master fill:#0f172a,stroke:#38bdf8,stroke-width:2px,color:#f8fafc
style Replica fill:#0f172a,stroke:#34d399,stroke-width:2px,color:#f8fafc
style Binlog fill:#1e293b,stroke:#818cf8,stroke-width:1px,color:#f8fafc
style RelayLog fill:#1e293b,stroke:#818cf8,stroke-width:1px,color:#f8fafc
style Data fill:#1e293b,stroke:#f87171,stroke-width:1px,color:#f8fafc
- Binlog Dump Thread (на стороне Master):
Когда реплика устанавливает сетевое соединение с мастером, процессmysqldна мастере выделяет для нее персональный поток операционной системы. Этот поток открывает файлы Binlog, читает бинарные события и пересылает их по TCP-соединению. Если реплика отстает от мастера, поток непрерывно читает старые архивы логов; если реплика догнала мастер, поток переходит в спящий режим ожидания (wait) на системном условном мьютексе и мгновенно просыпается при записи новых данных в Binlog. - IO Thread / Receiver Thread (на стороне Replica):
Фоновый поток реплики принимает сетевые пакеты от мастера через TCP-сокет и сохраняет их в промежуточные локальные файлы — Relay Log (Журнал ретрансляции). Это быстрая операция последовательной записи на накопитель (Sequential Disk Append), поэтому задержка на этапе приема сетевых пакетов обычно составляет доли миллисекунды. - SQL Thread / Applier Thread (на стороне Replica):
Этот поток последовательно читает записи из Relay Log, декодирует изменения строк и вызывает функции подсистемы хранения InnoDB, повторяя транзакции в локальных страницах данных.
Проблема отставания (Replication Lag) и многопоточный Applier (MTS)
Исторически ахиллесовой пятой репликации MySQL был тот факт, что SQL Thread на реплике работал в один-единственный поток.
Спасение через Mechanical Sympathy: MTS и Logical Clock
Чтобы устранить этот фундаментальный дисбаланс, в современных версиях MySQL появился механизм Multi-Threaded Slave / Multi-Threaded Applier (MTS):
- В MySQL 5.7+ и MySQL 8.0 одиночный SQL Thread трансформировался в Coordinator-поток, управляющий пулом воркеров (
slave_parallel_workersв 5.7 илиreplica_parallel_workersв 8.0).
Но как параллельно исполнять зависимые транзакции на реплике и не нарушить хронологический порядок? Если транзакция 1 создала пользователя, а транзакция 2 сразу же списала с него деньги, воркеры не имеют права поменять их местами!
MySQL элегантно решает эту задачу с помощью алгоритма Logical Clock и механизма Group Commit:
- На мастере транзакции группируются перед сбросом в Redo Log и Binlog.
- Если группа транзакций смогла одновременно закоммититься на мастере без взаимных блокировок, это служит математическим доказательством того, что эти транзакции не пересекаются по строкам.
- В заголовок каждой транзакции в Binlog записываются два логических счетчика:
sequence_number(номер транзакции) иlast_committed(номер коммита, с которым она не конфликтовала). - Координатор на реплике видит: все транзакции с одинаковым
last_committedполностью независимы! Он мгновенно распределяет их по свободным Worker-потокам, достигая десятков тысяч транзакций в секунду на реплике.
Режимы синхронизации: Архитектурные Trade-offs
В распределенных системах мы неизбежно сталкиваемся с компромиссом между скоростью отклика (Latency) и надежностью хранения (Durability).
sequenceDiagram
autonumber
participant App as Go Application
participant Master as Master Node
participant Replica as Replica Node
rect rgb(30, 41, 59)
note right of App: Асинхронная репликация (Async)
App->>Master: 1. COMMIT Transaction
Master->>Master: 2. Write Engine & Binlog
Master-->>App: 3. OK (Транзакция зафиксирована)
Master-)Replica: 4. Отправка Binlog по сети (в фоне)
end
rect rgb(15, 23, 42)
note right of App: Полусинхронная репликация (Semi-Sync AFTER_SYNC)
App->>Master: 1. COMMIT Transaction
Master->>Master: 2. Write Binlog & Flush Redo Log (Prepare)
Master->>Replica: 3. Отправка пакета Binlog по сети
Replica->>Replica: 4. Запись в Relay Log (fsync)
Replica-->>Master: 5. ACK (Подтверждение записи)
Master->>Master: 6. Commit в движке InnoDB
Master-->>App: 7. OK (Ответ клиенту)
end
1. Asynchronous Replication (Асинхронная, по умолчанию)
Мастер фиксирует транзакцию в движке и в локальном Binlog, немедленно возвращает OK клиенту и лишь затем в фоновом режиме пересылает события на реплику.
- Плюсы: Минимальная сетевая задержка. Скорость коммита на мастере абсолютно не зависит от задержек сети до реплик.
- Минусы: Риск потери данных при аварии (Data Loss). Если мастер сгорел через 1 миллисекунду после отправки ответа клиенту, а пакеты Binlog еще не покинули сетевой буфер, данные навсегда потеряны для мира.
2. Semi-Synchronous Replication (Полусинхронная)
Мастер ждет, пока хотя бы одна подключенная полусинхронная реплика не примет пакет данных и не запишет его в свой Relay Log.
- Плюсы: Нулевая потеря данных (RPO = 0). При падении мастера гарантируется, что хотя бы на одной реплике есть точная копия всех подтвержденных клиенту транзакций.
- Минусы: Задержка ответа клиенту на запись возрастает ровно на сетевой Round-Trip Time (RTT) до реплики плюс время вызова
fsyncRelay Log на стороне реплики.
GTID: Глобальные идентификаторы транзакций
Исторически репликация в MySQL настраивалась по координатам файлов: MASTER_LOG_FILE='mysql-bin.000142', MASTER_LOG_POS=48201. Это было кошмаром системных инженеров: при падении мастера было крайне сложно понять, на какой именно позиции в разных файлах остановились подчиненные реплики, чтобы переключить их на нового лидера.
Начиная с MySQL 5.6+, де-факто стандартом стал GTID (Global Transaction Identifier).
Каждая закоммиченная транзакция получает уникальный на весь мир идентификатор вида:
3E11FA47-71CA-11E1-9E33-C80AA9429562:23
(UUID инстанса мастера, породившего транзакцию, и монотонно возрастающий номер транзакции).
Благодаря GTID репликация настраивается буквально одной командой:
CHANGE REPLICATION SOURCE TO
SOURCE_HOST='master-host',
SOURCE_AUTO_POSITION=1;
Реплика сама сообщает мастеру список уже выполненных ею GTID-диапазонов (Retrieved_Gtid_Set), а мастер автоматически отсылает ей недостающие транзакции. Автоматический Failover (например, через Orchestrator или Patroni-подобные утилиты) с GTID стал простым и надежным.
Инженерия на Go: Паттерн Read/Write Split и Read-After-Write
В архитектуре Go-микросервисов масштабирование чтения реализуется разделением потоков запросов (Read/Write Splitting): все модифицирующие операции (INSERT, UPDATE, DELETE) уходят на Master, а тяжелые выборки (SELECT) балансируются по пулу реплик.
Однако асинхронная природа репликаций создает классическую проблему консистентности:
Идиоматичный пул с маршрутизацией запросов в Go
Для решения этой проблемы в бэкенд закладывают абстракцию кластера и применяют паттерн "Sticky Master" (Липкий мастер): если пользователь недавно внес изменения, его запросы на чтение временно (в течение 2–3 секунд) принудительно направляются на Master.
package database
import (
"context"
"database/sql"
"fmt"
"sync/atomic"
"time"
)
// DBCluster инкапсулирует мастер и пул доступных реплик для прозрачной маршрутизации.
type DBCluster struct {
Master *sql.DB
Replicas []*sql.DB
counter uint64
}
// NewDBCluster инициализирует отказоустойчивый кластерный клиент.
func NewDBCluster(master *sql.DB, replicas []*sql.DB) *DBCluster {
return &DBCluster{
Master: master,
Replicas: replicas,
}
}
// WriteDB возвращает пул соединений к Master-узлу для модифицирующих операций.
func (c *DBCluster) WriteDB() *sql.DB {
return c.Master
}
// ReadDB возвращает соединение к реплике по алгоритму Round-Robin.
// Если реплики недоступны, прозрачно деградирует (fallback) до чтения с Master.
func (c *DBCluster) ReadDB() *sql.DB {
if len(c.Replicas) == 0 {
return c.Master
}
idx := atomic.AddUint64(&c.counter, 1) % uint64(len(c.Replicas))
return c.Replicas[idx]
}
// QueryWithAffinity решает проблему Read-After-Write Consistency:
// если с момента последней записи пользователя прошло меньше stickyDuration,
// запрос на чтение принудительно направляется на Master.
func (c *DBCluster) QueryWithAffinity(
ctx context.Context,
lastWriteTime time.Time,
stickyDuration time.Duration,
query string,
args ...any,
) (*sql.Rows, error) {
db := c.ReadDB()
if time.Since(lastWriteTime) < stickyDuration {
// "Липкий мастер" гарантирует, что пользователь увидит свои собственные свежие изменения
db = c.WriteDB()
}
return db.QueryContext(ctx, query, args...)
}
Резюме
- Binlog (Server Layer) — фундамент репликации в MySQL. Для продакшена обязателен формат Row-Based Replication (ROW) в сочетании с глобальными идентификаторами транзакций (GTID).
- Репликация приводится в движение тремя системными потоками: Binlog Dump Thread (на мастере) отправляет байты по TCP, IO Thread (на реплике) сохраняет их в Relay Log, а SQL Thread накладывает изменения на локальный движок.
- Отставание реплик (Replication Lag) минимизируется многопоточным применением транзакций (MTS), где алгоритм Logical Clock безопасно распараллеливает независимые группы коммитов.
- Выбор между Asynchronous и Semi-Synchronous (AFTER_SYNC) репликацией — это фундаментальный архитектурный выбор между максимальным RPS и гарантией нулевой потери данных (RPO = 0).
- Приложение на Go должно осознанно управлять разделением соединений на Master и Replicas и предотвращать аномалии Read-After-Write с помощью паттернов вроде Sticky Master или передачи GTID-метки в заголовках.
Теперь, когда мы досконально изучили устройство транзакций, блокировок и репликации в MySQL, самое время сопоставить эти знания с другим титаном мира Open Source — PostgreSQL. В следующей лекции мы проведем глубокое архитектурное вскрытие и сравним их внутренние механизмы: 6. Отличия MySQL и PostgreSQL.