Die Anwendung setzt dafür SockJS als Abstraktionsebene über die Websocket-Unterstützung der verschiedenen Browser ein. Leider hat sich SockJS hartnäckig geweigert, einen Websocket zu benutzen und ist permanent auf XHR-Polling zurückgefallen, was sich negativ auf die Systemlast und den Netzwerk-Traffic ausgewirkt hat. Das Problem trat jedoch nur beim Zugriff über den Reverse Proxy (RP) auf und musste somit in dessen Konfiguration oder im Netzwerk zu suchen sein.
Diagnose des Problems
Ich habe also die gleiche Konfiguration des RP mit direktem Zugriff auf die Websocket-API von Firefox, Chrome und Edge nachgestellt. In diesem Fall konnte eine Verbindung zuverlässig hergestellt werden. Hingegen unter Verwendung von SockJS lieferten alle Browser stets einen Timeout-Fehler. Folglich musste der durch SockJS festgelegte Connect-Timeout zu niedrig sein.
Durch das Debuggen von SockJS habe ich nun herausgefunden, dass zunächst eine Abfrage nach
GET /websocket/info
an den Server gestellt wird. Darüber werden verschiedene Einstellungen der serverseitigen SockJS-Unterstützung ermittelt, wie z.B. welches Subprotokoll der Server unterstützt (Websockets, XHR-Longpolling, XHR-Polling, Http2-Push,...). In unserem Fall unterstützt der Server alle, mit einer Präferenz zu Websockets. Ferner ermittelt SockJS aber über diesen Request auch die Roundtrip-Zeit zum Server (RTT).
Nun ist es aber so, dass unser Server auf diesem Endpunkt zwar mit
Cache-Control: no-cache
geantwortet hat, aber durch einen weiteren Proxy auf dem Weg ein Cache eingeführt wurde, wodurch der Browser diese Abfrage extrem schnell beantwortet bekommen hat. Das war nie ein Problem, weil ohne Reverse Proxy im eigenen internen Subnetz der Server nie weiter als 300ms entfernt war (was dem Minimum des von SockJS angewendeten Timeouts entspricht). Nun aber durch den Reverse Proxy dauert das Aufbauen eines Websockets ein wenig länger (auch aufgrund der Dauer des Routings durch das Subnetz der Clients).
Lösung
Da wir wegen diverser Bestimmungen keinen Zugriff auf den cachenden Proxy beim Kunden bekommen haben, mussten wir also die Timeouts für die Websockets anpassen, sodass diese nicht mehr aufgrund der RTT ermittelt wurden. Dafür haben wir in den Code von SockJS eingegriffen und die Funktion countRTO(rtt) überschrieben, sodass diese einen konfigurierten Timeout liefert statt einem dynamisch ermittelten. Das stellt insofern in der Anwendungsdomäne kein Problem dar, als dass wir die eingesetzten Web-Clients hier kennen und somit garantiert ist, dass SockJS via Websockets unterstützt wird. Dadurch ist auch die Network-RTT hinreichend gut bekannt, sodass wir den Timeout statisch konfigurieren können.
Der behebende Commit hat also nur 3 Zeilen effektiven Code in unserer Anwendung hinzugefügt:
SockJS.prototype.countRTO = function (rtt) {
return CONFIGURED_WEBSOCKET_TIMEOUT;
}
Da wir hier in die Implementierung einer Dependency eingreifen, müssen wir sicherstellen, dass der Eingriff nicht in zukünftigen Versionen zu Problemen führt. Dafür haben wir eine Regel in unserem Statischem-Code-Analyse-Werkzeug eingeführt, die auf Änderungen an der Dependency prüft und uns somit alarmiert, wenn sich hier etwas ändern sollte. In diesem Fall haben wir dann einen Integration-Test hinterlegt, der sicherstellt, dass die Timeout-Berechnung immer noch funktioniert.