На одном из проектов нам потребовалось развернуть систему централизованной аутентификации и авторизации для 25 000 – 30 000 активных пользователей, территориально распределенных по всей стране. Такая специфика диктует жесткие требования к масштабируемости и надежности системы. Наш выбор пал на Keycloak как на популярное open-source решение.
Но одиночный сервер Keycloak физически не способен обеспечить необходимую пропускную способность и гарантировать SLA при таком объеме трафика. Кластеризация стала единственным жизнеспособным вариантом, так как она позволяет горизонтально масштабировать вычислительную мощность и обеспечивает отказоустойчивость.
В этой статье рассказываем про наш опыт развертывания кластера Keycloak на трех изолированных виртуальных машинах с Embedded Infinispan и JGroups. Расскажем, почему выбрали именно такую архитектуру, как настраивали, что сломалось при тестировании отказоустойчивости и как мы это чинили (включая баг, который нашли и исправили вместе с командой Keycloak).
Почему Kubernetes не подошел?
Часто по умолчанию для кластеризации выбирают Kubernetes, однако в рамках данного проекта поддержка распределенного kubernetes кластера была бы избыточным решением, поэтому было принято решение разворачивать Keycloak просто в контейнерах на базе трех изолированных виртуальных машин (VM).
Информации о таком варианте развертывания не так уж и много. В Keycloak кластеризация состоит в том, что все узлы используют одну базу данных и распределенный кеш, за который отвечает infinispan.
Keycloak предлагает два варианта интеграции с Infinispan:
Внешний кластер Infinispan: Keycloak подключается к отдельно стоящему, независимому кластеру Infinispan.
Встроенный модуль Embedded Infinispan: Модуль кэша запускается внутри того же процесса (и контейнера), что и сам Keycloak.
Мы осознанно отказались от внешнего кластера Infinispan. В условиях высокой географической распределенности и строгих требований к аптайму введение еще одного независимого распределенного компонента значительно повышает риски. Если внешний кластер Infinispan становится недоступен по сетевым причинам или из-за аппаратного сбоя, весь контур аутентификации Keycloak мгновенно прекращает работу.
Для реализации честной схемы High Availability (HA), при которой отказ одного из узлов никак не влияет на работоспособность системы для конечных пользователей, мы остановились именно на встроенном модуле Embedded Infinispan. В такой конфигурации кэш реплицируется между нашими тремя виртуальными машинами. Если одна VM становится недоступна, балансировщик перенаправляет трафик на оставшиеся две, а встроенный Infinispan гарантирует, что пользователи даже не разлогинятся, так как их сессии уже синхронизированы на живых нодах.
Настройка
Схема кластера получилась следующей:

