KeepAlive im Verbindungspool
Beide Pool-Clients können ihre Leerlaufverbindungen in einem festen Intervall mit dem Serverjob krn.CheckServerConnection anpingen. Der Job ist serverseitig nur ein Zähler, der über die bestehende Session läuft — das billigste verfügbare Lebenszeichen. Er hält NAT-, Firewall- und Idle-Timeouts offen und deckt still weggeräumte Sockets auf, weil die Schreib- oder Leseoperation dann fehlschlägt.
Ohne KeepAlive fällt eine tote Verbindung im Sync-Pool erst beim nächsten fachlichen Aufruf auf: dieser Aufruf schlägt fehl, der Pool wirft die Verbindung weg, und erst der Aufruf danach läuft wieder. Der Async-Pool erfasst jeden Verlust, den der Kernel beobachten kann (Server oder ein Gerät dazwischen schliesst den Socket), dagegen sofort über das connection_lost-Ereignis des Transports — auch ohne KeepAlive. Das KeepAlive ist das verbleibende Sicherheitsnetz: Es fängt die Verluste, die der Kernel nicht sehen kann — weggeräumte Firewall-Regel, abgelaufener NAT-Eintrag — und ist zugleich der einzige Detektionsmechanismus des Sync-Pools.
1. Konfiguration
from ecmind_blue_client.pool import SyncPoolClient
pool = SyncPoolClient(
servers="server1:4000:1",
username="<user>",
password="<pass>",
keepalive_interval=300, # Sekunden
)
|
KeepAlive deaktiviert, es wird kein Hintergrund-Worker gestartet |
|
ebenfalls deaktiviert |
|
Sekunden zwischen zwei Durchläufen |
AsyncPoolClient nimmt denselben Parameter entgegen und verwendet statt eines Threads einen asyncio.Task.
Das Intervall richtet sich nach dem kürzesten Idle-Timeout auf dem Weg zum Server (Firewall, Loadbalancer, NAT-Gateway) und sollte deutlich darunter liegen. 300 Sekunden sind ein brauchbarer Startwert.
2. Ablauf eines Durchlaufs
-
Der Worker startet verzögert: erst wenn die erste Verbindung erzeugt wird. Ein Pool, der nie benutzt wird, erzeugt keinen Thread und keinen Task.
-
Das Intervall wird zwischen dem Ende eines Durchlaufs und dem Beginn des nächsten gemessen. Ein langsamer Durchlauf führt daher weder zu Aufholbursts noch zu überlappenden Durchläufen.
-
Pro Durchlauf wird die Anzahl der aktuell freien Verbindungen als Momentaufnahme genommen; genau so viele Verbindungen werden geprüft. Eine geprüfte und zurückgelegte Verbindung kann im selben Durchlauf nicht erneut geprüft werden.
-
Jede entnommene Verbindung wird mit
krn.CheckServerConnection(Flags=0) geprüft:Erfolg
Verbindung geht zurück in den Pool,
keepalive_countundlast_keepalive_atwerden hochgezähltFehler (Returncode
!= 0oder Netzwerkfehler)Verbindung wird geschlossen und aus der Buchführung entfernt
3. Sofort-Erkennung verlorener Verbindungen (Async-Pool)
Der Async-Pool beobachtet das connection_lost-Ereignis des Transports jeder gepoolten Verbindung. Schliesst der Server — oder ein Gerät dazwischen — den Socket, während die Verbindung im Pool liegt, bemerkt der Pool den Verlust im selben Moment, ohne auf den nächsten Ausleih oder Durchlauf zu warten:
-
Die Verbindung wird aus der Buchführung und aus der Leerlauf-Warteschlange entfernt; der nächste Aufruf baut transparent eine frische Verbindung auf.
-
Es erscheint eine
INFO-Zeile im Loggerecmind_blue_client.pool._async_pool_client(inklusive einmaligem Hinweis, das KeepAlive zu aktivieren, falls es deaktiviert ist).
Ist die Verbindung gerade ausgeliehen, wenn sie stirbt, wird der Verlust nur auf DEBUG protokolliert: Der aufrufende Code erhält den Verbindungsfehler ohnehin und entsorgt die Verbindung über seinen üblichen Ausnahme-Pfad.
Der Hook deckt vom Gegenpeer veranlasste Schliessungen ab — Server-Idle-Timeouts, Serverneustart, explizites Schliessen. Eine still weggeworfene Verbindung (weggeräumte Firewall-Regel, abgelaufener NAT-Eintrag) sieht er nicht, weil dann gar keine Schliessung den Socket erreicht; dafür ist das KeepAlive da. Der Sync-Pool hat dazu keine Event-Loop-Variante und verlässt sich auf den KeepAlive-Durchlauf.
Die beiden Mechanismen teilen den Fehlerraum: Der Hook reagiert sofort auf jeden Verlust, den der Kernel beobachten kann; das KeepAlive pollt für die Verluste, die der Kernel nicht sehen kann.
4. Eigenschaften
- Nur Leerlaufverbindungen
-
Eine gerade ausgeliehene Verbindung ist aus der Warteschlange heraus und für den Durchlauf unsichtbar. Laufende Jobs werden nie gestört.
- Keine Sperre über den RPC-Aufruf
-
Nur die Warteschlangen-Buchführung läuft unter dem Pool-Lock, der Serverjob selbst nicht. Ein hängender Server blockiert den Pool also nicht für die Dauer des Durchlaufs.
- Kein Neuaufbau im Durchlauf
-
Eine kaputte Verbindung wird nur geschlossen, nicht ersetzt. Der Pool schrumpft, statt kaputte Slots zu blockieren; der nächste Aufruf erzeugt bei Bedarf eine frische Verbindung.
- Kein Fehler nach aussen
-
Ausfälle landen ausschliesslich auf
logging.DEBUGim Loggerecmind_blue_client.pool._sync_pool_clientbzw.…_async_pool_client. Es gibt kein Event, keinen Callback und keine Exception im aufrufenden Thread. - Getrennt von der Anwendungsstatistik
-
KeepAlive-Verkehr zählt in
keepalive_count/last_keepalive_at, nicht incall_count/last_call_at.
5. Was das KeepAlive nicht tut
-
Es prüft nicht die konfigurierten, aber gerade nicht verbundenen Server. Dafür gibt es
ecm.check_connections(), das pro Server eine eigene Wegwerf-Verbindung aufbaut. -
Es rührt die geteilte Server-Session nicht an. Ist die serverseitige Session weg (Serverneustart, Session-Timeout,
krn.SessionDrop), erkennt das erst der nächste Verbindungsaufbau:krn.SessionAttachliefert dann ein abweichendesSessionGUID, der veraltete Eintrag wird verworfen und ein vollständiger Login durchgeführt. KeepAlive und Session-Recovery sind zwei unabhängige Mechanismen, die sich ergänzen: das KeepAlive entfernt die tote Verbindung, der nächsteexecute()baut eine neue mit frischer Session auf.
6. Statistik auslesen
for stats in pool.connection_stats():
print(
f"{stats.hostname}:{stats.port} "
f"calls={stats.call_count} "
f"keepalives={stats.keepalive_count} "
f"last_keepalive={stats.last_keepalive_at}"
)
7. Pool sauber beenden
Solange ein KeepAlive läuft, sollte der Pool explizit geschlossen werden. close() (sync) bzw. aclose() (async) stoppt den Worker und schliesst alle Leerlaufverbindungen; beide sind mehrfach aufrufbar.
# Sync
pool = SyncPoolClient(..., keepalive_interval=300)
try:
...
finally:
pool.close()
# oder als Kontextmanager
with SyncPoolClient(..., keepalive_interval=300) as pool:
...
# Async
async with AsyncPoolClient(..., keepalive_interval=300) as pool:
...
Der Worker hält den Pool nur über eine schwache Referenz. Ein Pool, der ohne close() einfach losgelassen wird, kann daher weiterhin vom Garbage Collector eingesammelt werden — der Worker beendet sich dann von selbst. Das ist die Rückfallebene, nicht der empfohlene Weg: Zeitpunkt und Reihenfolge bestimmt dann der Garbage Collector.
8. FastAPI
Im Lifespan gehört der Pool einmal aufgebaut und beim Herunterfahren explizit geschlossen:
@asynccontextmanager
async def lifespan(app: FastAPI):
client = AsyncPoolClient(
servers=os.environ["ECM_SERVERS"],
username=os.environ["ECM_USERNAME"],
password=os.environ["ECM_PASSWORD"],
keepalive_interval=300,
)
app.state.ecm = ECM(client)
yield
await client.aclose()
app.state.ecm = None
Wird nur die ECM-Instanz herumgereicht, kommt man über ecm.client an den Pool: await ecm.client.aclose().
Zwei Punkte, die in einem Web-Service leicht übersehen werden:
-
Der
asyncio.Taskwird beim Erzeugen der ersten Verbindung im laufenden Event Loop angelegt, nicht im Konstruktor. Ein im Lifespan gebauter Pool bindet sich damit immer an den Loop, der ihn auch benutzt. -
Läuft der Service mit mehreren Worker-Prozessen (
uvicorn --workers, Gunicorn), hat jeder Prozess seinen eigenen Pool und damit seinen eigenen KeepAlive-Worker. Das Intervall gilt pro Prozess.
Das vollständige Beispiel steht in FastAPI-Integration.