Qu'est-ce que gRPC ? Comment travailler avec APISIX ?

Zexuan Luo

Zexuan Luo

September 28, 2022

Ecosystem

Qu'est-ce que gRPC

gRPC est un framework RPC open-source par Google qui vise à unifier la communication entre les services. Le framework utilise HTTP/2 comme protocole de transfert et Protocol Buffers comme langage de description d'interface. Il peut générer automatiquement du code pour les appels entre services.

La domination de gRPC

gRPC est devenu le standard des frameworks RPC grâce à l'influence exceptionnelle de Google sur les développeurs et les environnements cloud-native.

Vous voulez invoquer les fonctions d'etcd ? gRPC !

Vous voulez envoyer des données OpenCensus ? gRPC !

Vous voulez utiliser RPC dans un microservice implémenté en Go ? gRPC !

La domination de gRPC est si forte que si vous ne choisissez pas gRPC comme framework RPC, vous devez donner une raison solide pour justifier ce choix. Sinon, quelqu'un vous demandera toujours pourquoi vous n'avez pas choisi le gRPC mainstream. Même Alibaba, qui a vigoureusement promu son framework RPC Dubbo, a considérablement révisé la conception du protocole dans la dernière version de Dubbo 3, le transformant en une variante de gRPC compatible à la fois avec gRPC et Dubbo 2. En fait, plutôt que de dire que Dubbo 3 est une mise à niveau de Dubbo 2, il est plus juste de dire que c'est une reconnaissance de la suprématie de gRPC.

De nombreux services qui fournissent gRPC offrent également des interfaces HTTP correspondantes, mais ces interfaces sont souvent uniquement destinées à la compatibilité. La version gRPC offre une bien meilleure expérience utilisateur. Si vous pouvez accéder via gRPC, vous pouvez directement importer le SDK correspondant. Si vous ne pouvez utiliser que des API HTTP ordinaires, vous serez généralement redirigé vers une page de documentation, et vous devrez implémenter vous-même les opérations HTTP correspondantes. Bien que l'accès HTTP puisse générer le SDK correspondant via la spécification OpenAPI, peu de projets prennent les utilisateurs HTTP aussi au sérieux que gRPC, car HTTP est une priorité faible.

Devrais-je utiliser gRPC

APISIX utilise etcd comme centre de configuration. Depuis la version 3, etcd a migré son interface vers gRPC. Cependant, aucun projet ne supporte gRPC dans l'écosystème OpenResty, donc APISIX ne peut appeler que les API HTTP d'etcd. Les API HTTP d'etcd sont fournies via gRPC-gateway. En essence, etcd exécute un proxy HTTP vers gRPC côté serveur, puis les requêtes HTTP externes sont converties en requêtes gRPC via gRPC-gateway. Après avoir déployé cette méthode de communication pendant quelques années, nous avons rencontré quelques problèmes dans l'interaction entre l'API HTTP et l'API gRPC. Avoir un gRPC-gateway ne signifie pas que l'accès HTTP est parfaitement supporté. Il existe encore des différences subtiles.

Voici une liste des problèmes liés que nous avons rencontrés avec etcd au cours des dernières années :

  1. gRPC-gateway désactivé par défaut. En raison de la négligence du mainteneur, la configuration par défaut d'etcd n'active pas gRPC-gateway dans certains projets. Nous avons donc dû ajouter des instructions dans la documentation pour vérifier si l'etcd actuel a gRPC-gateway activé. Voir https://github.com/apache/apisix/pull/2940.
  2. Par défaut, gRPC limite les réponses à 4MB. etcd supprime cette restriction dans le SDK qu'il fournit mais a oublié de la supprimer dans gRPC-gateway. Il s'avère que l'officiel etcdctl (construit sur le SDK qu'il fournit) fonctionne bien, mais pas APISIX. Voir https://github.com/etcd-io/etcd/issues/12576.
  3. Même problème - cette fois avec le nombre maximum de requêtes pour la même connexion. L'implémentation HTTP2 de Go a une configuration MaxConcurrentStreams qui contrôle le nombre de requêtes simultanées qu'un seul client peut envoyer, par défaut à 250. Quel client enverrait normalement plus de 250 requêtes en même temps ? Donc etcd a toujours utilisé cette configuration. Cependant, gRPC-gateway, le "client" qui proxy toutes les requêtes HTTP vers l'interface gRPC locale, peut dépasser cette limite. Voir https://github.com/etcd-io/etcd/issues/14185.
  4. Après qu'etcd active mTLS, etcd utilise le même certificat à la fois comme certificat serveur et client, le certificat serveur pour gRPC-gateway, et le certificat client lorsque gRPC-gateway accède à l'interface gRPC. Si l'extension d'authentification serveur est activée sur le certificat, mais que l'extension d'authentification client ne l'est pas, une erreur se produira lors de la vérification du certificat. Une fois de plus, accéder directement avec etcdctl fonctionne bien (car le certificat ne sera pas utilisé comme certificat client dans ce cas), mais pas APISIX. Voir https://github.com/etcd-io/etcd/issues/9785.
  5. Après avoir activé mTLS, etcd permet la configuration des politiques de sécurité des informations utilisateur des certificats. Comme mentionné ci-dessus, gRPC-gateway utilise un certificat client fixe lors de l'accès à l'interface gRPC plutôt que les informations du certificat utilisé pour accéder à l'interface HTTP au départ. Ainsi, cette fonctionnalité ne fonctionnera pas naturellement puisque le certificat client est fixe et ne sera pas modifié. Voir https://github.com/apache/apisix/issues/5608.

