Перейти к основному содержимому

Публичное собеседование по System Design

· 9 мин. чтения

Сегодня мы разберем публичное собеседование по системному дизайну, в ходе которого интервьюер (Вова Иванов) и кандидат (Денис Усов) совместно проектируют высоконагруженный аналог Google Docs с поддержкой совместного редактирования, комментариев и изображений. В процессе обсуждения кандидат продемонстрировал хорошее понимание доменной модели, предложил разумные подходы к хранению данных и обработке потоков изменений, однако недостаточно глубоко проработал нефункциональные требования, такие как производительность, безопасность и отказоустойчивость. Интервьюер отметил как сильные стороны — структурированность мышления и умение декомпозировать задачу, так и зоны роста — необходимость более тщательного сбора требований и формализации оценок ресурсов.

Вопрос 1. Расскажите о себе и своём опыте.

Таймкод: 00:00:48

Ответ собеседника: Правильный. Кандидат — Денис Усов, работает в компании Тинькофф последние 4 года, до этого около 15 лет писал на Java.

Правильный ответ:

Это вступительный вопрос, который задаётся для разогрева и понимания бэкграунда кандидата. Ответ должен быть структурированным и включать следующие ключевые блоки:

Общая информация

  • Имя, текущая должность, компания, общее количество лет в разработке.

Профессиональный путь

  • Хронология ключевых мест работы и технологический стек на каждом этапе.
  • Переход от Java к Go (если применимо) — почему и какие задачи решались на новом языке.

Ключевые достижения и проекты

  • Описание наиболее значимых проектов: архитектурные решения, масштаб, влияние на бизнес.
  • Примеры оптимизаций: снижение latency, повышение throughput, уменьшение стоимости инфраструктуры.

Роль в команде

  • Лидерство, менторинг, code review, участие в найме.
  • Взаимодействие с другими командами и стейкхолдерами.

Технические компетенции

  • Глубокое знание Go: конкурентность, профилирование, работа с базами данных, микросервисная архитектура.
  • Опыт с облачными платформами (Yandex Cloud, AWS, GCP), Kubernetes, CI/CD.

Пример хорошего ответа:

> «Меня зовут Денис, я Go-разработчик с четырёхлетним опытом в Тинькофф. До этого около 15 лет работал на Java, занимался backend-разработкой высоконагруженных систем. Перешёл на Go, когда команда начала переписывать критичные сервисы для снижения latency и потребления ресурсов. За это время спроектировал и внедрил несколько микросервисов, обрабатывающих тысячи запросов в секунду, участвовал в профилировании и оптимизации горячих путей, настраивал мониторинг через Prometheus и Grafana. В команде выступаю в роли технического лида — провожу code review, менторю джуниоров, участвую в проектировании API и выборе архитектурных решений.»

Такой ответ даёт интервьюеру чёткую картину: опыт, мотивация перехода на Go, глубина технических знаний и soft skills. Это задаёт позитивный тон для дальнейшего технического разговора.

Вопрос 2. Спроектируйте упрощённую версию Google Docs с совместным редактированием в реальном времени, комментариями, изображениями, системой прав доступа и аутентификацией для ~100 млн пользователей.

Таймкод: 00:03:18

Ответ собеседника: Правильный. Кандидат выстроил архитектуру из: балансировщика нагрузки, слоя API-сервисов (API Gateway) с авторизацией, сервиса авторизации с выдачей токена, хранилища профилей пользователей. Данные разделил на три хранилища: документы (key-value, например S3), картинки и комментарии. Комментарии предложил хранить сгруппированными по документу (JSON-блок), загружая одним запросом. Для совместного редактирования предложил поток событий через WebSocket с группировкой нажатий клавиш по времени (~0.5 сек). Для картинок описал 4-шаговый процесс: загрузка → локальное отображение → применение ссылки в документе → трансляция получателям. Предложил кэширование на уровне API-сервиса и маршрутизацию запросов к одному документу на один экземпляр сервиса. Для алгоритма совместного редактирования описал подход на основе позиций (номер строки, позиция в строке) с операциями вставки/удаления, аналогичный Operational Transformation. Признал, что не знаком детально с OT и CRDT, но интуитивно пришёл к схожему решению с серверной трансформацией операций. Для комментариев предложил key-value хранилище с ключом — ID документа, значением — массив комментариев. Упомянул использование UUID для уникальных ключей и шардирование по первым байтам UUID. Оценил суммарный объём хранилища документов в ~1 петабайт (100 млн пользователей × 10 документов × 1 МБ). Для организации кластера предложил несколько кластеров Kubernetes (3-4 штуки) для отказоустойчивости, упомянув, что единичный кластер Kubernetes является единой точкой отказа. Упомянул Cassandra как возможное решение для распределённого хранилища. Обсудил накладные расходы pod'ов (JVM), необходимость экспериментального подбора оптимального количества документов на один pod. Для обеспечения согласованности данных при совместном редактировании предложил схему, где клиент сначала отправляет изменения на сервер, а отображает их только после получения подтверждения через обратный поток событий. Отметил, что не успел детально обсудить модель прав доступа (RBAC), репликацию, disaster recovery, мониторинг, дата-центры и шифрование документов.

