Mastodon鯖を速くしたいときに踏みがちな罠(SidekiqとWeb)

## 概要
Mastodon鯖を速くしたくていろいろググるとなんか出るけど、効果がなかったり逆効果になったりしがちなやり方も出てくる。よくあるそんなやつの例と、それよりはマシなやり方を書いていく。

## Sidekiq
MastodonにおけるSidekiqは、他鯖からのトゥートをTLに流したり、逆に自鯖のトゥートを他鯖に配信したりする。
Fediverseが賑わったり、あるいは鯖をしばらく落としてから復旧すると、他鯖からのトゥートが一斉に降ってきて処理が詰まる。
あるいはフォローインポートや垢消しがあると、他鯖への配信処理が一気に走って詰まる。

### よくない方法

* Sidekiqの並列数(とDB_POOL)を(大幅に)増やす。

``` mastodon-sidekiq.service
# 関係ない行は省略
[Service]
Environment="DB_POOL=50"
ExecStart=/home/mastodon/.rbenv/shims/bundle exec sidekiq -c 50
```

PostgresqlとSidekiqのメモリ消費が並列数相応に増える。
しかし、性能改善にはあまり効果がない。というのも、Rubyのプロセスは基本的に1CPUコアしか使えないのである。Mastodonのデフォルトである5並列から増やしても、すぐにCPUが律速になってしまう。少し増やすだけなら効果はあるかもしれないが、この例のように大幅に(5 → 50)増やす意味はない。
さらに悪いことに、小さい鯖でこのような設定をすると、メモリ不足を起こしてかえって遅くなってしまう。

### ましな方法

* Sidekiqプロセス自体を複数作る。
* (CPUやメモリに余裕があれば)Sidekiqキューを複数のプロセスに振り分ける。

まずは前者の例を記載する。
sidekiq用のsystemdのファイルの名前を変更して、簡単に複数起動できるようにしている。
なお、v3.3.0rc2でschedulerキューが追加された。これは複数のsidekiqプロセスで動かしてはいけないので、これ専用のsidekiqプロセスを用意している。

また、sidekiqは時間と共にメモリを無駄に消費してしまうので、定期的に再起動させるようにする。sidekiqが一時的に停止しても、WebUIやAPIがエラーを吐いたりはしないので安心していい。

```
↓このようにファイル名を変えてコピーする
# cp mastodon-sidekiq.service mastodon-sidekiq@.service
```

``` mastodon-sidekiq@.service
#他の行はそのままでおｋ
[Service]
ExecStart=/home/mastodon/.rbenv/shims/bundle exec sidekiq -q default -q push -q pull -q mailers

#定期再起動を仕込む(例では24時間)
RuntimeMaxSec=86400
Restart=always
```

```
↓このようにするだけで複数起動できる
# systemctl enable --now mastodon-sidekiq@0.service
# systemctl enable --now mastodon-sidekiq@1.service

↓紛らわしいのでこっちは消してしまおう
# systemctl disable --now mastodon-sidekiq
# rm mastodon-sidekiq.service
```

``` mastodon-sidekiq-scheduler.service
#他の行はデフォルトの設定からコピーする
[Unit]
Description=mastodon-sidekiq-scheduler
[Service]
ExecStart=/home/mastodon/.rbenv/shims/bundle exec sidekiq -q scheduler
```

```
そしてこいつも有効にする + 起動する
# systemctl enable --now mastodon-sidekiq-scheduler
```


後者の例も記載しておく。

``` mastodon-sidekiq-default.service
#他の行はデフォルトの設定からコピーする
[Unit]
Description=mastodon-sidekiq-default
[Service]
ExecStart=/home/mastodon/.rbenv/shims/bundle exec sidekiq -q default
```
``` mastodon-sidekiq-pull.service
#他の行はデフォルトの設定からコピーする
[Unit]
Description=mastodon-sidekiq-pull
[Service]
ExecStart=/home/mastodon/.rbenv/shims/bundle exec sidekiq -q pull
```
``` mastodon-sidekiq-push-mailers.service
#他の行はデフォルトの設定からコピーする
[Unit]
Description=mastodon-sidekiq-push-mailers
[Service]
ExecStart=/home/mastodon/.rbenv/shims/bundle exec sidekiq -q push -q mailers
```

MastodonはSidekiqキューを以下の4つに分けている。
それぞれの役目は非常に大雑把にいうとこうなる。

* default: ↓以外全部
* ~~push: 他鯖にトゥートを配信したりする~~
* ~~pull: 他鯖のトゥートを保存したりする~~
* push: 連合とのやりとりのうち、優先度が高いもの(トゥートの配信など)
* pull: 連合(同文)、優先度が低いもの(他鯖のトゥをTLに反映する、トゥ消しのうちBTやリプライしてきた相手のTLから消す処理など)
* mailers: メールを送る**など**
* scheduler: スケジュールに基づいて実行する処理(v3.3.0rc2で追加) これは1個のsidekiqプロセスでしか動かしてはいけない。

