SwordArMor

Faire tenir la charge à une instance mastodon malgré Musk

Si vous lisez ce blog, vous savez sûrement qu’un riche mégalomane étasunien a racheté twitter, et qu’on en attendait pas moins de son mode de gestion. Cela a entraîné un exode vers mastodon, qui ronronnait tranquillement dans son coin jusqu’ici.
Je vais ici partager les différentes actions que j’ai dû entreprendre ces derniers jours afin de maintenir hostux.social à flots. Avant toutes choses, il convient de remercier Leonora Tindall d’avoir écrit Scaling Mastodon in the Face of an Exodus, car cet article m’a beaucoup aidé, pour la configuration sidekiq notamment.

Afin de contextualiser ce billet, hostux.social hébergeait environ 4000 personnes (pour moins de 1000 actives) avant la vague d’arrivées. Le tout tenait sur une VM avec 8 vCPU et 12 Go de RAM.
Je précise également que gérer des serveurs n’est pas mon métier, je suis dans le réseau à la base. Alors même si ça reste « de l’informatique », ce n’est pas quelque chose avec lequel je suis forcément à l’aise, ni qui me fait particulièrement plaisir. (Proposer une alternative libre et décentralisée, même à mon échelle, ça me fait plaisir, par contre.)

La première action afin de gérer l’afflux de personnes a simplement été de fermer les inscriptions, et ce pour une raison totalement non technique, mais par manque de temps pour traiter toutes les demandes. Nous réouvrions les vannes de temps en temps, quand la vague précédente était passée.

Ensuite, quelque chose qui avait l’air d’être une bonne idée mais qui en fait n’en est pas une : supprimer les anciens comptes inactifs afin d’alléger un peu la base de données. Ce n’est en fait pas une bonne idée car celà crée plein de tâches dans le sidekiq et ralenti considérablement l’instance le temps que ça soit traité. Nous sommes monté à plus de deux millions de tâches en attente à cause de ça alors que même durant les périodes de charge dépasser les 100 000 reste rare.

L’étape suivante qui me venait naturellement à l’esprit a été de séparer la VM de base de données des autres services afin qu’il ait de la RAM dédiée qu’il puisse utiliser en cache. Et ceci a été efficace puisque l’intégralité de la RAM a directement été utilisée à cet effet, permettant de dépiler plus vite le sidekiq.

Arrivé ici, l’interface web a commencé a devenir de plus en plus lente, en raison de nombre de requêtes en parallèle à traiter. Nous avions à ce moment déjà plus ou moins doublé le nombre de comptes actifs. J’ai donc augmenté le nombre de threads de puma à 5 et le nombre de processus par thread à 8.

Environment="WEB_CONCURRENCY=8"
Environment="MAX_THREADS=5"

Cela a augmenté le nombre de connexions à la base de données, je suis donc passé de 100 à 500 connexions maximum dans PostgreSQL (histoire d’avoir un peu de marge, changer cette valeur nécessitant de redémarrer le service).

La charge continuant d’augmenter, tous les soirs la queue sidekiq montait à plus de 10 000 tâches avec parfois des pics aux alentours des 20 000, induisant une latence énorme sur la réception de messages d’autres instances. Là j’ai commencé à regarder comment optimiser sidekiq, et le moins que l’on puisse dire, c’est que la littérature n’est pas très fournie. Dans le doute, j’ai décidé de mettre redis dans sa VM à lui, ça ne pouvait pas faire de mal… C’est préconisé dans la doc de mastodon, mais je ne suis pas sûr que ça ait réellement changé grand chose.

Je suis ensuite tombé sur PGTune qui m’a permis d’ajuster quelques paramètres de PostgreSQL, évitant ainsi à sidekiq d’attendre les réponses au lieu de travailler. Ceci a été très efficace, la taille de la queue a été divisée par deux en quelques minutes, mais je restais pourtant à environ un millier de tâches en attente.
Cela a par contre considérablement augmenté la charge de cette VM. Là où elle tournait avec un load average d’environ 5 avant, il a doublé désormais.

Valère (sur IRC) m’a partagé l’aricle de Leonora Tindall cité en introduction qui explique comment configurer son sidekiq de manière efficace (enfin !). J’ai adapté leurs conseils à ma configuration (je n’utilise pas docker mais des units systemd directement) et j’ai ajouté une target systemd afin de ne pas avoir besoin de démarer les six services sidekiq à la main, ce qui nous donne donc ceci :

root@hostux:~# cat /etc/systemd/system/mastodon-sidekiq.target 
[Unit]
Description=mastodon-sidekiq
After=network.target
Wants=mastodon-sidekiq-default-push-pull.service mastodon-sidekiq-mailers.service mastodon-sidekiq-pull-default-push.service mastodon-sidekiq-push-default-pull.service mastodon-sidekiq-scheduler.service mastodon-sidekiq-default-pull-push.service

[Install]
WantedBy=multi-user.target


root@hostux:~# cat /etc/systemd/system/mastodon-sidekiq-default-pull-push.service 
[Unit]
Description=mastodon-sidekiq
After=network.target
PartOf=mastodon-sidekiq.target