Правильный ответ:

Это комплексный вопрос на проектирование высоконагруженных систем (System Design). Рассмотрим эталонную архитектуру по ключевым компонентам.

1. Общая архитектура (High-Level Design)

Система делится на несколько логических слоёв:

  • Клиентская часть (Frontend): SPA-приложение (React/Vue) с редактором на основе ProseMirror, Slate.js или TipTap. Клиент поддерживает WebSocket-соединение для real-time обновлений и HTTP/REST или gRPC для остальных операций.
  • Edge/CDN: CloudFlare / Yandex Cloud CDN — раздача статики, изображений, защита от DDoS.
  • Load Balancer: L7-балансировка (Envoy, NGINX) с маршрутизацией по типам запросов: WebSocket → кластер real-time сервисов, HTTP → API Gateway.
  • API Gateway: Единая точка входа. Отвечает за rate limiting, аутентификацию (проверка JWT), маршрутизацию, агрегацию ответов. Решения: Kong, Tyk, или кастомный на Go.
  • Микросервисы:
    • Auth Service — регистрация, аутентификация, выдача и ротация JWT/OAuth2-токенов.
    • Document Service — CRUD документов, метаданные, версионирование.
    • Collaboration Service — обработка операций редактирования, разрешение конфликтов (OT/CRDT).
    • Comment Service — создание, чтение, привязка комментариев к документу/выделенному фрагменту.
    • Permission Service — RBAC/ACL модель, проверка прав доступа.
    • Media Service — загрузка, обработка, хранение изображений.
    • Notification Service — уведомления об изменениях, упоминаниях.

2. Хранение данных

Документы (основное содержимое)

Для содержимого документа используется гибридный подход:

  • Активные (редактируемые) документы хранятся в in-memory базе данных (Redis Cluster или кастомное решение) для обеспечения низкой latency при real-time операциях. Структура — JSON или бинарный формат с привязкой к document_id.
  • Персистентное хранение — распределённая документоориентированная БД (MongoDB) или колоночная (Cassandra). Для каждого документа хранится: document_id, owner_id, content (или ссылка на blob), version, created_at, updated_at, acl_rules.
  • Версионирование — снимки (snapshots) через заданные интервалы или по количеству операций (например, каждые 100 операций). Хранятся отдельно в объектном хранилище (S3/MinIO).

Изображения

  • Загрузка через Media Service → валидация (формат, размер до 50 МБ) → обработка (ресайз, генерация превью) → сохранение в объектное хранилище (S3/MinIO).
  • Метаданные изображения (image_id, document_id, position, width, height, url) хранятся в реляционной БД (PostgreSQL) или вместе с метаданными документа.
  • CDN для раздачи изображений конечным пользователям.

Комментарии

  • Хранение в реляционной БД (PostgreSQL) с таблицей: comment_id, document_id, author_id, content, anchor (привязка к выделенному фрагменту — offset + length или XPath), parent_comment_id (для тредов), created_at, resolved.
  • Альтернатива: хранить комментарии как массив JSON в key-value хранилище (Redis) с ключом doc:{id}:comments — быстрая загрузка всех комментариев документа одним запросом, но без гибких запросов по фильтрации.
  • Для масштаба 100 млн пользователей рекомендуется PostgreSQL с шардированием по document_id или выделенный сервис с собственной БД.

Профили пользователей

  • PostgreSQL или специализированное хранилище. Кэширование в Redis (TTL 15–30 мин).

3. Механизм совместного редактирования в реальном времени

Транспорт

  • WebSocket для двунаправленной связи клиент ↔ сервер. Альтернатива — SSE (Server-Sent Events) для входящих обновлений + HTTP для отправки операций.
  • Для масштабирования WebSocket используется sticky session или маршрутизация через consistent hashing: все запросы к документу X направляются на один и тот же сервер (или группу серверов).

Буферизация и батчинг на клиенте

  • Клиент группирует нажатия клавиш в пакеты (debouncing, ~200–500 мс) и отправляет как одну операцию, чтобы уменьшить трафик.
  • Каждая операция содержит: type (insert/delete/retain), position, content, client_id, client_version, timestamp.

Алгоритм разрешения конфликтов

Два основных подхода:

Operational Transformation (OT)

Классический подход, использованный в Google Docs. Сервер получает операции от клиентов, трансформирует их относительно параллельных операций и рассылает преобразованные версии.

Пример: два клиента одновременно вставляют текст в одну и ту же позицию. Сервер определяет порядок, трансформирует вторую операцию (сдвигает позицию на длину вставки первой) и отправляет обоим клиентам согласованные версии.

