FlashProxy Logo

FlashProxy

ProxiesTechnologieActualitésTutorielsGuidesAnnonce

3× plus de connexions. Même cœur. Au cœur de la recherche de performance derrière le moteur d'acceptation de FlashProxy

3× plus de connexions. Même cœur. Au cœur de la recherche de performance derrière le moteur d'acceptation de FlashProxy

Comment FlashProxy a utilisé un optimiseur IA autonome pour concevoir un moteur d'acceptation TCP qui consomme 3× moins d'instructions CPU que Go. Open source et sous licence MIT.

É
Équipe FlashProxy
21 juin 2026
11 min de lecture

Il existe une catégorie de problèmes de performance qui ne se manifeste qu'à grande échelle, et c'est profondément déstabilisant quand cela arrive. Le goulot d'étranglement ne vient pas de votre logique de routage. Ni du chiffrement. Ni de quoi que ce soit que votre application est censée faire. Il vient de l'infrastructure qui l'entoure. De la plomberie.

C'est exactement ce qui nous est arrivé.

Aux alentours de 40 000 connexions par seconde, notre proxy Go en production a commencé à atteindre les limites CPU dans le chemin d'acceptation. Le coupable était le modèle standard de Go pour les services réseau : une goroutine par connexion, un appel système par opération. Chaque connexion de courte durée (un health check, une sonde d'équilibreur de charge, une petite redirection) touche le noyau quatre fois. accept, read, write, close. À fort taux de renouvellement des connexions, ces quatre allers-retours cessent d'être une surcharge pour devenir le coût total. Le code applicatif ne coûte presque rien. Faire entrer et sortir des octets du noyau, si.

La solution vers laquelle tout le monde se tourne, c'est io_uring, l'interface d'E/S asynchrone de Linux qui regroupe les opérations noyau en lots et réduit considérablement le nombre de franchissements de la frontière utilisateur/noyau. Nous le savions. La question plus difficile était de savoir comment l'affiner.

Pourquoi nous ne l'avons pas simplement ajusté nous-mêmes

io_uring dispose d'une longue liste d'optimisations documentées : multishot accept, descripteurs de fichiers enregistrés, DEFER_TASKRUN, chaînes de complétion, coalescence MSG_MORE. Le problème, c'est que ces techniques interagissent entre elles de manières genuinement difficiles à prévoir. Certaines combinaisons se renforcent mutuellement. D'autres s'annulent. Certaines régressions sont totalement invisibles tant qu'on ne fait pas de benchmark sous charge réaliste sur du matériel réel.

Un développeur assis devant perf stat peut tester une poignée de combinaisons en une journée. Mais l'espace de recherche réel — couvrant les combinaisons, les ordres, les valeurs de paramètres et les interactions avec les différentes versions du noyau — est bien plus vaste. Plus important encore, l'intuition humaine est un handicap ici. Nous avons tendance à nous accrocher à des théories qui semblent justes sur le papier, même quand les données disent le contraire.

Nous avons donc construit une boucle de mesure fermée et confié le problème de recherche à un agent IA.

Comment la boucle de recherche fonctionnait

Nous avons mis en place deux implémentations serveur fonctionnant en parallèle. Le contrôle était un serveur Go modélisant exactement le chemin d'acceptation de notre proxy en production : une goroutine par connexion, répartition SO_REUSEPORT. Il était figé pour toute la durée de l'expérience. Le traitement était un serveur C utilisant liburing, partant d'une boucle d'acceptation io_uring à acceptation simple basique. C'était le seul code qui pouvait être modifié.

Les deux serveurs respectaient le même contrat : accepter une connexion, lire les octets de la requête, écrire une réponse HTTP 200 fixe, fermer. Même matériel, même noyau, même charge. La seule variable était la façon dont ils le géraient.

Claude Code tournait en mode sans interface graphique en tant qu'optimiseur. À chaque itération, il lisait le champion actuel, l'historique complet des mutations précédentes stockées sous forme de commits git, une base de connaissances de leçons accumulées au fil des exécutions, et une base de données de données de profilage. Il formulait une hypothèse unique et effectuait sa modification. Puis il passait entièrement la main.

Un harnais bash séparé, que l'IA ne pouvait pas toucher, compilait la mutation, la fixait sur un cœur CPU isolé, lançait le générateur de charge et notait le résultat. La formule de notation était :

score = 1 000 000 000 / moyenne(instructions par connexion)

Les comptages d'instructions CPU sont une mesure matérielle exacte. Contrairement aux chiffres de débit, ils sont insensibles à la limitation thermique et aux variations de fréquence d'horloge. Ils indiquent précisément la quantité de travail effectuée par le CPU par connexion, ce qui détermine réellement la capacité à grande échelle.

