Der Kern von OpenResty: cosocket

API7.ai

October 28, 2022

OpenResty (NGINX + Lua)

Heute werden wir die Kerntechnologie in OpenResty lernen: cosocket.

Wir haben es in den vorherigen Artikeln bereits oft erwähnt, cosocket ist die Grundlage verschiedener lua-resty-* nicht-blockierender Bibliotheken. Ohne cosocket können Entwickler Lua nicht verwenden, um schnell eine Verbindung zu externen Webdiensten herzustellen.

In früheren Versionen von OpenResty musste man, wenn man mit Diensten wie Redis und memcached interagieren wollte, die C-Module redis2-nginx-module, redis-nginx-module und memc-nginx-module verwenden. Diese Module sind immer noch in der OpenResty-Distribution verfügbar.

Mit der Einführung der cosocket-Funktion wurden die C-Module jedoch durch lua-resty-redis und lua-resty-memcached ersetzt. Niemand verwendet mehr C-Module, um eine Verbindung zu externen Diensten herzustellen.

Was ist cosocket?

Was genau ist also cosocket? cosocket ist ein Fachbegriff in OpenResty. Der Name cosocket setzt sich aus coroutine + socket zusammen.

cosocket benötigt die Unterstützung der Lua-Concurrency-Funktion und den grundlegenden Ereignismechanismus in NGINX, die zusammen nicht-blockierende Netzwerk-I/O ermöglichen. cosocket unterstützt auch TCP, UDP und Unix Domain Socket.

Die interne Implementierung sieht wie folgt aus, wenn wir eine cosocket-bezogene Funktion in OpenResty aufrufen.

Aufruf einer cosocket-bezogenen Funktion

Ich habe dieses Diagramm auch im vorherigen Artikel über OpenResty-Prinzipien und Grundkonzepte verwendet. Wie Sie im Diagramm sehen können, wird bei jedem Netzwerkvorgang, der durch das Lua-Skript des Benutzers ausgelöst wird, sowohl das yield als auch das resume der Coroutine ausgeführt.

Wenn Netzwerk-I/O auftritt, wird das Netzwerkereignis in die NGINX-Listener-Liste eingetragen und die Kontrolle (yield) an NGINX übergeben. Wenn ein NGINX-Ereignis die Auslösebedingung erreicht, wird die Coroutine aufgeweckt, um die Verarbeitung fortzusetzen (resume).

Der obige Prozess ist der Bauplan, den OpenResty verwendet, um die Connect-, Send-, Receive- usw. Operationen zu kapseln, die die cosocket-APIs bilden, die wir heute sehen. Ich werde die API für die Behandlung von TCP als Beispiel verwenden. Die Schnittstelle für die Steuerung von UDP und Unix Domain Sockets ist dieselbe wie die von TCP.

Einführung in die cosocket-APIs und Befehle

Die TCP-bezogenen cosocket-APIs können in die folgenden Kategorien unterteilt werden.

  • Objekte erstellen: ngx.socket.tcp.
  • Timeout setzen: tcpsock:settimeout und tcpsock:settimeouts.
  • Verbindung herstellen: tcpsock:connect.
  • Daten senden: tcpsock:send.
  • Daten empfangen: tcpsock:receive, tcpsock:receiveany und tcpsock:receiveuntil.
  • Verbindungspooling: tcpsock:setkeepalive.
  • Verbindung schließen: tcpsock:close.

Wir sollten auch besonders auf die Kontexte achten, in denen diese APIs verwendet werden können.

rewrite_by_lua*, access_by_lua*, content_by_lua*, ngx.timer.*, ssl_certificate_by_lua*, ssl_session_fetch_by_lua*_

Ein weiterer Punkt, den ich betonen möchte, ist, dass es aufgrund verschiedener Einschränkungen im NGINX-Kernel viele nicht verfügbare Umgebungen gibt. Zum Beispiel ist die cosocket-API in set_by_lua*, log_by_lua*, header_filter_by_lua* und body_filter_by_lua* nicht verfügbar. Sie ist derzeit auch in init_by_lua* und init_worker_by_lua* nicht verfügbar, aber der NGINX-Kernel schränkt diese beiden Phasen nicht ein, sodass die Unterstützung später hinzugefügt werden kann.

Es gibt acht NGINX-Befehle, die mit lua_socket_ beginnen und mit diesen APIs zusammenhängen. Schauen wir uns diese kurz an.

  • lua_socket_connect_timeout: Verbindungs-Timeout, standardmäßig 60 Sekunden.
  • lua_socket_send_timeout: Sende-Timeout, standardmäßig 60 Sekunden.
  • lua_socket_send_lowat: Sende-Schwellenwert (low water), standardmäßig 0.
  • lua_socket_read_timeout: Lese-Timeout, standardmäßig 60 Sekunden.
  • lua_socket_buffer_size: Puffergröße für das Lesen von Daten, standardmäßig 4k/8k.
  • lua_socket_pool_size: Verbindungspool-Größe, standardmäßig 30.
  • lua_socket_keepalive_timeout: Leerlaufzeit des Verbindungspool-cosocket-Objekts, standardmäßig 60 Sekunden.
  • lua_socket_log_errors: Ob cosocket-Fehler protokolliert werden sollen, wenn sie auftreten, standardmäßig on.

Hier können Sie auch sehen, dass einige Befehle die gleiche Funktionalität wie die API haben, wie das Setzen des Timeouts und der Verbindungspool-Größe. Wenn jedoch ein Konflikt zwischen den beiden besteht, hat die API eine höhere Priorität als die Befehle und überschreibt den durch den Befehl gesetzten Wert. Daher empfehlen wir im Allgemeinen, die APIs für die Einstellungen zu verwenden, was auch flexibler ist.

Als Nächstes schauen wir uns ein konkretes Beispiel an, um zu verstehen, wie man diese cosocket-APIs verwendet. Die Funktion des folgenden Codes ist einfach, er sendet eine TCP-Anfrage an eine Website und gibt den zurückgegebenen Inhalt aus:

$ resty -e 'local sock = ngx.socket.tcp()
sock:settimeout(1000) -- ein Sekunden-Timeout
local ok, err = sock:connect("api7.ai", 80)
if not ok then
    ngx.say("failed to connect: ", err)
    return
end
local req_data = "GET / HTTP/1.1\r\nHost: api7.ai\r\n\r\n"
local bytes, err = sock:send(req_data)
if err then
    ngx.say("failed to send: ", err)
    return
end
local data, err, partial = sock:receive()
if err then
    ngx.say("failed to receive: ", err)
    return
end
sock:close()
ngx.say("response is: ", data)'

Lassen Sie uns diesen Code im Detail analysieren.

  • Zuerst wird ein TCP-cosocket-Objekt mit dem Namen sock mit ngx.socket.tcp() erstellt.
  • Dann wird mit settimeout() das Timeout auf 1 Sekunde gesetzt. Beachten Sie, dass das Timeout hier nicht zwischen Verbinden und Empfangen unterscheidet; es ist eine einheitliche Einstellung.
  • Als Nächstes wird die connect()-API verwendet, um eine Verbindung zu Port 80 der angegebenen Website herzustellen und bei einem Fehler zu beenden.
  • Wenn die Verbindung erfolgreich ist, wird send() verwendet, um die konstruierten Daten zu senden und bei einem Fehler zu beenden.
  • Wenn die Daten erfolgreich gesendet wurden, wird receive() verwendet, um die Daten von der Website zu empfangen. Hier ist der Standardparameter von receive() *l, was bedeutet, dass nur die erste Zeile der Daten zurückgegeben wird. Wenn der Parameter auf *a gesetzt ist, werden Daten empfangen, bis die Verbindung geschlossen ist.
  • Schließlich wird close() aufgerufen, um die Socket-Verbindung aktiv zu schließen.

Wie Sie sehen können, ist die Verwendung der cosocket-APIs für die Netzwerkkommunikation in nur wenigen Schritten einfach. Lassen Sie uns einige Anpassungen vornehmen, um das Beispiel tiefer zu erkunden.

1. Setzen Sie die Timeout-Zeit für jede der drei Aktionen: Socket-Verbindung, Senden und Lesen.

Das settimeout(), das wir verwendet haben, setzt die Timeout-Zeit auf einen einzelnen Wert. Um die Timeout-Zeit separat zu setzen, müssen Sie die Funktion settimeouts() verwenden, wie im Folgenden gezeigt.

sock:settimeouts(1000, 2000, 3000)

Die Parameter von settimeouts sind in Millisekunden. Diese Codezeile gibt ein Verbindungs-Timeout von 1 Sekunde, ein Sende-Timeout von 2 Sekunden und ein Lese-Timeout von 3 Sekunden an.

In den OpenResty- und lua-resty-Bibliotheken sind die meisten Parameter der zeitbezogenen APIs in Millisekunden. Es gibt jedoch Ausnahmen, auf die Sie besonders achten müssen, wenn Sie sie aufrufen.

2. Empfangen Sie die Inhalte der angegebenen Größe.

