Reviers, le 28 juillet 2023

La première journée de codage dans PostgreSQL ayant été très appréciée par les participants, une deuxième journée a eu lieu.

Due à un planning compliqué, cette journée a été réalisée à distance. La version 15 nous a fourni un sujet bien intéressant : les modules d’archivage.

Qu’est-ce qu’un module d’archivage ?

Depuis la version 8.2, il est possible de configurer une commande d’archivage des journaux de transactions. Un processus archiver se charge d’exécuter cette commande à chaque fois qu’un journal est prêt à être archivé. La configuration est assez simple : il faut activer le mode archivage en configurant le paramètre archive_mode à on et indiquer la commande d’archivage avec le paramètre archive_command. Ce paramètre accepte deux caractères joker : %f qui est remplacé par le nom du fichier, %p qui est remplacé par le chemin relatif vers ce fichier (relatif par rapport au répertoire principal des données).

En version 15, les développeurs de PostgreSQL ont proposé de configurer un module d’archivage. Celui-ci est déclaré en utilisant le paramètre archive_library. Passer en version 15 n’oblige pas à basculer de commande à module. Il est possible d’utiliser l’un ou l’autre. Si les deux sont configurés, la commande est ignorée et seul le module est utilisé. Il est à noter qu’à partir de la version 16 (en beta 2 actuellement), PostgreSQL refusera la nouvelle configuration si les deux sont configurés.

La journée de codage a donc pour but de coder un module d’archivage qui passe par la bibliothèque libzip pour créer rapidement des archives ZIP des journaux de transactions.

Je ne reviendrai pas sur la création d’une extension, le premier article faisant un tour assez complet sur la question.

Commençons un squelette d’extension

Avant de commencer, une petite note sur le code qui va être présenté ici. Ce code est disponible sur le dépôt GitHub des journées de hacking PostgreSQL. Nous avons ajouté des tags pour chaque étape. Cette étape correspond au tag j2e1.

Le module, et donc l’extension, va s’appeler zip_archive. Nous avons besoin de plusieurs fichiers que voici.

Le fichier contrôle (zip_archive.control) est très basique et ne changera pas sur tout l’article :

comment = 'Archivage ZIP des journaux de transactions'
default_version = '1.0'

Nous allons déclarer une fonction get_libzip_version() qui, pour l’instant, ne renverra qu’un simple texte en dur. Voici donc le fichier zip_archive--1.0.sql :

\echo Ne pas exécuter ce script, mais passer par CREATE EXTENSION

CREATE OR REPLACE FUNCTION get_libzip_version()
RETURNS text
AS '$libdir/zip_archive', 'get_libzip_version'
LANGUAGE C;

Ce fichier sera amené à évoluer au fur et à mesure qu’on ajoutera des fonctionnalités à notre extension.

Le fichier Makefile ne change pas particulièrement de ce que nous avons pu voir la fois précédente :

EXTENSION = zip_archive
MODULE_big = zip_archive
OBJS = zip_archive.o
DATA = zip_archive--1.0.sql
PGFILEDESC = "zip_archive - zip archive module"

PG_CONFIG = pg_config
PGXS := $(shell $(PG_CONFIG) --pgxs)
include $(PGXS)

Et enfin, voici le code C (fichier zip_archive.c) pour notre fonction de version :

/* PostgreSQL headers */
#include "postgres.h"
#include "fmgr.h"
#include "utils/builtins.h"

/* module declaration */
PG_MODULE_MAGIC;

/* function code */

PG_FUNCTION_INFO_V1(get_libzip_version);

/*
 * get_libzip_version
 *
 * Returns libzip version.
 */
Datum
get_libzip_version(PG_FUNCTION_ARGS)
{
  PG_RETURN_TEXT_P(cstring_to_text("texte de test"));
}

La compilation et l’installation se font avec les étapes standards : make et make install. Voici le résultat :

