Skip to content

Instantly share code, notes, and snippets.

@dmitry-osin
Created January 21, 2026 09:46
Show Gist options
  • Select an option

  • Save dmitry-osin/b1913fb1f265bf47da2a76f633b84a74 to your computer and use it in GitHub Desktop.

Select an option

Save dmitry-osin/b1913fb1f265bf47da2a76f633b84a74 to your computer and use it in GitHub Desktop.
System Design снуля до эксперта на русском

Уровень 1: Фундамент (The Basics)

Прежде чем строить небоскреб, нужно понять, как работает кирпич и цемент.

1. Основы сетевого взаимодействия

  • Модель OSI и TCP/IP: Понимание уровней (особенно L4 Transport и L7 Application).
  • Протоколы: HTTP/1.1 vs HTTP/2 vs HTTP/3, HTTPS (TLS/SSL handshake), TCP vs UDP, DNS (как работает резолвинг).
  • API Design:
    • REST (ресурсы, методы, статус-коды).
    • GraphQL (плюсы и минусы, проблема N+1).
    • RPC (gRPC, Protocol Buffers).
    • WebSockets (для real-time).

2. Базы данных: Введение

  • Реляционные СУБД (RDBMS): PostgreSQL, MySQL. Нормализация, индексы (B-Tree), транзакции (ACID).
  • NoSQL:
    • Key-Value (Redis).
    • Document (MongoDB).
    • Column-family (Cassandra).
    • Graph (Neo4j).
  • SQL vs NoSQL: Критерии выбора.

3. Базовое масштабирование

  • Vertical Scaling (Scale Up) vs Horizontal Scaling (Scale Out).
  • Load Balancing (Балансировка нагрузки):
    • L4 vs L7 балансировка.
    • Алгоритмы: Round Robin, Least Connections, IP Hash.
    • Software (Nginx, HAProxy) vs Hardware.

Уровень 2: Компоненты системы (Core Building Blocks)

Инструментарий архитектора для решения типовых задач.

1. Кэширование (Caching)

  • Стратегии: Cache-Aside, Write-Through, Write-Back, Write-Around.
  • Уровни кэширования: Client-side, CDN, Load Balancer, Server-side, Database.
  • Проблемы: Cache Invalidation (инвалидация), Eviction policies (LRU, LFU), Thundering Herd problem.

2. Асинхронность и Очереди сообщений

  • Message Queues: RabbitMQ, ActiveMQ. Модели Point-to-Point и Publish/Subscribe.
  • Message Brokers vs Event Streaming: Разница между RabbitMQ и Apache Kafka.
  • Обработка: Idempotency (идемпотентность — критически важно для очередей), Dead Letter Queues.

3. Прокси и CDN

  • Reverse Proxy vs Forward Proxy.
  • CDN (Content Delivery Network): Принципы работы (Edge servers), Push vs Pull.

Уровень 3: Теория распределенных систем (Deep Dive)

Здесь начинается настоящий System Design. Это то, что отличает Senior инженера.

1. Теоремы и принципы

  • CAP Theorem: Consistency, Availability, Partition Tolerance. Почему нельзя получить все три.
  • PACELC Theorem: Расширение CAP (учет Latency при отсутствии Partition).
  • Availability Patterns: Fail-over (Active-Passive, Active-Active). Расчет доступности (SLA/SLO/SLI).
    • Формула доступности: $$ Availability=\frac{Uptime}{Uptime+Downtime} $$

2. Согласованность данных (Consistency)

  • Модели: Strong Consistency, Eventual Consistency, Causal Consistency.
  • Distributed Transactions:
    • Two-Phase Commit (2PC).
    • Saga Pattern (Choreography vs Orchestration).
    • Three-Phase Commit (3PC).

3. Алгоритмы консенсуса

  • Как узлы договариваются о правде?
  • Paxos, Raft, Zab.
  • Лидерские выборы (Leader Election).
  • Gossip Protocols: (используется в Cassandra/Dynamo).

4. Масштабирование Баз Данных (Advanced)

  • Sharding (Шардирование): Горизонтальное разделение данных.
  • Replication: Master-Slave, Master-Master.
  • Consistent Hashing: Как добавлять/удалять узлы без перераспределения всех ключей (Ring architecture).

Уровень 4: Продвинутые паттерны и надежность (Advanced & Resiliency)

Как сделать так, чтобы система не упала, когда все идет не так.

1. Микросервисы (Microservices Architecture)

  • Decomposition: Как делить монолит (DDD — Domain Driven Design).
  • Service Discovery: (Consul, Eureka, ZooKeeper).
  • API Gateway: (Zuul, Kong) — единая точка входа, аутентификация, rate limiting.

2. Устойчивость (Resiliency)

  • Circuit Breaker: Предотвращение каскадных сбоев.
  • Bulkhead: Изоляция ресурсов.
  • Retry & Exponential Backoff: Правильные стратегии повторных запросов.
  • Rate Limiting: Алгоритмы (Token Bucket, Leaky Bucket, Sliding Window).

3. Observability (Наблюдаемость)

  • Logging: Централизованный сбор логов (ELK Stack: Elasticsearch, Logstash, Kibana).
  • Metrics: Prometheus, Grafana.
  • Distributed Tracing: Jaeger, Zipkin (отслеживание запроса через 20 микросервисов).

Уровень 5: Экспертный уровень и специализации

Темы для проектирования специфических и сверхнагруженных систем.

1. Big Data и потоковая обработка

  • Batch Processing: MapReduce, Hadoop.
  • Stream Processing: Apache Spark, Apache Flink, Kafka Streams.
  • Lambda vs Kappa Architecture.

2. Хранилища специального назначения

  • Time-Series DB: (InfluxDB, Prometheus) — для метрик.
  • Spatial DB: (PostGIS, Quadtree, Geohash) — для карт и Uber-like сервисов.
  • Full-text Search: (Elasticsearch, Solr) — обратные индексы (Inverted Index).

3. Безопасность (Security by Design)

  • OAuth 2.0 / OIDC.
  • JWT vs Session-based auth.
  • Шифрование данных (At rest, In transit).

Уровень 6: Практика (Interview & Real World)

Применение знаний на классических задачах.

1. Фреймворк решения задач

  • Выяснение требований (функциональные / нефункциональные).
  • Оценка нагрузок (Back-of-the-envelope calculations).
  • Высокоуровневый дизайн.
  • Детальный дизайн (узкие места).

2. Классические кейсы для разбора

  1. URL Shortener (TinyURL) — основы, хэширование, уникальные ID.
  2. Pastebin — хранение текста, clean-up policies.
  3. Design Instagram/Twitter — Fan-out on write vs Fan-out on read, лента новостей.
  4. Design WhatsApp/Telegram — вебсокеты, шифрование, история сообщений.
  5. Design Youtube/Netflix — хранение блобов, CDN, адаптивный битрейт.
  6. Design Uber/Grab — геохеширование, matching водителей.
  7. Web Crawler — многопоточность, дедупликация.

Рекомендованная литература ("Библия" System Design)

Если нужно выбрать только одну книгу, начните с первой:

  1. "Designing Data-Intensive Applications" (DDIA)Martin Kleppmann. Это золотой стандарт. Must read.
  2. "System Design Interview – An insider's guide" (Vol 1 & 2)Alex Xu. Отлично для подготовки к собеседованиям и понимания схем.
  3. "Microservices Patterns"Chris Richardson.

Уровень 1: Фундамент

====================

Глава 1. Основы сетевого взаимодействия (Часть 1)


Прежде чем говорить о микросервисах, нужно понимать, как байты перемещаются по проводам. В System Design большинство проблем с производительностью (latency) и надежностью кроются именно здесь.

1. Модели: OSI и TCP/IP

Существует теоретическая модель (OSI — 7 уровней) и практическая (TCP/IP — 4 уровня), на которой работает современный интернет. Для нас, как инженеров, критически важны два уровня: L4 (Transport) и L7 (Application).

Уровень OSI Название Примеры протоколов Задача в System Design
L7 (Application) Прикладной HTTP, FTP, SMTP, SSH, GraphQL Логика приложения. Здесь работают балансировщики нагрузки L7 (Nginx), понимающие URL и заголовки.
L4 (Transport) Транспортный TCP, UDP Доставка данных "процессу" на машине (порты). L4 балансировщики просто перекидывают пакеты, не читая содержимое.
L3 (Network) Сетевой IP (IPv4, IPv6), ICMP Маршрутизация между сетями (IP-адреса).
L2 (Data Link) Канальный Ethernet, Wi-Fi (802.11) Доставка внутри одной локальной сети (MAC-адреса).
L1 (Physical) Физический Кабель, радиоволны Передача битов (0/1).

Почему это важно: Когда вы слышите "L7 Load Balancer", вы должны понимать, что он "умный" (может расшифровать HTTPS, посмотреть Cookie), но медленный (требует CPU). "L4 Load Balancer" — глупый, но невероятно быстрый (просто пересылает пакеты).

2. TCP vs UDP: Битва надежности и скорости

Это основа транспортного уровня. Все прикладные протоколы (HTTP, DB connections) строятся поверх одного из них.

TCP (Transmission Control Protocol)

Это протокол с гарантированной доставкой.

  • Ориентирован на соединение: Требует "рукопожатия" (3-way handshake) перед отправкой данных.
  • Надежность: Гарантирует, что пакеты придут без потерь и в правильном порядке. Если пакет потерян, TCP отправит его снова (Retransmission).
  • Flow Control: Если получатель не успевает, отправитель снижает скорость.

Применение: Веб-сайты (HTTP), базы данных, почта, передача файлов. Везде, где потеря одного байта критична.

UDP (User Datagram Protocol)

Это протокол "выстрелил и забыл".

  • Без соединения: Просто шлет пакеты на IP:Port.
  • Ненадежность: Пакеты могут теряться, дублироваться или приходить в разном порядке. Протокол это не волнует.
  • Скорость: Нет накладных расходов на рукопожатия и подтверждения.

Применение: Видеостриминг, онлайн-игры (шутеры), DNS, VoIP. Там, где лучше потерять кадр, чем ждать его повторную отправку (лаг).

Практика: TCP vs UDP на Python

Давайте посмотрим на код, чтобы понять разницу в реализации. Мы используем встроенную библиотеку socket.

Пример 1: TCP Сервер и Клиент

Обратите внимание на процесс соединения.

TCP Server:

import socket

def start_tcp_server():
    # AF_INET = IPv4, SOCK_STREAM = TCP
    server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    
    # Биндим сокет на localhost и порт 8080
    server_socket.bind(('127.0.0.1', 8080))
    
    # Переводим сокет в режим прослушивания. 
    # Аргумент 5 - размер очереди ожидающих подключений (backlog)
    server_socket.listen(5) 
    print("TCP Server слушает на порту 8080...")

    while True:
        # accept() - БЛОКИРУЮЩИЙ вызов.
        # Код останавливается здесь, пока не придет клиент.
        # Возвращает НОВЫЙ объект сокета (client_sock) специально для этого клиента
        # и адрес клиента (addr).
        client_sock, addr = server_socket.accept()
        print(f"Подключение от: {addr}")
        
        # Получаем данные (макс 1024 байта)
        data = client_sock.recv(1024)
        print(f"Получено: {data.decode('utf-8')}")
        
        # Отправляем ответ
        client_sock.sendall(b"TCP Message Received!")
        
        # В TCP соединение нужно закрывать
        client_sock.close()

# Запуск (в реальности нужен if __name__ == '__main__':)
# start_tcp_server()

TCP Client:

import socket

def run_tcp_client():
    client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    
    # КЛЮЧЕВОЕ ОТЛИЧИЕ: connect() инициирует 3-way handshake.
    # SYN -> SYN-ACK -> ACK
    client_socket.connect(('127.0.0.1', 8080))
    
    client_socket.sendall(b"Hello TCP World")
    
    response = client_socket.recv(1024)
    print(f"Сервер ответил: {response.decode('utf-8')}")
    
    client_socket.close()

Пример 2: UDP Сервер и Клиент

Здесь нет listen, accept или connect.

UDP Server:

import socket

def start_udp_server():
    # SOCK_DGRAM = UDP
    server_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    server_socket.bind(('127.0.0.1', 9090))
    
    print("UDP Server готов принимать данные...")

    while True:
        # recvfrom() получает данные и адрес отправителя мгновенно.
        # Нет создания отдельного канала связи.
        data, addr = server_socket.recvfrom(1024)
        print(f"Пришло от {addr}: {data.decode('utf-8')}")
        
        # Отправляем ответ (sendto) конкретному адресу.
        server_socket.sendto(b"UDP ACK", addr)

UDP Client:

import socket

def run_udp_client():
    client_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    
    # НЕТ connect(). Мы просто кидаем данные в "пустоту" по адресу.
    # Если сервера нет, ошибка может даже не вернуться (зависит от настроек сети).
    msg = b"Hello UDP World"
    client_socket.sendto(msg, ('127.0.0.1', 9090))
    
    # Ждем ответ (с таймаутом, т.к. гарантий нет)
    client_socket.settimeout(2)
    try:
        data, server = client_socket.recvfrom(1024)
        print(f"Ответ: {data.decode('utf-8')}")
    except socket.timeout:
        print("Ответ не пришел (пакет потерян или сервер лежит)")
    
    client_socket.close()

3. DNS (Domain Name System)

DNS — это телефонная книга интернета. Она превращает google.com (человеческое имя) в 142.250.185.78 (машинный IP-адрес).

В System Design DNS важен по трем причинам:

  1. Latency: DNS-запрос занимает время (20-100+ мс).
  2. Geo-Routing: DNS может вернуть разный IP пользователю из США и пользователю из РФ (ближайший дата-центр).
  3. Availability: Если DNS упал — ваш сервис недоступен, даже если сервера работают.

Как происходит резолвинг (Resolution Flow)

Когда вы вводите www.example.com в браузере:

  1. Browser Cache: Браузер проверяет, заходил ли я сюда недавно? Если да — берет IP из памяти.
  2. OS Cache (hosts file): Операционная система проверяет свой кэш.
  3. Resolver (ISP): Запрос уходит провайдеру интернета (или Google DNS 8.8.8.8).
  4. Root Server (.): Резолвер спрашивает корневой сервер: "Кто отвечает за зону .com?"
  5. TLD Server (.com): Корневой отправляет к серверу доменной зоны .com. Тот говорит: "Информацию о example.com знает сервер ns1.example.com".
  6. Authoritative Name Server: Резолвер идет к ns1 и получает финальный IP.

Типы записей (Records)

  • A (Address): Имя -> IPv4 (example.com -> 1.2.3.4).
  • AAAA: Имя -> IPv6.
  • CNAME (Canonical Name): Псевдоним. m.google.com -> mobile.google.com.
    • Важно: CNAME требует дополнительного DNS-запроса (резолвинг псевдонима в реальное имя, потом имени в IP). Это чуть медленнее.
  • MX (Mail Exchange): Куда слать почту.
  • NS (Name Server): Кто обслуживает этот домен.

TTL (Time To Live)

Это время, которое запись живет в кэше (у провайдера, в ОС).

  • Высокий TTL (24 часа): Меньше нагрузки на DNS сервера, быстрее ответ для юзера. Но если вы сменили IP сервера, юзеры сутки будут стучаться на старый адрес.
  • Низкий TTL (60 сек): Быстрая смена адресов (полезно для Failover), но выше нагрузка и latency.

Глава 1. Основы сетевого взаимодействия (Часть 2)


Здесь мы разберем эволюцию HTTP, шифрование (HTTPS) и основные стили проектирования API. Это "язык", на котором говорят ваши микросервисы и фронтенд.

1. Эволюция HTTP: Почему все меняется?

