Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feature/duckdb: POC duck db node api #17

Open
wants to merge 2 commits into
base: main
Choose a base branch
from

Conversation

jbuiquan
Copy link

@jbuiquan jbuiquan commented Feb 6, 2025

Tâche - Tester intégration node avec duckdb

Dans cette PR, j'ai testé l'intégration de la librairie duckdb/duckdb-node-neo pour faire des requêts sur la base duckdb.

  • J'ai fait un exemple avec une requête simple en lecture sur la table sise_resultats. En commentaire j'ai aussi donné d'autres exemples de requêtes.
    Je ne suis pas allé dans des requêtes très complexes mais à priori duckdb a son propre dialect de requête qui ressemble fortement à PostgreSQL (source).
  • J'ai fait une page exemple en utilisant directement un composant serveur qui va requêter la base, il est accessible à l'url http://localhost:3000/duckdb-example. Elle affiche simplement des lignes de la table requếtée.
  • J'ai fait un exemple d'API route qui fait une requête en base accessible à l'url http://localhost:3000/api/db-example

Note :

  • A priori la librairie est bien typée car développée en typescript. Les résultats retournent les types de chaque colonne mais je n'ai pas réussi à trouver un moyen autre que de me baser sur ces types et de "caster" ensuite la valeur associée. Cela fonctionne correctement comme ça (j'ai mis quelques exemples commentés dans le composant qui affiche les résultats). J'attend une réponse éventuel des contributeurs de la lib pour m'éclairer sur un meilleur moyen si existant (là voilà).
    Il est possible de définir une fonction de conversion à appliquer en fonction des types (voir ce gist).
  • La librairie est encore jeune et le développement est en cours puisque la librairie est marqué en "alpha" et que des nouvelles features/corrections sont régulièrement poussées.
    => Pour notre usage cela ne posera peut être pas de problème ?
    => Peut être faudra-t-il faire une archi qui permettrait de switcher de provider si besoin ? (ajout d'une interface + impl) ?
    => Voir ancienne lib probablement plus stable (lib) qui a un wrapper typescript (lib)
  • Il est possible de lire une partie des résultats avec le reader mais aussi de "streamer" la donnée en chunk (je n'ai pas exploré cette piste).
  • Voici quelques formats de données mis à disposition par l'api, je ne sais pas si certaines formes seraient plus utiles que d'autres :
// retourne un ensemble de valeur pour chaque lignes 
const rows = reader.getRows();   
// [ [0, 10], [1, 11], [2, 12] ]

// retourne un ensemble de valeur pour chaque lignes - i / n sont les noms des colonnes du SELECT
const rowObjects = reader.getRowObjects();
// [ { i: 0, n: 10 }, { i: 1, n: 11 }, { i: 2, n: 12 } ]  

//retourne un ensemble des valeurs pour chaque colonne
const columns = reader.getColumns(); 
// [ [0, 1, 2], [10, 11, 12] ]

//retourne un ensemble des valeurs pour chaque colonne - i / n sont les noms des colonnes du SELECT
const columnsObject = reader.getColumnsObject();
// { i: [0, 1, 2], n: [10, 11, 12] }

On peut aussi récupérer un format json, mais dans ce cas certains types de données sont transformées en objets "riches" qui permettent de représenter la valeur.

@jbuiquan
Copy link
Author

jbuiquan commented Feb 10, 2025

Concernant le test de charge, j'ai utilisé l'outil Artillery que je ne connaissais pas.
Il permet de faire des tests de charge en fournissant une config déclarative des tests via un fichier yaml. J'ai utilisé l'exemple de base pour faire les tests (example de la doc).
Lors de ces tests l'outil créé des "virtual users" qui vont suivre les scénarios définis (faire des requêtes à l'api ici). Il y a une étape de "warm up" ou quelques utilisateurs sont crées et font des requêtes, puis une phase ou de plus en plus d'utilisateurs sont créés et enfin une phase ou le nombre de requêtes est à son maximum et reste constant.
L'outil fournit des metrics ainsi qu'un apdex :

Apdex is an open standard for turning response time measurements into simple scores that reflect user satisfaction with the service. With the apdex plugin Artillery can track and visualize those scores for you.

Fichier de test :

config:
  target: http://localhost:3000/api/db-example
  phases:
    - duration: 60
      arrivalRate: 1
      rampTo: 5
      name: Warm up phase
    - duration: 60
      arrivalRate: 5
      rampTo: 10
      name: Ramp up load
    - duration: 30
      arrivalRate: 10
      rampTo: 30
      name: Spike phase
  # Load & configure a couple of useful plugins
  # https://docs.art/reference/extensions
  plugins:
    ensure: {}
    apdex: {}
    metrics-by-endpoint: {}
  apdex:
    threshold: 100
  ensure:
    thresholds:
      - http.response_time.p99: 100
      - http.response_time.p95: 75
scenarios:
  - flow:
      - loop:
          - get:
              url: "/"
        count: 100

Commande : ARTILLERY_DISABLE_TELEMETRY=true artillery run duckdb-load-test.yml --output results.json

J'ai fait plusieurs cas de tests, j'ai rajouté un limit sur les requêtes car sinon ça ne tenait pas la charge :

  1. requête SELECT avec limit=5000 sur la table sise_resultats : results-5000.json
  2. requête SELECT avec limit=100 sur la table sise_resultats : results-100.json
  3. requête SELECT avec limit=1 sur la table sise_resultats : results-1.json
  4. requête SELECT + prepared statement avec limit=100 sur la table sise_resultats : results-prepared-statement-100.json

Analyse des résultats :

  • Sur les résultats, on obtient des erreurs de créations des virtuals users "vusers.failed": 246, et je ne sais pas si c'est mon pc qui n'arrive pas à créer les virtuals users où si c'est parce que l'api pose problème.
  • Pour l'exemple des prepared statement avec limit 100, voici les résultats agrégés :
"aggregate": {
    "counters": {
      "vusers.created_by_name.0": 1230,
      "vusers.created": 1230,
      "http.requests": 212369,
      "http.responses": 212200,
      "http.codes.200": 106100,
      "apdex.satisfied": 44135,
      "apdex.tolerated": 28736,
      "apdex.frustrated": 33229,
      "plugins.metrics-by-endpoint./api/db-example/.codes.200": 106100,
      "vusers.failed": 169,
      "vusers.completed": 1061,
      "plugins.metrics-by-endpoint./api/db-example/.errors.ETIMEDOUT": 169,
      "errors.ETIMEDOUT": 169
    },
    "rates": {
      "http.request_rate": 1576  // req/s
    },
    "period": 1739100460000,
    "summaries": {
      "http.response_time": {
        "min": 0,
        "max": 9948,    //ms
        "count": 212200,
        "mean": 155.1,
      },
   }
}
  • sur ce meme exemple, on a sur une metrique intermédiaire (juste avant de voir des vusers failed) :
{
      "counters": {
       //...
        "http.responses": 12790,
        "http.requests": 13030,
        "http.codes.200": 6335,
        "apdex.satisfied": 0,
        "apdex.tolerated": 366,
        "apdex.frustrated": 5969,
        "plugins.metrics-by-endpoint./api/db-example/.codes.200": 6335,
        "vusers.created_by_name.0": 268,
        "vusers.created": 268,
        "vusers.failed": 0,
        "vusers.completed": 24
      },
      "rates": {
        "http.request_rate": 1311   //1311 requete/s
      }, 
      //...
      "summaries": {
        "http.response_time": {
          "min": 52,  
          "max": 7034,   //7 sec max, est ce représentatif ?
           "count": 12790,
          "mean": 352.8,   // 352ms en moyenne
          //...
        },
        //...
}

En comparaison, encore un peu avant, on a de meilleures metrics lorsqu'il y a moins de vusers :

{
      "counters": {
        "http.codes.308": 6475,
        "http.responses": 12902,
        "http.requests": 12954,
        "http.codes.200": 6427,
        "apdex.satisfied": 29,
        "apdex.tolerated": 6398,
        "apdex.frustrated": 0,
        "plugins.metrics-by-endpoint./api/db-example/.codes.200": 6427,
        "vusers.failed": 0,
        "vusers.completed": 43,
        "vusers.created_by_name.0": 95,
        "vusers.created": 95
      },
      "rates": {
        "http.request_rate": 1310
      },
      "summaries": {
        "http.response_time": {
          "min": 14,
          "max": 309,
          "count": 12902,
          "mean": 98.3,
        },
       }
 }

Je ne pense pas qu'on devra tenir une charge aussi grande mais cela me semble acceptable.

A suivre :

  • J'aimerai bien comprendre pourquoi les vusers sont en "failed".
  • Je ne sais pas si ce test rapporte des résultats acceptables.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants