Real-Time Backend
Design: Google Docs
50 человек редактируют один документ одновременно. Каждый видит изменения других в реальном времени. Никаких конфликтов, никаких 'кто-то сохранил поверх'. Как это работает технически - и почему это одна из сложнейших задач распределённых систем?
- Google Docs обрабатывает миллиарды операций в день - каждое нажатие клавиши становится транзакцией в распределённой системе
- Figma перешла с OT на CRDT в 2021 году, получив offline-first редактирование и возможность работы без центрального сервера-арбитра
- Notion, Linear, Coda, Quip - все построены на схожих принципах: operation log + WebSocket + ephemeral presence
- Microsoft Word Online и Apple iWork используют аналогичную архитектуру - collaborative editing стал commodity-фичей, но инфраструктура за ним нетривиальна
Архитектура Google Docs
В 2006 году Google купила стартап Writely за USD 1M и полностью переписала его. Результат - система, где 50+ пользователей редактируют один документ одновременно, а конфликты не просто решаются - они предотвращаются на уровне протокола.
Ядро архитектуры - **Operational Transformation (OT)**. Каждое нажатие клавиши становится операцией: `insert(pos, char)` или `delete(pos)`. Клиент применяет операцию локально немедленно (optimistic update), затем отправляет на сервер. Сервер сериализует все входящие операции в глобальный лог и возвращает трансформированные версии остальным клиентам.
Сервис делится на три компонента. **Docs Frontend** - stateless Node.js, только HTTP/WebSocket. **Docs Backend** - stateful Go-сервисы, хранят in-memory state документа и очередь операций. **Storage** - Bigtable для текущего состояния + Cloud Spanner для метаданных и прав доступа.
Google использует **channel-based модель**: каждый открытый документ - это channel с уникальным ID. WebSocket-соединение привязано к channel. При падении сервера клиент переподключается, получает diff от последнего revision и продолжает работу без потери данных.
Клиент A вставил 'X' в позицию 3, клиент B одновременно вставил 'Y' в позицию 3. Оба работают на revision 5. Что делает OT-сервер?
CRDT vs OT: эволюция подхода
OT работает, но у него есть болезненная точка: централизованный сервер обязан сериализовать все операции. При сетевом разрыве клиент не может продолжать работу автономно - версии разойдутся. Поэтому появились **CRDT (Conflict-free Replicated Data Types)**.
CRDT математически гарантирует: любые два узла, получившие одинаковый набор операций в произвольном порядке, придут к одному результату. Figma перешла с OT на CRDT в 2021 году - это дало offline-first редактирование и peer-to-peer синхронизацию без центрального сервера-арбитра.
Для текстовых документов используют **Logoot** или **LSEQ** - алгоритмы на основе fractional indexing. Каждый символ получает уникальный дробный идентификатор между соседями. Вставка 'X' между символами с ID 0.5 и 0.6 получает ID 0.55 - и никогда не конфликтует с параллельной вставкой 'Y' с ID 0.57.
- OT (Google Docs, 2006) — Требует центральный сервер-арбитр. Операции трансформируются попарно. Сложная реализация (алгоритм Jupiter). Нет offline - без сервера конфликты не разрешить.
- CRDT (Figma, Notion, Linear) — Без центрального арбитра. Операции коммутативны по математике. Проще реализация merge. Полноценный offline-first. Минус: tombstones раздувают документ.
Notion использует CRDT для блочного редактора с 2022 года. Linear - для синхронизации issue в реальном времени. Automerge и Yjs - популярные open-source библиотеки CRDT, на которых строят редакторы. Prosemirror + Yjs - стандартный стек для collaborative editing.
Пользователь работает в Google Docs при потере интернета. Операции накапливаются локально. Когда соединение восстановится - какой подход корректно обработает накопленные изменения?
Cursors и Presence
Цветные курсоры других пользователей - одна из самых узнаваемых фич Google Docs. За ними скрывается отдельный ephemeral-канал, полностью независимый от канала синхронизации документа.
Presence-данные (позиция курсора, выделение, имя пользователя) - это **ephemeral state**: не сохраняется в базе, не входит в revision log, не нужна consistency. Их отправляют через отдельный WebSocket-канал с TTL 3-5 секунд. Нет heartbeat - курсор исчезает автоматически.
Проблема: когда другой пользователь вставляет текст выше курсора, позиция курсора съезжает. Её нужно трансформировать так же, как операции OT. В CRDT это проще: курсор привязан к символу по ID, а не к абсолютной позиции.
Figma решила presence через привязку к объектам: каждый объект на canvas имеет UUID, курсор привязан к UUID + local offset. Перемещение других объектов не сдвигает чужие курсоры. Notion использует похожую схему для block-level cursors - курсор указывает на blockId, не на символьную позицию.
- **Throttling**: presence обновляется не чаще 50ms (20fps) - избыток WebSocket-трафика при быстром печатании
- **Color assignment**: детерминированный хэш userId → HSL-цвет, чтобы один пользователь всегда был одного цвета
- **Away detection**: 30 секунд без активности - курсор становится полупрозрачным, 5 минут - исчезает
- **Reconnect**: при переподключении клиент сразу отправляет presence-update, не ждёт следующего ввода
Пользователь B смотрит на документ: курсор A находится на позиции 100. Пользователь A вставляет 10 символов в позицию 50. Что произойдет с отображаемой позицией курсора A на экране B?
Version History и Permissions
История версий Google Docs хранит не снапшоты документа, а **operation log** - цепочку всех OT-операций с timestamp и authorId. Восстановление версии = replay операций до нужного moment. Это экономит хранилище: лог операций в 10-100x меньше набора снапшотов.
Но хранить каждый keystroke навечно нерационально. Google применяет **compaction**: операции за одну сессию (непрерывная работа без длительных пауз) сжимаются в один 'named version'. Named versions (которые пользователь сохранил вручную) не сжимаются никогда.
Система прав доступа строится на **три уровня**: Owner, Editor, Commenter, Viewer. Права хранятся в Cloud Spanner (strong consistency - важно, чтобы отзыв прав сработал немедленно для всех регионов). При смене прав сервер разрывает WebSocket-соединение для downgraded пользователей.
Права проверяются дважды: на WebSocket-upgrade (грубая проверка) и при каждой операции (точная). Это защита от race condition: пользователь открыл документ с правами editor, его права отозвали 5 секунд спустя - следующая операция будет отклонена без повторной проверки открытия сессии.
| Компонент | Хранилище | Причина |
|---|---|---|
| Текущий документ | Bigtable | Высокий throughput, низкая latency |
| Operation log | Bigtable | Append-only, range scan по revisionId |
| Метаданные / права | Cloud Spanner | Strong consistency, ACID-транзакции |
| Presence / cursors | Redis Pub/Sub | Ephemeral, не нужна persistence |
| Attachments / images | Google Cloud Storage | Blob-хранилище, CDN |
Google Docs хранит снапшот документа при каждом изменении - отсюда и история версий
История версий строится на operation log (цепочке OT-операций). Снапшоты создаются редко - только для ускорения восстановления, не как основной механизм хранения версий.
Operation log в 10-100x компактнее набора снапшотов. Документ на 100KB при 10 000 изменениях: снапшоты = 1GB, лог операций = 10-50MB. Кроме того, лог даёт детальный blame: кто, что и когда изменил.
Документ создан год назад, 50 000 операций в логе. Пользователь хочет посмотреть версию трёхмесячной давности. Как система находит это состояние эффективно?
Итоги
- **OT (Operational Transformation)** - каждое нажатие клавиши = операция, сервер трансформирует конкурирующие операции и сериализует их в глобальный лог
- **CRDT** - математически гарантированная сходимость без центрального арбитра; операции коммутативны, offline-first из коробки
- **Presence / cursors** - ephemeral-канал, полностью отдельный от синхронизации документа; TTL 3-5 сек, не персистируется
- **Version history** = operation log + редкие снапшоты для быстрого восстановления; replay операций, а не хранение снапшотов при каждом изменении
- **Права доступа** в Cloud Spanner (strong consistency) - проверяются при каждой операции, не только при открытии сессии
Связанные темы
Google Docs - пересечение нескольких фундаментальных областей distributed systems
- WebSocket и SSE — транспортный слой для операций и presence
- CAP Theorem — выбор между consistency и availability при network partition
- Event Sourcing — operation log - частный случай event sourcing; state = replay событий
- Redis Pub/Sub — механизм broadcast presence-обновлений между серверами
Вопросы для размышления
- Если бы нужно было выбрать между OT и CRDT для нового collaborative-редактора - какие вопросы задать заказчику, чтобы сделать правильный выбор?
- Presence-данные (курсоры) не сохраняются в базе. Какие ещё данные в реальных приложениях являются ephemeral и почему их не нужно персистировать?
- Права доступа проверяются при каждой операции, а не только при открытии сессии. Это накладные расходы. Как можно оптимизировать эту проверку без потери безопасности?