HTTP (HyperText Transfer Protocol) — это протокол прикладного уровня (L7).

HTTP/1.1 (Старый добрый стандарт)

  • Текстовый формат: Данные передаются как текст (можно читать глазами через Sniffer).
  • Keep-Alive: Позволяет использовать одно TCP-соединение для нескольких запросов (раньше для каждой картинки открывался новый сокет).
  • Проблема: Head-of-Line (HOL) Blocking на уровне приложения. Если вы отправили запрос на картинку, а следом на CSS, и сервер завис на генерации картинки — CSS не загрузится, пока не придет картинка. Браузеры обходят это, открывая 6 параллельных TCP-соединений к одному домену.

HTTP/2 (Революция мультиплексирования)

  • Бинарный протокол: Эффективнее парсится машинами, не читаем для человека без декодера.
  • Multiplexing (Мультиплексирование): Одно TCP-соединение на всё. Запросы разбиваются на фреймы и летят вперемешку. Если картинка тормозит, CSS пролетит в соседних фреймах.
  • Header Compression (HPACK): Заголовки (User-Agent, Cookies) сжимаются, экономя трафик.
  • Проблема: HOL Blocking на уровне TCP. Если потерялся один TCP-пакет, операционная система тормозит все потоки внутри этого соединения, пока пакет не будет перепослан.

HTTP/3 (QUIC — Будущее уже здесь)

  • Работает поверх UDP: Да, мы отказались от TCP, чтобы сделать свой надежный транспорт.
  • Решение проблем: Если потерялся пакет одного потока (стрима), остальные потоки продолжают работать.
  • Быстрый старт: Соединение устанавливается почти мгновенно (0-RTT или 1-RTT).
Характеристика HTTP/1.1 HTTP/2 HTTP/3 (QUIC)
Транспорт TCP TCP UDP
Параллелизм 6 соединений 1 соединение (Мультиплекс) 1 соединение (Мультиплекс)
Формат Текст Бинарный Бинарный
HOL Blocking Есть (App level) Есть (TCP level) Нет

2. HTTPS и TLS: Цена безопасности

HTTPS = HTTP + TLS (Transport Layer Security).

В System Design важно понимать, что шифрование стоит ресурсов (CPU) и времени (Latency).

TLS Handshake (Рукопожатие)

Прежде чем отправить байт данных, клиент и сервер должны договориться о шифре.

  1. Client Hello: "Я умею шифровать алгоритмами А и Б".
  2. Server Hello: "Выбираем А. Вот мой Сертификат (с Публичным ключом)".
  3. Key Exchange: Клиент проверяет сертификат, генерирует сессионный ключ, шифрует его публичным ключом сервера и отправляет серверу.
  4. Secure Connection: Теперь у обоих есть симметричный ключ, и они общаются шифрованно.

Важный паттерн: TLS Termination (SSL Offloading) Расшифровка трафика — тяжелая операция для CPU. В высоконагруженных системах её не делают на серверах приложений (Java/Python/Go). Решение: Ставят мощный Load Balancer (Nginx/HAProxy) или Cloud Load Balancer на входе. Он расшифровывает трафик (HTTPS) и передает его бэкенду уже по HTTP (внутри защищенного контура дата-центра). Это снимает нагрузку с бэкенда.

3. API Design: Как сервисам "говорить"

Выбор стиля API определяет производительность и удобство разработки.

A. REST (Representational State Transfer)

Самый популярный подход. Всё есть "Ресурс".

  • Методы: GET (читать), POST (создать), PUT (обновить полностью), PATCH (обновить частично), DELETE.
  • Stateless: Сервер не хранит состояние клиента между запросами.
  • Проблема: Over-fetching (получаем лишние поля) и Under-fetching (нужно делать 3 запроса, чтобы собрать данные пользователя, его посты и комментарии).

Пример (Python/Flask):

from flask import Flask, jsonify, request

app = Flask(__name__)

# База данных в памяти
users = {
    1: {"id": 1, "name": "Alice", "role": "admin"},
    2: {"id": 2, "name": "Bob", "role": "user"}
}

# GET - Получение ресурса
@app.route('/users/<int:user_id>', methods=['GET'])
def get_user(user_id):
    user = users.get(user_id)
    if user:
        return jsonify(user), 200 # 200 OK
    return jsonify({"error": "User not found"}), 404 # 404 Not Found

# POST - Создание ресурса
@app.route('/users', methods=['POST'])
def create_user():
    data = request.json
    new_id = len(users) + 1
    # Валидация данных должна быть здесь
    users[new_id] = {"id": new_id, "name": data['name'], "role": "user"}
    
    # 201 Created + заголовок Location
    return jsonify(users[new_id]), 201 

if __name__ == '__main__':
    app.run(port=5000)

B. GraphQL

Решает проблему гибкости выборки данных. Клиент сам говорит, что ему нужно.

  • Один Endpoint: Обычно /graphql (POST).
  • Схема: Строгая типизация.
  • Минус: Сложнее кэшировать (так как все запросы POST и разные тела), можно легко "положить" базу сложным вложенным запросом.

Сравнение запроса: REST: GET /users/1 -> Возвращает ВСЁ (id, name, email, address, history...) GraphQL:

query {
  user(id: 1) {
    name  # Я прошу ТОЛЬКО имя. Экономия трафика.
  }
}

C. RPC (gRPC / Protocol Buffers)

Используется для общения между микросервисами (Internal traffic).

  • Идея: Вызов удаленной процедуры выглядит как вызов локальной функции getUser(id).
  • Protocol Buffers: Бинарный формат сериализации от Google. Данные весят в разы меньше JSON и сериализуются быстрее.
  • Транспорт: Работает поверх HTTP/2.

Когда использовать:

  • REST/GraphQL: Для общения с фронтендом (Public API).
  • gRPC: Для общения микросервисов друг с другом (низкая задержка, компактность).

D. WebSockets

Для Real-time приложений (чаты, биржевые котировки, игры).

  • Принцип: Клиент отправляет HTTP-запрос с заголовком Upgrade: websocket. Сервер соглашается, и соединение превращается в постоянный двусторонний канал TCP.

Пример (Python/websockets):

# Асинхронный сервер
import asyncio
import websockets

connected_clients = set()

async def chat_handler(websocket):
    # Регистрация клиента
    connected_clients.add(websocket)
    try:
        async for message in websocket:
            # Получили сообщение от одного, рассылаем всем (Broadcasting)
            print(f"Received: {message}")
            for client in connected_clients:
                if client != websocket: # Не отправляем самому себе
                    await client.send(f"New msg: {message}")
    except websockets.exceptions.ConnectionClosed:
        pass
    finally:
        # Клиент отключился
        connected_clients.remove(websocket)

async def main():
    async with websockets.serve(chat_handler, "localhost", 8765):
        await asyncio.Future()  # run forever

# Запуск: asyncio.run(main())

Итог Главы 1

Мы прошли путь от кабеля до API.

  1. L4 vs L7: Понимаем разницу между пересылкой пакетов и логикой приложения.
  2. TCP/UDP: Надежность против скорости.
  3. DNS: Как работает адресация.
  4. HTTP/HTTPS: Эволюция протоколов и шифрование.
  5. API: REST (стандарт), GraphQL (гибкость), gRPC (скорость внутри), WebSockets (реал-тайм).

Глава 2. Базы данных: SQL и NoSQL


1. Реляционные СУБД (RDBMS / SQL)

Примеры: PostgreSQL, MySQL, Oracle, MS SQL.

Это классика. Данные хранятся в строгих таблицах (строки и столбцы), связанных между собой (Relations).

Главная суперсила: ACID

Если вы строите банковскую систему, вам нужна гарантия, что деньги не исчезнут в воздухе. Эту гарантию дает стандарт ACID.

  1. A — Atomicity (Атомарность): "Всё или ничего". Транзакция (группа операций) либо выполняется целиком, либо не выполняется вовсе.
    • Пример: Перевод денег. Снять со счета А и зачислить на счет Б. Если "снять" удалось, а на "зачислить" сервер упал — база данных отменит (Rollback) снятие. Деньги вернутся.
  2. C — Consistency (Согласованность): Данные всегда соответствуют правилам (constraints). Нельзя записать текст в поле для даты или нарушить уникальность ID.
  3. I — Isolation (Изолированность): Параллельные транзакции не должны мешать друг другу.
    • Проблема: Если два юзера одновременно покупают последний билет, база должна выстроить их в очередь.
  4. D — Durability (Долговечность): Если база сказала "ОК" (commit), значит данные записаны на жесткий диск. Даже если через миллисекунду выключат электричество, данные выживут.

Как работают Индексы (B-Tree)

Почему поиск в базе быстрый? Без индекса базе пришлось бы читать всю таблицу (Full Table Scan) — это $O\left(N\right)$ . С индексом это $O\left(\log N\right)$ .

Самая популярная структура данных для индексов в SQL — B-Tree (Сбалансированное дерево).

  • Как это работает: Данные отсортированы в дерево. Чтобы найти число 50, мы идем от корня: 50 больше 30? Идем направо. Меньше 70? Идем налево.
  • Цена индекса:
    • Чтение: Очень быстро.
    • Запись: Медленнее. При каждой вставке (INSERT) нужно перебалансировать дерево индексов.
    • Место: Индексы занимают место на диске.

2. NoSQL (Not Only SQL)

Примеры: MongoDB, Redis, Cassandra, Neo4j.

NoSQL возникли, когда данные стали слишком большими (Big Data) или слишком разнообразными для таблиц. Они часто жертвуют ACID ради скорости и масштабируемости.

Существует 4 основных типа NoSQL. В интервью нужно четко знать, какую когда брать.

A. Key-Value (Ключ-Значение)

  • Пример: Redis, Memcached, DynamoDB.
  • Суть: Огромная хэш-таблица. Есть ключ (ID) — есть значение (blob).
  • Сценарий: Кэширование, сессии пользователей, корзина покупок.
  • Скорость: Самая высокая ( $O\left(1\right)$ ).

B. Document (Документоориентированные)

  • Пример: MongoDB, CouchDB.
  • Суть: Храним данные в формате JSON (BSON). Нет жесткой схемы. У одного юзера есть поле "хобби", у другого нет — и это нормально.
  • Сценарий: CMS, каталоги товаров (где у утюга и ноутбука разные характеристики), профили пользователей.

C. Column-Family (Колоночные / Wide-Column)

  • Пример: Cassandra, HBase.
  • Суть: Данные хранятся не по строкам, а по колонкам. Оптимизировано под огромную скорость записи и чтение больших объемов данных.
  • Сценарий: Логи, история чатов (Discord использует Cassandra), метрики, IoT.
  • Архитектура: Обычно используют LSM-Tree (Log-Structured Merge-tree) вместо B-Tree, что позволяет писать данные последовательно, не прыгая по диску.

D. Graph (Графовые)

  • Пример: Neo4j.
  • Суть: Хранят узлы и связи между ними как физические ссылки.
  • Сценарий: Социальные сети ("друзья друзей"), рекомендательные системы, построение маршрутов. SQL очень плох в связях "многие-ко-многим" на большую глубину (слишком много JOIN-ов).

3. SQL vs NoSQL: Что выбрать?

Это классический вопрос на System Design Interview.

Характеристика SQL (RDBMS) NoSQL
Структура данных Строгая схема (Schema-on-write). Сначала создай таблицу, потом пиши. Гибкая схема (Schema-on-read). Пиши что угодно.
Связи (Relations) Отлично (JOINs). Плохо или отсутствуют. JOIN приходится делать в коде приложения.
Масштабирование Вертикальное (Scale Up): Нужен сервер мощнее (больше RAM/CPU). Горизонтально (Sharding) — сложно. Горизонтальное (Scale Out): Легко добавить 10 дешевых серверов. Изначально созданы для кластеров.
Транзакции ACID (строгие). BASE (Basically Available, Soft state, Eventual consistency). Часто жертвуют мгновенной согласованностью.
Оптимально для Финансы, биллинг, структурированные данные, сложные отчеты. Big Data, Real-time аналитика, контент, быстро меняющиеся данные.

4. Практика: Код

Посмотрим, как отличается работа с данными на уровне кода (Python).

Пример SQL (SQLite)

Мы обязаны сначала определить структуру.

import sqlite3

# Подключение к файловой БД
conn = sqlite3.connect('example.db')
cursor = conn.cursor()

# 1. Жесткое создание схемы
cursor.execute('''
    CREATE TABLE IF NOT EXISTS users (
        id INTEGER PRIMARY KEY,
        username TEXT NOT NULL,
        email TEXT
    )
''')

# 2. Транзакция
try:
    # Вставка данных
    cursor.execute("INSERT INTO users (username, email) VALUES ('alice', 'alice@ya.ru')")
    
    # Симуляция ошибки: пытаемся вставить строку вместо числа (если бы не было проверок драйвера)
    # или нарушить UNIQUE constraint.
    
    # Фиксация изменений (Commit)
    conn.commit()
    print("User Alice saved securely.")
except Exception as e:
    # Откат изменений при ошибке (Rollback)
    conn.rollback()
    print(f"Error: {e}. Transaction rolled back.")

conn.close()

Пример NoSQL (MongoDB style - псевдокод)

Мы можем менять структуру на лету.

# Представим, что мы используем pymongo
# db = client.my_database

user_doc_1 = {
    "username": "bob",
    "email": "bob@gmail.com",
    "age": 25
}

# Вставка первого документа
# db.users.insert_one(user_doc_1)

# Вставка второго документа СОВСЕМ ДРУГОЙ СТРУКТУРЫ
user_doc_2 = {
    "username": "charlie",
    "skills": ["python", "system design"], # Массив! В SQL нужна была бы отдельная таблица.
    "address": { # Вложенный объект!
        "city": "Moscow",
        "zip": 123456
    }
}

# MongoDB съест это без проблем
# db.users.insert_one(user_doc_2)

Итог Главы 2

Мы научились хранить данные.

  1. ACID — это про надежность (SQL).
  2. B-Tree — это про быстрый поиск в SQL.
  3. NoSQL — это про гибкость и масштаб.
  4. Если данные связаны и важна транзакционность — берем PostgreSQL.
  5. Если данные — это огромный поток логов или гибкие документы — смотрим в сторону Cassandra или MongoDB.

Глава 3. Масштабирование и Балансировка


1. Вертикальное vs Горизонтальное масштабирование

Когда ваш сервер начинает "захлебываться" (CPU 100%, RAM заканчивается), у вас есть два пути.

А. Vertical Scaling (Scale Up) — "Вертикальное"

Вы покупаете сервер мощнее. Заменяете 4 ядра на 64, 8 ГБ RAM на 1 ТБ.

  • Плюсы:
    • Простота. Не нужно менять код.
    • Не нужны сложные настройки сети.
  • Минусы:
    • Hard Limit: У любого железа есть потолок. Нельзя купить процессор с бесконечной частотой.
    • SPOF (Single Point of Failure): Единая точка отказа. Если этот супер-сервер сгорит, вы оффлайн.
    • Цена: Мощное железо стоит экспоненциально дороже.

Б. Horizontal Scaling (Scale Out) — "Горизонтальное"

Вы покупаете много дешевых серверов и объединяете их в кластер.

  • Плюсы:
    • Бесконечный рост: Нужно больше мощности? Добавь еще 10 машин.
    • Отказоустойчивость: Сгорел один сервер? Остальные 9 подхватят нагрузку.
  • Минусы:
    • Сложность: Нужно управлять множеством машин.
    • Распределенность: Данные теперь лежат в разных местах (проблема консистентности).

В System Design мы почти всегда стремимся к Горизонтальному масштабированию.

2. Главное условие: Stateless Архитектура

