Часть VI: Крупные проекты — архитектура на уровне гигантов
Глава 25

Multi-Region и Global Architecture

#Когда одного региона недостаточно для планеты

Вы запустили сервис. Всё работает. Сервер в us-east-1, пользователи довольны, инвесторы счастливы. Вы масштабируетесь — добавляете реплики, кэши, CDN. Всё летает.

А потом вы смотрите в метрики и видите: пользователи из Европы ждут по 800 мс. Из Австралии — 2.5 секунды. Ваш конкурент открыл офис в Сингапуре, и азиатские пользователи уходят к нему просто потому, что их приложение открывается мгновенно.

Поздравляю, вы дошли до точки, когда одного региона (region) больше не хватает.

Multi-region архитектура — это не про «крутость» и не про «масштаб». Это про физику. Скорость света — 300 000 км/с. В оптоволокне — примерно 200 000 км/с. Сигнал из Нью-Йорка в Сидней идёт ~80 мс только в одну сторону. А если ваш сервер должен сходить в базу, проверить авторизацию, загрузить данные и вернуть ответ — прощайте, sub-second загрузка для половины планеты.

В этой главе мы разберём, как строить системы, которые работают быстро для пользователей по всему миру. Как не потерять данные при отказе целого региона. И главное — когда это вообще нужно, а когда можно ограничиться CDN.


#25.1 Активная или пассивная: две философии multi-region

Первое, что вы решаете — как именно ваши регионы будут работать. Тут есть два принципиально разных подхода.

#25.1.1 Active-Passive

Один регион — основной (active). Он принимает трафик, пишет данные, делает всю работу. Второй регион — пассивный (passive или standby). Он стоит, ждёт и реплицирует данные из основного.

Нормальная работа:
  Users → [Region A (active)] → Primary DB
          [Region B (passive)] → Replica DB (async replication)

Сбой Region A:
  Users → [Region B (promoted to active)] → Replica → Primary

Плюсы:

  • Простота. Одна точка записи — нет конфликтов данных.
  • Предсказуемое поведение. Нет split-brain, нет state, который расходится.
  • Значительно дешевле active-active (меньше инфраструктуры).

Минусы:

  • Пассивный регион простаивает. Вы платите за инфраструктуру, которая не приносит пользы 99.9% времени.
  • Переключение (failover) — не моментальное. DNS propagation, проверка целостности, promotion реплики. Реалистичный RTO (Recovery Time Objective) — от минут до часов.
  • Read latency для удалённых пользователей не улучшается. Если ваш active регион в Виргинии, пользователь в Токио всё равно ждёт round-trip до Америки.

Вариация: Active-Passive с read replicas

Можно сделать пассивный регион «читающим» — отдавать из него read-only трафик для локальных пользователей, но запись по-прежнему гонять в основной регион.

Users in Asia → [Region B (replica)] → read from local replica
                                     → write to Region A (async)

Это компромисс: read latency падает, write latency остаётся высокой. И надо быть готовым к тому, что данные в регионе B могут быть не консистентны на несколько секунд (replication lag). Для соцсети с лентой новостей — ок. Для банковского перевода — нет.

#25.1.2 Active-Active

Оба региона активны. Каждый принимает трафик, каждый пишет данные. Пользователь из Европы идёт в европейский регион, из Азии — в азиатский.

Users in EU → [Region A] → Local DB ← replication →
Users in AS → [Region B] → Local DB ← replication →

Плюсы:

  • Низкая задержка для всех пользователей. Каждый регион обслуживает «своих».
  • Полная утилизация ресурсов. Нет простаивающих мощностей.
  • Быстрое восстановление при отказе региона — просто перенаправляем трафик на живой.

Минусы:

  • Конфликты записи. Если пользователь изменил данные в Европе, а через секунду другой пользователь изменил те же данные в Азии — чьи данные правильные? Вы только что открыли ящик Пандоры с conflict resolution (разрешением конфликтов).
  • Значительно сложнее. Репликация данных между регионами — это не просто «сделай реплику». Это синхронизация, обнаружение конфликтов, их разрешение.
  • Дороже. Нужна полноценная инфраструктура в каждом регионе.
  • Сетевые задержки между регионами. Даже синхронная репликация между Нью-Йорком и Лондоном — это ~20 мс round-trip. Каждая запись будет ждать подтверждения из другого региона.

#Decision Matrix: когда что выбирать

Критерий Active-Passive Active-Passive + Read Replicas Active-Active
Write latency для удалённых пользователей 🔴 Высокая 🔴 Высокая 🟢 Низкая
Read latency для удалённых пользователей 🔴 Высокая 🟢 Низкая 🟢 Низкая
RTO при отказе региона 🟡 Минуты-часы 🟡 Минуты-часы 🟢 Секунды
Сложность 🟢 Низкая 🟡 Средняя 🔴 Высокая
Data consistency 🟢 Strong 🟡 Eventual (для реплик) 🟡 Eventual / Custom
Стоимость 🟡 Средняя 🟡 Средняя 🔴 Высокая
Когда выбирать Disaster recovery, compliance Улучшение read latency Глобальный сервис, низкая latency

Мой совет: начинайте с active-passive. Active-active — это уровень Google, Netflix и Slack после лет эволюции. Если вы дочитали до этой главы и думаете «нам нужно active-active» — убедитесь, что вы хотя бы пережили отказ одного региона с active-passive и поняли, почему вам нужен второй.


