Teile und Herrsche - das ist in so vielen Aspekten immer wieder wahr.
Nach Jahrmillionen in denen man die Ressourcen eines Computers maximal auslastete, weil die sind ja teuer, begann man - als die Preise fielen, die Aufgaben zu verteilen, ein Server für eine Aufgabe. Dann erkannte man erstaunllicherweise, dass viele der Rechner nicht mal im Ansatz ausgelastet werden. Verschwendung. Dann fing man an, die Dienste in virtuelle Maschinen auf der Hardware zu konsolidieren. Dadurch waren die Rechner gut ausgelastet. Aber die Admins kamen kaum hinterher - jede VM ist ihr eigenes Betriebssystem mit einem Zoo von Diensten und Paketen, damit die VM das tut, wofür sie gedacht ist. Und jedes OS verbrennt auch Ressourcen für OS Aktionen, braucht Sicherheitsupdates und so weiter. Dann ging es los mit Paravirtualisierung und es dauerte dann nicht mehr lange und Docker war geboren.
Docker ist keine Virtualisierung. Auch wenn es manchmal so aussieht, aber es paravirtualisiert, genauer es nutzt die Möglichekten der CPU und des Betriebssystems, die Prozesse von einander zu trennen. Einzig die für einen Prozess notwendigen Ressourcen (Dateien) werden dupliziert und nicht mehr das ganze Betriebssystem. Ich vergleiche das immer gerne mit dem Chroot, was man schon länger kennt.
Das sind schon viele Vorteile, der eigentliche Vorteil ist aber - man verschmutzt mit einem Dockercontainer nicht sein Basissystem. Die Konfigurationsarbeiten für den Betrieb eines Dienstes bleibt im Docker - genauer im Dockercontainer. Wirft man das weg, ist alles spezifische weg. Das hat den Vorteil, dass man schnell was ausprobieren kann, das hat aber auch den Vorteil bei Fehlern schnell zurück zum Grundsystem zu kommen.
Die normale Nutzung von Docker basiert darauf, dass man mit dem Befehl Docker diverse unabhängige Befehle absendet und über Parametrisierung der einzelnen Befehle dann eine Infrastruktur aufbaut. Zum Beispiel braucht es für ein abgeschottetes System aus zwei Diensten Datenbank und Webserver einen Befehl um ein Netzwerk zu erzeugen, einen um den Dienst Datenbank zu starten und mit dem Netzwerk zu verknüpfen und einen um das gleiche mit dem Webserver zu tun. Das ist ziemlich viel Handarbeit und man fängt schnell an, dass zu scripten.
Und hier beginnt die Arbeit von docker-compose
. Dieses Tool bietet die Möglichkeit in einer YAML Datei eine Infrastruktur zu definieren.
Hier mal ein Beispiel:
version: '2.1'
services:
postgres:
image: postgres
volumes:
- ./postgres_data:/var/lib/postgresql/data
environment:
POSTGRES_DB: keycloak
POSTGRES_USER: keycloak
POSTGRES_PASSWORD: password
keycloak:
image: jboss/keycloak
command: -b 0.0.0.0 -Djboss.bind.address.private=127.0.0.1
environment:
DB_VENDOR: POSTGRES
DB_ADDR: postgres
DB_DATABASE: keycloak
DB_USER: keycloak
DB_SCHEMA: public
DB_PASSWORD: password
KEYCLOAK_USER: admin
KEYCLOAK_PASSWORD: pasword
ports:
- "2600:8443"
depends_on:
- postgres
networks:
default:
driver: bridge
enable_ipv6: true
ipam:
config:
- subnet: "172.26.0.0/24"
- subnet: "fc00:2600::/96"
Das ist ein einfaches Beispiel und es sieht komplizierter aus, als es ist. Die erste Zeile enthält eine Versionsnummer, die für docker-compose relevant ist. Hier ist 2.1 eine gute Wahl. Es gibt auch neuere Versionen, die bringen aber nur etwas in Zusammenhang mit docker-swarm, welches wir nicht nutzen. Dann gibt es drei Blöcke (in unserem einfachen Beispiel sind nur zwei zu sehen) - services, networks, volumes.
Im Bereich services
definieren wir, die jeweiligen Docker-Container, die zusammen einen “Service” abbilden. Theoretisch kann man hier eine ganze Infrastruktur abbilden - allerdings begrenz docker-compose sich auf einen Host und verteilt die Container nicht in einem Kubernetes Cluster. Jedes Element darunter hat einen frei zu wählenden Namen und darunter gibt es dann einige elementare Einträge wie image
oder build
um anzugeben, welcher Container zu verwenden ist, welche Ports freigegeben werden bestimmt man mit ports
. Der Rest ist optional. Zum Beispiel environment
definiert Umgebungsspezifische Variablen. Man kann diese auch aus einer Datei auslesen. Man kann für bestimmte Pfade im Container auch volumes
deklarieren, um sie außerhalb des Containers zu lagern. Hier verwende ich einen relativen Pfad - was in Docker eigentlich nicht erlaubt ist, aber mit docker-compose ab seiner Definitionsdatei funktioniert. Man kann auch Volumes definieren, die anders liegen. Dafür gibt es ja die Volumes Definition (die wir hier nicht verwenden).
Weiterhin kann man Netzwerke mit networks
definieren - ich mache das gerne explizit, weil man so mehr Kontrolle hat und für IPv6 ist das sogar zwingend notwendig, weil Docker bzgl. IPv6 leider kein gutes Vorbild in der Umsetzung ist. Es wird gesagt, IPv6 geht - man muss es aktivieren (via enable_ipv6), aber es wählt dann nicht selbständig ein Segment aus dem Host-Adressraum, es macht kein NAT (und arbeitet damit bei IPv6 gänzlich anders als IPv4). Viel schlimmer ist aber, dass offene Ports von Containern in IPv6 für alle offen sind, egal ob man das will oder nicht. Bei IPv4 wird die ports
deklaration also beachtet - bei IPv6 nicht. Ich verwende daher gerne ein Hilfspaket von robertkl/ipv6nat.
Zum Thema IPv6 NAT kann man viel erzählen und diskutieren. Aber so lange man keine dynamische Kontrolle über die Ports hat, die Docker aufmacht, ist das Thema bei einem direkt ans Internet gehängten Server obsolet. Da muss was “vor”.
Bedingt durch die Unabhängigkeit der einzelnen Services voneinander und der Tatsache, dass jedes seine eigene Infrastruktur bekommt, ergibt sich in einer gemeinsam zu nutzenden Ressource schnell ein Problem. Bei Docker ist das das Netzwerk. Jeder Service bekommt sein eigenes lokales internes Netzsegment und das landet alles auf einem Interface. Zum Teil schränkt Docker die Nutzung erheblich ein. Zum Teil ignoriert es bestehende Regeln und liefert keine Lösung. Dann muss die Firewall ran.
Wir haben auf einem Server A einen Service etabliert, und wollen nun nur von einem Server B darauf zugreifen. Alle anderen sollen den Service nicht sehen. Hängt der Server im Internet, kann man das nur mit lokalen Firewallregeln auf Server A direkt lösen.
Docker erstellt selber ein kleines Regelwerk in den IPTables Tabellen NAT und Filter. Dabei geht es Docker primär darum, die Pakete vom externen Netzwerkinterface auf das interne Netz des jeweiligen Service zu bekommen.
Die NAT Regeln sorgen dafür, dass Anfragen vom externen Interface auf dem externen Port auf das interne Interface des Docker Containers mit seinem internen Port weitergereicht werden. Hier sollte man nicht viel dran ändern. Bei den Filtern kann man aber sowohl INPUT als auch eine Docker-spezifische Chain DOCKER-USER befüllen. Wichtig dabei ist - wenn, dann nur diese beide Chains flushn und alles andere so lassen.
Was ich mache ist - die INPUT Chain wie üblich konfigurieren - alles was rein darf, darf rein, den Rest drop’n. Zu beachten gilt, dass die Forward Chain für NAT zuerst angesprochen wird und INPUT deshalb nur für Nicht-Docker Dienste relevant ist. Die für Docker relevanten Services finden sich in DOCKER-USER Anwendung und müssen dort getrennt behandelt werden. Bei der DOCKER-USER Chain (die in FORWARD angesiedelt ist), muss man vorsichtig sein mit dem DROP oder REJECT. Hier sollte man die Regeln nur auf das externe Interface anwenden, damit die Kommunikation zwischen Containern (die über interne Bridges und docker0 laufen) nicht gestört werden.
Ceph ist ein verteilter Objektspeicher für Linux.
Ceph benötigt im Gegensatz zu Gluster ganze Platten. Theoretisch könnte man wohl auch Partitionen angeben, das ist aber ein ziemlicher Hack und deshalb lassen wir das lieber.
Objektspeicher bedeutet, dass die Festplatten nicht mit einem klassischen Dateisystem arbeiten, sondern mit einer Datenbank und einem Index. Laienhaft werden dabei die Objekte als Dateien oder Dateiblöcke betrachtet, in der Datenbank abgelegt.
Auf den Proxmox Hosts ist Ceph bereits in Vorbereitung vorhanden. Als Grundvoraussetzung gilt, dass die Proxmox
in einem Cluster verbunden sind und sich gegenseitig kennen. Das sieht man daran, dass die Config im Corosync
Ordner unter /etc/pve
angelegt wird. Dieser wird in einem Proxmox Cluster über alle Knoten immer synchron gehalten.
Für jeden Host muss man
pveceph install
ausführen. Damit wird das Proxmox Ceph Debian Repository konfiguriert und die Ceph Pakete installiert.
Im Folgenden muss man Ceph initialisieren. Das geht mit
pveceph init -network 10.255.255.0/24 -cluster-network 10.255.255.0/24
Dies muss man auf dem ersten Host machen. Dadurch wird unter /etc/pve/ceph.conf
eine Konfiguration
im Corosync System angelegt, auf die in /etc/ceph/ceph.conf
ein Link erzeugt wird.
Auf allen anderen Nodes reicht ein
pveceph init
Wichtig ist noch der Keyring, der auch mit dem Init erzeugt wird und auf allen Nodes der gleiche sein muss.
Pro Proxmox Host kann man dann noch einen Monitor erzeugen, mit:
pveceph createmon
Wobei das veraltet scheint. Die Doku spricht davon, aber in der CLI heißt es pveceph mon create
.
Führt man den Befehl aus, wird auf dem dazugehörigen System ein Dienst gestartet, der auf Port 6789 horcht.
Analog sollte man auch mindestens einen Manager anlegen mit pveceph mgr create
. Auf dem ersten Knoten, bei dem ein Monitor angelegt wird, wird automatisch auch ein Manager angelegt. Alle weiteren sind dann nur im Standby relevant.
Erkenntnis - bei Proxmox/Ceph kann man nur ganze Platten hinzufügen. Das ist schwierig, wenn das Basissystem nur aus einem RAID-1 besteht, auf dem auch das OS liegt. Im Falle einer NVMe plus 2 SATA könnte man Ceph so aufbauen, dass es über die SATA Platten läuft.
Hier finden sich einige nützliche und interessante Links an, die sich im Laufe der Recherche angesammelt haben.
GlusterFS ist ein einfaches und nützliches verteiltes Dateisystem.
Hat man Dateien, die auf mehreren Servern synchron gehalten werden sollen, ist Gluster ideal. Man kann mit Gluster eine Reihe von unterschiedlichen Redundanz-Szenarien abbilden. Zum Beispiel kann man RAID-ähnliche Spiegelungen (0,1,5,10) aufbauen, in dem man bei der Anlage der Volumes die Zahl der Kopien definiert. Auf diese Weise lassen sich die Platten mehrerer Server verknüpfen und grosse Datenmengen ablegen.
Ein Wort der Warnung vorweg. Egal wie man es anstellt, die synchrone Verteilung von Daten verbraucht immer auch Netzwerkresourcen und ist von dessen Kapazität direkt abhängig. selbst für wenige Datenänderungen (ein schreibender Nutzer) kann ein Server mit einem 1Gbs Interface zu wenig sein. Die Server müssen darüber nicht nur den Dienst anbieten, sondern auch den Datenabgleich zwischen allen Knoten. Ausserdem ist Gluster für Dateien da, das Verteilen von Datenbankdateien oder Sockets ist kritisch und kann bei starker Netzauslastung für Fehler sorgen. Wir konnten das bei besagtem Setup nach nur wenigen Tagen Betrieb reproduzieren. Gluster muss nicht, aber sollte eine separate Netzwerkkarte für sich alleine haben und die sollte man gut im Auge behalten.
Was in dem Setup aber gut geht, sind reine Filesysteme mit grossen Readanteil, wie zB HTDoc-Verzeichnisse. Da Gluster auf Pfaden aufsetzt und damit keine eigenen Partitionen braucht, ist das Anlegen relativ einfach auf auf Maschinen mit nur einer Platte (VM) möglich.
Gluster ist als Paket bei Debian dabei und daher ist eine Installation denkbar einfach.
apt-get install glusterfs-server
service glusterd start
Damit ist der Server installiert und der Service bereit zur Nutzung. Zu beachten ist natürlich, dass das auf allen Nodes zu erfolgen hat.
Damit die Nodes sich gegenseitig kennt, beginnt man auf einem der Nodes ein “Probe” auf die anderen.
server1:$/> gluster peer probe server2.fqdn
Dadurch wird nicht nur eine Art Ping ausgeführt. Die Server tauschen auch im Zweifel Schlüssel aus und kennen sich von nun an. Man kann auch einen Server wieder aus dem Cluster werfen.
Die Konfiguration geschieht in zwei Phasen. Zunächst brauchen wir Volumes, die sich alle Nodes teilen. Die Teilung erfolgt auf drei Methoden - alles wird gespiegelt auf allen gleich, ein Teil wird gespiegelt, so dass alle Dateien mindestens x mal vorhanden sind oder es wird gestretcht - also aller Speicher wird genutzt, die Dateien liegen aber nur einmal vor und werden nur von der einen Quelle gelesen. Man kann das sehr gut mit den RAID Leveln vergleichen, wobei letzterer die geringste Ausfallsicherheit hat, erster maximal sicher ist.
Nun kommt es darauf an, wie man die Volumes konfiguriert und Gluster führt hier einen Begriff ein, der Brick heißt. Ich stelle mir vor, dass Brick ein Teil des Gluster-Clusters ist. Alle Steine zusammen ergeben das Volume. Was ich nicht verstehe, warum man bei Gluster derart unterscheidet zwischen dem Node und dem Brick und die Beispiele es einem Anfänger hier auch unnötig kompliziert machen. Letzlich ist es nur ein Pfad.
Man gibt also für ein Volume pro Node an, wo die Dateien für das Volume abgelegt sind. Ich tue das pro Volume und pro Node immer am gleichen Ort pro Server. Also legen wir auf allen Server ein Verzeichnispfad an.
mkdir -p /srv/gfs/gv0
In diesem Verzeichnis liegen zukünftig alle Daten unseres Volumes gv0. Dann können wir das Volume anlegen.
gluster volume create gv0 replica 2 server1.fqdn:/srv/gfs/gv0 server2.fqdn:/srv/gfs/gv0 force
In dem Verzeichnis wird ein .glusterfs Verzeichnis angelegt, in dem Gluster alle seine Verwaltungsdaten ablegt. Der Rest wird verteilt und kann so auch benutzt werden. Ich empfehle aber eher einen Mountpoint anzulegen und das GlusterFS zu mounten und richtig daraufzu schreiben.
Dafür braucht es einen Client. Auch den gibt es natürlich bei Debian als Paket.
apt-get install glusterfs-client
mount -t glusterfs ac-sh0003.acoby.de:gv2 /mnt/
Will man darüber hinaus Gluster für die Volumes von Docker verwenden, empfehle ich folgendes:
docker plugin install --alias gfs trajano/glusterfs-volume-plugin --grant-all-permissions --disable
docker plugin set gfs SERVERS=server1.fqdn,server2.fqdn
docker plugin enable gfs
Dann kann man sehr simple Docker anweisen, die Volumes mit dem PlugIn GFS verteilt abzulegen.
volumes:
htdocs:
driver: gfs:latest
name: "gv0/htdocs"
Will man die Performance optimieren, gibt es eine Vielzahl an Parametern, die man setzen kann.
gluster volume set gv0 nfs.disable on
gluster volume set gv0 performance.cache-max-file-size 128MB
gluster volume set gv0 performance.cache-size 256MB
gluster volume set gv0 performance.flush-behind on
Und hiermit kann man den Zustand des Systems einsehen.
gluster volume info
gluster volume status
gluster volume heal gv0 info summary
Hier finden sich einige nützliche und interessante Links an, die sich im Laufe der Recherche angesammelt haben.