MastodonのSidekiqキューの名前は嘘つきと化しており、issueが立つほどである(そして放置されている)
https://github.com/tootsuite/mastodon/issues/11175

defaultとpullを詰まらせると、自鯖のタイムラインが崩れるので、詰まらせてしまうのは望ましくない。(本当は全部…と言いたいところだが、mailersはどうせ暇だし、pushが詰まっても他鯖のタイムラインが崩れるが自鯖には影響はない。)
そこで、後者の例ではdefaultとpullは専用のプロセスで動かし、pushとmailersは同じプロセスで動かしている。このようにすると、例えばpushキューが詰まっても、defaultやpullは詰まらないので、連合タイムラインの崩壊を軽減したりできる。なにより、プロセスを複数建てたので、複数のCPUコアの処理能力をSidekiqに活用できるようになる。
さらに速くしたいときは、同じキューに対応するプロセスを複数建ててもいいが、メモリ不足には注意が必要。

一方、メモリに余裕がない場合や、CPUが余ってるのにSidekiqが詰まる場合は、キューを分割**しない**で、前者のように単にsidekiqを複数(最大でCPUコア数分)だけ建てるとよい。このようにすると、例えば連合タイムラインが速くなるとpullキューに大量のジョブが貯まる(→詰まる)が、全sidekiqがpullキューを受け付けていれば、全てのCPUコアの能力をpullキューのために活用できる。この場合は他のキューも同様に詰まってしまうが、遅延が増え続けてタイムラインが使い物にならなくなるよりはマシである。

pushキューに限っては、遅い鯖がある場合にはスレッド数を増やすのも意味がある(遅い鯖の影響でいくつかスレッドが詰まっても全体は詰まらない)…はずなのだが、手元では増やしても全くといっていいほど効果がなかった。

キュー毎にSidekiqを分けたりする効果についてはこの記事が詳しい。
https://logmi.jp/tech/articles/200000

ついでにjemallocを使わせてメモリを節約するのもよい。

``` mastodon-sidekiq-*.service
[Service]
#Ubuntuなどの場合
Environment="LD_PRELOAD=/usr/lib/x86_64-linux-gnu/libjemalloc.so"

#Fedoraなどの場合
Environment="LD_PRELOAD=/usr/lib64/libjemalloc.so"

#他の行は使い回す
```

## Puma(web)
WebUIの表示、ストリーミング以外のタイムライン等の取得、書き込みの受け付けなどを行う。

### よくない方法

* MAX_THREADSを(大幅に)増やす。
* DB_POOLの値を不適切に増やす。

``` mastodon-web.service
# 関係ない行は省略
[Service]
Environment="WEB_CONCURRENCY=4"
Environment="MAX_THREADS=10"
Environment="DB_POOL=40"
```

Sidekiqと同様に、Rubyのプロセスは1CPUコアしか使えないので、MAX_THREADS(各プロセス内での並列数)を増やすのはあまり効果がない。

そして、この例ではDB_POOLの設定の仕方を間違えている。
mastodon-webのDB_POOLは、Pumaの各プロセス毎に適用されるので、この例では 4プロセス * 40接続 = 160接続 まで増やすことを許可してしまっている。
実際にそのような状況になることはおそらくないだろうが、もし接続数が160に達したら、メモリ不足はもとより、Postgresqlの接続数制限に引っかかって鯖がまともに動かなくなるだろう。

mastodon-webのDB_POOLのデフォルト値は「MAX_THREADSの値を使い回す」(この例なら10になる)なので、ほとんどの場合はDB_POOLを設定する必要はない。

### ましな方法

* WEB_CONCURRENCY(プロセス数)を増やす。

``` mastodon-web.service
# 関係ない行は省略
[Service]
Environment="WEB_CONCURRENCY=4"
```
ちなみにWEB_CONCURRENCYの初期値は2である。
なお、CPUコア数より多くするのは、基本的に効果がないと考えていい。
もちろんメモリ不足にも注意。jemallocはここでも有効。


## そもそも
本当にこれらが遅い原因なのかは念入りに調べる必要がある。他にも…

* 回線が遅い
* CPUが遅い
    * 特に1コアだと、画像の圧縮(imagemagickのconvert)で詰まりがち
    * VPSの場合はCPUクレジットにも注意
* メモリがどうしようもなく足りない
    * Swapは多めに確保して、RubyがOSに返せない(確保してるけど使ってない)メモリをSwapに逃がしてやる
    * Ruby 2.7以前ではコンパクションがないため、OSに返せないメモリが発生しやすい(Mastodon v3.1.4時点ではRuby 2.6.6を使ってる)
    * Rubyにjemallocを使わせる
    * zswapを使う。VPSによってはswapへのI/OによってI/O制限を食らって遅くなる場合があるので、これへの対策としても有効。
    * それでもダメなら、スレッド数やプロセス数を絞ってメモリを節約した方がかえって速くなるかも
* DBをHDDに載せてる
    * SSDに載せよう
* 鯖じゃなくてブラウザを動かしてるPCが遅い
* 遅いのは自鯖じゃなくて相手の鯖だった

いろいろある(多分