Vorteile und Nachteile von `string` in OpenResty

API7.ai

December 8, 2022

OpenResty (NGINX + Lua)

Im letzten Artikel haben wir uns mit den gängigen Blockierungsfunktionen in OpenResty vertraut gemacht, die oft von Anfängern falsch verwendet werden. Ab diesem Artikel werden wir uns mit dem Kern der Leistungsoptimierung befassen, was viele Optimierungstechniken beinhaltet, die uns helfen können, die Leistung von OpenResty-Code schnell zu verbessern. Daher sollten wir dies nicht auf die leichte Schulter nehmen.

In diesem Prozess müssen wir mehr Testcode schreiben, um zu erfahren, wie man diese Optimierungstechniken verwendet und ihre Wirksamkeit überprüft, damit wir sie gut nutzen können.

Hinter den Kulissen der Leistungsoptimierungstipps

Optimierungstechniken gehören alle zum "praktischen" Teil, daher wollen wir vorher über die "Theorie" der Optimierung sprechen.

Leistungsoptimierungsmethoden werden sich mit den Iterationen von LuaJIT und OpenResty ändern. Einige Methoden könnten direkt durch die zugrunde liegende Technologie optimiert werden und müssen nicht mehr beherrscht werden; gleichzeitig wird es einige neue Optimierungstechniken geben. Daher ist es am wichtigsten, das konstante Konzept hinter diesen Optimierungstechniken zu beherrschen.

Schauen wir uns einige der kritischen Ideen zur Leistung in der OpenResty-Programmierung an.

Theorie 1: Die Verarbeitung von Anfragen sollte kurz, einfach und schnell sein

OpenResty ist ein Webserver, daher verarbeitet er oft 1.000+, 10.000+ oder sogar 100.000+ Client-Anfragen gleichzeitig. Um die höchste Gesamtleistung zu erreichen, müssen wir sicherstellen, dass einzelne Anfragen schnell verarbeitet werden und verschiedene Ressourcen, wie z.B. Speicher, zurückgewonnen werden.

  • Das hier erwähnte "kurz" bedeutet, dass der Lebenszyklus der Anfrage kurz sein sollte, um Ressourcen nicht lange zu blockieren, ohne sie freizugeben; selbst bei langen Verbindungen sollte ein Schwellenwert für die Zeit oder die Anzahl der Anfragen festgelegt werden, um Ressourcen regelmäßig freizugeben.
  • Das zweite "einfach" bezieht sich darauf, nur eine Sache in einer API zu tun. Komplexe Geschäftslogik sollte in mehrere APIs aufgeteilt werden, und der Code sollte einfach gehalten werden.
  • Schließlich bedeutet "schnell", dass der Hauptthread nicht blockiert werden sollte und nicht zu viele CPU-Operationen ausgeführt werden sollten. Selbst wenn dies notwendig ist, sollten andere Methoden, die wir im letzten Artikel vorgestellt haben, nicht vergessen werden.

Diese architektonische Überlegung ist nicht nur für OpenResty geeignet, sondern auch für weitere Entwicklungssprachen und Plattformen, daher hoffe ich, dass Sie dies sorgfältig verstehen und darüber nachdenken können.

Theorie 2: Vermeiden Sie die Erzeugung von Zwischendaten

Die Vermeidung von nutzlosen Daten im Zwischenprozess ist wohl die dominierende Optimierungstheorie in der OpenResty-Programmierung. Schauen wir uns ein kleines Beispiel an, um nutzlose Daten im Zwischenprozess zu erklären.

$ resty -e 'local s= "hello"
s = s .. " world"
s = s .. "!"
print(s)
'

In diesem Code-Snippet haben wir mehrere Verkettungsoperationen auf der Variablen s durchgeführt, um das Ergebnis hello world! zu erhalten. Aber nur der endgültige Zustand hello world! von s ist nützlich. Der Anfangswert von s und die Zwischenzuweisungen sind alle Zwischendaten, die so wenig wie möglich erzeugt werden sollten.

Der Grund dafür ist, dass diese temporären Daten Initialisierungs- und GC-Leistungsverluste mit sich bringen. Unterschätzen Sie diese Verluste nicht; wenn dies in heißem Code wie Schleifen auftritt, wird die Leistung offensichtlich beeinträchtigt. Ich werde dies später auch mit einem String-Beispiel erklären.

strings sind unveränderlich

Nun zurück zum Thema dieses Artikels, string. Hier möchte ich betonen, dass strings in Lua unveränderlich sind.

Das bedeutet natürlich nicht, dass strings nicht verkettet, modifiziert usw. werden können, aber wenn wir einen string ändern, ändern wir nicht den ursprünglichen string, sondern erstellen ein neues string-Objekt und ändern die Referenz auf den string. Wenn der ursprüngliche string keine anderen Referenzen hat, wird er natürlich von Lua's GC (Garbage Collection) zurückgewonnen.

Der offensichtliche Vorteil von unveränderlichen strings ist, dass sie Speicher sparen. Auf diese Weise gibt es nur eine Kopie des gleichen strings im Speicher, und verschiedene Variablen zeigen auf die gleiche Speicheradresse.

Der Nachteil dieses Designs ist, dass beim Hinzufügen und Zurückgewinnen von strings jedes Mal, wenn Sie einen string hinzufügen, LuaJIT lj_str_new aufrufen muss, um zu prüfen, ob der string bereits existiert; wenn nicht, muss ein neuer string erstellt werden. Wenn Sie dies sehr oft tun, wird dies die Leistung erheblich beeinträchtigen.

Schauen wir uns ein konkretes Beispiel für eine string-Verkettungsoperation wie in diesem Beispiel an, das in vielen OpenResty-Open-Source-Projekten zu finden ist.

$ resty -e 'local begin = ngx.now()
local s = ""
-- `for`-Schleife, die `..` verwendet, um String-Verkettungen durchzuführen
for i = 1, 100000 do
    s = s .. "a"
end
ngx.update_time()
print(ngx.now() - begin)
'

Was dieser Beispielcode tut, ist 100.000 string-Verkettungen auf der Variablen s durchzuführen und die Laufzeit auszugeben. Obwohl das Beispiel etwas extrem ist, gibt es einen guten Eindruck von der Differenz vor und nach der Leistungsoptimierung. Ohne Optimierung läuft dieser Code auf meinem Laptop 0,4 Sekunden, was immer noch relativ langsam ist. Wie sollten wir ihn also optimieren?

In den vorherigen Artikeln wurde die Antwort gegeben, nämlich table zu verwenden, um eine Ebene der Kapselung durchzuführen, alle temporären Zwischen-strings zu entfernen und nur die ursprünglichen Daten und das Endergebnis zu behalten. Schauen wir uns die konkrete Code-Implementierung an.

$ resty -e 'local begin = ngx.now()
local t = {}
-- for-Schleife, die ein Array verwendet, um den String zu halten, und jedes Mal die Länge des Arrays zählt
for i = 1, 100000 do
    t[#t + 1] = "a"
end
-- Verkettung der Strings mit der concat-Methode des Arrays
local s =  table.concat(t, "")
ngx.update_time()
print(ngx.now() - begin)
'

Wir können sehen, dass dieser Code jeden String nacheinander mit table speichert, und der Index wird durch #t + 1 bestimmt, also die aktuelle Länge von table plus 1. Schließlich wird die table.concat-Funktion verwendet, um jedes Array-Element zu verketten. Dies überspringt natürlich alle temporären Strings und vermeidet 100.000 Mal lj_str_new und GC.

Das war unsere Code-Analyse, aber wie funktioniert die Optimierung? Der optimierte Code dauert nur 0,007 Sekunden, was eine Leistungssteigerung von mehr als 50 Mal bedeutet. In einem tatsächlichen Projekt könnte die Leistungssteigerung noch deutlicher sein, da wir in diesem Beispiel nur ein Zeichen a hinzugefügt haben.

Wie wäre die Leistungsdifferenz, wenn der neue string die Länge von 10x a hätte?

Sind die 0,007 Sekunden des Codes gut genug für unsere Optimierungsarbeit? Nein, es kann noch optimiert werden. Lassen Sie uns noch eine Codezeile ändern und das Ergebnis sehen.

$ resty -e 'local begin = ngx.now()
local t = {}
-- for-Schleife, die ein Array verwendet, um den String zu halten, und die Länge des Arrays selbst verwaltet
for i = 1, 100000 do
    t[i] = "a"
end
local s =  table.concat(t, "")
ngx.update_time()
print(ngx.now() - begin)
'

Dieses Mal haben wir t[#t + 1] = "a" in t[i] = "a" geändert, und mit nur einer Codezeile können wir 100.000 Funktionsaufrufe vermeiden, um die Länge des Arrays zu erhalten. Erinnern Sie sich an die Operation, um die Länge eines Arrays zu erhalten, die wir im table-Abschnitt früher erwähnt haben? Sie hat eine Zeitkomplexität von O(n), eine relativ teure Operation. Hier umgehen wir einfach die Operation, die Länge des Arrays zu erhalten, indem wir unseren Array-Index selbst verwalten. Wie das Sprichwort sagt: Wenn Sie es sich nicht leisten können, es zu vermasseln, können Sie es vermeiden.

Natürlich ist dies eine einfachere Schreibweise. Der folgende Code zeigt deutlicher, wie wir den Index eines Arrays selbst verwalten können.

$ resty -e 'local begin = ngx.now()
local t = {}
local index = 1
for i = 1, 100000 do
    t[index] = "a"
    index = index + 1
end
local s = table.concat(t, "")
ngx.update_time()
print(ngx.now() - begin)
'

Reduzieren Sie andere temporäre strings

Die Fehler, über die wir gerade gesprochen haben, temporäre strings, die durch string-Verkettung verursacht werden, sind offensichtlich. Mit ein paar Erinnerungen an den obigen Beispielcode glaube ich, dass wir keine ähnlichen Fehler mehr machen werden. Es gibt jedoch einige verstecktere temporäre strings, die in OpenResty erzeugt werden und viel schwerer zu erkennen sind. Zum Beispiel die string-Behandlungsfunktion, die wir unten diskutieren werden, wird oft verwendet. Können Sie sich vorstellen, dass sie auch temporäre strings erzeugt?

Wie wir wissen, schneidet die string.sub-Funktion einen bestimmten Teil eines strings ab. Wie wir bereits erwähnt haben, sind strings in Lua unveränderlich, daher beinhaltet das Abschneiden eines neuen Strings lj_str_new und nachfolgende GC-Operationen.

resty -e 'print(string.sub("abcd", 1, 1))'

Die Funktion des obigen Codes besteht darin, das erste Zeichen des strings zu extrahieren und auszugeben. Natürlich wird dies unweigerlich einen temporären string erzeugen. Gibt es eine bessere Möglichkeit, den gleichen Effekt zu erzielen?

resty -e 'print(string.char(string.byte("abcd")))'

Natürlich. Schauen wir uns diesen Code an, wir verwenden zuerst string.byte, um den numerischen Code des ersten Zeichens zu erhalten, und dann string.char, um die Zahl in das entsprechende Zeichen umzuwandeln. Dieser Prozess erzeugt keine temporären strings. Daher ist es am effizientesten, string.byte zu verwenden, um string-bezogene Scans und Analysen durchzuführen.

Nutzen Sie die SDK-Unterstützung für den table-Typ

Nachdem Sie gelernt haben, wie man temporäre strings reduziert, sind Sie begierig darauf, es auszuprobieren? Dann können wir das Ergebnis des obigen Beispielcodes nehmen und es als Inhalt des Antwortkörpers an den Client ausgeben. An dieser Stelle können Sie pausieren und versuchen, diesen Code selbst zu schreiben.

$ resty -e 'local begin = ngx.now()
local t = {}
local index = 1
for i = 1, 100000 do
    t[index] = "a"
    index = index + 1
end
local response = table.concat(t, "")
ngx.say(response)
'

Wenn Sie diesen Code schreiben können, sind Sie bereits den meisten OpenResty-Entwicklern voraus. OpenResty's Lua API berücksichtigt bereits die Verwendung von tables für string-Verkettungen, daher akzeptiert ngx.say, ngx.print, ngx.log, cosocket:send und andere APIs, die viele strings verarbeiten können, nicht nur string als Parameter, sondern auch table als Parameter.

resty -e 'local begin = ngx.now()
local t = {}
local index = 1
for i = 1, 100000 do
    t[index] = "a"
    index = index + 1
end
ngx.say(t)
'

In diesem letzten Code-Snippet haben wir local response = table.concat(t, ""), den string-Verkettungsschritt, weggelassen und das table direkt an ngx.say übergeben. Dies verschiebt die string-Verkettungsaufgabe von der Lua-Ebene auf die C-Ebene und vermeidet eine weitere string-Suche, -Erzeugung und -GC. Für lange strings ist dies ein weiterer erheblicher Leistungsgewinn.

Zusammenfassung

Nachdem wir diesen Artikel gelesen haben, können wir sehen, dass sich viel von OpenResty's Leistungsoptimierung mit verschiedenen Details befasst. Daher müssen wir LuaJIT und OpenResty's Lua API gut kennen, um optimale Leistung zu erzielen. Dies erinnert uns auch daran, dass wir, wenn wir den vorherigen Inhalt vergessen haben, ihn rechtzeitig überprüfen und festigen müssen.

Zum Schluss denken Sie über ein Problem nach: Schreiben Sie die Strings hello, world und ! in das Fehlerprotokoll. Können wir einen Beispielcode ohne string-Verkettung schreiben?

Vergessen Sie auch nicht die andere Frage im Text. Wie wäre die Leistungsdifferenz im folgenden Code, wenn die neuen strings die Länge von 10x a hätten?

$ resty -e 'local begin = ngx.now()
local t = {}
for i = 1, 100000 do
    t[#t + 1] = "a"
end
local s =  table.concat(t, "")
ngx.update_time()
print(ngx.now() - begin)
'

Sie können diesen Artikel auch gerne mit Ihren Freunden teilen, um zu lernen und zu kommunizieren.