Misskeyはどのようにして分散型SNSを成立させているのか
はじめに
こんにちは、レアゾン・ホールディングスでエンジニアをしている新田です。
分散型SNSって、実際どうやって動いているんでしょうか?
MisskeyやMastodonなどの分散型SNSは、中央集権的なサーバーに依存せず、各インスタンスが独立して動作しながらも、ユーザーのフォロー関係を通じて「なんとなく」繋がっている……という概念は知っています。
でも、「具体的にコードレベルでどうやって実現しているのか?」 という部分が気になったので調べてみました。
この記事では、Misskeyの実際のソースコードを読み解きながら、分散型SNSを実現する裏側の仕組みを解説してみます。
(※読み解きが間違っていたらすみません)
Misskeyの分散型アーキテクチャ
システム全体像
まずはざっくりとした全体像から。
Misskeyは、各インスタンスが独立したデータベースを持つ分散型アーキテクチャを採用しています。
各インスタンスは以下の特徴を持っています:
- 独立したRDBを持つ
- ActivityPub プロトコルで通信する
- ユーザーのフォロー関係を通じて自動的に接続される
ここでのポイントは、「誰かが投稿したデータが、どうやって他のサーバーに飛んでいくか」 です。
ActivityPubプロトコルによる連合
Misskeyは、W3C標準の ActivityPub プロトコルを使用してインスタンス間で通信します。
ActivityPubでは、SNS上のアクションを以下のような「アクティビティ」として定義しています。
-
Create: note作成 -
Announce: Renote -
Like: リアクション -
Follow: フォロー -
Delete: 削除
Misskeyでは、投稿のことを「note(ノート)」と呼びます。X(Twitter)でいうPostやTweetと同じような概念です。
また、RenoteはXのRepostと同じ機能です。
ノート配信の仕組み
ここからコードを見ていきます。
ローカルユーザーが public なノートを投稿すると、どのようなフローでリモートインスタンスに配信されるのでしょうか。
1. ノート作成時の配信処理
ノートが作成されると、NoteCreateService がそれをActivityPub形式に変換し、配信先を決定します。
//#region AP deliver
if (!data.localOnly && this.userEntityService.isLocalUser(user)) {
(async () => {
const noteActivity = await this.renderNoteOrRenoteActivity(data, note);
const dm = this.apDeliverManagerService.createDeliverManager(user, noteActivity);
// メンションされたリモートユーザーに配送
for (const u of mentionedUsers.filter(u => this.userEntityService.isRemoteUser(u))) {
dm.addDirectRecipe(u as MiRemoteUser);
}
// リプライ先がリモートユーザーなら配送
if (data.reply && data.reply.userHost !== null) {
const u = await this.usersRepository.findOneBy({ id: data.reply.userId });
if (u && this.userEntityService.isRemoteUser(u)) dm.addDirectRecipe(u);
}
// Renote元がリモートユーザーなら配送
if (data.renote && data.renote.userHost !== null) {
const u = await this.usersRepository.findOneBy({ id: data.renote.userId });
if (u && this.userEntityService.isRemoteUser(u)) dm.addDirectRecipe(u);
}
// フォロワーに配送
if (['public', 'home', 'followers'].includes(note.visibility)) {
dm.addFollowersRecipe();
}
if (['public'].includes(note.visibility)) {
this.relayService.deliverToRelays(user, noteActivity);
}
trackPromise(dm.execute());
})();
}
//#endregion
この実装から、以下の挙動が読み取れます。
-
ローカルユーザーのノートのみ配信:
isLocalUser(user)でチェックしています。 -
配信先候補一覧:
- メンション先のリモートユーザー
- リプライ/Renote先のリモートユーザー
- フォロワー(ここが重要)
- Relayサーバー(publicノートの場合)
つまり、「誰もフォローしていない(フォロワーがいない)状態」かつ「誰にもメンションしていない」場合、フォロワーへの配信は行われません。ただし、Relayサーバーに接続していれば、そこ経由で配信されます(後述)。
2. 配信マネージャーによる最適化
配信先が決まったら、ApDeliverManagerService が実際に送る準備をします。
src/core/activitypub/ApDeliverManagerService.ts
@bindThis
public async execute(): Promise<void> {
// key: inbox URL, value: whether it is sharedInbox
const inboxes = new Map<string, boolean>();
// フォロワーへの配信レシピを処理
if (this.recipes.some(r => isFollowers(r))) {
const followers = await this.followingsRepository.find({
where: {
followeeId: this.actor.id,
followerHost: Not(IsNull()),
},
select: {
followerSharedInbox: true,
followerInbox: true,
},
});
for (const following of followers) {
// SharedInboxがあればそれを優先して使う
const inbox = following.followerSharedInbox ?? following.followerInbox;
if (inbox === null) throw new Error('inbox is null');
inboxes.set(inbox, following.followerSharedInbox != null);
}
}
// (中略: 直接指定の配信処理)
// deliver
await this.queueService.deliverMany(this.actor, this.activity, inboxes);
}
特筆すべきは SharedInbox の活用です。
同じインスタンスに100人のフォロワーがいても、そのインスタンス共通の SharedInbox に1回送れば済むようになっています。これがないと、フォロワーが増えるにつれて通信量が爆発してしまいますね。
ノート受信の仕組み
逆に、リモートからノートが飛んできたときは ApInboxService が処理します。
src/core/activitypub/ApInboxService.ts
@bindThis
private async createNote(resolver: Resolver, actor: MiRemoteUser, note: IObject, silent = false, activity?: ICreate): Promise<string> {
// (中略: 検証処理)
const unlock = await acquireApObjectLock(this.redisClient, uri);
try {
const exist = await this.apNoteService.fetchNote(note);
if (exist) return 'skip: note exists';
await this.apNoteService.createNote(note, actor, resolver, silent);
return 'ok';
} finally {
unlock();
}
}
受信時は、署名検証 → リモートオブジェクトの解決(取得) → DB保存 という流れです。Redisを使ってLockを取っているのも、並列処理での競合を防ぐために重要そうです。
グローバルタイムラインの実装
Misskeyの特徴でもあるグローバルタイムライン。これは「世界中の全ツイート」ではなく、「観測範囲内の全パブリックノート」 です。
src/server/api/endpoints/notes/global-timeline.ts
const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'),
ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate)
.andWhere('note.visibility = \'public\'')
.andWhere('note.channelId IS NULL')
// ...
クエリ自体は非常にシンプルで、DBにある visibility = 'public' なノートを全部引っ張ってきているだけです。
つまり、「DBに入っていない(=連合していない)インスタンスの投稿は、GTLには流れてこない」 ということになります。
実際にサーバーを立てて検証してみた
コードを読んだだけでは実感が湧かないので、ローカル開発環境で実際にMisskeyインスタンスを立ち上げてみました。
1. セットアップ
設定ファイルを作成してから docker compose でサクッと立ち上げます。
# RedisとPostgreSQLを起動
docker compose up -d
# 開発サーバー起動
pnpm dev
セットアップ画面を経て、管理者アカウントを作成しました。
さあ、自分だけのSNSの誕生です。
2. ひとりぼっちのグローバルタイムライン
サーバーを立てた直後、期待に胸を膨らませて「グローバルタイムライン」を開いてみました。
……静かすぎる。
GTLには、自分がテスト投稿したノートしか表示されていません。
「グローバル」と言いつつ、誰もフォローしていないため、他のサーバーからの投稿が一切流れてこないのです。
3. なぜ静かなのか?
先ほどの NoteCreateService のコードを思い出してみます。
// フォロワーに配送
if (['public', 'home', 'followers'].includes(note.visibility)) {
dm.addFollowersRecipe();
}
- こちらから発信しない: フォロワーがいないので、私の投稿は外部に飛ばない。
- あちらから来ない: 誰もフォローしていないので、外部インスタンスは私のサーバーに投稿を投げてくれない。
Misskeyのネットワークには「中央レジストリ」が存在しません。
そのため、ユーザーが能動的に他サーバーのユーザーをフォローしない限り、インスタンスは孤立したままなのです。
誰かを一人でもフォローすれば、そのユーザーのインスタンスと接続が確立され、そこから徐々にネットワークが広がっていくはずです。
余談: Relayサーバーという選択肢
実は、フォロー関係を作らなくても、他のインスタンスのpublicノートを受信する方法があります。それが Relayサーバー です。
Relayサーバーは、ActivityPubネットワーク上でpublicなノートを中継する専用のサーバーです。インスタンスがRelayサーバーに接続(Follow)することで、そのRelay経由で他のインスタンスのpublicノートを受信できるようになります。
Relayサーバーの仕組み:
- インスタンス → Relay: publicノートを投稿すると、登録済みのRelayサーバーに配信される
- Relay → 他のインスタンス: Relayサーバーは、自分に接続している他のインスタンスに、受信したpublicノートを中継配信する
- 結果: フォロー関係がなくても、Relay経由で他のインスタンスのpublicノートがグローバルタイムラインに流れてくる
つまり、Relayサーバーに接続していれば、フォローしていないインスタンスのpublicノートも受信できる ということです。これにより、ネットワークの接続性が向上し、より多くのノートがグローバルタイムラインに流れてきます。
ただし、Relayサーバーはオプション機能であり、インスタンス管理者が手動でRelayサーバーを登録する必要があります。また、Relayサーバー側も接続を承認する必要があります。
今回立てたインスタンスでは、Relayサーバーに接続していなかったため、フォローしていない状態では完全に孤立していました。もしRelayサーバーに接続していれば、フォロー関係がなくても他のインスタンスのpublicノートが流れてきていたはずです。
実際に運用されているRelayサーバーとしては、おたそーリレーサービスやMisskey.GG relayなどがあります。これらに接続することで、フォロー関係がなくても様々なインスタンスのpublicノートがグローバルタイムラインに流れてくるようになります。
まとめ
Misskeyのコードを追いかけ、実際に動かしてみることで、分散型SNSの「分散」の意味が腹落ちしました。
- ActivityPub という共通言語で喋る。
- SharedInbox などで通信を最適化している。
- 自動接続: 繋がりは「フォロー」というユーザーのアクションによって作られる。
誰もフォローしていないGTLの静けさは、「分散型SNSとは、参加者が作り上げるネットワークである」 という事実を如実に物語っていました。
技術的に興味深いポイント(HTTP署名やジョブキュー周りなど)はまだまだありますが、今回はこのあたりで。
さようなら
▼新卒エンジニア研修のご紹介
レアゾン・ホールディングスでは、2025年新卒エンジニア研修にて「個のスキル」と「チーム開発力」の両立を重視した育成に取り組んでいます。 実際の研修の様子や、若手エンジニアの成長ストーリーは以下の記事で詳しくご紹介していますので、ぜひご覧ください!
▼採用情報
レアゾン・ホールディングスは、「世界一の企業へ」というビジョンを掲げ、「新しい"当たり前"を作り続ける」というミッションを推進しています。 現在、エンジニア採用を積極的に行っておりますので、ご興味をお持ちいただけましたら、ぜひ下記リンクからご応募ください。