Чтобы горизонтальное масштабирование работало, ваши сервера приложений должны быть Stateless (без сохранения состояния).

  • Stateful (Плохо для скейлинга): Сервер А хранит сессию пользователя (логин, корзину) у себя в оперативной памяти. Если следующий запрос пользователя придет на Сервер Б, тот не узнает пользователя.
  • Stateless (Хорошо): Сервер не хранит данные о клиенте между запросами.
    • Вариант 1: Данные передаются в каждом запросе (например, JWT токен).
    • Вариант 2: Данные хранятся во внешнем хранилище (Shared State), например, в Redis или Базе Данных. Любой сервер может обратиться к Redis и узнать состояние пользователя.

3. Балансировщик нагрузки (Load Balancer)

Когда у вас 10 серверов, кто-то должен решать, на какой из них отправить пришедшего пользователя. Этим занимается Load Balancer (LB).

Он стоит на входе в вашу систему (как регулировщик) и распределяет трафик.

Типы балансировщиков (по реализации)

  1. Software (Программные): Nginx, HAProxy. Дешево, гибко, ставится на обычные сервера. Самый частый выбор.
  2. Hardware (Аппаратные): F5, Citrix. Специальные "железные коробки". Невероятно быстрые, но очень дорогие.
  3. Cloud: AWS ELB/ALB, Google Cloud Load Balancing. Облака делают всё за вас.

Алгоритмы балансировки

Как LB выбирает сервер?

  1. Round Robin (Карусель):
    • Запрос 1 -> Сервер А
    • Запрос 2 -> Сервер Б
    • Запрос 3 -> Сервер В
    • Запрос 4 -> Сервер А
    • Плюс: Простота. Минус: Не учитывает нагрузку (может отправить тяжелый запрос на уже загруженный сервер).
  2. Least Connections (Наименьшее число соединений):
    • LB смотрит, у кого сейчас меньше всего активных соединений, и шлет туда.
    • Идеально для: Долгих сессий (WebSocket, стриминг).
  3. IP Hash:
    • Берется IP пользователя, хэшируется, и результат привязывается к серверу.
    • $$ ServerIndex=hash\left(ClientIP\right)%N $$
    • Эффект: Пользователь Вася (IP 1.2.3.4) всегда попадает на Сервер А. Это называется Sticky Session.

4. Health Checks (Проверка здоровья)

Балансировщик должен быть умным. Если Сервер Б "умер" (завис, сгорел диск), LB должен перестать слать туда трафик.

  • Passive Check: Если LB отправил запрос на сервер, а тот ответил ошибкой (500) или не ответил (Timeout), LB помечает его как "больной".
  • Active Check: LB сам периодически (раз в 5 сек) пингует специальный endpoint (/health), чтобы убедиться, что сервер жив.

5. Практика: Настройка Nginx и Алгоритм

В реальной жизни вы редко пишете балансировщик на Python, вы настраиваете Nginx. Но чтобы понять логику, давайте посмотрим на оба варианта.

А. Логика Round Robin на Python

Представьте, что это код внутри балансировщика.

import itertools

class LoadBalancer:
    def __init__(self, servers):
        self.servers = servers
        # itertools.cycle создает бесконечный итератор: [A, B, C, A, B, C...]
        self._cycle = itertools.cycle(servers)

    def get_server(self):
        # next() берет следующий элемент и сдвигает указатель
        return next(self._cycle)

# Список IP наших бэкендов
backend_servers = ["10.0.0.1", "10.0.0.2", "10.0.0.3"]
lb = LoadBalancer(backend_servers)

# Симуляция прихода 5 запросов
for request_id in range(1, 6):
    server = lb.get_server()
    print(f"Request {request_id} -> sent to {server}")

Б. Реальный конфиг Nginx (Layer 7 Load Balancing)

Это то, что вы увидите в продакшене.

http {
    # Группа серверов (наши бэкенды)
    upstream my_backend_app {
        # По умолчанию используется Round Robin
        # Можно добавить weight=2, чтобы сервер получал в 2 раза больше запросов (если он мощнее)
        server 10.0.0.1:8080;
        server 10.0.0.2:8080;
        
        # Если сервер не отвечает 3 раза подряд (max_fails) в течение 30 сек,
        # он считается мертвым.
        server 10.0.0.3:8080 max_fails=3 fail_timeout=30s;
    }

    server {
        listen 80;
        server_name example.com;

        location / {
            # Проксируем запрос на группу 'my_backend_app'
            proxy_pass http://my_backend_app;
            
            # Передаем реальный IP пользователя, иначе бэкенд будет видеть IP балансировщика
            proxy_set_header X-Real-IP $remote_addr;
        }
    }
}

Важный нюанс: Когда вы ставите балансировщик, ваши сервера приложений перестают видеть реальный IP пользователя (они видят IP балансировщика). Поэтому критически важно передавать заголовок X-Forwarded-For или X-Real-IP, как в примере выше.

Итог Главы 3

Теперь наша архитектура выглядит так:

  1. ПользователиDNSLoad Balancer (Nginx).
  2. Load Balancer распределяет нагрузку между Кластером серверов приложений (Stateless).
  3. Сервера приложений сохраняют данные в Кластер Баз Данных (Primary/Replica) и кэш в Redis.

Уровень 2: Компоненты системы

=============================

Глава 1. Кэширование (Caching)


1. Зачем нам кэш? Физика процесса

Кэширование — это способ обменять емкость памяти на скорость. Базы данных (MySQL/PostgreSQL) хранят данные на диске (SSD/HDD). Диск — это надежно, но медленно. Кэш (Redis/Memcached) хранит данные в оперативной памяти (RAM). RAM — это ненадежно (выключил свет — данные пропали), но экстремально быстро.

  • Чтение с диска (SSD): ~1-4 миллисекунды.
  • Чтение из RAM: ~100-200 наносекунд.
  • Разница: В тысячи раз.

2. Уровни кэширования

В System Design интервью важно уточнять, где именно вы кэшируете.

  1. Client-Side (Браузер): HTTP заголовки (Cache-Control: max-age=3600). Браузер даже не отправляет запрос на сервер, если картинка закэширована.
  2. CDN (Content Delivery Network): Кэширование статики (картинки, JS, видео) на серверах, географически близких к пользователю.
  3. Load Balancer / Reverse Proxy: Nginx может кэшировать ответы бэкенда на короткое время.
  4. Distributed Cache (Распределенный кэш): Redis или Memcached. Именно здесь происходит основная магия бэкенда.
  5. Database Caching: У базы данных есть свой внутренний буфер (Buffer Pool), куда она складывает "горячие" данные.

3. Стратегии кэширования (Caching Patterns)

Как именно приложение взаимодействует с кэшем и БД? Это ключевой вопрос архитектуры.

A. Cache-Aside (Lazy Loading) — Самый популярный

Приложение "лениво" загружает данные в кэш.

  1. Приложение получает запрос на данные (например, user_id=1).
  2. Приложение идет в Кэш.
    • Hit: Данные есть? Отлично, возвращаем пользователю.
    • Miss: Данных нет? Приложение идет в Базу Данных.
  3. Приложение читает из БД, записывает результат в Кэш и возвращает пользователю.
  • Плюсы: В кэше лежит только то, что реально запрашивают. Если Redis упадет, система продолжит работать (просто медленнее, напрямую с БД).
  • Минусы: Первый запрос всегда медленный (нужно сходить в БД). Данные в кэше могут устареть, если их изменили в БД напрямую.

B. Write-Through (Сквозная запись)

Приложение пишет данные в кэш, а кэш синхронно пишет их в БД.

  1. Приложение сохраняет данные в Кэш.
  2. Кэш сохраняет данные в БД.
  3. Возвращаем "ОК" только когда записалось и туда, и туда.
  • Плюсы: В кэше всегда свежие данные (Strong Consistency). Нет риска потери данных.
  • Минусы: Запись медленная (ждем самую медленную часть — БД).

C. Write-Back (Write-Behind)

Приложение пишет только в кэш и сразу говорит "ОК". Кэш асинхронно (в фоне) сбрасывает данные в БД.

  • Плюсы: Мгновенная запись.
  • Минусы: Риск потери данных. Если кэш упадет до того, как успеет сохранить данные на диск, они исчезнут навсегда. Используется для некритичных данных (например, счетчик лайков).

4. Политики вытеснения (Eviction Policies)

Кэш (RAM) дорог и ограничен. Когда память заканчивается, нужно что-то удалить, чтобы записать новое. Как выбрать жертву?

  1. LRU (Least Recently Used): Удаляем то, к чему дольше всего не обращались. Самый популярный алгоритм. Основан на гипотезе: если данные нужны были недавно, они понадобятся снова.
  2. LFU (Least Frequently Used): Удаляем то, к чему обращаются реже всего (счетчик популярности).
  3. FIFO (First In, First Out): Удаляем самое старое, неважно, насколько оно популярно.

5. Проблема: Cache Invalidation (Инвалидация)

Как сделать так, чтобы данные в кэше не протухали? Пример: Мы поменяли цену товара в БД с $10 на $20, а в кэше все еще лежит $10. Пользователь видит старую цену.

Методы решения:

  1. TTL (Time To Live): Данные автоматически удаляются через N секунд (например, 5 минут). Это "Eventual Consistency".
  2. Explicit Deletion (Явное удаление): При обновлении данных в БД приложение обязано удалить ключ из кэша.
    • Совет: Лучше удалять ключ (delete), чем обновлять его (set). Это избавляет от гонок (Race Conditions).

6. Практика: Реализация Cache-Aside на Python + Redis

В Python для работы с кэшем часто используют паттерн Декоратор. Это позволяет добавить кэширование к любой функции, не меняя её код.

Представим, что у нас есть медленная функция get_user_profile_from_db.

import redis
import time
import json
from functools import wraps

# Подключение к Redis (локально или в Docker)
# decode_responses=True позволяет получать строки вместо байтов
r = redis.Redis(host='localhost', port=6379, decode_responses=True)

def cache_response(ttl_seconds=60):
    """
    Декоратор для Cache-Aside паттерна.
    :param ttl_seconds: Время жизни кэша (Time To Live)
    """
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            # 1. Генерируем уникальный ключ для кэша на основе аргументов функции
            # Например: "user_profile:123"
            cache_key = f"{func.__name__}:{args[0]}" 
            
            # 2. Пытаемся прочитать из Redis (HIT)
            cached_value = r.get(cache_key)
            if cached_value:
                print(f"[CACHE HIT] Читаем из памяти: {cache_key}")
                # Десериализуем JSON обратно в словарь
                return json.loads(cached_value)
            
            # 3. Если в кэше нет (MISS), выполняем реальную функцию (идем в БД)
            print(f"[CACHE MISS] Идем в медленную БД...")
            result = func(*args, **kwargs)
            
            # 4. Сохраняем результат в Redis с TTL
            # Сериализуем словарь в JSON строку
            r.setex(
                name=cache_key,
                time=ttl_seconds,
                value=json.dumps(result)
            )
            
            return result
        return wrapper
    return decorator

# --- Имитация работы приложения ---

# Симуляция БД
database = {
    1: {"name": "Alice", "balance": 100},
    2: {"name": "Bob", "balance": 500}
}

@cache_response(ttl_seconds=5) # Кэш живет 5 секунд
def get_user_from_db(user_id):
    # Имитация долгого запроса
    time.sleep(2) 
    return database.get(user_id)

# --- Тест ---

print("--- Запрос 1 (Кэш пуст) ---")
start = time.time()
print(get_user_from_db(1)) 
print(f"Время: {time.time() - start:.2f} сек\n")

print("--- Запрос 2 (Данные в кэше) ---")
start = time.time()
print(get_user_from_db(1))
print(f"Время: {time.time() - start:.2f} сек (Мгновенно!)\n")

print("--- Ждем 6 секунд (TTL истекает)... ---")
time.sleep(6)

print("--- Запрос 3 (Кэш протух) ---")
start = time.time()
print(get_user_from_db(1))
print(f"Время: {time.time() - start:.2f} сек")

Нюансы кода:

  1. Сериализация: Redis хранит строки или байты. Сложные объекты (dict, list) нужно превращать в JSON или Pickle.
  2. Генерация ключа: Ключ должен быть уникальным для каждого набора аргументов. user:1 и user:2 — это разные записи.
  3. setex: Атомарная операция "записать + поставить таймер". Очень важно использовать её, а не две команды (set потом expire), чтобы не случилось так, что запись создалась, а таймер не поставился (и запись осталась навечно).

7. Особые проблемы кэширования (Advanced)

На собеседованиях Senior уровня спрашивают про эти сценарии сбоев:

  • Cache Penetration (Пробой кэша): Злоумышленники запрашивают ключи, которых не существует ни в кэше, ни в БД (например, id=-1). Запросы постоянно пролетают сквозь кэш и бьют по БД.
    • Решение: Кэшировать "пустой ответ" (null) с коротким TTL или использовать Bloom Filter.
  • Cache Avalanche (Лавина кэша): Множество ключей протухают одновременно (например, вы поставили всем TTL ровно 1 час). В 12:00 кэш пустеет, и БД падает от нагрузки.
    • Решение: Добавлять "Jitter" (случайный разброс) к TTL. Не 60 минут, а 60 минут ± 5 минут.
  • Thundering Herd (Эффект толпы): Один очень популярный ключ (например, главная новость) протухает. 10,000 пользователей одновременно видят MISS и одновременно идут в БД за одной и той же записью.
    • Решение: Механизм блокировок (Mutex). Только один процесс идет обновлять кэш, остальные ждут.

Итог Главы 1 (Уровень 2)

Кэш — это лучший друг производительности.

  1. Мы используем Cache-Aside для большинства задач чтения.
  2. Мы используем Redis.
  3. Мы помним про TTL и стратегию LRU.
  4. Мы боимся "Лавины" и добавляем случайность во время жизни кэша.

Глава 2. Асинхронность и Очереди сообщений


1. Синхронное vs Асинхронное взаимодействие

  • Синхронно (Request-Response): Я звоню вам по телефону. Я жду на линии, пока вы не поднимете трубку. Если вы заняты, я вишу и жду.
    • Пример: REST API запрос POST /buy.
  • Асинхронно (Fire-and-Forget): Я отправляю вам email. Я нажимаю "Отправить" и иду заниматься своими делами. Вы прочитаете, когда сможете.
    • Пример: Очереди сообщений.

В System Design мы используем очереди, чтобы развязать (decouple) компоненты. Фронтенд не должен зависеть от скорости работы бэкенда по обработке видео.

2. Основные компоненты

  1. Producer (Производитель): Тот, кто создает задачу (например, Веб-сервер).
  2. Message Broker (Брокер): "Почтовое отделение". Сервер, который принимает сообщения, хранит их и отдает. Самые популярные: RabbitMQ, Kafka, AWS SQS.
  3. Queue (Очередь): Буфер внутри брокера, где лежат сообщения.
  4. Consumer (Потребитель / Воркер): Тот, кто разгребает завалы. Фоновый процесс, который берет задачу из очереди и выполняет её.

3. Модели доставки сообщений

А. Point-to-Point (Очередь задач / Worker Queue)

  • Сценарий: У нас 100 задач и 5 воркеров.
  • Логика: Одно сообщение доставляется ровно одному потребителю. Воркеры конкурируют за задачи. Это способ балансировки нагрузки для тяжелых задач.
  • Пример: RabbitMQ (Direct exchange). Обработка заказов, ресайз картинок.

Б. Publish / Subscribe (Pub/Sub)

  • Сценарий: Пользователь загрузил видео. Нужно: 1) Сжать видео, 2) Обновить поиск, 3) Отправить пуш подписчикам.
  • Логика: Одно сообщение копируется во все подписанные очереди. Producer не знает, кто его слушает.
  • Пример: RabbitMQ (Fanout exchange), Kafka (Consumer Groups).

