Flask + uWSGI mit nur "einem" dauerhaften Backgroud Task/Programm (threading)
Verfasst: Donnerstag 12. Oktober 2023, 13:45
Um es gleich vorweg zu nehmen. Ich bin auf eine Lösung gekommen (kommt am Ende). Auch wenn ich es lieber direkt in Python integriert hätte. (ohne abhängigkeit von uWSGI)
Jetzt etwas ausführlicher, sollte jemand das gleiche Problem haben.
Grobe Sache um was es geht "Stichworte" damit der Nächste weiß ob es passt.
Externer PostgreSQL-Server, Reverse SSH-Tunnel, Background-Task, Mailversand, Python, SQLite, uWSGI, Flask, nginx.
Ein schon kleines fertiges Tool/Programm. Läuft in einer Endlosschleife (Pause mit sleep(xx)) und macht eine DB-Abfragen, versendet Mails, etc... (Background Task)
Um nun die Einstellungen zu sehen, bzw. um überhaupt zu sehen ob/wann etwas passiert/versendet wurde, baue im um dieses Tool ein Frondend mit Flask.
Und hier sollte es eigentlich nur als ein Background Task im Hintergrund weiterlaufen.
Struktur:
Es ist evtl. ein Tick von mir, aber ich versuche immer ein Programm/Projekt mit allen benötigten Dingen unter einem Hut zu bekommen. Beides gehört ja auch zueinander.
Und hier liegt auch das eigentliche Problem.
Bisher war es immer so, das ich mit einem Thread und Paramiko einen Reverse Tunnel bei Programmstart aufgebaut habe, scheint auch bisher gut funktioniert zu haben. (bis auf, siehe nächsten Absatz)
Da ich jetzt aber Tool Nr. xx geschrieben habe, habe ich den Reverse Tunnel ausgelagert. Als ein "systemd" Service für alle Tools, welchen diesen benötigt.
Bisher stabil und keine Fehler (mehr).
Ok, damit fällt ein Thread/Process nun aus vielen Programmen raus. Was die Sache einfacher und weniger fehleranfälliger macht... (
Bei einem Projekt wo ich viele "kleine" Programme/Tools zusammentrage, ist es seit dem letzten Tool welches auch einen Tunnel benötigt, öfters dazu gekommen, das der Frontend Aufruf über nginx/Flask nicht mehr funktioniert hat, glaube 504 (TimeOut).
Seitdem ich den Tunnel ausgelagert habe, läuft es Problemlos.
Auch hier wieder Flask/uWGSI/nginx und Threads...
Jetzt gab es ein neues Projekt...
Ich habe eine Lokale SQLite-DB in welcher ich Einstellungen Speicher, auf was reagiert werden soll. Ich rufe eine externe Postgres-DB über den nun vorhandenen System SSH Tunnel ab, prüfe auf besondere Gegebenheiten, trifft etwas zu versende ich eine Mail und Speicher den Versand in die lokale SQLite-DB (damit man weiß, wurde schon versendet).
Dies habe ich als erstes geschrieben und lief Problemlos. (auf der Windows Entwicklungsumgebung)
Dann ist es innerhalb 1-2 Wochen gewachsen, mit mehreren Settings welche man vornehmen kann, welche zutreffen können/müssen, um ein versenden der Mail zu veranlassen.
Den Ablauf eines Aufrufs habe ich mal gemessen. ~1,4 Sekunden für einen Durchlauf und dies Remote über noch einen extra SSH-Tunnel.
Direkt auf dem Produktiv System sicherlich 1 Sekunden oder weniger.
Also nichts, was Ressourcen Intensiv ist, oder was extram lange braucht.
Jetzt kam meine Idee, ich baue mit Flask ein kleines Frontend auf.
Dies liest die Lokale SQLite-DB (nur lesend) und zeigt alle aktuellen Einstellungen und ob Mails versendet wurden an.
Auch dies verlief/lief Problemlos.
Hier nun die erste Version... Die "******" Kennzeichen, an welcher Stelle ich versucht habe, das "thread/multiprocessing" einzubauen/zu starten.
Bis heute kann ich nicht mal sagen, wo "wirklich" dafür die richtige Stelle ist. Macht auch sicherlich einen unterschied wo es aufgerufen wird ...
main.py
__init__.py:
Wie sollte es auch anders sein.
Sobald ich es in das Produktiv System aufgenommen habe, startete alles, aber das Frontend (Flask) über uWSGI/nginx ließ sich nicht aufrufen...
Ich konnte nachvollziehen das der Thread startet und dieser hat auch gearbeitet. Aber das Frontend ließ sich nicht aufrufen.
Ein paar Mal ging es, konnte aber nicht mehr 100% nachvollziehen ob es direkt nach einem einfachen Neustart des systemd war, oder doch eine Einstellung welche ich vorgenommen habe.
Aber auch wenn es mal funktionierte, war es ein Glücksspiel, ob es auch so bleibt.
uWSGI Settings:
Also liegt es irgendwie damit zusammen, dass sobald ein "forken" ins Spiel kommt, es zu Problemen kommt.
Mal lief es mit irgendwelchen Settings, aber nach einem manuellen "restart" oder nach Ablauf von "max-worker-lifetime" ging es wieder nicht. War eher ein Glücksspiel.
Ich habe auch versucht das Threading an verschiedenen Stellen einzubauen. mal direkt in die "main.py", noch vor dem "def create_app():" in der __init__ oder eben im "def create_app():" Teil.
Dann habe ich eine Augenscheinliche einfache Lösung gefunden.
Ich ersetze threading mit multiprocessing, dafür muss man nicht viel ändern.
Auf dem Windows Entwicklungssystem gab es dann wieder Probleme, da das multiprocessing aus dem __init__.py teil unter Windows nicht lief.
Ok, "multiprocessing.Process" will unter Windows nur unter/aus __main__ starten.
Gesagt getan, es lief.
Ab auf das Produktiv System. Flask lief, aber der Hintergrundprozess lief nicht...
von __main__ wieder ins __init__.py verschoben.
Es lief!
Der Background Prozess lief und auch Flask ließ sich öffnen.
Damit dachte ich, die Lösung gefunden zu haben.
1 Stunde später ... geschuldet durch "max-worker-lifetime = 3600" gab es Fehlermeldungen ... 'can only join a child process'.
Was mich auch hier etwas stutzig machte, warum kam der Fehler bei "jedem" Worker auf.
Auch wenn es für mich danach ausgesehen hatte, das der Hintergrund Task nur 1x lief und es lief nur 1x.
Die Meldung aber machte mich stutzig.
Nachdem ich diesen Fehler das erste Mal gesehen hatte, da ließ sich auch das Frontend nicht mehr öffnen. Erst dadurch bin ich überhaupt auf diesen Fehler gestoßen.
Bei weiteren Tests mit einem kleineren Timer hatte ich bisher immer das Glück, der Frondend aufzurufen ging. Es wurde trotzdem dieser Fehler weiterhin produziert.
Daraufhin habe ich auch diese Lösung verworfen.
Durch Zufall bin ich gestern dann auf uWSGI "add_cron" bzw. besser für meinen Fall "add_timer" gestoßen.
Mit diesem läuft es seit gestern Problemlos durch.
Dies wird, wenn nicht anders angegeben nur 1x ausgeführt und ruft im angegebenen Intervall die watchdog auf.
Natürlich musste ich diese umschreiben, ohne das while True... aber dann macht es genau was es soll.
Heute um ca ~16Uhr sehe ich dann auch ob alles wirklich klappt.
Eine andere, bisher meine letzte Idee wäre gewesen, welche ich aber nur zu allerletzt aufgegriffen hätte.
Ich erstelle zwei Appliaktionen/systemd. Eines nur für den watchdog und eines für das Frontend/Flask.
Damit würde ich weder timer/thread noch multiprocessing benötigen.
-------
Ich kenne das Problem mit dem GIL und wirklichem "parallelen" ausführen. Sollte aber bei 1Sek ausführung "eigentlich" keine Probleme darstellen.
Auch kann ich nicht sagen, was das forken (im wirklichen Sinn/Prozess) genau macht. Aber hier irgendwo scheint der Fehler zu liegen.
Ich dachte wirklich, ein Prozess, welcher nur 1 bis max. 2 Sekunden Ausführungszeit benötigt, würde ich ohne Probleme in einem Thread verpacken können.
Oder ich habe aber irgendwo einen riesigen Leichtsinnsfehler gemacht?
Ich habe kein Problem damit, das es nicht 100% parallel läuft. Die 1-2sek delay bei einem Aufruf des Frondend, sollte man genau diesen Zeitpunkt erwischen, stellen überhaupt kein Problem dar.
Ich möchte aber gerne immer alles unter einem Hut behalten, was auch zusammengehört.
Ein ganz klein wenig stört mich da das "uwsgi.add_timer(99, 60)", da ich mich hier wieder von uwsgi abhängig mache.
Ich hab dann z.b. mal "Apscheduler" ausprobiert. Soll ja genau für das gemacht sein.
... ich habe genau geschaut was das scheduler.add_job macht.
Im Endeffekt macht es genau das gleiche, startet am Ende, wie ich oben den Thread/Process. Also würde mir solch ein Tool in diesem Fall nicht weiterhelfen.
Bis heute und ich habe viel dazu gelsen, konnte ich keine Beispiellösung dazu finden.
Ist Python dafür wirklich vielleicht die Falsche Sprache? Wie gesagt, es geht nicht um wirkliches paralleles ausführen.
Prinzipielles an dieser Geschichte. Gehört der Aufruf eines Thread/multiprocessing evtl. an eine andere Stelle, als dies jetzt oben steht? (z.b. doch vor dem vor dem create_app(), ... ?)
Und bevor jemand nun Apscheduler, Celery etc. voreilig in den Raum wirft.
Es macht einen unterschied wie die Applikation am Ende betrieben wird. Denn in der Entwicklungsumgebung, haben alle Möglichkeiten welche ich probiert haben, ohne Probleme Funktioniert.
Erst mit nginx/uwsgi auf dem Produktions-System kommt es zu diesen Fehler(n).
Und diese Tools würden am Ende den gleichen Aufruf machen, nur einfacher mit mehr Funktionen, aber den gleichen Fehler provozieren.
Es liegt an dem "forken" (im uwsgi teil). Die Dokumentation habe ich gelesen, nur verstehen tue ich es nicht. Kann mir jemand wirklich nur "einfach und kurz" erklären, was dies genau macht, was passiert da?
Dann verstehe ich auch vielleicht den Fehler/ das Problem besser, da es damit zusammenhängt.
Mache ich nämlich ein "lazy-app=true" so läuft es, wenn auch nicht wie gewünscht nur 1x, sondern eben abhängig wieviele worker aktiv sind.
Würdet Ihr dies überhaupt eher als ein "Problem" seitens uWSGI einordnen?
Oder es ist wirklich nur eine Kleinigkeit und es fehlt nur an 1-2 Zeilen?
Viele Grüße
Chris
Jetzt etwas ausführlicher, sollte jemand das gleiche Problem haben.
Grobe Sache um was es geht "Stichworte" damit der Nächste weiß ob es passt.
Externer PostgreSQL-Server, Reverse SSH-Tunnel, Background-Task, Mailversand, Python, SQLite, uWSGI, Flask, nginx.
Ein schon kleines fertiges Tool/Programm. Läuft in einer Endlosschleife (Pause mit sleep(xx)) und macht eine DB-Abfragen, versendet Mails, etc... (Background Task)
Um nun die Einstellungen zu sehen, bzw. um überhaupt zu sehen ob/wann etwas passiert/versendet wurde, baue im um dieses Tool ein Frondend mit Flask.
Und hier sollte es eigentlich nur als ein Background Task im Hintergrund weiterlaufen.
Struktur:
Code: Alles auswählen
main.py
|
app/ -
|
__init__.py
watchdog.py # Das eigentliche "Programm". Der Rest ist nur für das Frontend da.
frontend/ -
|
static/
template/
routes.py
Es ist evtl. ein Tick von mir, aber ich versuche immer ein Programm/Projekt mit allen benötigten Dingen unter einem Hut zu bekommen. Beides gehört ja auch zueinander.
Und hier liegt auch das eigentliche Problem.
Bisher war es immer so, das ich mit einem Thread und Paramiko einen Reverse Tunnel bei Programmstart aufgebaut habe, scheint auch bisher gut funktioniert zu haben. (bis auf, siehe nächsten Absatz)
Da ich jetzt aber Tool Nr. xx geschrieben habe, habe ich den Reverse Tunnel ausgelagert. Als ein "systemd" Service für alle Tools, welchen diesen benötigt.
Bisher stabil und keine Fehler (mehr).
Ok, damit fällt ein Thread/Process nun aus vielen Programmen raus. Was die Sache einfacher und weniger fehleranfälliger macht... (
Bei einem Projekt wo ich viele "kleine" Programme/Tools zusammentrage, ist es seit dem letzten Tool welches auch einen Tunnel benötigt, öfters dazu gekommen, das der Frontend Aufruf über nginx/Flask nicht mehr funktioniert hat, glaube 504 (TimeOut).
Seitdem ich den Tunnel ausgelagert habe, läuft es Problemlos.
Auch hier wieder Flask/uWGSI/nginx und Threads...
Jetzt gab es ein neues Projekt...
Ich habe eine Lokale SQLite-DB in welcher ich Einstellungen Speicher, auf was reagiert werden soll. Ich rufe eine externe Postgres-DB über den nun vorhandenen System SSH Tunnel ab, prüfe auf besondere Gegebenheiten, trifft etwas zu versende ich eine Mail und Speicher den Versand in die lokale SQLite-DB (damit man weiß, wurde schon versendet).
Dies habe ich als erstes geschrieben und lief Problemlos. (auf der Windows Entwicklungsumgebung)
Dann ist es innerhalb 1-2 Wochen gewachsen, mit mehreren Settings welche man vornehmen kann, welche zutreffen können/müssen, um ein versenden der Mail zu veranlassen.
Den Ablauf eines Aufrufs habe ich mal gemessen. ~1,4 Sekunden für einen Durchlauf und dies Remote über noch einen extra SSH-Tunnel.
Direkt auf dem Produktiv System sicherlich 1 Sekunden oder weniger.
Also nichts, was Ressourcen Intensiv ist, oder was extram lange braucht.
Jetzt kam meine Idee, ich baue mit Flask ein kleines Frontend auf.
Dies liest die Lokale SQLite-DB (nur lesend) und zeigt alle aktuellen Einstellungen und ob Mails versendet wurden an.
Auch dies verlief/lief Problemlos.
Hier nun die erste Version... Die "******" Kennzeichen, an welcher Stelle ich versucht habe, das "thread/multiprocessing" einzubauen/zu starten.
Bis heute kann ich nicht mal sagen, wo "wirklich" dafür die richtige Stelle ist. Macht auch sicherlich einen unterschied wo es aufgerufen wird ...
main.py
Code: Alles auswählen
from app import app
if __name__ == "__main__":
******
app.run()
__init__.py:
Code: Alles auswählen
import xxxx
******
def create_app():
app_ini = Flask(__name__)
******
# Hier die Überwachung ... Eine While True Schleife welche ich mit sleep(60) alle 1 min Aufrufe. (der Aufruf selbst ~1 Sekunde)
from app import watchdog
t_watchdog = Thread(target=watchdog.monitoring, args=(SSH_LOCAL_PORT,), name="background_watchdog", daemon=True)
t_watchdog.start()
# Hier rufe ich das Blueprint für das Frontend auf
from .status.routes import status_bp
app_ini.register_blueprint(status_bp, url_prefix='/status')
return app_ini
******
app = create_app()
Sobald ich es in das Produktiv System aufgenommen habe, startete alles, aber das Frontend (Flask) über uWSGI/nginx ließ sich nicht aufrufen...
Ich konnte nachvollziehen das der Thread startet und dieser hat auch gearbeitet. Aber das Frontend ließ sich nicht aufrufen.
Ein paar Mal ging es, konnte aber nicht mehr 100% nachvollziehen ob es direkt nach einem einfachen Neustart des systemd war, oder doch eine Einstellung welche ich vorgenommen habe.
Aber auch wenn es mal funktionierte, war es ein Glücksspiel, ob es auch so bleibt.
uWSGI Settings:
Code: Alles auswählen
[uwsgi]
strict = true
module = main:app
; ************************
Hier habe ich unzählige Einstellungen probiert.
Das Einfachste wäre ein lazy-apps=true. Damit ließ sich das Flask Frontend ohne Probleme aufrufen. Dann hätte ich aber diesen Background Task in "allen" workern gehabt. Würde also 4x laufen, was nicht seien muss und ist auch keine Lösung mit der ich mich zufriedengebe.
enable-threads = true/false
Master = true/false
Process = xx
threads = xx
enable-threads = true ; sobald Threads in Python ins spiel kommen, muss dies aktiviert sein. Sonst würden der/die Threads in Python nicht starten.
master = true
processes = 4
threads = 2
;lazy-apps=true ; Funktioniert. Läd aber die "Anwendung" komplett in jeden "Worker/process". Somit würde dieser "Watchdog" auch 4x laufen. Dies ist aber keine Lösung, es sollte nur 1x laufen.
; ************************
callable = app
socket = watchdog_app.socket
chmod-socket = 660
vacuum = tue
die-on-term = true
need-app = true
harakiri = 240
; ************************
max-worker-lifetime = 3600 ; Restart workers after xxxx seconds
; ************************
Mal lief es mit irgendwelchen Settings, aber nach einem manuellen "restart" oder nach Ablauf von "max-worker-lifetime" ging es wieder nicht. War eher ein Glücksspiel.
Ich habe auch versucht das Threading an verschiedenen Stellen einzubauen. mal direkt in die "main.py", noch vor dem "def create_app():" in der __init__ oder eben im "def create_app():" Teil.
Dann habe ich eine Augenscheinliche einfache Lösung gefunden.
Ich ersetze threading mit multiprocessing, dafür muss man nicht viel ändern.
Auf dem Windows Entwicklungssystem gab es dann wieder Probleme, da das multiprocessing aus dem __init__.py teil unter Windows nicht lief.
Code: Alles auswählen
RuntimeError:
Attempt to start a new process before the current process
has finished its bootstrapping phase.
Gesagt getan, es lief.
Ab auf das Produktiv System. Flask lief, aber der Hintergrundprozess lief nicht...
von __main__ wieder ins __init__.py verschoben.
Es lief!
Der Background Prozess lief und auch Flask ließ sich öffnen.
Damit dachte ich, die Lösung gefunden zu haben.
1 Stunde später ... geschuldet durch "max-worker-lifetime = 3600" gab es Fehlermeldungen ... 'can only join a child process'.
Was mich auch hier etwas stutzig machte, warum kam der Fehler bei "jedem" Worker auf.
Auch wenn es für mich danach ausgesehen hatte, das der Hintergrund Task nur 1x lief und es lief nur 1x.
Die Meldung aber machte mich stutzig.
Code: Alles auswählen
...
Okt 11 13:29:31 app_srv uwsgi[974038]: Respawned uWSGI worker 2 (new pid: 974379)
Okt 11 13:29:31 app_srv uwsgi[974038]: Respawned uWSGI worker 1 (new pid: 974378)
Okt 11 13:29:30 app_srv uwsgi[974350]: AssertionError: can only join a child process
Okt 11 13:29:30 app_srv uwsgi[974350]: assert self._parent_pid == os.getpid(), 'can only join a child process'
Okt 11 13:29:30 app_srv uwsgi[974350]: File "/usr/lib/python3.9/multiprocessing/process.py", line 147, in join
Okt 11 13:29:30 app_srv uwsgi[974350]: p.join()
Okt 11 13:29:30 app_srv uwsgi[974350]: File "/usr/lib/python3.9/multiprocessing/util.py", line 357, in _exit_function
Okt 11 13:29:30 app_srv uwsgi[974350]: Traceback (most recent call last):
Okt 11 13:29:30 app_srv uwsgi[974350]: Error in atexit._run_exitfuncs:
... DIESER FEHLER TEIL 4x. FÜR JEDEN WORKER 1x. ...
Okt 11 13:29:30 app_srv uwsgi[974352]: Error in atexit._run_exitfuncs:
Okt 11 13:29:30 app_srv uwsgi[974038]: worker 4 lifetime reached, it was running for 61 second(s)
Okt 11 13:29:30 app_srv uwsgi[974038]: worker 3 lifetime reached, it was running for 61 second(s)
...
Bei weiteren Tests mit einem kleineren Timer hatte ich bisher immer das Glück, der Frondend aufzurufen ging. Es wurde trotzdem dieser Fehler weiterhin produziert.
Daraufhin habe ich auch diese Lösung verworfen.
Durch Zufall bin ich gestern dann auf uWSGI "add_cron" bzw. besser für meinen Fall "add_timer" gestoßen.
Code: Alles auswählen
import uwsgi
from app import watchdog
uwsgi.register_signal(99, "", watchdog.monitoring)
uwsgi.add_timer(99, 60)
ef create_app():
app_ini = Flask(__name__)
....
Dies wird, wenn nicht anders angegeben nur 1x ausgeführt und ruft im angegebenen Intervall die watchdog auf.
Natürlich musste ich diese umschreiben, ohne das while True... aber dann macht es genau was es soll.
Heute um ca ~16Uhr sehe ich dann auch ob alles wirklich klappt.
Eine andere, bisher meine letzte Idee wäre gewesen, welche ich aber nur zu allerletzt aufgegriffen hätte.
Ich erstelle zwei Appliaktionen/systemd. Eines nur für den watchdog und eines für das Frontend/Flask.
Damit würde ich weder timer/thread noch multiprocessing benötigen.
-------
Ich kenne das Problem mit dem GIL und wirklichem "parallelen" ausführen. Sollte aber bei 1Sek ausführung "eigentlich" keine Probleme darstellen.
Auch kann ich nicht sagen, was das forken (im wirklichen Sinn/Prozess) genau macht. Aber hier irgendwo scheint der Fehler zu liegen.
Ich dachte wirklich, ein Prozess, welcher nur 1 bis max. 2 Sekunden Ausführungszeit benötigt, würde ich ohne Probleme in einem Thread verpacken können.
Oder ich habe aber irgendwo einen riesigen Leichtsinnsfehler gemacht?
Ich habe kein Problem damit, das es nicht 100% parallel läuft. Die 1-2sek delay bei einem Aufruf des Frondend, sollte man genau diesen Zeitpunkt erwischen, stellen überhaupt kein Problem dar.
Ich möchte aber gerne immer alles unter einem Hut behalten, was auch zusammengehört.
Ein ganz klein wenig stört mich da das "uwsgi.add_timer(99, 60)", da ich mich hier wieder von uwsgi abhängig mache.
Ich hab dann z.b. mal "Apscheduler" ausprobiert. Soll ja genau für das gemacht sein.
Code: Alles auswählen
scheduler = BlockingScheduler()
scheduler.add_job(func=my_job, trigger='interval', seconds=2, id='my custom task')
scheduler.start()
Im Endeffekt macht es genau das gleiche, startet am Ende, wie ich oben den Thread/Process. Also würde mir solch ein Tool in diesem Fall nicht weiterhelfen.
Bis heute und ich habe viel dazu gelsen, konnte ich keine Beispiellösung dazu finden.
Ist Python dafür wirklich vielleicht die Falsche Sprache? Wie gesagt, es geht nicht um wirkliches paralleles ausführen.
Prinzipielles an dieser Geschichte. Gehört der Aufruf eines Thread/multiprocessing evtl. an eine andere Stelle, als dies jetzt oben steht? (z.b. doch vor dem vor dem create_app(), ... ?)
Und bevor jemand nun Apscheduler, Celery etc. voreilig in den Raum wirft.
Es macht einen unterschied wie die Applikation am Ende betrieben wird. Denn in der Entwicklungsumgebung, haben alle Möglichkeiten welche ich probiert haben, ohne Probleme Funktioniert.
Erst mit nginx/uwsgi auf dem Produktions-System kommt es zu diesen Fehler(n).
Und diese Tools würden am Ende den gleichen Aufruf machen, nur einfacher mit mehr Funktionen, aber den gleichen Fehler provozieren.
Es liegt an dem "forken" (im uwsgi teil). Die Dokumentation habe ich gelesen, nur verstehen tue ich es nicht. Kann mir jemand wirklich nur "einfach und kurz" erklären, was dies genau macht, was passiert da?
Dann verstehe ich auch vielleicht den Fehler/ das Problem besser, da es damit zusammenhängt.
Mache ich nämlich ein "lazy-app=true" so läuft es, wenn auch nicht wie gewünscht nur 1x, sondern eben abhängig wieviele worker aktiv sind.
Würdet Ihr dies überhaupt eher als ein "Problem" seitens uWSGI einordnen?
Oder es ist wirklich nur eine Kleinigkeit und es fehlt nur an 1-2 Zeilen?
Viele Grüße
Chris