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

Коммутатор базы данных с HAProxy и HAProxy Runtime API

Используя HAProxy Runtime API, мы можем добавлять и удалять внутренние серверы баз данных вручную, а также с помощью скрипта или программы.

13 августа 2024
post main image
https://unsplash.com/@georgiadelotz

Один из моих проектов нуждался в базе данных высокой доступности (разве мы все не хотим этого...). Это означает (асинхронную) репликацию и многоузловую настройку. Существует несколько решений, об этом я напишу в другом посте. В этих сценариях у нас есть несколько копий основной базы данных, и когда возникает проблема, мы переключаемся с одной базы данных на другую.

Чтобы сделать такое переключение баз данных, не трогая клиента и базы данных, клиент должен получать доступ к базе данных не напрямую, а через proxy. Именно этому и посвящен данный пост. Мы реализуем "ручной переключатель" между базами данных. Это просто демонстрация того, как это может работать.

Как обычно, я делаю это на Ubuntu 22.04.

База данных proxy: HAProxy

Существует множество вариантов базы данных proxy. Здесь мы используем HAProxy. mЭто программное обеспечение часто используется для distраспределения нагрузки между веб-приложениями, действуя как балансировщик нагрузки, но у него есть много других возможностей.

Проверки здоровья HAProxy не подходят для нашей цели

HAProxy может проверять здоровье внутренних серверов и использовать правила для выбора лучшего внутреннего сервера для запроса. Это замечательно, но у нас есть несколько баз данных, и клиент должен получить доступ только к той, которую мы указали.

HAProxy также поддерживает внешний агент для проверки работоспособности. Мы можем сами создать такого агента. В продвинутых сценариях агент может собирать гораздо больше информации о всей системе и принимать решения лучше, чем HAProxy.

Оказывается, что использование внешнего агента HAProxy проблематично для наших целей, одна из причин заключается в том, что начальное состояние не является "обслуживанием" или "отключением", и нет возможности изменить это. Внешний агент HAProxy лучше всего использовать для балансировки нагрузки, а не для включения/выключения.

HAProxy Runtime API

У HAProxy также есть API - HAProxy Runtime API. С помощью этого API мы получаем желаемый контроль. Мы можем добавлять новые внутренние серверы, изменять их состояние и удалять внутренние серверы. Это именно то, что нам нужно!

Обзор проекта

У нас есть две базы данных, но только к одной из них может получить доступ клиент. Для этого мы помещаем между ними службу базы данных proxy , и, конечно же, нам нужен некий менеджер для управления базой данных proxy. Ниже приведена диаграмма того, чего мы хотим добиться.

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

Краткое описание компонентов в этой демонстрации:

  • клиент
    Простая программа, посылающая запрос к db-proxy:8620 и печатающая ответ. Программа представляет собой не более чем однострочный Shell-скрипт, отправляющий запрос каждую секунду.
  • db-proxy
    Это HAProxy. Мы включаем HAProxy Runtime API на порту 9999.
  • db-proxy-man
    Отсюда мы отправляем команды на API, по адресу db-proxy:9999. Мы можем сделать это вручную или создать сценарий Shell или программу Python , чтобы добавить выбранный нами сервер базы данных в (пустой) список серверов backend.
  • backend-db1, backend-db2
    Наши серверы баз данных. Мы используем не настоящие серверы баз данных, а эхо-серверы. Они передают полученное эхо с помощью prefix.

Файлы в этом проекте следующие:

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

Вы можете добавить файл .env и установить DOCKER_COMPOSE_PROJECT на имя вашего проекта, например, db-proxy-demo.

Файл docker-compose.yml

Файл docker-compose.yml содержит все элементы, о которых мы говорили выше. Мы добавляем 'hostname' для удобства использования (когда мы используем Docker Swarm, мы опускаем '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  

Личное мнение: никогда не позволяйте Docker-Compose создавать сети. Делайте это вручную:

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

Файл haproxy.cfg и Dockerfile.

Мы не добавляем внутренние серверы баз данных здесь, вместо этого мы добавляем их с помощью 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

Вы можете изменить уровень журнала с 'debug' на 'info'.

Строка, делающая доступным HAProxy Runtime API :

  stats socket ipv4@*:9999 level admin

Dockerfile используется для сборки образа, в нем нет ничего особенного:

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

Вызываем проект

Сначала мы собираем образ:

docker-compose build --no-cache

Затем запускаем его:

docker-compose up

В терминале вы увидите следующее:

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

Обратите внимание, что "клиент" посылает запросы в нашу db-proxy. Эти запросы не отвечают.

Чтобы увидеть страницу состояния, направьте браузер на:

http://127.0.0.1:8360/stats

Перед восстановлением удалите контейнеры:

docker-compose down

Отправка команд в HAProxy Runtime API

Согласно документации:

  • Сервер, добавленный с помощью API , называется динамическим сервером.
  • Бэкэнд должен быть настроен на использование алгоритма динамической балансировки нагрузки.
  • Динамический сервер не восстанавливается после операции перезагрузки балансировщика нагрузки.
  • "В настоящее время динамический сервер инициализируется статически с помощью метода init-addr "none". Это означает, что при указании FQDN в качестве адреса не будет произведено разрешение, даже если создание сервера будет подтверждено."

Для отправки команд на API мы используем команду 'nc' (netcat), присутствующую в образе busybox :

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

Мы используем только несколько команд для добавления и удаления серверов, обратите внимание, что в haproxy.cfg мы назвали наш бэкэнд: 'db_servers'.

Чтобы показать состояние серверов:

show servers state

Добавить сервер:

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

Первая команда добавляет сервер, но он будет находиться в режиме обслуживания. Вторая команда делает сервер готовым.

См. также выше: HAProxy Runtime API не разрешает для нас имена хостов динамических серверов! Это означает, что мы должны использовать здесь IP address , а не имя хоста!

Чтобы удалить сервер:

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

Теперь по-настоящему: Добавление и удаление серверов

Для отправки команд на API мы заходим в контейнер db-proxy-man busybox :

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

Во время выполнения команд вы можете наблюдать страницу состояния в браузере.

Мы присваиваем нашим серверам следующие имена:

backend_db1_8310
backend_db2_8311

Как уже упоминалось выше, API разрешает IP address имена хостов динамических серверов. Мы используем команду 'nslookup', чтобы получить IP address:

nslookup backend-db1

Результат:

Server:		127.0.0.11
Address:	127.0.0.11:53

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

Проверим состояние серверов:

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

Результат:

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

Отображается только строка заголовка. Серверов нет.

Теперь добавьте сервер:

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

Результат:

New server registered.

В браузере вы увидите, что этот сервер был добавлен со статусом 'MAINT'.

Далее мы включаем сервер, изменяя его состояние на 'ready':

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

Вы также можете увидеть эти изменения в журнале 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 
...

После изменения состояния сервера на 'ready' мы видим, что backend-db1 теперь отвечает на запросы 'клиента', и 'клиент' получает данные от backend-db1. Отлично!

Снова проверяем состояние сервера:

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

Result, показывая, что наш сервер был добавлен:

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

Теперь давайте удалим этот сервер, сначала переведя его в состояние обслуживания:

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

А затем удалим сервер:

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

Вы можете увидеть изменения в журналах и в браузере.

Теперь мы знаем, как работают команды, и можем написать сценарий Shell, сценарий Bash или программу Python для отправки команд на API.

Использование Python для доступа к HAProxy Runtime API

Это блог о Python , но я не буду вдаваться в подробности программы Python для выполнения вышеописанных действий.
Я покажу только функцию, которую вы можете использовать для доступа к 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
    
    ...    

Программа должна делать примерно следующее:

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

Если IP address внутренних серверов может меняться, то мы также проверяем изменения здесь.

Резюме

Мы использовали HAProxy для создания переключателя для наших баз данных. HAProxy - это очень сложное программное обеспечение, и, хотя документация по HAProxy обширна, порой она также неясна. Я считаю, что это связано с постоянным развитием HAProxy. Использовать HAProxy Runtime API легко, но информация, которую можно получить о внутренних серверах, ограничена.

Но в итоге HAProxy просто работает и должен быть в наборе инструментов каждого системного разработчика.

Ссылки / кредиты

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

Оставить комментарий

Комментируйте анонимно или войдите в систему, чтобы прокомментировать.

Комментарии

Оставьте ответ

Ответьте анонимно или войдите в систему, чтобы ответить.