REX sur une migration de données d’une base PostgreSQL vers DynamoDB dans AWS
Cet article est un retour d’expérience sur une migration de données dans l’écosystème AWS entre une base relationnelle type Postgres et une base DynamoDB.
La justification de cette refonte vient des problèmes de performances constatés par notre client et nous même, mais avant d’aller plus loin, il est nécessaire de présenter le contexte historique et le cahier des charges initial tels que validés avec le client.
Cet article est aussi disponible en lecture sur medium.com
L’architecture version 1
- Notre client dépose dans un espace de stockage des fichiers CSV.
- Ces fichiers sont lus, transformés, compilés et sauvegardés en base de données.
- Une Api REST doit permettre d’interroger cette base selon des critères bien précis.
Nous avons décidé, pour des raisons historiques et de compétences internes, de déployer cette infrastructure sur AWS dont le schéma est le suivant :

Il est important de préciser plusieurs points sur cette première version de l’architecture :
- Api gateway est utilisé pour exposer une api de type REST.
- On utilise S3 comme espace de stockage.
- Une base Aurora Serverless v1 est utilisée (Postgres SQL) avec une volumétrie d’environ 200 000 lignes en production. Nous utilisions la fonctionnalité Data Api ce qui nous permettait de transmettre notre requête SQL depuis la lambda sans avoir à se soucier de la gestion du pool de connexion ou des identifiants de connexion.
- Des lambdas sont utilisées pour la lectures des fichiers déposés dans le bucket et l’insertion en base de données.
Mais pourquoi Aurora Serverless v1 ?
Ce choix à été motivé par les raisons suivantes:
- Relationnelle ? Historiquement chez RedFroggy, on a une forte compétence sur les bases relationnelles en comparaison aux bases NoSQL de type DynamoDB.
- Aurora ? Le besoin ici est d’avoir un accès lecture et écriture et donc le service managé d’AWS nous semblait tout indiqué.
- Serverless ? Ce choix est motivé par la charge, c’est à dire le nombre d’appels api, qui peut être plus forte à certains moments de la journée. La base serverless nous semblait donc tout indiquée puisque son avantage principal est d’adapter sa capacité en fonction de la volumétrie.
- v1 ? Au moment des faits, AWS vient tout juste de sortir la version 2 d’Aurora serverless et il nous semblait donc plus sûr de se baser sur une version plus “mature”.
Quel est le problème ?
Le problème principal est la dégradation des temps de réponses de l’api, au fil du temps, et ce, même sur des appels unitaires. Ces temps se dégradent exponentiellement avec la multiplication des critères passés via l’api et le nombre d’appels importants (pics de charges).
Les raisons sont multiples et peuvent être découpées en deux groupes : les causes internes et externes :
Causes internes:
- La requête SQL (qui en fait en cachait plusieurs) n’était pas optimisée et donc peu performante.
- Certaines transformations de données étaient faites à la volée alors qu’elles auraient pu l’être au moment de l’insertion des données en base depuis le bucket S3.
Causes externes:
- Bien qu’elle s’adaptait à la charge (puisque c’est son principe) les exécutions de requêtes via la base de données serverless (et data api) se sont avérées dans les faits très lentes.
- Le client a fait un usage surdimensionné de l’api en faisant des millions d’appels sur un temps très court.
Pour donner un ordre de grandeur, un seul appel api pouvait prendre jusqu’à 1 seconde, voir 4 à 5 secondes si on multipliait les critères par 2, et générait des erreurs 500 (car la requête était trop longue) pour un plus grand nombre de critères. Ce qui évidemment était problématique notamment en période de pics de charges.
Nous avons donc décidé de migrer vers DynamoDB

