JDBC Connection Piscina consulta de teste "SELECT 1" não pegar AWS RDS Writer / failover Leitor

Bernie Lenz:

Estamos executando um banco de dados AWS RDS Aurora / MySQL em um cluster com um escritor e uma instância de leitor onde o escritor é replicado para o leitor.

O aplicativo acessando o banco de dados é uma aplicação Java padrão usando uma conexão HikariCP Pool. A piscina está configurado para usar uma "SELECT 1"consulta de teste no check-out.

O que percebemos é que de vez em quando RDS falha sobre o escritor para o leitor. O failover também pode ser replicado manualmente clicando em "Ações de instância / Failover" no console AWS.

O pool de conexão não é capaz de detectar o failover eo fato de que ele está agora ligado a um banco de dados leitor, como as "SELECT 1"consultas de teste ainda ter sucesso. No entanto nenhuma atualização de banco de dados subsequentes falhar com "java.sql.SQLException: The MySQL server is running with the --read-only option so it cannot execute this statement"erros.

Parece que em vez de uma "SELECT 1"consulta de teste, o pool de conexão pode detectar que está agora ligado ao leitor usando uma "SELECT count(1) FROM test_table WHERE 1 = 2 FOR UPDATE"consulta de teste.

  1. Alguém experimentou o mesmo problema?
  2. Existem quaisquer desvantagens em usar "FOR UPDATE"na consulta de teste?
  3. Há alguma alternativa ou melhor se aproxima de lidar com um failover AWS RDS conjunto escritor / leitor?

Sua ajuda é muito apreciada

Bernie

kdgregory:

Eu estive dando um monte de pensamento nos dois meses desde a minha resposta inicial ...


Como Aurora endpoints trabalho

Quando você iniciar um Aurora cluster que obter vários nomes de host para acessar o cluster. Para efeitos da presente resposta, os dois únicos que nos preocupamos são o "endpoint cluster," que é leitura e escrita, eo "read-only endpoint", que é (você adivinhou) somente leitura. Você também tem um terminal para cada nó no cluster, mas acessando nós derrota diretamente a finalidade do uso Aurora, por isso não vou mencioná-los novamente.

Por exemplo, se eu criar um cluster com o nome "exemplo", eu vou pegar os seguintes parâmetros:

  • endpoint Cluster: example.cluster-x91qlr44xxxz.us-east-1.rds.amazonaws.com
  • Read-only endpoint: example.cluster-ro-x91qlr44xxxz.us-east-1.rds.amazonaws.com

Você pode pensar que estes parâmetros se referem a algo como um Elastic Load Balancer, o que seria suficiente inteligente para o tráfego de redirecionamento no failover, mas você pode estar errado. Na verdade, eles são simplesmente entradas DNS CNAME com um tempo muito curto-to-live:

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

