Real-Time Backend

Design: Collaborative IDE

Replit позволяет 20 млн пользователей писать код вместе в браузере. VS Code Live Share запускают сотни тысяч pair programming сессий в день. Что происходит под капотом, когда два разработчика одновременно правят одну строку кода?

  • **VS Code Live Share** использует peer-to-peer архитектуру с relay-сервером: хост держит реальный Language Server, гость получает LSP-ответы через туннель. Latency для autocomplete - менее 150 мс при трансатлантическом соединении.
  • **GitHub Codespaces** запускает container-per-session в Azure: каждый workspace - это отдельный pod с 2-8 ядрами CPU и 4-16 ГБ RAM. Стоимость от USD 0.18/час для 2-core. Prebuild'ы сокращают cold start с 3 минут до 10 секунд.
  • **Replit Multiplayer** обрабатывает конкурентное редактирование через OT (Operational Transformation) с централизованным сервером трансформаций. Shared terminal реализован на Go с PTY multiplexer'ом, поддерживающим до 50 участников на одну сессию.
  • **Gitpod** использует Kubernetes + Theia/VS Code в браузере. Workspace snapshot'ы позволяют остановить контейнер и восстановить точное состояние включая запущенные процессы через CRIU (Checkpoint/Restore in Userspace).

Архитектура Collaborative IDE

Collaborative IDE - это система реального времени, где несколько разработчиков одновременно редактируют код, видят курсоры друг друга и запускают один терминал. Replit обслуживает более 20 млн пользователей на такой архитектуре. Главный вызов: сохранить консистентность состояния редактора при конкурентных изменениях и при этом удержать latency ниже 100 мс.

GitHub Codespaces решает задачу радикально: каждая сессия - это отдельный Docker-контейнер с полной копией окружения. Пользователь получает изолированный workspace, container-per-session, а весь трафик редактора проксируется через WebSocket-туннель. VS Code Live Share выбрал другой подход: peer-to-peer с relay-сервером, без контейнера - гость видит файлы хоста через специальный протокол обмена дельтами.

Два фундаментальных паттерна

  • **Container-per-session** (Codespaces, Replit): полная изоляция, но дорогой cold start (3-8 сек). Каждый workspace - отдельный под в Kubernetes. Масштаб: тысячи подов на кластер.
  • **Host-relay-guest** (VS Code Live Share): хост держит реальный LS-процесс, гость получает только diff'ы. Дешевле, но хост должен быть онлайн.
  • **Shared remote VM** (Gitpod): один VM на сессию, несколько участников подключаются к нему. Золотая середина по стоимости.

Ключевой компонент - Collab Gateway: сервис, который принимает операции от всех клиентов, пропускает их через OT (Operational Transformation) или CRDT-движок, и рассылает результирующие дельты обратно. Именно здесь решается проблема конкурентных изменений: если A и B одновременно вставили символ в одну позицию, OT трансформирует одну из операций так, чтобы итоговый документ у всех оказался одинаковым.

CRDT (Conflict-free Replicated Data Type) - математически гарантирует сходимость без центрального координатора. YATA, используемый в Yjs, и LOGOOT дают eventual consistency при любом порядке применения операций.

GitHub Codespaces использует паттерн container-per-session. Главная причина такого выбора:

LSP в collaborative-контексте

Language Server Protocol (LSP) - это JSON-RPC протокол, стандартизированный Microsoft в 2016 году. Редактор (client) и языковой сервер (server) общаются через stdin/stdout или TCP. Одно сообщение выглядит так: `{"jsonrpc": "2.0", "method": "textDocument/completion", "params": {...}}`. Языковой сервер держит полную модель проекта в памяти - индекс символов, AST, граф зависимостей.

Проблема: один LS на N пользователей