Nous pouvons résumer les problèmes en deux points :

  1. gRPC-gateway (et peut-être d'autres tentatives de conversion HTTP vers gRPC) n'est pas une solution miracle qui résout tous les problèmes.
  2. Les développeurs d'etcd ne mettent pas assez l'accent sur la méthode HTTP. Et leur plus grand utilisateur, Kubernetes, n'utilise pas cette fonctionnalité.

Nous ne parlons pas ici des problèmes d'un logiciel spécifique, etcd est juste un exemple typique qui utilise gRPC. Tous les services qui utilisent gRPC comme framework RPC principal ont des limitations similaires dans leur support pour HTTP.

Comment APISIX 3.0 résout ce problème

Comme dit le proverbe, "si la montagne ne vient pas à Mahomet, Mahomet doit aller à la montagne". Si nous implémentons un client gRPC sous OpenResty, nous pouvons communiquer directement avec le service gRPC.

Considérant la charge de travail et la stabilité, nous avons décidé de développer basé sur la bibliothèque gRPC couramment utilisée au lieu de réinventer la roue. Nous avons examiné les bibliothèques gRPC suivantes :

  1. Le service gRPC de NGINX. NGINX n'expose pas gRPC aux utilisateurs externes, pas même une API de haut niveau. Si vous voulez l'utiliser, vous ne pouvez que copier quelques fonctions de bas niveau et ensuite les intégrer dans une interface de haut niveau. Les intégrer causera une charge de travail supplémentaire.
  2. La bibliothèque gRPC officielle pour C++. Comme notre système est basé sur NGINX, il peut être un peu compliqué d'intégrer des bibliothèques C++. De plus, la dépendance de cette bibliothèque est proche de 2GB, ce qui sera un grand défi pour la construction d'APISIX.
  3. L'implémentation officielle de gRPC en Go. Go a un puissant outillage, et nous pouvons rapidement construire des projets dedans. Cependant, il est dommage que la performance de cette implémentation soit loin de la version C++. Nous avons donc regardé une autre implémentation Go : https://github.com/bufbuild/connect-go/. Malheureusement, la performance de ce projet n'est pas meilleure que la version officielle non plus.
  4. L'implémentation de la bibliothèque gRPC en Rust. Cette bibliothèque serait un choix naturel pour combiner la gestion des dépendances et la performance. Malheureusement, nous ne sommes pas familiers avec Rust et ne parierions pas dessus.

Considérant que les opérations d'un client gRPC sont essentiellement toutes liées à l'IO, la performance n'est pas la principale exigence. Après une réflexion approfondie, nous l'avons implémenté basé sur Go-gRPC.

Pour coordonner avec le planificateur de coroutines de Lua, nous avons écrit un module C NGINX : https://github.com/api7/grpc-client-nginx-module. Au début, nous voulions intégrer le code Go dans ce module C en le compilant en une bibliothèque statiquement liée via cgo. Mais nous avons constaté que puisque Go est une application multi-thread, le processus enfant n'héritera pas de tous les threads du processus parent après un fork. Il n'y a aucun moyen de s'adapter à l'architecture multi-processus master-worker de NGINX. Nous avons donc compilé le code Go en une bibliothèque de liens dynamiques (DLL) et l'avons chargée dans le processus worker au moment de l'exécution.

Nous avons implémenté un mécanisme de file d'attente de tâches pour coordonner les coroutines de Go avec celles de Lua. Lorsque le code Lua initie une opération IO gRPC, il soumet une tâche au côté Go et se suspend. Une coroutine Go exécutera cette tâche, et le résultat de l'exécution sera écrit dans la file d'attente. Un thread en arrière-plan du côté NGINX consomme le résultat de l'exécution de la tâche, replanifie la coroutine Lua correspondante et continue l'exécution du code Lua. De cette manière, les opérations IO gRPC ne sont pas différentes des opérations socket ordinaires aux yeux du code Lua.

Maintenant, la plupart du travail du module C NGINX est fait. Tout ce que nous avons à faire est de prendre le fichier .proto d'etcd (qui définit son interface gRPC), le modifier, puis charger le fichier en Lua pour obtenir le client etcd suivant :

local gcli = require("resty.grpc")
assert(gcli.load("t/testdata/rpc.proto"))
local conn = assert(gcli.connect("127.0.0.1:2379"))
local st, err = conn:new_server_stream("etcdserverpb.Watch", "Watch",
                                        {create_request =
                                            {key = ngx.var.arg_key}},
                                        {timeout = 30000})
if not st then
    ngx.status = 503
    ngx.say(err)
    return
end
for i = 1, (ngx.var.arg_count or 10) do
    local res, err = st:recv()
    ngx.log(ngx.WARN, "received ", cjson.encode(res))
    if not res then
        ngx.status = 503
        ngx.say(err)
        break
    end
end

Cette implémentation basée sur gRPC est meilleure que lua-resty-etcd, un projet client etcd-HTTP avec 1600 lignes de code en Lua seul.

Bien sûr, nous sommes encore loin de remplacer lua-resty-etcd. Pour se connecter complètement à etcd, grpc-client-nginx-module doit également compléter les fonctions suivantes :

  1. Support de mTLS
  2. Support de la configuration des métadonnées gRPC
  3. Support des configurations de paramètres (par exemple MaxConcurrentStreams et MaxRecvMsgSize)
  4. Support des requêtes de L4

Heureusement, nous avons posé les bases, et supporter ces choses est juste une question de temps.

grpc-client-nginx-module sera intégré dans APISIX 3.0, puis les utilisateurs d'APISIX pourront utiliser les méthodes de ce module dans le plugin APISIX pour communiquer directement avec les services gRPC.

Avec le support natif de gRPC, APISIX obtiendra une meilleure expérience etcd et ouvre la porte à des fonctionnalités telles que la vérification de santé gRPC et le rapport de données open telemetry basé sur gRPC.

Nous sommes impatients de voir plus de fonctionnalités basées sur gRPC d'APISIX à l'avenir !

Tags: