web-dev-qa-db-fra.com

Analyse d'une exception de connexion fermée dans l'application Spring/JPA/Mysql/Tomcat

PROBLÈME

J'ai récemment été chargé d'une application Web Java avec du code déjà écrit et en place. L'application reçoit un trafic modérément élevé et a des heures de pointe entre 11h00 et 15h00 tous les jours. L'application utilise Spring, JPA (Hibernate), MYSQL DB. Spring a été configuré pour utiliser le pool de connexions Tomcat jdbc pour établir des connexions avec la base de données. (Détails de la configuration à la fin du message)

Au cours des derniers jours, pendant les heures de pointe de l'application, celle-ci a cessé de fonctionner car Tomcat ne répondait plus aux demandes. Tomcat a dû être redémarré plusieurs fois.

En parcourant les journaux Tomcat catalina.out, j’ai remarqué beaucoup de 

Caused by: Java.sql.SQLException: Connection has already been closed.
    at org.Apache.Tomcat.jdbc.pool.ProxyConnection.invoke(ProxyConnection.Java:117)
    at org.Apache.Tomcat.jdbc.pool.JdbcInterceptor.invoke(JdbcInterceptor.Java:109)
    at org.Apache.Tomcat.jdbc.pool.DisposableConnectionFacade.invoke(DisposableConnectionFacade.Java:80)
    at com.Sun.proxy.$Proxy28.prepareStatement(Unknown Source)
    at org.hibernate.jdbc.AbstractBatcher.getPreparedStatement(AbstractBatcher.Java:505)
    at org.hibernate.jdbc.AbstractBatcher.getPreparedStatement(AbstractBatcher.Java:423)
    at org.hibernate.jdbc.AbstractBatcher.prepareQueryStatement(AbstractBatcher.Java:139)
    at org.hibernate.loader.Loader.prepareQueryStatement(Loader.Java:1547)
    at org.hibernate.loader.Loader.doQuery(Loader.Java:673)
    at org.hibernate.loader.Loader.doQueryAndInitializeNonLazyCollections(Loader.Java:236)
    at org.hibernate.loader.Loader.loadCollection(Loader.Java:1994)
    ... 115 more

Ceux-ci apparaissent fréquemment juste avant le crash.

En allant plus loin avant ces exceptions, j'ai remarqué que de nombreuses connexions étaient abandonnées juste avant les exceptions Connexion fermée.

WARNING: Connection has been abandoned PooledConnection[com.mysql.jdbc.Connection@543c2ab5]:Java.lang.Exception
    at org.Apache.Tomcat.jdbc.pool.ConnectionPool.getThreadDump(ConnectionPool.Java:1065)
    at org.Apache.Tomcat.jdbc.pool.ConnectionPool.borrowConnection(ConnectionPool.Java:782)
    at org.Apache.Tomcat.jdbc.pool.ConnectionPool.borrowConnection(ConnectionPool.Java:618)
    at org.Apache.Tomcat.jdbc.pool.ConnectionPool.getConnection(ConnectionPool.Java:188)
    at org.Apache.Tomcat.jdbc.pool.DataSourceProxy.getConnection(DataSourceProxy.Java:128)
    at org.hibernate.ejb.connection.InjectedDataSourceConnectionProvider.getConnection(InjectedDataSourceConnectionProvider.Java:47)
    at org.hibernate.jdbc.ConnectionManager.openConnection(ConnectionManager.Java:423)
    at org.hibernate.jdbc.ConnectionManager.getConnection(ConnectionManager.Java:144)
    at org.hibernate.jdbc.AbstractBatcher.prepareQueryStatement(AbstractBatcher.Java:139)

Celles-ci semblent apparaître fréquemment juste avant les exceptions Connection Closed. Et ceux-ci semblent être les premiers symptômes de Doom imminent dans les journaux. 

ANALYSE

En parcourant les journaux, j’ai cherché à savoir s’il existait une configuration de pool de connexions/configuration mysql pouvant être à l’origine du problème. Nous avons passé en revue quelques excellents articles qui montrent le réglage du pool pour l’environnement de production. Liens 1 & 2

En parcourant ces articles, j'ai remarqué que:

  1. La ligne ci-dessous dans l'article de JHanik (lien 1) mentionne cette

    Définir la valeur de abandonWhenPercentageFull à 100 signifierait que les connexions ne sont> pas considérées comme abandonnées sauf si nous avons atteint notre limite maxActive. 

    J'ai pensé que cela pourrait être important dans mon cas, car je vois beaucoup de connexions abandonnées.

  2. Mon paramètre max_connections ne correspond pas à ce qui est recommandé (dans le lien 2)

    mysql max_connections doit être égal à max_active + max_idle

CE QUE J'AI ESSAYÉ

Donc, conformément aux recommandations des articles, j'ai fait les deux choses suivantes:

  1. A changé d'abandon quand le pourcentage est rempli à 100
  2. Dans mon serveur MySQL, max_connections était défini sur 500. Augmenté à 600 Dans mes paramètres de pool de connexions, max_active était à 200 et max_idle à 50. Modifié à max_active = 350, max_idle = 250

CE DID NOT HELP

Le lendemain, les observations suivantes ont été faites aux heures de pointe:

  1. Tomcat n'est pas descendu. L'application est restée active pendant les heures de pointe. Cependant, les performances ont empiré et l’application était à peine utilisable, même si elle n’a pas vraiment baissé. 
  2. Le pool de connexions à la base de données, bien que de taille accrue, a été complètement utilisé et j'ai pu voir 350 connexions actives vers la base de données à un moment donné.

FINALEMENT, MA QUESTION:

Il semble clairement y avoir des problèmes avec la façon dont les connexions à la base de données sont établies à partir du serveur d'applications. J'ai donc deux directions pour faire avancer cette analyse. 

Ma question est laquelle de ces devrais-je prendre?

1. Le problème ne concerne pas les paramètres du pool de connexions. Le code est ce qui cause le problème

Il peut y avoir des endroits dans le code où les connexions à la base de données ne sont pas fermées. Ce qui cause le nombre élevé de connexions ouvertes.

Le code utilise un GenericDao qui est étendu à toutes les classes de Dao. GenericDao utilise JpaTemplate de Spring pour extraire une instance EntityManager, qui est utilisée pour toutes les opérations de base de données. D'après ce que je comprends, l'utilisation de JpaTemplate permet de bien comprendre la fermeture interne des connexions à la base de données. 

Alors, où devrais-je chercher des fuites de connexion?

2. Le problème concerne les paramètres de configuration pool/mysql de connexions. Cependant, les optimisations que j'ai apportées doivent encore être ajustées

Si oui, quels paramètres devrais-je examiner? Devrais-je collecter des données à utiliser pour déterminer des valeurs plus appropriées pour mon pool de connexions? (Par exemple, pour max_active, max_idle, max_connections)


Addendum: configuration complète du pool de connexions}

   <bean id="dataSource" class="org.Apache.Tomcat.jdbc.pool.DataSource" destroy-method="close">
        <property name="driverClassName" value="com.mysql.jdbc.Driver" />
        <property name="url" value="jdbc:mysql://xx.xx.xx.xx" />
        <property name="username" value="xxxx" />
        <property name="password" value="xxxx" />
        <property name="initialSize" value="10" />
        <property name="maxActive" value="350" />
        <property name="maxIdle" value="250" />
        <property name="minIdle" value="90" />
        <property name="timeBetweenEvictionRunsMillis" value="30000" />
        <property name="removeAbandoned" value="true" />
        <property name="removeAbandonedTimeout" value="60" />
        <property name="abandonWhenPercentageFull" value="100" />
        <property name="testOnBorrow" value="true" />
        <property name="validationQuery" value="SELECT 1" />
        <property name="validationInterval" value="30000" />
        <property name="logAbandoned" value="true" />
        <property name="jmxEnabled" value="true" />
    </bean>
12
ncmadhan

C'est tristement en retard pour le PO, mais peut-être que ça aidera quelqu'un d'autre à l'avenir:

J'ai rencontré quelque chose de similaire dans un environnement de production avec des tâches par lots de longue durée. Le problème est que votre code nécessite une connexion plus longue que l'heure spécifiée par la propriété:

name="removeAbandonedTimeout" value="60

et vous avez activé:

<property name="removeAbandoned" value="true" />

il sera alors déconnecté pendant le traitement après 60 secondes. Une solution de contournement possible (qui n'a pas fonctionné pour moi) consiste à activer l'intercepteur:

jdbcInterceptors="ResetAbandonedTimer"

Cela réinitialisera le minuteur abandonné pour cette connexion pour chaque lecture/écriture qui se produit. Malheureusement, dans mon cas, le traitement prend parfois encore plus longtemps que le délai d'attente avant que quoi que ce soit soit lu/écrit dans la base de données. J'ai donc été obligé de modifier la durée de temporisation ou de désactiver removeAbandonded (j'ai choisi l'ancienne solution).

J'espère que cela aidera quelqu'un d'autre s'il rencontre quelque chose de similaire!

10
Ryan P.

On m'a récemment demandé de rechercher pourquoi le système de production était parfois en panne. Je souhaitais partager mes conclusions, car il implique une corrélation d'événements pour prendre une application JVM Tomcat avec les problèmes JDBC décrits ci-dessus afin de provoquer le blocage de l'application. Ceci utilise mysql en tant que back-end, donc probablement le plus utile pour ce scénario, mais si le problème est rencontré sur une autre plate-forme, la cause sera probablement la même.

Le simple fait de fermer la connexion n'implique pas que l'application est cassée

Ceci est sous une application grails mais sera relatif à toutes les applications liées à JVM:

Tomcat/context.xml configuration de la base de données, remarquez un très petit pool de bases de données et removeAbandonedTimeout="10" vous avez raison, nous voulons que les choses se cassent

<Resource
 name="jdbc/TestDB"  auth="Container" type="javax.sql.DataSource"
              driverClassName="com.mysql.jdbc.Driver"
              url="jdbc:mysql://127.0.0.1:3306/test"
              username="XXXX"
              password="XXXX"
              testOnBorrow="true"
              testWhileIdle="true"
              testOnReturn="true"
              factory="org.Apache.Tomcat.jdbc.pool.DataSourceFactory"
              removeAbandoned="true"
              logAbandoned="true"
              removeAbandonedTimeout="10"
              maxWait="5000"
              initialSize="1"
              maxActive="2"
              maxIdle="2"
              minIdle="2"
              validationQuery="Select 1" />

Un travail de quartz qui s'exécute à la minute près, mais qui importe peu, je pense que l'application meurt à la première tentative:

class Test2Job {
    static  triggers = {
               cron name: 'test2', cronExpression: "0 0/1 * * * ?"
        }
        def testerService
        def execute() {
        println "starting job2 ${new Date()}"
        testerService.basicTest3()

    }

}

Maintenant notre testService avec des commentaires donc s'il vous plaît suivre les commentaires:

def dataSource

  /**
   * When using this method in quartz all the jdbc settings appear to get ignored
   * the job actually completes notice huge sleep times compared to basicTest
   * strange and very different behaviour.
   * If I add Tester t = Tester.get(1L) and then execute below query I will get
   * connection pool closed error
   * @return
   */
  def basicTest2() {
      int i=1
      while (i<21) {
          def sql = new Sql(dataSource)
          def query="""select id as id  from tester t
                  where id=:id"""
          def instanceList = sql.rows(query,[id:i as Long],[timeout:90])
          sleep(11000)
          println "-- working on ${i}"
          def sql1 = new Sql(dataSource)
          sql1.executeUpdate(
                  "update tester t set t.name=? where t.id=?",
                  ['aa '+i.toString()+' aa', i as Long])

          i++
          sleep(11000)
      }
      println "run ${i} completed"
  }


  /**
   * This is described in above oddity
   * so if this method is called instead you will see connection closed issues
   */
  def basicTest3() {
      int i=1
      while (i<21) {
          def t = Tester.get(i)
          println "--->>>> test3 t ${t.id}"

          /**
           * APP CRASHER - This is vital and most important
           * Without this declared lots of closed connections and app is working
           * absolutely fine,
           * The test was originally based on execRun() which returns 6650 records or something
           * This test query is returned in time and does not appear to crash app
           *
           * The moment this method is called and please check what it is currently doing. It is simply
           * running a huge query which go beyond the time out values and as explained in previous emails MYSQL states
           *
           * The app is then non responsive and logs clearly show application is broke 
           */
          execRun2()


          def sql1 = new Sql(dataSource)
          sleep(10000)
          sql1.executeUpdate("update tester t set t.name=? where t.id=?",['aa '+i.toString()+' aa', t.id])
          sleep(10000)
          i++
      }

  }


  def execRun2() {
      def query="""select new map (t as tester) from Tester t left join t.children c
left join t.children c
                  left join c.childrena childrena
                  left join childrena.childrenb childrenb
                  left join childrenb.childrenc childrenc , Tester t2 left join t2.children c2 left join t2.children c2
                  left join c2.childrena children2a
                  left join children2a.childrenb children2b
                  left join children2b.childrenc children2c
             where ((c.name like (:name) or
                  childrena.name like (:name) or
                  childrenb.name like (:name) or (childrenc is null or childrenc.name like (:name))) or
                  (
                  c2.name like (:name) or
                  children2a.name like (:name) or
                  children2b.name like (:name) or (children2c is null or children2c.name like (:name))
      ))

          """
      //println "query $query"
      def results = Tester.executeQuery(query,[name:'aa'+'%'],[timeout:90])
      println "Records: ${results.size()}"

      return results
  }


  /**
   * This is no different to basicTest2 and yet
   * this throws a connection closed error and notice it is 20 not 20000
   * quite instantly a connection closed error is thrown when a .get is used vs
   * sql = new Sql(..) is a manuall connection
   *
   */
  def basicTest() {
      int i=1
      while (i<21) {
          def t = Tester.get(i)
          println "--- t ${t.id}"
          sleep(20)
          //println "publishing event ${event}"
          //new Thread({
          //    def event=new PurchaseOrderPaymentEvent(t,t.id)
          //    publishEvent(event)
          //} as Runnable ).start()

          i++
      }
  }

Ce n'est que lorsque la requête prend alors plus de temps que prévu mais qu'il doit y avoir un autre élément, la requête elle-même doit alors s'asseoir sur MYSQL même si elle est tuée. MYSQL le ronge pour le traiter. 

Je pense que ce qui se passe est 

job 1 - hits app -> hits mysql ->    (9/10 left)
         {timeout} -> app killed  -> mysql running (9/10)
 job 2 - hits app -> hits mysql ->    (8/10 left)
         {timeout} -> app killed  -> mysql running (8/10) 
.....
 job 10 - hits app -> hits mysql ->    (10/10 left)
         {timeout} -> app killed  -> mysql running (10/10)
 job 11 - hits app -> 

Si à ce moment-là, job1 n'est pas encore terminé, il ne nous reste plus rien dans la piscine, mais l'application est tout simplement en panne. Les erreurs jdbc sont survenues, etc. Peu importe si l'opération se termine après le crash.

Vous pouvez surveiller ce qui se passe via vérifiant mysql Il semble que sa durée de vie est longue, ce qui va à l'encontre de ce qu'ils ont suggéré de faire de cette valeur, mais peut-être que ce n'est pas le cas vraiment basé sur tout cela et concerne un problème ailleurs.

Lors des tests, il y avait deux états: Envoi de données/Envoi au client:

|  92 | root | localhost:58462 | test | Query   |   80 | Sending data      | select tester0_.id as col_0_0_ from tester tester0_ left outer join tester_childa children1_ on test |
|  95 | root | localhost:58468 | test | Query   |  207 | Sending to client | select tester0_.id as col_0_0_ from tester tester0_ left outer join tester_childa children1_ on test |
|  96 | root | localhost:58470 | test | Query   |  147 | Sending data      | select tester0_.id as col_0_0_ from tester tester0_ left outer join tester_childa children1_ on test |
|  97 | root | localhost:58472 | test | Query   |  267 | Sending data      | select tester0_.id as col_0_0_ from tester tester0_ left outer join tester_childa children1_ on test |
|  98 | root | localhost:58474 | test | Sleep   |   18 |                   | NULL                                                                                                 |
|  99 | root | localhost:58476 | test | Query   |  384 | Sending to client | select tester0_.id as col_0_0_ from tester tester0_ left outer join tester_childa children1_ on test |
| 100 | root | localhost:58478 | test | Query   |  327 | Sending data      | select tester0_.id as col_0_0_ from tester tester0_ left outer join tester_childa children1_ on test |

Sseconds plus tard:

|  91 | root | localhost:58460 | test | Query   |   67 | Sending to client | select tester0_.id as col_0_0_ from tester tester0_ left outer join tester_childa children1_ on test |
|  92 | root | localhost:58462 | test | Query   |  148 | Sending to client | select tester0_.id as col_0_0_ from tester tester0_ left outer join tester_childa children1_ on test |
|  97 | root | localhost:58472 | test | Query   |  335 | Sending to client | select tester0_.id as col_0_0_ from tester tester0_ left outer join tester_childa children1_ on test | |
| 100 | root | localhost:58478 | test | Query   |  395 | Sending to client | select tester0_.id as col_0_0_ from tester tester0_ left outer join tester_childa children1_ on test |

Seconds after that: (all dead)
|  58 | root | localhost       | NULL | Query   |    0 | starting | show processlist |
|  93 | root | localhost:58464 | test | Sleep   |  167 |          | NULL             |
|  94 | root | localhost:58466 | test | Sleep   |  238 |          | NULL             |
|  98 | root | localhost:58474 | test | Sleep   |   74 |          | NULL             |
| 101 | root | localhost:58498 | test | Sleep   |   52 |          | NULL             |

Il se peut qu'un script ait besoin d'être créé pour surveiller la liste de processus et peut-être un ensemble de résultats plus profond contenant des requêtes exactes en cours d'exécution pour déterminer lequel de vos événements de requêtes tue votre application.

1
Vahid

Le code utilise un GenericDao qui est étendu à toutes les classes de Dao. GenericDao utilise JpaTemplate de Spring pour extraire une instance EntityManager, qui est utilisée pour toutes les opérations de base de données. D'après ce que je comprends, l'utilisation de JpaTemplate permet de bien comprendre la fermeture interne des connexions à la base de données.

Ceci est probablement la racine de votre problème, vous ne devriez pas utiliser la JpaTemplate pour obtenir la EntityManager cela vous donnera un non géré Entitymanager. En fait, vous ne devriez pas utiliser JpaTemplate du tout. 

Il est recommandé d'écrire des daos basés sur l'API plain EntityManager et d'injecter simplement la EntityManager comme vous le feriez normalement (avec @PersistenceContext). 

Si vous voulez vraiment utiliser la variable JpaTemplate, utilisez la méthode execute et transmettez-lui une JpaCallback qui vous donnera une EntityManager gérée.

Assurez-vous également que les transactions de configuration sont correctes sans que les connexions de configuration de tx appropriées ne soient pas fermées car spring ne sait pas qu'elle doit fermer la connexion.

0
M. Deinum