Тестовый запрос пула соединений JDBC «SELECT 1» не перехватывает аварийное переключение AWS RDS Writer / Reader - PullRequest
0 голосов
/ 03 октября 2018

Мы работаем с базой данных AWS RDS Aurora / MySQL в кластере с устройством записи и экземпляром устройства чтения, где средство записи реплицируется для устройства чтения.

Приложение, обращающееся к базе данных, является стандартным приложением Java, использующим пул соединений HikariCP.Пул настроен на использование тестового запроса "SELECT 1" при оформлении заказа.

Что мы заметили, так это то, что время от времени RDS переключается с писателя на читателя.Отработка отказа также может быть реплицирована вручную, нажав «Действия экземпляра / Отработка отказа» в консоли AWS.

Пул соединений не может обнаружить аварийное переключение и тот факт, что он теперь подключен к базе данных считывателя, поскольку тестовые запросы "SELECT 1" все еще успешно выполняются.Однако любые последующие обновления базы данных завершаются с ошибкой "java.sql.SQLException: The MySQL server is running with the --read-only option so it cannot execute this statement".

Похоже, что вместо тестового запроса "SELECT 1" пул соединений может обнаружить, что теперь он подключен к считывателю, с помощью тестового запроса "SELECT count(1) FROM test_table WHERE 1 = 2 FOR UPDATE".

  1. Кто-нибудь испытывал такую ​​же проблему?
  2. Есть ли недостатки при использовании "FOR UPDATE" в тестовом запросе?
  3. Существуют ли альтернативные или более эффективные подходы к работе с отказоустойчивым устройством записи / чтения кластеров AWS RDS?

Ваша помощь очень ценится

Берни

Ответы [ 2 ]

0 голосов
/ 04 октября 2018

Я много размышлял об этом за два месяца с момента моего первоначального ответа ...


Как работают конечные точки Aurora

Когдапри запуске кластера Aurora вы получаете несколько имен хостов для доступа к кластеру.Для целей этого ответа мы заботимся только о двух «конечных точках кластера», которые доступны для чтения и записи, и «конечных точках только для чтения», которые (как вы уже догадались) доступны только для чтения.У вас также есть конечная точка для каждого узла в кластере, но доступ к узлам напрямую отрицает цель использования Aurora, поэтому я не буду упоминать их снова.

Например, если я создаю кластер с именем "example"Я получу следующие конечные точки:

  • Конечная точка кластера: example.cluster-x91qlr44xxxz.us-east-1.rds.amazonaws.com
  • Конечная точка только для чтения: example.cluster-ro-x91qlr44xxxz.us-east-1.rds.amazonaws.com

Вы можете подуматьчто эти конечные точки будут относиться к чему-то вроде Elastic Load Balancer, который будет достаточно умен, чтобы перенаправлять трафик при отработке отказа, но вы ошибаетесь.На самом деле это просто записи DNS CNAME с очень коротким временем жизни:

dig example.cluster-x91qlr44xxxz.us-east-1.rds.amazonaws.com


; <<>> DiG 9.11.3-1ubuntu1.3-Ubuntu <<>> example.cluster-x91qlr44xxxz.us-east-1.rds.amazonaws.com
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 40120
;; flags: qr rd ra; QUERY: 1, ANSWER: 3, AUTHORITY: 0, ADDITIONAL: 1

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 65494
;; QUESTION SECTION:
;example.cluster-x91qlr44xxxz.us-east-1.rds.amazonaws.com. IN A

;; ANSWER SECTION:
example.cluster-x91qlr44xxxz.us-east-1.rds.amazonaws.com. 5 IN CNAME example.x91qlr44xxxz.us-east-1.rds.amazonaws.com.
example.x91qlr44xxxz.us-east-1.rds.amazonaws.com. 4 IN CNAME ec2-18-209-198-76.compute-1.amazonaws.com.
ec2-18-209-198-76.compute-1.amazonaws.com. 7199 IN A 18.209.198.76

