Wie ist Apache APISIX schnell?

Navendu Pottekkat

Navendu Pottekkat

June 12, 2023

Technology

"High speed," "minimum latency," und "ultimate performance" sind oft verwendete Begriffe, um Apache APISIX zu beschreiben. Selbst wenn mich jemand nach APISIX fragt, lautet meine Antwort immer "hochperformanter Cloud-native API-Gateway."

Performance-Benchmarks (im Vergleich zu Kong und Envoy) bestätigen, dass diese Eigenschaften tatsächlich zutreffen (selbst testen).

High speed, minimum latency, and ultimate performance

Tests durchgeführt über 10 Runden mit 5000 eindeutigen Routen auf Standard D8s v3 (8 vCPUs, 32 GiB RAM).

Aber wie erreicht APISIX dies?

Um diese Frage zu beantworten, müssen wir uns drei Dinge ansehen: etcd, Hash-Tabellen und Radix-Bäume.

In diesem Artikel werfen wir einen Blick unter die Haube von APISIX und sehen, was diese sind und wie sie zusammenarbeiten, um APISIX bei der Bewältigung von hohem Datenverkehr auf Spitzenleistung zu halten.

etcd als Konfigurationszentrum

APISIX verwendet etcd, um Konfigurationen zu speichern und zu synchronisieren.

etcd ist als Schlüssel-Wert-Speicher für die Konfigurationen von groß angelegten verteilten Systemen konzipiert. APISIX ist von Grund auf als verteilt und hochskalierbar konzipiert, und die Verwendung von etcd anstelle traditioneller Datenbanken erleichtert dies.

APISIX-Architektur

Eine weitere unverzichtbare Funktion für API-Gateways ist die hohe Verfügbarkeit, um Ausfallzeiten und Datenverlust zu vermeiden. Dies kann effizient erreicht werden, indem mehrere Instanzen von etcd bereitgestellt werden, um eine fehlertolerante, Cloud-native Architektur zu gewährleisten.

APISIX kann Konfigurationen mit minimaler Latenz aus etcd lesen und in etcd schreiben. Änderungen an den Konfigurationsdateien werden sofort benachrichtigt, sodass APISIX nur die etcd-Updates überwachen muss, anstatt häufig eine Datenbank abzufragen, was zu Leistungseinbußen führen kann.

Diese Tabelle fasst zusammen, wie etcd im Vergleich zu anderen Datenbanken abschneidet.

Hash-Tabellen für IP-Adressen

IP-Adressen-basierte Allowlists/Denylists sind ein häufiger Anwendungsfall für API-Gateways.

Um hohe Leistung zu erreichen, speichert APISIX die Liste der IP-Adressen in einer Hash-Tabelle und verwendet sie für den Abgleich (O(1)) anstatt die Liste zu durchlaufen (O(N)).

Mit zunehmender Anzahl von IP-Adressen in der Liste wird die Leistungsauswirkung der Verwendung von Hash-Tabellen für die Speicherung und den Abgleich deutlich.

Unter der Haube verwendet APISIX die lua-resty-ipmatcher-Bibliothek, um diese Funktionalität zu implementieren. Das folgende Beispiel zeigt, wie die Bibliothek verwendet wird:

local ipmatcher = require("resty.ipmatcher")
local ip = ipmatcher.new({
    "162.168.46.72",
    "17.172.224.47",
    "216.58.32.170",
})

ngx.say(ip:match("17.172.224.47")) -- true
ngx.say(ip:match("176.24.76.126")) -- false

Die Bibliothek verwendet Lua-Tabellen, die Hash-Tabellen sind. Die IP-Adressen werden gehasht und als Indizes in einer Tabelle gespeichert, und um nach einer bestimmten IP-Adresse zu suchen, muss man nur die Tabelle indizieren und prüfen, ob sie nil ist oder nicht.

Speichern von IP-Adressen in einer Hash-Tabelle

Um nach einer IP-Adresse zu suchen, wird zunächst der Hash (Index) berechnet und sein Wert überprüft. Wenn er nicht leer ist, haben wir eine Übereinstimmung. Dies geschieht in konstanter Zeit O(1).

Radix-Bäume für das Routing

Bitte verzeihen Sie mir, dass ich Sie in eine Datenstrukturen-Lektion gelockt habe! Aber hören Sie mir zu; hier wird es interessant.

Ein Schlüsselbereich, in dem APISIX die Leistung optimiert, ist das Routen-Matching.

APISIX gleicht eine Route mit einer Anfrage anhand ihrer URI, HTTP-Methoden, des Hosts und anderer Informationen ab (siehe Router). Und dies muss effizient sein.

Wenn Sie den vorherigen Abschnitt gelesen haben, wäre eine offensichtliche Antwort, einen Hash-Algorithmus zu verwenden. Aber das Routen-Matching ist knifflig, da mehrere Anfragen auf dieselbe Route passen können.

Zum Beispiel, wenn wir eine Route /api/* haben, dann müssen sowohl /api/create als auch /api/destroy auf die Route passen. Aber dies ist mit einem Hash-Algorithmus nicht möglich.

Reguläre Ausdrücke könnten eine alternative Lösung sein. Routen können in einem Regex konfiguriert werden, und es kann mehrere Anfragen abgleichen, ohne jede Anfrage hartkodieren zu müssen.

Wenn wir unser vorheriges Beispiel nehmen, können wir den Regex /api/[A-Za-z0-9]+ verwenden, um sowohl /api/create als auch /api/destroy abzugleichen. Komplexere Regexe könnten komplexere Routen abgleichen.

Aber Regex ist langsam! Und wir wissen, dass APISIX schnell ist. Stattdessen verwendet APISIX Radix-Bäume, die komprimierte Präfix-Bäume (Trie) sind und sich sehr gut für schnelle Lookups eignen.

Schauen wir uns ein einfaches Beispiel an. Angenommen, wir haben die folgenden Wörter:

  • romane
  • romanus
  • romulus
  • rubens
  • ruber
  • rubicon
  • rubicundus

Ein Präfix-Baum würde sie so speichern:

Präfix-Baum

Die hervorgehobene Traversierung zeigt das Wort "rubens."

Ein Radix-Baum optimiert einen Präfix-Baum, indem er Kindknoten zusammenführt, wenn ein Knoten nur ein Kind hat. Unser Beispiel-Trie würde als Radix-Baum so aussehen:

Radix-Baum

Die hervorgehobene Traversierung zeigt immer noch das Wort "rubens." Aber der Baum sieht viel kleiner aus!

Wenn Sie Routen in APISIX erstellen, speichert APISIX sie in diesen Bäumen.

APISIX kann dann einwandfrei arbeiten, da die Zeit, die zum Abgleichen einer Route benötigt wird, nur von der Länge der URI in der Anfrage abhängt und unabhängig von der Anzahl der Routen ist (O(K), K ist die Länge des Schlüssels/der URI).

Daher wird APISIX genauso schnell sein, wenn Sie 10 Routen abgleichen, wie wenn Sie 5000 Routen abgleichen, wenn Sie skalieren.

Dieses grobe Beispiel zeigt, wie APISIX Routen mit Radix-Bäumen speichern und abgleichen kann:

Grobe Beispiel für Routen-Matching in APISIX

Die hervorgehobene Traversierung zeigt die Route /user/*, wobei das * ein Präfix darstellt. Eine URI wie /user/navendu wird also auf diese Route passen. Der Beispielcode unten sollte diese Ideen klarer machen.

APISIX verwendet die lua-resty-radixtree-Bibliothek, die rax, eine Radix-Baum-Implementierung in C, umschließt. Dies verbessert die Leistung im Vergleich zur Implementierung der Bibliothek in reinem Lua.

Das folgende Beispiel zeigt, wie die Bibliothek verwendet wird:

local radix = require("resty.radixtree")
local rx = radix.new({
    {
        paths = { "/api/*action" },
        metadata = { "metadata /api/action" }
    },
    {
        paths = { "/user/:name" },
        metadata = { "metadata /user/name" },
        methods = { "GET" },
    },
    {
        paths = { "/admin/:name" },
        metadata = { "metadata /admin/name" },
        methods = { "GET", "POST", "PUT" },
        filter_fun = function(vars, opts)
            return vars["arg_access"] == "admin"
        end
    }
})

local opts = {
    matched = {}
}

-- matches the first route
ngx.say(rx:match("/api/create", opts)) -- metadata /api/action
ngx.say("action: ", opts.matched.action) -- action: create

ngx.say(rx:match("/api/destroy", opts)) -- metadata /api/action
ngx.say("action: ", opts.matched.action) -- action: destroy

local opts = {
    method = "GET",
    matched = {}
}

-- matches the second route
ngx.say(rx:match("/user/bobur", opts)) -- metadata /user/name
ngx.say("name: ", opts.matched.name) -- name: bobur

local opts = {
    method = "POST",
    var = ngx.var,
    matched = {}
}

-- matches the third route
-- the value for `arg_access` is obtained from `ngx.var`
ngx.say(rx:match("/admin/nicolas", opts)) -- metadata /admin/name
ngx.say("admin name: ", opts.matched.name) -- admin name: nicolas

Die Fähigkeit, eine große Anzahl von Routen effizient zu verwalten, hat APISIX zum API-Gateway der Wahl für viele groß angelegte Projekte gemacht.

Unter die Haube schauen

Es gibt nur so viel, was ich in einem Artikel über die inneren Abläufe von APISIX erklären kann.

Aber das Beste ist, dass die hier erwähnten Bibliotheken und Apache APISIX vollständig Open Source sind, was bedeutet, dass Sie selbst unter die Haube schauen und Dinge ändern können.

Und wenn Sie APISIX verbessern können, um das letzte bisschen Leistung herauszuholen, können Sie die Änderungen beitragen und alle von Ihrer Arbeit profitieren lassen.

Tags: