Коммутатор базы данных с HAProxy и HAProxy Runtime API
Используя HAProxy Runtime API, мы можем добавлять и удалять внутренние серверы баз данных вручную, а также с помощью скрипта или программы.
Один из моих проектов нуждался в базе данных высокой доступности (разве мы все не хотим этого...). Это означает (асинхронную) репликацию и многоузловую настройку. Существует несколько решений, об этом я напишу в другом посте. В этих сценариях у нас есть несколько копий основной базы данных, и когда возникает проблема, мы переключаемся с одной базы данных на другую.
Чтобы сделать такое переключение баз данных, не трогая клиента и базы данных, клиент должен получать доступ к базе данных не напрямую, а через 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
Подробнее
Database Docker Docker-compose
Недавний
- Использование Ingress для доступа к RabbitMQ на кластере Microk8s
- Простая видеогалерея с Flask, Jinja, Bootstrap и JQuery
- Базовое планирование заданий с помощью APScheduler
- Коммутатор базы данных с HAProxy и HAProxy Runtime API
- Docker Swarm rolling updates
- Скрытие первичных ключей базы данных UUID вашего веб-приложения
Большинство просмотренных
- Использование PyInstaller и Cython для создания исполняемого файла Python
- Уменьшение времени отклика на запросы на странице Flask SQLAlchemy веб-сайта
- Используя Python pyOpenSSL для проверки SSL-сертификатов, загруженных с хоста
- Подключение к службе на хосте Docker из контейнера Docker
- Использование UUID вместо Integer Autoincrement Primary Keys с SQLAlchemy и MariaDb
- SQLAlchemy: Использование Cascade Deletes для удаления связанных объектов