Читай реальный конфиг репликации, выбор ключа партиционирования, функцию маршрутизации и настройку кворума, затем рассуждай: найди риск потери записи, горячий шард, ловушку mod-N решардинга и гарантирует ли кворум свежее чтение.
SDSenior◷ 14 min
Уровень
ОсновыJuniorMiddleSenior
Баги распределения живут в конфиге и коде маршрутизации: режим репликации, ключ партиционирования, модуль, число кворума. Читай каждый сниппет, проследи режим отказа, который он подразумевает, и выбери ответ, на который senior-инженер пойдёт на ревью.
Отработай цикл, что гоняешь на ревью дизайна или инцидента: найди решение распределения в конфиге или коде, проследи его режим отказа и выбери изменение, которое механизм реально поддерживает.
Сниппет 1 — конфиг репликации
# репликация базыreplication: mode: async # лидер подтверждает до получения записи фолловерами followers: 3 sync_replicas: 0 # ни один не обязан подтвердить# при отказе лидера: автоповышение самого догнавшего фолловера
Викторина
Completed
Какой риск долговечности несёт этот конфиг и какое минимальное изменение?
Heads-up Повышение самого догнавшего фолловера минимизирует потерю, но не может восстановить записи, дошедшие лишь до мёртвого лидера. При sync_replicas: 0 эти подтверждённые записи пропали. Нужна синхронная реплика, чтобы сам ack подразумевал долговечность.
Heads-up Больше фолловеров масштабируют чтения, но не делают ack долговечным — они все async здесь. Риск — ПОТЕРЯ ЗАПИСЕЙ при failover, что чинится требованием синхронной реплики, а не добавлением async.
Heads-up Все-синхронные связывают доступность записи с самым медленным из 3 фолловеров — одна медленная реплика тормозит каждую запись. Минимальная безопасная починка — sync_replicas: 1 (полусинхрон): долговечно на двух узлах, не держа записи в заложниках у всех трёх.
Сниппет 2 — ключ партиционирования
# таблица events шардирована; выбери ключ шардаdef shard_for(event): return hash(event.created_at_day) % NUM_SHARDS # шард по дню# самый горячий запрос: SELECT ... WHERE tenant_id = ? AND created_at_day = ?
Викторина
Completed
Что не так с шардированием events по created_at_day при такой нагрузке?
Heads-up Хеширование дня размазывает РАЗНЫЕ дни по шардам, но все записи одного дня делят ключ, так что сегодняшние записи все бьют в один шард — горячая точка записи. И запросу нужен tenant_id, по которому ключ-день не маршрутизирует.
Heads-up Простой модуль нерелевантен этому багу. Проблема в ВЫБОРЕ ключа (день) против паттерна доступа (tenant + день): сегодняшние записи концентрируются и горячий запрос нельзя маршрутизировать одношардово.
Heads-up Вторичный индекс на не-ключе шардинга локален на шард (scatter-gather для опроса) или отдельный глобальный индекс — он не чинит горячую точку записи от ключа-дня. Перевод ключа на tenant_id адресует и размазывание, и запрос.
Сниппет 3 — функция маршрутизации
# клиент кеша выбирает узел для каждого ключаdef node_for(key, nodes): return nodes[hash(key) % len(nodes)] # nodes — живой автомасштабируемый список# автоскейлер добавляет/убирает узлы кеша по нагрузке
Викторина
Completed
Флот кеша автомасштабируется, так что len(nodes) меняется в рантайме. Что вызывает эта маршрутизация и какая починка?
Heads-up Нет: делитель len(nodes) меняется при каждом событии скейла, так что hash(key) % len(nodes) переназначает почти каждый ключ. Это проблема «перехешировать всё»; mod-N небезопасен для живого меняющегося набора узлов.
Heads-up Хеш в порядке; проблема — зависимость mod-N от len(nodes), что переотображает всё при смене числа. Лучший хеш не поможет — consistent hashing (развязка маршрутизации и N) поможет.
Heads-up Хардкод числа ломает маршрутизацию в тот момент, когда реальный набор узлов отличается от константы (ключи маршрутизируются на узлы, которых может не быть). Верная починка — consistent hashing, обрабатывающий меняющийся набор узлов by design.
Сниппет 4 — настройка кворума
# leaderless key-value хранилищеN = 5 # реплик на ключW = 2 # узлов должны подтвердить записьR = 2 # узлов должны ответить на чтение# требование продукта: чтение всегда должно видеть последнюю подтверждённую запись
Викторина
Completed
Удовлетворяет ли W = 2, R = 2 при N = 5 требованию, и если нет — какая минимальная верная настройка?
Heads-up Лишь кворумы с R + W > N гарантируют пересечение. Здесь R + W = 4 ≤ 5, так что двухузловое множество чтения может вовсе разминуться с двухузловым множеством записи; чтение может вернуть устаревшие данные. Надо поднять сумму выше N.
Heads-up W = 5 гарантирует свежие чтения (любой R пересекается), но требует все реплики на запись — наименьшая доступность записи, больше нужного. R + W > 5 достаточно; W = 3, R = 3 удовлетворяет с лучшей доступностью записи.
Heads-up Снижение R даёт R + W = 3, всё ещё ≤ 5 и всё ещё без пересечения — чтения становятся быстрее И менее корректными. Чтобы гарантировать свежесть, надо увеличить сумму выше N, например W = 3, R = 3.
Вспомните перед уходом
01
Как читать риск долговечности по конфигу репликации?
02
Почему hash(key) % len(nodes) небезопасен для автомасштабируемого набора узлов и что его заменяет?
03
Имея N, W, R, как понять, гарантированно ли свежи чтения, и какова минимальная починка, когда нет?
Итог
Каждое решение распределения в этом юните — то, что можно прочитать прямо из конфига или кода маршрутизации. Конфиг репликации с mode: async и sync_replicas: 0 подтверждает до того, как у любого фолловера есть запись, так что failover теряет подтверждённые записи — починка — одна синхронная реплика. Ключ партиционирования, выбранный против нагрузки (шардинг events по дню, когда горячий запрос фильтрует по tenant), создаёт горячую точку записи и немаршрутизируемый запрос — переведи ключ под паттерн доступа. Функция маршрутизацииhash(key) % len(nodes) над автомасштабируемым набором узлов переотображает почти все ключи при каждом событии скейла — замени её кольцом consistent hashing, чтобы двигалось лишь ~1/N. А кворум гарантирует свежее чтение лишь когда R + W > N — W = 2, R = 2, N = 5 (сумма 4) нет, а W = 3, R = 3 (сумма 6) да. Senior-привычка — найти решение распределения в конфиге, проследить его режим отказа и выбрать починку, которую механизм поддерживает, — а не ту, что его прячет.
Что-то непонятно?
Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.