5 вопросов
Репликация - копирование данных с primary на replicas. Цели: отказоустойчивость, масштабирование чтения. Запись - на primary; чтение можно распределять по репликам. В Go настраивают два DSN (или пулы): один для записи (primary), один или несколько для чтения. Роутинг запросов: только чтение - на реплику, запись и чтение в транзакции - на primary. Библиотеки: manual два *sql.DB или middleware с выбором по контексту.
func (db *DB) QueryContext(ctx context.Context, query string, args ...interface{}) (*sql.Rows, error) {
if readOnly(ctx) { return db.replica.QueryContext(ctx, query, args...) }
return db.primary.QueryContext(ctx, query, args...)
}Синхронная: primary ждет подтверждения от реплики перед коммитом; при падении primary данные есть на реплике. Задержка записи выше. Асинхронная: primary не ждет; реплика догоняет. Меньше задержка записи, но при падении primary возможна потеря последних данных. В Go при асинхронной репликации чтение после записи может не увидеть только что записанное (read-your-writes) - решают чтением с primary после записи или задержкой.
Партиционирование - логическая таблица разбита на партиции внутри одной СУБД. Шардирование - данные на разных серверах (разные инстансы БД). В Go при шардировании приложение выбирает шард по ключу (user_id, hash(id) % N) и обращается к нужному *sql.DB. Транзакции и JOIN между шардами в приложении не поддерживаются - делают в коде или избегают. Партиционирование прозрачно для приложения - один DSN.
shard := db.shards[userID%len(db.shards)]
return shard.QueryContext(ctx, "SELECT ... WHERE user_id = $1", userID)Consistent hashing распределяет ключи по узлам так, что при добавлении/удалении узла перераспределяется минимум ключей. Обычный hash(key) % N при изменении N инвалидирует почти все. Ключи и узлы на кольце; ключ принадлежит следующему узлу по часовой стрелке. В Go используют для выбора шарда/реплики при динамическом наборе узлов (например, кеш, очереди). Реализации: github.com/golang/groupcache, кастом по кольцу.
type Ring struct { nodes []uint32 }
func (r *Ring) Get(key string) string { h := hash(key); ... return nodeFor(h) }Split-brain - после разрыва связи оба узла считают себя primary и принимают записи; при восстановлении связи данные расходятся. Предотвращение: quorum, единственный arbiter, fencing (отключение старого primary при смене). В Go приложение должно подключаться только к текущему primary (через discovery, etcd, Consul или ручной конфиг). При failover переключать DSN на нового primary; идемпотентность и повторные запросы помогают при краткой неоднозначности.