Jenseits des Web-Servers: Privilegierte Prozesse und Timer-Tasks

API7.ai

November 3, 2022

OpenResty (NGINX + Lua)

Im vorherigen Artikel haben wir die OpenResty-APIs, shared dict und cosocket vorgestellt, die alle Funktionen im Bereich von NGINX und Webservern implementieren und einen programmierbaren Webserver bieten, der kostengünstiger und einfacher zu warten ist.

Allerdings kann OpenResty noch mehr. Lassen Sie uns heute einige Funktionen in OpenResty vorstellen, die über den Webserver hinausgehen. Es handelt sich um Timer-Aufgaben, privilegierte Prozesse und nicht-blockierendes ngx.pipe.

Timer-Aufgaben

In OpenResty müssen wir manchmal regelmäßig bestimmte Aufgaben im Hintergrund ausführen, wie z.B. das Synchronisieren von Daten oder das Bereinigen von Protokollen. Wie würden Sie das gestalten? Der einfachste Ansatz wäre, eine API-Schnittstelle bereitzustellen, die diese Aufgaben ausführt, und dann das System-crontab zu verwenden, um in regelmäßigen Abständen curl aufzurufen und diese Schnittstelle zu nutzen, um die Anforderung indirekt zu erfüllen.

Dies wäre jedoch nicht nur fragmentiert, sondern würde auch die Komplexität der Wartung erhöhen. Daher bietet OpenResty ngx.timer, um diese Art von Anforderung zu lösen. Sie können sich ngx.timer als einen von OpenResty simulierten Client-Request vorstellen, der die entsprechende Callback-Funktion auslöst.

Die Timer-Aufgaben in OpenResty können in die folgenden zwei Typen unterteilt werden:

  • ngx.timer.at wird verwendet, um einmalige Timer-Aufgaben auszuführen.
  • ngx.timer.every wird verwendet, um periodische Timer-Aufgaben auszuführen.

Erinnern Sie sich an die nachdenkliche Frage, die ich am Ende des letzten Artikels gestellt habe? Die Frage war, wie man die Einschränkung umgehen kann, dass cosocket nicht in init_worker_by_lua verwendet werden kann, und die Antwort lautet ngx.timer.

Der folgende Code startet eine Timer-Aufgabe mit einer Verzögerung von 0. Er startet die Callback-Funktion handler, und in dieser Funktion wird cosocket verwendet, um auf eine Website zuzugreifen.

init_worker_by_lua_block {
    local function handler()
        local sock = ngx.socket.tcp()
        local ok, err = sock:connect(“api7.ai", 80)
    end

    local ok, err = ngx.timer.at(0, handler)
}

Auf diese Weise umgehen wir die Einschränkung, dass cosocket in dieser Phase nicht verwendet werden kann.

Zurück zu der Benutzeranforderung, die wir zu Beginn dieses Abschnitts erwähnt haben: ngx.timer.at erfüllt nicht die Anforderung, periodisch ausgeführt zu werden; im obigen Codebeispiel handelt es sich um eine einmalige Aufgabe.

Wie können wir dies also periodisch tun? Sie scheinen zwei Optionen basierend auf der ngx.timer.at-API zu haben:

  • Sie können die periodische Aufgabe selbst implementieren, indem Sie eine while true-Endlosschleife in der Callback-Funktion verwenden, die nach der Ausführung der Aufgabe eine Weile sleept.
  • Sie können auch am Ende der Callback-Funktion einen neuen Timer erstellen.

Bevor Sie jedoch eine Wahl treffen, müssen wir eines klären: Der Timer ist im Wesentlichen ein Request, obwohl dieser Request nicht vom Client initiiert wurde. Für den Request muss er nach Abschluss seiner Aufgabe beendet werden und kann nicht dauerhaft aktiv bleiben. Andernfalls kann dies leicht zu verschiedenen Arten von Ressourcenlecks führen.

Daher ist die erste Lösung, periodische Aufgaben mit while true zu implementieren, unzuverlässig. Die zweite Lösung ist machbar, erstellt jedoch rekursiv Timer, was nicht leicht zu verstehen ist.

Gibt es also eine bessere Lösung? Die neue ngx.timer.every-API in OpenResty wurde speziell entwickelt, um dieses Problem zu lösen, und ist eine Lösung, die näher an crontab liegt.

Der Nachteil ist, dass Sie eine Timer-Aufgabe nach dem Start nie mehr abbrechen können. Schließlich ist ngx.timer.cancel immer noch eine To-Do-Funktion.

An diesem Punkt stehen Sie vor einem Problem: Der Timer läuft im Hintergrund und kann nicht abgebrochen werden; wenn es viele Timer gibt, können die Systemressourcen leicht erschöpft sein.

Daher bietet OpenResty zwei Direktiven, lua_max_pending_timers und lua_max_running_timers, um sie zu begrenzen. Ersteres stellt die maximale Anzahl von Timern dar, die auf die Ausführung warten, und Letzteres die maximale Anzahl der derzeit laufenden Timer.

Sie können auch die Lua-API verwenden, um die Werte der derzeit wartenden und laufenden Timer-Aufgaben zu erhalten, wie in den folgenden beiden Beispielen gezeigt.

content_by_lua_block {
    ngx.timer.at(3, function() end)
    ngx.say(ngx.timer.pending_count())
}

Dieser Code gibt eine 1 aus, was bedeutet, dass eine geplante Aufgabe auf die Ausführung wartet.

content_by_lua_block {
    ngx.timer.at(0.1, function() ngx.sleep(0.3) end)
    ngx.sleep(0.2)
    ngx.say(ngx.timer.running_count())
}

Dieser Code gibt eine 1 aus, was bedeutet, dass eine geplante Aufgabe ausgeführt wird.

Privilegierter Prozess

Als Nächstes betrachten wir den privilegierten Prozess. Wie wir alle wissen, ist NGINX in Master-Prozess und Worker-Prozesse unterteilt, wobei die Worker-Prozesse Benutzeranfragen bearbeiten. Wir können den Typ des Prozesses über die in lua-resty-core bereitgestellte process.type-API erhalten. Zum Beispiel können Sie resty verwenden, um die folgende Funktion auszuführen.

$ resty -e 'local process = require "ngx.process"
ngx.say("process type:", process.type())'

Sie werden sehen, dass es ein single-Ergebnis zurückgibt und nicht worker, was bedeutet, dass resty NGINX mit einem Worker-Prozess startet, nicht mit einem Master-Prozess. Dies ist wahr. In der resty-Implementierung können Sie sehen, dass der Master-Prozess mit einer Zeile wie dieser deaktiviert wird.

master_process off;

OpenResty erweitert NGINX durch das Hinzufügen eines privileged agent. Der privilegierte Prozess hat die folgenden besonderen Merkmale:

  • Er überwacht keine Ports, was bedeutet, dass er keine Dienste nach außen bereitstellt.

  • Er hat die gleichen Privilegien wie der Master-Prozess, was in der Regel die Privilegien des root-Benutzers sind, was es ihm ermöglicht, viele Aufgaben auszuführen, die für den Worker-Prozess unmöglich sind.

  • Der privilegierte Prozess kann nur im init_by_lua-Kontext geöffnet werden.

  • Außerdem macht der privilegierte Prozess nur Sinn, wenn er im init_worker_by_lua-Kontext läuft, da keine Anfragen ausgelöst werden und sie nicht in die content-, access- usw. Kontexte gelangen.

Schauen wir uns ein Beispiel für einen privilegierten Prozess an, der geöffnet wird.

init_by_lua_block {
    local process = require "ngx.process"

    local ok, err = process.enable_privileged_agent()
    if not ok then
        ngx.log(ngx.ERR, "enables privileged agent failed error:", err)
    end
}

Nachdem der privilegierte Prozess mit diesem Code geöffnet und der OpenResty-Dienst gestartet wurde, können wir sehen, dass der privilegierte Prozess jetzt Teil des NGINX-Prozesses ist.

nginx: master process
nginx: worker process
nginx: privileged agent process

Wenn jedoch Privilegien nur einmal während der init_worker_by_lua-Phase ausgeführt werden, was keine gute Idee ist, wie sollten wir dann den privilegierten Prozess auslösen?

Ja, die Antwort liegt in dem gerade vermittelten Wissen. Da er keine Ports überwacht, d.h. er kann nicht durch Terminalanfragen ausgelöst werden, ist die einzige Möglichkeit, ihn periodisch auszulösen, die Verwendung des gerade vorgestellten ngx.timer:

init_worker_by_lua_block {
    local process = require "ngx.process"

    local function reload(premature)
        local f, err = io.open(ngx.config.prefix() .. "/logs/nginx.pid", "r")
        if not f then
            return
        end
        local pid = f:read()
        f:close()
        os.execute("kill -HUP " .. pid)
    end

    if process.type() == "privileged agent" then
         local ok, err = ngx.timer.every(5, reload)
        if not ok then
            ngx.log(ngx.ERR, err)
        end
    end
}

Der obige Code implementiert die Fähigkeit, alle 5 Sekunden HUP-Signale an den Master-Prozess zu senden. Natürlich können Sie darauf aufbauen, um noch spannendere Dinge zu tun, wie z.B. die Datenbank abzufragen, um zu sehen, ob es Aufgaben für den privilegierten Prozess gibt, und diese auszuführen. Da der privilegierte Prozess root-Privilegien hat, ist dies offensichtlich ein bisschen wie ein "Hintertür"-Programm.

Nicht-blockierendes ngx.pipe

Schließlich betrachten wir das nicht-blockierende ngx.pipe, das die Standardbibliothek von Lua verwendet, um einen externen Befehl auszuführen, der in dem gerade beschriebenen Codebeispiel ein Signal an den Master-Prozess sendet.

os.execute("kill -HUP " .. pid)

Natürlich blockiert dieser Vorgang. Gibt es also eine nicht-blockierende Möglichkeit, externe Programme in OpenResty aufzurufen? Schließlich wissen Sie, dass Sie, wenn Sie OpenResty als vollständige Entwicklungsplattform und nicht als Webserver verwenden, dies benötigen. Aus diesem Grund wurde die lua-resty-shell-Bibliothek erstellt, und ihre Verwendung zum Aufrufen der Befehlszeile ist nicht-blockierend:

$ resty -e 'local shell = require "resty.shell"
local ok, stdout, stderr, reason, status =
    shell.run([[echo "hello, world"]])
    ngx.say(stdout)

Dieser Code ist eine andere Art, hello world zu schreiben, indem der Systembefehl echo verwendet wird, um die Ausgabe zu erledigen. Ebenso können Sie resty.shell als Alternative zum os.execute-Aufruf in Lua verwenden.

Wir wissen, dass die zugrunde liegende Implementierung von lua-resty-shell auf der ngx.pipe-API in lua-resty-core basiert, daher würde dieses Beispiel, das lua-resty-shell verwendet, um hello world auszugeben, mit ngx.pipe so aussehen.

$ resty -e 'local ngx_pipe = require "ngx.pipe"
local proc = ngx_pipe.spawn({"echo", "hello world"})
local data, err = proc:stdout_read_line()
ngx.say(data)'

Das obige ist der zugrunde liegende Code der lua-resty-shell-Implementierung. Sie können die ngx.pipe-Dokumentation und Testfälle überprüfen, um mehr über die Verwendung zu erfahren. Daher werde ich hier nicht weiter darauf eingehen.

Zusammenfassung

Das war's. Wir haben den Hauptinhalt für heute abgeschlossen. Aus den oben genannten Funktionen können wir sehen, dass OpenResty auch versucht, sich in Richtung einer universellen Plattform zu bewegen, während es ein besserer NGINX wird, in der Hoffnung, dass Entwickler versuchen können, den Technologie-Stack zu vereinheitlichen und OpenResty zu verwenden, um ihre Entwicklungsanforderungen zu lösen. Dies ist ziemlich freundlich für den Betrieb und die Wartung, da die Wartungskosten niedriger sind, solange Sie ein OpenResty darauf bereitstellen.

Abschließend hinterlasse ich Ihnen eine nachdenkliche Frage. Da es möglicherweise mehrere NGINX-Worker gibt, wird der Timer einmal für jeden Worker ausgeführt, was in den meisten Szenarien nicht akzeptabel ist. Wie können wir sicherstellen, dass der Timer nur einmal ausgeführt wird?

Hinterlassen Sie gerne einen Kommentar mit Ihrer Lösung und teilen Sie diesen Artikel gerne mit Ihren Kollegen und Freunden, damit wir gemeinsam kommunizieren und uns verbessern können.