Как реплики помогают распределить нагрузку
Что проверяют
Понимаете ли вы, что реплики масштабируют чтение, но не запись, и чем за это платите. Почти всегда следом спрашивают: пользователь сохранил профиль, обновил страницу и увидел старые данные, что произошло и как это чинить.
Как отвечать
Начните с разделения: реплики бывают у stateless-сервиса и у базы, и это разные истории.
- копии сервиса: несколько экземпляров Go-приложения за балансировщиком или
Serviceв Kubernetes. Состояния в памяти нет, запрос может уйти на любой экземпляр; - реплики базы: в PostgreSQL это потоковая репликация, primary принимает все записи и передаёт WAL на standby. Чтения можно разнести по standby, записи остаются на одном узле;
- маршрутизация в коде:
database/sqlсам ничего не разделяет, поэтому держат два пула*sql.DB, для primary и для реплик, и выбирают пул по типу запроса. Вpgxможно указать несколько хостов иtarget_session_attrs, в GORM для этого есть плагинdbresolver; - отставание: по умолчанию репликация асинхронная, реплика отстаёт на миллисекунды, а под нагрузкой на секунды. Смотреть его в
pg_stat_replication, полеreplay_lag; - read-your-writes: сразу после записи читать с primary. Варианты: закрепить пользователя за primary на несколько секунд после записи или сравнивать LSN (
pg_current_wal_lsn()на primary иpg_last_wal_replay_lsn()на реплике); - отказоустойчивость: реплика годится для переключения при падении primary, но переключает отдельный инструмент вроде Patroni, а не сам Postgres.
Итог: если упираетесь в запись, реплики не помогут, дальше шардирование или очереди.
Где спотыкаются
Говорят, что реплики «делят нагрузку поровну», хотя каждая запись проходит через primary и повторяется на каждом standby.
Забывают про отставание и отправляют чтение сразу после записи на реплику. В итоге код проверяет только что вставленную строку и не находит её.
Считают синхронную репликацию решением проблемы старых данных. При synchronous_commit = on коммит ждёт, пока WAL запишется на standby, но не пока изменения станут видны запросам: для этого нужен remote_apply. И в любом режиме задержка каждой записи растёт.