#25.2 Global Load Balancing — как направить пользователя в правильный регион

Допустим, у вас есть регионы в Европе, Америке и Азии. Как сделать так, чтобы пользователь из Франции попал во Франкфурт, а не в Виргинию?

Есть два основных подхода.

#25.2.1 GeoDNS (гео-маршрутизация по DNS)

Идея: DNS-сервер возвращает разный IP-адрес в зависимости от географического положения клиента.

DNS запрос из Франции:
  → myapp.example.com? → GeoDNS видит: Франция
  → Возвращает: 51.xx.xx.xx (Франкфурт)

DNS запрос из Японии:
  → myapp.example.com? → GeoDNS видит: Япония
  → Возвращает: 54.xx.xx.xx (Токио)

Так работают AWS Route 53 с политикой Geolocation Routing, Google Cloud Load Balancing, Azure Traffic Manager.

Плюсы:

  • Простота. Настраивается на уровне DNS, не требует изменений в приложении.
  • Поддержка всеми облачными провайдерами.

Минусы:

  • DNS-кэширование. Браузеры и DNS-резолверы кэшируют записи (TTL — от 30 секунд до минут). Если регион упал, вы не сможете быстро перенаправить трафик — часть пользователей будет долбиться в мёртвый IP.
  • Точность GeoIP. Определение местоположения по IP — не идеально. Пользователь может сидеть через VPN, и DNS увидит его в другой стране.
  • Нет балансировки по нагрузке. DNS не знает, сколько у вас свободных мощностей в каждом регионе.

Когда использовать: когда нужна простая гео-маршрутизация для сервисов, где допустима задержка переключения в 1-5 минут.

#25.2.2 Anycast (BGP anycast)

Anycast — это когда один и тот же IP-адрес анонсируется из нескольких дата-центров через BGP. Маршрутизаторы в интернете сами направляют пользователя к ближайшему дата-центру.

IP 203.0.113.42 анонсирован из:
  - AS15169 (Frankfurt)
  - AS15169 (Tokyo)
  - AS15169 (Virginia)

Пользователь в Париже → BGP видит: Франкфурт — ближайший
Пользователь в Осаке → BGP видит: Токио — ближайший

Так работает DNS-система Cloudflare, их CDN, и большинство крупных CDN-провайдеров.

Плюсы:

  • Мгновенное переключение при отказе региона — BGP пересчитывает маршруты за секунды.
  • Балансировка по сети — трафик естественным образом распределяется по регионам.
  • Нет проблем с DNS-кэшированием.

Минусы:

  • BGP — сложно. Вам нужна своя AS (Autonomous System), IP-блок, настройка BGP-пиринга с провайдерами.
  • Нет тонкого контроля. BGP решает по топологии сети, а не по реальной нагрузке на серверы.
  • Anycast не работает для TCP-соединений с состоянием (stateful TCP). Если соединение установилось через Франкфурт, а маршрут изменился — пакеты пойдут в Токио, и TCP-соединение разорвётся.

Когда использовать: CDN, DNS-серверы, stateless API endpoints. Для stateful приложений — только в комбинации с Layer 7 балансировкой.

#25.2.3 Application Layer Routing

Самый гибкий, но и самый сложный подход — маршрутизация на уровне приложения или API Gateway. Вы смотрите на заголовки запроса (гео-данные от CDN, язык браузера, token пользователя) и направляете запрос в нужный регион.

# Псевдокод: глобальный API Gateway
async def route_request(request):
    region = get_user_preferred_region(request)

    if region_healthy(region):
        return proxy_to_region(region, request)

    # fallback: ближайший здоровый регион
    for fallback in get_ordered_fallbacks(region):
        if region_healthy(fallback):
            return proxy_to_region(fallback, request)

    return 503  # все регионы упали

Плюсы:

  • Полный контроль. Вы знаете, какие регионы здоровы, какая у них нагрузка, какие данные доступны.
  • Можно делать sticky sessions (привязывать пользователя к региону по cookie/token).
  • Гранулярная маршрутизация (по пользователю, по типу запроса, по A/B эксперименту).

Минусы:

  • Дополнительная инфраструктура. Нужен глобальный load balancer, который знает про все регионы.
  • Дополнительная задержка. Первый запрос пользователя проходит дополнительный hop.

#Комбинированный подход (что используют в реальности)

Большинство крупных систем используют комбинацию:

1. Anycast DNS → направляет пользователя к ближайшему entry point (Edge)
2. Edge Load Balancer → проверяет здоровье регионов
3. API Gateway → принимает финальное решение о маршрутизации

Cloudflare Workers, AWS Global Accelerator, Google Cloud Global Load Balancer — все они работают примерно так. Первый hop — Anycast для минимальной задержки, а внутри — Layer 7 маршрутизация с health checks.


#25.3 Database Replication между регионами: ищем консенсус

Вот где начинается настоящее веселье. Репликация данных между регионами — это не «сделайте read replica в другом регионе». Это сложная распределённая задача с компромиссами по CAP-теореме (помните главу 3?).

#25.3.1 Synchronous vs Asynchronous Replication

Синхронная репликация:

Каждая запись подтверждается только после того, как как минимум N регионов подтвердили, что записали данные.