Si une mutation améliorait le score de plus de 3 % par rapport au champion actuel, elle était promue. Sinon, elle était supprimée avec git reset --hard et la boucle continuait. L'IA rédigeait les hypothèses. Le harnais prenait chaque décision de conservation ou d'annulation. Ni l'un ni l'autre n'interférait dans le travail de l'autre.

Pour éviter que l'optimiseur ne contourne le benchmark, chaque exécution notée était validée : les octets de réponse devaient être exactement corrects, les connexions devaient se terminer de bout en bout, et le taux d'échec devait rester sous 0,01 %. Toute violation donnait un score de zéro.

Ce que nous avons découvert

L'optimiseur a tourné pendant deux jours et a convergé vers six modifications qui, ensemble, expliquent l'intégralité de l'écart de performance.

  • Les indicateurs de ring DEFER_TASKRUN et SINGLE_ISSUER. Ils déplacent le travail de complétion dans la boucle d'événements propre au thread de travail, éliminant ainsi les réveils inter-CPU. Ce fut le bond le plus important de toute l'exécution : une réduction directe du coût CPU noyau par opération, et non un artifice de débit.

  • Les descripteurs de fichiers enregistrés. Avec les descripteurs directs, les connexions acceptées résident dans la table propre au ring plutôt que dans la table des descripteurs de fichiers du processus. Cela évite l'installation dans la table fd lors de l'acceptation et la recherche à chaque opération suivante. L'optimiseur a testé plusieurs tailles de table et a déterminé que 4 096 entrées était la configuration optimale.

  • Le multishot accept. Au lieu de réarmer l'opération d'acceptation après chaque connexion, on l'arme une seule fois et le noyau publie automatiquement une complétion pour chaque nouvelle connexion. Cela a réduit les appels io_uring_enter par connexion à 0,34.

  • La liste libre de connexions par worker. La pré-allocation de 128 objets de connexion par worker a entièrement éliminé les appels malloc sur le chemin critique. La part mesurée des instructions CPU allouées à libc est passée de 1,32 % à 0,92 %.

  • La fusion de la réponse MSG_MORE et du FIN. Envoyer la réponse avec MSG_MORE la maintient dans la file d'écriture TCP afin que le FIN de fermeture de connexion puisse y être annexé, expédiant les deux dans un seul segment TCP. Un seul déclenchement NIC au lieu de deux. L'optimiseur a trouvé cela en remarquant qu'une fonction d'écriture noyau de bas niveau consommait deux fois sa part attendue d'instructions, puis en remontant jusqu'au fractionnement inutile de segments.

  • Les complétions groupées avec CQE_SKIP_SUCCESS. Étiqueter les opérations d'envoi et de fermeture pour qu'elles ne génèrent pas d'événements de complétion en cas de succès signifie que seules l'acceptation et la réception produisent des complétions — environ deux par connexion au lieu de quatre. Un seul appel submit-and-wait pilote de nombreuses connexions simultanément.

L'échec qui a le plus appris

À un moment, l'optimiseur a essayé d'enchaîner les opérations de réception, d'envoi et de fermeture — une technique qui fait se déclencher chaque opération automatiquement lorsque la précédente se termine. L'objectif était de réduire les allers-retours noyau, et cela a fonctionné : les appels enter par connexion sont passés de 1,90 à 1,40.

Le score a chuté de 34 %. Annulé immédiatement.

La leçon inscrite dans la base de connaissances : minimiser les entrées noyau n'est pas le bon levier. La sérialisation par chaînage coûte plus cher que les entrées qu'elle économise.

C'est exactement le genre de résultat qui brise l'intuition humaine. La métrique qui semblait être le goulot d'étranglement n'était pas le vrai goulot d'étranglement. Un ingénieur aurait probablement défendu cette optimisation plus longtemps. La boucle l'a mesurée, rejetée, et est passée à la suite.

Où la recherche s'est arrêtée

Après les six gains, l'optimiseur a exploré une douzaine de candidats supplémentaires environ. Tous ont été annulés, non pas parce qu'ils provoquaient des régressions, mais parce que le bruit de mesure dépassait le seuil de promotion de 3 %. Il ne restait rien à trouver.

Avec la configuration championne, environ 94 % du CPU restant appartient à la pile TCP du noyau Linux. Environ 1 % correspond au code applicatif. Environ 4 % correspond aux mécanismes internes de io_uring. Il ne reste aucun code en espace utilisateur à optimiser de manière significative. La recherche a correctement identifié le plancher et s'y est arrêtée.

Les résultats

Sur un seul cœur fixé, limité par le CPU, avec 512 connexions en vol simultané sur loopback :

  • Goroutine Go par connexion : 83 250 instructions par connexion

  • Référence de départ io_uring brute : 59 931 instructions par connexion

  • Le moteur d'acceptation de FlashProxy : 27 363 instructions par connexion

Soit 3,04× moins d'instructions CPU par connexion que Go, et 2,19× moins que la référence io_uring. Sur un seul cœur saturé, cela se traduit par environ six fois le débit de connexion par rapport au modèle goroutine.