4. Великая битва: RabbitMQ vs Apache Kafka

На собеседовании это обязательный вопрос. Это совершенно разные звери.

Характеристика RabbitMQ (Traditional Queue) Apache Kafka (Event Streaming)
Модель Smart Broker, Dumb Consumer. Брокер сам следит, кому отдал сообщение, и удаляет его после обработки. Dumb Broker, Smart Consumer. Брокер — это просто лог (журнал), который хранит всё подряд. Потребитель сам помнит, на какой строчке он остановился (Offset).
Хранение данных Сообщение удаляется сразу после подтверждения (ACK). Очередь должна быть пустой. Сообщения хранятся днями или неделями (Retention Policy). Можно "перемотать" время назад и обработать заново.
Производительность 10к - 50к сообщений/сек. Миллионы сообщений/сек (Высокая пропускная способность).
Сценарий Сложная маршрутизация, задачи "сделай и удали" (email, PDF). Сбор логов, аналитика кликов, Event Sourcing, стриминг данных.

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

  • Нужна сложная логика маршрутизации или задача должна исчезнуть после выполнения? -> RabbitMQ.
  • Нужно переваривать огромный поток данных (Big Data) или хранить историю событий? -> Kafka.

5. Проблемы надежности (Resiliency Patterns)

В очередях, как и в сети, всё может сломаться. Воркер может упасть прямо во время обработки задачи.

A. Acknowledgements (ACK / Подтверждения)

Как брокер узнает, что воркер не просто взял задачу, а успешно выполнил её?

  1. Брокер выдает задачу воркеру.
  2. Воркер делает работу.
  3. Воркер отправляет ACK брокеру.
  4. Только тогда брокер удаляет сообщение из очереди. Если ACK не пришел (таймаут или разрыв соединения), брокер отдает задачу другому воркеру.

B. Dead Letter Queue (DLQ)

Что если задача "битая"? Воркер берет задачу, падает с ошибкой, задача возвращается в очередь. Другой воркер берет её и тоже падает. Это "Poison Message" (ядовитое сообщение), которое может зациклить всю систему. Решение: После N неудачных попыток сообщение перекладывается в отдельную очередь Dead Letter Queue. Инженеры потом разбирают эту очередь вручную.

C. Idempotency (Идемпотентность)

Из-за механизма Retry (повторов) одно и то же сообщение может быть доставлено дважды. Пример: Воркер списал деньги, но перед отправкой ACK упал интернет. Брокер думает, что задача не сделана, и отдает её второму воркеру. Второй воркер снова списывает деньги. Решение: Операции должны быть идемпотентны.

  • Плохо: UPDATE accounts SET balance = balance - 100 (при повторе спишет 200).
  • Хорошо: Проверить transaction_id в БД перед списанием. Если такой ID уже обработан — просто вернуть OK.

6. Практика: RabbitMQ на Python (библиотека pika)

Мы реализуем классическую модель Worker Queue.

Вам понадобится запущенный RabbitMQ (обычно через Docker: docker run -p 5672:5672 rabbitmq).

Producer (Отправитель задач)

Веб-сервер, который принимает заказ.

import pika
import sys

# 1. Подключение к брокеру
connection = pika.BlockingConnection(pika.ConnectionParameters('localhost'))
channel = connection.channel()

# 2. Объявление очереди (durable=True значит, что очередь переживет перезагрузку RabbitMQ)
channel.queue_declare(queue='task_queue', durable=True)

message = "Process Order #12345 (Resize Image)"

# 3. Публикация сообщения
channel.basic_publish(
    exchange='', # Дефолтный exchange
    routing_key='task_queue',
    body=message,
    properties=pika.BasicProperties(
        delivery_mode=2,  # Сделать сообщение персистентным (сохранить на диск)
    ))

print(f" [x] Sent '{message}'")
connection.close()

Consumer (Воркер)

Фоновый скрипт. Можно запустить 5 копий этого скрипта в разных терминалах.

import pika
import time

def main():
    connection = pika.BlockingConnection(pika.ConnectionParameters('localhost'))
    channel = connection.channel()

    channel.queue_declare(queue='task_queue', durable=True)
    print(' [*] Waiting for messages. To exit press CTRL+C')

    # Логика обработки
    def callback(ch, method, properties, body):
        print(f" [x] Received {body.decode()}")
        
        # Симуляция тяжелой работы (resize, email sending)
        time.sleep(5) 
        
        print(" [x] Done")
        
        # ВАЖНО: Ручной ACK. 
        # Подтверждаем выполнение ТОЛЬКО после завершения работы.
        ch.basic_ack(delivery_tag=method.delivery_tag)

    # Fair Dispatch: Не давать воркеру больше 1 задачи за раз, пока он не сделал ACK.
    # Это предотвращает перегрузку одного воркера, пока другие простаивают.
    channel.basic_qos(prefetch_count=1)
    
    channel.basic_consume(queue='task_queue', on_message_callback=callback)

    channel.start_consuming()

if __name__ == '__main__':
    main()

Что здесь происходит:

  1. Если вы запустите два воркера и отправите 5 сообщений, RabbitMQ распределит их по кругу (Round Robin).
  2. Если вы убьете воркер (Ctrl+C) во время выполнения time.sleep, вы увидите, что сообщение не пропало! RabbitMQ заметит разрыв соединения (так как ACK не пришел) и отдаст задачу другому живому воркеру.

Итог Главы 2 (Уровень 2)

Очереди — это кровеносная система асинхронной архитектуры.

  1. RabbitMQ для задач (Task Queue), Kafka для потоков (Streaming).
  2. Всегда используйте ACK, чтобы не терять задачи.
  3. Всегда думайте об Идемпотентности, чтобы не списать деньги дважды.
  4. Очереди сглаживают пиковые нагрузки (Traffic Spikes), позволяя воркерам работать в своем темпе.

Глава 3. Прокси и CDN


1. Прокси-серверы: Посредники

В System Design часто путают Forward Proxy и Reverse Proxy. Разница фундаментальна.

A. Forward Proxy (Прямой прокси)

Действует от имени Клиента.

  • Сценарий: Вы в офисе. Корпоративный админ закрыл доступ к YouTube. Весь трафик из офиса идет через один сервер (Proxy), который фильтрует запросы.
  • Для сервера: Сервер (например, Google) не знает, кто вы. Он видит только IP офисного прокси.
  • Задачи: Обход блокировок (VPN), кэширование трафика организации, скрытие IP клиента.

B. Reverse Proxy (Обратный прокси)

Действует от имени Сервера. Это наш случай.

  • Сценарий: Пользователь заходит на google.com. Он попадает не на конкретный сервер с кодом, а на огромный входной шлюз.
  • Для клиента: Клиент не знает, что за этим прокси скрывается 10,000 серверов. Он думает, что общается с одним.
  • Примеры: Nginx, HAProxy, AWS CloudFront.

Зачем нам Reverse Proxy в архитектуре?

Даже если у вас всего один сервер с приложением (например, Node.js или Python), перед ним всегда ставят Nginx. Зачем?

  1. SSL Termination: Расшифровка HTTPS требует много CPU. Пусть Nginx делает это быстро на C++, а бэкенд получает уже чистый HTTP и занимается бизнес-логикой.
  2. Security: Бэкенд никогда не "торчит" в интернет напрямую. Прокси скрывает топологию внутренней сети.
  3. Compression: Сжатие ответов (Gzip/Brotli) делает прокси, разгружая приложение.
  4. Static Content: Отдавать картинки/CSS через Python/Java — это преступление против производительности. Nginx делает это через системный вызов sendfile() почти мгновенно.

2. CDN (Content Delivery Network)

CDN — это географически распределенная сеть серверов.

  • Origin (Источник): Ваш основной сервер, где лежат оригиналы файлов.
  • Edge Servers (Граничные сервера): Тысячи серверов, разбросанных по всему миру (у провайдеров, в точках обмена трафиком).

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

  1. Пользователь из Австралии заходит на ваш сайт.
  2. Он запрашивает картинку logo.png.
  3. DNS направляет его не на ваш сервер в Москве, а на ближайший Edge-сервер в Сиднее.
  4. Cache Miss: Если в Сиднее картинки нет, Edge скачивает её у вас (Origin), сохраняет себе и отдает пользователю.
  5. Cache Hit: Следующий пользователь из Австралии получит картинку мгновенно из памяти сервера в Сиднее.

Типы CDN: Push vs Pull

Это важный вопрос при проектировании. Как контент попадает на CDN?

Стратегия Pull (Reactive) Push (Proactive)
Принцип Edge скачивает файл с Origin только когда первый пользователь его запросил. Вы загружаете файлы на CDN заранее, до того как кто-то их попросит.
Плюсы Простота. Хранится только то, что реально нужно (экономия места). Мгновенная доступность для первого пользователя.
Минусы Первый пользователь ждет дольше (Latnecy). Возможен всплеск трафика на Origin при вирусном контенте. Нужно самому следить за загрузкой. Дороже хранить "мусор", который никто не смотрит.
Когда использовать Маленькие блоги, сайты с большим архивом старых картинок. Netflix, релиз патча для игры, ПО (когда точно известно, что качать будут все).

3. Практика: Настройка Nginx как Reverse Proxy

Давайте посмотрим на "золотой стандарт" конфигурации для высоконагруженного сервиса. Это файл nginx.conf.

Обратите внимание на комментарии — в них вся суть оптимизации.

worker_processes auto; # Использовать все ядра CPU

events {
    worker_connections 1024; # Сколько соединений держит один процесс
}

http {
    # Включаем sendfile. Это позволяет копировать данные с диска в сеть
    # прямо внутри ядра OS, не копируя их в память приложения. 
    # Критично для отдачи статики!
    sendfile on;
    
    # Сжатие Gzip (экономим трафик клиента)
    gzip on;
    gzip_types text/plain application/json text/css;

    # Определяем наш бэкенд (Load Balancing)
    upstream my_app {
        server 127.0.0.1:8000;
        server 127.0.0.1:8001;
    }

    server {
        listen 443 ssl; # Слушаем HTTPS
        server_name example.com;

        # SSL сертификаты (SSL Termination)
        ssl_certificate /etc/nginx/cert.pem;
        ssl_certificate_key /etc/nginx/key.pem;

        # 1. Отдача статики (CDN для бедных)
        # Nginx сам отдаст картинки с диска, не трогая Python/Java
        location /static/ {
            root /var/www/html;
            expires 30d; # Заголовок для браузера: "кэшируй это на 30 дней"
            add_header Cache-Control "public, no-transform";
        }

        # 2. Проксирование на приложение
        location / {
            proxy_pass http://my_app;
            
            # Передача заголовков (чтобы бэкенд знал реальный IP)
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            
            # Таймауты (если бэкенд завис, Nginx не будет ждать вечно)
            proxy_read_timeout 10s;
        }
    }
}

4. Важный нюанс: Динамический CDN

Раньше CDN кэшировали только статику (картинки, JS). Сейчас современные CDN (Cloudflare, AWS CloudFront) умеют кэшировать и динамику (HTML страницы), и даже выполнять код (Edge Computing).

  • Edge Functions (Lambda @ Edge): Вы можете написать код, который выполняется прямо на CDN сервере.
    • Пример: Проверка авторизации пользователя. Если токен протух, CDN сам отправит на логин, даже не пуская запрос к вашему основному серверу. Это колоссально снижает нагрузку.

Уровень 3: Теория распределенных систем

=======================================

Глава 1. Теоремы CAP и PACELC. Доступность (SLA).


1. CAP Теорема

Автор: Эрик Брюер (2000 год).

Теорема гласит, что распределенная система может гарантировать только два из трех свойств одновременно:

  1. C — Consistency (Согласованность):
    • Определение: Все узлы видят одни и те же данные в одно и то же время.
    • Пример: Вы положили 100 рублей на счет. Если вы тут же запросите баланс с другого сервера в другом городе, вы должны увидеть эти 100 рублей. Если сервер еще не знает об этом — он должен вернуть ошибку или ждать, но не показывать старый баланс.
  2. A — Availability (Доступность):
    • Определение: Каждый запрос получает успешный ответ (без ошибок), но без гарантии, что данные самые свежие.
    • Пример: Лента новостей. Если один сервер не синхронизировался, лучше показать старые посты, чем ошибку 500.
  3. P — Partition Tolerance (Устойчивость к разделению):
    • Определение: Система продолжает работать, даже если пропала связь между узлами (обрыв кабеля, падение свича).

Главный обман: "Выберите любые два"

В реальности выбора "любые два" нет. В распределенной системе (где серверов больше одного) сеть ненадежна. Кабель может быть перерезан. P (Partition Tolerance) — это данность, от которой нельзя отказаться.

Поэтому реальный выбор всегда стоит так: В случае аварии сети (P), что вы выберете: C или A?

CP (Consistency + Partition Tolerance)

  • Логика: "Лучше упасть, чем соврать".
  • Поведение при сбое: Если сервер А потерял связь с сервером Б, он блокирует возможность записи, чтобы не создать конфликт данных.
  • Применение: Банки, платежные системы, бронирование билетов (нельзя продать одно место двоим).
  • БД: MongoDB (по умолчанию), HBase, Redis (в некоторых конфигурациях).

AP (Availability + Partition Tolerance)

  • Логика: "Show must go on".
  • Поведение при сбое: Если связи нет, пишем локально. Потом, когда сеть починят, попробуем разрешить конфликты (merge).
  • Применение: Соцсети (лента), счетчики просмотров, корзина в Amazon (лучше дать добавить товар, чем потерять клиента).
  • БД: Cassandra, DynamoDB, CouchDB.

2. PACELC: Расширение CAP

Автор: Даниэль Абади.

CAP описывает только ситуацию аварии (Partition). Но 99% времени система работает нормально. Как она ведет себя тогда?

PACELC формулируется так:

  • Если есть P (Partition), то выбираем A или C (как в CAP).
  • E (Else) — иначе (когда сеть работает нормально):
  • Выбираем L (Latency — скорость) или C (Consistency — согласованность).

Суть: Если вы хотите мгновенную синхронизацию данных между Нью-Йорком и Лондоном (Strong Consistency), вам придется пожертвовать скоростью (Latency), так как свету нужно время, чтобы долететь. Если нужна скорость — жертвуем мгновенной согласованностью.

  • DynamoDB / Cassandra: PA/EL — При аварии выбирают доступность (A), в мирное время — низкую задержку (L).
  • BigTable / HBase: PC/EC — Всегда выбирают согласованность (C), даже ценой скорости.

3. Измерение надежности: SLA, SLO, SLI

Как менеджеру объяснить инженеру, насколько надежной должна быть система?

  1. SLI (Service Level Indicator): Что именно мы меряем?
    • Пример: Количество ошибок 500 деленное на общее число запросов.
  2. SLO (Service Level Objective): Цель, к которой мы стремимся (внутренняя метрика).
    • Пример: "99.9% запросов должны быть успешными".
  3. SLA (Service Level Agreement): Юридический контракт с клиентом (со штрафами).
    • Пример: "Если доступность упадет ниже 99.9%, мы вернем вам 10% оплаты".

Таблица "Девяток" (The Nines)

Выучите эти цифры наизусть. На интервью часто просят оценить допустимое время простоя (Downtime).

Доступность Простоев в год Простоев в день Тип системы
99% (2 девятки) 3 дня 15 часов 14.4 мин MVP, внутренние тулзы.
99.9% (3 девятки) 8.76 часов 1.44 мин Стандартный веб-сервис.
99.99% (4 девятки) 52.6 мин 8.64 сек Критические системы (Банкинг, E-commerce).
99.999% (5 девяток) 5.26 мин 0.86 сек Телеком, Медицина, Авиация.