Client → Region A (write) → wait for ack from Region B → response to client
                           → wait for ack from Region C →
  • Плюс: Нет потери данных при отказе региона. Сильная консистентность (strong consistency).
  • Минус: Задержка записи = latency до самого дальшего региона. При отказе региона — запись блокируется до таймаута.
  • Компромисс: Можно настроить «полу-синхронный» режим — ждать подтверждения от одного из регионов, а не от всех. Но тогда при отказе всех синхронных реплик — переходим на асинхронный режим с риском потери.

Асинхронная репликация:

Запись подтверждается сразу в локальном регионе. Репликация в другие регионы происходит асинхронно — через CDC, log shipping, или streaming.

Client → Region A (write) → response to client (быстро!)
         Region A → (async) → Region B (через 0.1-5 секунд)
  • Плюс: Низкая задержка записи, независимо от расстояния между регионами.
  • Минус: При отказе основного региона до завершения репликации — данные потеряны.
  • Реальность: Даже у Google Spanner (который «синхронный») есть async fallback для некоторых сценариев.

PACELC — практический компромисс:

Если сеть работает (normal operation) — выбираем между консистентностью и задержкой. Если сеть отказала (partition) — выбираем между доступностью и консистентностью.

Сценарий Synchronous Asynchronous
Normal ops Strong consistency, latency ~100ms Eventual consistency, latency ~5ms
Region failure Writes blocked until recovery 🔴 Writes continue, possible data loss 🟡
Recovery Automatic, no data loss 🟢 Read repair / manual reconciliation 🔴

#25.3.2 Технологии multi-region replication

PostgreSQL — Logical Replication

-- На мастере: публикуем изменения
CREATE PUBLICATION global_pub FOR ALL TABLES;

-- На реплике: подписываемся
CREATE SUBSCRIPTION global_sub
CONNECTION 'host=master-host dbname=mydb'
PUBLICATION global_pub;

Logical replication в PG 16+ — уже вполне боевая технология. Она асинхронная, передаёт изменения на уровне строк, поддерживает фильтрацию. Но: нет автоматического разрешения конфликтов при multi-master. Если вы пишете в обе базы — вы сами отвечаете за то, чтобы не создать конфликт.

MySQL — Group Replication / InnoDB Cluster

MySQL Group Replication поддерживает multi-primary режим. Но, как и PostgreSQL, не умеет автоматически разрешать конфликты — последняя запись побеждает (last-write-wins), что может привести к потере данных.

Cassandra / ScyllaDB

Cassandra изначально спроектирована как multi-region, multi-master. У неё есть:

  • Configurable consistency level (ONE, QUORUM, ALL)
  • Hinted Handoff — если регион недоступен, записи хранятся локально и доставляются при восстановлении
  • Read Repair — при чтении проверяется консистентность между репликами
-- Запись с консистентностью LOCAL_QUORUM:
-- Ждём подтверждения от большинства реплик в локальном регионе
INSERT INTO users (id, name, email) VALUES (?, ?, ?)
USING CONSISTENCY LOCAL_QUORUM;

-- Чтение с консистентностью QUORUM:
-- Проверяем большинство реплик во всех регионах
SELECT * FROM users WHERE id = ?
USING CONSISTENCY QUORUM;

Cassandra — пожалуй, самый простой путь к multi-region active-active. Но он требует понимания trade-offs: операции с консистентностью ALL — медленные, а с ONE — могут вернуть устаревшие данные.

DynamoDB Global Tables

Amazon DynamoDB Global Tables — managed multi-region replication. Конфликты разрешаются по last-write-wins. Простота AWS managed сервиса — да, но:

  • Нет синхронной репликации (только eventual consistency между регионами)
  • Конфликты решаются примитивно
  • Стоимость может быть высокой при большом количестве регионов

CockroachDB / YugabyteDB

PostgreSQL-совместимые distributed SQL базы с native multi-region поддержкой. Они используют Raft консенсус для синхронной репликации между регионами. Но:

  • Latency записи = round-trip до самого дальнего региона
  • Сложность настройки топологий (survival goals, lease preferences)
  • Дороже в эксплуатации, чем managed PostgreSQL

#25.3.3 Реальность: компромиссы на практике

В реальном мире редко используют один подход для всех данных. Более типичная картина:

Глобальный сервис с multi-region:

User Data (профиль, настройки):
  - Primary регион пользователя (определяется при регистрации)
  - Асинхронная репликация в другие регионы для read-only
  - При смене региона — миграция данных

Session Data (кэш, временные данные):
  - Локальный для региона (Redis)
  - Не реплицируется — потеря сессии не критична

Content / Catalog (товары, статьи):
  - Изменяется редко
  - Репликация + инвалидация через CDN
  - Strong consistency не обязательна

Financial Data (баланс, транзакции):
  - Только один регион для записи (active-passive)
  - Дублируется в DR регион в реальном времени
  - Read-only реплики для локального чтения

Этот подход называется data sovereignty per entity — сущность имеет «домашний» регион, и все записи идут в него. Репликация в другие регионы — только для чтения.


#25.4 Conflict Resolution: теория и практика

Active-active — это конфликты. Их не избежать. Вопрос только: как вы их разрешаете.

#25.4.1 Last-Write-Wins (LWW)

