Contexte technique
Turso propose deux moteurs d’accès aux fichiers : un qui utilise pread(2) (mode syscall) et un qui s’appuie sur io_uring. Dans ce dernier, le fichier de base de données est ouvert avec O_DIRECT, ce qui désactive le readahead du noyau. Le benchmark TPC‑H, exécuté sur une base de 1,2 GiB, mesure le temps d’exécution de la requête Q6, qui effectue un scan complet de la table lineitem.
Deux configurations sont comparées : off (PRAGMA prefetch_pages=0, aucune logique de readahead) et on (fenêtre de 32 pages pré‑chargées). Les mesures sont prises avec iostat, perf stat et les tracepoints io_uring:io_uring_submit_req.
Fonctionnement sans readahead
En l’absence de readahead, Turso soumet un seul SQE (Submission Queue Entry) à la fois. Chaque lecture attend la complétion de la précédente, ce qui empêche toute forme de parallélisme. Le compteur rareq‑sz montre que chaque requête atteint le disque séparément, et le taux de fusion %rrqm reste très faible : seulement 140 fusions sur 195 516 bios, soit 0,07 %.
Cette séquence monothread entraîne un temps d’exécution de 8,55 s pour io_uring, contre 3,02 s pour le backend syscall. Le nombre d’instructions exécutées augmente de 0,2 milliard, mais le nombre de défauts de cache monte de 4,8 millions, indiquant que le CPU doit récupérer les données directement depuis la RAM sans bénéfice du cache L1/L2/L3.
Impact du readahead appliqué en application
Lorsque Turso détecte un accès séquentiel, il soumet simultanément 32 SQE couvrant les pages 100 à 131. Cette profondeur de file d’attente permet au bloc‑layer du noyau de fusionner les requêtes adjacentes : 202 539 fusions sur 218 493 bios, soit 92,7 % de merges, et le disque ne reçoit que 15 951 requêtes. Le nombre total de SQE augmente de 23 005, reflétant le surplus de pages pré‑chargées.
Le temps d’exécution passe à 8,22 s de mur, mais le thread de sqpoll consomme 8,46 s de temps système, supérieur au temps réel, preuve que le thread de polling tourne en parallèle avec le thread de requête. En désactivant le polling (construction IoUring::new), le temps système chute à 1,27 s, tandis que le temps mur augmente légèrement à 8,62 s, confirmant que le gain de latence provient surtout de la réduction du CPU partagé.
Analyse des coûts et limites
Le modèle io_uring avec O_DIRECT et sans readahead souffre d’une profondeur de 1, ce qui empêche la fusion de requêtes et entraîne le pire des temps. Le polling reste pertinent uniquement sur des machines disposant de cœurs excédentaires ; sinon il concurrence les threads applicatifs et augmente le temps système, comme le montre l’étude SYSTOR 2022 (13 KIOPS sur un seul cœur).
Le backend syscall bénéficie du cache grâce à la copie du page‑cache vers le buffer utilisateur, ce qui réduit les défauts de cache mais impose une surcharge de copie. En revanche, O_DIRECT évite la copie mais génère davantage de miss de cache, ce qui explique la différence de 4,8 M de miss entre les deux modèles.
En résumé, le readahead implémenté au niveau de l’application augmente la concurrence, permet la fusion de requêtes au niveau du bloc‑layer et compense partiellement le manque de chaleur du cache introduit par O_DIRECT. Le choix entre polling et soumission batchée dépend du nombre de vCPU disponibles et du profil CPU de l’application.