Java >> Java tutorial >  >> Tag >> Jdbc

JDBC Connection Pool testforespørgsel SELECT 1 fanger ikke AWS RDS Writer/Reader failover

Jeg har tænkt meget over det i de to måneder siden mit oprindelige svar...

Sådan fungerer Aurora-endepunkter

Når du starter en Aurora-klynge, får du flere værtsnavne for at få adgang til klyngen. I forbindelse med dette svar er de eneste to, vi bekymrer os om, "cluster-endepunktet", som er læse-skrive, og "skrivebeskyttet slutpunktet", som er (du gættede det) skrivebeskyttet. Du har også et slutpunkt for hver node i klyngen, men at få adgang til noder direkte besejrer formålet med at bruge Aurora, så jeg vil ikke nævne dem igen.

Hvis jeg f.eks. opretter en klynge med navnet "eksempel", får jeg følgende slutpunkter:

  • Klyngeslutpunkt:example.cluster-x91qlr44xxxz.us-east-1.rds.amazonaws.com
  • Skrivebeskyttet slutpunkt:example.cluster-ro-x91qlr44xxxz.us-east-1.rds.amazonaws.com

Du tror måske, at disse endepunkter ville referere til noget som en Elastic Load Balancer, som ville være smart nok til at omdirigere trafik ved failover, men du tager fejl. Faktisk er de simpelthen DNS CNAME-poster med en meget kort levetid:

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

Når der sker en failover, opdateres CNAME'erne (fra example til 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

Den anden ting, der sker under en failover, er, at alle forbindelserne til "klynge"-slutpunktet bliver lukket, hvilket vil fejle eventuelle transaktioner i processen (forudsat at du har angivet rimelige forespørgsels-timeouts).

Forbindelserne til "skrivebeskyttet"-slutpunktet ikke bliver lukket, hvilket betyder, at uanset hvilken node der bliver forfremmet, vil den få læse-skrive-trafik ud over skrivebeskyttet trafik (under forudsætning af, selvfølgelig, at din applikation ikke bare sender alle anmodninger til klyngeslutpunktet). Da skrivebeskyttede forbindelser typisk bruges til relativt dyre forespørgsler (f.eks. rapportering), kan dette forårsage ydeevneproblemer for dine læse-skrive-operationer.

Problemet:DNS-cache

Når failover sker, vil alle igangværende transaktioner mislykkes (igen, forudsat at du har indstillet forespørgselstimeouts). Der vil være et kort stykke tid, at eventuelle nye forbindelser også mislykkes, da forbindelsespuljen forsøger at oprette forbindelse til den samme vært, før den er færdig med gendannelsen. Efter min erfaring tager failover omkring 15 sekunder, i hvilket tidsrum din applikation ikke skal forvente at få en forbindelse.

Efter de 15 sekunder (eller deromkring), skulle alt vende tilbage til det normale:din forbindelsespulje forsøger at oprette forbindelse til klyngeslutpunktet, den løser til IP-adressen på den nye læse-skriveknude, og alt er godt. Men hvis noget forhindrer løsningen af ​​den kæde af CNAME'er, kan du opleve, at din forbindelsespulje forbinder til et skrivebeskyttet slutpunkt, hvilket vil mislykkes, så snart du prøver en opdateringshandling.

I tilfældet med OP havde han sit eget CNAME med en længere timeout. Så i stedet for at oprette forbindelse til klyngeslutpunktet direkte, ville han oprette forbindelse til noget som database.example.com . Dette er en nyttig teknik i en verden, hvor du manuelt vil fail-over til en replikadatabase; Jeg formoder, at det er mindre nyttigt med Aurora. Uanset hvad, hvis du bruger dine egne CNAME'er til at referere til databaseslutpunkter, skal du have dem til at have korte tid-til-live-værdier (bestemt ikke mere end 5 sekunder).

I mit oprindelige svar påpegede jeg også, at Java cacher DNS-opslag, i nogle tilfælde for altid. Opførselen af ​​denne cache afhænger af (tror jeg) versionen af ​​Java, og også om du kører med en sikkerhedsmanager installeret. Med OpenJDK 8 kørende som en applikation, ser det ud til, at JVM'en vil uddelegere alle navngivningsopslag og ikke cache noget selv. Du bør dog være bekendt med networkaddress.cache.ttl systemegenskab, som beskrevet i dette Oracle-dokument og dette SO-spørgsmål.