gcc -Wall -Wmissing-prototypes -Wpointer-arith -Wdeclaration-after-statement -Werror=vla -Wendif-labels -Wmissing-format-attribute -Wimplicit-fallthrough=3 -Wcast-function-type -Wformat-security -fno-strict-aliasing -fwrapv -fexcess-precision=standard -Wno-format-truncation -Wno-stringop-truncation -g -fno-omit-frame-pointer -ggdb -fPIC -I. -I./ -I/opt/postgresql/15/include/server -I/opt/postgresql/15/include/internal  -D_GNU_SOURCE -I/usr/include/libxml2   -c -o zip_archive.o zip_archive.c
gcc -Wall -Wmissing-prototypes -Wpointer-arith -Wdeclaration-after-statement -Werror=vla -Wendif-labels -Wmissing-format-attribute -Wimplicit-fallthrough=3 -Wcast-function-type -Wformat-security -fno-strict-aliasing -fwrapv -fexcess-precision=standard -Wno-format-truncation -Wno-stringop-truncation -g -fno-omit-frame-pointer -ggdb -fPIC -shared -o zip_archive.so zip_archive.o -L/opt/postgresql/15/lib    -Wl,--as-needed -Wl,-rpath,'/opt/postgresql/15/lib',--enable-new-dtags  
/usr/bin/mkdir -p '/opt/postgresql/15/lib'
/usr/bin/mkdir -p '/opt/postgresql/15/share/extension'
/usr/bin/mkdir -p '/opt/postgresql/15/share/extension'
/usr/bin/install -c -m 755  zip_archive.so '/opt/postgresql/15/lib/zip_archive.so'
/usr/bin/install -c -m 644 .//zip_archive.control '/opt/postgresql/15/share/extension/'
/usr/bin/install -c -m 644 .//zip_archive--1.0.sql  '/opt/postgresql/15/share/extension/'

Il ne reste plus qu’à tester :

# CREATE EXTENSION zip_archive ;
CREATE EXTENSION
# SELECT get_libzip_version();
┌────────────────────┐
│ get_libzip_version │
├────────────────────┤
│ texte de test      │
└────────────────────┘
(1 row)

Bon, ceci, c’était la partie facile. Nous n’avons fait que reprendre ce que nous avions vu la dernière fois. Commençons maintenant à découvrir de nouvelles choses.

Comment se lier à une bibliothèque externe

Nous allons utiliser les fonctions de la bibliothèque libzip. Pour compiler notre extension, nous allons avoir besoin du paquet de développement de libzip. Pour les distributions compatibles Red Hat, cela se fait ainsi :

sudo dnf install libzip-devel

Le code C doit être modifié pour inclure le fichier d’en-tête de libzip :

/* libzip header */
#include <zip.h>

ainsi que l’appel à la fonction zip_libzip_version() (documentation) :

PG_RETURN_TEXT_P(cstring_to_text(zip_libzip_version()));

Le fichier Makefile doit aussi être modifié pour inclure la bibliothèque partagée :

SHLIB_LINK = -lzip

Le code est disponible sur le tag j2e2. À la compilation, nous pouvons remarquer la prise en compte de la bibliothèque libzip :

[...]
gcc -Wall [...] -lzip 
[...]

Il n’est pas nécessaire de redémarrer PostgreSQL ou de recréer l’extension. L’ouverture d’une session suffit :

# SELECT get_libzip_version();
┌────────────────────┐
│ get_libzip_version │
├────────────────────┤
│ 1.9.2              │
└────────────────────┘
(1 row)

Nous pouvons donc utiliser les fonctions de cette bibliothèque. Et ça tombe bien, nous allons en avoir besoin pour créer et remplir notre archive ZIP !

Créer un module d’archivage

Commençons par discuter de la conception d’un module d’archivage. Le manuel décrit cela au chapitre Archive modules. En résumé, un module d’archivage peut définir quatre fonctions callback :

  • _PG_archive_module_init(), fonction d’initialisation ;
  • check_configured_cb(), fonction de vérification de la configuration, optionnelle ;
  • archive_file_cb(), fonction d’archivage d’un journal ;
  • shutdown_cb(), fonction d’arrêt de l’archivage, optionnelle.

Nous allons commencer par un module basique qui ne fera que tracer le nom du fichier à archiver et son chemin. Pour cela, nous allons devoir coder une fonction archive_file_cb() et il faudra la déclarer dans la fonction _PG_archive_module_init().

La fonction _PG_archive_module_init() sert à renseigner les fonctions callback, un peu comme _PG_init() avec le système des hooks. Voici le code de notre fonction :

void
_PG_archive_module_init(ArchiveModuleCallbacks *cb)
{
  cb->archive_file_cb = zip_archive_file;
}

Notre fonction d’archivage s’appelle donc zip_archive_file(). Elle prend deux arguments :

  • file, qui est le nom du fichier ;
  • path, qui est le chemin relatif à partir du répertoire principal des données de PostgreSQL.

Elle renvoie en fin d’exécution un booléen indiquant le succès ou l’échec de l’archivage.

Cette fonction va pour l’instant simplement renvoyer deux lignes de trace indiquant la valeur des arguments de la fonction. En voici le code :

static bool
zip_archive_file(const char *file, const char *path)
{
  elog(LOG, "file is \"%s\"", file);
  elog(LOG, "path is \"%s\"", path);

  return true;
}

D’autres modifications sont nécessaires. Notamment, il faut ajouter l’inclusion du fichier d’en-tête pgarch.h mais aussi déclarer les deux fonctions. Voici les lignes concernées :

#include "postmaster/pgarch.h"

[...]

/* function definitions */
void    _PG_archive_module_init(ArchiveModuleCallbacks *cb);
static bool zip_archive_file(const char *file, const char *path);

Le code final est disponible sur le tag j2e3.

Après compilation et installation, il nous reste à configurer PostgreSQL. Le paramètre archive_mode doit être configuré à on, et le paramètre archive_library doit être configuré à zip_archive.

Ceci fait, il est nécessaire de redémarrer PostgreSQL.

Pour tester le bon fonctionnement de l’archivage (et donc de notre code), il ne reste plus qu’à réaliser une écriture dans une base (par exemple en ajoutant une ligne dans une table), puis d’exécuter pg_switch_wal().

Et voici la trace obtenue :

archiver[25570] LOG:  file is "000000010000000800000026"
archiver[25570] LOG:  path is "pg_wal/000000010000000800000026"

(ceci avec un paramètre log_line_prefix à '%b[%p] ').

Et si on archivait réellement le journal de transaction

Pour cela, il faut utiliser la bibliothèque libzip. Pour insérer un fichier dans une archive ZIP par l’intermédiaire de cette bibliothèque, il faut commencer par ouvrir l’archive avec la fonction zip_open, ajouter un fichier à l’archive ZIP grâce aux fonctions zip_source_file et zip_file_add et enfin fermer l’archive avec zip_close.

Voici donc le code de la fonction zip_archive_file() une fois ces fonctions ajoutées :

static bool
zip_archive_file(const char *file, const char *path)
{
  zip_t        *ziparchive;
  zip_source_t *zipsource;
  int           error;

  elog(LOG, "archiving \"%s\"", file);

  ziparchive = zip_open("/tmp/test.zip", ZIP_CREATE, &error);
  zipsource = zip_source_file(ziparchive, path, 0, 0);
  zip_file_add(ziparchive, file, zipsource, ZIP_FL_ENC_GUESS);
  zip_close(ziparchive);

  elog(LOG, "\"%s\" is archived!", file);

  return true;
}

Une fois le source (tag j2e4) compilé, installé et PostgreSQL redémarré, voici le résultat dans les traces :

archiver[28197] LOG:  archiving "000000010000000800000028"
archiver[28197] LOG:  "000000010000000800000028" is archived!

Ce sont bien les nouvelles traces. Mais avons-nous une archive ZIP contenant un journal de transactions ?

$ ll /tmp/test.zip 
-rw-------. 1 guillaume guillaume 33257 Jun 19 14:31 /tmp/test.zip
$ zipinfo /tmp/test.zip 
Archive:  /tmp/test.zip
Zip file size: 33257 bytes, number of entries: 2
-rw-------  6.3 unx 16777216 b- defX 23-Jun-19 14:30 000000010000000800000028
-rw-------  6.3 unx 16777216 b- defX 23-Jun-19 14:31 000000010000000800000029
2 files, 33554432 bytes uncompressed, 32987 bytes compressed:  99.9%

