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 должен видеть консистентное состояние документа, иначе автодополнение и диагностика выдадут мусор.
- **Один LS на workspace** (Replit, Codespaces): все клиенты подключаются к одному LS-процессу через LSP Proxy. Proxy сериализует `didChange`-нотификации - LS всегда видит документ в порядке, определённом OT-движком.
- **LS на стороне хоста** (VS Code Live Share): гость не общается с LS напрямую. Хост проксирует LSP-запросы от гостя через relay, добавляет к ответу информацию о позиции курсора гостя.
- **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-события и отправляет клиентам только дельты изменённых файлов.
- **inotify** (Linux) / **FSEvents** (macOS): ядро уведомляет о create/modify/delete на уровне inodes. Сервер подписывается на workspace-директорию.
- **Debounce**: быстрые последовательные изменения одного файла (компилятор пишет inkrementally) группируются с задержкой 50-100 мс перед отправкой клиентам.
- **Content-addressed cache**: файлы хешируются (SHA-256). Если клиент уже имеет версию с таким хешем - delta не отправляется. Экономит трафик при git checkout.
- **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 существуют и в каких сценариях они лучше?