小さく畳めて、そのまま伸びる —— sukhi-fedi
こんにちは、nyanrusです。うちの小さな連合サーバ sukhi.f3liz.casa を動かしているソフトウェア、sukhi-fedi を紹介します。
sukhi-fedi は、ActivityPub を喋る、連合 SNS サーバです。Mastodon 互換の API を主な入口にして(Misskey とは連合が通っています)、JSON-LD と HTTP 署名の翻訳を Elixir で自前でやっています。大きなフェディバースと、いま実際にメッセージが行き来しています。AGPL-3.0、いまのバージョンは v0.4.14。
言いたいことは、ひとつだけです。派手な新機能ではなく、地味な、設計の順番の話 —— この一本のコードが、無料枠に載るくらい小さな箱でも回り、必要ならノードを分けてそのまま伸びる。そして連合の芯は、実際動いています。順に見ていきます。
ほかと、どう違うか
連合サーバを立てるときには、多くは、最初に「サイズ」を選ばせます。大きいものは何とでも連合できるけれど、それなりのサーバを欲しがる。軽いものは小さな箱に収まるけれど、その箱から広げていくことは、あまり想定されていない。sukhi-fedi の賭けは、ここが分かれ道であるべきじゃない、というものです —— 同じコードが、1GB の無料枠の箱にも畳めて、手狭になったらノードを分けて広がる。境界を、デプロイの都合ではなく OTP のモジュールで引いてあるからです。
また、小さな箱でも、大きな箱でも同じように扱えるサーバーがあればなぁという思いもあります。
もうひとつの特徴は、機能の増やし方です。クライアント API はプラグインの棚になっていて、1 ファイル=1 capability、起動時に自分で登録される(ルータは触りません)。capability は addon でまとまり、そのひとまとめを別ノードに載せることもできます。サーバを拡張するのは、芯にパッチを当てることではなく、ファイルを一枚足すこと —— だから新しいエンドポイントも、機能のかたまりも、互いの邪魔をしません。
魔法ではありません。境界をひとつの場所に置いて、そのぶんの代金を一度だけ払う、という地味な規律です。でもそのおかげで、趣味の箱として sukhi-fedi を始めて、書き直しなしで育てて、フォークなしで拡張していける —— あまり普通の取引ではないと思います。
OTP の境界を、ちゃんと割る
両方が同じコードで成り立つ理由は、ほとんどここに集まっています。sukhi-fedi は、役割ごとに OTP のアプリケーションを分けています。
- 案内人(gateway,
:sukhi_fedi) — 人間からの HTTP を受ける唯一の入口。ログイン、投稿の書き込み、inbox の受信、WebFinger。 - 配達員(delivery,
:sukhi_delivery) — outbox を読んで、遠くのサーバの inbox へ POST する。Oban のキューで再送も見る。 - API プラグイン(
:sukhi_api) — Mastodon 互換の REST 面。
大事なのは、この境界が ネットワークではなくモジュールで引かれていることです。それぞれが独立した OTP app です。だから、別々の BEAM ノードとして動かすこともできるし、まるごとひとつの BEAM に畳むこともできる。境界は、畳んでも生き残る。 これは「あとで速くするために足す層」ではなく、最初からそう割ってある、ということです。
プロセス間はメッセージで、状態は supervision に預ける。OTP の素直な使い方をそのまま守っておくと、構成の変更が「配線をやり直す」ではなく「起動のしかたを選ぶ」だけの問題になります。畳むのと伸ばすのは、その両端です。
小さく畳める
sukhi-fedi は、メモリ 512〜768MB の小さな箱で動かすことを一つの目標にしています。
docker-compose.combined.yml を重ねると、案内人と配達員が ひとつの BEAM(combined リリース) になります。1 コア / 2GB の箱を想定した構成です。ある 1 コアの耐久走では、この combined が読み書きと inbox の負荷の下で ~130 MiB のまま横ばいで完走しました。512MB 予算での既知の内訳(combined 192M / postgres 112M / …)まで、compose のコメントに書いてあります。たぶん768MBでも回ります。1GBの環境で、UIなしでボット専用のサーバーとしても回したことがあります(https://watch-mjw.f3liz.casa, いまはsukhi.f3liz.casaと同じ箱にいます。)。
「小さいサーバでも回せる」は、スローガンではなくて、畳めるように割ってあるから本当になっている、という順番です。
ちなみに、この小箱の話でいちばん先に倒れたのは、実は Elixir ではなく別ランタイムの Bun でした。いまは ActivityPub の翻訳を Elixir が自前でやっていて、Bun は本番から退役しています。その顛末は別の記事に。 —— fedify を使って、fedify を卒業した話。
そのまま拡張する
小さいサーバーでも回せるのとは逆向きに、拡張するのも同じコードです。拡張の向きは、二つあります。
配達の量は、分散 Erlang ではなく PostgreSQL と NATS が引き受けます。Postgres が事実の台帳(system of record)、NATS JetStream がイベントの面。案内人は outbox のデータを書くだけ、配達員はそれを読んで配るだけ。役割がこの一枚を挟んで分かれているので、配達員を増やすのに、クラスタリングの曲芸は要りません。配達の要 Outbox.Relay は OTP の中の singleton な GenServer で、コードには最初から "for future horizontal scale" と書いてあります。
機能の面は、分散 Erlang が受けます。Mastodon 互換の REST は、別ノードに置いたプラグインの棚で、:rpc で届きます。1 ファイル=1 capability で、起動時に自動登録され(SukhiApi.Registry)、addon でまとまる —— エンドポイントを足すのは、ファイルを一枚置くだけ、ルータは触りません。plugin_nodes はリストで、案内人は最初に届いたノードを使う。だから plugin ノードは複数置けて、機能の層だけを別ノードで伸ばせます。案内人と配達員には触らずに。
二つの面、二つの伸び方 —— 配達は Postgres と NATS、機能は分散 Erlang。畳んだ状態から始めて、要るところだけノードに切り出す。設計をやり直さずに、です。
動いているのが、見える
紹介記事でいちばん正直に言いたいのは、ここです。これは構想ではなくて、動いているものです。
実際に連合して round-trip を確かめたのは、Mastodon、Misskey、hackers.pub です。ActivityPub の翻訳は Elixir ネイティブ(SukhiFedi.Fedi)で、そこを通った activity が、ちゃんと相手に届いて戻ってきます。ほかのサーバぶん —— 署名方式(draft-cavage と RFC 9421)、引用の書き方、絵文字リアクションといったワイヤの癖 —— も、相互運用のためにコードには先回りして入れてありますが、そちらはまだ、実地でつないだわけではありません。Mastodon 互換 API では、Elk / Phanpy などのブラウザクライアントや Ivory / Tusky が、ログイン後にエラーで止まらない程度には、細かい対応が入っています。
それを、数字だけでなく景色でも見られます。sukhi の /map は、ここまで話した構造 —— 案内人から NATS の操車場を抜けて配達員へ、そして連合の宇宙へ —— をそのまま路線図にした公開ページで、その上を走る電車の本数は、飾りではなく、その一日にほんとうに流れた量です。駅と線路は比喩ですが、つながり方は実配線と一対一。作った顛末は「サーバーの中に、電車を走らせた」に書きました。
残っている縁もあります。全文検索、streaming の WebSocket、native な Misskey client API —— これらは TODO.md と OPEN_QUESTIONS.md に、芝として並んでいます。芯ではなく、芝です。未来に乞うご期待ってことですね。
四か月、積んできた
最初のコミットは 2026 年 3 月 16 日、タイプミスの残る "inital commit" でした。そこから四か月、461 のコミットを積んで、いまの形になりました。
4 月に配達員を本体から切り離して、連合の骨格ができました。5 月はひたすら肉をつける月で、引用も、絵文字リアクションも、MFM も、DM も、遠くのサーバとやりとりする経路が一気に増えました。楽な月ではなくて、月末には、署名した POST が hackers.pub に 401 で弾かれ続けたときも。その日には、log と、署名の作り直しと、自己検証が延々と続いて、最後の一行が「Mastodon と同じで、必須のヘッダだけ署名する」でした。連合が通らない夜は、たいてい原因がこちら側にあったことを実感しました。
いちばん大きな作り直しは、6 月 12 日に集まっています。Bun がやっていた ActivityPub の翻訳を Elixir に移し、小箱向けの combined リリースを切り出し、Ed25519 の鍵を端から端まで通して、その同じ日に Bun のサイドカーを「退役」とメモしました。小さくできるように分離してたから、設計のやり直しのかわりに、起動のしかたの入れ替えで済みました。
その後も、取り返しのつかないデータ喪失を二つ、間一髪で止めた日があり、/map を作ったのは、つい先週です。派手ではないけれど、こうして一日ずつ、積んできました。
触ってみる
git clone https://github.com/sukhi-social/sukhi-fediREADME の Quick start に沿えば、手元でスタックまるごとをビルドして動かせます(localhost:4000 で開きます)。ちゃんとしたセルフホストは SETUP.md、中を知りたいなら docs/ARCHITECTURE.md。どちらも「この一枚で全部を組み直せる」ことを目標に書いてあります。AGPL-3.0 なので、持って帰って使ってもらってもいいです。
小さく畳めて、そのまま伸びる連合サーバが、ひとつ在ります。気になったら、覗いてみてください。
(sukhī は、パーリ語で「幸せな」。人と人がつながる、気持ちのいい場を紡いでいけるように —— そういう名前です。)
コメント
この記事は fediverse に流れています。あなたのアカウントから返信すると、ここに並びます。
まだコメントはありません。