quentin.dufour.io/_posts/2021-07-12-chroniques-admin...

14 KiB

layout slug status sitemap title description categories tags
post chroniques-administration-synapse draft false Chroniques d'administration de Synapse Pour l'instant tout va bien, pour l'instant tout...

Tout a commencé le mercredi 30 juin par un message parfaitement innocent de Max qui nous dit que les disques sont presque pleins :

Capture d'écran d'un message de Max alertant sur les disques presque pleins

Pour éviter la catastrophe annoncée, je décide de trouver le coupable. Après une rapide recherche, il apparait que PostgreSQL occupe plus de 50Go. Ayant des SSD de 120 Go, c'est donc majoritairement PostgreSQL qui occupe de la place. En creusant plus profondément, c'est la base de donnée de Synapse (le serveur Matrix) qui prend tout cet espace, les autres bases ne comptant que pour quelques kilo-octets.

À ce même moment, nous ne le savions pas encore, mais le réseau Matrix faisait face à une vague de spam. Sans être responsable de l'indisponibilité qui va suivre, elle plante le décor et participe à expliquer pourquoi nous avons été pris de court. En effet, la croissance rapide de notre base de donnée n'est pas seulement due à des usages normaux, mais aussi au spam qui a généré beaucoup d'activité sur les autres serveurs, et via la fédération, participé à remplir notre base.

Pour finir de planter le décor, notre instance Matrix a 4 ans mais n'a jamais nécessité de nettoyage ou maintenance de quelque sorte que ce soit jusqu'ici, bien que jusqu'à récemment, nous nous fédérerions avec des salons bruyants comme Matrix HQ. Nous avons donc aucune expérience dans ce domaine.

Sûr de moi, je vise une petite maintenance de deux heures où je compte supprimer les salons de discussions vides et réduire l'historique distant des salons très bruyants. La documentation sur le sujet est quasi inexistante mais je me dis que c'est parce que la tâche ne doit pas être si complexe. En réalité, la maintenance durera 4 jours et ne se sera pas passée du tout comme prévu. Je vous propose de revenir ici sur tous les points qui ont bloqué !

Avant d'aller plus loin, je souhaite souligner l'existence de deux guides sur le sujet qui m'ont aidé et qui sont de très bons compléments à cet article :

L'API d'administration de Synapse

Replaçons le contexte : Matrix est un protocole, et spécifie entre autre des API de communications clients à serveurs et serveurs à serveurs. Cependant, à ce jour, les API de Matrix ne permettent pas à des communautés de se gérer totalement en autonomie. La preuve, nous étions en train d'épuiser les ressources du serveur mais nous pouvions rien faire en tant qu'utilisateur pour les libérer. Au delà de la gestion des ressources, il manque aussi des outils pour la gestion du spam et la modération.

En suivant l'actualité de Matrix, on peut voir qu'ils travaillent déjà sur ces fonctionnalités. Pour la gestion de l'espace disque, ils ont une option pour définir la durée de rétention de l'historique d'un salon de discussion mais l'option n'est pas encore disponible dans l'interface. On sait aussi qu'ils ont échangé avec la Quadrature du Net sur les questions de modération.

En attendant la publication de ces fonctionnalités, les développeurs de Synapse ont déplacé ces responsabilités depuis les utilisateurs vers les administrateurs. Ils fournissent aux administrateurs des fonctionnalités manuelles et naives via une API ne faisant pas partie de la norme Matrix.

J'ai commencé par explorer cette API via l'interface web synapse-admin réalisée par la communauté. Elle m'a permis de supprimer presque un millier de comptes invités et quelques salons de discussions vides. Cependant, cette interface montre vite ses limites : elle est très vite ralentie quand il y a beaucoup de contenu et gèrent très mal les opérations en lot (suppression de 40 salons d'un coup par exemple). Enfin, en affichant par défaut tout le contenu qu'elle a à disposition, elle expose inutilement des données personnelles aux administrateurs.

Très vite, je suis passé en ligne de commande avec curl (pour les requêtes HTTP) et jq (pour intéragir avec le JSON), ce qui semble être une pratique qui fait consensus parmi les administrateurs de serveurs Synapse. La mise en route est rapide : il faut commencer par passer un compte Synapse en administrateur dans la base de données

UPDATE users SET admin = 1 WHERE name = '@foo:bar.com';

Ensuite, il faut récupérer un bearer pour ce compte. Pour ma part, je me suis simplement connecté sur Element avec puis j'ai utilisé l'inspecteur réseau de mon navigateur, regardé les détails d'une requête partant vers l'API et extrait l'entête Authorization qui contient le bearer.