[Service]
Type=simple
User=mastodon
WorkingDirectory=/opt/mastodon/live
Environment="RAILS_ENV=production"
Environment="DB_POOL=25"
Environment="MALLOC_ARENA_MAX=2"
Environment="LD_PRELOAD=/usr/lib/x86_64-linux-gnu/libjemalloc.so.2"
ExecStart=/opt/mastodon/.rbenv/shims/bundle exec sidekiq -c 25 -q default -q pull -q push
TimeoutSec=15
Restart=always

[Install]
WantedBy=mastodon-sidekiq.target


root@hostux:~# cat /etc/systemd/system/mastodon-sidekiq-default-push-pull.service 
[Unit]
Description=mastodon-sidekiq
After=network.target
PartOf=mastodon-sidekiq.target

[Service]
Type=simple
User=mastodon
WorkingDirectory=/opt/mastodon/live
Environment="RAILS_ENV=production"
Environment="DB_POOL=25"
Environment="MALLOC_ARENA_MAX=2"
Environment="LD_PRELOAD=/usr/lib/x86_64-linux-gnu/libjemalloc.so.2"
ExecStart=/opt/mastodon/.rbenv/shims/bundle exec sidekiq -c 25 -q default -q push -q pull
TimeoutSec=15
Restart=always

[Install]
WantedBy=mastodon-sidekiq.target


root@hostux:~# cat /etc/systemd/system/mastodon-sidekiq-mailers.service 
[Unit]
Description=mastodon-sidekiq
After=network.target
PartOf=mastodon-sidekiq.target

[Service]
Type=simple
User=mastodon
WorkingDirectory=/opt/mastodon/live
Environment="RAILS_ENV=production"
Environment="DB_POOL=25"
Environment="MALLOC_ARENA_MAX=2"
Environment="LD_PRELOAD=/usr/lib/x86_64-linux-gnu/libjemalloc.so.2"
ExecStart=/opt/mastodon/.rbenv/shims/bundle exec sidekiq -c 25 -q mailers -q scheduler
TimeoutSec=15
Restart=always

[Install]
WantedBy=mastodon-sidekiq.target


root@hostux:~# cat /etc/systemd/system/mastodon-sidekiq-pull-default-push.service 
[Unit]
Description=mastodon-sidekiq
After=network.target
PartOf=mastodon-sidekiq.target

[Service]
Type=simple
User=mastodon
WorkingDirectory=/opt/mastodon/live
Environment="RAILS_ENV=production"
Environment="DB_POOL=25"
Environment="MALLOC_ARENA_MAX=2"
Environment="LD_PRELOAD=/usr/lib/x86_64-linux-gnu/libjemalloc.so.2"
ExecStart=/opt/mastodon/.rbenv/shims/bundle exec sidekiq -c 25 -q pull -q default -q push
TimeoutSec=15
Restart=always

[Install]
WantedBy=mastodon-sidekiq.target


root@hostux:~# cat /etc/systemd/system/mastodon-sidekiq-push-default-pull.service 
[Unit]
Description=mastodon-sidekiq
After=network.target
PartOf=mastodon-sidekiq.target

[Service]
Type=simple
User=mastodon
WorkingDirectory=/opt/mastodon/live
Environment="RAILS_ENV=production"
Environment="DB_POOL=25"
Environment="MALLOC_ARENA_MAX=2"
Environment="LD_PRELOAD=/usr/lib/x86_64-linux-gnu/libjemalloc.so.2"
ExecStart=/opt/mastodon/.rbenv/shims/bundle exec sidekiq -c 25 -q push -q default -q pull
TimeoutSec=15
Restart=always

[Install]
WantedBy=mastodon-sidekiq.target


root@hostux:~# cat /etc/systemd/system/mastodon-sidekiq-scheduler.service 
[Unit]
Description=mastodon-sidekiq
After=network.target
PartOf=mastodon-sidekiq.target

[Service]
Type=simple
User=mastodon
WorkingDirectory=/opt/mastodon/live
Environment="RAILS_ENV=production"
Environment="DB_POOL=25"
Environment="MALLOC_ARENA_MAX=2"
Environment="LD_PRELOAD=/usr/lib/x86_64-linux-gnu/libjemalloc.so.2"
ExecStart=/opt/mastodon/.rbenv/shims/bundle exec sidekiq -c 25 -q scheduler
TimeoutSec=15
Restart=always

[Install]
WantedBy=mastodon-sidekiq.target

Avec tout ceci l’instance fonctionne de nouveau aussi bien que mastodon le permet. On verra donc dans sur la duré. Pour donner un ordre de grandeur des ressources utilisées, voici ce que j’ai :

  • VM « web » : 12 Go de RAM disponibles, 6 Go utilisés par les applications, le reste en cache. 8 vCPU disponibles, 2 utilisés
  • VM de DB : 12 Go de RAM disponibles, tout est utilisé en cache, 8 vCPU disponibles, 3 utilisés
  • VM ES : 4 Go de RAM disponibles, 3 utilisés, quasi pas de CPU utilisé
  • VM redis : quelques Mo de RAM utilisés et une utilisation CPU négligeable
L’instance comporte environ 2 500 comptes actifs et génère environ 20 Mbps de trafic. Je suis toujours sur la branche 3, passer en 4 nécessiretra de revoir la configuration sidekiq car une queue supplémentaire a été ajoutée.

Si vous avez des questions, je vous invite à me contacter sur mastodon : @alarig@hostux.social.