Was ist gRPC? Wie arbeitet man mit APISIX?

September 28, 2022

Ecosystem

Was ist gRPC

gRPC ist ein von Google open-sourcetes RPC-Framework, das darauf abzielt, die Kommunikation zwischen Diensten zu vereinheitlichen. Das Framework verwendet HTTP/2 als Übertragungsprotokoll und Protocol Buffers als Schnittstellenbeschreibungssprache. Es kann automatisch Code für Aufrufe zwischen Diensten generieren.

Dominanz von gRPC

gRPC ist aufgrund des außergewöhnlichen Einflusses von Google auf Entwickler und Cloud-native-Umgebungen zum Standard für RPC-Frameworks geworden.

Möchten Sie etcd-Funktionen aufrufen? gRPC!

Möchten Sie OpenCensus-Daten senden? gRPC!

Möchten Sie RPC in einem in Go implementierten Microservice verwenden? gRPC!

Die Dominanz von gRPC ist so stark, dass Sie, wenn Sie gRPC nicht als Ihr RPC-Framework gewählt haben, einen soliden Grund dafür angeben müssen. Andernfalls wird immer jemand fragen, warum Sie nicht das Mainstream-gRPC wählen. Selbst Alibaba, das sein RPC-Framework Dubbo stark gefördert hat, hat in der neuesten Version von Dubbo 3 das Protokolldesign dramatisch überarbeitet und es in eine gRPC-Variante geändert, die sowohl mit gRPC als auch mit Dubbo 2 kompatibel ist. Tatsächlich ist Dubbo 3 weniger ein Upgrade von Dubbo 2, sondern eher eine Anerkennung der Vorherrschaft von gRPC.

Viele Dienste, die gRPC anbieten, bieten auch entsprechende HTTP-Schnittstellen an, aber solche Schnittstellen dienen oft nur der Kompatibilität. Die gRPC-Version bietet ein viel besseres Benutzererlebnis. Wenn Sie über gRPC darauf zugreifen können, können Sie direkt das entsprechende SDK importieren. Wenn Sie nur gewöhnliche HTTP-APIs verwenden können, werden Sie normalerweise auf eine Dokumentationsseite weitergeleitet und müssen die entsprechenden HTTP-Operationen selbst implementieren. Obwohl HTTP-Zugriffe das entsprechende SDK über die OpenAPI-Spezifikation generieren können, nehmen nur wenige Projekte HTTP-Benutzer so ernst wie gRPC, da HTTP eine niedrige Priorität hat.

Sollte ich gRPC verwenden?

APISIX verwendet etcd als Konfigurationszentrum. Seit Version 3 hat etcd seine Schnittstelle auf gRPC migriert. Allerdings unterstützt kein Projekt im OpenResty-Ökosystem gRPC, sodass APISIX nur die HTTP-APIs von etcd aufrufen kann. Die HTTP-APIs von etcd werden über gRPC-gateway bereitgestellt. Im Wesentlichen führt etcd einen HTTP-zu-gRPC-Proxy auf der Serverseite aus, und dann werden die externen HTTP-Anfragen über das gRPC-gateway in gRPC-Anfragen umgewandelt. Nachdem wir diese Kommunikationsmethode einige Jahre lang eingesetzt haben, haben wir einige Probleme bei der Interaktion zwischen der HTTP-API und der gRPC-API festgestellt. Ein gRPC-gateway bedeutet nicht, dass der HTTP-Zugriff perfekt unterstützt wird. Es gibt immer noch subtile Unterschiede.