Формула расчета доступности для последовательных компонентов (если упал любой — упало всё):

$$ A_{total}=A_{1}\times A_{2}\times ...\times A_{n} $$

Пример: Если у вас API (99.9%) зависит от БД (99.9%), общая надежность: $0.999\times 0.999=0.998$ (она падает!).

4. Паттерны отказоустойчивости (Failover)

Чтобы достичь этих "девяток", сервера дублируют.

A. Active-Passive (Master-Slave)

  • Схема: Один сервер (Active) принимает трафик, второй (Passive) просто смотрит и получает репликацию данных.
  • Heartbeat: Пассивный сервер шлет пинг ("Ты жив?"). Если Активный не ответил 3 раза — Пассивный объявляет себя главным и забирает Виртуальный IP.
  • Плюс: Простота, данные не расходятся.
  • Минус: Простой ресурсов (второй сервер простаивает). Переключение занимает время (downtime 10-60 сек).

B. Active-Active (Master-Master)

  • Схема: Оба сервера принимают трафик.
  • Плюс: Двойная производительность. Если один упал, второй просто берет на себя 100% нагрузки (мгновенно).
  • Минус: Кошмар синхронизации. Если юзер А изменил запись на сервере 1, а юзер Б ту же запись на сервере 2 — возникает конфликт. Нужны сложные алгоритмы слияния.

5. Практика: Симуляция Split Brain (Python)

Одной из главных проблем распределенных систем является Split Brain (Раздвоение личности). Это когда два сервера теряют связь и оба решают, что они — Мастер.

Давайте напишем простую симуляцию того, как кворум решает эту проблему.

import random
import time