Men selv efter at du har elimineret eventuelle uventede caches, kan der stadig være tidspunkter, hvor klyngeslutpunktet er løst til en skrivebeskyttet node. Det efterlader spørgsmålet om, hvordan du håndterer denne situation.

Ikke så god løsning:Brug en skrivebeskyttet test ved kassen

OP'et håbede at bruge en databaseforbindelsestest til at bekræfte, at hans applikation kørte på en skrivebeskyttet node. Dette er overraskende svært at gøre:de fleste forbindelsespuljer (inklusive HikariCP, som er, hvad OP'en bruger) bekræfter blot, at testforespørgslen udføres med succes; der er ingen mulighed for at se på, hvad det returnerer. Dette betyder, at enhver testforespørgsel skal give en undtagelse for at mislykkes.

Jeg har ikke været i stand til at finde på en måde at få MySQL til at kaste en undtagelse med blot en enkeltstående forespørgsel. Det bedste jeg er kommet frem til er at lave en funktion:

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 ;

Så kalder du den funktion i din testforespørgsel:

select throwIfReadOnly() 

Dette virker for det meste. Når jeg kørte mit testprogram, kunne jeg se en række "mislykkedes at validere forbindelse"-meddelelser, men så ville opdateringsforespørgslen på uforklarlig vis køre med en skrivebeskyttet forbindelse. Hikari har ikke en fejlretningsmeddelelse til at angive, hvilken forbindelse den udleverer, så jeg kunne ikke identificere, om den angiveligt havde bestået valideringen.

Men bortset fra det mulige problem, er der et dybere problem med denne implementering:den skjuler det faktum, at der er et problem. En bruger fremsætter en anmodning og venter måske i 30 sekunder på at få et svar. Der er intet i loggen (medmindre du aktiverer Hikaris fejlretningslogning) for at give en årsag til denne forsinkelse.

Desuden, mens databasen er utilgængelig, forsøger Hikari rasende at skabe forbindelser:i min enkelttrådede test ville den forsøge en ny forbindelse hvert 100. millisekund. Og det er rigtige forbindelser, de går simpelthen til den forkerte vært. Smid en app-server ind med et par dusin eller hundrede tråde, og det kan forårsage en betydelig ringvirkning på databasen.

Bedre løsning:Brug en skrivebeskyttet test ved kassen via en indpakning Datasource

I stedet for at lade Hikari i det stille prøve forbindelser igen, kan du indpakke HikariDataSource i din egen DataSource implementering og test/prøv dig selv igen. Dette har den fordel, at du faktisk kan se på resultaterne af testforespørgslen, hvilket betyder, at du kan bruge en selvstændig forespørgsel i stedet for at kalde en separat installeret funktion. Det lader dig også logge problemet ved hjælp af dine foretrukne logniveauer, lader dig pause mellem forsøgene og giver dig mulighed for at ændre poolkonfigurationen.

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

Denne løsning lider stadig af det problem, at den introducerer en forsinkelse i brugeranmodninger. Sandt nok, du ved, at det sker (hvilket du ikke gjorde med testen ved kassen), og du kunne indføre en timeout (begræns antallet af gange gennem løkken). Men det repræsenterer stadig en dårlig brugeroplevelse.

Den bedste (imo) løsning:Skift til "vedligeholdelsestilstand"

Brugere er utroligt utålmodige:Hvis det tager mere end et par sekunder at få et svar tilbage, vil de sandsynligvis forsøge at genindlæse siden eller indsende formularen igen eller gøre noget det hjælper ikke og kan gøre ondt.

Så jeg tror, ​​den bedste løsning er at fejle hurtigt og lade dem vide, at der er noget galt. Et sted nær toppen af ​​opkaldsstakken burde du allerede have noget kode, der reagerer på undtagelser. Måske returnerer du bare en generisk 500-side nu, men du kan gøre det lidt bedre:se på undtagelsen, og returner en "beklager, midlertidigt utilgængelig, prøv igen om et par minutter", hvis det er en skrivebeskyttet databaseundtagelse.

Samtidig skal du sende en meddelelse til dit ops-personale:dette kan være en normal fejl i vedligeholdelsesvinduet, eller det kan være noget mere alvorligt (men du skal ikke vække dem, medmindre du har en måde at vide, at det er mere alvorligt ).


indstil forbindelsespuljens inaktiv forbindelsestimeout i din java-kodedatakilde. indstillet til omkring 1000ms


Java tag