Самый простой подход: последняя запись побеждает. Берём timestamp (или version vector), сравниваем, побеждает запись с большим значением.

class LWWRegister:
    def __init__(self):
        self.value = None
        self.timestamp = 0

    def write(self, value, timestamp):
        if timestamp > self.timestamp:
            self.value = value
            self.timestamp = timestamp
            return True
        return False  # отбрасываем "старую" запись

# При репликации: два региона обменялись записями
# A: значение "Привет", timestamp 1000
# B: значение "Hello", timestamp 1001
# → Побеждает "Hello" (новее)

Проблема: Теряются данные. Если пользователь в Европе поменял имя, а в Азии через секунду вернул старое — данные «старого» изменения потеряны безвозвратно.

Когда это нормально:

  • Тимлайны сообщений в чате
  • Посты в соцсетях (важно, что опубликовано, порядок не критичен)
  • Last-update timestamp

Когда это опасно:

  • Финансовые данные
  • Инвентаризация (два региона продали последний товар — оба думают, что он есть)
  • Совместное редактирование

#25.4.2 CRDT — Conflict-Free Replicated Data Types

CRDT (Conflict-Free Replicated Data Types) — это структуры данных, которые математически гарантируют отсутствие конфликтов при слиянии. Независимо от того, в каком порядке применяются изменения — результат всегда одинаковый (convergent).

Виды CRDT:

G-Counter (Grow-only Counter):

class GCounter:
    """Счётчик, который можно только увеличивать."""
    def __init__(self, node_id, num_nodes):
        self.node_id = node_id
        self.counts = [0] * num_nodes

    def increment(self):
        self.counts[self.node_id] += 1

    def merge(self, other):
        for i in range(len(self.counts)):
            self.counts[i] = max(self.counts[i], other.counts[i])

    def value(self):
        return sum(self.counts)

# Region A: local count = [5, 0, 0]
# Region B: local count = [0, 3, 0]
# После merge: [5, 3, 0] → value = 8 ✅ не зависит от порядка

PN-Counter (счётчик с увеличением и уменьшением): Работает как комбинация двух G-Counter — один для инкрементов, другой для декрементов.

LWW-Register (Last-Write-Wins Register как CRDT): Да, это тоже CRDT. Берёт версию с наибольшим timestamp. Проблема та же — потеря данных. Но это предсказуемая потеря, что уже лучше.

OR-Set (Observed-Remove Set): Множество, где можно добавлять и удалять элементы. Гарантирует, что если элемент добавлен — он не исчезнет при merge, даже если в одном регионе его удалили, а в другом не знают об удалении.

class ORSet:
    """Observed-Remove Set — элементы с тегами."""
    def __init__(self):
        self.elements = {}  # element → {tag, node_id}

    def add(self, element, tag):
        self.elements[element] = tag

    def remove(self, element):
        self.elements.pop(element, None)

    def merge(self, other):
        for elem, tag in other.elements.items():
            if elem not in self.elements or self.elements[elem] < tag:
                self.elements[elem] = tag

# Region A: add("item1", tag=1)   → {"item1": 1}
# Region B: remove("item1")        → {}
# Region C: add("item1", tag=2)    → {"item1": 2}
# После merge всех трёх: {"item1": 2} ← тег 2 > тега 1

Реальные примеры CRDT:

  • Figma — использует CRDT для много-пользовательского редактирования
  • Redis CRDT — Redis Enterprise поддерживает CRDT-базированную репликацию
  • Automerge / Yrs — библиотеки для collaborative editing
  • SoundCloud — использует CRDT для управления очередью воспроизведения

#25.4.3 Custom Conflict Resolution

Когда CRDT или last-write-wins не подходят, приходится писать свою логику.

Типы custom resolution:

  1. CRDT-like для бизнес-логики:

    • Вместо «последняя побеждает» — «наибольшая сумма» или «наименьшее значение»
    • Например, для инвентаря: merge = min(Region A stock, Region B stock) — никогда не продадим того, чего нет
  2. Manual review queue:

    • Конфликты складываются в очередь, оператор решает вручную
    • Используется в финансовых системах, где цена ошибки высока
    • Проблема: очередь может расти быстрее, чем операторы успевают её разбирать
  3. Domain-specific merge:

    # Пример: календарь (Google Calendar sync)
    # Если встреча добавлена в Region A в 14:00,
    # а в Region B — в 15:00:
    # → Сохраняем обе встречи (CRDT-like add-only)
    # Если встреча в 14:00 удалена в Region A,
    # а в Region B изменена:
    # → Вероятно, пользователь не хотел удалять изменённую встречу
    # → Сохраняем изменённую, удаляем только исходную (OR-Set semantics)
  4. Application-level reconciliation:

    • Некоторые системы пишут «операции» (commands), а не «состояние» (state)
    • При конфликте воспроизводят операции в правильном порядке
    • Это практически Event Sourcing между регионами — но требует идемпотентности (см. главу 17)

#Decision Matrix: Conflict Resolution

Подход Гарантии Complexity Потеря данных Когда
Last-Write-Wins Eventual 🟢 Низкая 🟡 Возможна Некритичные данные
CRDT Strong convergence 🟡 Средняя 🟢 Нет (для counter/set) Совместное редактирование
Custom merge Domain-specific 🔴 Высокая 🟢 По логике Финансы, инвентарь
Manual review Человек решает 🔴 Очень высокая 🟢 Нет (но долго) Compliance-critical
Single-writer region Strong 🟢 Минимальная 🟢 Нет Когда можно отказаться от active-active