Enfin, pour ne pas retaper le bearer à chaque fois, je définis un alias pour ma session (il faut remplacer les points d'interogation avec le bearer que vous avez récupéré précédemment !) :

alias mctl='curl --header "Authorization: Bearer ???"'

Vous pouvez commencer par récupérer quelques informations simples comme le nombre de salons et le nombres de comptes sur votre serveur :

mctl 'https://synapse.tld/_synapse/admin/v1/rooms?limit=0'
mctl 'https://synapse.tld/_synapse/admin/v2/users?from=0&limit=0&guests=true'

Maintenant qu'on a le nombre, on va vouloir récupérer la liste. À vous de choisir si vous voulez écrire un script utilisant le système de pagination de l'API ou tout récupérer d'un coup au risque de mettre une pression importante sur votre serveur.

Ayant moins de 10 000 entrées, j'ai tout récupéré d'un coup :

mctl 'https://synapse.tld/_synapse/admin/v1/rooms?limit=10000' > rooms.json
mctl 'https://synapse.tld/_synapse/admin/v2/users?from=0&limit=10000&guests=true' > users.json

J'ai procédé à chaque fois en deux étapes : référencement des objets à supprimer dans un fichier puis appels à l'API. J'ai commencé par les comptes, et plus particulièrement les comptes invités, qui étaient souvent utilisés quelques minutes avant d'être définitivement perdus :

cat users.json \
  # requête jq manquante 
  >> users_to_delete.txt

Ensuite, LX avait développé un bridge entre Synapse et plusieurs autres protocols de communication comme Mattermost, Facebook ou IRC. Ne nous donnant pas entière satisfaction, nous avons décidé de le décomissioner. Le bridge devant répliqué un grand nombre de données, il était intéressant de supprimer ses données également. Étant donné son évolution et ses différents protocoles, nous avons du réaliser plusieurs requetes via jq :

cat users.json \
  | jq -r '.users[] | select(.name) | select(.name|test("_ezbr_:deuxfleurs.fr$")) | .name' \
  >> users_to_delete.txt
cat users.json \
  | jq -r '.users[] | select(.name) | select(.name|test("^@_ezbr_")) | .name' \
  >> users_to_delete.txt

Il ne reste plus alors qu'à appeler l'API de Synapse :

cat users_to_delete.txt \
  | while read u; do 
      echo "delete $u"
      mctl \
        -w "\n%{http_code}\n" \
        -X POST \
        -H "Content-Type: application/json" \
        -d '{"erase": true}' \
        https://synapse.tld/_synapse/admin/v1/deactivate/$u
      echo -e "done $u\n"
    done

Après que mon nettoyage utilisateur soit terminé, je suis passé du côté des salons. J'ai commencé par référencer les salons vides :

cat rooms.json \
  | jq -r '.rooms[] | select(.joined_local_members == 0) | .room_id'  \
  >> rooms_to_delete.txt

Ensuite je me suis occupé de Easybridge : en effet, il ne créait pas seulement des comptes mais aussi des salons. Là aussi, il m'a fallu chercher différents motifs pour repérer les salons, ce qui a nécessité plusieurs requêtes :

cat rooms.json \
  | jq -r '.rooms[] | select(.canonical_alias) | select(.canonical_alias|test("_ezbr_:deuxfleurs.fr$")) | .room_id' \
  >> rooms_to_delete.txt
cat rooms.json \
  | jq -r '.rooms[] | select(.creator) | select(.creator|test("_ezbr_:deuxfleurs.fr$")) | .room_id' \
  >> rooms_to_delete.txt
cat rooms.json \
  | jq -r '.rooms[] | select(.creator) | select(.creator|test("^@_ezbr_")) | .room_id' \
  >> rooms_to_delete.txt

Enfin, bien que nous n'avions pas prévu à l'origine de supprimer des salons auxquelles nous participions encore, il est apparu que certains étaient particulièrement couteux à suivre. Cette information n'est pas disponible via l'API, cependant à l'aide de requêtes SQL plus loin, nous avons déterminé que les 6 salons suivants étaient trop couteux à suivre pour nous (Matrix HQ, Matrix HQ (old), Arch Linux (old), tor, openwrt et fedora-devel) :

cat >> rooms_to_delete.txt <<EOF
!OGEhHVWSdvArJzumhm:matrix.org 
!mpvDHdMSZHzhzEDirR:matrix.org
!QtykxKocfZaZOUrTwp:matrix.org
!gVMacPcvhtqaEfaANo:matrix.org
!SEgsRQLScqPxYtucHl:archlinux.org
!MqVoatBTzkpWvekEvo:matrix.org
EOF

Maintenant que notre liste est complète, on peut supprimer les salons (l'API est synchrone) :

cat rooms_to_delete.txt \
  | while read r; do 
    echo -e "delete $r\n"
    mctl \
      -w "\n%{http_code}\n" \
      -X DELETE \
      -H "Content-Type: application/json" \
      -d '{"purge": true}' \
      https://synapse.tld/_synapse/admin/v1/rooms/$r 
  done

Pour les salons qui restent, nous voulons limiter l'historique des contenus distants (c'est à dire des messages postés par les internautes ayant un compte rattaché à un autre serveur que le notre) à seulement deux mois. Si on veut remonter plus loin, on peut simplement redemander leur historique à leur serveur d'accueil.

Cette fois-ci l'API est asynchrone et on ne veut pas surcharger le serveur : on va surveiller la suppression courante avant d'en lancer une autre. On veut aussi retélécharger la liste des salons avant de commencer car elle a bien changé : on en a supprimé beaucoup juste avant !

mctl 'https://synapse.tld/_synapse/admin/v1/rooms?limit=10000' > rooms.json

#                    c'est ici qu'on définit le deux mois VVVVVVVVVVVV
export MX_UNIX_TIMESTAMP=$(date +%s%3N --date='TZ="UTC+2" 2 months ago')

cat rooms.json \
  | jq -r '.rooms[] | .room_id' \
  | while read room; do 
      JOB_ID=$(mctl -s -X POST -H "Content-Type: application/json" -d "{\"delete_local_events\": false, \"purge_up_to_ts\": $MX_UNIX_TIMESTAMP}" "https://synapse.tld/_synapse/admin/v1/purge_history/$room" | jq -r '.purge_id')
      echo Purge $room, job $JOB_ID
      while true; do
        STATUS=$(mctl -s "https://synapse.tld/_synapse/admin/v1/purge_history_status/$JOB_ID" | jq -r ".status")
        echo Purge $room, job $JOB_ID, status $STATUS
        if [ "$STATUS" = "complete" ]; then break; fi
        if [ "$STATUS" = "null" ]; then break; fi
        sleep 10
      done
    done

Finir par mettre les mains dans le SQL

export PGPASSWORD="???"
alias mpsql='psql -h 127.0.0.1 -U matrix synapse'

Pour avoir une idée de ce "coût", nous avons du passer par SQL (attention les commandes mettent beaucoup de temps à s'exécuter).

Cette première commande permet d'avoir le nombre ??? Vous pouvez adapter la limite pour afficher plus de

SELECT room_id, count(*) AS count
    FROM state_groups_state
    GROUP BY room_id
    ORDER BY count DESC
    LIMIT 6;

-- Result (the last column has been added by me):
-- !OGEhHVWSdvArJzumhm:matrix.org    | 51203310 | matrix HQ 
-- !mpvDHdMSZHzhzEDirR:matrix.org    | 19954563 | tor
-- !QtykxKocfZaZOUrTwp:matrix.org    | 14727741 | Matrix HQ
-- !gVMacPcvhtqaEfaANo:matrix.org    | 11356723 | fedora-devel
-- !SEgsRQLScqPxYtucHl:archlinux.org |  5326554 | Arch Linux (old)
-- !MqVoatBTzkpWvekEvo:matrix.org    |  2961032 | #openwrt

Cette commande est plus rapide, c'est elle aussi qui est utilisée en interne pour calculer une complexité arbitraire pour les salons :

 SELECT r.name, s.room_id, s.current_state_events
  FROM room_stats_current s
  LEFT JOIN room_stats_state r USING (room_id)
  ORDER BY current_state_events DESC
  LIMIT 6;

; Result:
; Matrix HQ                      | !OGEhHVWSdvArJzumhm:matrix.org         |                57475
; #openwrt                       | !MqVoatBTzkpWvekEvo:matrix.org         |                16083
; Arch Linux (old)               | !SEgsRQLScqPxYtucHl:archlinux.org      |                15718
; Yggdrasil                      | !DwmKuvGvRKciqyFcxv:matrix.org         |                 3462
; Synapse Announcements          | !qBFNwucQebGPQldAnq:matrix.org         |                 2755
; Synapse Announcements          | !iyIlInqJyxXrRmRHFx:matrix.org         |                 2544

Quelques changements de configuration

presence:
  enabled: false
limit_remote_rooms:
  enabled: true
  complexity: 3.0
  complexity_error: "Ce salon de discussion a trop d'activité, le serveur n'est pas assez puissant pour le rejoindre. N'hésitez pas à remonter l'information à l'équipe technique, nous pourrons ajuster la limitation au besoin."
  admins_can_join: false
retention:
  enabled: true
  # no default policy for now, this is intended. 
  # DO NOT ADD ONE BECAUSE THIS IS DANGEROUS AND WILL DELETE CONTENT WE WANT TO KEEP!
  purge_jobs:
    - interval: 1d

VACUUM FULL et Tablespace, un duo de choc

Quand le WAL te met au pied du mur

Stolon : initialisation, mise à jour et subtilités

keeper a besoin de la libc

très facile de flush totalement le cluster : reinit le cluster

import/export sql, le trick de pv :P

D'autres outils