Hier ist eine Liste der damit verbundenen Probleme, die wir in den letzten Jahren mit etcd festgestellt haben:

  1. gRPC-gateway standardmäßig deaktiviert. Aufgrund der Nachlässigkeit des Maintainers ist das gRPC-gateway in einigen Projekten in der Standardkonfiguration von etcd nicht aktiviert. Daher mussten wir in der Dokumentation Anweisungen hinzufügen, um zu überprüfen, ob das aktuelle etcd das gRPC-gateway aktiviert hat. Siehe https://github.com/apache/apisix/pull/2940.
  2. Standardmäßig begrenzt gRPC Antworten auf 4 MB. etcd entfernt diese Einschränkung im bereitgestellten SDK, hat sie aber im gRPC-gateway vergessen. Es stellt sich heraus, dass das offizielle etcdctl (basierend auf dem bereitgestellten SDK) einwandfrei funktioniert, APISIX jedoch nicht. Siehe https://github.com/etcd-io/etcd/issues/12576.
  3. Dasselbe Problem – diesmal mit der maximalen Anzahl von Anfragen für dieselbe Verbindung. Die HTTP2-Implementierung von Go hat eine MaxConcurrentStreams-Konfiguration, die die Anzahl der gleichzeitigen Anfragen steuert, die ein einzelner Client senden kann, standardmäßig 250. Welcher Client würde normalerweise mehr als 250 Anfragen gleichzeitig senden? Daher hat etcd immer diese Konfiguration verwendet. Das gRPC-gateway, der "Client", der alle HTTP-Anfragen an die lokale gRPC-Schnittstelle weiterleitet, kann jedoch dieses Limit überschreiten. Siehe https://github.com/etcd-io/etcd/issues/14185.
  4. Nachdem etcd mTLS aktiviert hat, verwendet etcd dasselbe Zertifikat sowohl als Server-Zertifikat als auch als Client-Zertifikat, das Server-Zertifikat für das gRPC-gateway und das Client-Zertifikat, wenn das gRPC-gateway auf die gRPC-Schnittstelle zugreift. Wenn die Server-Auth-Erweiterung auf dem Zertifikat aktiviert ist, die Client-Auth-Erweiterung jedoch nicht, führt dies zu einem Fehler bei der Zertifikatsüberprüfung. Auch hier funktioniert der direkte Zugriff mit etcdctl einwandfrei (da das Zertifikat in diesem Fall nicht als Client-Zertifikat verwendet wird), APISIX jedoch nicht. Siehe https://github.com/etcd-io/etcd/issues/9785.
  5. Nach der Aktivierung von mTLS ermöglicht etcd die Konfiguration von Sicherheitsrichtlinien für Benutzerinformationen von Zertifikaten. Wie oben erwähnt, verwendet das gRPC-gateway ein festes Client-Zertifikat, wenn es auf die gRPC-Schnittstelle zugreift, und nicht die Zertifikatinformationen, die zu Beginn für den Zugriff auf die HTTP-Schnittstelle verwendet wurden. Daher funktioniert diese Funktion natürlich nicht, da das Client-Zertifikat fest ist und nicht geändert wird. Siehe https://github.com/apache/apisix/issues/5608.

Wir können die Probleme in zwei Punkten zusammenfassen:

  1. gRPC-gateway (und vielleicht andere Versuche, HTTP in gRPC umzuwandeln) ist kein Allheilmittel, das alle Probleme löst.
  2. Die Entwickler von etcd legen nicht genug Wert auf die HTTP-Methode. Und ihr größter Benutzer, Kubernetes, verwendet diese Funktion nicht.

Wir sprechen hier nicht über die Probleme einer bestimmten Software, etcd ist nur ein typisches Beispiel, das gRPC verwendet. Alle Dienste, die gRPC als primäres RPC-Framework verwenden, haben ähnliche Einschränkungen in ihrer Unterstützung für HTTP.

Wie löst APISIX 3.0 dieses Problem?

Wie das Sprichwort sagt: "Wenn der Berg nicht zu Mohammed kommt, muss Mohammed zum Berg gehen." Wenn wir einen gRPC-Client unter OpenResty implementieren, können wir direkt mit dem gRPC-Dienst kommunizieren.

Unter Berücksichtigung der Arbeitslast und Stabilität haben wir uns entschieden, auf der Grundlage der häufig verwendeten gRPC-Bibliothek zu entwickeln, anstatt das Rad neu zu erfinden. Wir haben die folgenden gRPC-Bibliotheken untersucht:

  1. NGINX's gRPC-Dienst. NGINX stellt gRPC nicht für externe Benutzer bereit, nicht einmal eine High-Level-API. Wenn Sie es verwenden möchten, können Sie nur einige Low-Level-Funktionen kopieren und dann in eine High-Level-Schnittstelle integrieren. Die Integration verursacht zusätzliche Arbeitslast.
  2. Die offizielle gRPC-Bibliothek für C++. Da unser System auf NGINX basiert, kann die Integration von C++-Bibliotheken etwas kompliziert sein. Darüber hinaus beträgt die Abhängigkeit dieser Bibliothek fast 2 GB, was eine große Herausforderung für den Aufbau von APISIX darstellt.
  3. Die offizielle Go-Implementierung von gRPC. Go hat ein leistungsstarkes Toolchain, und wir können Projekte schnell darin erstellen. Es ist jedoch schade, dass die Leistung dieser Implementierung weit von der C++-Version entfernt ist. Daher haben wir uns eine andere Go-Implementierung angesehen: https://github.com/bufbuild/connect-go/. Leider ist die Leistung dieses Projekts auch nicht besser als die offizielle Version.
  4. Rust-Implementierung der gRPC-Bibliothek. Diese Bibliothek wäre eine natürliche Wahl für die Kombination von Abhängigkeitsmanagement und Leistung. Leider sind wir mit Rust nicht vertraut und würden nicht darauf wetten.

