Au cours de notre semaine anniversaire 2020, nous avons lancé la prise en charge de gRPC®. L'immense intérêt suscité par la version bêta nous a touchés, et nous tenons à remercier tous ceux qui ont postulé et testé gRPC ! Dans ce post, nous allons examiner les détails techniques de notre mise en œuvre du support.

Qu'est-ce que gRPC ?

gRPC est un framework RPC open source exécuté sur HTTP/2. RPC (Remote Procedure Call) permet à une machine de dire à une autre machine d'effectuer une action, plutôt que d'appeler une fonction locale dans une bibliothèque. RPC existe depuis longtemps dans l'histoire de l'informatique distribuée, avec différentes implémentations se concentrant sur différents domaines. Unique en son genre, gRPC présente les caractéristiques suivantes :

  • Il nécessite le protocole moderne HTTP/2 pour le transport, qui est maintenant largement disponible.
  • Des suites complètes d'implémentation, de démonstration et de test de référence client/serveur sont disponibles en open source.
  • Il ne spécifie pas de format de message, bien que les Protocol Buffers constituent le mécanisme de sérialisation privilégié.
  • Les clients et serveurs peuvent transmettre des données en continu, ce qui évite d'avoir à demander de nouvelles données ou à créer d'autres connexions.

En ce qui concerne le protocole, gRPC utilise des trames HTTP/2 de manière extensive : les requêtes et réponses ressemblent beaucoup à une requête HTTP/2 normale.

gRPC utilise cependant le trailer HTTP, ce qui est inhabituel. Bien qu'il ne soit pas largement utilisé, le trailer HTTP existe depuis 1999, selon la définition du RFC 2616 HTTP/1.1 d'origine. Les en-têtes de message HTTP sont définis pour venir avant le corps du message HTTP, mais le trailer HTTP est un ensemble d'en-têtes HTTP qui peut être ajouté après le corps du message. Cependant, comme les cas d'utilisation de trailers sont rares, de nombreuses implémentations de serveurs et de clients ne les prennent pas entièrement en charge. Alors que HTTP/1.1 doit utiliser un codage de transfert par blocs pour que son corps envoie un trailer HTTP, dans le cas de HTTP/2, le trailer se trouve dans la trame HEADER après la trame DATA du corps.

Il arrive qu'un trailer HTTP soit utile. Par exemple, nous utilisons un code de réponse HTTP pour indiquer le statut d'une requête, mais le code de réponse est la toute première ligne de la réponse HTTP. Nous devons donc décider du code de réponse très tôt. Un trailer permet d'envoyer des métadonnées après le corps. Par exemple, imaginons que votre serveur web envoie un flux de données volumineuses (dont la taille n'est pas fixe), et qu'au final vous vouliez envoyer une somme de contrôle SHA256 des données envoyées afin que le client puisse en vérifier le contenu. Normalement, cela n'est pas possible avec un code de statut HTTP ou l'en-tête de réponse à envoyer au début de la réponse. En utilisant un en-tête trailer HTTP, vous pouvez envoyer un autre en-tête (par exemple, Digest) après avoir envoyé toutes les données.

Le protocole gRPC utilise les trailers HTTP pour deux raisons. La première est d'envoyer son statut final (grpc-status) sous la forme d'un en-tête trailer après l'envoi du contenu. La deuxième est de prendre en charge les cas de diffusion en continu. Ces cas durent beaucoup plus longtemps que les requêtes HTTP classiques. Le trailer HTTP permet de donner le résultat du post-traitement de la requête ou de la réponse. Par exemple, en cas d'erreur lors du traitement des données en continu, vous pouvez envoyer un code d'erreur à l'aide du trailer, ce qui n'est pas possible avec l'en-tête placé avant le corps du message.

Voici un exemple simple de requête et de réponse gRPC dans des trames HTTP/2 :

Ajout de la prise en charge de gRPC à Cloudflare Edge

