GNU libc 2.28, sortie le 1er août 2018, comprend une mise à jour majeure des locales Unicode en général et des données relatives aux collations en particulier.

L'annonce indique:

The localization data for ISO 14651 is updated to match the 2016 Edition 4 release of the standard, this matches data provided by Unicode 9.0.0. This update introduces significant improvements to the collation of Unicode characters. […] With the update many locales have been updated to take advantage of the new collation information. The new collation information has increased the size of the compiled locale archive or binary locales.

Pour les instances PostgreSQL qui utilisent des collations glibc dépendant de la région et de la langue (exemples: fr_FR.iso885915 ou `en_US.utf-8'), cela signifie que certaines chaînes de caractères seront triées différemment après cette mise à jour. Une conséquence critique est que les index qui dépendent de ces collations doivent impérativement être reconstruits immédiatement après la montée de version de glibc. Les serveurs en réplication WAL/streaming doivent aussi être mis à jour simultanément car un secondaire doit tourner rigoureusement avec les mêmes locales que son primaire.

Le risque autrement est d'engendrer des corruptions d'index, comme illustré par ces deux discussions sur la liste pgsql-general en anglais: "Issues with german locale on CentOS 5,6,7", et "The dangers of streaming across versions of glibc: A cautionary tale".

En résumé, si Postgres parcourt un index avec une fonction de comparaison qui diffère de celle utilisée pour écrire cet index, il est possible que des valeurs présentes ne soient plus trouvées en lecture. Et en cas d'insertion, c'est pire puisque les nouvelles entrées risquent d'être insérées à des emplacements incohérents par rapport à la version précédente, et corrompre irrémédiablement l'index.

Ce problème de mise à jour des locales n'est donc pas nouveau, mais ce qui est particulier avec cette version 2.28 de la glibc, c'est l'importance de la mise à jour, qui est sans précédent dans la période récente. En effet depuis l'an 2000, d'après le bug#14095, les données des locales dans la glibc étaient modifiées au cas par cas. Cette fois-ci, il s'agit d'un rattrapage massif pour recoller au standard Unicode.

Pour tester un peu l'effet de ces changements, j'ai installé ArchLinux qui a déjà la glibc-2.28, avec PostgreSQL 10.5, et comparé les résultats de quelques requêtes avec ceux obtenus sous Debian 9 ("stretch"), qui est en glibc-2.24.

Je m'attendais bien à quelques changements, mais pas aussi étendus. Car il s'avère que des tests simples sur des chaînes avec uniquement des caractères ASCII de base montrent tout de suite des différences importantes.

Par exemple, avec la locale en_US.UTF-8:

Debian stretch (glibc 2.24)

=# select version();
                                                             version                                                              
----------------------------------------------------------------------------------------------------------------------------------
 PostgreSQL 10.5 (Debian 10.5-1.pgdg90+1) on x86_64-pc-linux-gnu, compiled by gcc (Debian 6.3.0-18+deb9u1) 6.3.0 20170516, 64-bit
(1 row)

=# show lc_collate ;
 lc_collate  
-------------
 en_US.UTF-8
(1 row)

=# SELECT * FROM (values ('a'), ('$a'), ('a$'), ('b'), ('$b'), ('b$'), ('A'), ('B'))
   AS l(x) ORDER BY x ;
 x  
----
 a
 $a
 a$
 A
 b
 $b
 b$
 B
(6 rows)

ArchLinux (glibc 2.28):

=# select version();
                                   version                                   
-----------------------------------------------------------------------------
 PostgreSQL 10.5 on x86_64-pc-linux-gnu, compiled by gcc (GCC) 8.2.0, 64-bit
(1 row)

=# show lc_collate;
 lc_collate  
-------------
 en_US.UTF-8
(1 row)

=# SELECT * FROM (values ('a'), ('$a'), ('a$'), ('b'), ('$b'), ('b$'), ('A'), ('B'))
   AS l(x) ORDER BY x ;
 x  
----
 $a
 $b
 a
 A
 a$
 b
 B
 b$
(6 rows)

Ces changements ne sont pas limités aux locales UTF-8. Les différences ci-dessus s'appliquent aussi à l'encodage LATIN9 avec lc_collate = 'fr_FR.iso885915@euro', par exemple.

Et voici une requête encore plus simple qui montre aussi des résultats de tri de chaînes différents entre versions:

Debian stretch (glibc 2.24)

=# SELECT * FROM (values ('"0102"'), ('0102')) AS x(x)
   ORDER BY x;
   x    
--------
 0102
 "0102"
(2 rows)

ArchLinux (glibc 2.28):

=# SELECT * FROM (values ('"0102"'), ('0102')) AS x(x)
   ORDER BY x;
   x    
--------
 "0102"
 0102
(2 rows)

J'ai pris l'habitude d'utiliser la requête ci-dessus pour illustrer les différences entre FreeBSD et Linux/glibc mais alors que la collation en_US dans FreeBSD 11 triait jusque-là ces chaînes à l'opposé de glibc, maintenant il s'avère que la nouvelle glibc donne un résultat identique aux locales et libc de FreeBSD…

Naturellement la plupart des utilisateurs ne changent pas de version de libc de leur propre initiative, mais dans le cadre d'une montée de version du système. Si Postgres est mis à jour au passage avec un dump/reload, les index seront recréés avec les nouvelles règles. Sinon un REINDEX global de toutes les bases devrait être envisagé, ou a minima des index concernés. A noter que pg_upgrade pour cette situation ne réindexe pas automatiquement, et ne signale pas non plus l'obligation de le faire.

A la date de ce billet, les seules distributions Linux ayant déjà la glibc-2.28 doivent être les "bleeding edge" comme ArchLinux. Pour Fedora c'est prévu au 30 octobre 2018; Debian a actuellement la 2.27-5 dans testing, et Ubuntu "cosmic" (18.10) a la 2.27-3.

Si vous êtes utilisateur de Postgres sous Linux, ne manquez pas de vérifier si vos bases sont concernées par ces mises à jour de locales, et si oui, regardez bien quand vos systèmes passent à la glibc 2.28 pour prévoir une phase de réindexation pour éviter tout risque de corruption de données!

Pour savoir quelles collations chaque base utilise par défaut:

 SELECT datname, datcollate FROM pg_database;

Pour savoir quelles collations sont plus spécifiquement utilisées dans les index (à faire tourner sur chaque base):

SELECT distinct collname FROM pg_collation JOIN
  (SELECT regexp_split_to_table(n::text,' ')::oid  AS o
    FROM (SELECT distinct indcollation AS n FROM pg_index) AS a) AS b on o=oid
 -- WHERE collprovider <> 'i'
;

Avec Postgres 10 ou plus récent, on peut décommenter la dernière ligne pour éviter les collations ICU, qui ne sont pas concernées par la mise à jour de la glibc. Les locales C et POSIX ne sont également pas concernées étant donné qu'elles comparent au niveau de l'octet, sans règle linguistique.