2013-12-22

Об HA-JDBC

Есть такая, довольно молодая и еще малоизвестная, библиотека HA-JDBC. HA - это High Availability. JDBC - это, очевидно, Java DataBase Connectivity.


Как и следует из названия, библиотека предназначена для осуществления доступа к вашему высокодоступному кластеру БД посредством JDBC. Это прокси и балансировщик SQL соединений и запросов.
Обычно балансировка SQL запросов осуществляется отдельными демонами. Например, Pgpool для PostgreSQL или MySQL Proxy. HA-JDBC же позволяет добавить "интеллекта" прямо в ваше ява-приложение, добавив возможность подключения одновременно к нескольким СУБД.
Вообще-то что постгресовый, что мускульный JDBC драйверы сами умеют подключаться к нескольким хостам. И, собственно, каким-то образом балансировать нагрузку. Но HA-JDBC позволяет более тонко настраивать как балансировщик, так и другие плюшки.
HA-JDBC также может взять на себя репликацию и восстановление после сбоев. Вы можете взять две (или более) независимых БД, а HA-JDBC будет распараллеливать все запросы записи, чтобы данные во всех базах были одинаковыми. При этом он даже может позаботиться, чтобы всяческие последовательности и автоинкременты в базах совпадали. В случае, если какая-то из БД кластера уходила, а потом вернулась, HA-JDBC может синхронизировать данные на ней, вплоть до снятия дампа с рабочей базы и накатывания его на восстановленную. А чтобы несколько инстансов HA-JDBC (несколько экземпляров вашего приложения) не подрались друг с другом за то, кто будет восстанавливать, HA-JDBC сами объединяются в свой кластер посредством JGroups.

Впрочем, пока я предпочитаю полагаться на встроенные механизмы репликации, а HA-JDBC оставить лишь балансировку нагрузки и отработку падений узлов БД. Собственно, мы имеем два PostgreSQL 9.3 с потоковой репликацией между ними, несколько клиентов с HA-JDBC на борту и балансировку запросов чтения. Характер данных таков, что чтения значительно больше чем записи. Поэтому мы спокойно (и без всяких балансировщиков) пишем в единственный мастер. Полагаемся на потоковую репликацию. И интенсивно читаем с балансировкой и отказоустойчивостью.


HA-JDBC весьма универсальная штука. Она может проксировать как старые добрые JDBC драйвера (где адрес и свойства подключения задаются в URL вида jdbc:blablabla), так и DataSource и даже ConnectionPoolDataSource. Впрочем, библиотека не умеет преобразовывать одни виды подключения в другие. Если вы запрашиваете HA-JDBC подключение у DriverManager, библиотека ожидает, что проксируемые подключения тоже определены через URL драйвера. Если же вы запрашиваете DataSource, то и проксируемые подключения должны быть определены как DataSource. Тут главное - не запутаться. Видимо, в сложных случаях стоит использовать либо URL драйверов (где каждое подключение хорошо идентифицируется именно URL), либо DataSource, помещенные в JNDI (тогда у каждого подключения будет свое JNDI имя).
Вариантов синхронизации БД (после сбоев) - множество. Но так как у меня репликация осуществляется средствами БД, я поставил просто passive, т.е. ничего не делать.
Хранить состояние кластера (какие узлы живые и т.п.) можно в памяти, можно в SQLite под боком, а можно в любой SQL базе. В моем случае simple в памяти выглядел достаточным.
HA-JDBC поддерживает кучу БД (диалектов), включая DB2, Oracle и Sybase. PostgreSQL, понятно, поддерживается.
Я пробовал пару балансеров: round-robin и random. Почему-то random показал более интересные результаты, запросы более равномерно распределялись между двумя Постгресами.
У HA-JDBC есть еще различные политики работы с транзакциями (повторять ли транзакцию целиком на втором узле, если она сломалась на одном), идентификаторами, последовательностями и т.п. Так как у меня через библиотеку проходят запросы только на чтение, я все эти возможности просто поодключал.

