OS : Debian12

Objectif : Maintenir sa base de données Postgresql SYNAPSE

0) Contexte

J'administre un serveur Matrix-Synapse depuis maintenant 4 ans environs. Sa base de données grossie gentiment pour atteindre 300GB aujourd'hui, et ce malgré quelques gestes d’hygiènes basiques (Rétention à 180 jours, Purge des caches médias locaux, distants, Rooms vides, etc...). Il faut donc passer au niveau supérieur.

Si vous êtes pressé, mon script de maintenance complet est en bas de page.

1) Les mesures AVANT

Relevons le poids des différentes table de la base de données:

psql -t -A -h localhost --dbname=$DBNAME --username=$DBUSER --command="SELECT nspname || '.' || relname AS "relation",
    pg_size_pretty(pg_total_relation_size(C.oid)) AS "total_size"
  FROM pg_class C
  LEFT JOIN pg_namespace N ON (N.oid = C.relnamespace)
  WHERE nspname NOT IN ('pg_catalog', 'information_schema')
    AND C.relkind <> 'i'
    AND nspname !~ '^pg_toast'
  ORDER BY pg_total_relation_size(C.oid) DESC
  LIMIT 20;"

La raison de ce poids important semble provenir d'une accumulation "naturelle" (car progressive dans le temps suivant mon monitoring disque #zabbix) d’événements d’état (state_groups_state) depuis la naissance de l'instance:

public.state_groups_state|297 GB
public.device_lists_changes_in_room|4229 MB
public.received_transactions|1960 MB
public.event_json|1521 MB
public.current_state_delta_stream|1165 MB
public.events|684 MB
public.e2e_cross_signing_keys|646 MB
public.event_auth|475 MB
public.event_edges|423 MB
public.device_lists_remote_cache|371 MB
public.device_lists_stream|262 MB
public.room_memberships|246 MB
public.event_search|183 MB
public.state_events|162 MB
public.event_auth_chains|144 MB
public.device_inbox|138 MB
public.event_auth_chain_links|133 MB
public.event_to_state_groups|100 MB
public.state_group_edges|96 MB
public.cache_invalidation_stream_by_instance|80 MB

On creuse un peu plus pour connaitre la répartition de ces "state_groups_state" entre toutes les rooms:

psql -t -A -h localhost --dbname=$DBNAME --username=$DBUSER --command="SELECT room_id, count(*) AS count FROM state_groups_state GROUP BY room_id HAVING count(*) > 99999 ORDER BY count DESC;"

Avec comme résultat la liste des rooms ayant plus de 99999 de "state_groups_state", par ordre décroissant:

!OGEhHVWSdvArJzumhm:matrix.org|175103593
!zAeWUNBoXONgkVtgqn:mozilla.org|96728651
!yUHxXrvmurdnsPyhiH:sakura.ci|87619662
!wOlkWNmgkAZFxbTaqj:matrix.org|28272082
!PpSxdCZfUIFXMZXivf:waifuhunter.club|9488267
!mKkDdfCLCtkTyqcxEc:nerdsin.space|5182764
!fzfHhoTplYBEXfWOaI:matrix.org|4386744
!UxQFGskJBlUowxdIxQ:tapenet.org|2827290
!GtIfdsfQtQIgbQSxwJ:archlinux.org|2072471

Notons ici que "!OGEhHVWSdvArJzumhm:matrix.org" est la room la plus riches en events "state_groups_state". Retenons ce RoomID.

A savoir que ce type d’événement n'est jamais purgé par les mécanisme de rétention (https://github.com/matrix-org/synapse/blob/develop/docs/message_retention_policies.md) ou de purge d'historique (https://matrix-org.github.io/synapse/latest/admin_api/purge_history_api.html).

Du coup, que fait-on si on ne peut pas les purger? On va les compresser!

2) Installation de rust-synapse-compress-state

C'est un outil disponible ici (https://github.com/matrix-org/rust-synapse-compress-state#compress-synapse-state-tables).

