この記事は分散SNS Advent Calendar 2019 の9日目です.
ふだんはMastodon/Pleromaのクライアントアプリケーションである,Whalebirdの話が多いけど,実は https://pleroma.io というPleromaサーバも運用しています.
なお,この記事は,分散SNSのサーバを自分で建てたい,管理したいという方に,Pleromaをおすすめする記事です. 利用者として登録するサーバとしてPleromaを強く薦めているわけではありません.もちろんPleromaサーバ管理者としては,Pleromaに登録してくれたら嬉しいけれども.
Pleromaって?
ActivityPubを喋る分散SNSの一つですが,MisskeyやPixelfed等と違って,APIはできるだけMastodonに近づけようとしているため,Mastodonクライアントでもそのままログインできる場合が多い. MastodonがRuby on Railsで作られているのに対し,PleromaはPhoenix (Elixir) で作られている. 外側から叩くAPIが同じようなのでも,言語とフレームワークが違うとパフォーマンスはだいぶ違うよっていう話が今日の主題.
リポジトリはこちら.
そもそもActivityPubみたいなプロトコルはElixir向きである
ActivityPubのプロトコルに合わせて,どのようにするとSNSのサーバを構築できるかはこのへんを参照してもらうとして……
分散SNS Advent Calendarでもlocalyouserさんが解説してくれている.
前提として,ActivityPubを喋る分散SNSは,外部サーバとのやりとりが必ず必要になる.
まず,外部サーバから自分のサーバへのリクエストが発生する.これは /inbox
に様々な型のオブジェクトをPOSTしてくるので,これを自分のサーバのDBに合わせた形で保存する必要がある.
そして,このリクエストは,自分のサーバが連携しているサーバ全てから飛んでくる可能性がある.
一般的なWebサービスでは,自分のサーバに登録した人が
- フォローしたり
- 投稿したり
- 投稿を閲覧したり
というリクエストを飛ばしてくるだけなのだが,ActivityPubはこれに加えて連携先のサーバから様々なリクエストが飛んでくる. そのため,リクエスト数は登録者に比べるとかなり多めになる.これはおひとりさまサーバを運営していても,連携していればどんどんPOSTされてくることになる.
次に,自分のサーバから外部サーバへのリクエストも必要になる.
これは,自分のサーバに投稿したものを,連携先サーバの /inbox
にPOSTしてやることで,相互に連携を保っているからだ.
なので,自サーバ登録者が投稿した場合には,その投稿をDBに保存するだけでなく,連携先のサーバにPOSTして周る必要がある.
Erlang/OTPから考えるActivityPub対応
このような仕様を前提に考えると,Erlang/OTPが使えるElixirとの親和性が高いことがわかる.
Elixir, Erlangは並行処理が非常に得意な言語だ.Erlangに組み込まれているOTPは,軽量プロセス(いわゆるスレッドではない)を大量に作成し,それによって並行処理を行うことができる. プロセスなのでメモリ空間は共有しない.ただしOSのプロセスではなくErlangVMが管理しているプロセスで,非常に軽量.
そのため,上記のような仕様の場合,
/inbox
へのリクエストがあったら,軽量プロセスを一つ割り当ててそこで処理をする- 何個リクエストが来ても,それぞれ軽量プロセスに割り当てられるので同時リクエスト数が増えても問題ない
- ユーザ投稿時には,ユーザの投稿をとりあえずDBに保存
- 各サーバへの配信はそれぞれ軽量プロセスを割り当て,DBに保存した値を配信する
というようなことができる. なお,この1リクエスト-1プロセスというのは,Phoenixを使っていれば当たり前に実現されている状態なので,特にPleroma側で特殊な実装をしているわけではない.
これをRailsでやるとなると,同時リクエスト数は,UnicornやPumaのプロセス,スレッド数で上限が決まってしまう. また,各サーバへの配信はSidekiqを使う必要があり,こちらもconcurrencyで同時実行の上限が決まり,さらにこれはこれでRailsとは別のプロセスを立ち上げる必要がある.そしてSidekiqのバックエンドにRedisも必要になる.
ちなみにこういったアーキテクチャに非常に近いものとして,Golangがある. Golangでもだいたいこれと同じようなことができるとは思うし,相性もいいと思う.GolangのActivityPub実装は見たことないのだけれど,それは多分俺があんまり調べてないからだと思う…….
ただGolangより圧倒的に並行処理は書きやすいと思っているし,なにより 適当に書いても落ちない というのはすごい利点だ.
なぜ Erlang/OTP を使い続けるのか · GitHub
Pleroma軽いってホント?
たまにささやかれるMastodon重い,Pleroma軽いというウワサだが,これは本当.
ActivityPubと,Elixirのアーキテクチャからして相性がいいのは分かってもらえたと思うが,それに加えてWebSocketとの相性の良さというのもある.
WebSocketは,Phoenixが得意とする分野のひとつだ.Elixirが軽量プロセスを使った並行処理が得意という話はしたが,このプロセスごとにWebSocketのコネクションを一つずつ持たせることができる. これにより,Railsのようにスレッド切り替えでWebSocketを維持したりする必要がない.そして一つ一つのプロセスが軽量であるがゆえに,同時接続数が増えてもサーバ負荷が極端に増すことはない.
そのため,自分のサーバの利用者が増えて,同時接続数が増えたとしても,サーバへの負荷がRailsより圧倒的に少なくて済む.
ただし,負荷は少ないとはいえ,同時接続数が増えるとErlangはメモリをそれなりに使うようになる. 並行処理を行う上では,CPUのコア数が非常に大事な要素になるのだが,同時接続を維持するには,それぞれのプロセスがメモリを保持し続けるので全体としてメモリ使用量が増える.
といっても,そういうレベルの接続は,そもそもRailsでは落ちているレベルの話ではあるのだが.
(追記: ちなみにMastodon自体はstreaming部分だけはnode.jsにしているらしいですよ: https://github.com/tootsuite/mastodon/tree/master/streaming)
現状のPleromaへの不満点
と,アーキテクチャ的な利点は持ち上げてきたけど,まだ一部Pleromaの実装として不満なところはある.
Streaming用WebSocketにPhoenix.Channelを使っていない
現状,PleromaのWebSocketは複数台構成をサポートしていない.そのため,PleromaのWebサーバを複数台のサーバで運用することができない.
Erlangは複数台のErlangVM同士を接続し,Erlang Clusterを作ることができる.これによりクラスタ内部でデータのやり取りをすることができる.
こうすることで,サーバAが受け取った /inbox
への投稿を,サーバBが WebSocketに乗せて接続者に配信する,ということなことが可能になり,真の負荷分散ができるはずだった.
ところが,現状,PleromaはWebSocketのバックエンドにPhoenix.Channelを使っていない.そのため,Erlang Clusterを構築したとしても,WebSocketにおいては別サーバとのメッセージのやり取りを行わないため,サーバAが受け取った /inbox
の投稿は,サーバAが管理しているWebSocketにしか乗らない.
例えばこのときに,ユーザがサーバBに接続していた場合,この投稿はWebSocketに載ってStreamingされることはない.
もちろん,サーバBの /inbox
に届いた投稿はStreamingされるのだが…….
RESTでのリクエストは大抵の場合,前段にLoad Balancerを用意してバランシングしているので,リクエストをどちらのサーバが受け取るかはこちらではわからないし,それが適当にバランシングされるからこそ複数台構成にするわけで…….
一応リクエストは出しているものの,まだここは解決しきっていない.
PostgreSQLへの負荷はそれなりにでかい
Mastodonは,途中から検索用途にElasticsearchを導入した.しかし,PleromaはまだElasticsearchを導入していない.
検索は,例えば
- ユーザを探す
- ハッシュタグタイムラインを見る
- 特定の投稿を探す
というようなときに活用される. で,これに関しては明らかにElasticsearchに軍配が上がる(コストもそれなりにかかるけどね……).
Pleromaは現状これをPostgreSQLの全文検索に頼っており,これは流石に負荷がかかる.
通常運用時はまったくDBに負荷はかからないが,検索が走るときだけIOPSをフルに使い切っているのでこれはきつい. パフォーマンスを考えるのであれば,Elasticsearchのような全文検索エンジンを導入することを考えたほうがいいだろう.
ただ,サーバ代を安くするという観点で言うと,Elasticsearchは全然お安くないんだけどね.
Pleromaサーバを立てる
だいたい利点は説明したので,実際の建方について. といっても,インストール方法については,公式を参照してもらうのが一番良いので,
https://docs-develop.pleroma.social/backend/installation/debian_based_jp/
これの補足を少しするだけに留める.
Configについて
pleromaをインストールするときに,
$ mix pleroma.instance.gen
というコマンドを叩いて,config/generated_config.exs
を生成したと思う.で,こいつを prod.secret.exs
に変更するという案内がある.
Phoenixではconfigとして,
- config.exs
- dev.exs
- test.exs
- prod.exs
を用意する.これは MIX_ENV
ごとに用意し,各MIX_ENV
ごとに読み込まれるファイルが変更される.
なので,環境にかかわらず共通の設定は, config.exs
に記述する.そして,config.exs
の末尾の
import_config "#{Mix.env()}.exs"
という記述がにより,追加で各環境の設定ファイルを読み込んでいる.
そして,各環境ごとの設定にも,
import_config "prod.secret.exs"
というような設定がされている.
- config.exs
- dev.exs
- test.exs
- prod.exs
はgitにコミットして管理しておき,本番サーバで使いたい秘匿情報だけを prod.secret.exs
に記述し,これはgitにコミットしないというのが一般的な運用方法だ.
Pleromaでは,prod.exs
までは本家で管理してしまっているため,各サーバごとのカスタムの設定は prod.secret.exs
に入れて管理するように案内している.そのため,最初に generated_config.exs
を prod.secret.exs
にコピーさせている.そのため,設定を変更したい場合は,prod.secret.exs
を変更する.
また,ローカルや,テスト環境を構築している方が,その設定をカスタムしたい場合には dev.secret.exs
やtest.secret.exs
を用意し,そこに設定を追加すれば良い.
Mixのimport config
は,同じ設定項目であればあとから読み込んだもので上書きしてくれるので,既存の設定値を書き換えたいときも *.secret.exs
に書けば良い.
例えばファイルのアップロード先をS3にする設定や,
config :pleroma, Pleroma.Uploaders.S3, bucket: System.get_env("S3_BUCKET"), # Using CloudFront which name is same as s3 bucket name. # So if we set public endpoint, the URL is `https://media.pleroma.io/media.pleroma.io/filename.png`. public_endpoint: "https://" config :ex_aws, :s3, access_key_id: [{:system, "AWS_ACCESS_KEY_ID"}, :instance_role], secret_access_key: [{:system, "AWS_SECRET_ACCESS_KEY"}, :instance_role], region: "ap-northeast-1", scheme: "https://" config :pleroma, Pleroma.Upload, uploader: Pleroma.Uploaders.S3, strip_exif: false
エラーログをSlackに出す設定とか,
config :logger, backends: [:console, Quack.Logger] config :quack, level: :error
入れたりする.
Dockerを使う場合
Dockerを使う場合は,上記のような設定を詰め込んだDockerfileを作った上で,migrationを流す必要がある.
Dockerの場合,Image内に秘匿ファイルを入れるのはあまり良い方法ではない.そのため,prod.secret.exs
に平文でパスワード等を書いて,Dockerfileに詰め込むのは危険だ.
そこで,パスワード等はすべて環境変数から取得させる.
config :pleroma, Pleroma.Repo, adapter: Ecto.Adapters.Postgres, username: System.get_env("DB_USER"), password: System.get_env("DB_PASSWORD"), database: System.get_env("DB_NAME"), hostname: System.get_env("DB_HOST"), pool_size: 15, timeout: 60_000
DB_USER
やDB_PASSWORD
は,docker実行時に環境変数として差し込めば良い.
pleroma.ioの場合,これらの秘匿情報はAWS SSM Patameter Storeに格納してあり,これをentrypointで起動時に復号化している.
あとは,起動時にmigrationを流せれば良い.
$ MIX_ENV=prod mix ecto.migrate
これは,docker run
コマンドで実行しても良いし,ECSやKubernetes上で実行するのであれば,タスクやJobを使えば実行できるだろう.
pleroma.ioでの運用方法
リポジトリについて
pleroma.ioは,github上にリポジトリを用意している. たまに(気が向いたときに),本家からupstreamをpullしてきて,mergeコミットを積んだPullRequestを作ってマージしている.
マージ後はCircleCI内から自動デプロイされる.
pleroma.io/config.yml at master · h3poteto/pleroma.io · GitHub
サーバの運用について
そもそも俺はもともとPleroma以外にも個人のウェブサービスを運用しており,それらをまとめて全部AWS上のKubernetesクラスタに乗せている. そのため,Pleroma.ioも同じクラスた上で動いている.
pleroma.io/Dockerfile at master · h3poteto/pleroma.io · GitHub
こんなDockerfileを用意して,こいつをCircleCIでbuildした上で,imageはECRにpushする.
KubernetesはEKSではなく,kopsで構築している.
なおAWS自体はterraform管理しているので,kopsが触るところを微妙に上書きしている.
その上で,先程ECRにpushしたimageを使って,DeploymentとJobを定義している.
Jobはmigration実行用に定義してあり,これをCircleCIから叩いてデプロイ前にmigrationを実行している.
監視について
PleromaはPrometheusの口を用意してくれている.
https://docs-develop.pleroma.social/backend/API/prometheus/
これを使って,Prometheusでメトリクスを取得し,Grafanaで可視化している.
同じ方法でKubernetesクラスタ自体の監視も行っている.
まとめ
そもそもなんで俺がPleromaが好きかというと,Pleromaに出会う前からElixirがめちゃくちゃ好きなのと,ActivityPubのプロトコルを眺めたときに,こういう仕様のサーバが実にElixir/Erlang向きだと思ったからだ. もちろん,ElixirであれRubyであれJavascriptであれ,だいたいなんでもやろうと思えばできるように作られている. ただ,頑張って本来得意でないものを作ると,それは運用コストとしてサーバ管理者の手間に載ってくる.アーキテクチャが仕様にぴったりあっているというのは,それだけで運用コストが大きく減るくらいに大事なことだ.
そういう運用面の手間という意味でも,PleromaはMastodonより軽量だ.
最後に,基本的に現状ではMastodonのほうが開発者が多く,コミットも多い.なのでMastodonのほうが新機能の提案,開発は早くなる傾向にある. これは単純に人数が多いから仕方ない.それ故,Pleromaは未実装の機能を結構見つけることができるし,developブランチはバグも結構ある.
が,つまりこれはコミットチャンスだ. 日頃仕事でElixirを書きたくて書きたくてしょうがない俺としては,本番運用でマトモなユーザが使っていて,それなりに仕様が複雑になりつつあるElixirプロダクトを触れるチャンスだ.
もしこの記事を読んで,Pleromaに興味を持った方がいたら,ぜひサーバを立ててみてほしい.そしてバグがあったら報告してくれると嬉しい.直せるかはわからないけど……,楽しみにしている.