Unter Berücksichtigung der Tatsache, dass die Operationen eines gRPC-Clients im Wesentlichen alle IO-gebunden sind, ist die Leistungsanforderung nicht primär. Nach sorgfältiger Überlegung haben wir es auf der Grundlage von Go-gRPC implementiert.

Um mit Lua's Coroutine-Scheduler zu koordinieren, haben wir ein NGINX-C-Modul geschrieben: https://github.com/api7/grpc-client-nginx-module. Zunächst wollten wir den Go-Code in dieses C-Modul integrieren, indem wir ihn über cgo in eine statisch verlinkte Bibliothek kompilieren. Wir stellten jedoch fest, dass Go eine Multithread-Anwendung ist und der Kindprozess nach dem Forken nicht alle Threads des Elternprozesses erbt. Es gibt keine Möglichkeit, sich an die Master-Worker-Multiprozess-Architektur von NGINX anzupassen. Daher haben wir den Go-Code in eine dynamisch verlinkte Bibliothek (DLL) kompiliert und sie dann zur Laufzeit in den Worker-Prozess geladen.

Wir haben einen Task-Queue-Mechanismus implementiert, um Go's Coroutines mit Lua's Coroutines zu koordinieren. Wenn Lua-Code eine gRPC-IO-Operation initiiert, sendet er eine Aufgabe an die Go-Seite und hält sich selbst an. Eine Go-Coroutine führt diese Aufgabe aus, und das Ausführungsergebnis wird in die Warteschlange geschrieben. Ein Hintergrundthread auf der NGINX-Seite verarbeitet das Ausführungsergebnis der Aufgabe, plant die entsprechende Lua-Coroutine neu und führt den Lua-Code fort. Auf diese Weise unterscheiden sich gRPC-IO-Operationen in den Augen von Lua-Code nicht von gewöhnlichen Socket-Operationen.

Jetzt ist der größte Teil der Arbeit des NGINX-C-Moduls erledigt. Alles, was wir tun müssen, ist, die .proto-Datei von etcd (die seine gRPC-Schnittstelle definiert) herauszunehmen, sie zu modifizieren und dann die Datei in Lua zu laden, um den folgenden etcd-Client zu erhalten:

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

Diese gRPC-basierte Implementierung ist besser als lua-resty-etcd, ein etcd-HTTP-Client-Projekt mit 1600 Zeilen Code allein in Lua.

Natürlich sind wir noch weit davon entfernt, lua-resty-etcd zu ersetzen. Um eine vollständige Verbindung mit etcd herzustellen, muss grpc-client-nginx-module auch die folgenden Funktionen vervollständigen:

  1. mTLS-Unterstützung
  2. Unterstützung für gRPC-Metadatenkonfiguration
  3. Unterstützung für Parameterkonfigurationen (z. B. MaxConcurrentStreams und MaxRecvMsgSize)
  4. Unterstützung für Anfragen von L4

Glücklicherweise haben wir die Grundlage geschaffen, und die Unterstützung dieser Dinge ist nur eine Frage der Zeit.

grpc-client-nginx-module wird in APISIX 3.0 integriert, dann können APISIX-Benutzer die Methoden dieses Moduls im APISIX-Plugin verwenden, um direkt mit gRPC-Diensten zu kommunizieren.

Mit nativer Unterstützung für gRPC erhält APISIX ein besseres etcd-Erlebnis und öffnet die Tür für Funktionen wie gRPC-Gesundheitschecks und gRPC-basierte Open-Telemetry-Datenberichterstattung.

Wir freuen uns darauf, in Zukunft mehr gRPC-basierte Funktionen von APISIX zu sehen!

Share article link