web-dev-qa-db-ja.com

Spring / JPA / MySQL / Tomcatアプリでの接続クローズ例外の分析

[〜#〜]問題[〜#〜]

私は最近、コードが既に記述され、配置されているJava Webアプリケーションを担当しています。アプリは適度に高いトラフィックを受信し、毎日午前11時から午後3時の間にトラフィックのピーク時間があります。アプリケーションはSpring、JPA(Hibernate)、MYSQLDB。Springは、Tomcat jdbc接続プールを使用してDBに接続するように構成されています。(構成の詳細は投稿の最後にあります)

過去数日間、アプリケーションのピーク負荷時間中に、Tomcatが要求に応答しなくなったため、アプリケーションがダウンしていました。 Tomcatを複数回再起動する必要があります。

Tomcat catalina.outログを調べてみると、たくさんのことに気づきました。

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

これらはクラッシュの直前に頻繁に表示されます。

これらの例外の前にさらに進んで、接続が閉じられた例外の直前に多くの接続が放棄されていることに気づきました。

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)

これらは、接続が閉じられた例外の直前に頻繁に表示されるようです。そして、これらはログで差し迫った破滅の最初の症状であるようです。

[〜#〜]分析[〜#〜]

ログを調べて、問題の原因となっている可能性のある接続プール構成/ mysql構成があるかどうかを確認しました。本番環境向けのプールの調整を示すいくつかの優れた記事を読みました。リンク 12

これらの記事を見て、私はそれに気づきました:

  1. JHanikの記事(リンク1)の以下の行はこれに言及しています

    AbandonWhenPercentageFullの値を100に設定すると、maxActive制限に達しない限り、接続が放棄されたと見なされないことを意味します。

    多くの接続が放棄されているのを見ると、これは私の場合は重要かもしれないと思いました。

  2. Max_connections設定が推奨されているものと一致しません(リンク2)

    mysqlmax_connectionsはmax_active + max_idleと等しくなければなりません

私が試したこと

それで、記事からの推薦に従って、私は次の2つのことをしました:

  1. AbandonWhenPercentageFullを100に変更しました
  2. 私のMYSQLサーバーでは、max_connectionsは500に設定されていました。600に増やしました。接続プールの設定では、max_activeは200、max_idleは50でした。max_active= 350、max_idle = 250に変更しました。

これDID役に立たない

翌日、ピーク時に次の観測が行われました。

  1. Tomcatはダウンしませんでした。アプリはピーク時間中ずっと稼働し続けました。しかし、パフォーマンスは悪化し、実際には低下しなかったにもかかわらず、アプリはほとんど使用できなくなりました。
  2. DB接続プールはサイズが大きくなりましたが、完全に利用され、DBへ​​の350のアクティブな接続を一度に確認できました。

最後に、私の質問:

アプリサーバーからのDB接続の方法に問題があるようです。したがって、この分析を進めるには2つの方向性があります。

私の質問は、これらのうちどれを取るべきかということです。

1。問題は接続プールの設定ではありません。コードが問題の原因です

コード内にDB接続が閉じられていない場所がある可能性があります。これにより、多数の接続が開かれています。

このコードは、すべてのDaoクラスで拡張されたGenericDaoを使用しています。 GenericDaoは、SpringのJpaTemplateを使用してEntityManagerインスタンスをフェッチし、これがすべてのDB操作に使用されます。私の理解では、JpaTemplateを使用して、DB接続を内部的に閉じるという本質的な問題を処理します。

では、接続リークの可能性を正確にどこで探す必要がありますか?

2。問題は、接続プール/ mysql構成パラメーターにあります。ただし、私が行った最適化はさらに調整する必要があります

はいの場合、どのパラメータを確認する必要がありますか?接続プールのより適切な値を決定するために使用するデータを収集する必要があります。 (たとえば、max_active、max_idle、max_connectionsの場合)


補遺:完全な接続プール構成

   <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

これはOPにとってひどく遅いですが、将来誰かを助けるかもしれません:

長時間実行されるバッチジョブのある実稼働環境で、これに似たものに遭遇しました。問題は、コードがプロパティで指定された時間よりも長い接続を必要とする場合です。

name="removeAbandonedTimeout" value="60

有効にしたもの:

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

その後、60秒後の処理中に切断されます。考えられる回避策の1つ(私にはうまくいきませんでした)は、インターセプターを有効にすることです。

jdbcInterceptors="ResetAbandonedTimer"

これにより、読み取り/書き込みが発生するたびに、その接続の放棄されたタイマーがリセットされます。残念ながら、私の場合、データベースに対して何かが読み取られたり書き込まれたりする前に、処理にタイムアウトよりも時間がかかることがありました。そのため、タイムアウトの長さを増やすか、removeAbandondedを無効にする必要がありました(前者のソリューションを選択しました)。

他の誰かが似たようなものに遭遇した場合、これが他の誰かに役立つことを願っています!

10
Ryan P.

最近、本番システムがダウンすることがある理由を調査するように依頼されました。上記で概説したように、JVM Tomcatアプリを取得するイベントとJDBCの問題との相関関係が含まれているため、調査結果を共有したいと思いました。これはバックエンドとしてmysqlを使用しているため、おそらくこのシナリオで最も役立ちますが、別のプラットフォームで問題が発生した場合は、同じである可能性があります。

単に接続を閉じるだけでは、アプリケーションが壊れていることを意味するわけではありません

これはgrailsアプリケーションの下にありますが、すべてのJVM関連アプリに関連しています。

Tomcat/context.xml db構成、非常に小さいdbプールとremoveAbandonedTimeout="10"そうだね私たちは物事を壊したい

<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" />

毎分実行されるクォーツジョブ。最初の試行で死ぬと思うアプリは重要ではありません。

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

    }

}

コメント付きのtestServiceになりましたので、コメントに従ってください。

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++
      }
  }

クエリに予想よりも長い時間がかかる場合にのみ、別の要素が必要になります。クエリ自体は、強制終了されたとしても、MYSQLに座る必要があります。 MYSQLはそれを処理して食い尽くしています。

何が起こっているのだと思います

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 -> 

この時点でjob1が完了していない場合は、プールウェルアプリに何も残っていません。アプリは単に壊れています。jdbcエラーがスローされるなど。クラッシュ後に完了してもかまいません。

何が起こっているかを監視することができます mysqlをチェック この値が実行する必要があると彼らが示唆したことに反して、より長い期間実行されているように見えましたが、おそらくこれは実際には何にも基づいていませんこれのそして他の場所の問題に関連しています。

テストでは、2つの状態があることに気づきました。データの送信/クライアントへの送信:

|  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 |

数秒後:

|  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             |

プロセスリストを監視するためにスクリプトを作成する必要がある場合があります。また、実行中の正確なクエリを含むより深い結果セットを作成して、どのクエリイベントがアプリを強制終了しているかを特定する必要がある場合もあります。

1
Vahid

このコードは、すべてのDaoクラスで拡張されたGenericDaoを使用しています。 GenericDaoは、SpringのJpaTemplateを使用してEntityManagerインスタンスをフェッチし、これがすべてのDB操作に使用されます。私の理解では、JpaTemplateを使用して、DB接続を内部で閉じるという本質的な問題を処理します。

これがおそらく問題の原因です。JpaTemplateを使用してEntityManagerを取得しないでください。これにより、管理されていないEntitymanagerが生成されます。実際、JpaTemplateを使用するべきではありません。

プレーンなEntityManager AP​​Iに基づいてdaosを記述し、通常どおりにEntityManagerを挿入することをお勧めします(@PersistenceContext)。

本当にJpaTemplateを使用したい場合は、executeメソッドを使用し、JpaCallbackを渡すと、マネージドEntityManagerが得られます。

また、Springは接続を閉じる必要があることを認識していないため、適切なtxセットアップ接続がない場合はセットアップトランザクションが正しく行われていることを確認してください。

0
M. Deinum