#25.5 Cross-Region Failover: как тестировать то, что надеешься никогда не использовать

Failover между регионами — как огнетушитель. Он либо работает идеально, либо вы узнаёте об этом в самый неподходящий момент.

#25.5.1 Что должно произойти при отказе региона

  1. Detection — как вы узнаёте, что регион упал?

    • Health checks снаружи региона (если health check идёт из того же региона — он может быть неактуален при сетевом разделении).
    • Synthetic monitoring из разных точек мира.
    • Аномалии в метриках: резкое падение трафика, рост ошибок, timeout соединений.
  2. Decision — кто принимает решение о failover?

    • Автоматически или вручную?
    • Анекдот: «Automated failover никогда не делают вручную, ручной failover никогда не делают автоматически».

    Моя рекомендация: автоматическое обнаружение (detection), ручное принятие решения (decision). Автоматический failover — для Netflix и Google, у которых SRE нанимают ракетчиков. Для всех остальных — ручной триггер.

  3. DNS Update / Traffic Drain — перенаправление трафика.

    • Если используете GeoDNS — меняете DNS-записи (помните про кэширование?).
    • Если Anycast — withdraw BGP-анонс упавшего региона.
    • Если Application Layer Routing — обновляете конфиг route-распределения.
  4. Data Promotion — делаем регион B основным.

    • Для active-passive: promote реплику до primary.
    • Для active-active: просто перенаправляем трафик (если здоровый регион справляется с нагрузкой обоих).
  5. Verification — проверяем, что система работает.

    • Smoke test критических путей.
    • Мониторинг ошибок и задержек.
    • Бизнес-метрики (регистрации, транзакции).

#25.5.2 Что идёт не так (мой опыт)

DNS propagation — ваш враг.

Вы переключили DNS на здоровый регион. И тут выясняется, что провайдер в Юго-Восточной Азии кэширует DNS-записи на 5 минут (хотя TTL выставлен 30 секунд). И ещё 3% пользователей упорно долбятся в мёртвый регион.

Решение: используйте короткие TTL (30-60 секунд) для critical DNS-записей. Но — короткий TTL значит больше DNS-запросов и больше расходов на DNS-серверы.

Data consistency после failover.

Вы переключили трафик на Region B. Асинхронная репликация не успела передать последние 500 записей из Region A. Region B работает, но потерял данные. Как узнавать, что Region A оживёт — и эти данные появятся?

  • Проблема: Время жизни региона в отключке и объём потерянных данных — обратно пропорциональны числу седых волос у инженера.
  • Решение: Pre-flight checks перед failover — проверьте replication lag. Если он слишком велик, может быть, проще подождать, чем запускать failover с потерей данных.

Cascade failure.

Регион А упал → трафик перенаправлен в регион B → регион B не рассчитан на двойную нагрузку → регион B тоже лёг. Теперь у вас outage на всей планете.

Решение: capacity planning для региона B с запасом. Регион B должен выдерживать 100% трафика (или хотя бы столько, сколько у вас активных пользователей). Если это дорого — примите, что при отказе региона сервис будет деградировать (degradation), а не падать полностью (outage).

#25.5.3 Chaos Engineering для failover

Failover надо тестировать. Не раз в год «для галочки», а регулярно.

Game Day сценарий:

  1. Раз в квартал — плановый failover. Перенаправляем трафик из Region A в Region B.
  2. Сначала в «теневом режиме» (shadow traffic) — дублируем запросы, но не переключаем реальный трафик.
  3. Потом с реальным трафиком — но в «тихий час» (например, 4 утра воскресенья в регионе с минимумом пользователей).
  4. Потом — в рабочее время. Да, страшно. Но это единственный способ проверить, что ваша команда умеет реагировать.

Chaos Monkey для multi-region: Region Monkey — инструмент, который отключает целый регион и проверяет, что failover сработал.


#25.6 Data Gravity — почему данные притягивают вычисления

Термин «data gravity» (гравитация данных) ввёл Dave McCrory в 2010 году. Идея простая: данные имеют массу. Чем больше данных — тем сильнее они притягивают приложения и сервисы.

Когда у вас данные в одном регионе, а compute в другом — каждое чтение и запись идёт через сеть. А сеть между регионами — это:

  • Задержка (latency) — 50-200 ms round-trip
  • Пропускная способность (bandwidth) — дороже, чем внутри региона
  • Надёжность — межрегиональные соединения обрываются чаще, чем внутрирегиональные

#Data Gravity в действии

Проблема:

Data в us-east-1 (100 TB)
Compute в eu-west-1 (там живут ваши Kubernetes кластеры)
Каждый запрос: eu-west-1 → us-east-1 → eu-west-1 → клиенту
Latency: ~90 ms только на межрегиональное соединение

Решение: перенесите compute туда, где данные.

Data в us-east-1
Compute в us-east-1 (добавьте ещё один K8s кластер здесь)
Каждый запрос: us-east-1 → us-east-1 → клиенту
Latency: ~2 ms внутри региона

Следствие: нельзя полностью изолировать данные в одном регионе и при этом иметь compute, размазанный по всему миру. Данные диктуют, где живут вычисления.

#Стратегии работы с Data Gravity

  1. Partition by region: разделяйте данные по «домашнему» региону пользователя.

    • Пользователи из Европы → данные в Европе, compute в Европе
    • Пользователи из Азии → данные в Азии, compute в Азии
    • Проблема: пользователь переезжает, или работает из трёх стран
  2. Replicate data globally: храните копии данных во всех регионах.

    • Решает latency, создаёт проблемы консистентности
    • Работает для read-heavy, редко изменяемых данных
  3. Separate hot/cold data:

    • Hot data (активные пользователи, последние транзакции) — реплицируется
    • Cold data (архив, история) — хранится в одном регионе
    • Пример: Slack хранит историю сообщений в регионе команды, а текущие — реплицирует
  4. Follow-the-sun: бизнес-процессы естественным образом мигрируют за солнцем.

    • Утро в Азии — batch jobs на азиатских данных
    • День в Европе — аналитика на европейских данных
    • Ночь — глобальные операции, которые требуют все данные

#Практическое правило

Если ваш датасет больше 10 TB — data gravity становится вашей главной архитектурной проблемой.

Для меньших датасетов можно тупо копировать данные между регионами (стоимость хранения дешевле стоимости инженерного времени на оптимизацию).


#25.7 Кейс: Google Spanner — глобальная база данных, которая не врёт

Spanner — это не просто база данных. Это ответ Google на вопрос: «Можно ли иметь ACID-транзакции с глобальной консистентностью на планете Земля?»

#Проблема

У Google есть сервисы, которые работают глобально: Google Ads, Google Photos, Gmail (ну, хотя бы метаданные), YouTube. Им нужна база данных, где:

  • Транзакции ACID (а не «почти ACID»)
  • Strong consistency на всех континентах
  • Read latency < 10 ms для пользователя в любом регионе
  • 5 nines доступности

Ни одна существующая база не удовлетворяла этим требованиям. Даже Bigtable (их собственная NoSQL) не даёт ACID-транзакций между строками.

#Как это работает

Spanner использует несколько ключевых технологий:

TrueTime API

Обычные распределённые базы используют logical clocks (Lamport clocks, version vectors) для упорядочивания событий. Это работает, но сложно и требует дополнительных протоколов для синхронизации.

Spanner использует физическое время. Он знает точное время с погрешностью (error bound) ~1-10 мс благодаря:

  • GPS-приёмникам в каждом дата-центре
  • Атомным часам
  • Специальному протоколу синхронизации времени (Google Time Service)
// TrueTime гарантирует: текущее время ∈ [TT.now().earliest, TT.now().latest]
// Коммит транзакции:
// Если commit timestamp = T, то TT.after(T) истинно для всех мировых нод
// Это даёт external consistency — как будто работает на одной машине

Paxos между регионами

Spanner шардирует данные по узлам. Каждый шард реплицируется через Paxos (протокол консенсуса, предшественник Raft) в несколько регионов. Обычно 5 реплик, из которых 3 подтверждают запись.

Шард 1:
  us-east1: Leader (Paxos)
  us-central1: Follower
  europe-west1: Follower
  asia-east1: Follower
  us-west1: Follower

Запись → идёт к Leader → Paxos подтверждает от 3 из 5
       → данные записаны глобально
       → ответ клиенту

Follower Reads

Чтобы read latency была низкой для удалённых пользователей, Spanner поддерживает чтение с реплик-фолловеров. Данные могут быть слегка устаревшими, но Spanner гарантирует, что читатель увидит все данные, записанные до определённого момента времени (read timestamp).

-- Spanner SQL: читаем с ближайшей реплики
-- (может быть слегка устаревшей, но bounded staleness)
SELECT * FROM AdCampaigns
WHERE campaign_id = @id
@{
  read_timestamp: timestamp
  -- или exact_staleness: 5s
}

#TrueTime — это не про скорость сети

Важный момент: TrueTime не делает запись быстрее. Он делает её консистентной. Запись в Spanner всё равно ждёт подтверждения от других регионов (Paxos). TrueTime просто позволяет упорядочивать транзакции по всему миру без сложных distributed locks.

Запись в Spanner с репликами на трёх континентах — это ~50-200 ms. Но вы получаете ACID, external consistency и отсутствие конфликтов.

#Когда Spanner — это overkill

Spanner — потрясающая технология. Но она не нужна 99.9% проектов.

Spanner для стартапа — это как купить Boeing 747, чтобы летать на дачу.

Если ваш датасет влезает на одну машину — PostgreSQL с streaming replication решит ваши проблемы за 1% стоимости Spanner. Если вам нужно multi-region — подумайте, может быть, CockroachDB или managed PostgreSQL с read replicas в других регионах — это то, что вам нужно.

Spanner — выбор для компаний, у которых уже есть:

  • Десятки терабайт данных
  • Миллионы пользователей по всему миру
  • Финансовые транзакции, где цена ошибки консистентности — деньги
  • Команда, которая понимает распределённые системы

#25.8 Кейс: Slack — от single-region к multi-region Active-Active

Slack — интересный кейс, потому что они прошли путь, который многие проходят: от монолита в одном регионе к глобальной архитектуре.

#Проблема