L’architecture version 2
L’architecture version 2 est très similaire à celle de la version 1 à l’exception de :
- La base de données qui est bien sûr DynamoDB
- Les données présentes dans les fichiers CSV déposés par le client dans le bucket S3 sont insérées en base et sont évidemment optimisées pour éviter un retraitement à la lecture :
- Extraction de données
- Formattage et conversion des dates au format international (ISO8601)
- Regroupement de données sur une seule et même ligne en base (déduplication)
- La reprise de données des fichiers CSV vers la nouvelle base DynamoDB ne s’est pas faite sans contraintes :
- Les capacités de lecture et d’écriture ont été revues à la hausse et l’auto scaling à été activé.
- Pour limiter les coûts, nous avions décider de traiter un fichier CSV toutes les 20 secondes pour éviter un scaling trop important des lambdas et éviter une surcharge côté DynamoDB
Pourquoi DynamoDB ?
Pour les raisons suivantes :
- Les données telles que présentes dans les fichiers déposés par le client dans le bucket S3 sont destructurées c’est à dire que le besoin de relation entre les données est faible et donc une base non relationnelle n’est pas un frein.
- Le principe de partitionnement (nous y reviendrons) sur lequel repose DynamoDB ainsi que ses spéficités technologiques (la donnée est présente physiquement sur des SSD) rend le requêtage extrêmement rapide.
- Il s’agit d’une base de données propriétaire AWS et donc très bien intégrée aux autres services AWS comme les lambdas.
- Pas de gestion de pool de connexion ni même d’identifiant de connexion à gérer.
- Son coût est très faible par rapport à une base serverless aurora.
- Mécanisme d’auto scaling qui permet à la bdd de s’adapter en temps réel à la charge.
Elligibilité
Malheureusement, décider d’utiliser DynamoDB n’est pas suffisant en soi car, de par son mécanisme de partitionnement et ses limitations de requêtage, il y a des règles à respecter qui peuvent rendre son utilisation complexe voir impossible.
Le partitionnement
La puissance de DynamoDB vient de son mécanisme de partitionnement qui lui permet de ranger les données sous une clé de partition avec une partition par valeur de la clé.
Prenons l’exemple suivant:

