Ein Datenbankschalter mit HAProxy und der HAProxy Runtime API
Mit der HAProxy Runtime API können wir Backend-Datenbankserver manuell und mit einem Skript oder Programm hinzufügen und entfernen.
Eines meiner Projekte benötigte eine Hochverfügbarkeitsdatenbank (wollen wir das nicht alle?), d.h. (asynchrone) Replikation und ein Multi-Node-Setup. Es gibt mehrere Lösungen, über die ich in einem anderen Beitrag schreiben werde. In diesen Szenarien haben wir mehrere Replikate der Hauptdatenbank, und wenn ein Problem auftritt, wechseln wir von einer Datenbank zur anderen.
Um dieses Umschalten der Datenbanken zu ermöglichen, ohne den Client und die Datenbanken zu berühren, muss der Client nicht direkt auf die Datenbank zugreifen, sondern über einen proxy. Und genau darum geht es in diesem Beitrag. Wir implementieren eine "manuelle Umschaltung" zwischen den Datenbanken. Dies ist nur eine Demonstration, wie das funktionieren kann.
Wie immer mache ich das auf Ubuntu 22.04.
Datenbank proxy: HAProxy
Es gibt viele Möglichkeiten für eine Datenbank proxy. Hier verwenden wir HAProxy. mDiese Software wird oft verwendet, um dist Lasten zwischen Webanwendungen zu verteilen, indem sie als Load Balancer fungiert, aber sie hat noch viele weitere Optionen.
HAProxy -Gesundheitsprüfungen sind für unseren Zweck nicht verwendbar
HAProxy kann den Zustand von Backend-Servern überprüfen und anhand von Regeln den besten Backend-Server für eine Anfrage auswählen. Das ist großartig, aber wir haben eine Reihe von Datenbanken und nur auf die eine, die wir angegeben haben, soll der Client zugreifen.
HAProxy unterstützt auch einen externen Agenten für Health Checks. Diesen Agenten können wir selbst erstellen. In fortgeschrittenen Szenarien kann der Agent viel mehr Informationen über das gesamte System sammeln und bessere Entscheidungen treffen als HAProxy.
Es scheint, dass die Verwendung eines externen HAProxy -Agenten für unsere Zwecke problematisch ist, unter anderem deshalb, weil der Ausgangszustand nicht "Wartung" oder "Ausfall" ist und es keine Möglichkeit gibt, dies zu ändern. Der externe Agent HAProxy eignet sich am besten für den Lastausgleich, nicht für das Ein- und Ausschalten.
HAProxy Laufzeit API
Zu HAProxy gibt es auch einen API, den HAProxy Runtime API. Mit diesem API haben wir die Kontrolle, die wir wollen. Wir können neue Backend-Server hinzufügen, den Zustand der Server ändern und Backend-Server löschen. Das ist genau das, wonach wir suchen!
Projektübersicht
Wir haben zwei Datenbanken, von denen nur eine für den Kunden zugänglich ist. Wir tun dies, indem wir einen Datenbank proxy Dienst dazwischen schalten, und natürlich brauchen wir eine Art Manager, um die Datenbank proxy zu kontrollieren. Unten sehen Sie ein Diagramm dessen, was wir erreichen wollen.
+--------+ +----------+ +-------------+
| client | | db-proxy | | backend-db1 |
| | | | | |
| |------|8320 |------+-------------|8310 |
| | | | | | |
+--------+ | | | +-------------+
| | |
| | | +-------------+
statistics ----|8360 | | | backend-db2 |
| | | | |
| api |-------------+------|8311 |
| 9999 | | | | |
+----------+ | | +-------------+
^ | |
| | |
| +------------------+
| | db-proxy-man |
+----- | |
| |
+------------------+
^
|
select backend
Kurze Beschreibung der Komponenten in dieser Demonstration:
- client
Einfaches Programm, das eine Anfrage an db-proxy:8620 sendet und die Antwort ausgibt. Das Programm ist nichts weiter als ein Shell-Skript-Einzeiler, der jede Sekunde eine Anfrage sendet. - db-proxy
Dies ist der HAProxy. Wir aktivieren die HAProxy Runtime API auf Port 9999. - db-proxy-man
Von hier aus senden wir Befehle an den API, an db-proxy:9999. Wir können dies manuell tun oder ein Shell-Skript oder Python -Programm erstellen, um unseren ausgewählten Datenbankserver zur (leeren) Liste der Backend-Server hinzuzufügen. - backend-db1, backend-db2
Unsere Datenbankserver. Wir verwenden keine echten Datenbankserver, sondern Echo-Server. Sie echoten, was mit einem prefix empfangen wird.
Die Dateien in diesem Projekt sind:
.
├── docker-compose.yml
├── Dockerfile
└── haproxy.cfg
Sie können eine .env -Datei hinzufügen und DOCKER_COMPOSE_PROJECT auf den Namen Ihres Projekts setzen, z.B. db-proxy-demo.
Die Datei docker-compose.yml
Die Datei docker-compose.yml enthält alle Elemente, die wir oben erwähnt haben. Wir fügen den "hostname" zur einfachen Referenzierung hinzu (wenn wir Docker Swarm verwenden, lassen wir den "hostname" weg).
# docker-compose.yml
version: "3.7"
x-service_defaults: &service_defaults
restart: always
logging:
driver: json-file
options:
max-size: "10m"
max-file: "5"
services:
client:
<< : *service_defaults
image: busybox:1.35.0
hostname: client
command:
- /bin/sh
- -c
- "i=0; while true; do echo \"sending: hello $$i \"; echo \"hello $$i\" | nc db-proxy 8320; i=$$((i+1)); sleep 1; done"
networks:
- frontend-db-network
db-proxy:
<< : *service_defaults
image: haproxy:1.001-d
hostname: db-proxy
build:
context: .
dockerfile: Dockerfile
ports:
# http://127.0.0.1:8360/stats
- "127.0.0.1:8360:8360"
networks:
- db-proxy-network
- frontend-db-network
- backend-db1-network
- backend-db2-network
db-proxy-man:
<< : *service_defaults
image: busybox:1.35.0
hostname: db-proxy-man
# keep alive
command: tail -f /dev/null
networks:
- db-proxy-network
- backend-db1-network
- backend-db2-network
backend-db1:
<< : *service_defaults
image: venilnoronha/tcp-echo-server
hostname: backend-db1
# echo with a prefix
entrypoint: /bin/main 8310 "backend-db1 at 8310:"
networks:
- backend-db1-network
backend-db2:
<< : *service_defaults
image: venilnoronha/tcp-echo-server
hostname: backend-db2
# echo with a prefix
entrypoint: /bin/main 8311 "backend-db2 at 8311:"
networks:
- backend-db2-network
networks:
frontend-db-network:
external: true
name: frontend-db-network
db-proxy-network:
external: true
name: db-proxy-network
backend-db1-network:
external: true
name: backend-db1-network
backend-db2-network:
external: true
name: backend-db2-network
Persönliche Meinung: Lassen Sie Docker-Compose niemals Netzwerke erstellen. Tun Sie dies manuell:
docker network create frontend-db-network
docker network create db-proxy-network
docker network create backend-db1-network
docker network create backend-db2-network
Die haproxy.cfg-Datei und Dockerfile
Wir fügen hier keine Backend-Datenbankserver hinzu, sondern fügen sie mit der HAProxy Runtime API hinzu.
# haproxy.cfg
global
maxconn 100
stats socket ipv4@*:9999 level admin
log stdout format raw local0 debug
#log stdout format raw local0 info
defaults
log global
retries 2
timeout client 30m
timeout connect 15s
timeout server 30m
timeout check 15s
frontend db
mode tcp
option tcplog
bind :8320
default_backend db_servers
backend db_servers
balance roundrobin
mode tcp
option tcplog
# servers here are added using the HAProxy Runtime API
frontend stats
bind :8360
mode http
stats enable
stats uri /stats
stats refresh 5s
#stats auth username:password
stats admin if LOCALHOST
Möglicherweise möchten Sie die Protokollebene von 'debug' auf 'info' ändern.
Die Zeile, um die HAProxy Runtime API verfügbar zu machen:
stats socket ipv4@*:9999 level admin
Die Dockerfile wird zum Erstellen des Images verwendet, es gibt nicht viel dazu:
# Dockerfile
FROM haproxy:lts-alpine3.20
COPY haproxy.cfg /usr/local/etc/haproxy/haproxy.cfg
Starten Sie das Projekt
Zuerst bauen wir das Image:
docker-compose build --no-cache
Dann starten wir es:
docker-compose up
Sie werden folgendes im Terminal sehen:
backend-db2_1 | listening on [::]:8311, prefix: backend-db2 at 8311:
backend-db1_1 | listening on [::]:8310, prefix: backend-db1 at 8310:
client_1 | sending: hello 0
db-proxy_1 | [NOTICE] (1) : haproxy version is 3.0.3-95a607c
db-proxy_1 | [WARNING] (1) : config : parsing [/usr/local/etc/haproxy/haproxy.cfg:25] : backend 'db_servers' : 'option tcplog' directive is ignored in backends.
db-proxy_1 | [NOTICE] (1) : New worker (8) forked
db-proxy_1 | [NOTICE] (1) : Loading success.
db-proxy_1 | 172.17.15.2:38293 [13/Aug/2024:08:05:01.543] db db_servers/<NOSRV> -1/-1/0 0 SC 1/1/0/0/0 0/0
client_1 | sending: hello 1
db-proxy_1 | 172.17.15.2:46745 [13/Aug/2024:08:05:02.548] db db_servers/<NOSRV> -1/-1/0 0 SC 1/1/0/0/0 0/0
client_1 | sending: hello 2
db-proxy_1 | 172.17.15.2:37693 [13/Aug/2024:08:05:03.552] db db_servers/<NOSRV> -1/-1/0 0 SC 1/1/0/0/0 0/0
client_1 | sending: hello 3
...
Beachten Sie, dass der "Client" Anfragen an unsere db-proxy sendet. Diese Anfragen werden nicht beantwortet.
Um die Statusseite zu sehen, rufen Sie Ihren Browser auf:
http://127.0.0.1:8360/stats
Entfernen Sie vor dem Wiederaufbau die Container:
docker-compose down
Senden von Befehlen an die HAProxy Runtime API
Gemäß der Dokumentation:
- Ein Server, der mit dem API hinzugefügt wird, wird als dynamischer Server bezeichnet.
- Das Backend muss für die Verwendung eines dynamischen Lastausgleichsalgorithmus konfiguriert sein.
- Ein dynamischer Server wird nach einem Reload des Load Balancers nicht wiederhergestellt.
- "Derzeit wird ein dynamischer Server statisch mit der init-addr-Methode "none" initialisiert. Dies bedeutet, dass keine Auflösung vorgenommen wird, wenn ein FQDN als Adresse angegeben wird, auch wenn die Servererstellung validiert wird."
Um Befehle an den API zu senden, verwenden wir den Befehl 'nc' (netcat), der im Image busybox vorhanden ist:
echo "<your-command>" | nc db-proxy 9999
Wir verwenden nur einige wenige Befehle, um Server hinzuzufügen und zu entfernen. Beachten Sie, dass wir unser Backend in haproxy.cfg "db_servers" genannt haben.
So zeigen Sie den Status der Server an:
show servers state
Um einen Server hinzuzufügen:
add server <backend-name>/<server-name> <addr>:<port>
set server <backend-name>/<server-name> state ready
Der erste Befehl fügt einen Server hinzu, aber er wird im Wartungsmodus sein. Der zweite Befehl macht den Server bereit.
Siehe dazu auch oben: Die HAProxy Runtime API löst keine Hostnamen von dynamischen Servern für uns auf! Das heißt, wir müssen hier eine IP address verwenden, nicht den Hostnamen!
Um einen Server zu entfernen:
set server <backend-name>/<server-name> state maint
del server <backend-name>/<server-name>
Jetzt aber wirklich: Hinzufügen und Entfernen von Servern
Um Befehle an den API zu senden, geben wir den Container db-proxy-man busybox ein:
docker exec -it $(docker container ls | grep db-proxy-man | awk '{print $1; exit}') sh
Während der Befehle können Sie die Statusseite in Ihrem Browser beobachten.
Wir weisen unseren Servern die folgenden Namen zu:
backend_db1_8310
backend_db2_8311
Wie bereits oben erwähnt, löst der API den IP address von Hostnamen dynamischer Server auf. Wir verwenden den Befehl 'nslookup', um die IP address zu ermitteln:
nslookup backend-db1
Ergebnis:
Server: 127.0.0.11
Address: 127.0.0.11:53
Non-authoritative answer:
Name: backend-db1
Address: 172.17.13.2
Überprüfen wir den Zustand der Server:
echo "show servers state" | nc db-proxy 9999
Ergebnis:
1
# be_id be_name srv_id srv_name srv_addr srv_op_state srv_admin_state srv_uweight srv_iweight srv_time_since_last_change srv_check_status srv_check_result srv_check_health srv_check_state srv_agent_state bk_f_forced_id srv_f_forced_id srv_fqdn srv_port srvrecord srv_use_ssl srv_check_port srv_check_addr srv_agent_addr srv_agent_port
Es wird nur die Kopfzeile angezeigt. Es sind keine Server vorhanden.
Fügen Sie nun einen Server hinzu:
echo "add server db_servers/backend_db1_8310 172.17.13.2:8310" | nc db-proxy 9999
Ergebnis:
New server registered.
Im Browser sehen Sie, dass dieser Server mit dem Status 'MAINT' hinzugefügt wurde.
Als nächstes aktivieren wir den Server, indem wir den Status auf 'bereit' ändern:
echo "set server db_servers/backend_db1_8310 state ready" | nc db-proxy 9999
Sie können diese Änderungen auch im Docker-Compose-Protokoll sehen:
...
client_1 | sending: hello 61
db-proxy_1 | 172.17.15.2:38285 [13/Aug/2024:10:05:54.779] db db_servers/<NOSRV> -1/-1/0 0 SC 2/1/0/0/0 0/0
db-proxy_1 | Connect from 172.17.13.1:54778 to 172.17.13.4:8360 (stats/HTTP)
db-proxy_1 | Server db_servers/backend_db1_8310 is UP/READY (leaving forced maintenance).
db-proxy_1 | [WARNING] (8) : Server db_servers/backend_db1_8310 is UP/READY (leaving forced maintenance).
client_1 | sending: hello 62
backend-db1_1 | request: hello 62
backend-db1_1 | response: backend-db1 at 8310: hello 62
db-proxy_1 | 172.17.15.2:34601 [13/Aug/2024:10:05:55.781] db db_servers/backend_db1_8310 1/0/0 30 -- 2/1/0/0/0 0/0
client_1 | backend-db1 at 8310: hello 62
client_1 | sending: hello 63
backend-db1_1 | request: hello 63
backend-db1_1 | response: backend-db1 at 8310: hello 63
client_1 | backend-db1 at 8310: hello 63
db-proxy_1 | 172.17.15.2:42305 [13/Aug/2024:10:05:56.784] db db_servers/backend_db1_8310 1/0/0 30 -- 2/1/0/0/0 0/0
client_1 | sending: hello 64
...
Nachdem wir den Serverstatus auf 'ready' geändert haben, sehen wir, dass backend-db1 jetzt auf 'Client'-Anfragen antwortet und der 'Client' die Daten von backend-db1 erhält. Großartig!
Überprüfen Sie erneut den Status des Servers:
echo "show servers state" | nc db-proxy 9999
Ergebnis, das zeigt, dass unser Server hinzugefügt wurde:
1
# be_id be_name srv_id srv_name srv_addr srv_op_state srv_admin_state srv_uweight srv_iweight srv_time_since_last_change srv_check_status srv_check_result srv_check_health srv_check_state srv_agent_state bk_f_forced_id srv_f_forced_id srv_fqdn srv_port srvrecord srv_use_ssl srv_check_port srv_check_addr srv_agent_addr srv_agent_port
3 db_servers 1 backend_db1_8310 172.17.13.2 2 0 1 1 10 1 0 0 0 0 0 0 - 8310 - 0 0 - - 0
Entfernen wir nun diesen Server, indem wir den Server zunächst in den Wartungszustand versetzen:
echo "set server db_servers/backend_db1_8310 state maint" | nc db-proxy 9999
Und als nächstes entfernen wir den Server:
echo "del server db_servers/backend_db1_8310" | nc db-proxy 9999
Sie können die Änderungen in den Protokollen und im Browser sehen.
Da wir nun wissen, wie die Befehle funktionieren, können wir ein Shell-Skript, Bash-Skript oder ein Python -Programm schreiben, um Befehle an den API zu senden.
Verwendung von Python für den Zugriff auf die HAProxy Runtime API
Dies ist ein Blog über Python , aber ich gehe nicht in die Details eines Python -Programms ein, um das oben genannte zu tun.
Ich zeige nur die Funktion, die Sie verwenden können, um auf die API zuzugreifen:
import socket
import typing as _t
BUFFER_SIZE = 4096
class HAProxyRuntimeAPIUtils:
def __init__(
self,
host: _t.Optional[str] = 'db-proxy',
port: _t.Optional[int] = 9999,
):
self.host = host
self.port = port
def send(self, send_data: str) -> _t.Optional[str]:
send_bytes = bytes(send_data + '\n', 'utf-8')
client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
client_socket.connect((self.host, self.port))
client_socket.send(send_bytes)
recv_data = client_socket.recv(BUFFER_SIZE).decode('utf-8').strip()
client_socket.close()
return recv_data
...
Das Programm sollte in etwa so funktionieren:
# set the selected_server
# keep repeating as db-proxy may restart
while True:
try:
# get all servers for the backend
...
# delete all servers that are not the selected_server
...
# if not present, add the selected_server, with the proper addr, and set to ready
...
Wenn sich die IP address der Backend-Server ändern kann, dann prüfen wir auch hier auf Änderungen.
Zusammenfassung
Wir haben HAProxy verwendet, um einen Switch für unsere Datenbanken zu erstellen. HAProxy ist ein sehr komplexes Stück Software, und obwohl die HAProxy -Dokumentation umfangreich ist, ist sie manchmal auch unklar. Ich glaube, das hat mit der ständigen Weiterentwicklung von HAProxy zu tun. Die Verwendung der HAProxy Runtime API ist einfach, aber die Informationen, die über die Backend-Server abgerufen werden können, sind begrenzt.
Letztendlich funktioniert HAProxy aber einfach und sollte in der Toolbox eines jeden Systementwicklers vorhanden sein.
Links / Impressum
Dynamic DNS Resolution with HAProxy and Docker
https://stackoverflow.com/questions/41152408/dynamic-dns-resolution-with-haproxy-and-docker
HAProxy
https://en.wikipedia.org/wiki/HAProxy
HAProxy Runtime API
https://www.haproxy.com/documentation/haproxy-runtime-api/
venilnoronha/tcp-echo-server
https://hub.docker.com/r/venilnoronha/tcp-echo-server
Mehr erfahren
Database Docker Docker-compose
Neueste
- Zeitreihenchart mit Flask, Bootstrap und Chart.js
- Verwendung von IPv6 mit Microk8s
- Verwendung von Ingress für den Zugriff auf RabbitMQ auf einem Microk8s -Cluster
- Einfache Videogalerie mit Flask, Jinja, Bootstrap und JQuery
- Grundlegende Auftragsplanung mit APScheduler
- Ein Datenbankschalter mit HAProxy und der HAProxy Runtime API