До 2019 года Slack работал из одного региона AWS — us-west-2 (Орегон). Для американских пользователей — отлично. Для европейских — задержка 100-200 мс. Для азиатских — 200-400 мс.

Slack — это real-time коммуникация. Задержка в 400 мс для сообщения в чате — это не «чуть-чуть медленнее». Это разница между «нормальный разговор» и «все говорят и перебивают друг друга, потому что не видят сообщений в реальном времени».

#Подход

Slack выбрал active-active архитектуру. Почему не active-passive?

У Slack есть две критические особенности:

  1. WebSocket persistence — пользователь подключается к WebSocket и держит соединение для real-time сообщений. При failover все WebSocket-соединения рвутся, и пользователи переподключаются — это заметно.
  2. Сообщения — это данные, которые пишутся постоянно. Каждое сообщение — запись в БД. Если все записи гонять в один регион, пользователи в Европе будут ждать 100+ мс для отправки каждого сообщения.

#Архитектура

Slack использует подход workspace locality:

У каждой рабочей области (workspace) есть «домашний» регион, определяемый при создании (или по географии большинства участников). Все данные workspace — сообщения, файлы, каналы — живут в его домашнем регионе.

Workspace из Лондона → находится в eu-west-1
  - Сообщения пишутся в eu-west-1 (low latency)
  - Файлы хранятся в eu-west-1
  - История каналов читается из eu-west-1

Workspace из Токио → находится в ap-northeast-1
  - Сообщения пишутся в ap-northeast-1
  - Файлы хранятся в ap-northeast-1

Но есть данные, которые глобальны: поиск по всем workspace, shared channels (каналы между разными workspace), user presence. Для них Slack использует централизованные глобальные сервисы с репликацией.

#Технологии

  • База данных: MySQL + Vitess (шардинг). Vitess позволяет им шардировать данные по workspace внутри региона и реплицировать между регионами.
  • Кэш: Memcached (да, они до сих пор используют Memcached, не Redis).
  • Сообщения: Своя система на основе Kafka для доставки сообщений в реальном времени.
  • Sync: Собственная система для асинхронной репликации данных между регионами.

#Что пошло не так

Slack честно рассказывали про проблемы на QCon:

  1. Зависимости между регионами. Некоторые сервисы случайно оказывались «глобальными» — запрос из Европы неожиданно уходил в Америку для проверки лицензии/авторизации. Пришлось перепроектировать так, чтобы все зависимости были локальными или асинхронными.

  2. Shared state. Наличие общих данных (user presence, shared channels) означает, что полной изоляции регионов добиться нельзя. Приходится выбирать: latency для этих данных или eventual consistency.

  3. Сообщения в реальном времени. Если пользователь из workspace в Европе, но сейчас в командировке в Токио — его сообщения должны доставляться быстро, но его данные для записи находятся в Европе. Slack решили это через глобальный слой доставки сообщений (WebSocket gateway в каждом регионе), который не зависит от региона хранения данных.

  4. Bootstrapping. Переезд существующего workspace из одного региона в другой — это полноценная миграция данных с даунтаймом. Slack сделали возможность переезда, но это не мгновенный процесс.

#Уроки

Слак, в отличие от многих, не строил распределённую базу данных. Они использовали существующие технологии (MySQL + Vitess) и сконцентрировались на архитектурном паттерне: локальность данных по workspace + глобальный слой для общих данных.

Это прагматичный подход. Вы не Google, у вас нет ресурсов на написание Spanner. Но workspace locality — паттерн, который применим к огромному количеству сервисов: разделите пользователей по регионам, и пусть их данные живут локально.


#25.9 Когда это применимо? Context Assessment

#Для стартапа

Не делайте multi-region. Серьёзно. Возьмите CDN для статики, Cloudflare для кэша, и если ваш бэкенд в одном регионе — это ок. Для первых 10 миллионов пользователей одного региона AWS (или вашего облачного провайдера) достаточно.

Исключение: ваш рынок с первого дня — глобальный, и вы конкурируете за latency-sensitive пользователей (real-time gaming, trading). Но даже тогда — active-passive с read replicas, не active-active.

#Для средней компании

  • Active-passive обязателен для DR (Disaster Recovery). Если ваш бизнес зависит от доступности — план восстановления в другом регионе должен быть.
  • Read replicas в стратегических регионах — для улучшения user experience.
  • Data gravity — начните замечать, что данные «притягивают» compute.

#Для enterprise / high-load

  • Active-Active — если у вас глобальная аудитория и latency — конкурентное преимущество.
  • CRDT для конфликтующих данных — если не можете терпеть last-write-wins.
  • Chaos engineering для failover — регулярные game days.
  • Data sovereignty — разделение данных по региональным требованиям (GDPR, местные законы).

#Уровень нагрузки

Нагрузка Multi-region
Low (< 1K RPS) Не нужен
Medium (1K-50K RPS) Active-Passive DR
High (50K-500K RPS) Active-Active (если глобально)
Extreme (> 500K RPS) Multi-region обязателен + sharding внутри регионов