Сложность: алгоритм трансформации зависит от типа операций и структуры документа. Для текста — относительно просто, для структурированных документов (таблицы, вложенные элементы) — значительно сложнее.

CRDT (Conflict-free Replicated Data Types)

Более современный подход. Каждый символ (или элемент) получает уникальный идентификатор. Операции коммутативны и идемпотентны — конфликты разрешаются автоматически без серверной трансформации.

Пример: каждый символ представлен как (unique_id, value, visible). Вставка добавляет новый элемент с уникальным ID. Удаление — помечает как visible: false. Порядок определяется сортировкой по unique_id.

Преимущества CRDT:

  • Не требует центрального сервера для разрешения конфликтов (возможен peer-to-peer).
  • Проще в реализации для сложных структур.
  • Библиотеки: Yjs (JavaScript), Automerge.

Недостатки:

  • Больший объём метаданных (каждый символ имеет ID).
  • Для документа 1 МБ (~1 млн символов) — значительный оверхед.

Рекомендация для данного масштаба: использовать CRDT (Yjs-совместимый протокол) на клиенте и OT-подобный серверный подход для финализации состояния. Это даёт баланс между простотой и производительностью.

Согласованность

  • Клиент отправляет операцию на сервер. Сервер применяет её, присваивает глобальный version_number и рассылает всем подписчикам документа.
  • Клиент отображает изменения только после подтверждения от сервера (optimistic UI с откатом при конфликте).
  • Для обеспечения строгой согласованности все операции к одному документу проходят через один сервер (лидер для документа).

4. Система прав доступа

Модель RBAC с расширениями

  • Роли: owner, editor, commenter, viewer.
  • ACL (Access Control List): для каждого документа хранится список [(user_id, role), ...].
  • Наследование: поддержка папок/пространств с наследованием прав.
  • Ссылки с доступом: генерация токена для доступа по ссылке (link_token) с ограничением по сроку и роли.

Реализация:

  • Permission Service проверяет права при каждом запросе (через middleware в API Gateway).
  • Кэширование прав в Redis с TTL (например, 5 мин). Инвалидация при изменении ACL.
  • Для real-time подключений: проверка прав при установке WebSocket-соединения. При изменении прав — принудительное отключение через сервер.

5. Оценка ресурсов

Хранище данных

  • 100 млн пользователей × 10 документов × 1 МБ (средний размер) = 1 ПБ для документов.
  • Изображения: 100 млн × 5 изображений × 500 КБ = 250 ТБ.
  • Комментарии: 100 млн × 20 комментариев × 1 КБ = 2 ТБ.
  • Итого: ~1.25–1.5 ПБ с учётом репликации (×3) и снапшотов.

Нагрузка

  • DAU (Daily Active Users): ~30 млн (30% от общего числа).
  • Одновременных подключений (WebSocket): ~3 млн (10% от DAU).
  • Операций редактирования: ~100 000 ops/sec (в пиковые часы).
  • Загрузка документов: ~50 000 req/sec.
  • Загрузка изображений: ~10 000 req/sec.

Инфраструктура

  • Real-time сервисы (WebSocket + Collaboration): 200–300 инстансов (Go, каждый обслуживает ~10 000 соединений). Размещение в нескольких дата-центрах.
  • API-сервисы: 50–100 инстансов (Go, горизонтальное масштабирование).
  • Redis Cluster: 10–20 нод для кэширования и хранения активных документов.
  • PostgreSQL: кластер с шардированием (10–20 шардов), репликация (master + 2 реплики).
  • S3/MinIO: кластер объектного хранилища с erasure coding.
  • Kafka: для асинхронных событий (уведомления, аудит, аналитика). Кластер из 10–20 брокеров.
  • Kubernetes: 3–4 кластера в разных зонах доступности для отказоустойчивости.

6. Дополнительные аспекты

Репликация и Disaster Recovery

  • Междата-центровая репликация данных (синхронная для метаданных, асинхронная для контента).
  • RPO (Recovery Point Objective): 1 минута. RTO (Recovery Time Objective): 5 минут.
  • Регулярные бэкапы в отдельный регион.

Мониторинг

  • Метрики: Prometheus + Grafana (latency, error rate, WebSocket connections, memory usage).
  • Логирование: ELK Stack или Loki.
  • Трейсинг: Jaeger / Zipkin для распределённого трейсинга.
  • Алертинг: PagerDuty / Opsgenie.

Безопасность

  • TLS для всех соединений (включая WebSocket — WSS).
  • Шифрование документов в состоянии покоя (at rest) с использованием KMS.
  • Аудит действий: логирование всех операций изменения прав, удаления, экспорта.
  • Rate limiting на уровне API Gateway (per user, per IP).

Оптимизация загрузки изображений

  • Клиент сначала отображает изображение из локального кэша (optimistic UI), затем загружает на сервер.
  • После загрузки сервер возвращает image_id и url, которые вставляются в документ как операция.
  • Все участники получают обновление через WebSocket-поток.