Comme le protocole gRPC utilise HTTP/2, il peut sembler facile de prendre nativement en charge gRPC, car Cloudflare est déjà compatible avec HTTP/2. Cependant, nous nous sommes heurtés à quelques difficultés :

  • Les en-têtes trailer de requête/réponse HTTP n'ont pas été parfaitement pris en charge par notre proxy en périphérie : Cloudflare utilise NGINX pour accepter le trafic provenant des visiteurs, et NGINX n'assure qu'une prise en charge limitée des en-têtes trailer. Pour compliquer encore les choses, les requêtes et réponses envoyées via Cloudflare passent par un ensemble d'autres proxys.
  • HTTP/2 vers serveur origine : notre proxy en périphérie utilise HTTP/1.1 pour récupérer des objets (qu'ils soient dynamiques ou statiques) depuis un serveur d'origine. Pour transmettre le trafic gRPC par proxy, nous avons besoin de prendre en charge les connexions aux serveurs d'origine gRPC des clients via HTTP/2.
  • La diffusion en continu gRPC doit permettre le flux bidirectionnel de requêtes/réponses : gRPC a deux types de flux de protocole ; l'un est unaire (simple requête et réponse), l'autre est la diffusion en continu (qui permet un flux de données ininterrompu dans chaque direction). Pour une compatibilité totale avec la diffusion en continu, le corps du message HTTP doit être envoyé après avoir la réception de l'en-tête de réponse de l'autre extrémité. Par exemple, la diffusion en continu du client continuera d'envoyer un corps de requête après avoir reçu un en-tête de réponse.

C'est pour cela que les requêtes gRPC seraient interrompues lors de leur traitement par proxy via notre réseau. Pour surmonter ces limites, nous avons étudié différentes solutions. Par exemple, NGINX a un module gRPC en amont pour prendre en charge l'origine gRPC HTTP/2, mais il s'agit d'un module distinct qui, de plus, nécessite HTTP/2 en aval, fonction qui ne peut pas être utilisée pour notre service puisque les requêtes passent parfois en cascade par plusieurs proxys HTTP. L'utilisation de HTTP/2 partout dans le pipeline n'est pas réaliste, en raison des caractéristiques de notre architecture interne d'équilibrage de charge, et parce qu'il faudrait trop d'efforts pour s'assurer que tout le trafic interne utilise HTTP/2.

Convertir au format HTTP/1.1 ?

Finalement, nous avons découvert un meilleur moyen : convertir les messages gRPC en messages HTTP/1.1 sans trailer au sein de notre réseau, puis les reconvertir en HTTP/2 avant d'envoyer la demande au serveur d'origine. Cela fonctionnerait avec la plupart des proxys HTTP à l'intérieur du réseau Cloudflare qui ne prennent pas en charge les trailer HTTP, et nous n'aurions besoin que de modifications minimes.

Plutôt que d'inventer notre propre format, la communauté gRPC a déjà créé une version compatible HTTP/1.1 : gRPC-web. gRPC-web est une modification de la spécification gRPC d'origine basée sur HTTP/2. L'objectif initial était une utilisation avec les navigateurs web, qui n'ont pas de trames HTTP/2 à accès direct. Avec gRPC-web, le trailer HTTP est déplacé vers le corps. Nous n'avons donc pas à nous soucier de la compatibilité du trailer HTTP avec le proxy. À cela s'ajoute la prise en charge de la diffusion en continu. Le message HTTP/1.1 qui en résulte peut toujours être inspecté par nos produits de sécurité, tels que le WAF et la gestion des bots, pour fournir le même niveau de sécurité que Cloudflare apporte à d'autres flux de traffic HTTP.

Lorsqu'un message gRPC HTTP/2 est reçu sur le proxy de périphérie de Cloudflare, le message est « converti » au format gRPC-web HTTP/1.1. Une fois le message gRPC converti, il passe par notre pipeline, en appliquant différents services (WAF, Cache, Argo, etc.) de la même manière qu'une requête HTTP classique.

Juste avant qu'un message web gRPC ne quitte le réseau Cloudflare, il doit être « reconverti » en gRPC HTTP/2. Les requêtes qui sont converties par notre système sont marquées de manière à ce que notre système ne convertisse pas accidentellement le trafic gRPC-web provenant de clients.

Prise en charge des serveurs d'origine en HTTP/2

L'un des défis techniques consistait à prendre en charge HTTP/2 pour se connecter aux serveurs d'origine. Avant ce projet, Cloudflare n'était pas en mesure de se connecter aux serveurs d'origine via HTTP/2.

C'est pourquoi nous avons décidé de mettre en place une compatibilité interne avec les serveurs d'origine HTTP/2. Nous avons créé un proxy d'origine autonome capable de se connecter aux serveurs d'origine via HTTP/2. En plus de cette nouvelle plateforme, nous avons implémenté la logique de conversion pour gRPC. La compatibilité avec gRPC est la première fonctionnalité qui tire parti de cette nouvelle plateforme. La compatibilité étendue avec les connexions HTTP/2 aux serveurs d'origine fait partie de notre feuille de route.

Prise en charge de la diffusion en continu gRPC

Comme nous l'avons expliqué précédemment, gRPC dispose d'un mode de diffusion en continu qui permet d'envoyer le corps de la requête ou celui de la réponse en continu. Pendant la durée de vie des requêtes gRPC, des blocs de messages gRPC peuvent être envoyés à tout moment. À la fin du flux, une trame HEADER indiquera la fin du flux. Lorsque le corps est converti en gRPC-web, nous l'envoyons en recourant à un encodage par blocs et maintenons la connexion ouverte, en acceptant les deux côtés du corps jusqu'à ce que nous recevions un bloc de message gRPC indiquant la fin du flux. Cette opération nécessite que notre proxy prenne en charge le transfert bidirectionnel.

Par exemple, le streaming du client est un mode intéressant dans lequel le serveur répond déjà avec un code de réponse et son en-tête, mais où le client peut encore envoyer le corps de la requête.

Test d'interopérabilité

Chaque nouvelle fonctionnalité de Cloudflare doit être correctement testée avant sa sortie. Au cours du développement initial, nous avons utilisé le proxy Envoy avec son filtre gRPC-web et des exemples officiels de gRPC. Nous avons préparé un environnement de test avec Envoy et un serveur d'origine gRPC test pour nous assurer que le proxy de périphérie fonctionnait correctement avec les requêtes gRPC. Les requêtes émanant du client test gRPC sont envoyées au proxy de périphérie et converties en gRPC-web, puis transmises au proxy Envoy. Ensuite, Envoy reconvertit la requête gRPC et l'envoie au serveur d'origine test gRPC. Nous avons pu vérifier le comportement de base de cette manière.

Une fois les fonctionnalités élémentaires prêtes, nous devions également nous assurer que les fonctionnalités de conversion aux deux extrémités fonctionnaient correctement. Pour ce faire, nous avons développé des tests d'interopérabilité plus approfondis.

Nous avons référencé les cas de test d'interopérabilité gRPC existants pour notre suite de tests et avons exécuté la première itération de tests entre le proxy de périphérie et le nouveau proxy de serveur d'origine localement.

Pour la deuxième itération de tests, nous avons utilisé différentes implémentations de gRPC. Par exemple, certains serveurs ont envoyé leur statut final (grpc-status) dans une requête ne contenant que des en-têtes trailer lorsqu'une erreur immédiate est survenue. Cette réponse contenait le trailer et les en-têtes de la réponse HTTP/2 dans un seul bloc de trame HEADERS avec les drapeaux END_STREAM et END_HEADERS activés. D'autres implémentations envoyaient le statut final comme trailer dans une trame HEADER séparée.

Après avoir vérifié localement l'interopérabilité, nous avons exécuté le harnais de test dans un environnement de développement qui prend en charge tous les services que nous avons en production. Nous avons ensuite pu nous assurer de l'absence d'effet secondaire indésirable sur les requêtes gRPC.

Nous adorons l'efficacité ! L'un des premiers services sur lesquels nous avons déployé avec succès la prise en charge de gRPC à la périphérie est la balise aléatoire drand de Cloudflare. L'intégration a été facile, et nous avons exécuté la balise en production ces dernières semaines sans accroc.

Conclusion

Prendre en charge un nouveau protocole est un travail passionnant ! Assurer la compatibilité de nouvelles technologies dans des systèmes existants est à la fois passionnant et complexe, impliquant souvent des compromis entre la rapidité de mise en œuvre et la complexité globale du système. Dans le cas du protocole gRPC, nous avons pu obtenir rapidement une compatibilité, sans nécessiter de changements importants à la périphérie de Cloudflare. Pour ce faire, nous avons soigneusement étudié les options de mise en œuvre avant de nous décider à effectuer une conversion du format gRPC HTTP/2 au format gRPC-web HTTP/1.1. Ce choix de conception a accéléré et facilité l'intégration du service tout en répondant aux attentes et aux contraintes de nos utilisateurs.

Si vous souhaitez utiliser Cloudflare pour sécuriser et accélérer votre service gRPC, vous pouvez obtenir plus d'informations ici. Et si vous cherchez à relever des défis d'ingénierie intéressants comme celui décrit dans cet article, postulez !

gRPC® est une marque déposée de The Linux Foundation.