angle-uparrow-clockwisearrow-counterclockwisearrow-down-uparrow-leftatcalendarcard-listchatcheckenvelopefolderhouseinfo-circlepencilpeoplepersonperson-fillperson-plusphoneplusquestion-circlesearchtagtrashx

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.

13 August 2024
post main image
https://unsplash.com/@georgiadelotz

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

Einen Kommentar hinterlassen

Kommentieren Sie anonym oder melden Sie sich zum Kommentieren an.

Kommentare

Eine Antwort hinterlassen

Antworten Sie anonym oder melden Sie sich an, um zu antworten.