Contexte et expérimentation
L’auteur a reproduit le problème sur une machine Hetzner équipée du noyau Linux 6.8 avec MGLRU activé. Deux processus étaient placés dans le même cgroup : un programme Go qui lit un flux, désérialise un protobuf et crée une structure de graphe, et un serveur HTTP peu actif. En condition de pression mémoire, le noyau a commencé à évincer des pages vers le dispositif de swap. Les mesures montrent une pause médiane de 51 µs, mais un pic de 40 ms lorsqu’une partie du métadonnées du GC était stockée sur le NVMe et a dû être relue depuis le swap.
Mécanisme du GC et interaction avec le swap
Le ramasse‑miettes de Go effectue deux arrêts du monde : la terminaison du balayage (sweep) et la terminaison du marquage (mark). Pendant ces phases, le runtime lit des structures de métadonnées situées en dehors du tas, dans des pages qui ne sont jamais libérées mais réutilisées. Si le noyau évince ces pages, chaque accès déclenche un défaut de page majeur. Le script BPF développé par l’auteur a compté 228 défauts de page pendant la pause la plus longue, totalisant 39 ms sur 40 ms de pause.
0x42e5c8 runtime.(*spanSet).reset /usr/local/go/src/internal/runtime/atomic/types.go:194
0x4219de runtime.finishsweep_m /usr/local/go/src/runtime/mcentral.go:71
0x4629cf runtime.gcStart.func2 /usr/local/go/src/runtime/mgc.go:724
0x46dd8a runtime.systemstack /usr/local/go/src/runtime/asm_amd64.s:518
0x4169dc runtime.gcStart /usr/local/go/src/runtime/mgc.go:722
0x4276a4 runtime.nextMarkBitArenaEpoch /usr/local/go/src/runtime/mheap.go:2481
0x421a65 runtime.finishsweep_m /usr/local/go/src/runtime/mgcsweep.go:268
0x4629cf runtime.gcStart.func2 /usr/local/go/src/runtime/mgc.go:724
0x46dd8a runtime.systemstack /usr/local/go/src/runtime/asm_amd64.s:518
0x4169dc runtime.gcStart /usr/local/go/src/runtime/mgc.go:722Ces adresses montrent que les défauts surviennent dans les fonctions de réinitialisation du jeu de spans et de finalisation du balayage, exactement là où le GC parcourt ses métadonnées.
Analyse des coûts de pause
Sur 30 minutes d’observation, 312 pauses similaires ont été enregistrées, soit une fréquence d’environ une pause toutes les 5,8 secondes. Chaque défaut de page implique la lecture d’une entrée de table de pages (PTE), l’appel à do_swap_page, l’allocation d’un nouveau cadre, la soumission d’une I/O bloc et l’attente du disque. Le temps total de 39 ms représente 99 % du temps d’arrêt, le reste étant le traitement interne du GC. En comparaison, la construction d’un message de 511 KiB, normalement réalisée en 3‑5 ms, a atteint 105 ms sur le NVMe et 903 ms sur un volume réseau, indiquant un coût supplémentaire localisé pour le goroutine qui alloue.
Implications pour la production
Une pause de 40 ms bloque l’ensemble des goroutines ; les opérations d’I/O en attente ne sont pas traitées, ce qui peut entraîner des délais de réponse importants. Le phénomène apparaît surtout lors de pics de consommation mémoire, lorsque le swap est sollicité. La version Go 1.26 avec le garbage collector « Green Tea » n’a pas modifié la façon dont les métadonnées sont lues, l’impact reste donc comparable. Les équipes doivent surveiller l’utilisation du swap au niveau du cgroup, envisager de réserver de la mémoire pour les pages du runtime ou désactiver le swap pour les workloads Go critiques afin de limiter les défauts de page pendant les arrêts du monde.