Почему именно так:
Embedded Infinispan – избавляемся от отдельного инфраструктурного компонента, если падает VM, падает только один узел Keycloak вместе с его кешем; остальные ноды продолжают работать.
Три изолированные VM в разных ДЦ – географическая распределенность дает физическую отказоустойчивость на уровне железки/сети/энергоснабжения.
Общая PostgreSQL – кластерное обнаружение через jdbc-ping и хранение сессий в БД.
Режим cache=ispn – распределенный кеш включен. В режиме local сессии не реплицируются и при падении ноды пользователи разлогинятся.
Для работы кластера embedded Infinispan между нодами должны быть открыты следующие tcp порты:
7800 – основной порт для взаимодействия Infinispan между узлами.
57800 – используется FD_SOCK2 для обнаружения развала кластера.
Эти порт не должны блокироваться на стороне firewall между узлами.
PostgreSQL развернут отдельно на трех VM с кластеризацией через Patroni. Для Keycloak строка подключения к БД указаны все три узла через jdbc c параметром targetServerType=master. Это позволяет узлам подключаться к мастеру без использования балансировщика.
Кластер Keycloak использует таблицу JGROUPS_PING для динамического обнаружения участников. При старте каждый узел записывает свой адрес в эту таблицу, а при рестарте получает актуальный список живых нод. Это позволяет добавлять и удалять ноды без синхронного редактирования конфигурационных файлов на всех машинах, в отличие от статического TCPPING.
Для корректного отображения внешнего IP в таблице JGroups нужно указать внешний ip через параметр -Djgroups.external_addr=<ВНЕШНИЙ_IP_ЭТОЙ_VM>" по которому данная VM доступна из других датацентров.
Так же в файле cache-ispn.xml в блоке необходимо явно указать node-name с именем хоста VM. Это обеспечивает корректное отображение имени узла и внешнего IP в таблицах JGroups в логах и через JMX, что критично для диагностики работы кластера в многодоменной среде.
Как мы тестировали отказоустойчивость
Для проведения тестирования отказоустойчивости мы проводили серию испытаний, имитирующих различные сценарии деградации инфраструктуры:
перезапуск узлов Keycloak;
потеря связи с базой данных;
потеря связи с одним из дата-центров (отсутствие связи с узлом БД и Keycloak).
Что обнаружили:
При разрыве связи между узлами Keycloak, но при сохранении доступности узлов БД у всех участников кластера, Keycloak успешно восстанавливается при возобновлении сетевой связности.
Однако если теряется связь с целым датацентром (одновременная потеря доступа к узлу БД и Keycloak), начинаются серьхзные проблемы с восстановлением работоспособности вплоть до появления 5xx ошибок при попытках авторизации. Кластер фактически оказывался в состоянии, близком к split-brain.
Встроенный healthcheck в Keycloak не отрабатывал должным образом в таких условиях. На одной из итераций разбора проблемы мы обновили версию Keycloak, поскольку в новой версии был добавлен отдельный healthcheck для кластера. Но это не решило проблему.
Временное решение проблемы
Для оперативного решения ситуации достаточно перезапустить проблемный контейнер Keycloak. Мы написали скрипт на Python, который выполняет следующие проверки: анализирует healthcheck каждого узла, проверяет таблицу JGROUPS_PING в базе данных на наличие нескольких координаторов кластера и в случае обнаружения проблем автоматически перезапускает соответствующий контейнер Keycloak. Скрипт был упакован в контейнер и развернут рядом с каждым контейнером Keycloak. Таким образом, за каждым экземпляром Keycloak следит свой выделенный watcher. Это решение рассматривалось как временное.
Корень проблемы: баг в Keycloak
Для более глубокого исследования проблемы мы развернули тестовый стенд и начали воспроизводить ситуацию при простой конфигурации, но в режиме работы кластера. Проблема подтвердилась: как только один из узлов исключался из маршрутизации, а затем возвращался обратно, кластер не восстанавливался самостоятельно.
Мы оформили issue в официальном репозитории Keycloak на GitHub. Команда разработчиков Keycloak оперативно отреагировала, предложила несколько вариантов решения, но после детального изучения проблемы был обнаружен баг, который впоследствии был исправлен.
Ссылка на issue: https://github.com/keycloak/keycloak/issues/45980
Что в итоге
После выхода релиза с фиксом мы проверили наш сценарий с разваливанием кластера на демонстрационном стенде – проблема исчезла. После нескольких итераций проверок мы обновили тестовое окружение, а затем и продовое.
Наш скрипт-watcher мы решили пока оставить на всякий случай: ресурсов он практически не потребляет, но в случае непредвиденных ситуаций поможет оперативно восстановить работоспособность кластера.
Что мы вынесли из этой истории:
Кластеризация Keycloak без Kubernetes – рабочий вариант для проектов, где Kubernetes избыточен.
Тестируйте все сценарии отказоустойчивости, особенно потерю целого датацентра. Именно этот сценарий оказался критичным у нас.
Обращайтесь к разработчикам продукта, если нашли баг, пишите issue. Так вы и свою проблему решите, и продукт лучше сделаете.
В конце концов мы получили кластер, который выдерживает нашу нагрузку, спокойно переживает падение одного датацентра без потери сессий и автоматически восстанавливается после сбоев.
А вы сталкивались с развертыванием Keycloak без Kubernetes? Какие сценарии отказоустойчивости тестировали и с какими проблемами встретились?