Après avoir installé une version récente de RUST/CARGO (https://www.rust-lang.org/tools/install) comme ceci:

curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

Et après avoir installé quelques dépendances comme celles-ci:

apt install git cmake pkg-config libssl-dev

La compilation du binaire s’effectue sans heurt (https://github.com/matrix-org/rust-synapse-compress-state#building):

su monuser
cd monsuer
git clone https://github.com/matrix-org/rust-synapse-compress-state.git
cd rust-synapse-compress-state
/monuser/.cargo/bin/cargo build
cd rust-synapse-compress-state/synapse_auto_compressor
/monuser/.cargo/bin/cargo build
cp /monuser/rust-synapse-compress-state/target/debug/synapse_auto_compressor /monuser/
cp /monuser/rust-synapse-compress-state/target/debug/synapse_compress_state /monuser/

Au final, on obtient deux binaires => synapse_auto_compressor et synapse_compress_state

synapse_auto_compressor: effectue une compression sur l'ensemble des state_groups_state suivant 2 paramètres synapse_compress_state: effectue une compression sur une room spécifique

Mon approche a été de d'abord cibler manuellement le TOP 10 des rooms riches en state_groups_state avec synapse_compress_state, puis de mettre en place une tache régulière avec synapse_auto_compressor une fois le plus gros déjà compressé.

La compression (manuelle et/ou auto) est une tache qui a nécessité de disposer de:

  • au moins autant d'espace libre que la taille de la BDD sur le disque
  • de 64Go de RAM pour permettre l’exécution sans swap de la tache de compression
  • du temps et de la patience, car les taches peuvent durer plus de 6h sur les grosses rooms. L'usage de Screen est indispensable, et il fortement recommander de disposer d'un monitoring mémoire, cpu, disque du serveur.

3) Exécution de synapse_compress_state

L’exécution, qui peut s'effectuer pendant que synapse est démarré, peu durée longtemps, plusieurs heures. S'agissant de traitement sur la base de données, il serait dommageable de se faire déconnecter durant l'opération.

On va donc utiliser Screen comme filet de sécurité:

apt install screen

On ouvre une session Screen:

screen -S compressor

L'action se fait en 2 étapes. La première consiste en la génération d'un fichier .sql comportant les modifications à effectuer dans le but de compresser. La seconde étape consiste en l'application de ce fichier dans Postgresql.

C'est assez simple (mais probablement très long) car on a déjà fait toute le travail préparatoire. Il suffit d'indiquer l'ID de la room à compresser:

/monsuser/synapse_compress_state -t -o /monuser/synapse_compress_state_OGEhHVWSdvArJzumhm.sql -p "host=localhost user=${DBUSER} password=${PGPASSWORD} dbname=${DBNAME}" -r "!OGEhHVWSdvArJzumhm:matrix.org"

Une fois le fichier .sql généré, il faut l'appliquer à la base de données:

psql -t -A -h localhost --dbname=$DBNAME --username=$DBUSER < /monuser/synapse_compress_state_OGEhHVWSdvArJzumhm.sql

Effectuez cela sur le TOP5/TOP10 selon votre analyse de la base de données de votre Synapse.

Contrôlez via les commandes du chapitre 1 l'évolution de la taille de votre base de données.

NOTA: Postgresql ne libèrera l'espace disque qu'il utilise que s'il est effectué un FULL VACUUM. Cette action nécessite un arrêt de la base de données et un espace disque libre d'au moins l'espace occupé. On aborde ce sujet plus bas.

3) Exécution de synapse_auto_compressor

L’exécution, qui peut s'effectuer pendant que synapse est démarré, peu durée longtemps, plusieurs heures, jours. S'agissant de traitement sur la base de données, il serait dommageable de se faire déconnecter durant l'opération.

On va donc utiliser Screen comme filet de sécurité:

apt install screen

On ouvre une session Screen:

screen -S compressor

Et dans ce "Screen" on exécute:

synapse_auto_compressor -p postgresql://$DBUSER:$PGPASSWORD@localhost/$DBNAME -c 100000 -n 10000000

NOTA2: -c 100000 peut être réduit jusqu’à 500 si vous n'avez pas assez de RAM. Plus -c est élevé, meilleur est la compression. A vous de trouver/tester la valeur qui vous convient

4) Les mesures APRÈS

Relevons le poids des différentes table de la base de données:

