Пошук уроків, статей та іншого контенту
Дослідите архітектуру розподілених баз, кворуми, відмовостійкість і компроміси між масштабованістю та складністю.
Розподілена база даних зберігає дані на кількох мережевих вузлах, які спільно обслуговують запити. Для клієнта така система може виглядати як одна база, хоча фактично дані розміщені на різних серверах, у різних зонах доступності або навіть регіонах.
Основні причини використання розподілених баз:
горизонтальне масштабування;
висока доступність;
зменшення затримки завдяки розміщенню даних ближче до користувачів;
переживання відмов окремих серверів або зон;
збільшення обсягу доступного сховища.
Однак розподіл даних створює нові проблеми:
мережеві затримки;
часткові відмови;
конфлікти між копіями;
складнішу координацію;
тимчасову або постійну неузгодженість даних.
У локальній базі даних транзакція може покладатися на спільну пам’ять і локальний журнал. У розподіленій системі кожна операція проходить через мережу, а мережа може бути повільною, недоступною або розділеною на ізольовані частини.
Типова розподілена база складається з таких компонентів:
вузли — сервери, які зберігають дані та обробляють запити;
репліки — копії одного фрагмента даних на різних вузлах;
шардинг — поділ набору даних між вузлами;
маршрутизатор або координатор — визначає, до яких вузлів надсилати запит;
механізм консенсусу або координації — узгоджує стан вузлів, коли це необхідно;
служба виявлення вузлів — допомагає визначити, які вузли доступні та кому належить певний фрагмент даних.
Не кожна система містить усі ці компоненти в окремому вигляді. Наприклад, роль координатора може виконувати будь-який вузол, до якого звернувся клієнт.
Реплікація означає збереження кількох копій тих самих даних.
Нехай фактор реплікації дорівнює 3. Це означає, що кожен фрагмент даних зберігається на трьох вузлах:
Ключ user:42 → Node A, Node B, Node CЯкщо один вузол стає недоступним, система може продовжити роботу з іншою копією.
Під час синхронної реплікації операція вважається завершеною лише після підтвердження від визначеної кількості реплік.
Переваги:
менша ймовірність втрати підтвердженого запису;
сильніша узгодженість;
зрозуміліша поведінка після відмови.
Недоліки:
більша затримка;
тимчасова недоступність, якщо необхідні репліки не відповідають;
залежність від найповільнішої репліки.
Під час асинхронної реплікації основний вузол може підтвердити запис до того, як усі копії отримають зміни.
Переваги:
менша затримка запису;
вища доступність;
система може продовжувати роботу при проблемах із частиною реплік.
Недоліки:
вікно можливої втрати даних;
читання застарілої копії;
складніше відновлення після відмови;
можливі конфлікти під час одночасних записів.
Шардинг поділяє дані між вузлами. Кожен вузол відповідає лише за частину ключів.
Наприклад, дані можна розподілити за хешем ідентифікатора:
hash(user_id) % 4
0 → Shard 0
1 → Shard 1
2 → Shard 2
3 → Shard 3У реальних системах часто використовують узгоджене хешування або діапазони ключів. Це зменшує кількість даних, які потрібно переміщувати при додаванні вузла.
обсяг даних розподіляється між серверами;
запити до одного ключа можуть оброблятися незалежно;
можна масштабувати пропускну здатність запису та читання.
нерівномірний розподіл даних;
«гарячі» ключі, які отримують надто багато запитів;
складні запити між кількома шардами;
транзакції між шардами;
переміщення даних при зміні схеми розподілу.
Реплікація і шардинг вирішують різні задачі:
реплікація створює копії одного набору даних;
шардинг розподіляє різні частини даних між вузлами.
Їх часто комбінують: кожен шард має кілька реплік.
Кворум — це мінімальна кількість вузлів, які мають підтвердити операцію.
Позначимо:
N — кількість реплік фрагмента;
W — кількість підтверджень для успішного запису;
R — кількість реплік, з яких потрібно отримати відповідь для читання.
Наприклад, при N = 3:
W = 1 — запис достатньо підтвердити одному вузлу;
W = 2 — потрібні дві репліки;
R = 2 — читання звертається до двох реплік.
Якщо виконується умова:
W + R > Nто множини реплік запису та читання гарантовано перетинаються. Це може допомогти отримати актуальніше значення, якщо система порівнює версії та правильно обробляє конфлікти.
Наприклад:
N = 3
W = 2
R = 2
W + R = 4 > 3Тоді при записі щонайменше дві репліки отримують нове значення, а при читанні перевіряються щонайменше дві репліки. Принаймні одна з них має належати до множини реплік, які підтвердили запис.
Важливо: сама нерівність W + R > N не гарантує повної лінійної узгодженості. Для цього потрібні додаткові умови:
коректне визначення порядку версій;
обробка одночасних записів;
відсутність помилкових відповідей від вузлів;
належне відновлення відсталих реплік;
узгоджені правила вибору переможця конфлікту.
Наведена програма моделює репліки, записи з кворумом і читання з перевіркою версій.
from dataclasses import dataclass
from typing import Dict, List, Optional
@dataclass
class VersionedValue:
version: int
value: str
class QuorumStore:
def __init__(self, node_names: List[str], write_quorum: int, read_quorum: int):
if not node_names:
raise ValueError("Потрібен хоча б один вузол")
if not 1 <= write_quorum <= len(node_names):
raise ValueError("Некоректний write quorum")
if not 1 <= read_quorum <= len(node_names):
raise ValueError("Некоректний read quorum")
self.nodes: Dict[str, Dict[str, VersionedValue]] = {
name: {} for name in node_names
}
self.available = {name: True for name in node_names}
self.write_quorum = write_quorum
self.read_quorum = read_quorum
self.next_version = 0
def set_node_availability(self, node_name: str, available: bool) -> None:
if node_name not in self.nodes:
raise KeyError(f"Невідомий вузол: {node_name}")
self.available[node_name] = available
def write(self, key: str, value: str) -> bool:
self.next_version += 1
item = VersionedValue(self.next_version, value)
acknowledgements = 0
for node_name, data in self.nodes.items():
if self.available[node_name]:
data[key] = item
acknowledgements += 1
if acknowledgements < self.write_quorum:
# У спрощеній моделі відкат не імітується.
# У реальній системі потрібні журнали та відновлення.
return False
return True
def read(self, key: str) -> Optional[str]:
responses = []
for node_name, data in self.nodes.items():
if self.available[node_name] and key in data:
responses.append(data[key])
if len(responses) == self.read_quorum:
break
if len(responses) < self.read_quorum:
raise RuntimeError("Недостатньо доступних реплік для читання")
newest = max(responses, key=lambda item: item.version)
return newest.value
def print_state(self, key: str) -> None:
for node_name, data in self.nodes.items():
item = data.get(key)
state = "недоступний" if not self.available[node_name] else "доступний"
if item is None:
value = "немає даних"
else:
value = f"{item.value!r}, версія {item.version}"
print(f"{node_name}: {state}, {value}")
store = QuorumStore(
node_names=["node-a", "node-b", "node-c"],
write_quorum=2,
read_quorum=2,
)
print("Запис v1:", store.write("order:100", "created"))
store.print_state("order:100")
print("Читання:", store.read("order:100"))
store.set_node_availability("node-c", False)
print("\nПісля відмови node-c:")
print("Запис v2:", store.write("order:100", "paid"))
print("Читання:", store.read("order:100"))
store.print_state("order:100")
store.set_node_availability("node-b", False)
print("\nПісля відмови node-b:")
try:
print("Читання:", store.read("order:100"))
except RuntimeError as error:
print("Помилка:", error)У цій конфігурації N = 3, W = 2, R = 2. Поки доступні щонайменше дві репліки, читання та запис можуть завершуватися. Після втрати двох вузлів кворум недоступний.
Це навчальна модель. Вона не реалізує мережеві затримки, конкурентні записи, журнал транзакцій, відновлення реплік або справжній консенсус.
У розподіленій системі потрібно визначити, що саме означає «актуальне значення».
Після успішного запису наступне читання повертає цей запис або пізніший стан.
Сильна узгодженість спрощує логіку клієнта, але часто вимагає:
синхронної координації;
очікування відповіді від кворуму;
відмови від операції при недостатній кількості доступних вузлів.
Цей підхід підходить для даних, де застаріле читання неприпустиме: балансів, стану платежу або блокувань ресурсів.
За тимчасової відсутності нових записів усі репліки зрештою мають прийти до одного стану.
Між записом і синхронізацією реплік клієнт може отримати старе значення. Це часто прийнятно для:
лічильників переглядів;
кешованих профілів;
рекомендацій;
статусів, які можуть оновлюватися повторно.
Eventual consistency не означає хаотичну поведінку. Система все одно має визначати:
як передаються зміни;
як визначається порядок версій;
як вирішуються конфлікти;
коли репліка вважається синхронізованою.
Під час читання система може порівняти версії кількох реплік і виправити відсталі копії.
Наприклад:
Node A: version 8
Node B: version 8
Node C: version 7Після читання значення версії 8 система може записати його на Node C.
Read repair допомагає поступово вирівнювати репліки, але не замінює повноцінний механізм відновлення.
Розподілена система має враховувати не лише повну відмову сервера.
Можливі сценарії:
вузол працює, але дуже повільно;
мережевий канал між двома вузлами недоступний;
вузол бачить лише частину кластера;
відповідь загубилася, хоча операція фактично виконалася;
вузол повертає старі або пошкоджені дані;
координатор недоступний, хоча самі дані збережені.
Це називається частковою відмовою. Вона складніша за локальну помилку, оскільки система не завжди може відразу відрізнити повільний вузол від непрацюючого.
Кожен розподілений запит повинен мати тайм-аут. Без нього один недоступний вузол може утримувати ресурси необмежено довго.
Але тайм-аут не доводить, що операція не виконалася. Наприклад:
клієнт відправив запис;
вузол зберіг дані;
відповідь загубилася;
клієнт отримав тайм-аут.
Повторний запис може бути безпечним лише тоді, коли операція ідемпотентна або має унікальний ідентифікатор.
CAP-теорема розглядає три властивості:
Consistency — узгоджене бачення даних;
Availability — кожен запит до доступного вузла отримує відповідь;
Partition tolerance — система продовжує працювати після мережевого розділення.
Під час мережевого розділення неможливо одночасно гарантувати і сильну узгодженість, і повну доступність. Архітектура має обрати компроміс:
зупинити частину операцій, щоб зберегти узгодженість;
продовжити приймати операції, допускаючи тимчасові розбіжності.
На практиці P не є опцією: мережеві розділення можуть траплятися. Тому головне практичне питання — яку властивість послабити під час partition.
Це не означає, що будь-яка база є просто «CP» або «AP» для всіх операцій. Одна система може мати різні режими читання, записи та транзакцій.
Якщо репліка була недоступною, після повернення вона може містити застарілі дані. Система повинна синхронізувати її з актуальним станом.
Поширені підходи:
пересилання журналу змін — репліка отримує пропущені операції;
read repair — відновлення під час читання;
повна перебудова репліки — повторне копіювання всього потрібного набору даних;
hinted handoff — тимчасове збереження змін на іншому вузлі, якщо цільова репліка недоступна.
Відновлення повинно враховувати:
обсяг пропущених змін;
порядок операцій;
видалення;
конфлікти версій;
навантаження на кластер.
Видалення особливо складні. Якщо просто видалити ключ на одній репліці, стара копія може згодом «воскресити» його. Тому розподілені системи часто зберігають спеціальний маркер видалення з версією або часовою міткою.
Операція над одним шардом зазвичай простіша, ніж операція над кількома.
Наприклад, створення замовлення і резервування товару можуть розташовуватися на різних шардах. Якщо одна частина операції успішна, а інша — ні, система може залишитися у проміжному стані.
Для таких випадків використовують:
розподілені транзакції;
протокол двофазного коміту;
саги з компенсувальними операціями;
проєктування даних так, щоб пов’язана операція потрапляла в один шард;
асинхронну обробку через події.
Розподілені транзакції дають сильніші гарантії, але збільшують кількість мережевих обмінів і можуть блокувати ресурси. Тому їх не варто застосовувати за замовчуванням для кожної бізнес-операції.
Перед проєктуванням потрібно визначити вимоги до кожного типу даних:
Чи можна читати застаріле значення?
Чи можна тимчасово не приймати записи?
Яку втрату даних допускає система?
Яка максимальна затримка запису?
Скільки вузлів або зон може відмовити?
Чи потрібні транзакції між різними фрагментами даних?
Як система розв’язуватиме конфлікти?
Чи можна повторити операцію без побічних ефектів?
Після цього визначають:
фактор реплікації;
політику читання і запису;
ключ шардингу;
розміщення реплік по зонах доступності;
правила вибору лідера або кворуму;
процедури відновлення;
метрики та сповіщення.
Масштабованість не безкоштовна. Додавання вузлів може збільшити пропускну здатність, але також додає мережеві обміни, складніші відмови, дорожче тестування і складніше розгортання.
Реплікація зазвичай поширює і помилкові записи, і випадкові видалення. Для відновлення після логічної помилки потрібні резервні копії та перевірка їх відновлення.
Єдиний координатор може стати вузьким місцем або єдиною точкою відмови. Координацію потрібно або розподіляти, або забезпечити коректне перемикання.
Навіть за рівномірного хешування один популярний ключ може перевантажити всі його репліки. Потрібні обмеження, кешування, розділення навантаження або інша модель ключів.
Тайм-аут означає лише, що клієнт не отримав відповідь вчасно. Запис міг бути застосований.
W + R > N повною гарантією узгодженостіКворум визначає перетин наборів реплік, але не вирішує автоматично проблеми версій, конфліктів, помилкових відповідей і відновлення.
Тестування лише відмови процесу не показує поведінку системи при затримках, втраті пакетів і різних уявленнях вузлів про доступність кластера.
Розподілена база зберігає дані на кількох мережевих вузлах.
Реплікація підвищує доступність, а шардинг розподіляє обсяг і навантаження.
Кворуми задаються параметрами N, W і R.
Умова W + R > N забезпечує перетин реплік, але сама по собі не гарантує лінійну узгодженість.
Сильна узгодженість зазвичай потребує більшої координації та може зменшити доступність під час partition.
Eventual consistency підвищує доступність, але вимагає роботи із застарілими даними та конфліктами.
Часткові відмови, тайм-аути й відновлення реплік є центральними проблемами розподіленої архітектури.
Вибір між масштабованістю, доступністю, затримкою та узгодженістю залежить від вимог конкретних даних і операцій.