
대규모 자가 호스팅 소셜 네트워킹을 위한 성능 최적화된 AppView, Rust 기반 firehose 인덱서, Redis 캐싱 및 커뮤니티 기능을 갖춘 AT Protocol 참조 구현의 포크
이것은 Bluesky Social PBC의 AT 프로토콜 참조 구현을 포크한 Blacksky의 포크입니다. 이 포크는 api.blacksky.community에서 AppView를 구동합니다.
투명성과 다른 커뮤니티가 이 작업의 혜택을 받을 수 있도록 공개합니다. 이 저장소는 기여, 이슈 또는 PR을 받지 않습니다. 표준 atproto 구현을 원한다면 bluesky-social/atproto를 사용하세요.
모든 변경 사항은 packages/bsky (appview 로직), services/bsky (런타임 설정) 및 하나의 사용자 정의 마이그레이션에 있습니다. 나머지는 업스트림과 동일합니다.
업스트림 데이터플레인에는 이벤트를 직접 인덱싱하는 TypeScript firehose 컨슈머(subscription.ts)가 포함되어 있습니다. 여러 가지 이유로 이를 Rust 인덱서인 rsky-wintermute로 교체했습니다.
이 저장소의 데이터플레인과 appview는 그대로 실행됩니다. wintermute가 쓰는 PostgreSQL 데이터베이스에서 읽습니다. 기본 제공 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 객체가 Redis를 통한 JSON 왕복 후 .toDate() 메서드를 잃어버려 캐시 적중 시 프로필 하이드레이션이 불완전해집니다. 현재 Redis 캐싱을 비활성화한 상태로 실행 중입니다. 수정 방법은 캐시 쓰기 시 타임스탬프를 ISO 문자열로 직렬화하고 읽을 때 재구성하는 것입니다.알림 기본 설정 서버 측 적용 (packages/bsky/src/api/app/bsky/notification/listNotifications.ts)
reasons를 지정하지 않으면 서버는 사용자의 저장된 알림 기본 설정을 적용합니다. 이것이 없으면 기본 설정은 클라이언트 측에서만 적용되며 효과가 없습니다.인증 검증기 만료된 서명 키 수정 (packages/bsky/src/auth-verifier.ts)
forceRefresh) 시 데이터플레인의 인메모리 ID 캐시를 우회하고 PLC 디렉터리에서 직접 DID 문서를 확인합니다. 계정 마이그레이션 후 서명 키가 회전되지만 캐시에 이전 키가 남아 있을 때 발생하는 인증 실패를 수정합니다.JSON 정리 (packages/bsky/src/data-plane/server/routes/records.ts)
\u0000) 및 제어 문자를 제거합니다. 이는 RFC 8259에 유효하지만 Node.js JSON.parse()에서 거부되어 데이터플레인에서 rowToRecord 파싱이 조용히 실패하고 누락된 게시물로 나타납니다.개별 PDS가 아닌 AppView에서 호스팅되는 비공개 커뮤니티 게시물을 위한 인프라입니다. 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 indexer) | (Go search)
- firehose consumer | |
- backfiller | v
- label indexer | OpenSearch
- direct indexer |
v
bsky-dataplane (gRPC :2585) <--- Redis (optional)
|
v
bsky-appview (HTTP :2584)
|
v
Reverse proxy (Caddy/nginx)
Wintermute는 4개의 병렬 처리 경로를 가진 모놀리식 Rust 서비스입니다.
bsky.network firehose에 연결, 이벤트를 Fjall(임베디드 키-값 저장소) 큐에 기록ON CONFLICT로 PostgreSQL에 기록하여 멱등성 보장rsky 저장소에 포함된 추가 CLI 도구:
queue_backfill -- CSV, PDS 검색 또는 직접 DID 목록에서 백필할 DID 큐에 추가direct_index -- 큐를 우회하여 특정 리포지토리 가져오기 및 인덱싱 (개별 계정 수정에 유용)label_sync -- 커서 0에서 레이블 스트림 재생하여 누락된 부정 처리 따라잡기plc_import -- PLC 디렉터리에서 핸들/DID 매핑 대량 가져오기palomar-sync -- 팔로워 수 및 PageRank를 OpenSearch에 동기화PDS가 Bluesky의 video.bsky.app를 지원하지 않는 사용자를 위한 동영상 업로드 서비스입니다. 자체 DID(did:web:video.blacksky.community)를 사용하여 서비스 인증 JWT를 통해 사용자 PDS에 인증합니다. 흐름:
조정 레이블은 WebSocket 구독을 통해 레이블러 서비스(예: Bluesky의 Ozone)에서 제공됩니다. Wintermute의 ingester는 전용 label_live 큐(낮은 볼륨, 기본 firehose와 분리)에서 레이블을 처리합니다. label_sync 도구는 레이블러의 전체 스트림을 재생하여 레이블을 다시 삽입하지 않고도 누락된 부정(레이블 제거)을 따라잡을 수 있습니다.
bsky 스키마)bsky 스키마는 데이터플레인의 마이그레이션에 의해 생성됩니다. 처음 실행 시 데이터플레인이 모든 마이그레이션을 자동으로 적용합니다. Blacksky 전용 마이그레이션은 20260202T120000000Z-add-community-post.ts(커뮤니티 게시물 테이블)뿐입니다. 커뮤니티 게시물이 필요하지 않으면 제거할 수 있습니다.
rsky-wintermute는 동일한 스키마에 씁니다. 모든 INSERT 문은 ON CONFLICT를 사용하므로 wintermute와 데이터플레인 마이그레이션을 어떤 순서로 실행해도 안전합니다.
pnpm install
pnpm build
node services/bsky/dataplane.js
node services/bsky/api.js
전체 네트워크 백필(약 4,200만 사용자, 약 185억 레코드)은 wintermute의 병렬 처리에도 불구하고 몇 주가 걸립니다. 예상:
백필 중에 AppView는 기능하지만 아직 백필되지 않은 사용자에 대해 불완전한 데이터를 표시합니다. 라이브 이벤트는 백필 진행 상황에 관계없이 즉시 인덱싱됩니다.
전체 네트워크 AppView를 부트스트래핑할 때 겪었던 문제들입니다. 동일한 작업을 수행하는 경우 이러한 문제 중 일부를 겪을 가능성이 높습니다.
COPY 텍스트 형식 JSON 손상: PostgreSQL의 COPY 텍스트 프로토콜은 백슬래시를 이스케이프 문자로 취급합니다. 대량 로더가 JSON 문자열에서 백슬래시를 이스케이프하지 않으면 \"가 "가 되어 조용히 레코드가 손상됩니다. record.json 열은 text 유형(jsonb 아님)이므로 PostgreSQL이 이를 포착하지 않습니다. 약 66,000개의 손상된 레코드를 찾았으며 공개 API에서 다시 가져와 복구해야 했습니다.
JSON의 null 바이트: 일부 AT 프로토콜 레코드에는 \u0000(null 바이트)가 포함되어 있으며 이는 RFC 8259에 유효하지만 Node.js JSON.parse()에서 거부됩니다. 데이터플레인은 이러한 레코드에 대해 조용히 null을 반환합니다. 데이터베이스에 쓰기 전에 null 바이트를 제거하십시오.
타임스탬프 형식 민감성: 데이터플레인은 밀리초 정밀도와 Z 접미사(2026-01-12T19:45:23.307Z)가 있는 타임스탬프를 예상합니다. 나노초 정밀도나 시간대 오프셋 형식(+00:00)은 미묘한 정렬 및 비교 문제를 일으킵니다.
알림 테이블 비대화: (did, recordUri, reason)에 고유 제약 조건이 없으면 알림 테이블이 중복으로 무한히 커집니다. 우리의 경우 이를 발견하기 전에 13억 행(663GB)에 도달했습니다. INSERT에 ON CONFLICT DO NOTHING을 추가하는 것은 고유 인덱스가 먼저 존재하는 경우에만 도움이 되며, 인덱스를 생성하려면 기존 데이터의 중복 제거가 필요합니다.
게시물 임베드 테이블: post_embed_image 및 post_embed_video 테이블은 인덱서가 처리하지 않으면 기본적으로 채워지지 않습니다. 이것이 없으면 getAuthorFeed의 미디어 필터가 아무것도 반환하지 않습니다. 이들은 별도로 백필해야 합니다.
레이블 부정 순서: 레이블 부정(제거) 이벤트는 소스, URI 및 값으로 원래 레이블을 참조합니다. 부정이 원래 레이블보다 먼저 도착하면(백필 중 일반적) 조용히 삭제됩니다. label_sync 도구는 전체 스트림을 재생하여 이를 따라잡습니다.
Fjall 큐 중독: 임베디드 데이터베이스인 Fjall(wintermute의 큐에서 사용)은 충돌 후 "중독" 상태가 되어 모든 큐 작업을 차단할 수 있습니다. 해결 방법은 큐 데이터베이스 디렉터리를 삭제하고 다시 시작하는 것입니다. wintermute는 릴레이의 커서에서 따라잡습니다(릴레이는 약 72시간의 기록 유지).
TLS 공급자 초기화: Rust의 rustls는 모든 TLS 연결 전에 암호화 공급자를 명시적으로 설치해야 합니다. 시작 시 rustls::crypto::aws_lc_rs::default_provider().install_default()가 없으면 firehose에 대한 첫 번째 WebSocket 연결이 패닉을 일으킵니다.
계정 마이그레이션 후 서명 키 회전: 사용자가 PDS 간에 마이그레이션하면 서명 키가 변경됩니다. 데이터플레인은 ID 데이터를 staleTTL 1시간으로 캐시합니다. 이 기간 동안 마이그레이션된 사용자에 대한 JWT 검증이 실패합니다. 해결 방법은 검증 재시도 시 캐시를 우회하고 PLC 디렉터리에서 직접 확인하는 것입니다.
전체 네트워크 AppView(약 4,200만 사용자, 약 185억 레코드) 실행 기준.
스토리지 세부 내역 (대략, 전체 네트워크):
| 테이블 그룹 |
|---|
더 작은 커뮤니티가 부분 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 | Rust firehose 인덱서: 이벤트 소비, 리포지토리 백필, 레코드를 PostgreSQL에 인덱싱 |
| rsky-relay | blacksky-algorithms/rsky | 레이블러 서비스의 조정 레이블을 수신하기 위한 AT 프로토콜 릴레이 |
| rsky-video | blacksky-algorithms/rsky | 동영상 업로드 서비스: Bunny Stream CDN을 통해 트랜스코딩, blob 참조를 사용자 PDS에 업로드 |
| bsky-dataplane | 이 저장소 (services/bsky) | PostgreSQL 위의 gRPC 데이터 계층 |
| bsky-appview | 이 저장소 (services/bsky) | app.bsky.* XRPC 엔드포인트용 HTTP API 서버 |
| Palomar | blacksky-algorithms/indigo | 전체 텍스트 검색: 팔로워 수 부스팅으로 프로필 및 게시물을 OpenSearch에 인덱싱 |
| palomar-sync | blacksky-algorithms/rsky | 팔로워 수 및 PageRank 점수를 PostgreSQL에서 OpenSearch로 동기화 |
| 변수 | 필수 | 설명 |
|---|
DB_PRIMARY_URL | 예 | ?options=-csearch_path%3Dbsky가 포함된 PostgreSQL 연결 문자열 |
DB_REPLICA_URL | 아니오 | 읽기 복제본 연결 문자열 |
BSKY_DATAPLANE_PORT | 아니오 | gRPC 포트 (기본값 2585) |
BSKY_REDIS_HOST | 아니오 | Redis 호스트:포트 (캐싱 용, 현재는 비활성화 상태로 두는 것이 좋음) |
BLACKSKY_MEMBERSHIP_DB_URL | 아니오 | 커뮤니티 멤버십용 별도 DB (Blacksky 전용) |
| 변수 | 필수 | 설명 |
|---|
BSKY_APPVIEW_PORT | 아니오 | HTTP 포트 (기본값 2584) |
BSKY_DATAPLANE_URLS | 예 | 쉼표로 구분된 데이터플레인 gRPC URL |
BSKY_DID | 예 | AppView의 DID (예: did:web:api.example.com) |
BSKY_MOD_SERVICE_DID | 예 | Ozone 조정 서비스 DID |
BSKY_ADMIN_PASSWORDS | 예 | 기본 인증용 쉼표로 구분된 관리자 비밀번호 |
| 리소스 | 최소 | 권장 |
|---|
| CPU | 16 코어 | 48+ 코어 |
| RAM | 64 GB | 256 GB |
| 스토리지 | 10 TB NVMe | 28+ TB NVMe (RAID) |
| PostgreSQL | 전용, 동일 머신 또는 저지연 | 동일 머신 권장 |
| 네트워크 | 지속 100 Mbps | 1 Gbps+ |
| 크기 |
|---|
| 게시물 + 레코드 | ~3.5 TB |
| 좋아요 | ~2 TB |
| 팔로우 | ~500 GB |
| 알림 | ~600 GB |
| 인덱스 | ~4 TB |
| OpenSearch (Palomar) | ~500 GB |