Quando um failover acontece, os CNAMEs são atualizados (a partir examplede 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

A outra coisa que acontece durante um failover é que todas as conexões para o endpoint "cluster" ficar fechado, que irá falhar quaisquer transações em processo (supondo que você tenha definido o tempo limite de consulta razoáveis).

As conexões com o "read-only" endpoint não ficar fechado, o que significa que tudo o que nó é promovido irá receber o tráfego de leitura e gravação , além de somente leitura de tráfego (assumindo, claro, que a sua aplicação não basta enviar todos os pedidos para o endpoint cluster). Desde conexões somente leitura são normalmente utilizados para consultas relativamente caros (por exemplo, relatórios), isso pode causar problemas de desempenho para suas operações de leitura e gravação.

O Problema: DNS Cache

Quando failover acontece, tudo-em processo de transações irá falhar (novamente, assumindo que você tenha definido o tempo limite de consulta). Haverá um curto período de tempo que quaisquer novas conexões também falhará, como as tentativas do pool de conexão para conectar-se ao mesmo host antes de ser feito com a recuperação. Na minha experiência, failover leva cerca de 15 segundos, durante os quais a sua aplicação não deve esperar para obter uma ligação.

Depois que 15 segundos (ou menos), tudo deve voltar ao normal: suas tentativas piscina de conexão para conectar ao terminal cluster, ele resolve para o endereço IP do novo nó de leitura e escrita, e está tudo bem. Mas se impede que qualquer coisa resolver essa cadeia de CNAMEs, você pode achar que seu pool de conexão faz conexões com um ponto de extremidade de somente leitura, que irá falhar logo que você tentar uma operação de atualização.

No caso do OP, ele tinha sua própria CNAME com um tempo limite maior. Então ao invés de conexão para o endpoint conjunto diretamente, ele iria ligar para algo como database.example.com. Esta é uma técnica útil em um mundo onde você failover manualmente para um banco de dados de réplica; Eu suspeito que é menos útil com Aurora. Independentemente disso, se você usar seus próprios CNAMEs para se referir a endpoints de banco de dados, você precisa deles para ter valores curto espaço de tempo a habitar (certamente não mais do que 5 segundos).

Na minha resposta original, eu também apontou que Java armazena em cache pesquisas de DNS, em alguns casos, para sempre. O comportamento desta cache depende (creio eu) a versão do Java, e também se você estiver executando com um gerente de segurança instalado. Com OpenJDK 8 em execução como um aplicativo, parece que a JVM irá delegar todas as pesquisas de nomes e não de cache em si nada. No entanto, você deve estar familiarizado com a networkaddress.cache.ttlpropriedade do sistema, conforme descrito no presente documento a Oracle e esta questão SO .

No entanto, mesmo depois de você ter eliminado quaisquer caches inesperados, ainda pode haver momentos em que o ponto final cluster é resolvido para um nó só de leitura. Isso deixa a questão de como você lida com essa situação.

Não tão boa solução: usar um teste de somente leitura no check-out

O OP estava esperando para usar um teste de conexão de banco de dados para verificar se o seu pedido foi executado em um nó só de leitura. Este é surpreendentemente difícil de fazer: a maioria dos pools de conexão (incluindo HikariCP, que é o que o OP está usando) simplesmente verificar se a consulta de teste executado com êxito; não há nenhuma capacidade de olhar para o que ele retorna. Isto significa que qualquer consulta de teste tem que lançar uma exceção ao fracasso.

Eu não tenho sido capaz de chegar a uma maneira de fazer o MySQL lançar uma exceção com apenas uma consulta stand-alone. O melhor que eu vim acima com é criar uma função:

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 ;

Então você chamar essa função em sua consulta de teste:

select throwIfReadOnly() 

Isso funciona, principalmente. Ao executar o meu programa de teste que eu podia ver uma série de "fracassadas de conexão validar" mensagens, mas depois, inexplicavelmente, a consulta atualização seria executado com uma conexão somente leitura. O Hikari não tem uma mensagem de depuração para indicar qual a ligação que distribui, então eu não conseguia identificar se tinha supostamente passou na validação.

Mas, além disso possível problema, há uma questão mais profunda com esta implementação: ele esconde o fato de que há um problema. Um usuário faz uma solicitação, e talvez aguardará 30 segundos para obter uma resposta. Não há nada no log (a menos que você ativar o log de depuração do Hikari) para dar uma razão para este atraso.

Além disso, enquanto o banco de dados é inacessível Hikari está furiosamente tentando fazer conexões: no meu teste single-threaded, seria tentar uma nova conexão a cada 100 milissegundos. E estes são conexões reais, eles simplesmente ir para o host errado. Jogar em um aplicativo de servidor com algumas dezenas ou centenas de tópicos, e que poderia causar um efeito cascata significativo no banco de dados.

Melhor solução: usar um teste de somente leitura no check-out, por meio de um invólucro Datasource

Em vez de deixar as conexões de repetição Hikari silenciosamente, você poderia envolver o HikariDataSourceem sua própria DataSourceimplementação e teste / repetir-se. Isto tem a vantagem de que você pode realmente olhar para os resultados da consulta de teste, o que significa que você pode usar uma consulta auto-suficiente em vez de chamar uma função em separado, instalado. Ele também permite que você registrar o problema usando seus níveis de log preferenciais, permite fazer uma pausa entre as tentativas, e dá-lhe uma chance de configuração mudança piscina.

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

Esta solução ainda sofre com o problema que ele apresenta um atraso em solicitações do usuário. É verdade, você sabe que isso está acontecendo (que não o fez com o teste on-checkout), e você poderia introduzir um tempo limite (limitar o número de vezes através do loop). Mas ainda representa uma má experiência do usuário.

A melhor (IMO) solução: mudar para "modo de manutenção"

Os usuários são incrivelmente impaciente: se é preciso mais do que alguns segundos para obter uma volta de resposta, que provavelmente vai tentar recarregar a página, ou enviar o formulário novamente, ou fazer algo que não ajuda e pode machucar.

Então eu acho que a melhor solução é a falhar rapidamente e que eles saibam errado desse somethng. Em algum lugar perto do topo da pilha de chamadas que você já deve ter algum código que responde a exceções. Talvez você só voltar um genérico página 500 agora, mas você pode fazer um pouco melhor: olhar para a exceção, e retornar um "desculpe, temporariamente indisponível, tente novamente em alguns minutos" página se é uma exceção de banco de dados somente leitura.

Ao mesmo tempo, você deve enviar uma notificação para a equipe você ops: este pode ser um failover normal da janela maintenance, ou pode ser algo mais sério (mas não acordá-los a menos que tenha alguma maneira de saber que é mais grave ).

Acho que você gosta

Origin http://43.154.161.224:23101/article/api/json?id=140352&siteId=1
Recomendado
Clasificación