В общем, HA-JDBC тупо проксирует подключения к БД. Например, если приложение запросило два подключения (дважды вызвало getConnection() у DriverManager или DataSource), то HA-JDBC открывает по два подключения к каждой БД.
При этом запросы (именно запросы, Statement, а не подключения) вполне успешно балансируются между базами.
HA-JDBC сразу искаропки успешно отрабатывает проблемы с БД. Я, с помощью фаейрвола, блокировал подключение к одной из БД. Это приводило к исключению при выполнении одного из запросов. Это исключение перехватывалось HA-JDBC, она помечала базу, как недоступную. Запрос повторялся на живой базе. И все последующие запросы шли только к живой базе. Примечательно, что исключение даже не доходило до приложения.
С настройками по умолчанию HA-JDBC никогда не вернет отключенную базу обратно в кластер. Можно, конечно, перезапустить HA-JDBC (т.е. ваше приложение), тогда (если вы не использовали персистентное хранилище состояние кластера) снова будут открыты подключения ко всем сконфигурированным БД. В рантайме же можно дать понять HA-JDBC о том, что мы хотим вернуть БД в кластер, через JMX, есть соответствующий бин. Ну или можно просто задать расписание (в Cron-синтаксисе) проверки доступности отключенных узлов. Например, так: auto-activate-schedule="*/15 * * ? * *".
В моем тесте возникли сложности с активацией вернувшейся в строй БД. Приложение открывает подключения к БД в самом начале эксперимента. И понятия не имеет, что там какие-то узлы отключались и подключились обратно. HA-JDBC все тонкости успешно скрывает. Т.е. приложение имеет свои два экземпляра java.sql.Connection и продолжает использовать их. Однако, драйвер PostgreSQL тоже не дурак, он знает, что запрос на одной коннекции свалился из-за проблем с сетью, и помечает эту коннекцию как закрытую. Это не Connection, видимое приложению, это одно из Connection, проксируемых HA-JDBC. HA-JDBC, по указанному расписанию, соображает, что проблема со связью ушла, и начинает передавать запросы через закрытое соединение. Что приводит к новой ошибке, о том, что соединение уже закрыто. И HA-JDBC вновь помечает БД как неработоспособную. В результате БД никогда не возвращается в работоспособное состояние, а все запросы продолжают падать на БД, над которой не издевались.
Т.е. HA-JDBC не пересоздает подключения, если с ними возникли проблемы. У MySQL драйвера есть параметр autoReconnect, который, теоретически, как раз и решает подобную проблему. Но у PostgreSQL нет такого параметра.
Казалось бы, задачей проверки валидности подключений должны заниматься пулы коннекций. Я помучал C3P0 и DBCP. Первый вообще не удалось завести под HA-JDBC, по всей видимости потому, что в Яве 1.6 умудрились модифицировать java.sql.Connection, добавив метод isValid(), а в сгенерированных прокси C3P0 этого метода еще нет. Однако выяснилось, что оба пула проверяют коннекции только в трех случаях: при получении коннекции из пула, при возврате коннекции в пул и в фоне для неиспользуемых коннекций пула. Коннекции, используемые в приложении никем не проверяются.
Ну а если тупо для каждого запроса получать новую коннекцию и закрывать её потом, то получается все хорошо. При возвращении БД в строй запросы снова начинают честно балансироваться.
Конечно, открывать коннекцию на каждый запрос - неэффективно. Но, выходит, держать в приложении одну и ту же коннекцию часами - тоже неудобно. Нужно периодически запрашивать новые коннекции, иначе ни HA-JDBC, ни пулы коннекций правильно работать не будут.
Понятно, что нужно использовать пул коннекций, чтобы пул успешно переиспользовал и проверял коннекции. Однако тут возникает вопрос, что правильнее: пул HA-JDBC коннекций или HA-JDBC поверх нескольких пулов. Мне почему-то кажется, что второй вариант правильнее. Осталось только запилить его.

P.S. Код экспериментов и конфигурацию HA-JDBC можно подглядеть на Bitbucket.
P.P.S. Графики рисовались Graphite, по данным, собираемым Diamond.