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

Un commutateur de base de données avec HAProxy et HAProxy Runtime API

En utilisant le Runtime HAProxy API, nous pouvons ajouter et supprimer des serveurs de base de données backend manuellement et à l'aide d'un script ou d'un programme.

13 août 2024
post main image
https://unsplash.com/@georgiadelotz

Un de mes projets nécessitait une base de données à haute disponibilité (n'est-ce pas ce que nous voulons tous ...), ce qui signifie une réplication (asynchrone) et une configuration multi-nœuds. Plusieurs solutions existent, j'en parlerai dans un autre article. Dans ces scénarios, nous avons plusieurs répliques de la base de données principale, et lorsqu'un problème survient, nous passons d'une base de données à l'autre.

Pour faire ce changement de base de données sans toucher au client et aux bases de données, le client doit accéder à la base de données non pas directement mais via un proxy. Et c'est exactement ce dont il est question dans cet article. Nous mettons en œuvre un "commutateur manuel" entre les bases de données. Il s'agit simplement d'une démonstration de la manière dont cela peut fonctionner.

Comme toujours, je le fais sur Ubuntu 22.04.

Base de données proxy : HAProxy

Il existe de nombreux choix pour la base de données proxy. Nous utilisons ici HAProxy. mCe logiciel est souvent utilisé pour distribuer les charges entre les applications web, agissant comme un équilibreur de charge, mais il a beaucoup plus d'options.

Les contrôles de santé de HAProxy ne sont pas utilisables pour notre objectif.

HAProxy peut vérifier la santé des serveurs backend et utiliser des règles pour sélectionner le meilleur serveur backend pour une requête. C'est très bien, mais nous avons un certain nombre de bases de données et seul celui que nous avons spécifié doit être accessible par le client.

HAProxy supporte également un agent externe pour les contrôles de santé. Nous pouvons créer cet agent nous-mêmes. Dans les scénarios avancés, l'agent peut recueillir beaucoup plus d'informations sur l'ensemble du système et prendre de meilleures décisions que HAProxy.

Il semble que l'utilisation d'un agent externe HAProxy soit problématique pour notre objectif, l'une des raisons étant que l'état initial n'est pas "maintenance" ou "down" et qu'il n'y a aucun moyen de le modifier. L'agent externe HAProxy est mieux adapté à l'équilibrage de la charge, et non à la commutation marche/arrêt.

HAProxy Runtime API

HAProxy a également un API, le HAProxy Runtime API. Avec ce API , nous obtenons le contrôle que nous voulons. Nous pouvons ajouter de nouveaux serveurs dorsaux, modifier l'état des serveurs et supprimer des serveurs dorsaux. C'est exactement ce que nous recherchons !

Aperçu du projet

Nous avons deux bases de données, dont une seule est accessible au client. Pour ce faire, nous plaçons un service de base de données proxy entre les deux, et bien sûr, nous avons besoin d'une sorte de gestionnaire pour contrôler la base de données proxy. Le diagramme ci-dessous illustre ce que nous voulons réaliser.

  +--------+      +----------+                    +-------------+
  | client |      | db-proxy |                    | backend-db1 |
  |        |      |          |                    |             |
  |        |------|8320      |------+-------------|8310         |
  |        |      |          |      |             |             |
  +--------+      |          |      |             +-------------+
                  |          |      |
                  |          |      |             +-------------+
   statistics ----|8360      |      |             | backend-db2 |
                  |          |      |             |             |
                  |  api     |-------------+------|8311         |
                  |  9999    |      |      |      |             |
                  +----------+      |      |      +-------------+
                       ^            |      |   
                       |            |      |    
                       |      +------------------+
                       |      |   db-proxy-man   |
                       +----- |                  |
                              |                  |
                              +------------------+
                                       ^
                                       |
                                select backend      

Brève description des composants de cette démonstration :

  • client
    Programme simple envoyant une requête à db-proxy:8620 et imprimant la réponse. Le programme n'est rien d'autre qu'un script Shell one-liner, envoyant une requête toutes les secondes.
  • db-proxy
    Il s'agit du HAProxy. Nous activons le Runtime HAProxy API sur le port 9999.
  • db-proxy-man
    A partir de là, nous envoyons des commandes au API, sur le port db-proxy:9999. Nous pouvons effectuer cette opération manuellement ou créer un script Shell ou un programme Python pour ajouter le serveur de base de données sélectionné à la liste (vide) des serveurs dorsaux.
  • backend-db1, backend-db2
    Nos serveurs de base de données. Nous n'utilisons pas de véritables serveurs de base de données, mais des serveurs d'écho. Ils répercutent ce qui est reçu avec un prefix.

Les fichiers de ce projet sont les suivants :

.
├── docker-compose.yml
├── Dockerfile
└── haproxy.cfg

Vous pouvez ajouter un fichier .env et définir DOCKER_COMPOSE_PROJECT avec le nom de votre projet, par exemple db-proxy-demo.

Le fichier docker-compose.yml

Le fichier docker-compose.yml contient tous les éléments mentionnés ci-dessus. Nous ajoutons le 'hostname' pour faciliter le référencement (lorsque nous utilisons Docker Swarm, nous omettons le 'hostname').

# 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  

Avis personnel : ne laissez jamais Docker-Compose créer des réseaux. Faites-le manuellement :

docker network create frontend-db-network
docker network create db-proxy-network
docker network create backend-db1-network
docker network create backend-db2-network

Le fichier haproxy.cfg et Dockerfile.

Nous n'ajoutons pas ici les serveurs de base de données dorsaux, mais nous les ajoutons en utilisant le Runtime HAProxy 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

Vous voudrez peut-être changer le niveau du journal de 'debug' à 'info'.

La ligne pour rendre disponible le Runtime HAProxy API :

  stats socket ipv4@*:9999 level admin

Le Dockerfile est utilisé pour construire l'image, il n'y a pas grand chose à faire :

# Dockerfile 
FROM haproxy:lts-alpine3.20
COPY haproxy.cfg /usr/local/etc/haproxy/haproxy.cfg

Lancer le projet

Tout d'abord, nous construisons l'image :

docker-compose build --no-cache

Ensuite, nous la démarrons :

docker-compose up

Vous verrez ce qui suit dans le terminal :

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 
...

Notez que le 'client' envoie des requêtes à notre db-proxy. Ces requêtes ne sont pas traitées.

Pour voir la page d'état, pointez votre navigateur sur :

http://127.0.0.1:8360/stats

Avant de reconstruire, enlevez les conteneurs :

docker-compose down

Envoi de commandes au runtime HAProxy API

D'après la documentation :

  • Un serveur ajouté à l'aide de API est appelé serveur dynamique.
  • Le backend doit être configuré pour utiliser un algorithme d'équilibrage de charge dynamique.
  • Un serveur dynamique n'est pas restauré après une opération de rechargement de l'équilibreur de charge.
  • "Actuellement, un serveur dynamique est initialisé statiquement avec la méthode init-addr "none". Cela signifie qu'aucune résolution ne sera entreprise si un FQDN est spécifié comme adresse, même si la création du serveur sera validée."

Pour envoyer des commandes au API , nous utilisons la commande 'nc' (netcat) présente dans l'image busybox :

echo "<your-command>" | nc db-proxy 9999

Nous n'utilisons que quelques commandes pour ajouter et supprimer des serveurs, notez que nous avons nommé notre backend dans haproxy.cfg : 'db_servers'.

Pour afficher l'état des serveurs :

show servers state

Pour ajouter un serveur :

add server <backend-name>/<server-name> <addr>:<port>
set server <backend-name>/<server-name> state ready

La première commande ajoute un serveur mais il sera en mode maintenance. La seconde commande rend le serveur prêt.

Là encore, voir ci-dessus : Le Runtime HAProxy API ne résout pas les noms d'hôtes des serveurs dynamiques pour nous ! Cela signifie que nous devons utiliser un IP address ici, et non le nom d'hôte !

Pour supprimer un serveur :

set server <backend-name>/<server-name> state maint
del server <backend-name>/<server-name>

Maintenant pour de vrai : Ajouter et supprimer des serveurs

Pour envoyer des commandes au API , nous entrons dans le conteneur db-proxy-man busybox :

docker exec -it $(docker container ls | grep db-proxy-man | awk '{print $1; exit}') sh

Pendant les commandes, vous pouvez consulter la page d'état dans votre navigateur.

Nous attribuons les noms suivants à nos serveurs :

backend_db1_8310
backend_db2_8311

Comme déjà mentionné ci-dessus, le API résout les IP address des noms d'hôtes des serveurs dynamiques. Nous utilisons la commande 'nslookup' pour obtenir le IP address :

nslookup backend-db1

Résultat :

Server:		127.0.0.11
Address:	127.0.0.11:53

Non-authoritative answer:
Name:	backend-db1
Address: 172.17.13.2

Vérifions l'état des serveurs :

echo "show servers state" | nc db-proxy 9999

Résultat :

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

Seule la ligne d'en-tête est affichée. Aucun serveur n'est présent.

Ajoutons maintenant un serveur :

echo "add server db_servers/backend_db1_8310 172.17.13.2:8310" | nc db-proxy 9999

Résultat :

New server registered.

Dans le navigateur, vous verrez que ce serveur a été ajouté avec le statut 'MAINT'.

Ensuite, nous activons le serveur en changeant son état en "prêt" :

echo "set server db_servers/backend_db1_8310 state ready" | nc db-proxy 9999

Vous pouvez également voir ces changements dans le journal 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 
...

Après avoir changé l'état du serveur en 'ready', nous voyons que backend-db1 répond maintenant aux requêtes du 'client', et que le 'client' reçoit les données de backend-db1. Très bien !

Vérifier à nouveau l'état du serveur :

echo "show servers state" | nc db-proxy 9999

Resultat, montrant que notre serveur a été ajouté :

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

Supprimons maintenant ce serveur, en le plaçant d'abord en état de maintenance :

echo "set server db_servers/backend_db1_8310 state maint" | nc db-proxy 9999

Et ensuite, supprimons le serveur :

echo "del server db_servers/backend_db1_8310" | nc db-proxy 9999

Vous pouvez voir les changements dans les logs et le navigateur.

Maintenant que nous savons comment les commandes fonctionnent, nous pouvons écrire un script Shell, un script Bash ou un programme Python pour envoyer des commandes au API.

Utilisation de Python pour accéder au Runtime HAProxy API

Ceci est un blog sur Python mais je ne vais pas entrer dans les détails d'un programme Python pour faire ce qui précède.
Je montre seulement la fonction que vous pouvez utiliser pour accéder à 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
    
    ...    

Le programme doit faire quelque chose comme ceci :

    # 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
            ...

Si le IP address des serveurs dorsaux peut changer, nous vérifions également les changements ici.

Résumé

Nous avons utilisé HAProxy pour créer un commutateur pour nos bases de données. HAProxy est un logiciel très complexe et, bien que la documentation de HAProxy soit très complète, elle n'est pas toujours claire. Je pense que cela est dû au développement continu de HAProxy. L'utilisation du Runtime HAProxy API est facile, mais les informations que l'on peut obtenir sur les serveurs backend sont limitées.

Mais en fin de compte, HAProxy fonctionne parfaitement et devrait faire partie de la boîte à outils de tout développeur de système.

Liens / crédits

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

Laissez un commentaire

Commentez anonymement ou connectez-vous pour commenter.

Commentaires

Laissez une réponse

Répondez de manière anonyme ou connectez-vous pour répondre.