class Node:
    def __init__(self, node_id, total_nodes):
        self.node_id = node_id
        self.total_nodes = total_nodes
        self.is_leader = False
        
    def vote_for_leader(self, available_nodes):
        """
        Алгоритм кворума.
        Лидером можно стать, только если тебя видят > 50% узлов (N/2 + 1).
        """
        quorum = (self.total_nodes // 2) + 1
        
        # Симуляция: кого видит этот узел в данный момент
        visible_count = len(available_nodes)
        
        print(f"[Node {self.node_id}] Вижу узлов: {visible_count}. Нужен кворум: {quorum}")
        
        if visible_count >= quorum:
            # Упрощение: Лидером становится тот, у кого меньший ID
            potential_leader = min(n.node_id for n in available_nodes)
            if potential_leader == self.node_id:
                self.is_leader = True
                print(f" -> [Node {self.node_id}] Я ЛИДЕР! (Есть кворум)")
            else:
                self.is_leader = False
                print(f" -> [Node {self.node_id}] Я фолловер. Лидер -> {potential_leader}")
        else:
            self.is_leader = False
            # Если кворума нет, узел переходит в режим Read-Only или останавливается
            print(f" -> [Node {self.node_id}] ! НЕТ КВОРУМА ! Останавливаю запись (CP-system).")

# --- Симуляция ---

# Создаем кластер из 5 узлов
nodes = [Node(i, 5) for i in range(1, 6)]

print("\n--- Сценарий 1: Сеть работает нормально ---")
# Все видят всех
for node in nodes:
    node.vote_for_leader(nodes)

print("\n--- Сценарий 2: Сеть разорвалась (Partition) ---")
# Кластер раскололся: Узлы 1,2 в одной комнате. Узлы 3,4,5 в другой.
partition_A = [nodes[0], nodes[1]]       # 2 узла
partition_B = [nodes[2], nodes[3], nodes[4]] # 3 узла

print("Группа А (Узлы 1, 2):")
for node in partition_A:
    node.vote_for_leader(partition_A)

print("\nГруппа Б (Узлы 3, 4, 5):")
for node in partition_B:
    node.vote_for_leader(partition_B)

Результат работы кода:

  1. В Группе А (2 узла из 5) кворум собрать нельзя (2 < 3). Узлы понимают это и отказываются выбирать лидера. Система переходит в Read-Only. Это предотвращает запись противоречивых данных.
  2. В Группе Б (3 узла из 5) кворум есть (3 >= 3). Они выбирают нового лидера (Node 3) и продолжают работать.

Это классический пример CP-системы.

Итог Главы 1 (Уровень 3)

Мы коснулись философии распределенных систем.

  1. CAP: Нельзя получить всё сразу. При аварии выбирай: тормозить (CP) или врать (AP).
  2. SLA: 99.99% — это всего 50 минут простоя в год.
  3. Active-Active: Круто, но больно.
  4. Split Brain: Лечится кворумом (голосами > 50%).

Глава 2. Согласованность и Распределенные транзакции


1. Спектр согласованности (Consistency Models)

Согласованность — это не просто "да" или "нет". Это шкала. Чем строже модель, тем медленнее система.

  1. Strong Consistency (Linearizability):
    • Как это выглядит: Как будто система — это один компьютер. После записи данных любой следующий запрос на чтение (откуда угодно) вернет новое значение.
    • Цена: Огромные задержки (Latency). Требует блокировок и консенсуса (Paxos/Raft).
  2. Sequential Consistency:
    • Порядок операций важен. Если я запостил "Всем привет", а потом комментарий "Как дела", все должны видеть их именно в таком порядке.
  3. Causal Consistency (Причинная):
    • Связанные события (вопрос-ответ) должны быть упорядочены. Несвязанные события могут приходить в разнобой.
  4. Eventual Consistency (В конечном счете):
    • Как это выглядит: Я лайкнул фото. Мой друг увидит лайк через 10 секунд.
    • Цена: Очень быстро. Но данные могут на время расходиться.

2. Проблема распределенной транзакции

Сценарий: Покупка билета.

  1. Service A (Order): Создать заказ.
  2. Service B (Payment): Списать деньги.

Если шаг 1 прошел успешно, а на шаге 2 упала сеть или не хватило денег — у нас проблема. Заказ висит как "созданный", но не оплаченный. В монолите мы бы откатили транзакцию. В распределенной системе нам нужно отменить изменения в Сервисе А вручную.

3. Решение 1: Two-Phase Commit (2PC)

Классический, но устаревший метод.

Вводится Координатор транзакций. Процесс идет в две фазы:

  • Фаза 1 (Prepare): Координатор спрашивает у всех сервисов (БД): "Вы сможете сделать коммит? Заблокируйте ресурсы, но пока не сохраняйте".
    • Сервис А: "Да, могу".
    • Сервис Б: "Да, могу".
  • Фаза 2 (Commit): Если все ответили "Да", Координатор командует: "Всем COMMIT!".
  • Проблема (Blocking): Это блокирующий протокол. Если Координатор упадет после фазы 1, все базы данных будут держать блокировки на строках и ждать его воскрешения. Система встанет колом.
  • Вывод: Не используйте 2PC в высоконагруженных микросервисах.

4. Решение 2: Saga Pattern

Современный стандарт.

Сага — это последовательность локальных транзакций. Каждый сервис делает свою работу и публикует событие. Если на каком-то шаге происходит ошибка, запускаются Компенсирующие транзакции (Compensation), чтобы отменить изменения предыдущих шагов.

  • Шаг 1: Order Service создает заказ (Status: PENDING). -> Успех.
  • Шаг 2: Payment Service пытается списать деньги. -> Ошибка (нет средств).
  • Компенсация: Payment Service сообщает об ошибке. Order Service ловит событие и меняет статус заказа на FAILED (или удаляет его).

Саги бывают двух типов:

А. Choreography (Хореография) — Без дирижера

Сервисы общаются через события (RabbitMQ/Kafka).

  • Сервис А сделал дело -> кинул ивент OrderCreated.
  • Сервис Б слушает OrderCreated, делает дело -> кидает PaymentProcessed.
  • Сервис В слушает PaymentProcessed...
  • Плюс: Простая архитектура, слабая связность.
  • Минус: Сложно понять бизнес-процесс ("Кто вообще слушает этот ивент?"). Риск циклических зависимостей.

Б. Orchestration (Оркестрация) — С дирижером

Есть отдельный сервис (Orchestrator), который говорит всем, что делать.

  • Оркестратор: "Сервис А, создай заказ".
  • Сервис А: "Готово".
  • Оркестратор: "Сервис Б, спиши деньги".
  • Сервис Б: "Ошибка!".
  • Оркестратор: "Сервис А, отменяй заказ!".
  • Плюс: Весь процесс виден в одном месте (в коде оркестратора). Легче искать ошибки.
  • Минус: Оркестратор становится узким местом.
  • Инструменты: Temporal.io, Camunda, AWS Step Functions.

5. Практика: Сага с Компенсацией (Python)

Давайте напишем простую реализацию Оркестрации на Python.

import time

class DatabaseError(Exception):
    pass

# --- Сервисы ---

class FlightService:
    def book_flight(self):
        print("[Flight] Бронирую билет...")
        # Логика записи в БД
        return "flight_123" # ID брони

    def cancel_flight(self, flight_id):
        print(f"[Flight] ! Компенсация: Отменяю билет {flight_id}")

class HotelService:
    def book_hotel(self):
        print("[Hotel] Бронирую отель...")
        # Симуляция ошибки: мест нет или база упала
        raise DatabaseError("Нет мест в отеле!")
        # return "hotel_456"

    def cancel_hotel(self, hotel_id):
        print(f"[Hotel] ! Компенсация: Отменяю отель {hotel_id}")

class PaymentService:
    def charge(self):
        print("[Payment] Списываю деньги...")
        return "tx_789"

    def refund(self, tx_id):
        print(f"[Payment] ! Компенсация: Возвращаю деньги {tx_id}")

# --- Оркестратор Саги ---

class TravelSagaOrchestrator:
    def __init__(self):
        self.flight_svc = FlightService()
        self.hotel_svc = HotelService()
        self.payment_svc = PaymentService()
        
        # Журнал успешных операций для отката
        self.history = []

    def book_trip(self):
        try:
            # Шаг 1: Перелет
            flight_id = self.flight_svc.book_flight()
            self.history.append({"step": "flight", "id": flight_id})
            
            # Шаг 2: Оплата (допустим, списываем до отеля)
            tx_id = self.payment_svc.charge()
            self.history.append({"step": "payment", "id": tx_id})
            
            # Шаг 3: Отель (ТУТ БУДЕТ ОШИБКА)
            hotel_id = self.hotel_svc.book_hotel()
            self.history.append({"step": "hotel", "id": hotel_id})
            
            print(">>> Успех! Путешествие забронировано.")
            
        except Exception as e:
            print(f"\n[SAGA ERROR] Произошел сбой: {e}")
            print("Начинаем откат транзакций (Rollback)...\n")
            self.compensate()

    def compensate(self):
        # Идем по истории в обратном порядке (LIFO)
        for operation in reversed(self.history):
            step = operation["step"]
            resource_id = operation["id"]
            
            if step == "hotel":
                self.hotel_svc.cancel_hotel(resource_id)
            elif step == "payment":
                self.payment_svc.refund(resource_id)
            elif step == "flight":
                self.flight_svc.cancel_flight(resource_id)
        
        print("\n>>> Сага отменена. Система вернулась в исходное состояние.")

# --- Запуск ---

saga = TravelSagaOrchestrator()
saga.book_trip()

Результат выполнения кода:

  1. Билет забронирован.
  2. Деньги списаны.
  3. Отель выдал ошибку.
  4. Компенсация: Деньги возвращены -> Билет отменен.

В реальной жизни (например, используя Temporal или Cadence) этот код выполнялся бы надежно, даже если бы сам скрипт оркестратора перезагрузился посередине процесса. Он бы запомнил состояние и продолжил с момента сбоя.

Итог Главы 2 (Уровень 3)

  1. ACID работает только локально. В микросервисах забудьте про глобальный COMMIT.
  2. 2PC — зло для HighLoad систем (блокировки).
  3. Saga Pattern — наш выбор. Разбиваем процесс на шаги.
  4. Всегда пишите компенсацию. На каждый create должен быть код undo (или delete). Это называется "Forward Recovery" (идти до конца) или "Backward Recovery" (откатить все назад).

Глава 3. Алгоритмы Консенсуса


1. Зачем это нужно?

Консенсус позволяет набору ненадежных машин работать как один надежный компьютер.

Главная абстракция здесь — Replicated State Machine (Реплицируемый автомат).

  • Идея: Если у всех узлов одинаковое начальное состояние и мы применим к ним одни и те же команды в одном и том же порядке, то их конечное состояние тоже будет одинаковым.
  • Задача консенсуса: Гарантировать, что "Лог команд" (Log Replication) одинаков на всех узлах.

2. Paxos: Дедушка всех алгоритмов

Автор: Лесли Лэмпорт (1989).

Paxos был первым математически доказанным алгоритмом консенсуса.

  • Суть: Очень сложный. Даже в Google инженеры шутят, что "в мире есть только 5 человек, которые понимают Paxos, и они не согласны друг с другом".
  • Проблема: Paxos сложен в реализации. На практике (в Zookeeper, например) использовали модифицированный Zab, потому что чистый Paxos трудно запрограммировать без багов.

3. Raft: Консенсус для людей

Авторы: Диего Онгаро и Джон Оустерхаут (2014).

Raft был создан специально, чтобы быть понятным. Сегодня это стандарт индустрии (используется в etcd, Consul, Kubernetes, CockroachDB).

Как работает Raft?

В любой момент времени узел может быть в одном из 3 состояний:

  1. Leader (Лидер): "Диктатор". Все запросы на запись идут только к нему. Он рассылает приказы остальным.
  2. Follower (Последователь): Пассивно слушает Лидера и выполняет приказы.
  3. Candidate (Кандидат): Если Лидер замолчал, Последователь выдвигает себя в Кандидаты, чтобы стать новым Лидером.

Процесс 1: Выборы Лидера (Leader Election)

  1. Лидер должен постоянно слать пустые пакеты (Heartbeats) каждые 50-100 мс: "Я жив, я тут".
  2. У каждого Фолловера есть таймер ("Терпение"). Если он не слышит Лидера, например, 300 мс, он психует: "Лидер умер!".
  3. Фолловер переходит в статус Candidate, голосует сам за себя и шлет всем просьбу: "Голосуйте за меня! (Term N+1)".
  4. Если он получает большинство голосов ( $Quorum=N/2+1$ ), он становится новым Лидером.

Процесс 2: Репликация Лога (Log Replication)

  1. Клиент шлет команду SET x = 5 Лидеру.
  2. Лидер записывает это в свой лог, но не комитит (не применяет к состоянию).
  3. Лидер шлет эту запись всем Фолловерам: "Запишите у себя".
  4. Фолловеры пишут и отвечают "ОК".
  5. Как только Лидер получил "ОК" от большинства (Кворума), он делает COMMIT (значение реально меняется) и отвечает клиенту "Успех".
  6. Лидер сообщает Фолловерам: "Я закоммитил, вы тоже можете".

4. Инструменты в реальной жизни

Вам вряд ли придется писать Raft с нуля (это сложно). Вы будете использовать готовые хранилища, которые его реализуют.

  1. ZooKeeper (Zab): Старый, надежный, написан на Java. Используется в Kafka, Hadoop для хранения конфигураций и выбора контроллера.
  2. etcd (Raft): Написан на Go. "Мозги" Kubernetes. Именно в etcd хранится все состояние вашего кластера (поды, сервисы).
  3. Consul (Raft): Service Discovery + KV хранилище от HashiCorp.

5. Практика: Распределенная блокировка (Distributed Lock)

Самый частый кейс использования etcd/Zookeeper в коде — это блокировки.

Задача: У вас есть 5 серверов, на каждом крутится Cron-скрипт отправки рассылки. Нужно, чтобы рассылка ушла ровно один раз. Если запустить на всех 5 — пользователи получат 5 писем.

Решение: Первый, кто захватит "ключ" (Lock) в etcd, делает работу. Остальные видят, что ключ занят, и пропускают запуск.

# pip install etcd3
import etcd3
import time
import sys

# Подключение к кластеру etcd
client = etcd3.client(host='localhost', port=2379)

def run_critical_job(worker_name):
    lock_name = "/locks/daily_email_job"
    
    # 1. Создаем объект блокировки с TTL (временем жизни) 10 секунд.
    # TTL нужен, чтобы если скрипт упадет (сгорит сервер),
    # блокировка сама исчезла через 10 сек, и другие могли её взять.
    lock = client.lock(lock_name, ttl=10)
    
    print(f"[{worker_name}] Пытаюсь захватить лидерство...")
    
    # 2. acquire() - атомарная операция. Внутри Raft гарантирует, 
    # что только один получит True.
    is_locked = lock.acquire() 
    
    if is_locked:
        try:
            print(f"[{worker_name}] >>> Я ЛИДЕР! Блокировка захвачена.")
            print(f"[{worker_name}] Выполняю сложную работу (отправка писем)...")
            
            # Эмуляция работы
            for i in range(5):
                print(f"[{worker_name}] Работаю... {i}")
                time.sleep(1)
                # Важно: в реальной жизни тут надо обновлять TTL (refresh),
                # если работа длится дольше 10 секунд.
                lock.refresh()
                
            print(f"[{worker_name}] Работа завершена успешно.")
            
        finally:
            # 3. Обязательно отпускаем блокировку
            lock.release()
            print(f"[{worker_name}] Блокировка снята.")
    else:
        print(f"[{worker_name}] Блокировка занята кем-то другим. Я пропускаю ход.")

# Симуляция запуска:
# Если запустить этот скрипт в двух терминалах одновременно,
# один выиграет, второй сразу скажет "Блокировка занята".
run_critical_job(sys.argv[1] if len(sys.argv) > 1 else "Worker-1")

Почему нельзя сделать это в Postgres/MySQL?

Можно (через SELECT FOR UPDATE или GET_LOCK), но:

  1. Если мастер БД упадет, блокировки могут потеряться или зависнуть.
  2. etcd/Zookeeper созданы специально для этого: они держат блокировку в памяти и мгновенно (через Watchers) уведомляют других, если она освободилась.

Итог Уровня 3

Мы прошли самый сложный теоретический блок.

  1. CAP-теорема: Мы поняли, что распределенные системы — это компромисс.
  2. Saga: Мы научились делать транзакции без блокировок.
  3. Raft/Consensus: Мы поняли, как Kubernetes и Kafka выбирают главного и почему это надежно.

Уровень 4: Продвинутые паттерны

===============================

Глава 1. Микросервисы, API Gateway и Discovery


1. Как правильно резать монолит? (Decomposition)

Главная ошибка новичков: делить сервисы по типу данных (UserService, PaymentService, ProductService) или по размеру кода ("Слишком большой класс, вынесу в сервис").

Правильный подход: DDD (Domain-Driven Design). Сервисы нужно делить по Bounded Context (Ограниченным контекстам) — бизнес-областям.

  • Плохой пример: Сервис User. Все ходят в него за данными пользователя. Если он упал — лежит всё.
  • Хороший пример:
    • Контекст Продаж: Здесь "Пользователь" — это Customer (есть история заказов, адрес доставки).
    • Контекст Поддержки: Здесь "Пользователь" — это TicketRequester (история жалоб).
    • Контекст Аутентификации: Здесь "Пользователь" — это Account (логин, пароль, роли).

Данные могут дублироваться, но сервисы становятся независимыми.

2. API Gateway: Единая точка входа

Если у вас 50 микросервисов, нельзя заставлять мобильное приложение знать адреса их всех (service-a.com, service-b.com).

API Gateway — это швейцар (Pattern Facade), который стоит на входе. Примеры: Kong, Zuul, AWS API Gateway, Nginx (с Lua скриптами).

Задачи Gateway:

  1. Маршрутизация: Клиент шлет запрос на /api/v1/buy, Gateway пересылает его на order-service:8080.
  2. Аутентификация: Проверяет JWT токен один раз на входе. Микросервисам уже не нужно париться с криптографией, они получают чистый User-ID в заголовке.
  3. Rate Limiting: "Не больше 10 запросов в секунду от одного IP".
  4. Protocol Translation: Клиент общается по HTTP/JSON, а Gateway внутри кластера преобразует это в эффективный gRPC/Protobuf.

Паттерн BFF (Backend for Frontend)

Иногда мобильному приложению нужны одни данные (мало, компактно), а веб-версии — другие (много деталей). Вместо одного монструозного Gateway делают несколько маленьких:

  • BFF for Mobile
  • BFF for Web
  • BFF for Public API

3. Service Discovery: Где ты, брат?

В мире контейнеров (Docker/Kubernetes) сервисы не живут вечно. Они умирают, перезапускаются, переезжают на другие IP-адреса. Хардкодить IP (http://192.168.1.50) нельзя.

Как Сервис А найдет Сервис Б?

Client-Side Discovery (Старый подход)

Клиент (Сервис А) идет в реестр (Consul/Eureka), получает список адресов Сервиса Б и сам выбирает, куда слать (Round Robin).

  • Минус: В каждом микросервисе нужно писать логику балансировки.

Server-Side Discovery (Kubernetes подход)

Сервис А просто шлет запрос на виртуальное имя http://my-service. Инфраструктура (K8s Service / CoreDNS) сама перехватывает запрос и направляет его на живой под. Разработчик вообще не думает об IP.

4. Sidecar Pattern и Service Mesh

Когда микросервисов становится 100+, управлять ими через код (библиотеки) становится адом.

  • "Обновите библиотеку аутентификации во всех 100 сервисах!" — это задача на полгода.

Решение: Sidecar (Мотоцикл с коляской). Рядом с каждым контейнером вашего приложения (Python/Java) запускается маленький прокси-агент (например, Envoy).

  • Приложение ничего не знает о сети. Оно шлет запрос на localhost:8080.
  • Sidecar перехватывает запрос, находит нужный сервис, шифрует трафик (mTLS), делает повторы (Retries), собирает метрики и отправляет запрос другому Sidecar-у.

Сетка из таких Sidecar-ов называется Service Mesh (Istio, Linkerd).

5. Практика: Простой API Gateway на Python

Напишем примитивный Gateway, который маршрутизирует запросы и проверяет API-ключ.

import requests
from flask import Flask, request, jsonify

app = Flask(__name__)

# Реестр наших микросервисов (в реальности это был бы Consul или K8s DNS)
SERVICES = {
    "users": "http://localhost:5001",
    "orders": "http://localhost:5002"
}

API_KEY = "secret-token-123"

def check_auth(headers):
    token = headers.get("Authorization")
    if token != API_KEY:
        return False
    return True

@app.route('/<service_name>/<path:subpath>', methods=['GET', 'POST', 'PUT', 'DELETE'])
def gateway(service_name, subpath):
    """
    Универсальный маршрутизатор.
    Пример: GET /users/profile/1 -> идет в сервис 'users' на /profile/1
    """
    
    # 1. Централизованная Аутентификация
    if not check_auth(request.headers):
        return jsonify({"error": "Unauthorized"}), 401

    # 2. Поиск сервиса (Service Discovery)
    base_url = SERVICES.get(service_name)
    if not base_url:
        return jsonify({"error": "Service not found"}), 404

    # 3. Формирование целевого URL
    target_url = f"{base_url}/{subpath}"
    
    print(f"[Gateway] Проксирую запрос на: {target_url}")

    # 4. Проксирование запроса (используем библиотеку requests)
    try:
        resp = requests.request(
            method=request.method,
            url=target_url,
            headers={key: value for (key, value) in request.headers if key != 'Host'},
            data=request.get_data(),
            cookies=request.cookies,
            allow_redirects=False
        )
        
        # 5. Возврат ответа клиенту
        return (resp.content, resp.status_code, resp.headers.items())
        
    except requests.exceptions.ConnectionError:
        return jsonify({"error": "Service is down"}), 503

if __name__ == '__main__':
    # Gateway запускается на порту 5000
    app.run(port=5000)

Что дает этот код:

  1. Безопасность: Микросервисам users и orders не нужно проверять API_KEY. Они доверяют Gateway (находятся в закрытой сети).
  2. Абстракция: Клиент не знает, где физически находятся сервисы.

Итог Главы 1 (Уровень 4)

  1. API Gateway обязателен для любого публичного API.
  2. DDD помогает правильно разбить монолит.
  3. Service Discovery позволяет забыть про IP-адреса.
  4. Service Mesh выносит сетевую логику из кода в инфраструктуру (но добавляет сложности, осторожнее с ним!).

Глава 2. Устойчивость: Circuit Breaker, Retry, Rate Limiting


1. Circuit Breaker (Предохранитель)

Это самый важный паттерн для защиты от каскадных сбоев. Он работает точно так же, как пробки в электрощитке вашей квартиры.

Если сервис Б начинает тормозить или сыпать ошибками, сервис А должен перестать слать туда запросы и сразу возвращать ошибку (Fail Fast) или дефолтное значение. Это дает сервису Б время "прийти в себя".

Машина состояний Circuit Breaker

У предохранителя есть 3 состояния:

  1. Closed (Закрыт): Всё хорошо. Ток идет, запросы проходят. Мы считаем ошибки. Если ошибок стало больше порога (например, 50% за 10 сек) -> переходим в Open.
  2. Open (Открыт): Цепь разомкнута. Запросы мгновенно отбиваются с ошибкой, даже не пытаясь достучаться до удаленного сервиса. Мы ждем тайм-аут (например, 30 сек).
  3. Half-Open (Полуоткрыт): Тайм-аут прошел. Мы пропускаем один пробный запрос.
    • Если успех -> переходим в Closed (счетчик ошибок сброшен).
    • Если ошибка -> снова в Open (ждем еще 30 сек).

2. Retry & Exponential Backoff (Повторы)

Если запрос не прошел, глупо сразу сдаваться. Возможно, просто моргнула сеть. Но еще глупее — долбить упавший сервер в бесконечном цикле while True. Это называется Retry Storm (шторм повторов), который добьет лежачий сервис.

Правильная стратегия: Exponential Backoff + Jitter

Мы увеличиваем время ожидания перед каждой следующей попыткой экспоненциально.

  • Попытка 1: Ждем 1 сек.
  • Попытка 2: Ждем 2 сек.
  • Попытка 3: Ждем 4 сек.
  • Попытка 4: Ждем 8 сек.
  • ...Stop.

Jitter (Дрожание): Если 1000 микросервисов упали одновременно, и они одновременно (через ровно 1 сек) попробуют переподключиться, они снова положат базу. Нужно добавить случайность: sleep = 2^retry_count + random(0, 100ms). Это "размажет" нагрузку.

3. Bulkhead (Переборки)

Паттерн назван в честь переборок на корабле. Если пробит один отсек, вода не должна затопить весь Титаник.

В IT это означает изоляцию ресурсов.

  • Сценарий: У вас один сервер Tomcat/Python с пулом в 100 потоков.
  • Проблема: Сервис "Картинки" завис. Все 100 потоков заняты ожиданием ответа от него.
  • Результат: Сервис "Логин" (который работает идеально) недоступен, потому что нет свободных потоков.
  • Решение (Bulkhead): Выделить жесткие лимиты. 20 потоков на Картинки, 30 на Логин, 50 на остальное. Если потоки "Картинок" кончились — новые запросы на картинки отбиваются, но Логин работает.

4. Rate Limiting (Ограничение нагрузки)

Мы должны защищать свои сервисы не только от поломок соседей, но и от слишком активных пользователей (или DDoS).

Алгоритм 1: Token Bucket (Ведро с токенами)

  • Есть ведро, в которое капают токены с фиксированной скоростью (например, 10 токенов в секунду). Максимальная емкость ведра ограничена (например, 100).
  • Когда приходит запрос, он должен забрать 1 токен.
  • Если токенов нет -> запрос отбрасывается (HTTP 429 Too Many Requests).
  • Плюс: Позволяет кратковременные всплески (Bursts) нагрузки (пока есть накопленные токены).

Алгоритм 2: Leaky Bucket (Дырявое ведро)

  • Запросы попадают в очередь (ведро).
  • Из ведра запросы выходят на обработку с постоянной скоростью (как вода через дырку).
  • Если ведро переполнилось — новые запросы выливаются через край (отбрасываются).
  • Плюс: Идеально сглаживает трафик.
  • Минус: Не умеет обрабатывать резкие всплески активности.

5. Практика: Circuit Breaker на Python

Напишем упрощенную реализацию паттерна "Предохранитель" в виде класса.

import time
import random

class CircuitBreakerOpenException(Exception):
    pass

class CircuitBreaker:
    def __init__(self, failure_threshold=3, recovery_timeout=5):
        self.failure_threshold = failure_threshold # Сколько ошибок допустимо
        self.recovery_timeout = recovery_timeout   # Сколько ждать в Open state (сек)
        
        self.state = "CLOSED"
        self.failure_count = 0
        self.last_failure_time = 0

    def call(self, func, *args, **kwargs):
        # 1. Логика OPEN состояния
        if self.state == "OPEN":
            # Проверяем, не прошло ли время тайм-аута
            if time.time() - self.last_failure_time > self.recovery_timeout:
                print(">>> Circuit Breaker перешел в HALF-OPEN. Пробуем запрос...")
                self.state = "HALF_OPEN"
            else:
                # Мгновенный отказ без вызова реальной функции
                remaining = self.recovery_timeout - (time.time() - self.last_failure_time)
                raise CircuitBreakerOpenException(f"Circuit is OPEN. Wait {remaining:.1f}s")

        # 2. Попытка вызова функции (для CLOSED и HALF-OPEN)
        try:
            result = func(*args, **kwargs)
            
            # Если успех в HALF-OPEN -> Сбрасываем в CLOSED
            if self.state == "HALF_OPEN":
                print(">>> Успех! Circuit Breaker ЗАКРЫЛСЯ.")
                self.state = "CLOSED"
                self.failure_count = 0
                
            return result
            
        except Exception as e:
            # Ловим ошибку реальной функции
            self.failure_count += 1
            self.last_failure_time = time.time()
            print(f"!!! Ошибка вызова ({self.failure_count}/{self.failure_threshold})")
            
            # Если превышен порог или мы были в HALF-OPEN -> Открываем цепь
            if self.failure_count >= self.failure_threshold or self.state == "HALF_OPEN":
                self.state = "OPEN"
                print(">>> Circuit Breaker ОТКРЫЛСЯ!")
            
            raise e

# --- Тестирование ---

# Нестабильная функция
def unreliable_service():
    if random.random() < 0.7: # 70% шанс ошибки
        raise ConnectionError("Service unavailable")
    return "Success 200 OK"

cb = CircuitBreaker(failure_threshold=3, recovery_timeout=3)

print("--- Начинаем бомбардировку запросами ---")
for i in range(15):
    try:
        time.sleep(0.5)
        print(f"Запрос {i+1}: ", end="")
        res = cb.call(unreliable_service)
        print(res)
    except CircuitBreakerOpenException as e:
        print(f"БЛОКИРОВКА: {e}")
    except Exception as e:
        print(f"Ошибка сервиса: {e}")

Анализ работы кода:

  1. Сначала запросы идут и иногда падают.
  2. Как только накапливается 3 ошибки, Breaker переходит в OPEN.
  3. Следующие запросы получают сообщение "БЛОКИРОВКА" мгновенно, даже не дергая unreliable_service.
  4. Через 3 секунды один запрос прорывается (Half-Open). Если повезет — система восстановится.

Итог Главы 2 (Уровень 4)

Вы научились защищать свою систему от самой себя.

  1. Circuit Breaker спасает от каскадного ожидания.
  2. Exponential Backoff спасает от DDoS-атаки "самого на себя" при рестарте.
  3. Rate Limiting спасает от внешних нагрузок.

Глава 3. Observability: Логи, Метрики и Трассировка


1. Столп 1: Логирование (Logging)

Вопрос: "Что именно произошло?" (Событие).

Лог — это запись дискретного события: "Ошибка подключения к БД", "Пользователь вошел".

Проблема микросервисов

Нельзя заходить по SSH на каждый сервер читать логи. Решение: Централизованный сбор логов (Log Aggregation).

  1. Приложение пишет логи в STDOUT (консоль).
  2. Агент (Filebeat/Fluentd) читает эти потоки со всех контейнеров.
  3. Агент отправляет их в Elasticsearch (база данных для поиска по тексту).
  4. Вы открываете Kibana (веб-интерфейс) и ищете ошибку сразу по всем сервисам.

Это называется ELK Stack (Elastic, Logstash, Kibana) или EFK (Fluentd).

Structured Logging (Структурированные логи)

Забудьте про print(f"User {id} login failed"). Это текст, его сложно парсить машине. Пишите в JSON:

{
  "level": "ERROR",
  "timestamp": "2023-10-27T10:00:00Z",
  "message": "Login failed",
  "user_id": 12345,
  "service": "auth-service",
  "trace_id": "abc-999"
}

В Kibana вы сможете кликнуть на user_id и отфильтровать все логи по этому пользователю.

2. Столп 2: Метрики (Metrics)

Вопрос: "Как здоровье системы?" (Числа во времени).

Метрики — это агрегированные числовые данные.

  • Плохо: Писать в лог каждое обращение к процессору (терабайты текста).
  • Хорошо: Раз в 15 секунд отправлять число: "CPU Load = 45%".

Инструменты: Prometheus + Grafana

  • Prometheus: База данных временных рядов (Time-Series DB). Она работает по модели Pull. Она сама приходит к вашим сервисам раз в N секунд и забирает метрики с эндпоинта /metrics.
  • Grafana: Рисует красивые графики на основе данных из Prometheus.

Четыре золотых сигнала (The Four Golden Signals)

Google SRE рекомендует мониторить минимум эти 4 метрики для каждого сервиса:

  1. Latency (Задержка): Сколько времени занял запрос? (p99, p95).
  2. Traffic (Трафик): Сколько запросов в секунду (RPS)?
  3. Errors (Ошибки): Процент 500-х ошибок.
  4. Saturation (Насыщение): Насколько мы загружены? (Очередь задач, память).

3. Столп 3: Распределенная Трассировка (Distributed Tracing)

Вопрос: "Где именно мы потеряли время?"

Это самое сложное и самое полезное. Представьте запрос:

  1. Gateway (2 мс) -> Auth Service (50 мс) -> Order Service (2000 мс) -> DB (5 мс).
  2. В логах Auth Service всё ок. В логах DB всё ок. Почему Order Service тормозил 2 секунды? Может, он ждал ответа от другого сервиса?

Трассировка рисует Waterfall (Водопад) прохождения одного запроса через все микросервисы.

Как это работает? (Context Propagation)

Магия держится на двух ID, которые передаются в HTTP-заголовках:

  1. Trace ID: Глобальный ID всего запроса. Генерируется на входе (в Gateway).
  2. Span ID: ID конкретной операции (шага).

Каждый сервис, принимая запрос, обязан:

  1. Прочитать Trace ID.
  2. Сделать свою работу (создать Span).
  3. Если он вызывает другой сервис — передать Trace ID дальше.

Популярные инструменты: Jaeger, Zipkin, и современный стандарт OpenTelemetry.

4. Практика: Как работает Трейсинг "под капотом"

В реальности вы возьмете библиотеку opentelemetry, и она сделает всё сама. Но чтобы понять суть, давайте сымитируем ручную передачу контекста (Context Propagation) на Python.

Представим два сервиса: Service A вызывает Service B.

import uuid
import time
import requests # pip install requests

# --- Эмуляция инфраструктуры трейсинга ---

def log_span(service_name, span_name, trace_id, duration_ms, parent_id=None):
    """
    В реальности это отправлялось бы в Jaeger/Zipkin.
    Мы просто печатаем в консоль.
    """
    print(f"[{service_name}] Span: {span_name} | TraceID: {trace_id} | "
          f"Parent: {parent_id} | Time: {duration_ms}ms")

# --- Service B (Downstream) ---

def service_b_handler(headers):
    # 1. Извлекаем контекст (Trace ID) из заголовков пришедшего запроса
    trace_id = headers.get('x-trace-id')
    parent_span_id = headers.get('x-span-id') # Span ID вызывающего сервиса становится Parent ID
    
    start_time = time.time()
    
    # Работа сервиса
    time.sleep(0.1) 
    
    duration = (time.time() - start_time) * 1000
    log_span("Service-B", "process_payment", trace_id, duration, parent_span_id)
    return "Payment OK"

# --- Service A (Upstream / Initiator) ---

def service_a_handler():
    # 1. Запрос пришел от пользователя - генерируем новый Trace ID
    trace_id = str(uuid.uuid4())[:8]
    span_id = "A-100" # ID текущей операции в сервисе А
    
    print(f"--- New Request Trace: {trace_id} ---")
    start_time = time.time()
    
    # 2. Нужно вызвать Сервис Б. ВНЕДРЯЕМ контекст в заголовки.
    headers = {
        'x-trace-id': trace_id,
        'x-span-id': span_id
    }
    
    # В реальности тут был бы requests.get('http://service-b', headers=headers)
    # Мы вызываем функцию напрямую для эмуляции
    response = service_b_handler(headers)
    
    # Работа самого Сервиса А
    time.sleep(0.05)
    
    duration = (time.time() - start_time) * 1000
    log_span("Service-A", "buy_item", trace_id, duration, parent_id=None)
    
    return f"Result: {response}"

# Запуск
service_a_handler()

Результат в консоли:

--- New Request Trace: a1b2c3d4 ---
[Service-B] Span: process_payment | TraceID: a1b2c3d4 | Parent: A-100 | Time: 100.0ms
[Service-A] Span: buy_item        | TraceID: a1b2c3d4 | Parent: None  | Time: 155.0ms

Система трассировки (Jaeger) увидит эти логи, объединит их по TraceID и построит график:

  • buy_item (Всего 155мс)
    • process_payment (Внутри него, 100мс)

Если бы мы забыли передать x-trace-id, Jaeger подумал бы, что это два совершенно разных, несвязанных события, и мы бы не нашли причину тормозов.


5. OpenTelemetry (OTel)

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

Сейчас индустрия пришла к стандарту OpenTelemetry. Это единый набор библиотек и агентов, которые собирают ВСЁ (Logs, Metrics, Traces) и отправляют куда скажете (в Prometheus, в Jaeger, в Datadog).

Совет эксперта: В новых проектах сразу настраивайте OpenTelemetry. Не используйте проприетарные агенты.

Уровень 5: Экспертные системы

=============================

Глава 1. Batch vs Stream Processing. Архитектуры Lambda и Kappa.


1. Batch Processing (Пакетная обработка)

Девиз: "Лучше поздно, но точно и много".

Это классический подход Big Data. Мы накапливаем данные за день/месяц (логи, транзакции), а потом запускаем огромную молотилку, которая переваривает их за ночь.

MapReduce: Прадедушка

Google придумал концепцию MapReduce, чтобы индексировать весь интернет.

  1. Map (Отображение): Разбить задачу на тысячи мелких кусков и раздать тысяче серверов. Каждый сервер считает слова в своем куске текста.
  2. Reduce (Свертка): Собрать результаты от всех серверов и сложить их.

Apache Spark: Современный стандарт

Spark работает по тому же принципу, но в 100 раз быстрее Hadoop MapReduce, потому что он старается держать данные в оперативной памяти (In-Memory), а не писать на диск после каждого шага.

  • Применение: Генерация ежемесячных отчетов, обучение ML-моделей, расчет кредитного скоринга за историю в 10 лет.

2. Stream Processing (Потоковая обработка)

Девиз: "Данные теряют ценность каждую секунду".

Если мошенник крадет деньги с карты, нам не нужен отчет "завтра утром". Нам нужно заблокировать транзакцию прямо сейчас (до 200 мс). Здесь данные не лежат на диске, они летят через трубу (Kafka), и мы пытаемся анализировать их на лету.

Windowing (Оконные функции)

В потоке нет "начала" и "конца". Как посчитать "среднее количество кликов"? Мы используем Окна:

  1. Tumbling Window (Кувыркающееся окно): Каждые 10 секунд. (00:00-00:10, 00:10-00:20). Окна не пересекаются.
  2. Sliding Window (Скользящее окно): Окно размером 10 секунд, которое сдвигается каждую секунду. (Данные за 00:00-00:10, потом 00:01-00:11).
  3. Session Window: Пока пользователь активен + 5 минут тишины.
  • Инструменты: Apache Flink (самый мощный), Kafka Streams, Spark Structured Streaming.

3. Архитектуры обработки данных

Как объединить точность Batch и скорость Stream?

Lambda Architecture

Классика 2010-х. Мы строим две параллельные системы:

  1. Batch Layer (Hadoop/Spark): Хранит "Master Dataset" (абсолютно все сырые данные). Пересчитывает всё раз в сутки. Это "Source of Truth".
  2. Speed Layer (Flink/Storm): Считает приблизительные данные за сегодня в реальном времени.
  3. Serving Layer: Объединяет результаты (Вчерашние точные + Сегодняшние быстрые).
  • Плюс: Если в коде Stream-обработки был баг, мы просто исправим код и перезапустим Batch ночью. Данные восстановятся.
  • Минус: Нужно поддерживать две кодовые базы (одна на Java для Hadoop, другая на Scala для Flink). Это дорого и сложно.

Kappa Architecture

Современный подход (предложен LinkedIn). Зачем нам Batch, если Stream движок (Flink/Kafka) стал надежным? У нас только Speed Layer. Всё есть поток. Даже история за 5 лет — это просто очень длинный поток, который можно "проиграть" заново из Kafka (увеличив скорость чтения).

  • Плюс: Одинаковая логика обработки и для реал-тайма, и для истории.

4. Практика: MapReduce на "пальцах" (Python/Spark Style)

Напишем простейший пример подсчета слов (Word Count), который имитирует логику Apache Spark.

from functools import reduce
from collections import defaultdict

# Огромный массив данных (в реальности - терабайты текста, разбитые на файлы)
raw_data = [
    "hello world",
    "hello spark",
    "big data is cool",
    "spark is fast",
    "hello big data"
]

# --- 1. MAP (Распараллеливание) ---
# Превращаем строки в пары (Слово, 1)
# В реальности это делали бы 100 разных серверов параллельно
mapped_data = []
for sentence in raw_data:
    for word in sentence.split():
        mapped_data.append((word, 1))

print(f"Mapped: {mapped_data[:5]}...") 
# [('hello', 1), ('world', 1), ('hello', 1), ('spark', 1), ('big', 1)...]

# --- 2. SHUFFLE (Перемешивание) ---
# Самая дорогая операция. Нужно собрать все единички для слова "hello" на один сервер,
# а для слова "spark" - на другой.
shuffled_data = defaultdict(list)
for word, count in mapped_data:
    shuffled_data[word].append(count)

# --- 3. REDUCE (Свертка) ---
# Суммируем список [1, 1, 1] в одно число 3
reduced_data = {}
for word, counts in shuffled_data.items():
    reduced_data[word] = sum(counts)

# Результат
# Сортируем для красоты
sorted_result = sorted(reduced_data.items(), key=lambda x: x[1], reverse=True)

print("\n--- Top Words ---")
for word, count in sorted_result:
    print(f"{word}: {count}")

В чем магия Spark? Этот код на Python работает на одном ядре. Если вы запустите это в PySpark:

  1. Массив raw_data будет называться RDD (Resilient Distributed Dataset).
  2. Spark сам разобьет его на партиции.
  3. Функция map улетит на 50 разных серверов.
  4. Если один сервер сгорит посередине работы, Spark заметит это и перезапустит обработку этого кусочка на другом сервере. Вам не нужно писать ни строчки кода для этого.

Итог Главы 1 (Уровень 5)

  1. Batch (Spark) нужен для сложных, тяжелых отчетов и обучения AI.
  2. Stream (Flink/Kafka) нужен для мгновенной реакции.
  3. Kappa архитектура побеждает Lambda, потому что проще поддерживать одну систему вместо двух.

Глава 2. Time-Series, Гео-индексы и Полнотекстовый поиск


1. Time-Series Database (TSDB)

Задача: Хранить метрики с серверов, котировки акций, данные с датчиков IoT. Характер данных:

  • Write-Heavy: Запись происходит постоянно (тысячи вставок в секунду).
  • Append-Only: Данные почти никогда не меняются задним числом.
  • Time-Centric: Запросы всегда идут за интервал времени ("дай среднюю температуру за прошлый час").

Почему не MySQL?

В обычном B-Tree индексе, если вы пишете случайные данные, диск "мечется" туда-сюда. TSDB оптимизированы для последовательной записи. Кроме того, TSDB умеют делать Downsampling (прореживание): хранить данные за сегодня посекундно, за месяц — поминутно, за год — почасово. Это экономит терабайты места.

  • Примеры: InfluxDB, Prometheus (стандарт для мониторинга), TimescaleDB (надстройка над PostgreSQL).

2. Spatial Indexing (Гео-данные)

Задача: Найти 5 ближайших водителей Uber вокруг меня. Проблема: Базы данных хранят данные линейно (1D). А карта — двумерная (2D). Обычный индекс "отсортировать по X, потом по Y" работает плохо.

Решение 1: Geohash

Мы превращаем 2D координаты (широту и долготу) в одну строку (1D). Весь мир делится на прямоугольники. Каждый прямоугольник делится еще на 32.

  • u — Европа.
  • u4 — Центральная Европа.
  • u4p — Чехия.
  • u4pru — Прага.

Свойство: У близких точек — общий префикс хэша. Поиск "кто рядом со мной" превращается в быстрый поиск по строке: WHERE geohash LIKE 'u4pru%'.

Решение 2: Quadtree (Квадродерево)

Вся карта — это квадрат.

  1. Если в квадрате слишком много точек, делим его на 4 квадрата поменьше.
  2. Повторяем рекурсивно. Получается дерево, где густонаселенные районы (центр Москвы) имеют глубокую детализацию, а тайга — один большой квадрат.
  • Примеры: PostGIS (расширение для Postgres), Redis Geo, Google S2.

3. Full-Text Search (Полнотекстовый поиск)

Задача: Найти документы, где встречается слово "System Design", даже если там написано "Systems Designs" (с ошибкой или в другом числе). Проблема: SELECT * FROM table WHERE text LIKE '%word%' делает Full Scan (читает каждую строку). Это непозволительно медленно.

Решение: Inverted Index (Обратный индекс)

Это структура, на которой построен весь Google, Elasticsearch и Lucene.

Вместо того чтобы хранить "Документ -> Слова", мы храним "Слово -> Список Документов".

  1. Tokenization: Разбиваем текст на слова.
  2. Normalization (Stemming/Lemmatization): Приводим слова к корню (running -> run, cats -> cat).
  3. Indexing:
Term (Слово) Document IDs (Где встречается)
apple 1, 5, 14
banana 2
design 1, 3, 99

Теперь, чтобы найти документы про "apple design", нам не нужно читать тексты. Мы берем списки [1, 5, 14] и [1, 3, 99] и ищем их пересечение. Результат: Документ 1. Это мгновенно.

4. Практика: Пишем свой Search Engine (Python)

Реализуем простейший обратный индекс, который умеет делать поиск AND (пересечение).

import re

class SimpleSearchEngine:
    def __init__(self):
        # Обратный индекс: слово -> set(id документов)
        self.index = {}
        # Хранилище самих документов
        self.documents = {}

    def _tokenize(self, text):
        # 1. Приводим к нижнему регистру
        # 2. Выкидываем знаки препинания
        # 3. Разбиваем на слова
        text = text.lower()
        words = re.findall(r'\w+', text)
        return set(words) # Убираем дубликаты слов внутри одного документа

    def add_document(self, doc_id, text):
        self.documents[doc_id] = text
        tokens = self._tokenize(text)
        
        for token in tokens:
            if token not in self.index:
                self.index[token] = set()
            self.index[token].add(doc_id)

    def search(self, query):
        tokens = self._tokenize(query)
        if not tokens:
            return []

        # Начинаем с множества документов первого слова
        # Например, ищем "fast python". Берем документы, где есть "fast".
        # Потом оставляем только те, где ЕЩЕ есть и "python" (пересечение).
        
        # Сортируем токены, чтобы начать с самого редкого слова (оптимизация)
        sorted_tokens = sorted(list(tokens), key=lambda t: len(self.index.get(t, set())))
        
        first_word = sorted_tokens[0]
        result_ids = self.index.get(first_word, set()).copy()
        
        for token in sorted_tokens[1:]:
            docs_with_token = self.index.get(token, set())
            # Пересечение множеств (Intersection)
            result_ids &= docs_with_token
            
            # Если пересечение пустое - дальше искать нет смысла
            if not result_ids:
                break
                
        return [self.documents[doc_id] for doc_id in result_ids]

# --- Тест ---
engine = SimpleSearchEngine()

engine.add_document(1, "Python is great for System Design")
engine.add_document(2, "Java is great for Enterprise")
engine.add_document(3, "System Design interview preparation")
engine.add_document(4, "Python and Java comparison")

print("Query: 'System Design'")
print(engine.search("System Design")) 
# Должен найти doc 1 и 3

print("\nQuery: 'Python'")
print(engine.search("Python")) 
# Должен найти doc 1 и 4

print("\nQuery: 'Java Design'")
print(engine.search("Java Design")) 
# Пусто (нет документа, где есть И Java, И Design)

Нюансы кода:

  1. Пересечение множеств (&): Это самая главная операция в поиске.
  2. Размер индекса: Обратный индекс может занимать много места, но он все равно меньше, чем сами данные.

Итог Главы 2 (Уровень 5)

Вы теперь знаете, что SQL — не панацея.

  1. Geohash помогает найти такси.
  2. Inverted Index помогает найти твит.
  3. TSDB помогает узнать нагрузку на CPU за прошлый год.

Глава 3. Безопасность: OAuth 2.0, JWT и Шифрование


1. AuthN vs AuthZ

Первое, что спросят на собеседовании по безопасности.

  1. Authentication (Аутентификация — AuthN): Кто ты?
    • Проверка личности (Логин + Пароль, Биометрия, SMS-код).
    • Результат: "Это Дмитрий".
  2. Authorization (Авторизация — AuthZ): Что тебе можно?
    • Проверка прав доступа.
    • Результат: "Дмитрию можно читать новости, но нельзя удалять базу данных".

2. OAuth 2.0 и OIDC

Почему мы не просим пользователя ввести пароль от Google в нашем приложении? Потому что пользователь не должен давать нам свой пароль. Для этого придумали OAuth 2.0.

Это протокол делегирования доступа. "Я (пользователь) разрешаю этому Приложению сходить в Google и взять только мое Имя и Email".

OpenID Connect (OIDC) — это надстройка над OAuth 2.0, которая стандартизирует процесс Аутентификации (добавляет ID Token).

Главные роли:

  • Resource Owner: Пользователь.
  • Client: Ваше приложение.
  • Authorization Server: Google / Facebook / Ваш Identity Server (Keycloak).
  • Resource Server: API, где лежат данные (Google Drive API).

Authorization Code Flow (Золотой стандарт)

Так работает кнопка "Войти через Google":

  1. Клиент перенаправляет браузер пользователя на Сервер Авторизации.
  2. Пользователь вводит пароль на сайте Google.
  3. Google спрашивает: "Разрешить приложению доступ?" Пользователь жмет "Да".
  4. Google редиректит пользователя назад в ваше приложение с коротким Authorization Code.
  5. Ваш бэкенд (не браузер!) берет этот Code, добавляет свой секретный ключ (Client Secret) и идет к Google напрямую.
  6. Google проверяет код и отдает Access Token (ключ от двери) и ID Token (паспорт пользователя).

3. Session vs JWT (JSON Web Token)

Где хранить состояние "Пользователь залогинен"?

A. Session-based (Reference Token)

  • Механизм: Сервер создает сессию, кладет её в Redis (session_id_123 -> {user: dmitry}), а пользователю отдает только ID в Cookies.
  • Плюс: Полный контроль. Можно в любой момент "разлогинить" пользователя (удалить ключ из Redis).
  • Минус: Stateful. При каждом запросе нужно ходить в Redis. Плохо для микросервисов (лишняя задержка).

B. Token-based (JWT — Value Token)

  • Механизм: Сервер упаковывает данные пользователя в JSON, подписывает своей криптографической подписью и отдает пользователю. Сервер ничего не хранит.
  • Плюс: Stateless. Микросервис получает токен, проверяет подпись (математика, без похода в базу) и верит ему. Отлично масштабируется.
  • Минус: Нельзя отозвать. Если хакер украл JWT, он имеет доступ, пока не истечет срок жизни токена (TTL).
  • Решение: Делать Access Token короткоживущим (5-15 мин) и использовать Refresh Token (который хранится в БД и который можно отозвать) для обновления.

4. Хэширование паролей

Никогда, ни при каких обстоятельствах не храните пароли в открытом виде. И даже в MD5.

Если хакер украдет базу, он увидит:

  • user: admin
  • pass: 5f4dcc3b5aa765d61d8327deb882cf99 (Гуглится как "password" за 1 секунду через Rainbow Tables).

Правильный рецепт: Salt + Slow Hash

  1. Salt (Соль): Случайная строка, добавляемая к паролю. У каждого юзера — своя уникальная соль. Это убивает Rainbow Tables (хакеру придется ломать каждый пароль отдельно).
  2. Slow Hash: Алгоритм должен быть медленным. Если MD5 считается за 1 наносекунду, то хакер переберет миллиарды паролей. Нам нужен алгоритм, который работает 100 мс. Для пользователя это незаметно, для хакера — вечность.

Стандарты индустрии:

  • Argon2 (Победитель конкурса хэширования).
  • bcrypt (Старый, надежный стандарт).
  • scrypt.

5. Практика: Работа с JWT и Паролями (Python)

Вам понадобятся pyjwt и bcrypt.

import jwt
import datetime
import bcrypt
import time

# --- 1. Хэширование паролей (bcrypt) ---

def hash_password(plain_password):
    # Генерация соли и хэширование. Work factor (rounds) по умолчанию 12.
    # Это займет время (ощутимую долю секунды)!
    salt = bcrypt.gensalt()
    hashed = bcrypt.hashpw(plain_password.encode('utf-8'), salt)
    return hashed

def check_password(plain_password, hashed_password):
    return bcrypt.checkpw(plain_password.encode('utf-8'), hashed_password)

# Демонстрация
user_pass = "superSecret123"
db_hash = hash_password(user_pass)

print(f"Пароль: {user_pass}")
print(f"В базе лежит: {db_hash}") # Выглядит как $2b$12$....
print(f"Проверка верного: {check_password('superSecret123', db_hash)}") # True
print(f"Проверка неверного: {check_password('wrong', db_hash)}")       # False

print("-" * 20)

# --- 2. Работа с JWT ---

SECRET_KEY = "my_super_secret_key_never_share_this"

def create_jwt(user_id):
    payload = {
        "sub": user_id, # Subject (кто)
        "role": "admin",
        # Обязательно ставим срок жизни (Expiration Time)
        "exp": datetime.datetime.utcnow() + datetime.timedelta(minutes=15),
        "iat": datetime.datetime.utcnow() # Issued At
    }
    
    token = jwt.encode(payload, SECRET_KEY, algorithm="HS256")
    return token

def verify_jwt(token):
    try:
        # Библиотека сама проверит подпись и exp (время жизни)
        decoded = jwt.decode(token, SECRET_KEY, algorithms=["HS256"])
        return decoded
    except jwt.ExpiredSignatureError:
        return "Error: Token expired"
    except jwt.InvalidTokenError:
        return "Error: Invalid token"

# Демонстрация
token = create_jwt(123)
print(f"JWT Token для клиента:\n{token}")

# Клиент шлет токен назад...
decoded_data = verify_jwt(token)
print(f"Данные из токена: {decoded_data}")

Анатомия JWT

Если вы возьмете строку token из кода выше и вставите на сайт jwt.io, вы увидите три части, разделенные точками:

  1. Header: Алгоритм шифрования (HS256).
  2. Payload: Данные ({"sub": 123, "role": "admin"}). Внимание: Это Base64, оно не зашифровано! Любой может прочитать содержимое токена. Не кладите туда секретные данные.
  3. Signature: Хэш от (Header + Payload + SecretKey). Это гарантия того, что данные не подделаны.

6. Шифрование данных (Encryption)

  • In Transit (В пути): Всегда используйте HTTPS (TLS 1.3). Это шифрует канал между клиентом и сервером. Внутри кластера (между микросервисами) используйте mTLS (Mutual TLS) — когда сервисы проверяют сертификаты друг друга.
  • At Rest (В покое):
    • Шифрование диска (AWS EBS Encryption, LUKS).
    • Шифрование колонок в БД (для паспортных данных, карт).
    • Управление ключами: Никогда не храните ключи шифрования рядом с данными. Используйте KMS (Key Management Service) или Vault.

Уровень 6: Практика и Реальный мир

==================================

Глава 1. Фреймворк решения задач (System Design Framework)


Чтобы спроектировать сложную систему и не сойти с ума, нужно двигаться по строгому алгоритму. Это "боевой устав" архитектора.

Шаг 1: Уточнение требований (Clarify Requirements) — 5-10 минут

Самая частая ошибка: Кандидат слышит "Спроектируй Twitter" и бросается рисовать схему. Интервьюер думает: "Он даже не спросил, какой именно Твиттер. Может, это внутренний чат для 5 человек?"

Никогда не делайте предположений. Задавайте вопросы, чтобы сузить задачу (Scope).

Вопросы, которые нужно задать:

  1. Функциональные требования (Что система делает?):
    • Мы постим твиты?
    • Мы подписываемся на людей?
    • Есть ли поиск?
    • Нужны ли личные сообщения? (Интервьюер может сказать: "Нет, давай сосредоточимся только на Ленте новостей").
  2. Нефункциональные требования (Как система работает?):
    • DAU (Daily Active Users): 1 тысяча или 100 миллионов? (Это определяет архитектуру).
    • CAP-теорема: Нам важнее Consistency (банкинг) или Availability (соцсеть)?
    • Read/Write Ratio: Мы больше читаем или пишем? (В Twitter читают в 100 раз чаще, чем пишут).

Шаг 2: Оценка нагрузок (Back-of-the-envelope calculations) — 5 минут

Вам нужно прикинуть цифры "на салфетке", чтобы понять масштаб бедствия.

Магические числа, которые надо помнить:

  • Секунд в сутках: ~86 400 (для простоты берите 100 000 или $10^{5}$ ).
  • Запросов в день на 1 миллион DAU: Если каждый юзер делает 10 запросов -> 10 млн запросов в сутки.
    • RPS (Requests Per Second): $10^{7}/10^{5}=100$ RPS. Это смешно, выдержит один ноутбук.
    • А если 100 млн DAU? Это уже 10 000 RPS (на среднем) и 50 000 RPS (в пике). Тут нужен кластер.

Пример расчета (хранилище для Instagram):

  • Юзеров: 100 млн.
  • Каждый грузит 1 фото в день.
  • Среднее фото: 2 МБ.
  • Трафик на запись: $100M\times 2MB/100k sec=200\times 10^{6}/10^{5}=2000MB/s$ (2 ГБ/сек).
  • Место за год: $100M\times 2MB\times 365=73PB$ (Петабайта).
  • Вывод: Нам точно нужен шардинг и объектное хранилище (S3), одна БД не вывезет.

Шаг 3: Высокоуровневый дизайн (High-Level Design) — 10-15 минут

Нарисуйте "вид с вертолета". Не вдавайтесь в детали. Покажите основные потоки данных.

Стандартная схема для веб-сервиса:

  1. Client (Mobile/Web).
  2. Load Balancer (на входе).
  3. Web Servers (API Gateway + Application Service).
  4. Database (хранение метаданных).
  5. Object Storage (для картинок/видео).

На этом этапе спросите интервьюера: "Выглядит разумно? На какой части вы хотите, чтобы я сосредоточился?"

Шаг 4: Детальный дизайн (Deep Dive) — 15-20 минут

Интервьюер укажет на узкое место. Например: "Как ты будешь генерировать ленту новостей для Джастина Бибера, у которого 100 млн подписчиков?"

Здесь вы достаете свой арсенал (Уровни 1-5):

  • Базы данных: SQL или NoSQL? (Для ленты лучше Cassandra/DynamoDB).
  • Кэширование: Где поставить Redis? (Cache-Aside для профилей).
  • Масштабирование: Как шардировать базу? (По user_id или tweet_id?).
  • Асинхронность: Используем Kafka для обработки загрузки видео.
  • Специфика: Geo-sharding для поиска такси.

Пример рассуждения (Twitter Feed):

"Если я буду делать SELECT * FROM tweets WHERE user_id IN (my_friends) — это убьет базу. Поэтому я применю Fan-out on Write. Когда Джастин постит твит, я асинхронно положу ID этого твита в готовые списки (Redis List) кэшей всех его подписчиков. Чтение будет мгновенным ( $O\left(1\right)$ ). Но для знаменитостей это слишком дорого, для них применим гибридный подход..."

Шаг 5: Поиск узких мест (Wrap up) — 5 минут

В конце окиньте взглядом свою схему и честно скажите, где она сломается. Идеальных систем нет, и интервьюер это знает. Ценится умение видеть риски.

  • "Единая точка отказа — Load Balancer (надо добавить второй в Active-Passive)".
  • "Если отключат электричество, мы потеряем данные в Redis (надо включить AOF/Snapshotting)".
  • "Latency может быть высокой для юзеров из Азии (надо добавить CDN)".

Шпаргалка: Паттерны для разных типов задач

  1. Read-Heavy (Twitter, Instagram, News):
    • Кэш везде (Redis + CDN).
    • Денормализация данных (храним готовые view).
    • Eventual Consistency допустима.
  2. Write-Heavy (IoT, Chat logs, Metrics):
    • Message Queue (Kafka) как буфер на входе.
    • NoSQL (Cassandra/HBase) или Time-Series DB.
    • Batch processing для аналитики.
  3. Transactional (Payment, Booking):
    • ACID (PostgreSQL/Oracle).
    • Strong Consistency.
    • Idempotency keys (защита от повторов).
  4. Search (Google, E-commerce catalog):
    • Elasticsearch (Inverted Index).
    • Crawler -> Parser -> Indexer.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment