In heutigen Cloud und Datacenter-Deployments ist Netzwerklatenz udn Bandbreite meist kein kritisches Problem mehr.
Interessant wird es aber bei der Verfügbarkeit anderer Dienste, und deren Antwortverhalten, besonders wenn man nicht das Netzwerk dahin und den Dienst dahinter kontrolliert und kennt.
Das können Datenbanken, Addressvalidierungsdienste, Logistikdienstleister und anderes sein. Die eigene Antwortzeit ist immer mindestens langsam wie die Antwortzeiten der aufgerufenen Dienste.
Bei der Konfiguration unseres Dienstes muss aufgepasst werden, an welche Netzwerkinterfaces und Routen wir uns binden, besonders, wenn dankenswerterweise mehrere Netzwerkinterfaces vorliegen.
Es ist wichtig, die IP zu spezifizieren an die jeweiligen Netzwerk sich binden will, sonst schaut man in die "falsche Röhre".
Besonders in Java ist IP-Lookup ein unerwartet komplexes Thema, wenn man nicht konkret genug ist. Dann bindet sich der Socket an ein zufälliges (oder alle) Interfaces.
Sofern man sich auf DNS-Round-Robin zum Load-balancing verlässt, sollte man wissen dass (nicht-)aufgelöste DNS Einträge lange gecached werden.
Java hat seinen eigenen Cache (u.a. aus Sicherheitsgründen), den man auf minimale Werte setzen kann.
Wenn ein SecurityManager installiert ist, wird standardmässig unendlich lange gecached (wg. DNS Spoofing), sonst 30 Sekunden.
Die Einstellungen findet man in ${JRE_HOME}/lib/security/java.security
oder über java.security.Security.setProperty
.
// alternatives, internes Setting, wenn Security Property nicht gesetzt
-Dsun.net.inetaddr.ttl=5 -Dsun.net.inetaddr.negative.ttl=30
java.security.Security.setProperty("networkaddress.cache.ttl")
java.security.Security.setProperty("networkaddress.cache.negative.ttl")
Wenn der aufgerufene Dienst die Verbindung ablehnt, sind wir gut raus, unser Aufruf schlägt schnell fehl und wir können darauf reagieren.
Andererseits ist man gut beraten, eine Timeout für die Verbindung angegeben zu haben, ansonsten blockiert der Aufruf potentiell bis in alle Ewigkeit.
Oft ist das nicht in der API möglich, aber oft können Maximalzeiten für Verbindungsaufbau (connect) und Lesen (read) vom Socket konfiguriert werden, wie z.B. bei den meisten HTTP-Clients und sogar HttpURLConnection
.
JDBC Timeouts
// timeout für connection initialization
DriverManager.setLoginTimeout(timeoutInSeconds);
Connection con = DriverManager.getConnection(url, username, password);
// Vorzeitiger Abbruch von SQL Connections, bei langen Netzwerk (TCP) Timeouts
con.setNetworkTimeout(executor, timeoutInMillis);
// Transaktions-Timeout, e.g. via Spring oder JTA (vor tx.begin() aufrufen)
UserTransaction.setTransactionTimeout(timeoutInSeconds);
// Timeout pro statement (standardmässig kein Timeout)
Statement.setQueryTimeout(timeoutInSeconds);
Für einige JDBC Treiber gibt es dafür eigene Methoden, oder URL parameter für den JDBC connection String oder JDBC Properties, siehe auch [JDBCTimeouts].
HTTP connect und read timeout
HttpURLConnection con = ...
con.setConnectTimeout(3_0000);
con.setReadTimeout30_000);
....
In einigen APIs, wie ist das nicht möglich, dann kann man entweder Sockets mit Timout erzeugen, bevor man diese für die eigentliche Verbindung verwendet.
Socket timeout
Socket socket = new Socket();
socket.setSoTimeout(readTimeout);
socket.connect(addr, connectTimeout);
newSocket = socket.getSocketFactory().createSocket(socket, hostname, port, true);
Oder sogar eine eigene SocketFactory
im System installieren, die immer solche Timeouts setzt.
Eigene SocketFactory
// default is java.net.SocksSocketImpl
// Client Sockets
Socket.setSocketImplFactory(new TimoutSocketFactory())
// Server Sockets
ServerSocket.setSocketFactory(SocketImplFactory)
public class TimoutSocketFactory extends java.net.SocksSocketImpl {
private final int defaultSoTimeout = 5000; // oder aus System property oder Constructor
protected void connect(SocketAddress endpoint, int timeout) throws IOException {
super.connect(endpoint, timeout == 0 ? defaultSoTimeout : timeout);
}
protected void connect(String host, int port) throws UnknownHostException, IOException {
setTimeoutIfNecessary();
super.connect(host, port);
}
protected void connect(InetAddress address, int port) throws IOException {
setTimeoutIfNecessary();
super.connect(address, port);
}
private void setTimeoutIfNecessary() throws SocketException {
if (super.getTimeout() == 0)
setOption(SO_TIMEOUT, defaultSoTimeout);
}
}
Ein oft unbeachteter Aspekt ist die Blockierung von Lese- oder Schreiboperationen, z.b. auf HTTP-Verbindunge, wenn die Input- und Output-Streams nicht geleert werden.
So kann es z.B. passieren, dass man keine Daten empfangen kann, wenn der InputStream nicht abgeschlossen wurde, oder die Vebindung solange blockiert, bis man wieder genug Daten aus dem OutputStream gelesen hat.
Mit Resourcenmanagement (z.b. try-with-resources für `AutoCloseable`s) kann man zumindest dafür sorgen, dass Verbindungen garantiert die Gelegenheit gegeben wird, ihre Resourcen aufzuräumen.
Wenn das nicht oder unvollständig passiert, können z.B. halb-geschlossene Verbindungen existieren, oder verhindert werden dass Connections in den Pool zurückgegeben werden.
In einem Beispiel im Buch passierte das nach einem Datenbank-Failover, wenn existierende Connections und Statements partiell invalidiert wurden.
Dasselbe gilt natürlich für andere Resourcen, wie z.B. Datei-Handles.
Falsch - kein null-check, und wenn ein close fehlschlägt, werden die anderen nicht mehr ausgeführt
Connection con = driverMgr.getConnection();
Statement stmt = con.createStatement();
ResultSet rs = stmt.executeQuery(query);
try {
...
} finally {
rs.close();
stmt.close();
con.close();
}
Besser mit try-with-resources
try (Connection con = driverMgr.getConnection()) {
try (PreparedStatement pstmt = con.prepareStatement(statement)) {
try (ResultSet rs = pstmt.executeQuery()) {
}
}
}
Um diesen (temporären) Ausfall von Fremdkomponenten zu handhaben, ist ein Muster wie "Circuit-Breaker" (Sicherung) gut geeignet.
So eine Sicherung kapselt Aufrufe von Fremdsysteme und zählt aufeinanderfolgende Fehlschläge (in einem Zeitrahmen) mit und weist ab einer gewissen Quote (je nach Regel) Aufrufe ab.
Nach einiger Zeit können einzelne Aufrufe testweise durchgelassen werden, um zu testen ob das Fremdsystem wieder verfügbar ist.
Die Fehlerzähler, -raten und -zustände dieser Sicherungen sollten definitiv ins Monitoring mit einbezogen werden.
Das kann in Java mittels Bibliotheken, wie z.b. der Hystrix-Annotation: @HystrixCommand(fallbackMethod = "fallbackMethodName")
oder [Resilience4j], die jeweils auch Integration mit Monitoring anbieten.
Beispiel aus Resilience4j
public interface BackendService {
String doSomething();
}
CircuitBreaker circuitBreaker = CircuitBreaker.ofDefaults("backendName");
// Create a default Retry with at most 3 retries and a fixed time interval between retries of 500ms
Retry retry = Retry.ofDefaults("backendName");
// Decorate your call to BackendService.doSomething() with a CircuitBreaker
Supplier<String> decoratedSupplier = CircuitBreaker
.decorateSupplier(circuitBreaker, backendService::doSomething);
// Decorate your call with automatic retry
decoratedSupplier = Retry
.decorateSupplier(retry, decoratedSupplier);
// Execute the decorated supplier and recover from any exception
String result = Try.ofSupplier(decoratedSupplier)
.recover(throwable -> "Hello from Recovery").get();
// When you don't want to decorate your lambda expression,
// but just execute it and protect the call by a CircuitBreaker.
String result = circuitBreaker.executeSupplier(backendService::doSomething);
Nur bei transienten Problemen (z.b. kurze Netzwerkstörung, Neustart des Dienstes), was uns auch erst einmal korrekt vermittelt werden muss, könnte man den Aufruf noch einmal versuchen.
Die automatische Wiederholung des Aufrufs (retry), ggf. mit inkrementellem Verzögerungen, klingt erst einmal wie eine gute Idee, geht aber oft eher nach hinten los.
Denn all diese Versuche und Wartezeiten summieren sich schnell auf (und übersteigen dann unsere garantierte Antwortzeit) und blockieren Resourcen, es ist viel besser dem Aufrufer schnell das Fehlschlagen mitzuteilen (fail-fast) und ihm dann die Entscheidung zu überlassen.
Endnutzer klicken sowieso noch einmal, oder laden ihre Seite neu, wenn ihnen die Antwort nicht schnell genug kommt.
Oft wären die angeforderten Daten nach einem Retry schon veraltet und würden sowieso verworfen.
Auf dem TCP Schicht sorgen Empfangs- und Sendewarteschlangen für eine Pufferung eingehender Pakete, was im Sinne der Bandbreite erst einmal hilfreich sein kann.
Wenn diese voll sind, wird die andere Seite und damit der Aufrufer geblockt und damit eine Art "Backpressure" erzeugt.
Reaktive Systeme wie Akka, Reactor usw. nutzen einen ähnlichen Ansatz auf dem höheren Infrastrukturlevel um skalierbare Systeme zu gewährleisten, in denen Konsumenten nicht überlastet werden.
Die Länge dieser Warteschlangen kann ein Problem darstellen besonders wenn die Weiterverarbeitung der gepufferten Anfragen, dann gar nicht passiert oder extrem verzögert ist.
Dann wartet man ggf. unnötig lange auf ein Ergebnis das dann doch nicht kommt.
Daher gibt es oft die Empfehlung diese TCP-Puffer (auch den für Nachzügler-Pakete) im Rechenzentrum eher klein zu halten, damit man schnell eine Rückmeldung über die Kapazitätsprobleme der anderen Seite bekommt.
Das ganze Thema Orchestrierung, Discovery und Configuration Management für service- oder komponentenbasierte Dienste ist ein eigenes Thema das ganze Bücher füllen kann.