Zu einer TCP Verbindung zwischen zwei Endpunkten gehört auch, sich darüber einig zu sein ob eine Verbindung noch besteht oder nicht. Wartet eine Seite auf weitere Daten und ist die andere Seite inzwischen verschwunden, so sieht die wartende Seite nichts von dem TCP Verbindungsabbruch.
Dies geschieht nur, wenn die TCP Verbindung unsauber abgebrochen wurde. Ursachen einer solchen unsauberen Verbindungsbeendigung könnte Stromausfall, Hardwarecrash, Kernelpanic oder das unerwartete entfernen der IP-Addresse (wie z.B. bei Failover-Clustern) sein. Ebenso kann ein Netzwerkausfall dazu führen dass die Beendigung einer Verbindung nur auf einer Seite bekannt wird. Manche Firewalls die eine Verbindung nur eine bestimmte Zeit zulassen können hier auch zu Problemen führen.
Übrigens, ein Beenden der Anwendung (auch mit kill -9) alleine führt nicht zu so einer Situation. Denn das Betriebsystem schliesst alle Sockets eines beendeten Prozesses sauber.
Welche Methoden stehen für ein Endpunkt zur Verfügung einseitig bestehende Verbindungen mit Lese-Operationen zu erkennen?
- sie implementiert einen Lese-Timeout. Dies hat den Vorteil dass auch hängende Gegenstellen erkannt werden. Problem dabei ist aber die Dauer des Timouts. Ist dieser zu kurz gewählt so werden Verbindungen vorzeitig beendet wenn die Gegenstelle langsam antwortet. Um dies zu verhindern muss der Timeout recht groß gewählt werden. Bei Anwendungen die nur sehr sporadisch Anfragen bekommen und deren Verbindungen lange Zeit idle sein sollen lassen sich deswegen Timeouts fast garnicht verwenden.
- die Seite die lesend auf dem Socket wartet muss regelmäßig Daten senden oder erwarten. Dies wird gemeinhin als Heartbeat bezeichnet. Dieser hat aber den Nachteil dass zum einen das eingesetzte Protokoll solche Heartbeat Nachrichten erlauben (wenn ich auf eine Antwort warte und stattdessen eine Heartbeat Nachricht erhalte). Die Implementierung wird dadurch komplexer, braucht z.B. mehrere Threads oder Asynchrone methoden. Zudem hat das Versenden von Heartbeat Nachrichten auf einer Verbindung den Nachteil dass im Falle einer Unerreichbarkeit der TCP retry Mechanismus zuschlägt, der ggf. Relativ lange benötigen kann bis er Probleme feststellt,
- eine weitaus unproblematischere Möglichkeit ist der TCP Keepalive Mechanismus. Dieser wird entgegen seinem Namen hauptsächlich dazu verwendet abgerissene Connections zu erkennen. Der Mechanismus funktioniert so, dass auf der Seite einer TCP Verbindung auf der Keepalive aktiv ist (ggf. auf beiden) regelmäßig geprüft wird ob eine erfolgreiche Kommunikation stattfand, oder der Socket idle ist. Wenn er Idle ist weil nichts versendet oder empfangen wurde so sendet der Keepalive Mechanismus ein leeres TCP Paket. Die Anwendungen selbst bekommen davon nichts mit, nur der TCP Stack der Gegenseite bestätigt das Paket. in Fehler beim versenden führt zu einem Abbruch der Verbindung, genauso wie das ausbleiben von einer Antwort nach eingestellter Wiederholung.
Um das in der Praxis zu sehen habe ich eine Oracle 12c mit „Dead Client Detection“ konfiguriert. Dabei handelt es sich um eine Option welche den TCP Keepalive auf eingehenden Client Connections anschaltet (und auch gleich auf den angegebenen Wert in Minuten konfiguriert). Das ist deswegen ganz praktisch weil der default Wert unter Linux bei 2h liegt und damit nicht nur relativ nutzlos ist, sondern auch zum testen sehr langatmig.
Wenn ich jetzt eine Oracle Verbindung aufbaue, so werden nur Daten an den Server gesendet wenn ich im Client einen SQL Befehl absetze. Mit netstat oder dem neueren ss (socket statistics) kann man den Zustand der TCP timer einer einzelnen Verbindung betrachten. In diesem Fall verwende ich die -o option gibt die timer Information mit aus.
[oracle@bernd-db centos]$ netstat -tnpo
Active Internet connections (w/o servers)
Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name Timer
tcp 0 0 10.14.100.82:22 10.0.103.42:53111 ESTABLISHED - on (0.22/0/0)
tcp 0 0 127.0.0.1:58466 127.0.0.1:1521 ESTABLISHED 1207/ora_lreg_orcl off (0.00/0/0)
tcp6 0 0 10.14.100.82:1521 10.0.103.42:54076 ESTABLISHED 18470/oracleorcl keepalive (22.76/0/0)
Um nur die relevante Verbindung fortwährend zu betrachten verwende ich die -c option. Mittels grep beschränke ich mich auf eine Verbindung. (Dieses Filtern kann mit ss effizienter gemacht werden, aber auf meinem Testsystem gibt es keine große Anzahl an Verbindungen) Parallel dazu läuft folgender tcpdump Befehl
[oracle@bernd-db centos]$ sudo tcpdump -ttt -nn --dont-verify-checksums -U -v host 10.0.103.42 and port 54076 &
[1] 18819
tcpdump: listening on eth0, link-type EN10MB (Ethernet), capture size 262144 bytes
[oracle@bernd-db centos]$ netstat -tnpoc 2>/dev/null | grep 10.0.103.42:54076
tcp6 0 0 10.14.100.82:1521 10.0.103.42:54076 ESTABLISHED 18470/oracleorcl keepalive (8.66/0/0)
tcp6 0 0 10.14.100.82:1521 10.0.103.42:54076 ESTABLISHED 18470/oracleorcl keepalive (7.65/0/0)
tcp6 0 0 10.14.100.82:1521 10.0.103.42:54076 ESTABLISHED 18470/oracleorcl keepalive (6.64/0/0)
tcp6 0 0 10.14.100.82:1521 10.0.103.42:54076 ESTABLISHED 18470/oracleorcl keepalive (5.64/0/0)
tcp6 0 0 10.14.100.82:1521 10.0.103.42:54076 ESTABLISHED 18470/oracleorcl keepalive (4.63/0/0)
tcp6 0 0 10.14.100.82:1521 10.0.103.42:54076 ESTABLISHED 18470/oracleorcl keepalive (3.62/0/0)
tcp6 0 0 10.14.100.82:1521 10.0.103.42:54076 ESTABLISHED 18470/oracleorcl keepalive (2.61/0/0)
tcp6 0 0 10.14.100.82:1521 10.0.103.42:54076 ESTABLISHED 18470/oracleorcl keepalive (1.61/0/0)
tcp6 0 0 10.14.100.82:1521 10.0.103.42:54076 ESTABLISHED 18470/oracleorcl keepalive (0.60/0/0)
00:00:00.000000 IP (tos 0x0, ttl 64, id 2241, offset 0, flags [DF], proto TCP (6), length 40)
10.14.100.82.1521 > 10.0.103.42.54076: Flags [.], ack 3621868956, win 1184, length 0
00:00:00.001712 IP (tos 0x0, ttl 124, id 10289, offset 0, flags [DF], proto TCP (6), length 40)
10.0.103.42.54076 > 10.14.100.82.1521: Flags [.], ack 1, win 256, length 0
tcp6 0 0 10.14.100.82:1521 10.0.103.42:54076 ESTABLISHED 18470/oracleorcl keepalive (5.61/0/0)
tcp6 0 0 10.14.100.82:1521 10.0.103.42:54076 ESTABLISHED 18470/oracleorcl keepalive (4.60/0/0)
tcp6 0 0 10.14.100.82:1521 10.0.103.42:54076 ESTABLISHED 18470/oracleorcl keepalive (3.59/0/0)
tcp6 0 0 10.14.100.82:1521 10.0.103.42:54076 ESTABLISHED 18470/oracleorcl keepalive (2.59/0/0)
tcp6 0 0 10.14.100.82:1521 10.0.103.42:54076 ESTABLISHED 18470/oracleorcl keepalive (1.58/0/0)
tcp6 0 0 10.14.100.82:1521 10.0.103.42:54076 ESTABLISHED 18470/oracleorcl keepalive (0.57/0/0)
tcp6 0 0 10.14.100.82:1521 10.0.103.42:54076 ESTABLISHED 18470/oracleorcl keepalive (53.93/0/0)
tcp6 0 0 10.14.100.82:1521 10.0.103.42:54076 ESTABLISHED 18470/oracleorcl keepalive (52.92/0/0)
...
Was ist passiert? In diesem Beispiel sieht man die letzten 8 Sekunden des herabzählenden keepalive timers. In der ganzen Minute davor wurden keine Daten ausgetauscht, und so sendet der Kernel beim Ablauf des Timers ein leeres ACK Paket (Flags [.] length 0) und wartet 6 weitere Sekunden bevor er prüft ob dieses angekommen ist. Wenn ja setzt er einen neuen Timer auf (60s abzüglich der bereits gewarteten knapp 6s)
Der Keepalive Timer wird bei Oracle 12.2 mit der Option sqlnet.expire_time=1 in ${ORACLE_HOME}/network/admin/sqlnet.ora auf eine Minute konfiguriert. Das Wiederholungsintervall von 6s wird durch Oracle fest vorgegeben.
Wenn ich jetzt 18 Sekunden bevor der Timer abläuft eine Client Anfrage stelle, so ändert sich die Ausgabe etwas:
tcp6 0 0 10.14.100.82:1521 10.0.103.42:54076 ESTABLISHED 18470/oracleorcl keepalive (19.71/0/0)
tcp6 0 0 10.14.100.82:1521 10.0.103.42:54076 ESTABLISHED 18470/oracleorcl keepalive (18.70/0/0)
00:01:41.833122 IP (tos 0x0, ttl 124, id 10335, offset 0, flags [DF], proto TCP (6), length 61)
10.0.103.42.54076 > 10.14.100.82.1521: Flags [P.], seq 1:22, ack 1, win 256, length 21
00:00:00.000306 IP (tos 0x0, ttl 64, id 2242, offset 0, flags [DF], proto TCP (6), length 55)
10.14.100.82.1521 > 10.0.103.42.54076: Flags [P.], seq 1:16, ack 22, win 1184, length 15
00:00:00.007188 IP (tos 0x0, ttl 124, id 10337, offset 0, flags [DF], proto TCP (6), length 53)
10.0.103.42.54076 > 10.14.100.82.1521: Flags [P.], seq 22:35, ack 16, win 256, length 13
00:00:00.000077 IP (tos 0x0, ttl 64, id 2243, offset 0, flags [DF], proto TCP (6), length 55)
10.14.100.82.1521 > 10.0.103.42.54076: Flags [P.], seq 16:31, ack 35, win 1184, length 15
00:00:00.041996 IP (tos 0x0, ttl 124, id 10339, offset 0, flags [DF], proto TCP (6), length 40)
10.0.103.42.54076 > 10.14.100.82.1521: Flags [.], ack 31, win 256, length 0
tcp6 0 0 10.14.100.82:1521 10.0.103.42:54076 ESTABLISHED 18470/oracleorcl keepalive (17.69/0/0)
00:00:00.881484 IP (tos 0x0, ttl 124, id 10342, offset 0, flags [DF], proto TCP (6), length 139)
10.0.103.42.54076 > 10.14.100.82.1521: Flags [P.], seq 35:134, ack 31, win 256, length 99
00:00:00.000610 IP (tos 0x0, ttl 64, id 2244, offset 0, flags [DF], proto TCP (6), length 234)
10.14.100.82.1521 > 10.0.103.42.54076: Flags [P.], seq 31:225, ack 134, win 1184, length 194
00:00:00.003408 IP (tos 0x0, ttl 124, id 10344, offset 0, flags [DF], proto TCP (6), length 61)
10.0.103.42.54076 > 10.14.100.82.1521: Flags [P.], seq 134:155, ack 225, win 255, length 21
00:00:00.000238 IP (tos 0x0, ttl 64, id 2245, offset 0, flags [DF], proto TCP (6), length 55)
10.14.100.82.1521 > 10.0.103.42.54076: Flags [P.], seq 225:240, ack 155, win 1184, length 15
00:00:00.041847 IP (tos 0x0, ttl 124, id 10346, offset 0, flags [DF], proto TCP (6), length 40)
10.0.103.42.54076 > 10.14.100.82.1521: Flags [.], ack 240, win 255, length 0
tcp6 0 0 10.14.100.82:1521 10.0.103.42:54076 ESTABLISHED 18470/oracleorcl keepalive (16.69/0/0)
tcp6 0 0 10.14.100.82:1521 10.0.103.42:54076 ESTABLISHED 18470/oracleorcl keepalive (15.68/0/0)
tcp6 0 0 10.14.100.82:1521 10.0.103.42:54076 ESTABLISHED 18470/oracleorcl keepalive (14.67/0/0)
...
tcp6 0 0 10.14.100.82:1521 10.0.103.42:54076 ESTABLISHED 18470/oracleorcl keepalive (7.62/0/0)
tcp6 0 0 10.14.100.82:1521 10.0.103.42:54076 ESTABLISHED 18470/oracleorcl keepalive (6.61/0/0)
tcp6 0 0 10.14.100.82:1521 10.0.103.42:54076 ESTABLISHED 18470/oracleorcl keepalive (5.61/0/0)
tcp6 0 0 10.14.100.82:1521 10.0.103.42:54076 ESTABLISHED 18470/oracleorcl keepalive (4.60/0/0)
tcp6 0 0 10.14.100.82:1521 10.0.103.42:54076 ESTABLISHED 18470/oracleorcl keepalive (3.59/0/0)
tcp6 0 0 10.14.100.82:1521 10.0.103.42:54076 ESTABLISHED 18470/oracleorcl keepalive (2.58/0/0)
tcp6 0 0 10.14.100.82:1521 10.0.103.42:54076 ESTABLISHED 18470/oracleorcl keepalive (1.57/0/0)
tcp6 0 0 10.14.100.82:1521 10.0.103.42:54076 ESTABLISHED 18470/oracleorcl keepalive (0.57/0/0)
tcp6 0 0 10.14.100.82:1521 10.0.103.42:54076 ESTABLISHED 18470/oracleorcl keepalive (42.22/0/0)
tcp6 0 0 10.14.100.82:1521 10.0.103.42:54076 ESTABLISHED 18470/oracleorcl keepalive (41.21/0/0)
Es ist aber wieder zu sehen dass der Timer erst auf 0 zählt, dann erkennt dass ein Austausch vor 18 Sekunden stattfand, und deswegen einen neuen Timer mit der verbleibenden Restzeit von 40 Sekunden aufzieht aber kein ACK Paket versendet.
Um jetzt zu sehen was passiert wenn der Client keine Antworten versendet installiere ich einfach eine Firewall Regel die alle Pakete verwirft (DROP nicht REJECT):
[oracle@bernd-db centos]$ iptables -I INPUT -p tcp -s 10.0.103.42 --sport 54076 -j DROP
Im Folgenden habe ich die Antwort-Pakete (die tcpdump noch sieht) entfernt:
tcp6 0 0 10.14.100.82:1521 10.0.103.42:54076 ESTABLISHED 18470/oracleorcl keepalive (2.31/0/0)
tcp6 0 0 10.14.100.82:1521 10.0.103.42:54076 ESTABLISHED 18470/oracleorcl keepalive (1.30/0/0)
tcp6 0 0 10.14.100.82:1521 10.0.103.42:54076 ESTABLISHED 18470/oracleorcl keepalive (0.30/0/0)
00:02:00.190237 IP (tos 0x0, ttl 64, id 2258, offset 0, flags [DF], proto TCP (6), length 40)
10.14.100.82.1521 > 10.0.103.42.54076: Flags [.], ack 155, win 1184, length 0
tcp6 0 0 10.14.100.82:1521 10.0.103.42:54076 ESTABLISHED 18470/oracleorcl keepalive (5.31/0/1)
tcp6 0 0 10.14.100.82:1521 10.0.103.42:54076 ESTABLISHED 18470/oracleorcl keepalive (4.30/0/1)
tcp6 0 0 10.14.100.82:1521 10.0.103.42:54076 ESTABLISHED 18470/oracleorcl keepalive (3.29/0/1)
tcp6 0 0 10.14.100.82:1521 10.0.103.42:54076 ESTABLISHED 18470/oracleorcl keepalive (2.28/0/1)
tcp6 0 0 10.14.100.82:1521 10.0.103.42:54076 ESTABLISHED 18470/oracleorcl keepalive (1.27/0/1)
tcp6 0 0 10.14.100.82:1521 10.0.103.42:54076 ESTABLISHED 18470/oracleorcl keepalive (0.27/0/1)
00:00:06.014269 IP (tos 0x0, ttl 64, id 2259, offset 0, flags [DF], proto TCP (6), length 40)
10.14.100.82.1521 > 10.0.103.42.54076: Flags [.], ack 155, win 1184, length 0
tcp6 0 0 10.14.100.82:1521 10.0.103.42:54076 ESTABLISHED 18470/oracleorcl keepalive (5.27/0/2)
tcp6 0 0 10.14.100.82:1521 10.0.103.42:54076 ESTABLISHED 18470/oracleorcl keepalive (4.27/0/2)
...
tcp6 0 0 10.14.100.82:1521 10.0.103.42:54076 ESTABLISHED 18470/oracleorcl keepalive (1.04/0/9)
tcp6 0 0 10.14.100.82:1521 10.0.103.42:54076 ESTABLISHED 18470/oracleorcl keepalive (0.03/0/9)
00:00:06.014242 IP (tos 0x0, ttl 64, id 2267, offset 0, flags [DF], proto TCP (6), length 40)
10.14.100.82.1521 > 10.0.103.42.54076: Flags [.], ack 155, win 1184, length 0
tcp6 0 0 10.14.100.82:1521 10.0.103.42:54076 ESTABLISHED 18470/oracleorcl keepalive (5.04/0/10)
tcp6 0 0 10.14.100.82:1521 10.0.103.42:54076 ESTABLISHED 18470/oracleorcl keepalive (4.04/0/10)
tcp6 0 0 10.14.100.82:1521 10.0.103.42:54076 ESTABLISHED 18470/oracleorcl keepalive (3.03/0/10)
tcp6 0 0 10.14.100.82:1521 10.0.103.42:54076 ESTABLISHED 18470/oracleorcl keepalive (2.02/0/10)
tcp6 0 0 10.14.100.82:1521 10.0.103.42:54076 ESTABLISHED 18470/oracleorcl keepalive (1.01/0/10)
tcp6 0 0 10.14.100.82:1521 10.0.103.42:54076 ESTABLISHED 18470/oracleorcl keepalive (0.01/0/10)
00:00:06.013377 IP (tos 0x0, ttl 64, id 2268, offset 0, flags [DF], proto TCP (6), length 40)
10.14.100.82.1521 > 10.0.103.42.54076: Flags [R.], seq 240, ack 155, win 1184, length 0
Wenn dieses mal der Timer abläuft ohne Datenaustausch, so wird das Keepalive Paket vom Server versendet und der Timer auf 6s gestellt. Da das Paket aber nicht durchkommt, so kam auch nach 6s keine Antwort zurück. Der Kernel auf Serverseite erkennt dies, zählt den Keepalive Zähler hoch (letzte Stelle innerhalb von timer (time/retries/keepalives)), sendet noch ein Paket und wartet erneut 6s.
Oracle hat den Retry auf 10 Probes konfiguriert, entsprechend wird bei der letzten Runde (nach einer Minue) kein Keepalive Paket mehr gesendet sondern die Connection resettet. (Flags [R.] - In meinem Fall bekommt der Client das RST nicht, wegen der Firewall). Hätte ich die Firewall Regel vor Ablauf der 10 retries entfernt, so hätte der normale Keepalive Ablauf wieder weiter gemacht.
Falls die Anwendung keine Konfiguration von TCP Keepalives pro Socket zulässt, so werden folgende Linux Kernel Werte verwendet:
[oracle@bernd-db oracle]$ sysctl net.ipv4.tcp_keepalive_time net.ipv4.tcp_keepalive_intvl net.ipv4.tcp_keepalive_probes
net.ipv4.tcp_keepalive_time = 7200
net.ipv4.tcp_keepalive_intvl = 75
net.ipv4.tcp_keepalive_probes = 9
Es gibt noch eine Besonderheit, wenn der Keepalive wiederholt wird, aber gleichzeitig auch Daten versendet werden, so wird der Keepalive Mechanismus ausgesetzt, stattdessen versucht der normale Retry Mechanismus Daten zu versenden, aber das soll Gegenstand eines anderen Blogposts sein.