Een databaseswitch met HAProxy en de HAProxy Runtime API
Met de HAProxy Runtime API kunnen we backend databaseservers handmatig en met een script of programma toevoegen en verwijderen.
Een van mijn projecten had een database met hoge beschikbaarheid nodig (willen we dat niet allemaal...). Dit betekent (asynchrone) replicatie en een opstelling met meerdere knooppunten. Er bestaan verschillende oplossingen, waarover ik in een andere post zal schrijven. In deze scenario's hebben we meerdere replica's van de hoofddatabase, en als er een probleem optreedt, schakelen we van de ene database naar de andere.
Om dit wisselen van databases te doen zonder de client en databases aan te raken, moet de client de database niet direct benaderen, maar via een proxy. En dat is precies waar deze post over gaat. We implementeren een 'handmatige switch' tussen de databases. Dit is slechts een demonstratie van hoe dit kan werken.
Zoals altijd doe ik dit op Ubuntu 22.04.
Database proxy: HAProxy
Er zijn veel keuzes voor een database proxy. Hier gebruiken we HAProxy. mDeze software wordt vaak gebruikt om dist belastingen te verdelen tussen webapplicaties, waarbij het fungeert als een load balancer, maar het heeft veel meer opties.
HAProxy gezondheidscontroles zijn niet bruikbaar voor ons doel
HAProxy kan de gezondheid van backendservers controleren en regels gebruiken om de beste backendserver voor een verzoek te selecteren. Dat is geweldig, maar we hebben een aantal databases en alleen degene die we hebben gespecificeerd, moet worden benaderd door de client.
HAProxy ondersteunt ook een externe agent voor gezondheidscontroles. We kunnen deze agent zelf maken. In geavanceerde scenario's kan de agent veel meer informatie verzamelen over het totale systeem en betere beslissingen nemen dan HAProxy.
Het blijkt dat het gebruik van een HAProxy externe agent problematisch is voor ons doel, onder andere omdat de initiële status niet 'maintenance' of 'down' is en er geen manier is om dit te veranderen. De HAProxy externe agent kan het beste worden gebruikt voor load balancing, niet voor aan/uit-schakelen.
HAProxy runtime API
HAProxy heeft ook een API, de HAProxy Runtime API. Met deze API krijgen we de controle die we willen. We kunnen nieuwe backendservers toevoegen, de status van de servers wijzigen en backendservers verwijderen. Dat is precies wat we zoeken!
Projectoverzicht
We hebben twee databases, waarvan er maar één toegankelijk is voor de klant. We doen dit door er een database proxy service tussen te zetten, en natuurlijk hebben we een soort manager nodig om de database proxy te beheren. Hieronder staat een schema van wat we willen bereiken.
+--------+ +----------+ +-------------+
| client | | db-proxy | | backend-db1 |
| | | | | |
| |------|8320 |------+-------------|8310 |
| | | | | | |
+--------+ | | | +-------------+
| | |
| | | +-------------+
statistics ----|8360 | | | backend-db2 |
| | | | |
| api |-------------+------|8311 |
| 9999 | | | | |
+----------+ | | +-------------+
^ | |
| | |
| +------------------+
| | db-proxy-man |
+----- | |
| |
+------------------+
^
|
select backend
Korte beschrijving van de componenten in deze demonstratie:
- client
Eenvoudig programma dat een verzoek stuurt naar db-proxy:8620 en het antwoord afdrukt. Het programma is niets meer dan een Shell-script one-liner, die elke seconde een verzoek verstuurt. - db-proxy
Dit is de HAProxy. We schakelen de HAProxy Runtime API in op poort 9999. - db-proxy-man
Vanaf hier sturen we commando's naar de API, op db-proxy:9999. We kunnen dit handmatig doen of een Shell-script of Python programma maken om onze geselecteerde databaseserver toe te voegen aan de (lege) lijst met backendservers. - backend-db1, backend-db2
Onze databaseservers. We gebruiken geen echte databaseservers maar echoservers. Ze echoën wat wordt ontvangen met een prefix.
De bestanden in dit project zijn:
.
├── docker-compose.yml
├── Dockerfile
└── haproxy.cfg
Je kunt eventueel een .env bestand toevoegen en DOCKER_COMPOSE_PROJECT instellen op de naam van je project, bijvoorbeeld db-proxy-demo.
Het bestand docker-compose.yml
Het bestand docker-compose.yml bevat alle elementen die we hierboven hebben genoemd. We voegen de 'hostname' toe voor gemakkelijke verwijzing (wanneer we Docker Swarm gebruiken, laten we de '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
Persoonlijke mening: laat Docker-Compose nooit netwerken aanmaken. Doe dit handmatig:
docker network create frontend-db-network
docker network create db-proxy-network
docker network create backend-db1-network
docker network create backend-db2-network
Het haproxy.cfg bestand en Dockerfile
We voegen hier geen backend databaseservers toe, in plaats daarvan doen we dat met de HAProxy Runtime API.
# 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
Mogelijk wilt u het logniveau wijzigen van 'debug' in 'info'.
De regel om de HAProxy Runtime API beschikbaar te maken:
stats socket ipv4@*:9999 level admin
De Dockerfile wordt gebruikt om de image te bouwen, er is niet veel aan:
# Dockerfile
FROM haproxy:lts-alpine3.20
COPY haproxy.cfg /usr/local/etc/haproxy/haproxy.cfg
Het project ophalen
Eerst bouwen we de image:
docker-compose build --no-cache
Dan starten we het op:
docker-compose up
In de terminal zie je het volgende:
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
...
Merk op dat de 'client' verzoeken stuurt naar onze db-proxy. Deze verzoeken worden niet beantwoord.
Om de statuspagina te zien, wijst u uw browser naar:
http://127.0.0.1:8360/stats
Verwijder de containers voordat u opnieuw gaat bouwen:
docker-compose down
Opdrachten sturen naar de HAProxy Runtime API
Volgens de documentatie:
- Een server die wordt toegevoegd met behulp van de API wordt een dynamische server genoemd.
- De backend moet worden geconfigureerd om een dynamisch load balancing-algoritme te gebruiken.
- Een dynamische server wordt niet hersteld na een herlaadoperatie van de loadbalancer.
- "Momenteel wordt een dynamische server statisch geïnitialiseerd met de methode "none" init-addr. Dit betekent dat er geen resolutie wordt uitgevoerd als er een FQDN als adres wordt opgegeven, zelfs als het aanmaken van de server wordt gevalideerd."
Om commando's naar de API te sturen, gebruiken we het 'nc' (netcat) commando dat aanwezig is in de busybox image:
echo "<your-command>" | nc db-proxy 9999
We gebruiken slechts een paar commando's om servers toe te voegen en te verwijderen, merk op dat we onze backend in haproxy.cfg de naam 'db_servers' hebben gegeven.
De status van de servers weergeven:
show servers state
Om een server toe te voegen:
add server <backend-name>/<server-name> <addr>:<port>
set server <backend-name>/<server-name> state ready
Het eerste commando voegt een server toe, maar deze zal in onderhoudsmodus staan. Het tweede commando maakt de server gereed.
Zie ook hierboven: De HAProxy Runtime API lost hostnamen van dynamische servers niet voor ons op! Dit betekent dat we hier een IP address moeten gebruiken, niet de hostnaam!
Een server verwijderen:
set server <backend-name>/<server-name> state maint
del server <backend-name>/<server-name>
Nu in het echt: Servers toevoegen en verwijderen
Om commando's naar de API te sturen, gaan we de container db-proxy-man busybox binnen:
docker exec -it $(docker container ls | grep db-proxy-man | awk '{print $1; exit}') sh
Tijdens de commando's kun je de statuspagina in je browser bekijken.
We kennen de volgende namen toe aan onze servers:
backend_db1_8310
backend_db2_8311
Zoals hierboven al vermeld, lost de API wel de IP address hostnamen van dynamische servers op. We gebruiken de opdracht 'nslookup' om de IP address te krijgen:
nslookup backend-db1
Resultaat:
Server: 127.0.0.11
Address: 127.0.0.11:53
Non-authoritative answer:
Name: backend-db1
Address: 172.17.13.2
Laten we de status van de servers controleren:
echo "show servers state" | nc db-proxy 9999
Resultaat:
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
We zien dat er geen servers aanwezig zijn.
Voeg nu een server toe:
echo "add server db_servers/backend_db1_8310 172.17.13.2:8310" | nc db-proxy 9999
Resultaat:
New server registered.
In de browser zie je dat deze server is toegevoegd met de status 'MAINT'.
Vervolgens schakelen we de server in door de status te veranderen in 'ready':
echo "set server db_servers/backend_db1_8310 state ready" | nc db-proxy 9999
Je kunt deze wijzigingen ook zien in het logboek Docker-Compose:
...
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
...
Na het veranderen van de status van de server naar 'ready', zien we dat backend-db1 nu reageert op 'client' verzoeken,
en de 'client' ontvangt de gegevens van backend-db1. Geweldig!
Controleer nogmaals de status van de server:
echo "show servers state" | nc db-proxy 9999
Resultaat, waaruit blijkt dat onze server is toegevoegd:
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
Laten we nu deze server verwijderen, door de server eerst in onderhoudstoestand te zetten:
echo "set server db_servers/backend_db1_8310 state maint" | nc db-proxy 9999
En vervolgens de server verwijderen:
echo "del server db_servers/backend_db1_8310" | nc db-proxy 9999
Je kunt de veranderingen zien in de logs en de browser.
Nu we weten hoe de commando's werken, kunnen we een Shell-script, Bash-script of een Python programma schrijven om commando's naar de API te sturen.
Python gebruiken om toegang te krijgen tot de HAProxy Runtime API
Dit is een blog over Python maar ik ga niet in op details van een Python programma om het bovenstaande te doen.
Ik laat alleen de functie zien die je kunt gebruiken om toegang te krijgen tot de API:
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
...
En het programma zou zoiets moeten doen:
# 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
...
Als de IP address van de backend servers kan veranderen, dan controleren we hier ook op veranderingen.
Samenvatting
We hebben HAProxy gebruikt om een switch te maken voor onze databases. HAProxy is een erg complex stuk software en hoewel de HAProxy documentatie uitgebreid is, is het soms ook onduidelijk. Ik denk dat dit te maken heeft met de voortdurende ontwikkeling van HAProxy. Het gebruik van de HAProxy Runtime API is eenvoudig, maar de informatie die kan worden opgevraagd over de backend servers is beperkt.
Maar uiteindelijk werkt HAProxy gewoon en zou het deel moeten uitmaken van de gereedschapskist van elke systeemontwikkelaar.
Links / credits
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
Lees meer
Database Docker Docker-compose
Recent
Meest bekeken
- Basis taakplanning met APScheduler
- Voorkomen dat dubbele berichten naar een extern systeem worden gestuurd
- LSTM meerstappen-optimalisatie hyperparameter met Keras Tuner
- Documenteren van een Flask RESTful API met OpenAPI (Swagger) met gebruikmaking van APISpec
- Met behulp van Python's pyOpenSSL om SSL-certificaten die van een host zijn gedownload te controleren
- IPv6 gebruiken met Microk8s