Ce sont des benchmarks sur loopback conçus pour isoler proprement le coût CPU. Ce sont les ratios qui importent, pas les valeurs absolues. Les techniques sous-jacentes (multishot accept, DEFER_TASKRUN, descripteurs enregistrés) sont documentées dans les pratiques courantes d'io_uring. Ce que la recherche a produit, c'est la preuve de quelles combinaisons fonctionnent réellement ensemble — et lesquelles semblent bonnes sur le papier mais vous coûtent en pratique.

La bibliothèque open source

La conception gagnante est désormais flashaccept, une bibliothèque C open source qui regroupe les six optimisations derrière une API simple. Vous lui fournissez un port et un gestionnaire de requêtes. Elle exécute automatiquement une boucle d'acceptation io_uring optimisée par cœur.

Elle nécessite Linux et liburing 2.3 ou supérieur. Sur les noyaux plus anciens, elle se dégrade gracieusement. Le cas d'usage visé est celui des connexions de courte durée à fort renouvellement : health checks, redirecteurs, sondes d'équilibreur de charge, petites réponses RPC. Le dispositif de recherche complet — incluant la référence, le harnais, la configuration de l'optimiseur et la base de connaissances accumulée — est inclus et entièrement reproductible.

Sous licence MIT. Disponible dès maintenant sur github.com/thealonlevi/flashaccept.

Cette recherche est née directement de la construction de l'infrastructure proxy de FlashProxy. Si vous exploitez un service Linux à fort renouvellement de connexions et que le chemin d'acceptation est votre goulot d'étranglement, nous avons construit ceci exactement pour ce problème. Si vous souhaitez l'étendre ou exécuter le dispositif de recherche vous-même, le dépôt contient tout ce dont vous avez besoin.

FAQ

Qu'est-ce que Flashaccept ?

Flashaccept est une bibliothèque C open source développée par FlashProxy qui fournit un moteur d'acceptation TCP haute performance pour Linux. Elle utilise io_uring en coulisses et accepte des connexions pour 3,04× moins d'instructions CPU qu'un serveur Go standard à goroutine par connexion. Elle est sous licence MIT et disponible sur GitHub.

Pour quels types de charge est-elle conçue ?

Flashaccept est conçue pour les connexions de courte durée à fort renouvellement, là où le cycle requête-réponse-fermeture se produit à haut volume : endpoints de health check, sondes d'équilibreur de charge, redirecteurs HTTP et petites réponses RPC. Elle n'est pas conçue pour les connexions keep-alive ni les sessions multi-échanges dans la version 1.

De quelle version de Linux et de liburing ai-je besoin ?

Vous avez besoin de Linux avec liburing version 2.3 ou supérieure, inclus dans Ubuntu 24.04 et les versions suivantes. Le chemin rapide (multishot accept, descripteurs directs) nécessite le noyau 5.19 ou supérieur. Sur les noyaux plus anciens, la bibliothèque se replie gracieusement sur l'acceptation simple et les descripteurs de fichiers classiques.

Qu'est-ce que io_uring et pourquoi est-ce important pour les performances d'un proxy ?

io_uring est une interface du noyau Linux introduite dans la version 5.1, qui permet aux applications de soumettre et de recevoir des opérations d'E/S de manière asynchrone via des tampons en anneau en mémoire partagée, réduisant considérablement le nombre d'appels système nécessaires. Pour une infrastructure proxy gérant des dizaines de milliers de connexions de courte durée par seconde, le coût du franchissement répété de la frontière utilisateur/noyau devient la dépense CPU dominante. io_uring réduit considérablement ce coût.

Flashaccept est-il ce que FlashProxy utilise en production ?

Cette recherche est née directement de nos travaux de mise à l'échelle en production. L'infrastructure proxy de FlashProxy opère dans plus de 190 pays et gère un volume de connexions considérable. L'optimisation du chemin d'acceptation était une exigence d'ingénierie réelle, pas un exercice de recherche. flashaccept est le résultat distillé de ce travail, rendu open source pour que d'autres puissent en bénéficier.

Puis-je utiliser flashaccept dès aujourd'hui ?

Oui. Il s'agit de la version 1.0.1, sous licence MIT, disponible via GitHub, vcpkg, Conan et l'Arch AUR. La bibliothèque a été testée sous AddressSanitizer et UBSan sur plus de 400 000 connexions couvrant les quatre chemins de configuration.

Où puis-je en apprendre davantage sur l'infrastructure de FlashProxy ?

Vous pouvez en savoir plus sur la façon dont nous construisons et faisons évoluer notre infrastructure proxy sur le blog FlashProxy ou explorer directement notre réseau de proxies.


flashacceptFlashProxyserveur proxy d'amélioration des performancesoptimisation serveuramélioration du serveur proxy