L’archive existe bien et contient des journaux. Notre module est fonctionnel !

Ajout d’un premier paramètre de configuration

Notre module fonctionne bien mais il reste assez basique. Notamment, l’archive est stockée dans /tmp, ce qui est très discutable, et son nom est fixe et moyennement bien choisi.

Nous allons donc ajouter un paramètre de configuration pour permettre la sélection du répertoire de stockage. Nous allons aussi nommer l’archive d’après le nom de l’instance si le paramètre cluster_name est configuré. Dans le cas contraire, nous utiliserons simplement zip_archive.zip.

Nous avons vu lors de la première journée comment ajouter un paramètre de configuration. Nous allons faire ici de même, en sachant que, cette fois, le type du paramètre est text.

Pour cela, il nous faut ajouter l’inclusion du fichier d’en-tête utils/guc.h (qui concerne les paramètres de configuration) :

#include "utils/guc.h"

Nous définissons ensuite une variable qui contiendra la valeur de notre paramètre de configuration :

static char *archive_directory = NULL;

Nous définissons les fonctions _PG_init (qui permet de déclarer notre paramètre de configuration), ainsi que zip_archive_configured() qui sera chargée de vérifier la bonne configuration de notre paramètre :

void        _PG_init(void);
static bool zip_archive_configured(void);

Voici le code de la fonction _PG_init

void
_PG_init(void)
{
  DefineCustomStringVariable("zip_archive.archive_directory",
                 gettext_noop("Répertoire contenant le fichier de l'archive ZIP."),
                 NULL,
                 &archive_directory,
                 "",
                 PGC_SIGHUP,
                 0,
                 NULL, NULL, NULL);

  MarkGUCPrefixReserved("zip_archive");
}

À noter la présence de la fonction MarkGUCPrefixReserved(). Nous ne connaissions pas cette fonction lors de la première journée, et c’est bien dommage. Cette fonction permet de réserver le préfixe de notre extension pour qu’aucune autre extension ne puisse aussi l’utiliser.

Ensuite, il faut définir la fonction callback check_configured_cb avec notre fonction de vérification de la configuration :

cb->check_configured_cb = zip_archive_configured;

La fonction de vérification reste assez basique. Elle s’assure que le paramètre n’est pas une chaîne vide :

static bool
zip_archive_configured(void)
{
  return archive_directory != NULL && archive_directory[0] != '\0';
}

On pourrait aller bien plus loin. Par exemple, il serait possible de tester l’existence ainsi que les droits de ce répertoire. Nous allons laisser cet exercice à nos lecteurs et lectrices.

Reste enfin le calcul de la destination :

char          destination[MAXPGPATH];

if (cluster_name != NULL && cluster_name[0] != '\0')
{
  snprintf(destination, MAXPGPATH, "%s/%s.zip", archive_directory, cluster_name);
}
else
{
  snprintf(destination, MAXPGPATH, "%s/%s.zip", archive_directory, "zip_archive");
}

ziparchive = zip_open(destination, ZIP_CREATE, &error);

Le code de cette partie est disponible sur le tag j2e5.

Après compilation, installation, redémarrage de PostgreSQL, et changement de journal de transactions, voici ce qui apparaît dans les traces :

archiver[32611] WARNING:  archive_mode enabled, yet archiving is not configured

C’est logique, nous n’avons pas configuré notre module. Ajoutons donc la ligne suivante dans le fichier postgresql.conf de l’instance, et rechargeons la configuration :

zip_archive.archive_directory = '/home/guillaume'

Et voici ce qui apparaît dans les traces du serveur :

postmaster[32598] LOG:  received SIGHUP, reloading configuration files
postmaster[32598] LOG:  parameter "zip_archive.archive_directory" changed to "/home/guillaume"
archiver[32611] LOG:  archiving "00000001000000080000002C"
archiver[32611] LOG:  archiver destination "/home/guillaume/r15.zip"
archiver[32611] LOG:  "00000001000000080000002C" is archived!