#25.10 Чек-лист: Готовы ли вы к multi-region?

  • У вас есть измеренная проблема с latency для удалённых пользователей (цифры, не ощущения)
  • Вы понимаете RPO (Recovery Point Objective) и RTO (Recovery Time Objective) для вашего бизнеса
  • У вас настроен мониторинг и alerting, который отличает «регион упал» от «нагрузка выросла»
  • Вы протестировали failover хотя бы раз (не в презентации, а реально — с отключением региона)
  • Ваши сервисы stateless (или state вынесен в Redis/БД) — без этого multi-region не имеет смысла
  • Вы знаете свои конфликтующие данные и выбрали стратегию resolution (LWW, CRDT, custom)
  • У вас есть capacity planning — второй регион выдержит 100% трафика при отказе первого
  • DNS TTL настроены минимально (30-60 секунд для critical записей)
  • Вы понимаете свою data gravity — какие данные где живут и почему
  • CI/CD pipeline умеет деплоить в оба региона одновременно (или хотя бы последовательно)
  • У вас есть план миграции пользователей/данных между регионами (кто-то переезжает)
  • Вы проверили compliance требования — можно ли данные европейских пользователей хранить в США?

#25.11 «Не делай так»: Active-Active без понимания конфликтов

Это не анти-паттерн. Это архитектурное самоубийство. И я видел это своими глазами.

#История

Стартап, B2B SaaS. Решили покорять мир. «Нам нужен active-active прямо сейчас» — сказал CTO. «Пользователи в Европе жалуются на тормоза».

Архитектор нарисовал красивую схему: два региона, оба активные, репликация через CDC, всё на PostgreSQL multi-master.

Первый месяц — всё прекрасно. Сайт летает, пользователи счастливы.

Второй месяц — начинаются странности. Товар, которого нет в наличии, продаётся дважды — в разных регионах. Один пользователь меняет email, а через минуту система показывает старый. Заявки тасуются — в Европе создали заявку, в Америке её закрыли, а в Европе она висит как открытая.

Третий месяц — аврал. База данных в рассинхроне. PostgreSQL multi-master не умеет разрешать конфликты «из коробки» — он просто применяет записи в порядке поступления, и последняя запись побеждает. Результат: в таблице inventory для одного товара — остаток «5» в Европе и «-2» в Америке. Заказы, которые нельзя выполнить. Клиенты, которые заплатили за товар, которого нет.

Четвёртый месяц — CTO уволен. Система переведена в single-region. Ещё полгода ушло на восстановление консистентности данных.

#Почему это произошло

Три ошибки, которые убивают active-active:

  1. «PostgreSQL multi-master решит все проблемы» — нет, не решит. Реляционные базы не спроектированы для multi-master консистентности. Они используют last-write-wins, потому что ничего лучше не умеют «из коробки».

  2. «Напишем свою conflict resolution на коленке» — если у вас нет команды, которая понимает распределённые системы и CRDT — ваша самописная система будет хуже, чем last-write-wins.

  3. «Active-active для всего» — не нужно делать active-active для всех данных. Read-only данные, кэш, некритичные профили — да. Финансы, инвентарь, заказы — категорически нет без проработанной стратегии конфликтов.

#Правило большого пальца

у вас проблемы с active-active

Если вы не можете ответить на вопрос «что произойдёт с данными, если два пользователя одновременно изменят одну запись в разных регионах?» — у вас проблемы с active-active.

#Что делать вместо этого

  • Active-passive с read replicas — 90% пользы при 10% сложности.
  • Active-active для read-only данных — replicate, cache, CDN. Пишите в один регион.
  • Workspace locality — как Slack. Разделите пользователей по регионам. Внутри региона — всё просто (single-master).
  • CRDT-only для конфликтующих данных — используйте готовые библиотеки (Redis CRDT, Cassandra, или CRDT библиотеки в приложении).

И помните: один здоровый регион лучше, чем два рассинхронизированных. Multi-region начинается не с «крутости». Он начинается с боли — когда пользователи реально страдают от латентности, когда отказ одного дата-центра угрожает бизнесу, когда требования регуляторов не позволяют хранить данные удалённо.

Не делайте multi-region «на вырост». Делайте, когда вырастете.


#Ключевые выводы

  1. Active-passive — дефолт. Active-active — только если вы готовы платить за сложность деньгами, временем и седыми волосами инженеров.

  2. Гео-маршрутизация начинается с DNS (GeoDNS) и Anycast. Для stateless сервисов — Anycast (CDN, кэш). Для stateful — GeoDNS + application layer routing.

  3. Межрегиональная репликация данных — это выбор между консистентностью и задержкой (PACELC). Синхронная репликация консистентна, но медленна. Асинхронная — быстра, но риск потери данных.

  4. Конфликты неизбежны в active-active. CRDT — математический способ гарантировать конвергенцию данных без потерь. Но не все данные можно представить в виде CRDT.

  5. Data gravity реальна. Чем больше ваши данные, тем сильнее они привязывают compute к региону. Разделяйте данные по «домашнему» региону пользователя (workspace locality).

  6. Failover нужно тестировать. Раз в квартал — плановое переключение региона. Раз в год — chaos engineering с имитацией отказа.

  7. Multi-region — это не про деньги в банке. Это про физику, математику и дисциплину. Начинайте с простого, усложняйте только когда боль от текущего решения становится сильнее боли от внедрения нового.

Forward reference: AI-агенты уже начинают использоваться для автоматической маршрутизации трафика и самовосстановления систем в multi-region архитектуре. Как AI меняет подход к глобальной инфраструктуре — в Главе 38 «Будущее — архитектор + AI».