Wie ich gerade sagte, kann die receive()-API eine Zeile von Daten empfangen oder kontinuierlich Daten empfangen. Wenn Sie jedoch nur Daten von 10K Größe empfangen möchten, wie sollten Sie das einrichten?

Hier kommt receiveany() ins Spiel. Es wurde entwickelt, um diesen Bedarf zu decken, also schauen Sie sich die folgende Codezeile an.

local data, err, partial = sock:receiveany(10240)

Dieser Code bedeutet, dass nur bis zu 10K Daten empfangen werden.

Natürlich ist eine weitere allgemeine Benutzeranforderung für receive(), Daten weiter zu empfangen, bis sie auf die angegebene Zeichenfolge stoßen.

Das receiveuntil() wurde entwickelt, um diese Art von Problem zu lösen. Es gibt keinen String wie receive() und receiveany() zurück, sondern einen Iterator. Auf diese Weise können Sie es in einer Schleife aufrufen, um die übereinstimmenden Daten in Segmenten zu lesen und nil zurückzugeben, wenn das Lesen abgeschlossen ist. Hier ist ein Beispiel.

local reader = sock:receiveuntil("\r\n")
     while true do
         local data, err, partial = reader(4)
         if not data then
             if err then
                 ngx.say("failed to read the data stream: ", err)
                 break
             end
             ngx.say("read done")
             break
         end
         ngx.say("read chunk: [", data, "]")
     end

Das receiveuntil gibt die Daten vor \r\n zurück und liest vier Bytes davon auf einmal durch den Iterator.

3. Anstatt den Socket direkt zu schließen, legen Sie ihn in den Verbindungspool.

Wie wir wissen, muss ohne Verbindungspool jedes Mal eine neue Verbindung erstellt werden, wodurch cosocket-Objekte jedes Mal erstellt werden, wenn eine Anfrage eingeht, und häufig zerstört werden, was zu unnötigem Leistungsverlust führt.

Um dieses Problem zu vermeiden, können Sie nach der Verwendung eines cosocket setkeepalive() aufrufen, um es in den Verbindungspool zu legen, wie im Folgenden gezeigt.

local ok, err = sock:setkeepalive(2 * 1000, 100)
if not ok then
    ngx.say("failed to set reusable: ", err)
end

Dieser Code setzt die Leerlaufzeit der Verbindung auf 2 Sekunden und die Größe des Verbindungspools auf 100, sodass beim Aufruf der connect()-Funktion das cosocket-Objekt zuerst aus dem Verbindungspool geholt wird.

Es gibt jedoch zwei Dinge, auf die wir achten müssen, wenn wir Verbindungspooling verwenden.

  • Erstens können Sie keine fehlerhafte Verbindung in den Verbindungspool legen. Andernfalls wird sie beim nächsten Mal, wenn Sie sie verwenden, fehlschlagen, Daten zu senden und zu empfangen. Dies ist einer der Gründe, warum wir feststellen müssen, ob jeder API-Aufruf erfolgreich ist oder nicht.
  • Zweitens müssen wir die Anzahl der Verbindungen herausfinden. Verbindungspooling ist Worker-basiert, und jeder Worker hat seinen eigenen Verbindungspool. Wenn Sie 10 Workers haben und die Größe des Verbindungspools auf 30 gesetzt ist, sind das 300 Verbindungen für den Backend-Dienst.

Zusammenfassung

Zusammenfassend haben wir die Grundkonzepte, die zugehörigen Befehle und die APIs von cosocket gelernt. Ein praktisches Beispiel hat uns vertraut gemacht, wie man TCP-bezogene APIs verwendet. Die Verwendung von UDP und Unix Domain Socket ist ähnlich wie die von TCP. Sie können alle diese Fragen leicht handhaben, nachdem Sie verstanden haben, was wir heute gelernt haben.

Wir wissen, dass cosocket relativ einfach zu verwenden ist, und wir können durch seine gute Verwendung eine Verbindung zu verschiedenen externen Diensten herstellen.

Abschließend können wir uns zwei Fragen stellen.

Die erste Frage, im heutigen Beispiel sendet tcpsock:send einen String; was ist, wenn wir eine Tabelle aus Strings senden müssen?

Die zweite Frage, wie Sie sehen können, kann cosocket in vielen Phasen nicht verwendet werden. Können Sie sich einige Möglichkeiten vorstellen, dies zu umgehen?

Bitte hinterlassen Sie einen Kommentar und teilen Sie ihn mit mir. Willkommen, diesen Artikel mit Ihren Kollegen und Freunden zu teilen, damit wir gemeinsam kommunizieren und Fortschritte erzielen können.