В single-user IDE один LSP-процесс на один проект - всё просто. В collaborative IDE несколько клиентов отправляют `textDocument/didChange` одновременно. LS должен видеть консистентное состояние документа, иначе автодополнение и диагностика выдадут мусор.

  1. **Один LS на workspace** (Replit, Codespaces): все клиенты подключаются к одному LS-процессу через LSP Proxy. Proxy сериализует `didChange`-нотификации - LS всегда видит документ в порядке, определённом OT-движком.
  2. **LS на стороне хоста** (VS Code Live Share): гость не общается с LS напрямую. Хост проксирует LSP-запросы от гостя через relay, добавляет к ответу информацию о позиции курсора гостя.
  3. **LSP Multiplexer**: отдельный сервис принимает LSP от всех клиентов, применяет трансформации позиций (т.к. у каждого клиента своя версия документа в полёте), и передаёт единый поток в LS.

Проблема позиций: LSP использует {line, character} для указания места в документе. При конкурентных изменениях позиция, актуальная для клиента A, может быть устаревшей после применения операции клиента B. LSP Proxy обязан трансформировать позиции запросов перед отправкой в LS.

Golang LSP (gopls) и rust-analyzer держат полный workspace index в памяти - от 200 МБ до 2 ГБ для крупных проектов. При container-per-session LS запускается холодным при первом подключении. Codespaces решает это prebuild'ами: LS прогревается по расписанию на бэкграунде, snapshot сохраняется, новый контейнер восстанавливает состояние за секунды.

Два клиента одновременно отправляют `textDocument/didChange` в LSP Proxy. Какой компонент отвечает за то, чтобы Language Server получил консистентное состояние документа?

Shared Terminal: PTY Multiplexing

Shared terminal в collaborative IDE - это не просто трансляция текста. Реальный терминал работает через PTY (pseudo-terminal): ядро создаёт пару master/slave, shell пишет в slave, пользователь читает из master. Для sharing'а нескольким клиентам нужен PTY Multiplexer - процесс, который читает из PTY master и рассылает поток нескольким WebSocket-подключениям одновременно.

tmux и screen используют этот же принцип для multiplexing'а уже десятки лет. Replit реализует собственный PTY mux на Go, который обрабатывает resize-события (SIGWINCH): при изменении размера окна у одного участника сервер должен решить, чей размер приоритетный - иначе shell начнёт переносить строки в неожиданных местах.

Права и безопасность

  • **Read-only mode**: гость видит вывод терминала, но не может вводить команды. Реализуется фильтрацией stdin на уровне multiplexer'а.
  • **Write mode**: гость пишет в PTY master напрямую. При конкурентном вводе символы смешиваются - проблема решается через input ownership: в каждый момент только один участник "владеет" stdin.
  • **Audit log**: все keystrokes записываются для compliance. Replit Enterprise хранит session recordings в object storage (S3-compatible).
  • **Размер терминала**: при разных размерах окон используется минимум по обоим измерениям (min-width, min-height), чтобы вывод был корректным у всех участников.

xterm.js - браузерный VT100/xterm-совместимый рендерер, используемый в VS Code, Replit и Gitpod. Принимает поток байт с escape-последовательностями и рендерит их в canvas. WebGL-рендерер xterm.js обрабатывает до 1 млн строк без просадки FPS.

Два участника collaborative session смотрят на shared terminal с разными размерами окна: 80x24 и 120x40. Какая стратегия resize корректна?

File Sync: от WebSocket до CRDT

Синхронизация файлов в collaborative IDE работает на двух уровнях: **document-level** (открытый файл в редакторе) и **filesystem-level** (изменения на диске от внешних процессов - npm install, git checkout, компилятор). Первый уровень покрывается OT/CRDT. Второй - принципиально сложнее.

Document-level: Yjs и CRDT

Yjs - наиболее популярная CRDT-библиотека для collaborative editing. VS Code использует модифицированный Yjs в Live Share. Yjs представляет документ как последовательность Items, каждый из которых имеет уникальный ID (clientID + clock). При merge двух независимых изменений Yjs детерминированно определяет порядок вставки по ID - результат одинаков у всех клиентов вне зависимости от порядка получения операций.

Filesystem-level: inotify + delta sync

Когда `npm install` добавляет 50 000 файлов в node_modules, наивная синхронизация всего дерева невозможна. Gitpod и Replit используют selective sync: node_modules, .git и build-артефакты исключаются из real-time sync по аналогии с .gitignore. Для остальных файлов сервер слушает inotify-события и отправляет клиентам только дельты изменённых файлов.

  1. **inotify** (Linux) / **FSEvents** (macOS): ядро уведомляет о create/modify/delete на уровне inodes. Сервер подписывается на workspace-директорию.
  2. **Debounce**: быстрые последовательные изменения одного файла (компилятор пишет inkrementally) группируются с задержкой 50-100 мс перед отправкой клиентам.
  3. **Content-addressed cache**: файлы хешируются (SHA-256). Если клиент уже имеет версию с таким хешем - delta не отправляется. Экономит трафик при git checkout.
  4. **Conflict detection**: если клиент редактировал файл локально, а с сервера пришла внешняя правка - система уведомляет о конфликте и предлагает merge через diff3.

Rsync-алгоритм (rolling checksum) используется для эффективной синхронизации больших бинарных файлов: вместо полного файла передаётся список совпадающих блоков + только изменённые части. Это позволяет синхронизировать 10 МБ файл при изменении 100 байт.

Collaborative IDE - это просто shared Google Doc для кода: один документ, один поток изменений, всё просто

Collaborative IDE сочетает минимум 4 независимых подсистемы с разными гарантиями консистентности: OT/CRDT для текста, LSP Proxy для language intelligence, PTY Mux для терминала, inotify+delta для файловой системы

Каждая подсистема имеет свою модель конкурентности. Текстовый редактор требует character-level CRDT. LSP требует трансформации позиций. Терминал требует PTY-мультиплексирования с ownership'ом stdin. Файловая система требует selective sync с debounce. Попытка решить всё одним механизмом приводит к либо неприемлемой latency, либо некорректному поведению.

Почему node_modules обычно исключают из real-time file sync в collaborative IDE?

Итоги

  • **OT/CRDT - фундамент**: без трансформации конкурентных операций два клиента неизбежно придут к разному состоянию документа. Yjs (CRDT) даёт eventual consistency без центрального координатора, OT требует сервера-арбитра.
  • **LSP Proxy сериализует доступ к Language Server**: LS не знает о collaborative-контексте. Прокси трансформирует позиции в LSP-запросах и гарантирует, что LS видит документ в консистентном порядке.
  • **PTY multiplexer - не просто трансляция байт**: shared terminal требует решения проблем resize (min-size стратегия), ownership stdin (только один пишет в момент времени) и audit logging.
  • **File sync работает на двух уровнях**: document-level (CRDT для открытых файлов) и filesystem-level (inotify + delta sync для изменений от внешних процессов). node_modules и build-артефакты исключаются из real-time sync.

Связанные темы

Collaborative IDE строится на пересечении нескольких систем:

  • Operational Transformation — Алгоритм, обеспечивающий консистентность при конкурентных изменениях текста - основа collaborative editing
  • WebSocket и real-time транспорт — Транспортный уровень для доставки операций между клиентами и сервером с минимальной latency
  • Kubernetes и контейнеризация — Container-per-session паттерн (Codespaces, Gitpod) требует оркестрации тысяч подов с быстрым lifecycle

Вопросы для размышления

  • Codespaces и Replit выбрали разные архитектуры (container-per-session vs shared infra). При каких условиях container-per-session перестаёт быть экономически оправданным?
  • VS Code Live Share не запускает отдельный контейнер - гость работает через хоста. Какие security-ограничения это накладывает на функциональность гостя?
  • PTY multiplexer должен решить, чей размер терминала приоритетный при разных размерах окон участников. Какие альтернативы стратегии min-size существуют и в каких сценариях они лучше?

Связанные уроки

  • sd-01-intro
Design: Collaborative IDE

0

1

Войти