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
Si vous avez des questions, je vous invite à me contacter sur mastodon : @alarig@hostux.social.