psql -t -A -h localhost --dbname=$DBNAME --username=$DBUSER --command="SELECT nspname || '.' || relname AS "relation",
    pg_size_pretty(pg_total_relation_size(C.oid)) AS "total_size"
  FROM pg_class C
  LEFT JOIN pg_namespace N ON (N.oid = C.relnamespace)
  WHERE nspname NOT IN ('pg_catalog', 'information_schema')
    AND C.relkind <> 'i'
    AND nspname !~ '^pg_toast'
  ORDER BY pg_total_relation_size(C.oid) DESC
  LIMIT 20;"

Résultat:

public.state_groups_state|91 GB
public.device_lists_changes_in_room|4229 MB
public.received_transactions|1965 MB
public.event_json|1522 MB
public.current_state_delta_stream|1165 MB
public.events|684 MB
public.e2e_cross_signing_keys|646 MB
public.event_auth|475 MB
public.event_edges|423 MB
public.device_lists_remote_cache|372 MB
public.device_lists_stream|262 MB
public.room_memberships|246 MB
public.event_search|183 MB
public.state_events|162 MB
public.event_auth_chains|144 MB
public.device_inbox|138 MB
public.event_auth_chain_links|133 MB
public.event_to_state_groups|100 MB
public.state_group_edges|96 MB
public.cache_invalidation_stream_by_instance|81 MB

On observe dans mon cas une baisse significative: public.state_groups_state|91 GB

5) Restitution de l'espace libéré - Postgresql FULL VACUUM

Cette action nécessite un arrêt de la base de données et un espace disque libre d'au moins l'espace occupé.

systemctl stop matrix-synapse

psql -t -A -h localhost --dbname=$DBNAME --username=$DBUSER --command="VACUUM FULL VERBOSE;"

systemctl start matrix-synapse

6) Merci

Un grand MERCI à  -> @thomas:lgy.fr [Matrix]

https://quentin.dufour.io/blog/2021-07-12/chroniques-administration-synapse/

https://levans.fr/shrink-synapse-database.html

https://www.tedomum.net/dev/service/matrix/administration/#nettoyage-du-serveur

https://gitea.prk.st/romain/synapse-tools/src/branch/master/synapse-purge.sh

https://github.com/matrix-org/synapse/issues/3364

https://github.com/matrix-org/rust-synapse-compress-state

https://wiki.chatons.org/doku.php/services/messagerie_instantanee/matrix#nettoyage_d_un_serveur_synapse

https://foss-notes.blog.nomagic.uk/2021/03/matrix-database-house-cleaning/

7) Mon script

https://www.underworld.fr/blog/matrix-synapse-script-maintenance-de-la-base-de-donnee-postgresql

8) ONE MORE THING

Avec le temps, ma base a continué de grossir et ce malgré l'application régulière des préceptes/règles d’hygiènes ci-dessus, qui permettaient quand même de limiter les dégâts.

En cherchant... encore... et encore, je suis tombé sur ce blog : https://blog.gelesneeuw.nl/technology/2024/04/20/matrix-maintenance/

Il est fait référence à un "bug" de jeunesse de synapse, qui perdure, dont voici l'issue : https://github.com/matrix-org/synapse/issues/3364

Et dont voici le remède : https://github.com/erikjohnston/synapse-find-unreferenced-state-groups

C'est EXACTEMENT ce qui causait l’embonpoint de ma base de donnée. Après application du traitement, celle-ci est passée de 250Go à 18Go... oui, DIX-HUIT Giga.

Pour faire simple, ARRÊTEZ SYNAPSE, ARRÊTEZ SYNAPSE et ARRÊTEZ SYNAPSE, puis générez le fichier csv qui va contenir la liste des "événements orphelins":

rust-synapse-find-unreferenced-state-groups -p "postgresql://$DBUSER:$PGPASSWORD@localhost/$DBNAME" -o "/home/user/unreferenced.csv"

Enfin, dans le cli psql, une fois connecté à votre base de donnée:

CREATE TEMPORARY TABLE unreffed(id BIGINT PRIMARY KEY);
COPY unreffed FROM '/home/user/unreferenced.csv' WITH (FORMAT 'csv');
DELETE FROM state_groups_state WHERE state_group IN (SELECT id FROM unreffed);
DELETE FROM state_group_edges WHERE state_group IN (SELECT id FROM unreffed);
DELETE FROM state_groups WHERE id IN (SELECT id FROM unreffed);

Ce bug m'aura pourri la vie durant 3 ans, m'imposant des plans de remédiations lourds, des arbitrages de ressources, avec indispo. bref, je suis content :)