
Форк эталонной реализации AT Protocol с оптимизированным по производительности AppView, индексером firehose на Rust, кэшированием Redis и функциями сообщества для самостоятельного хостинга социальных сетей в масштабе.
Это форк эталонной реализации AT Protocol от Blacksky, созданный компанией Bluesky Social PBC. Он обеспечивает работу AppView по адресу api.blacksky.community.
Мы публикуем этот код для прозрачности и чтобы другие сообщества могли воспользоваться нашей работой. Этот репозиторий не принимает вклады, issues или PR. Если вам нужна каноническая реализация atproto, используйте bluesky-social/atproto.
Все изменения находятся в packages/bsky (логика AppView), services/bsky (конфигурация времени выполнения) и одной пользовательской миграции. Всё остальное — из вышестоящего репозитория.
В вышестоящем dataplane входит потребитель firehose на TypeScript (subscription.ts), который индексирует события напрямую. Мы заменили его на rsky-wintermute — индексатор на Rust, по нескольким причинам:
Dataplane и AppView из этого репозитория по-прежнему работают как есть. Они читают из базы данных PostgreSQL, в которую пишет wintermute. Мы просто не запускаем встроенную подписку на firehose.
Эти исправления полезны всем, кто разворачивает AppView самостоятельно.
Оптимизация запросов с LATERAL JOIN (packages/bsky/src/data-plane/server/routes/feeds.ts)
getTimeline и getListFeed переписаны с использованием PostgreSQL LATERAL JOIN, чтобы принудительно использовать индексы по пользователям вместо полного сканирования таблиц. Значительное улучшение для пользователей, следящих за тысячами аккаунтов.Кэширующий слой Redis (packages/bsky/src/data-plane/server/cache/)
Timestamp теряют метод .toDate(), что приводит к неполной гидратации профилей при попадании в кэш. В настоящее время мы работаем с отключённым кэшированием Redis. Исправление: сериализовать метки времени как ISO-строки при записи в кэш и восстанавливать при чтении.Серверное применение предпочтений уведомлений (packages/bsky/src/api/app/bsky/notification/listNotifications.ts)
reasons, сервер применяет сохранённые предпочтения уведомлений пользователя. Без этого предпочтения применяются только на стороне клиента и не имеют эффекта.Исправление устаревшего ключа подписи в верификаторе аутентификации (packages/bsky/src/auth-verifier.ts)
forceRefresh) обходится кэш идентификаторов dataplane в памяти и DID-документ напрямую запрашивается из PLC-каталога. Исправляет ошибки аутентификации после миграции аккаунта, когда ключ подписи меняется, но в кэше остаётся старый.Очистка JSON (packages/bsky/src/data-plane/server/routes/records.ts)
\u0000) и управляющие символы. Они допустимы по RFC 8259, но отвергаются JSON.parse() в Node.js, что приводит к незаметным сбоям rowToRecord в dataplane, которые проявляются как отсутствующие посты.Инфраструктура для приватных постов сообщества, которые хранятся в AppView, а не на отдельных PDS. Специфично для того, как работает Blacksky, но может служить примером для других сообществ.
community.blacksky.feed.* с конечными точками для отправки, получения, удаления, ленты и просмотра тредовcommunity_post (миграция: 20260202T120000000Z-add-community-post.ts)getPostThreadV2 для смешанных тредов из стандартных и сообщественных постовBLACKSKY_MEMBERSHIP_DB_URL)Bluesky Relay (bsky.network)
|
v
rsky-wintermute -----> PostgreSQL 17 <----- Palomar
(индексатор на Rust) | (поиск на Go)
- потребитель firehose | |
- обратная загрузка | v
- индексатор меток | OpenSearch
- прямая индексация |
v
bsky-dataplane (gRPC :2585) <--- Redis (опционально)
|
v
bsky-appview (HTTP :2584)
|
v
Обратный прокси (Caddy/nginx)
Wintermute — монолитная служба на Rust с четырьмя параллельными путями обработки:
bsky.network через WebSocket, записывает события в очереди Fjall (встраиваемое key-value хранилище)ON CONFLICT для идемпотентностиДополнительные CLI-инструменты, включённые в репозиторий rsky:
queue_backfill — поставить DID в очередь обратной загрузки из CSV, из обнаружения PDS или прямого списка DIDdirect_index — загрузить и проиндексировать конкретные репозитории, минуя очереди (полезно для исправления отдельных аккаунтов)label_sync — повторно воспроизвести потоки меток с курсора 0, чтобы наверстать пропущенные отменыplc_import — массовый импорт соответствий handle/DID из PLC-каталогаpalomar-sync — синхронизировать количество подписчиков и PageRank в OpenSearchСервис загрузки видео для пользователей, чей PDS не поддерживает video.bsky.app от Bluesky. Использует собственный DID (did:web:video.blacksky.community) для аутентификации в PDS пользователя через JWT служебной аутентификации. Процесс:
Метки модерации поступают от сервисов-лейблеров (например, Ozone от Bluesky) через подписку WebSocket. Ingester от Wintermute обрабатывает метки в выделенной очереди label_live (малый объём, отдельно от основного firehose). Инструмент label_sync может воспроизвести полный поток лейблера, чтобы наверстать пропущенные отмены меток (удаление меток) без повторной вставки меток.
bskyСхема bsky создаётся миграциями dataplane. При первом запуске dataplane автоматически применит все миграции. Единственная специфичная для Blacksky миграция — 20260202T120000000Z-add-community-post.ts (таблица сообщений сообщества). Если вам не нужны сообщения сообщества, удалите её.
rsky-wintermute пишет в ту же схему. Все его INSERT используют ON CONFLICT, поэтому безопасно запускать wintermute и миграции dataplane в любом порядке.
pnpm install
pnpm build
node services/bsky/dataplane.js
node services/bsky/api.js
Полная обратная загрузка всей сети (все ~42 млн пользователей, ~18,5 млрд записей) занимает недели даже при параллельной обработке wintermute. Ожидайте:
Во время обратной загрузки AppView работает, но будет показывать неполные данные для пользователей, которые ещё не обработаны. Live-события индексируются немедленно независимо от прогресса обратной загрузки.
Это проблемы, с которыми мы столкнулись при начальной настройке AppView для всей сети. Если вы делаете то же самое, вы, вероятно, столкнётесь с некоторыми из них:
Повреждение JSON в текстовом формате COPY: Текстовый протокол COPY в PostgreSQL обрабатывает обратную косую черту как escape-символ. Если ваш массовый загрузчик не экранирует обратную косую черту в JSON-строках, \" становится ", и вы получаете незаметно повреждённые записи. Столбец record.json имеет тип text (не jsonb), поэтому PostgreSQL не обнаружит этого. Мы нашли ~66 000 повреждённых записей и пришлось их восстанавливать, повторно загружая из публичного API.
Нулевые байты в JSON: Некоторые записи AT Protocol содержат \u0000 (нулевой байт), что является допустимым JSON по RFC 8259, но отвергается JSON.parse() в Node.js. Dataplane молча возвращает null для таких записей. Удаляйте нулевые байты перед записью в базу данных.
Чувствительность к формату временных меток: Dataplane ожидает временные метки с точностью до миллисекунд и суффиксом Z (2026-01-12T19:45:23.307Z). С точностью до наносекунды или в формате смещения часового пояса (+00:00) это вызывает тонкие проблемы сортировки и сравнения.
Разрастание таблицы уведомлений: Без уникального ограничения на (did, recordUri, reason) таблица уведомлений бесконечно растёт с дубликатами. Наша достигла 1,3 миллиарда строк (663 ГБ), прежде чем мы это заметили. Добавление ON CONFLICT DO NOTHING в INSERT помогает только в том случае, если уникальный индекс существует заранее, а создание индекса требует дедупликации существующих данных.
Таблицы вложений постов: Таблицы post_embed_image и post_embed_video не заполняются по умолчанию, если ваш индексатор их не обрабатывает. Без них медиа-фильтр в getAuthorFeed ничего не возвращает. Их нужно заполнять отдельно.
Порядок отмены меток: События отмены меток (удаление) ссылаются на оригинальную метку по источнику, URI и значению. Если отмена приходит раньше оригинальной метки (часто при обратной загрузке), она молча отбрасывается. Инструмент label_sync воспроизводит полный поток, чтобы отловить такие случаи.
Отравление очередей Fjall: Встроенная база данных Fjall (используется для очередей wintermute) может перейти в «отравленное» состояние после сбоев, блокируя все операции с очередями. Исправление: удалите каталог с очередями и перезапустите — wintermute наверстает упущенное с курсора реле (реле хранят около 72 часов истории).
Инициализация провайдера TLS: rustls требует явной установки криптопровайдера перед любым TLS-соединением. Без rustls::crypto::aws_lc_rs::default_provider().install_default() при запуске первое WebSocket-соединение с firehose вызывает панику.
Смена ключа подписи после миграции аккаунта: Когда пользователи переходят между PDS, их ключ подписи меняется. Dataplane кэширует данные идентификации с staleTTL в 1 час. В течение этого окна верификация JWT для мигрированных пользователей завершается ошибкой. Исправление: обходить кэш при повторной попытке верификации и напрямую запрашивать данные из PLC-каталога.
На основе работы AppView для всей сети (все ~42 млн пользователей, ~18,5 млрд записей).
Распределение хранилища (приблизительно, для всей сети):
Для небольшого сообщества, работающего с частичным AppView (индексирование только членов сообщества), требования масштабируются примерно линейно с количеством индексированных аккаунтов.
git remote add upstream https://github.com/bluesky-social/atproto.git
git fetch upstream
git merge upstream/main
Конфликты, как правило, будут в packages/bsky/src/data-plane/server/routes/ и packages/bsky/src/api/. Разрешайте их, сохраняя наши дополнения вместе с изменениями вышестоящего репозитория.
То же, что и вышестоящий репозиторий: двойная лицензия MIT и Apache 2.0. См. LICENSE-MIT.txt и LICENSE-APACHE.txt.
| Компонент | Исходный код | Назначение |
|---|
| rsky-wintermute | blacksky-algorithms/rsky | Индексатор firehose на Rust: потребляет события, выполняет обратную загрузку репозиториев, индексирует записи в PostgreSQL |
| rsky-relay | blacksky-algorithms/rsky | Реле AT Protocol для получения меток модерации от сервисов-лейблеров |
| rsky-video | blacksky-algorithms/rsky | Сервис загрузки видео: транскодирование через Bunny Stream CDN, загрузка blob-ссылок в PDS пользователя |
| bsky-dataplane | Этот репозиторий (services/bsky) | Уровень данных gRPC поверх PostgreSQL |
| bsky-appview | Этот репозиторий (services/bsky) | HTTP-сервер API для конечных точек XRPC app.bsky.* |
| Palomar | blacksky-algorithms/indigo | Полнотекстовый поиск: индексирует профили и посты в OpenSearch с учётом количества подписчиков |
| palomar-sync | blacksky-algorithms/rsky | Синхронизирует количество подписчиков и оценки PageRank из PostgreSQL в OpenSearch |
| Переменная | Обязательно | Описание |
|---|
DB_PRIMARY_URL | Да | Строка подключения PostgreSQL с ?options=-csearch_path%3Dbsky |
DB_REPLICA_URL | Нет | Строка подключения реплики для чтения |
BSKY_DATAPLANE_PORT | Нет | Порт gRPC (по умолчанию 2585) |
BSKY_REDIS_HOST | Нет | Хост:порт Redis для кэширования (в настоящее время рекомендуется оставить отключённым) |
BLACKSKY_MEMBERSHIP_DB_URL | Нет | Отдельная база данных для членства в сообществе (специфично для Blacksky) |
| Переменная | Обязательно | Описание |
|---|
BSKY_APPVIEW_PORT | Нет | Порт HTTP (по умолчанию 2584) |
BSKY_DATAPLANE_URLS | Да | URL gRPC dataplane через запятую |
BSKY_DID | Да | DID AppView (например, did:web:api.example.com) |
BSKY_MOD_SERVICE_DID | Да | DID модерационного сервиса Ozone |
BSKY_ADMIN_PASSWORDS | Да | Пароли администраторов через запятую для базовой аутентификации |
| Ресурс | Минимум | Рекомендуется |
|---|
| CPU | 16 ядер | 48+ ядер |
| RAM | 64 ГБ | 256 ГБ |
| Накопитель | 10 ТБ NVMe | 28+ ТБ NVMe (RAID) |
| PostgreSQL | Выделенный, на той же машине или с низкой задержкой | Рекомендуется та же машина |
| Сеть | Стабильные 100 Мбит/с | 1+ Гбит/с |
| Группа таблиц | Размер |
|---|
| Посты + записи | ~3,5 ТБ |
| Лайки | ~2 ТБ |
| Подписки | ~500 ГБ |
| Уведомления | ~600 ГБ |
| Индексы | ~4 ТБ |
| OpenSearch (Palomar) | ~500 ГБ |