Le répertoire d’archivage et le nom de l’archive sont bien pris en compte.

Ajout d’un second paramètre de configuration

En parcourant la liste des fonctions de cette bibliothèque libzip, je me suis aperçu qu’il est possible d’indiquer une méthode de compression pour chaque fichier ajouté. Nous allons donc ajouter un paramètre de configuration pour permettre la sélection d’une méthode de compression.

Le paramètre sera de type enum parce que plusieurs méthodes sont disponibles (bzip2, zlib, xz et zstd).

Nous allons donc commencer par définir un type enum en C avec la liste des méthodes disponibles :

typedef enum CompressionMethods
{
  UNCOMPRESSED,
  BZIP2,
  ZLIB,
  XZ,
  ZSTD
} CompressionMethod;

Puis, nous allons définir les valeurs du type enum pour PostgreSQL :

static const struct config_enum_entry compression_methods[] = {
  {"uncompressed", UNCOMPRESSED, false},
  {"bzip2", BZIP2, false},
  {"zlib", ZLIB, false},
  {"xz", XZ, false},
  {"zstd", ZSTD, false},
  {NULL, 0, false}
};

Dans le sous-tableau, le premier élément indique le texte de l’option, le deuxième correspond à la valeur de l’option (suivant l’enum défini précédemment) et le troisième un booléen indiquant si l’option est visible ou non par les utilisateurs.

Nous pouvons maintenant définir la variable C pour notre paramètre de configuration :

static int compression_method = ZLIB;

La variable C est bien de type int mais la valeur qu’on lui donne est un des éléments du type C CompressionMethod.

Nous pouvons enfin définir le paramètre de configuration compression_method de type enum :

DefineCustomEnumVariable("zip_archive.compression_method",
  "Méthode utilisée pour la compression.",
  NULL,
  &compression_method,
  ZLIB,
  compression_methods,
  PGC_SIGHUP,
  0,
  NULL, NULL, NULL);

La valeur par défaut configurée est la valeur par défaut de la bibliothèque libzip.

Il ne reste plus qu’à prendre en compte la compression configurée juste après l’ajout du fichier :

int           compression;
switch(compression_method)
{
  case UNCOMPRESSED:
    compression = ZIP_CM_STORE;
    break;
  case BZIP2:
    compression = ZIP_CM_BZIP2;
    break;
  case ZLIB:
    compression = ZIP_CM_DEFLATE;
    break;
  case XZ:
    compression = ZIP_CM_XZ;
    break;
  case ZSTD:
    compression = ZIP_CM_ZSTD;
    break;
}
error = zip_set_file_compression(ziparchive, index, compression, 1);

Le code de cette partie est disponible sur le tag j2e7.

Après compilation, installation, configuration de la compression, voici à quoi peut ressembler le contenu de l’archive ZIP :

$ zipinfo /home/guillaume/r15.zip
Archive:  /home/guillaume/r15.zip
Zip file size: 561512 bytes, number of entries: 1
-rw-------  6.3 unx 16777216 b- defX 23-Jun-19 14:31 000000010000000800000032
-rw-------  6.3 unx 16777216 b- bzp2 23-Jun-19 14:45 000000010000000800000033
2 files, 33554432 bytes uncompressed, 561919 bytes compressed:  98.3%

Deux fichiers sont dans l’archive, avec des algorithmes de compression différents (ici compression par défaut, puis compression par bzip2, voir la sixième colonne dans la liste de fichiers), étant donné que j’avais changé la configuration de la compression entre les deux archivages.

Conclusion

Nous avons donc maintenant un module d’archivage fonctionnel, utilisant libzip pour le stockage au format ZIP, et un peu configurable. Cependant, il serait bien de pouvoir lire le contenu de l’archive sans avoir besoin de passer par l’outil zipinfo. Ceci fera l’objet d’un nouvel article.


DALIBO

DALIBO est le spécialiste français de PostgreSQL®. Nous proposons du support, de la formation et du conseil depuis 2005.