Warum du deine MTU Einstellungen prüfen solltest, wenn du Docker Swarm auf virtualisierter (OpenStack oder Qemu) Hardware (z.B. Hetzner Cloud) betreibst

Es gibt so Momente in der Karierre als Entwickler/DevOp, bei denen man sich fragt, ob man nicht mehr ganz Dicht ist und überhaupt keine Ahnung hat, was man eigentlich tut.

Genau so einen Moment hatte ich diese Woche mit einem Kollegen, als unser Docker Swarm Stack ein sehr komisches Verhalten zeigte. Wir haben eine simple Webapp mit einer extra API, Datenbanken, Storage usw. dahinter und lassen die Systeme mittels Nginx Reverse Proxy miteinander reden. All das funktionierte wunderbar, aber aus unerfindlichen Gründen wurde von der React Website nur der HTML Teil ausgeliefert, aber das Laden der komprimierten CSS und JS schlug fehl. Natürlich schießt einem da gleich in den Kopf, dass es vielleicht ein Permission Problem ist, oder dass die Dateien in einem Unterordner liegen. Vielleicht ist Nginx hinter einem anderen Nginx ein Problem usw. Wir haben sogar den Webserver ausgetauscht weil uns die Lösung nicht einfallen wollte.

Irgendwann kam ich auf die Idee, die funktionierende index.html Datei einfach in den Dateinamen der komprimierten CSS sowie der komprimierten JS umzubenennen und auf einmal konnte ich beide Dateien ohne Probleme abrufen. Sprich, es konnte nicht am Dateityp oder Pfad liegen, sondern es war relativ schnell klar, dass die Größe der Datei scheinbar einen Einfluss auf das Verhalten hat.

Nach etwas Google Magic hatten wir dann herausgefunden, dass scheinbar die MTU der Netzwerkinterfaces das Problem verursacht.

Kurzer Exkurs:
Die MTU ist eine Abkürzung für “Maximum Transmission Unit” und steht für die maximale Größe eines Pakets, das über ein Netzwerk gesendet werden kann, ohne fragmentiert zu werden. In einfacheren Worten bedeutet dies, dass MTU die maximale Größe eines Datenpakets ist, das über das Netzwerk gesendet werden kann, ohne in kleinere Teile aufgeteilt zu werden. Wenn ein Paket größer als die MTU-Einstellung ist, muss es in kleinere Pakete aufgeteilt werden, um über das Netzwerk gesendet zu werden. Zum Beispiel beträgt die Standard-MTU für Ethernet 1500 Bytes. Wenn ein Paket größer als 1500 Bytes ist, wird es in kleinere Pakete aufgeteilt, um über das Netzwerk gesendet zu werden.

Problematisch wird es, wenn Netzwerkinterfaces unterschiedliche MTU verwenden, in unserem speziellen Fall die virtuellen Netzwerk Interfaces von Docker (Swarm) sowie die Netzwerkschnittstellen des Servers selbst. Denn bei virtualisierter Hardware, wie sie viele Cloud Anbieter nutzen (speziell wenn OpenStack oder auch Qemu zum Einsatz kommt), kann es sein, dass die MTU dieser virtuellen Netzwerkschnittstellen geringer als die Default 1500 Bytes ist. Ich habe es so verstanden dass die virtualisierung sich hier einfach 50 Bytes nimmt und diese für Verschlüsselung usw verwendet.

Prüfen kann man das einfach mittels

ip link

auf der Linux Shell und bekommt dann folgende Ausgabe:

1: lo:  mtu 16436 qdisc noqueue
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
2: eth0:  mtu 1400 qdisc pfifo_fast qlen 1000
link/ether 00:0f:ea:91:04:07 brd ff:ff:ff:ff:ff:ff
3: docker0:  mtu 1500 qdisc noqueue state UP mode DEFAULT group default
link/ether 00:0f:ea:91:04:08 brd ff:ff:ff:ff:ff:ff

Wenn ihr so eine Konstellation vorfindet, also dass die MTU eures Netzwerkinterfaces (eth0 in diesem Fall) KLEINER ist als der Wert, der bei docker0 steht, dann könnte ein Problem bestehen.

Da das virtuelle Docker Interface auf der “echten” Hardware aufsetzt, muss dessen MTU gleich oder kleiner der MTU des Netzwerk-Interfaces sein.

Man kann nun einfach versuchen, die MTU mittels

ip link set dev eth0 mtu 1500

setzen und das Problem ist gelöst. In meinem Fall ging das aber nicht, da das virtuelle Netzwerkinterface keinen höheren Wert erlaubte.

Also mussten wir anders heran gehen und Docker dazu zwingen, nicht seinen Default Wert von 1500 zu nehmen, sondern einen geringeren Wert. Dazu gibt es zwei Punkte, an denen man ansetzen kann.

Zum einen kann man in der Datei /etc/docker/daemon.json den Wert

{
"mtu": 1400
}

setzen, den Docker Service neu starten und wird dann sehen, dass die MTU von docker0 sich entsprechend geändert hat. Dies bringt aber nur etwas, wenn man Docker als Single Node betreibt.

Wenn man aber Docker Swarm einsetzt (evtl passiert das auch im Kubernetes Umfeld, da bin ich nicht ganz sicher), dann reicht das nicht aus. Denn dann verwendet Docker virtuelle Netzwerke, die jeweils eigene virtuelle Interfaces anlegen. Ausserdem sind zwei Netzwerke standardmäßig auf allen Nodes vorhanden:

ingress
docker_gwbridge

Wir haben es folgendermaßen gelöst:
Bevor eine Node dem Docker Swarm beitritt, löschen wir ihr docker_gwbridge und setzen es so neu auf:

docker network create \
--subnet 10.11.0.0/16 \
--opt com.docker.network.bridge.name=docker_gwbridge \
--opt com.docker.network.bridge.enable_icc=false \
--opt com.docker.network.bridge.enable_ip_masquerade=true \
docker_gwbridge

Wichtig: “docker_gwbridge” wird zwar vom Typ lokal angezeigt, es ist aber DAS Netzwerk über welches die Swarm Nodes/Docker Daemons miteinander sprechen!

Anschließend lassen wir die Nodes dem Docker Swarm beitreten. Nachdem alle Nodes drin sind, haben wir auf der Swarm Manage Node das Ingress Netzwerk gelöscht (wahrscheinlich reicht es auch, dass der Swarm erstmal initialisiert wurde) und dann mittels folgendem Befehl neu angelegt:

docker network create -d overlay --ingress --opt com.docker.network.driver.mtu=1400 ingress

Dieses neue Ingress Netzwerk ist dann erstmal nur auf der Master Node des Swarms sichtbar, wird auf den anderen Nodes aber dymanisch erzeugt, sobald der erste Container darauf deployed wird. Wenn aktuell kein Container aus dem Swarm auf dieser Node läuft, verschwindet das Ingress Netzwerk wieder von der Node bzw. wird von ihr gelöscht!

Anschließend haben wir den Swarm Stack deployed und schon konnten alle Services wieder miteinander sprechen und auch größere Datein konnten ohne Probleme ausgetauscht werden.

Docker meldet Chain DOCKER does not exist

Wenn docker beim Starten neuer Container die Meldung

Chain 'DOCKER' does not exist

ausgibt, dann bedeutet dies, dass für Docker keine IP-Talbes rules hinterlegt sind. Vermutlich kann das passieren, wenn man mit docker system prune aufräumt, so richtig sicher bin ich da aber auch nicht.

Falls der Fehler bei dir auch auftritt, kannst du ihn mit folgenden 3 Commands mittles root Permission auf dem entsprechenden Server lösen:


iptables -t nat -N DOCKER
iptables -t nat -A PREROUTING -m addrtype --dst-type LOCAL -j DOCKER
iptables -t nat -A PREROUTING -m addrtype --dst-type LOCAL ! --dst 127.0.0.0/8 -j DOCKER

Nginx Docker Reverse Proxy wirft 404 Fehler ab dem zweiten Request

Als großer Fan von “nackten Servern” auf denen nur Docker Container laufen, nutze ich Nginx als Reverse Proxy – im Speziellen das großartige Setup von jwilder/nginx-proxy in Kombinantion mit dem Let’s Encrypt Addon. Effektiv läuft das Teil passiv mit und schaut nur auf bestimmte Environment-Variablen anderer Docker Container. Sobald es diese sieht, wird automatisch ein Reverse Proxy in Nginx so konfiguriert, dass er diesen Dienst erreichbar macht und gleichzeitig wird noch ein Let’s Encrypt Cert für die entsprechende Domain besorgt und auf dem neuesten Stand gehalten.

Nun hatte ich in einem Setup mit sehr vielen gleichzeitig laufenden Diensten das Problem, dass einer der Dienste immer nur auf den ersten Request erfolgreich geantwortet hat, aber bei jedem weiteren Request nur noch einen 404 geworfen hat. Der Dienst selbst lief, Nginx hat auch keinen Fehler im Log geworfen und ich war etwas ratlos.

Nach ein paar grauen Haaren mehr und dem ein oder anderen versteckten Hinweis auf Google kam ich dann auf des Rätsels Lösung: durch ein fehlerhaftes Deployment lief kurzzeitig ein weiterer Container, der mittels der Environment Variable die gleiche URL für sich beanspruchte. Der Nginx Automatismus machte daraus automatisch einen Load Balancer. Nachdem das fehlerhafte Deployment weg war, wurde der Loadbalancer aber nicht mehr aufgelöst und somit war nur noch einer von beiden Einträgen aktiv. Warum der Nginx dann trotzdem immer wieder auf den nicht vorhandenen Host gehen wollte erschließt sich mir nicht so richtig, aber das war des Problems Lösung.

Also bin ich einfach per docker exec in den Nginx Container und habe die Datei /etc/nginx/conf.d/default editiert:


# server.name.com
upstream server.name.com {
# CONTAINER_NAME_1
server 123.45.0.67:1234;
# CONTAINER_NAME_2
server 123.45.0.78:1234;
}

Nachdem ich den Part von CONTAINER_NAME_2 gelöscht hatte, konnte ich mittels

nginx -s reload

die Config von Nginx on the fly neu laden und ab dem Moment lief der Service wieder wie gewohnt.