
Разворачивая у нас в tutu Keycloak, мы столкнулись с необходимостью создания отказоустойчивого кластера. И если с БД всё более-менее понятно, то вот реализовать корректный обмен кешами между Keycloak оказалось довольно непростой для настройки задачей.
Мы упёрлись в то, что в документации Keycloak описано, как создать кластер, используя UDP-мультикаст. И это работает, если у вас все ноды будут находиться в пределах одного сегмента сети (например, ЦОДа). Если с этим сегментом что-то случится, то мы лишимся Keycloak. Нас это не устраивало.
Необходимо сделать так, чтобы ноды приложения были географически распределены между ЦОДами, находясь в разных сегментах сети.
В этом случае в документации Keycloak довольно неочевидно предлагается создать свой собственный кастомный JGroups транспортный стек, чтобы указать все необходимые вам параметры.
Бонусом приложу shell-скрипт, написанный для Consul, который предназначен для снятия анонсов путём выключения bird и попытки восстановления приложения.
Особенности
Нами была выбрана инсталляция без контейнеризации, приложение завёрнуто в systemd-сервис.
Keycloak может принять конфигурацию из четырёх разных источников:
-
CLI: kc.sh —key=value.
-
Переменная окружения: KC_KEY=value.
-
Файл конфигурации: key=value.
-
Файл Java KeyStore: kc.key=value.
Когда в туториале будет заходить речь про добавление переменной в конфигурацию, то подразумевается, что вы сами выбираете удобный для вас вариант.
В туториале я буду описывать передачу параметров через файл конфигурации.
Дано
-
Нода keycloak1.
-
Keycloak версии 20, завёрнутая в systemd-сервис.
-
Интерфейс eth0 с локальным IP-адресом виртуалки. Каждой ноде этот адрес должен быть доступен.
-
Интерфейс eth1, в котором через bgp анонсируется anycast IP-адрес.
-
Отказоустойчивая база данных за пределами Keycloak, к которой мы подключаем приложение.
Задача
Сделать Keycloak отказоустойчивым и геораспределённым.
Нам нужно создать кластер, в котором можно жёстко прибить адреса нод в конфигурации.
Для этого надо создать custom transport stack.
TCPPING
Остановим Keycloak.
Скопируем файл conf/cache-ispn.xml в новый файл conf/custom-cache-ispn.xml.
Добавим в секцию infinispan следующее:
<jgroups> <stack name="add_tcpping" extends="tcp"> <TCPPING initial_hosts="<eth0_ip_keycloak1>[7800],<eth0_ip_keyclaok2>[7800],<eth0_ip_keycloak3>[7800]" port_range="0" stack.combine="REPLACE" stack.position="MPING" /> </stack> </jgroups> <cache-container name="keycloak"> <transport lock-timeout="60000" stack="add_tcpping"/>
stack name ― имя стека, который мы потом используем в секции transport. Можно указать что угодно. Имя стека будет писаться в логах.
initial_hosts ― перечисляем IP-адреса с портами всех наших Keycloak-нод.
port_range ― TCPPING будет пытаться связать с каждой из нод кластера, начиная с указанного порта + port_range. В нашем случае будет использоваться только порт 7800.
stack.combine ― способ изменения параметров протокола. REPLACE заменяет протокол.
stack.position ― протокол, который мы меняем.
Теперь надо в конфигурации задать с помощью переменной cache-config-file наш .xml-файл, а также переменной http-host указать anycast-адрес (cache=ispn ― это дефолтное значение):
cache=ispn cache-config-file=cache-ispn-tcpping.xml http-host=<anycast_eth1_ip>
Из-за того, что мы используем anycast-адрес, надо указать IP-адрес хоста, по которому infinispan будет слушать порт 7800. Для этого при запуске сервера нам надо явно задать основной IP-адрес ноды:
bin/kc.sh start -Djgroups.bind.address=<eth0_ip>
После этого мы должны увидеть в логах, что JGroups запускается со стеком add_tcpping:
2023-04-21 10:40:54,586 INFO [org.infinispan.CLUSTER] (keycloak-cache-init) ISPN000078: Starting JGroups channel `ISPN` with stack `add_tcpping`
При запуске остальных нод с такой конфигурацией мы увидим, что кластер обнаружил новый хост и добавил его:
2023-04-21 10:41:02,197 INFO [org.infinispan.CLUSTER] (jgroups-12,keycloak1-57393) ISPN100000: Node keycloak2-60977 joined the cluster 2023-04-21 10:41:02,643 INFO [org.infinispan.CLUSTER] (jgroups-5,keycloak1-57393) [Context=authenticationSessions] ISPN100002: Starting rebalance with members [keycloak1-57393, keycloak2-60977], phase READ_OLD_WRITE_ALL, topology id 7 2023-04-21 10:41:06,963 INFO [org.infinispan.CLUSTER] (jgroups-12,keycloak1-57393) ISPN100000: Node keycloak3-7710 joined the cluster 2023-04-21 10:41:07,242 INFO [org.infinispan.CLUSTER] (jgroups-12,keycloak1-57393) [Context=authenticationSessions] ISPN100002: Starting rebalance with members [keycloak1-57393, keycloak2-60977, keycloak3-7710], phase READ_OLD_WRITE_ALL, topology id 11
Готово!
Объяснение
Понять, что мы сейчас настроили в .xml-файле, нам помог дефолтный конфиг стека TCP, находящегося по пути:
lib/lib/main/org.infinispan.infinispan-core-<version>.jar/default-configs/default-jgroups-tcp.xml
Там мы можем увидеть, что в качестве протокола обнаружения используется MPING. В conf/custom-cache-ispn.xml c помощью stack.position мы выбираем MPING, а с помощью stack.combine заменяем его на TCPPING.
HashiCorp Consul
Вы настроили anycast (у нас анонсируется адрес с помощью bird), кластеризацию, но вам надо как-то снимать анонсы, если с приложением что-то случится. Вариантов много, я рассмотрю используемый нами.
В этом туториале я не буду разбирать, как настраивать консул, рассмотрим лишь shell-скрипт, который запускается с его помощью раз в 15 секунд.
Keycloak имеет встроенный healthcheck, на его основе и построим проверку.
Чтобы включить его, надо в конфигурации задать переменную:
health-enabled=true
После этого у приложения становятся доступны следующие эндпоинты:
/health /health/live /health/ready
Будем отслеживать последний эндпоинт, так как там есть проверка подключения к базе данных. Её тоже будем отслеживать:
function keycloak_healthcheck { app_status=$(curl -sk https://127.0.0.1/health/ready | python -c "import sys, json; print(json.load(sys.stdin)['status'])" 2>/dev/null) db_status=$(curl -sk https://127.0.0.1/health/ready | python -c "import sys, json; print(json.load(sys.stdin)['checks'][0]['status'])" 2>/dev/null) if [ "$app_status" != "UP" ] || [ "$db_status" != "UP" ] then healthcheck=1 else healthcheck=0 fi }
Также попытаемся один раз восстановить работу Keycloak ребилдом приложения:
function keycloak_recover { echo $(date +%s) > $tmp_recover cmd="systemctl stop keycloak && <keycloak_dir>/bin/kc.sh build >/dev/null && systemctl start keycloak" timeout 50s bash -c "$cmd" & disown }
Запуск ребилда в фоне позволяет нам запускать скрипт сколько угодно часто, чтобы как можно быстрее реагировать на упавшее приложение и выключать bird.service.
Ну и для управления всем этим безобразием создаём tmp-файл для отслеживания времени запуска восстановления:
tmp_recover="/tmp/keycloak_recover_try" touch $tmp_recover recover_try=$(cat $tmp_recover)
Подробная настройка консула выходит за рамки данного туторила.
Собираем это всё вместе в скрипт:
Целиком скрипт
#!/bin/bash keyclaok_dir="<keycloak_dir>" tmp_recover="/tmp/keycloak_recover_try" touch $tmp_recover function disable_bird { pgrep bird > /dev/null 2>&1 bird_status=$? if [[ "$bird_status" == "1" ]] then echo "Bird disabled" else /bin/systemctl stop bird echo "Bird disabled" fi } function enable_bird { pgrep bird > /dev/null 2>&1 bird_status=$? if [[ "$bird_status" == "1" ]] then echo "Bird enabled" then /bin/systemctl start bird echo "Bird enabled" fi } function keycloak_healthcheck { app_status=$(curl -sk https://127.0.0.1/health/ready | python -c "import sys, json; print(json.load(sys.stdin)['status'])" 2>/dev/null) db_status=$(curl -sk https://127.0.0.1/health/ready | python -c "import sys, json; print(json.load(sys.stdin)['checks'][0]['status'])" 2>/dev/null) if [ "$app_status" != "UP" ] || [ "$db_status" != "UP" ] then healthcheck=1 else healthcheck=0 fi } # Попытка восстановления, запущенная в background с таймаутом function keycloak_recover { echo $(date +%s) > $tmp_recover cmd="systemctl stop keycloak && <keycloak_dir>/bin/kc.sh build >/dev/null && systemctl start keycloak" timeout 50s bash -c "$cmd" & disown } keycloak_healthcheck # Когда запускалось восстановление recover_try=$(cat $tmp_recover) # Если восстановление запускалось, то подсчитываем сколько секунд с тех пор прошло if [[ ! -z "$recover_try" ]] then let "try_s = $(date +%s) - $recover_try" fi if [[ "$healthcheck" == 0 ]] then enable_bird echo "Keycloak is ok" echo "" > $tmp_recover exit 0 elif [[ "$healthcheck" != 0 ]] then disable_bird if [[ -z "$recover_try" ]] then keycloak_recover exit 2 elif [[ ! -z "$recover_try" && "$try_s" -ge 60 ]] then if [[ "$app_status" != "UP" ]] then echo "Keycloak service is down, bird disabled" elif [[ "$db_status" != "UP" ]] then echo "Keycloak database problem, bird disabled" fi exit 2 fi fi
И настраиваем конфиг консула:
{ "check": { "id": "Keycloak", "name": "Keycloak healthcheck", "args": ["/opt/consul/check/script-check-keycloak.sh"], "interval": "15s", "timeout": "15s" } }
Заключение
Данная конфигурация позволила очень легко масштабировать приложение и сделать его геораспределённым. Конечно это далеко не всё, что можно сделать для отказоустойчивости, но, пожалуй, необходимый минимум.
Мы живём в такой конфигурации уже более полугода, и за это время она ни разу не давала сбой. За исключением не зависящих от кластера ситуаций.
Источники информации
ссылка на оригинал статьи https://habr.com/ru/articles/766284/
Добавить комментарий