Dans l’exemple ci-dessus, la clé de partition est color dans la table vehicule et donc dynamodb va créer une partition pour chaque couleur de véhicule. On aura ainsi une partition avec tous les véhicules de couleur rouge, une autre avec les véhicules de couleur bleue, etc..
Il est possible également de définir une sort key (clé de tri) pour trier dans la partition les données pour un requêtage encore plus rapide.
Par exemple si on définit height comme sort key, alors dans la partition color=”red” tous les véhicules rouges seront triés par hauteur (ascendant ou descendant au choix)
Une table, des index
Dans DynamoDB on peut créer une table et des index. Ces derniers vont contenir une copie conforme des données de la table mais sur lesquels les partitions key seront potentiellement différentes. Cela nous permet d’avoir plusieurs copies des données partitionnées et triées de manières différentes ce qui est très pratique quand le requêtage est hétérogène en terme de critères.
Par exemple:
- Une table avec id comme partition key
- Un index avec color comme partition key et height comme sort key
- Un index avec type comme partition key
Ici les données son dupliquées 3 fois (1 table, 2 index) et pour requêter par type on ciblera de préférence le troisième index.
Quel intérêt ?
Imaginons que notre table vehicule contienne 1 million de lignes et que les partitions (regroupant les données par couleur donc) contiennent en moyenne 10 000 lignes chacune. Si lors du requêtage on cible une partition donnée (color=”red”) alors la requête se fera sur les 10 000 lignes de la partition et non plus sur les 1 million de la table principale. C’est ce qu’on appelle dans DynamoDb une requête de type Query (qui requête une partition) en opposition à une requête de type Scan (qui requête toute la table).
Voici une requête Query (car on utilise un critère strict sur la clé de partition color) :
1 |
SELECT * FROM vehicule WHERE color = "red" |
Et voici une requête Scan :
1 |
SELECT * FROM vehicule where height >= 200 |
Ici on ne passe pas par la partition key et donc toute la table sera lue et la requête sera forcément plus longue.
Fort heureusement on peut combiner les deux :
1 |
SELECT * FROM vehicule were color = "red" and height >= 200 |
Ici c’est ce qu’on appelle une requête Query et Scan car on filtre d’abord sur une partition puis on scanne la partition (et non la table entière).
Quelle clé de partitionnement ?
Il faut donc pour être efficace, requêter par clé de partition. Mais comment la choisir ? Dans notre cas, il y avait deux critères à prendre en compte:
- Le point d’entrée étant une API REST, c’est forcément elle qui détermine les critères de requêtage (les paramètres de l’api rest devaient être exactement les mêmes entre la v1 et la v2). Si on définit une clé de partition sur une colonne qui n’est pas un paramètre d’entrée de l’api toutes nos requêtes seront de type Scan.
- Trouver un groupe de partition le plus petit possible pour avoir les meilleures performances.
Pour le premier point, notre api a 3 paramètres d’entrée dont 2 avec des valeurs limitées (enum) et 1 avec des valeurs infinies (ex: quantité). Les deux premiers semblent donc tout indiqué.
Pour le second point, il nous a suffit d’interroger la base de données historique Postgres pour connaitre un ordre de grandeur du dimensionnement des partitions dans DynamoDB en utilisant comme requête :
1 |
SELECT COUNT(*) from vehicule GROUP BY color="red" |
Après avoir fait cette requête sur plusieurs valeurs possibles, on a eu une meilleure idée du volume des partitions et de la pertinence de la clé de partition.
Dans notre cas de figure un des deux paramètres de l’api a été identifié comme clé de partition et l’autre comme clé de tri. Nous avons donc créé un index à partir de ces deux informations.
Les partitions ont une taille entre 600 et 1000 lignes sur une table de 200 000 lignes ce qui est plus que correct.
A noter qu’il est tout à fait possible dans DynamoDB de créer une clé de partition sur un groupement de colonnes (ex: color et type) pour avoir des partitions encore plus petites en taille.
DynamoDB: des requêtes aux pouvoirs limités
Certaines limitations dans les reqûetes peuvent être déroutantes pour ceux qui viennent d’un écosystème SQL :
- Pas de tri (ou presque) : Comme évoqué plus haut DynamoDb permet de trier les données en utilisant la sort key et c’est tout !
- Pas de groupement possible : max, min, group by etc.. n’existent pas.
- Pas de transformation possible à la volée dans la requête (transformation de date, manipulation de strings, etc..).
- Pas de sous requêtes possibles.
- La partition key ne peut plus être modifiée a posteriori, il faut pour cela supprimer la table ou l’index (et les données qui vont avec) et la recréer avec la bonne clé de partition.
Ces fonctionnalités non supportées doivent forcément être implémentées dans le code. Il faut donc bien s’assurer que les résultats les plus pertinents soient retournés en premier par la requête ce qui n’est pas sans poser de problème.
Conclusion
Serverless et base relationelle ne font pas bon ménage
La gestion de la charge / volumétrie sur les bases relationnelles dans un écosystème serverless (lambdas) n’est pas sans causer de nombreux problèmes de performances et de coût. Il nous semble aussi évident aujourd’hui qu’ Aurora Serverless V1 n’est pas production ready (et ne le sera probablement jamais) car cette base semble destinée à un usage de développement uniquement.
L’aspect pratique offert par le service Data Api est effacé par les lenteurs qu’elle génère.
Avec toutes ces contraintes, le requêtage doit être le plus efficace possible pour éviter de rajouter de la lenteur inutilement.
DynamoDB n’est pas simple
Le choix de la clé de partition et clé de tri , la gestion des index et le requêtage très restrictif obligent à se poser les bonnes questions dès le départ et prend forcément plus de temps à mettre en place qu’une base de données “classique”.
DynamoDB est très rapide

Le graphique ci-dessus, qui détaille le temps d’exécution moyen de notre api REST en millisecondes, parle de lui même et montre bien le changement d’échelle depuis la bascule vers dynamodb puisqu’on est passé des secondes aux millisecondes. On a en moyenne divisé les temps d’exécutions par 10 avec un trafic http plus conséquent et réduit la facture AWS d’au moins 30% !