;; Query time: 54 msec
;; SERVER: 127.0.0.53#53(127.0.0.53)
;; WHEN: Fri Dec 14 18:12:08 EST 2018
;; MSG SIZE  rcvd: 178

Когда происходит аварийное переключение, CNAME обновляются (с example до example-us-east-1a):

; <<>> DiG 9.11.3-1ubuntu1.3-Ubuntu <<>> example.cluster-x91qlr44xxxz.us-east-1.rds.amazonaws.com
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 27191
;; flags: qr rd ra; QUERY: 1, ANSWER: 3, AUTHORITY: 0, ADDITIONAL: 1

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 65494
;; QUESTION SECTION:
;example.cluster-x91qlr44xxxz.us-east-1.rds.amazonaws.com. IN A

;; ANSWER SECTION:
example.cluster-x91qlr44xxxz.us-east-1.rds.amazonaws.com. 5 IN CNAME example-us-east-1a.x91qlr44xxxz.us-east-1.rds.amazonaws.com.
example-us-east-1a.x91qlr44xxxz.us-east-1.rds.amazonaws.com. 4 IN CNAME ec2-3-81-195-23.compute-1.amazonaws.com.
ec2-3-81-195-23.compute-1.amazonaws.com. 7199 IN A 3.81.195.23

;; Query time: 158 msec
;; SERVER: 127.0.0.53#53(127.0.0.53)
;; WHEN: Fri Dec 14 18:15:33 EST 2018
;; MSG SIZE  rcvd: 187

Другая вещь, которая происходит во время восстановления после сбоя, заключается в том, что все соединения с конечной точкой «кластера» закрываются, что приведет к сбою любых транзакций в процессе (при условии, что вы установили разумные тайм-ауты запроса).

Соединения с конечной точкой «только для чтения» не закрываются, а это означает, что любой продвигаемый узел получит трафик чтения-записи в дополнение к трафик только для чтения (при условии, конечно, что ваше приложение не просто отправляет все запросы к конечной точке кластера).Поскольку подключения только для чтения обычно используются для относительно дорогих запросов (например, отчетов), это может вызвать проблемы с производительностью для операций чтения-записи.

Проблема: DNS-кэширование

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

По истечении этих 15 секунд (или около того) все должно вернуться в нормальное состояние: ваш пул соединений пытается подключиться к конечной точке кластера, он преобразуется в IP-адрес нового узла чтения-записи, и все в порядке.,Но если что-то мешает разрешить эту цепочку CNAME, вы можете обнаружить, что ваш пул соединений устанавливает соединения с конечной точкой только для чтения, которая завершится неудачно, как только вы попытаетесь выполнить операцию обновления.

В случае OPУ него был свой собственный CNAME с более длительным таймаутом.Поэтому вместо непосредственного подключения к конечной точке кластера он будет подключаться к чему-то вроде database.example.com.Это полезный метод в мире, где вы бы вручную переключались на базу данных реплик;Я подозреваю, что это менее полезно с Авророй.В любом случае, если вы используете свои собственные CNAME для ссылки на конечные точки базы данных, вам нужно, чтобы они имели короткие значения времени жизни (конечно, не более 5 секунд).

В своем первоначальном ответе я также указалчто Java кэширует DNS-поиски, в некоторых случаях навсегда.Поведение этого кэша зависит от (я полагаю) версии Java, а также от того, работаете ли вы с установленным менеджером безопасности.С OpenJDK 8, работающим как приложение, кажется, что JVM делегирует все поиски именования и не кэширует ничего непосредственно.Однако вы должны быть знакомы с системным свойством networkaddress.cache.ttl, как описано в в этом документе Oracle и в этом вопросе SO .

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

Не очень удачное решение: использовать проверку только для чтения при оформлении заказа

ОП надеялся использовать тест подключения к базе данных, чтобы убедиться, что его приложение работает на узле только для чтения.Это на удивление трудно сделать: большинство пулов соединений (включая HikariCP, который использует OP) просто проверяют, успешно ли выполняется тестовый запрос;нет возможности посмотреть на то, что он возвращает.Это означает, что любой тестовый запрос должен выдать исключение для сбоя.

Я не смог придумать способ заставить MySQL выдавать исключение только с помощью отдельного запроса.Лучшее, что я придумал, - это создание функции:

DELIMITER EOF

CREATE FUNCTION throwIfReadOnly() RETURNS INTEGER
BEGIN
    IF @@innodb_read_only THEN
        SIGNAL SQLSTATE 'ERR0R' SET MESSAGE_TEXT = 'database is read_only';
    END IF;
    RETURN 0;
END;
EOF

DELIMITER ;

Затем вы вызываете эту функцию в своем тестовом запросе:

select throwIfReadOnly() 

В основном это работает.При запуске моей тестовой программы я мог видеть серию сообщений "не удалось проверить соединение", но затем, необъяснимым образом, запрос на обновление будет выполняться с подключением только для чтения.У Hikari нет отладочного сообщения, указывающего, какое соединение оно раздает, поэтому я не смог определить, прошел ли он, предположительно, проверку.

Но кроме этой возможной проблемы, есть более глубокая проблема с этой реализацией:это скрывает тот факт, что есть проблема.Пользователь делает запрос и, возможно, ждет 30 секунд, чтобы получить ответ.В журнале нет ничего (если вы не включили ведение журнала отладки Hikari), чтобы объяснить причину этой задержки.

Более того, пока база данных недоступна, Hikari яростно пытается установить соединения: в моем однопоточном тесте онбудет пытаться новое соединение каждые 100 миллисекунд.И это реальные соединения, они просто идут не на тот хост.Добавьте сервер приложений с несколькими десятками или сотнями потоков, и это может привести к значительному волновому эффекту в базе данных.

Лучшее решение: использовать тест только для чтения при оформлении заказа через оболочку Datasource

Вместо того, чтобы позволить Hikari молча повторять попытки подключения, вы можете заключить HikariDataSource в собственную реализацию DataSource и протестировать / повторить попытку самостоятельно.Это дает то преимущество, что вы можете посмотреть на результаты тестового запроса, что означает, что вы можете использовать автономный запрос, а не вызывать отдельно установленную функцию.Он также позволяет регистрировать проблему, используя предпочитаемые уровни регистрации, позволяет делать паузу между попытками и дает возможность изменить конфигурацию пула.

private static class WrappedDataSource
implements DataSource
{
    private HikariDataSource delegate;

    public WrappedDataSource(HikariDataSource delegate) {
        this.delegate = delegate;
    }

    @Override
    public Connection getConnection() throws SQLException {
        while (true) {
            Connection cxt = delegate.getConnection();
            try (Statement stmt = cxt.createStatement()) {
                try (ResultSet rslt = stmt.executeQuery("select @@innodb_read_only")) {
                    if (rslt.next() && ! rslt.getBoolean(1)) {
                        return cxt;
                    }
                }
            }
            // evict connection so that we won't get it again
            // should also log here
            delegate.evictConnection(cxt);
            try {
                Thread.sleep(1000);
            }
            catch (InterruptedException ignored) {
                // if we're interrupted we just retry
            }
        }
    }

    // all other methods can just delegate to HikariDataSource

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

Лучшее (imo) решение: переключиться в «режим обслуживания»

Пользователи невероятно нетерпеливы: если требуется более нескольких секунд, чтобы получитьВ ответ они, вероятно, попытаются перезагрузить страницу, либо снова отправят форму, либо сделают что-то , что не поможет и не повредит.

Так что я думаю, что лучшее решение - этобыстро потерпеть неудачу и дать им понять, что что-то не так.Где-то в верхней части стека вызовов у вас уже должен быть какой-то код, отвечающий на исключения.Может быть, вы просто сейчас возвращаете обычную 500-страничную страницу, но можете сделать немного лучше: посмотрите на исключение и верните страницу «извините, временно недоступна, попробуйте снова через несколько минут», если это исключение базы данных только для чтения.

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

0 голосов
/ 04 октября 2018

установить время ожидания простоя соединения пула соединений в вашем источнике данных кода Java.установить около 1000 мс

...