Misskey & Webテクノロジー最前線

Misskeyのパフォーマンス改善の取り組み⁠2023年7月

本連載は分散型マイクロブログ用ソフトウェアMisskeyの開発に関する紹介と、関連するWeb技術について解説を行っています。

ここ最近でMisskeyのユーザー数がさらに急激に増えています。そのため、運営者がより少ないコストでサーバーを維持できるよう、Misskeyのスケーラビリティ改善を急いでいます。

今回は、そういった最近のMisskeyのパフォーマンス改善の取り組みについて、検討中のものも含めて紹介したいと思います。

misskey.ioの登録ユーザー数の推移
推移を示すグラフ

Identicon生成の無効化オプション

Identiconはユーザーが自身のアイコンを設定していないときに代わりに表示されるアイコンで、これはユーザーごとに異なるようになっています。

仕組みとしては、https://misskey.example.com/identicon/hogeにリクエストされた際に、hoge部分をシードとしてランダムな画像を生成し、それをレスポンスするようになっています。また、一度生成されたものはキャッシュされます。

ただ新規登録が増えるとidenticon生成の負荷が無視できなくなります。そこでサーバー管理者がユーザーごとのidenticon生成を無効にし、代わりに一律で仮のアイコンを表示するように設定できるようにしました。

child_process.execSyncの回避

Misskeyではサーバーの負荷の状態を見れるウィジェットをユーザーが設置できますが、サーバー負荷を取得するためにMisskeyが使用しているライブラリが、内部的にコストの高いchild_process.execSyncを頻繁に呼んでいるということが分かったため、これも管理者が無効にできるようにしました。

Node.jsにおいては、sync系の関数はイベントループをブロックしてしまうので使用は極力避けるべきです。

JSON.parseの呼び出しを削減

MisskeyのストリーミングAPIでは、内部的にRedisに接続してサーバーマシン間でイベントのやり取りを行っています。

今までは、ストリーミングのコネクションごとにイベントを受け取ってJSONを解析する処理が行われていましたが、これをストリーミングではなくRedisのコネクションごとに行うようにし、メッセージ解析の回数を減らしました。

JSON.parse自体はC++で非常に最適化されて書かれているため本来そこまで負荷の高い処理ではありませんが、塵も積もれば山となるで、呼び出し回数が非常に多い場合はコストが無視できなくなります。

リアクションのポーリング取得

MisskeyはWebSocketを使ったストリーミングによりリアルタイムでコンテンツが更新されることが特徴のひとつですが、コンテンツの更新頻度が高い場合は負荷や通信量が増えてしまいます。

特に「リアクション」に関しては、フォロワー数の多いユーザーが投稿を行った場合、数秒間で何十ものリアクションが付くことは普通で、今後ユーザー数が増えればさらにリアクションの数は増加します。

そこで、一定の条件下でリアクションをリアルタイム取得するのではなく、ポーリングで取得するようにできないか検討しています。ポーリングにすれば「どのリアクションがいくつ付いたか」という情報ひとつだけ送れば済むため、大幅に通信量を削減できると考えています。

リアクション情報のbulk update

MisskeyではDBのクエリを高速化するために、投稿のリアクション情報を投稿自体にもJSON形式で持たせています。

ただ、これについてもフォロワーの多いユーザーの投稿には短い間に大量のリアクションがつくため、投稿レコードの更新処理が頻発します。PostgreSQLにおいてレコードの更新というのは実質的にはレコードの新規挿入+削除と変わらないため、パフォーマンスへの影響は少なくありません。

そこで、まずRedisに新規リアクション情報を保存するようにし、定期的にそこから情報を読み出してまとめて投稿レコードを更新(bulk update)するようにできないか検討しています。

カスタム絵文字参照の効率化

フロントエンドにおいて、今まではカスタム絵文字の参照にJavaScriptのArray.findArray.filterなどを使用して線形探索を行っており、サーバーに登録されているカスタム絵文字の数が多い場合にパフォーマンス悪化の原因になっていました。

そこで、Mapを用いたハッシュテーブル検索に置き換えることで参照を高速化しました。

// before
const emoji = customEmojis.find(x => x.name === 'foo');

// after
const emoji = customEmojisMap.get('foo');

ストールしたWebSocketコネクションの破棄

MisskeyではWebSocketを多用しますが、たまに「既に切断されているが、切断を検知できない⁠⁠、ストールしたコネクションが発生します。今まではそういったケースの考慮がなかったため、サーバーを起動してから時間が経つにつれて不要なコネクションがメモリにたまり続けていました。

そこで、WebSocketのプロトコル制御フレームを利用して定期的にクライアントに対してpingを送信し、pongが返ってこなければコネクションが生きていないと判断してコネクションの破棄を行うようにしました。

また、使用するWebSocket用ライブラリも、今まで使用していたものはあまりメンテナンスされておらず非効率な処理になっている部分もあったため、より活発に開発されているwebsockets/wsに置き換えました。

Misskey Webのバンドルサイズ削減

Misskeyのフロントエンドにおいて、最初に読み込まれるJavaScriptのサイズを以下の改修によって50%ほど削減しました。

  • Unicode絵文字の辞書データを最適化かつ最小限化し、ユーザーが任意で追加の辞書情報をダウンロードできるように
  • CSS Modulesのコンパイル方法を効率化
  • より多くのモジュールをdynamic import化(=必要になった時だけ読み込む)
  • 不要なライブラリの削除など

これによって、Misskeyを最初に開いた時の読み込み速度が改善しています。

キャッシュの効率化

バックエンド内で利用する複数のキャッシュにおいて、共通化して持てるデータが存在していたため一部共有するようにし、メモリ使用量を減らしました。

署名アルゴリズムの変更 / 鍵のサイズの変更

ActivityPubでは、各メッセージに電子署名を行いますが、MisskeyはこれまでRSA 4096bitで署名をしていました。しかし、Mastodonなどが使うRSA 2048bitと比べるとセキュリティの高い設定になっていて、署名の際の計算コストがかなり高くなってしまいます。

そこでMisskeyもRSA 2048bitもしくは、ECDSA、EdDSAなどのより効率的な署名アルゴリズムに変更することを検討しています。ただ、すでに作成された鍵のマイグレーションをどう行うかなどの課題があります。

まとめ

今回は最近のMisskeyのパフォーマンス改善の取り組みを紹介しました。ここで紹介した以外にも、細かなパフォーマンス改善は常に行っています。

パフォーマンス改善においては、まずどの処理の負荷が高いのか、どこがボトルネックになっているのかを調べるところから始めます。Web・Node.jsでは、Chromeの開発者ツールが有用になります。また、時にはビルドツールによって生成されたJavaScriptを目視で確認し、無駄なデータが入っていないか・非効率な構造のコードにコンパイルされていないか確認することもしています。

JSON.parseのように、一見負荷が低い処理であっても、アクセス数が増えて大量に実行されるようになると負荷が問題になる処理というのもあるため、そういった「ユーザー数が増えても問題にならないか」を意識しながら設計・実装する必要もあります。

今後もMisskeyのパフォーマンス改善について特筆すべきものがあれば紹介していきたいと思います。

おすすめ記事

記事・ニュース一覧