Detaillierte Erklärung der Gründe, warum MySQL-Verbindungen hängen bleiben

Detaillierte Erklärung der Gründe, warum MySQL-Verbindungen hängen bleiben

1. Hintergrund

In letzter Zeit wurden von Testern viele Probleme gemeldet, darunter besonders beunruhigende Probleme, die bei Systemzuverlässigkeitstests auftraten. Erstens treten solche Probleme manchmal „sporadisch“ auf und lassen sich in der Umgebung nur schwer schnell reproduzieren. Zweitens kann die Lokalisierungskette von Zuverlässigkeitsproblemen manchmal sehr lang sein. In extremen Fällen kann es erforderlich sein, von Dienst A zu Dienst Z oder vom Anwendungscode bis zur Hardwareebene zu verfolgen.

Dieses Mal zeige ich Ihnen, wie ich ein MySQL-Hochverfügbarkeitsproblem lokalisiert habe. Der Prozess war voller Wendungen, aber das Problem selbst ist recht repräsentativ, deshalb zeichne ich es als Referenz auf.

Architektur

Zunächst einmal verwendet dieses System MySQL als Hauptkomponente zur Datenspeicherung. Das Ganze ist eine typische Microservice-Architektur (SpringBoot + SpringCloud), und die Persistenzschicht verwendet die folgenden Komponenten:

Mybatis, realisiert SQL <-> Methodenzuordnung

hikaricp, Implementierung eines Datenbankverbindungspools

mariadb-java-client, implementiert JDBC-Treiber

Im MySQL-Serverteil verwendet das Backend eine Dual-Master-Architektur und das Front-End verwendet Keepalived in Kombination mit Floating IP (VIP), um eine Schicht hoher Verfügbarkeit bereitzustellen. wie folgt:

veranschaulichen

  • MySQL stellt zwei Instanzen bereit und setzt sie in eine Master-Slave-Beziehung.
  • Stellen Sie für jede MySQL-Instanz einen Keepalived-Prozess bereit. Keepalived bietet VIP-Failover mit hoher Verfügbarkeit. Tatsächlich sind sowohl Keepalived als auch MySQL in Containern untergebracht und der VIP-Port ist dem NodePort-Service-Port auf der VM zugeordnet.
  • Alle Unternehmensdienste verwenden VIP, um auf die Datenbank zuzugreifen.

Keepalived implementiert eine Routing-Layer-Konvertierung basierend auf dem VRRP-Protokoll. Gleichzeitig verweist VIP nur auf eine virtuelle Maschine (Master). Wenn der Masterknoten ausfällt, erkennen andere Keepalived-Knoten das Problem und wählen einen neuen Master aus. Danach wechselt VIP zu einem anderen verfügbaren MySQL-Instanzknoten. Auf diese Weise verfügt die MySQL-Datenbank über grundlegende Hochverfügbarkeitsfunktionen.

Ein weiterer Punkt ist, dass Keepalived auch regelmäßige Integritätsprüfungen der MySQL-Instanz durchführt. Sobald es feststellt, dass die MySQL-Instanz nicht verfügbar ist, beendet es seinen eigenen Prozess, wodurch dann die VIP-Umschaltaktion ausgelöst wird.

Problemphänomen

Auch dieser Testfall basiert auf dem Szenario eines Ausfalls einer virtuellen Maschine:

Greifen Sie mit weniger Druck weiterhin auf den Geschäftsdienst zu und starten Sie dann eine der MySQL-Containerinstanzen (Master) neu. Der ursprünglichen Einschätzung zufolge kann es im Geschäftsbetrieb zu sehr geringen Schwankungen kommen, die Unterbrechungszeit sollte jedoch auf der zweiten Ebene gehalten werden.

Allerdings wurde nach vielen Tests festgestellt, dass nach einem Neustart des MySQL Masternode Containers mit einer gewissen Wahrscheinlichkeit das Business nicht mehr erreichbar ist!

2. Analyseprozess

Nachdem das Problem aufgetreten war, war die erste Reaktion des Entwicklers, dass es ein Problem mit dem Hochverfügbarkeitsmechanismus von MySQL gab. Da es in der Vergangenheit Probleme gab, bei denen der VIP-Wechsel aufgrund einer falschen Keepalived-Konfiguration nicht rechtzeitig erfolgte, sind wir bereits auf der Hut davor.

Nach einer gründlichen Untersuchung habe ich keine Konfigurationsprobleme mit Keepalived gefunden.

Da ich keine anderen Optionen hatte, habe ich die Tests ein paar Mal wiederholt und das Problem trat erneut auf.

Wir haben dann mehrere Fragen gestellt:

1.Keepalived wird eine Beurteilung basierend auf der Erreichbarkeit der MySQL-Instanz vornehmen. Könnte es ein Problem mit der Integritätsprüfung geben?

In diesem Testszenario führt die Zerstörung des MySQL-Containers jedoch dazu, dass die Porterkennung von Keepalived fehlschlägt, was wiederum dazu führt, dass Keepalived fehlschlägt. Wenn auch Keepalived beendet wird, sollte VIP automatisch vorzeitig beendet werden. Durch Vergleich der Informationen der beiden virtuellen Maschinenknoten wurde festgestellt, dass der VIP tatsächlich umgeschaltet wurde.

2. Ist der Container, in dem sich der Geschäftsprozess befindet, im Netzwerk nicht erreichbar?

Versuchen Sie, den Container zu betreten und führen Sie nach dem Wechsel einen Telnet-Test für die Floating-IP und den Port durch. Sie werden feststellen, dass der Zugriff weiterhin erfolgreich ist.

Verbindungspool

Nachdem wir die beiden vorherigen verdächtigen Punkte behoben haben, können wir unsere Aufmerksamkeit nur noch dem DB-Client des Business-Dienstes zuwenden.

Aus den Protokollen können wir ersehen, dass zum Zeitpunkt des Fehlers auf der Geschäftsseite einige Ausnahmen aufgetreten sind:

JDBC-Verbindung kann nicht hergestellt werden [n/a]

java.sql.SQLTransientConnectionException: HikariPool-1 – Verbindung ist nicht verfügbar, Anforderung nach 30000 ms abgelaufen.

bei com.zaxxer.hikari.pool.HikariPool.createTimeoutException(HikariPool.java:669) ~[HikariCP-2.7.9.jar!/?]

bei com.zaxxer.hikari.pool.HikariPool.getConnection(HikariPool.java:183) ~[HikariCP-2.7.9.jar!/?]

...

Die Meldung hier lautet, dass die Zeit zum Herstellen der Verbindung für den Geschäftsvorgang abgelaufen ist (über 30 Sekunden). Kann es sein, dass die Anzahl der Verbindungen nicht ausreicht?

Der Business-Zugang nutzt den hikariCP-Verbindungspool, welcher ebenfalls eine sehr beliebte Komponente auf dem Markt ist.

Anschließend haben wir die aktuelle Konfiguration des Verbindungspools wie folgt überprüft:

//Mindestanzahl inaktiver Verbindungen spring.datasource.hikari.minimum-idle=10
//Die maximale Größe des Verbindungspools spring.datasource.hikari.maximum-pool-size=50
// Maximale Leerlaufzeit der Verbindung spring.datasource.hikari.idle-timeout=60000
//Lebensdauer der Verbindung spring.datasource.hikari.max-lifetime=1800000
//Länge des Verbindungstimeouts abrufen spring.datasource.hikari.connection-timeout=30000

Es wird darauf hingewiesen, dass der Hikari-Verbindungspool mit einem Mindestleerlauf von 10 konfiguriert ist. Dies bedeutet, dass der Verbindungspool auch bei fehlender Geschäftstätigkeit 10 Verbindungen garantieren sollte. Hinzu kommt, dass das derzeitige Geschäftszugriffsvolumen äußerst gering ist und es nicht zu einer Situation kommen sollte, in der die Anzahl der Verbindungen nicht ausreicht.

Eine weitere Möglichkeit besteht darin, dass „Zombie-Verbindungen“ auftreten. Das heißt, während des Neustartvorgangs hat der Verbindungspool diese nicht verfügbaren Verbindungen nicht freigegeben, sodass keine Verbindungen verfügbar sind.

Die Entwickler glaubten an die „Zombie-Link“-Theorie und neigten zu der Annahme, dass die Ursache wahrscheinlich ein Fehler in der HikariCP-Komponente war …

Also begann ich, den Quellcode von HikariCP zu lesen und fand heraus, dass der Code, in dem die Anwendungsschicht eine Verbindung vom Verbindungspool anfordert, wie folgt lautet:

öffentliche Klasse HikariPool {

   //Holen Sie sich den Verbindungsobjekteintrag public Connection getConnection(final long hardTimeout) throws SQLException
   {
      suspendResumeLock.acquire();
      endgültige lange Startzeit = aktuelle Zeit ();

      versuchen {
         //Verwende das voreingestellte 30-Sekunden-Timeout. long timeout = hardTimeout;
         Tun {
            //Betreten Sie die Schleife und holen Sie sich innerhalb der angegebenen Zeit verfügbare Verbindungen. //Holen Sie sich Verbindungen vom ConnectionBag PoolEntry. poolEntry = connectionBag.borrow(timeout, MILLISECONDS);
            wenn (Pooleintrag == null) {
               break; // Unsere Zeit ist abgelaufen... break und Ausnahme auslösen
            }

            endgültig lang jetzt = aktuelleZeit();
            //Wenn das Verbindungsobjekt als gelöscht markiert ist oder die Überlebensbedingungen nicht erfüllt, schließen Sie die Verbindung, wenn (poolEntry.isMarkedEvicted() || (elapsedMillis(poolEntry.lastAccessed, now) > aliveBypassWindowMs && !isConnectionAlive(poolEntry.connection))) {
               Verbindung schließen(Pooleintrag, Pooleintrag.isMarkedEvicted() ? EVICTED_CONNECTION_MESSAGE : DEAD_CONNECTION_MESSAGE);
               Timeout = HardTimeout – verstricheneMillis(Startzeit);
            }
            //Verbindungsobjekt erfolgreich abrufen, sonst {
               metricsTracker.recordBorrowStats(poolEntry, startTime);
               gib poolEntry.createProxyConnection zurück (leakTaskFactory.schedule (poolEntry), jetzt);
            }
         } während (Timeout > 0L);

         //Timeout, Ausnahme auslösen metricsTracker.recordBorrowTimeoutStats(startTime);
         auslösen: createTimeoutException(startTime);
      }
      Fang (UnterbrocheneAusnahme e) {
         Thread.currentThread().interrupt();
         throw new SQLException(poolName + " - Während des Verbindungsaufbaus unterbrochen", e);
      }
      Endlich {
         suspendResumeLock.release();
      }
   }
}

Die Methode getConnection() zeigt den gesamten Prozess zum Herstellen einer Verbindung, wobei connectionBag ein Containerobjekt zum Speichern von Verbindungsobjekten ist. Wenn die von connectionBag erhaltene Verbindung die Überlebensbedingung nicht mehr erfüllt, wird sie manuell geschlossen. Der Code lautet wie folgt:

void closeConnection(finaler PoolEntry poolEntry, finaler String closureReason)
   {
      //Entfernen Sie das Verbindungsobjekt, wenn (connectionBag.remove(poolEntry)) {
         endgültige Verbindung Verbindung = poolEntry.close();
         //Verbindung asynchron schließen closeConnectionExecutor.execute(() -> {
            quietlyCloseConnection(Verbindung, Schließungsgrund);
            //Wenn die Anzahl der verfügbaren Verbindungen abnimmt, wird die Aufgabe zum Füllen des Verbindungspools ausgelöst, wenn (poolState == POOL_NORMAL) {
               Füllpool();
            }
         });
      }
   }

Beachten Sie, dass die Verbindung nur geschlossen wird, wenn eine der folgenden Bedingungen erfüllt ist:

  • Das Rückgabeergebnis von isMarkedEvicted() ist „true“, was bedeutet, dass es als gelöscht markiert ist. Wenn die Überlebenszeit der Verbindung die maximale Überlebenszeit (maxLifeTime) überschreitet oder die Zeit seit der letzten Verwendung „idleTimeout“ überschreitet, wird es von der geplanten Aufgabe als gelöscht markiert. Die Verbindung im gelöschten Zustand wird tatsächlich geschlossen, wenn sie abgerufen wird.
  • Die Verbindung ist nicht mehr aktiv, wenn sie nicht innerhalb von 500 ms verwendet wurde, d. h. isConnectionAlive() gibt „false“ zurück.

Da wir sowohl idleTimeout als auch maxLifeTime auf sehr große Werte gesetzt haben, müssen wir uns darauf konzentrieren, die Beurteilung in der isConnectionAlive-Methode wie folgt zu überprüfen:

öffentliche Klasse PoolBase {

   //Beurteilen, ob die Verbindung aktiv ist boolean isConnectionAlive(final Connection connection)
   {
      versuchen {
         versuchen {
            //Ausführungstimeout der JDBC-Verbindung festlegen setNetworkTimeout(connection, validationTimeout);

            letzte int ValidierungsSeconds = (int) Math.max(1000L, ValidierungsTimeout) / 1000;

            //Wenn TestQuery nicht festgelegt ist, verwenden Sie die JDBC4-Validierungsschnittstelle if (isUseJdbc4Validation) {
               gibt Verbindung zurück.istgültig(Validierungssekunden);
            }

            //Verwenden Sie eine TestQuery-Anweisung (z. B. select 1), um die Verbindung zu ermitteln. try (Statement statement = connection.createStatement()) {
               wenn (isNetworkTimeoutSupported != TRUE) {
                  setQueryTimeout(Anweisung, Validierungssekunden);
               }

               Anweisung.ausführen(config.getConnectionTestQuery());
            }
         }
         Endlich {
            setNetworkTimeout(Verbindung, Netzwerk-Timeout);

            wenn (isIsolateInternalQueries && !isAutoCommit) {
               Verbindung.Rollback();
            }
         }

         gibt true zurück;
      }
      Fang (Ausnahme e) {
         //Wenn eine Ausnahme auftritt, zeichnen Sie die Fehlerinformationen im Kontext auf lastConnectionFailure.set(e);
         logger.warn("{} - Verbindung {} ({}) konnte nicht validiert werden. Erwägen Sie ggf. die Verwendung eines kürzeren maxLifetime-Werts.",
                     PoolName, Verbindung, z. B. getMessage());
         gibt false zurück;
      }
   }

}

Wir können sehen, dass in der Methode PoolBase.isConnectionAlive eine Reihe von Erkennungen an der Verbindung durchgeführt werden, und wenn eine Ausnahme auftritt, werden die Ausnahmeinformationen im aktuellen Threadkontext aufgezeichnet. Wenn HikariPool dann eine Ausnahme auslöst, wird auch die Ausnahme der letzten fehlgeschlagenen Erkennung wie folgt erfasst:

private SQLException createTimeoutException(lange Startzeit)
{
   logPoolState("Timeout-Fehler");
   metricsTracker.recordConnectionTimeout();

   Zeichenfolge sqlState = null;
   //Letzte Verbindungsfehlerausnahme abrufen final Throwable originalException = getLastConnectionFailure();
   if (originalException-Instanz von SQLException) {
      sqlState = ((SQLException) originalException).getSQLState();
   }
   //Eine Ausnahme auslösen finale SQLException connectionException = new SQLTransientConnectionException(poolName + " - Verbindung ist nicht verfügbar, Zeitüberschreitung der Anforderung nach " + elapsedMillis(startTime) + "ms.", sqlState, originalException);
   if (originalException-Instanz von SQLException) {
      connectionException.setNextException((SQLException) originalException);
   }

   Verbindungsausnahme zurückgeben;
}

Die Ausnahmemeldung hier stimmt im Wesentlichen mit dem Ausnahmeprotokoll überein, das wir im Geschäftsdienst sehen. Zusätzlich zu der durch das Timeout generierten Meldung „Verbindung ist nicht verfügbar, Anforderung nach xxx ms abgelaufen“ gibt das Protokoll auch die Informationen zum Überprüfungsfehler aus:

Ursache: java.sql.SQLException: Connection.setNetworkTimeout kann bei einer geschlossenen Verbindung nicht aufgerufen werden

bei org.mariadb.jdbc.internal.util.exceptions.ExceptionMapper.getSqlException(ExceptionMapper.java:211) ~[mariadb-java-client-2.2.6.jar!/?]

bei org.mariadb.jdbc.MariaDbConnection.setNetworkTimeout(MariaDbConnection.java:1632) ~[mariadb-java-client-2.2.6.jar!/?]

bei com.zaxxer.hikari.pool.PoolBase.setNetworkTimeout(PoolBase.java:541) ~[HikariCP-2.7.9.jar!/?]

bei com.zaxxer.hikari.pool.PoolBase.isConnectionAlive(PoolBase.java:162) ~[HikariCP-2.7.9.jar!/?]

bei com.zaxxer.hikari.pool.HikariPool.getConnection(HikariPool.java:172) ~[HikariCP-2.7.9.jar!/?]

bei com.zaxxer.hikari.pool.HikariPool.getConnection(HikariPool.java:148) ~[HikariCP-2.7.9.jar!/?]

bei com.zaxxer.hikari.HikariDataSource.getConnection(HikariDataSource.java:128) ~[HikariCP-2.7.9.jar!/?]

An diesem Punkt haben wir den Code für die Anwendung zum Herstellen einer Verbindung grob aussortiert. Der gesamte Prozess wird in der folgenden Abbildung dargestellt:

Aus Sicht der Ausführungslogik gibt es bei der Verarbeitung des Verbindungspools kein Problem. Im Gegenteil, viele Details wurden berücksichtigt. Wenn eine nicht aktive Verbindung geschlossen wird, wird auch die Aktion „removeFromBag“ aufgerufen, um sie aus dem Verbindungspool zu entfernen. Daher sollte es kein Problem mit Zombie-Verbindungsobjekten geben.

Dann müssen unsere bisherigen Spekulationen falsch sein!

In Angst verfallen

Neben der Codeanalyse ist den Entwicklern auch aufgefallen, dass die aktuell verwendete hikariCP-Version 3.4.5 ist, während der Business-Service mit Problemen in der Umgebung die Version 2.7.9 ist. Das scheint auf etwas hinzuweisen... Nehmen wir erneut an, dass es in hikariCP Version 2.7.9 einen unbekannten BUG gibt, der zu dem Problem führt.

Um das Verhalten des Verbindungspools im Umgang mit serverseitigen Fehlern weiter zu analysieren, haben wir versucht, es auf einem lokalen Computer zu simulieren. Dieses Mal haben wir hikariCP 2.7.9 zum Testen verwendet und die hikariCP-Protokollebene auf DEBUG gesetzt.

Im Simulationsszenario stellt die lokale Anwendung für den Betrieb eine Verbindung zur lokalen MySQL-Datenbank her. Die Schritte sind wie folgt:

1. Initialisieren Sie die Datenquelle und setzen Sie den Mindestleerlauf des Verbindungspools auf 10.

2. Führen Sie alle 50 ms einen SQL-Vorgang aus, um die aktuelle Metadatentabelle abzufragen.

3. Stoppen Sie den MySQL-Dienst für eine Weile und beobachten Sie die Geschäftsleistung.

4. Starten Sie den MySQL-Dienst neu und beobachten Sie die Dienstleistung.

Das resultierende Protokoll sieht wie folgt aus:

// Initialisierungsprozess, 10 Verbindungen herstellen

DEBUG -HikariPool.logPoolState – Pool-Statistiken (Gesamt=1, aktiv=1, Leerlauf=0, wartend=0)

DEBUG -HikariPool$PoolEntryCreator.call- Verbindung hinzugefügt MariaDbConnection@71ab7c09

DEBUG -HikariPool$PoolEntryCreator.call- Verbindung hinzugefügt MariaDbConnection@7f6c9c4c

DEBUG -HikariPool$PoolEntryCreator.call- Verbindung hinzugefügt MariaDbConnection@7b531779

...

DEBUG -HikariPool.logPoolState- Nach dem Hinzufügen von Statistiken (Gesamt=10, aktiv=1, Leerlauf=9, wartend=0)

//Geschäftsbetrieb durchführen, Erfolg

Execute-Anweisung: true

Testzeit -------1

Execute-Anweisung: true

Testzeit -------2

...

//MySQL stoppen

...

//Ungültige Verbindung erkannt

WARNUNG -PoolBase.isConnectionAlive - Verbindung konnte nicht validiert werden MariaDbConnection@9225652 ((conn=38652)

Connection.setNetworkTimeout kann bei einer geschlossenen Verbindung nicht aufgerufen werden.) Erwägen Sie ggf. die Verwendung eines kürzeren maxLifetime-Werts.

WARNUNG -PoolBase.isConnectionAlive - Verbindung konnte nicht validiert werden MariaDbConnection@71ab7c09 ((conn=38653)

Connection.setNetworkTimeout kann bei einer geschlossenen Verbindung nicht aufgerufen werden.) Erwägen Sie ggf. die Verwendung eines kürzeren maxLifetime-Werts.

//Verbindung lösen

DEBUG -PoolBase.quietlyCloseConnection(PoolBase.java:134) – Verbindung wird geschlossen MariaDbConnection@9225652: (Verbindung ist tot)

DEBUG -PoolBase.quietlyCloseConnection(PoolBase.java:134) – Verbindung wird geschlossen MariaDbConnection@71ab7c09: (Verbindung ist tot)

//Verbindung konnte nicht hergestellt werden

DEBUG -HikariPool.createPoolEntry - Kann keine Verbindung von der Datenquelle herstellen

java.sql.SQLNonTransientConnectionException: Verbindung zu Adresse=(Host=localhost)(Port=3306)(Typ=Master) konnte nicht hergestellt werden:

Socket kann keine Verbindung zum Host: localhost, Port: 3306 herstellen. Verbindung abgelehnt: connect

Ursache: java.sql.SQLNonTransientConnectionException: Socket konnte keine Verbindung zu Host: localhost, Port: 3306 herstellen. Verbindung abgelehnt: connect

bei internal.util.exceptions.ExceptionFactory.createException(ExceptionFactory.java:73) ~[mariadb-java-client-2.6.0.jar:?]

...

// Scheitert weiterhin... bis MySQL neu gestartet wird

//Nach dem Neustart wird die Verbindung automatisch erfolgreich hergestellt

DEBUG -HikariPool$PoolEntryCreator.call -Verbindung hinzugefügt MariaDbConnection@42c5503e

DEBUG -HikariPool$PoolEntryCreator.call -Verbindung hinzugefügt MariaDbConnection@695a7435

//Status des Verbindungspools, 10 Verbindungen wiederherstellen

DEBUG -HikariPool.logPoolState(HikariPool.java:421) -Nach dem Hinzufügen von Statistiken (Gesamt=10, aktiv=1, Leerlauf=9, wartend=0)

//Geschäftsvorgang ausführen, Erfolg (Selbstheilung)

Execute-Anweisung: true

Aus den Protokollen ist ersichtlich, dass hikariCP die fehlerhafte Verbindung erfolgreich erkennen und aus dem Verbindungspool entfernen kann. Nach dem Neustart von MySQL kann der Geschäftsbetrieb automatisch erfolgreich wiederhergestellt werden. Aufgrund dieses Ergebnisses schlug die auf dem HikariCP-Versionsproblem basierende Idee erneut fehl und das Forschungs- und Entwicklungsteam geriet erneut in Angst und Schrecken.

Vertreibe die Wolken und sieh das Licht

Nachdem viele Versuche zur Verifizierung des Problems fehlschlugen, versuchten wir schließlich, Pakete im Container zu erfassen, in dem sich der Geschäftsdienst befindet, um zu sehen, ob wir irgendwelche Hinweise finden konnten.

Geben Sie den fehlerhaften Container ein, führen Sie „tcpdump -i eth0 tcp port 30052“ aus, um Pakete zu erfassen, und greifen Sie dann auf die Serviceschnittstelle zu.

In diesem Moment passierte etwas Seltsames, es wurden keine Netzwerkpakete generiert! Das Geschäftsprotokoll zeigte außerdem eine Ausnahme bezüglich des Fehlschlagens der Verbindungsherstellung nach 30 Sekunden.

Wir haben die Netzwerkverbindung mit dem Befehl netstat überprüft und festgestellt, dass nur eine TCP-Verbindung im Status ESTABLISHED vorhanden war.

Mit anderen Worten, es besteht eine hergestellte Verbindung zwischen der aktuellen Geschäftsinstanz und dem MySQL-Server, aber warum meldet das Unternehmen immer noch eine verfügbare Verbindung?

Dafür gibt es zwei mögliche Gründe:

  • Die Verbindung wird durch einen Dienst (z. B. einen Timer) belegt.
  • Die Verbindung ist noch nicht wirklich nutzbar und befindet sich möglicherweise in einem toten Zustand.

Grund eins lässt sich schnell widerlegen. Erstens hat der aktuelle Dienst keine Timer-Aufgabe. Zweitens, selbst wenn die Verbindung belegt ist, sollten neue Geschäftsanforderungen den Verbindungspool nach dem Prinzip des Verbindungspools veranlassen, eine neue Verbindung herzustellen, solange die Obergrenze nicht erreicht ist. Daher sollte es, unabhängig davon, ob es sich um die Überprüfung des Netstat-Befehls oder das TCPdump-Ergebnis handelt, nicht immer nur eine Verbindung geben.

Dann ist Situation 2 sehr wahrscheinlich. Behalten Sie diesen Gedanken im Hinterkopf und fahren Sie mit der Analyse des Thread-Stapels des Java-Prozesses fort.

Nach der Ausführung von kill -3 pid zur Ausgabe und Analyse des Thread-Stacks werden wie erwartet folgende Einträge im aktuellen Thread-Stack gefunden:

"HikariPool-1 Verbindungsaddierer" #121 Daemon prio=5 os_prio=0 tid=0x00007f1300021800 nid=0xad ausführbar [0x00007f12d82e5000]

java.lang.Thread.State: AUSFÜHRBAR

bei java.net.SocketInputStream.socketRead0 (native Methode)

bei java.net.SocketInputStream.socketRead(SocketInputStream.java:116)

bei java.net.SocketInputStream.read(SocketInputStream.java:171)

bei java.net.SocketInputStream.read(SocketInputStream.java:141)

bei java.io.FilterInputStream.read(FilterInputStream.java:133)

bei org.mariadb.jdbc.internal.io.input.ReadAheadBufferedStream.fillBuffer(ReadAheadBufferedStream.java:129)

bei org.mariadb.jdbc.internal.io.input.ReadAheadBufferedStream.read(ReadAheadBufferedStream.java:102)

- gesperrt <0x00000000d7f5b480> (ein org.mariadb.jdbc.internal.io.input.ReadAheadBufferedStream)

bei org.mariadb.jdbc.internal.io.input.StandardPacketInputStream.getPacketArray(StandardPacketInputStream.java:241)

bei org.mariadb.jdbc.internal.io.input.StandardPacketInputStream.getPacket(StandardPacketInputStream.java:212)

bei org.mariadb.jdbc.internal.com.read.ReadInitialHandShakePacket.<init>(ReadInitialHandShakePacket.java:90)

bei org.mariadb.jdbc.internal.protocol.AbstractConnectProtocol.createConnection(AbstractConnectProtocol.java:480)

bei org.mariadb.jdbc.internal.protocol.AbstractConnectProtocol.connectWithoutProxy(AbstractConnectProtocol.java:1236)

bei org.mariadb.jdbc.internal.util.Utils.retrieveProxy(Utils.java:610)

bei org.mariadb.jdbc.MariaDbConnection.newConnection(MariaDbConnection.java:142)

bei org.mariadb.jdbc.Driver.connect(Driver.java:86)

bei com.zaxxer.hikari.util.DriverDataSource.getConnection(DriverDataSource.java:138)

bei com.zaxxer.hikari.pool.PoolBase.newConnection(PoolBase.java:358)

bei com.zaxxer.hikari.pool.PoolBase.newPoolEntry(PoolBase.java:206)

bei com.zaxxer.hikari.pool.HikariPool.createPoolEntry(HikariPool.java:477)

Hier zeigt sich, dass sich der HikariPool-1-Verbindungs-Addierer-Thread immer im ausführbaren Zustand von socketRead befindet. Dem Namen nach sollte dieser Thread der Task-Thread sein, der vom HikariCP-Verbindungspool zum Herstellen von Verbindungen verwendet wird. Der Socket-Lesevorgang stammt von der Methode MariaDbConnection.newConnection(), die ein Vorgang der MariaDB-Java-Client-Treiberschicht zum Herstellen einer MySQL-Verbindung ist. Die ReadInitialHandShakePacket-Initialisierung ist ein Link im MySQL-Verbindungsaufbauprotokoll.

Kurz gesagt, der obige Thread befindet sich gerade im Prozess des Linkaufbaus. Der Prozess des Linkaufbaus zwischen dem MariaDB-Treiber und MySQL läuft wie folgt ab:

Der erste Schritt beim Aufbau einer MySQL-Verbindung besteht darin, eine TCP-Verbindung herzustellen (Drei-Wege-Handshake). Der Client liest ein erstes Handshake-Nachrichtenpaket des MySQL-Protokolls, das Informationen wie die MySQL-Versionsnummer, den Authentifizierungsalgorithmus usw. enthält, und tritt dann in die Phase der Identitätsauthentifizierung ein.

Das Problem hierbei besteht darin, dass die Initialisierung von ReadInitialHandShakePacket (Lesen des Handshake-Nachrichtenpakets) in einem Socket-Lesezustand stattgefunden hat.

Wenn der MySQL-Remotehost zu diesem Zeitpunkt ausfällt, bleibt der Vorgang hängen. Obwohl die Verbindung zu diesem Zeitpunkt hergestellt wurde (im Status ESTABLISHED), sind der Protokoll-Handshake und der nachfolgende Identitätsauthentifizierungsprozess noch nicht abgeschlossen. Das heißt, die Verbindung kann nur als Halbfertigprodukt betrachtet werden (sie kann nicht in die Liste des hikariCP-Verbindungspools aufgenommen werden). Aus dem DEBUG-Log des fehlerhaften Dienstes können wir außerdem ersehen, dass im Verbindungspool keine verfügbaren Verbindungen vorhanden sind, und zwar wie folgt:

DEBUG HikariPool.logPoolState --> Statistiken vor der Bereinigung (Gesamt=0, aktiv=0, Leerlauf=0, wartend=3)

Eine weitere zu klärende Frage ist, ob das Blockieren eines solchen Socket-Lesevorgangs zum Blockieren des gesamten Verbindungspools führt.

Nach dem Lesen des Codes haben wir den Prozess zum Herstellen einer Verbindung von hikariCP geklärt, der mehrere Module umfasst:

  • HikariPool, eine Verbindungspoolinstanz, wird zum Herstellen, Freigeben und Aufrechterhalten von Verbindungen verwendet.
  • ConnectionBag, ein Verbindungsobjekt-Container, speichert die aktuelle Liste der Verbindungsobjekte und wird zum Bereitstellen verfügbarer Verbindungen verwendet.
  • AddConnectionExecutor fügt einen Verbindungs-Executor hinzu, der beispielsweise „HikariPool-1 Connection Adder“ heißt und ein Thread-Pool mit einem Thread ist.
  • PoolEntryCreator fügt die Verbindungsaufgabe hinzu und implementiert die spezifische Logik zum Erstellen einer Verbindung.
  • HouseKeeper, ein interner Timer, wird verwendet, um die Beseitigung von Verbindungs-Timeouts, die Auffüllung des Verbindungspools usw. zu implementieren.

HouseKeeper wird 100 ms nach der Initialisierung des Verbindungspools ausgeführt. Es ruft die Methode fillPool() auf, um das Füllen des Verbindungspools abzuschließen. Wenn beispielsweise min-idle 10 beträgt, werden bei der Initialisierung 10 Verbindungen erstellt. ConnectionBag verwaltet eine Liste der aktuellen Verbindungsobjekte. Das Modul verwaltet außerdem einen Zähler der Verbindungsanforderer (Waiter), um die aktuelle Anzahl der Verbindungsanforderungen auszuwerten.

Die Logik der Borrow-Methode ist wie folgt:

öffentliches T-borgen (langes Timeout, letzte TimeUnit TimeUnit) wirft InterruptedException
   {
      // Versuchen Sie, die endgültige Liste <Objekt> list = threadList.get(); vom Thread-Local abzurufen.
      für (int i = Liste.Größe() - 1; i >= 0; i--) {
         ...
      }

      // Berechnen Sie die Aufgaben, die aktuell auf die Anforderung warten. final int waiting = waiters.incrementAndGet();
      versuchen {
         für (T bagEntry : sharedList) {
            if (bagEntry.compareAndSet(STATUS_NICHT_IN_VERWENDET, STATUS_IN_VERWENDET)) {
               //Wenn eine verfügbare Verbindung hergestellt wird, wird die Füllaufgabe ausgelöst, wenn (Warten > 1) {
                  listener.addBagItem(wartend - 1);
               }
               Beuteleintrag zurückgeben;
            }
         }

         //Keine Verbindung verfügbar, löse zuerst die Füllaufgabe aus listener.addBagItem(waiting);

         //Warten Sie, bis innerhalb der angegebenen Zeit eine verfügbare Verbindung zustande kommt. Timeout = timeUnit.toNanos(Timeout);
         Tun {
            endgültiger langer Start = aktuelleZeit();
            endgültiges T bagEntry = handoffQueue.poll(Timeout, NANOSEKUNDEN);
            if (bagEntry == null || bagEntry.compareAndSet(ZUSTAND_NICHT_IN_VERWENDET, ZUSTAND_IN_VERWENDET)) {
               Beuteleintrag zurückgeben;
            }

            Zeitüberschreitung -= verstricheneNanos(Start);
         } während (Timeout > 10_000);

         gibt null zurück;
      }
      Endlich {
         waiters.decrementAndGet();
      }
   }

Beachten Sie, dass diese Methode eine listener.addBagItem()-Methode auslöst, unabhängig davon, ob eine Verbindung verfügbar ist. HikariPool implementiert diese Schnittstelle wie folgt:

öffentliche void addBagItem(finale int wartend)
   {
      final boolean shouldAdd = waiting - addConnectionQueueReadOnlyView.size() >= 0; // Ja, >= ist beabsichtigt.
      wenn (sollteHinzufügen) {
         //Rufen Sie AddConnectionExecutor auf, um die Aufgabe zum Erstellen einer Verbindung zu übermitteln. addConnectionExecutor.submit(poolEntryCreator);
      }
      anders {
         logger.debug("{} - Verbindung ausgelassen hinzufügen, wartend {}, Warteschlange {}", Poolname, wartend, addConnectionQueueReadOnlyView.size());
      }
   }
PoolEntryCreator implementiert die spezifische Logik zum Erstellen einer Verbindung wie folgt:
öffentliche Klasse PoolEntryCreator {
     @Überschreiben
      öffentlicher Boolescher Aufruf ()
      {
         langer SleepBackoff = 250L;
         //Bestimmen Sie, ob eine Verbindung hergestellt werden muss, während (poolState == POOL_NORMAL && shouldCreateAnotherConnection()) {
            //MySQL-Verbindung erstellen final PoolEntry poolEntry = createPoolEntry();
 
            wenn (Pooleintrag != null) {
               //Die Verbindung wurde erfolgreich hergestellt und kehrt direkt zurück.
               VerbindungsBag.add(poolEntry);
               logger.debug("{} - Verbindung {} hinzugefügt", poolName, poolEntry.connection);
               if (loggingPrefix != null) {
                  logPoolState(loggingPrefix);
               }
               gibt Boolean.TRUE zurück;
            }
            ...
         }

         // Der Pool ist angehalten oder heruntergefahren oder hat die maximale Größe erreicht.
         gibt Boolean.FALSE zurück;
      }
}

Es ist ersichtlich, dass AddConnectionExecutor ein Single-Thread-Design annimmt. Wenn eine neue Verbindungsanforderung generiert wird, wird die PoolEntryCreator-Aufgabe asynchron ausgelöst, um sie zu ergänzen. PoolEntryCreator.createPoolEntry() übernimmt die gesamte Arbeit zum Herstellen der MySQL-Treiberverbindung, in unserem Fall ist der MySQL-Verbindungsherstellungsprozess jedoch dauerhaft blockiert. Daher wird die Aufgabe zum Herstellen einer neuen Verbindung immer in die Warteschlange gestellt, unabhängig davon, wie die Verbindung später hergestellt wird. Dies führt dazu, dass für das Unternehmen keine Verbindung verfügbar ist.

Die folgende Abbildung veranschaulicht den Linkaufbauprozess von hikariCP:

OK, lassen Sie uns das vorherige Szenario zum Zuverlässigkeitstest noch einmal durchgehen:

Zuerst fiel die MySQL-Masterinstanz aus, dann erkannte hikariCP eine tote Verbindung und gab sie frei. Beim Freigeben der geschlossenen Verbindung stellte es fest, dass die Anzahl der Verbindungen aufgefüllt werden musste, was sofort eine neue Verbindungsaufbauanforderung auslöste.
Das Problem lag zufällig bei dieser Verbindungsaufbauanforderung. Der TCP-Handshake war erfolgreich (der Client und der NodePort auf der MySQL-VM stellten die Verbindung her), aber da der aktuelle MySQL-Container angehalten wurde (der VIP wurde zu diesem Zeitpunkt auch auf eine andere MySQL-Instanz umgeschaltet), konnte der Client die Handshake-Paketantwort nicht mehr von der ursprünglichen MySQL-Instanz erhalten (der Handshake gehört zum MySQL-Anwendungsschichtprotokoll) und geriet in einen langwierigen, blockierenden SocketRead-Vorgang. Die Anforderung zum Linkaufbau erfolgt ausschließlich in einem Thread, was zur Blockierung aller Dienste führt.

3. Lösung

Nachdem wir uns mit der Materie befasst hatten, betrachteten wir die Optimierung hauptsächlich aus zwei Blickwinkeln:

  • Optimierung 1: Erhöhen Sie die Anzahl der AddConnectionExecutor-Threads in HirakiPool, sodass selbst wenn der erste Thread hängt, andere Threads an der Zuweisung von Linkaufbau-Aufgaben teilnehmen können.
  • Optimierung 2: Beim problematischen socketRead handelt es sich um einen synchronen blockierenden Aufruf, der durch die Verwendung von SO_TIMEOUT vermieden werden kann, um ein längeres Hängenbleiben zu verhindern.

Was Optimierungspunkt 1 betrifft, sind wir uns alle einig, dass er nicht sehr nützlich ist. Wenn die Verbindung hängt, bedeutet dies, dass die Thread-Ressourcen verloren gegangen sind, was sich sehr nachteilig auf den nachfolgenden stabilen Betrieb des Dienstes auswirkt. Darüber hinaus hat hikariCP dies hier bereits geschrieben. Die wichtigste Lösung besteht daher darin, das Blockieren von Anrufen zu vermeiden.

Nachdem ich die offizielle Dokumentation von MariaDB-Java-Client konsultiert hatte, stellte ich fest, dass der Parameter für das Netzwerk-E/A-Timeout in der JDBC-URL wie folgt angegeben werden kann:

Spezifische Referenz: https://mariadb.com/kb/en/about-mariadb-connector-j/

Wie beschrieben kann socketTimeout das SO_TIMEOUT-Attribut des Sockets festlegen, um die Timeout-Periode zu steuern. Der Standardwert ist 0, was bedeutet, dass kein Timeout erfolgt.

Wir haben der MySQL JDBC-URL die relevanten Parameter wie folgt hinzugefügt:

spring.datasource.url=jdbc:mysql://10.0.71.13:33052/appdb?socketTimeout=60000&connectTimeout=30000&serverTimezone=UTC

Danach haben wir die Zuverlässigkeit von MySQL mehrere Male überprüft und festgestellt, dass das Phänomen des Hängenbleibens der Verbindung nicht mehr auftrat und das Problem gelöst war.

IV. Zusammenfassung

Dieses Mal habe ich meine Erfahrungen bei der Behebung eines MySQL-Verbindungsdeadlock-Problems geteilt. Aufgrund des enormen Arbeitsaufwands beim Einrichten der Umgebung und der Zufälligkeit bei der Reproduktion des Problems war der gesamte Analyseprozess etwas holprig (und ich bin auch auf einige Fallstricke gestoßen). Tatsächlich lassen wir uns durch manche oberflächlichen Phänomene leicht verwirren und wenn wir das Gefühl haben, ein Problem sei schwer zu lösen, neigen wir eher dazu, das Problem mit voreingenommenem Denken anzugehen. Beispielsweise wurde in diesem Fall allgemein angenommen, dass ein Problem mit dem Verbindungspool vorlag, tatsächlich wurde es jedoch durch eine ungenaue Konfiguration des MySQL JDBC-Treibers (MariaDB-Treiber) verursacht.

Grundsätzlich sollte jedes Verhalten vermieden werden, das zum Hängenbleiben von Ressourcen führen kann. Wenn wir den Code und die zugehörigen Konfigurationen in den frühen Phasen gründlich untersuchen können, glaube ich, dass 996 weiter von uns entfernt sein wird.

Oben finden Sie eine ausführliche Erklärung der Gründe, warum MySQL-Verbindungen hängen bleiben. Weitere Informationen zu den Gründen, warum MySQL-Verbindungen hängen bleiben, finden Sie in den anderen verwandten Artikeln auf 123WORDPRESS.COM!

Das könnte Sie auch interessieren:
  • Lösung für das Problem der fehlenden PID-Datei, nachdem Mysql abstürzt und nicht neu gestartet werden kann
  • Bereitstellung eines MySQL-Hochverfügbarkeitsclusters und Implementierung eines Failovers
  • MySQL-Datenbank implementiert MMM-Hochverfügbarkeitsclusterarchitektur
  • Detaillierte Erläuterung des MySQL-Hochverfügbarkeits-MMM-Konstruktionsplans und der Architekturprinzipien
  • Zusammenfassung der Hochverfügbarkeitslösungen für MySQL-Datenbanken
  • Super-Deployment-Tutorial zur MHA-Hochverfügbarkeits-Failover-Lösung unter MySQL
  • Gemeinsame Nutzung von Installation und Bereitstellung der MySQL-Hochverfügbarkeits-MMM-Lösung
  • Ursachen und Lösungen für MySQL-Deadlocks
  • Analyse eines MySQL-Deadlock-Szenariobeispiels

<<:  Eine einfache Implementierungsmethode für eine digitale LED-Uhr in CSS3

>>:  Schauen wir uns einige leistungsstarke Operatoren in JavaScript an

Artikel empfehlen

Docker verpackt das lokale Image und stellt es auf anderen Maschinen wieder her

1. Verwenden Sie Docker-Images, um alle Image-Dat...

Die Implementierung der Ereignisbindung in React verweist auf drei Methoden

1. Pfeilfunktion 1. Nutzen Sie die Tatsache, dass...

Detaillierte Erklärung der Verwendung des Bash-Befehls

Unter Linux wird Bash als Standard übernommen, wa...

Eine kleine Frage zur Ausführungsreihenfolge von SQL in MySQL

Ich bin heute bei der Arbeit auf ein SQL-Problem ...

So zeigen Sie in CocosCreator eine Textur an der Wischposition an

Inhaltsverzeichnis 1. Projektanforderungen 2. Dok...

Grafisches Tutorial zur Installation und Konfiguration von MySQL 5.7.17

Funktionen von MySQL: MySQL ist ein relationales ...

Erläuterung des Arbeitsmechanismus von Namenode und SecondaryNameNode in Hadoop

1) Prozess 2) FSImage und Bearbeitungen Nodenode ...

Besser aussehende benutzerdefinierte CSS-Stile (Titel h1 h2 h3)

Rendern Häufig verwendete Stile im Blog Garden /*...

Definieren der Mindesthöhe der Inline-Elementspanne

Das Span-Tag wird häufig beim Erstellen von HTML-...

Drei Möglichkeiten, um einen Textblinkeffekt im CSS3-Beispielcode zu erzielen

1. Ändern Sie die Transparenz, um ein allmähliche...

Tipps zur Optimierung von MySQL SQL-Anweisungen

Wenn wir mit einer SQL-Anweisung konfrontiert wer...