Spring+HibernateEntityManager(DBCPのvalidationQuery編)
今回は、Spring+HibernateEntityManager(AspectJ AOP with Load-Time-Weaver編)の続きです。
コネクションタイムアウトのハンドリング忘れてた(^^;
以前、beans.xml に DBCP の設定は行いましたが、コネクションタイムアウト対策を忘れていました。
おいらは DB に MySQL を使用しているのですが、MySQL はデフォルトでは 8 時間でコネクションがタイムアウトするようになっています。そうすると、コネクションプールからコネクションを取得しても、それがタイムアウトしているという悲しいことになります。(´・ω・`)
Connector/J の autoReconnect は、おいら的には非推奨。
Connector/Jのマニュアルには、非推奨と書かれてます。
おいら的にも、めんどくさいことこのうえないので、使わないほうが良いと思う今日この頃。
そもそも、コネクション切れたらリコネクトよりも、切れてないものを使おうよ。
validationQuery 設定なしの場合
7時間時計を進めるテスト。
8時間という数字の妥当性を確認するために、ひとまず7時間時計を進めてテストしてみた。
...問題なくテストは動作しました。
8時間時計を進めるテスト。
こんなエラーが出ました。
Exception in thread "main" javax.persistence.PersistenceException: org.hibernate.exception.JDBCConnectionException: could not insert: [domain.entity.UserImpl] at org.hibernate.ejb.AbstractEntityManagerImpl.throwPersistenceException(AbstractEntityManagerImpl.java:637) at org.hibernate.ejb.AbstractEntityManagerImpl.persist(AbstractEntityManagerImpl.java:226) at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25) at java.lang.reflect.Method.invoke(Method.java:597) at org.springframework.orm.jpa.SharedEntityManagerCreator$SharedEntityManagerInvocationHandler.invoke(SharedEntityManagerCreator.java:194) at $Proxy17.persist(Unknown Source) at domain.service.UserServiceImpl.doRegisterUser(UserServiceImpl.java:45) at domain.service.UserServiceImpl.registerUser(UserServiceImpl.java:22) at application.Main.main(Main.java:40) Caused by: org.hibernate.exception.JDBCConnectionException: could not insert: [domain.entity.UserImpl] at org.hibernate.exception.SQLStateConverter.convert(SQLStateConverter.java:74) at org.hibernate.exception.JDBCExceptionHelper.convert(JDBCExceptionHelper.java:43) at org.hibernate.id.insert.AbstractReturningDelegate.performInsert(AbstractReturningDelegate.java:40) at org.hibernate.persister.entity.AbstractEntityPersister.insert(AbstractEntityPersister.java:2163) at org.hibernate.persister.entity.AbstractEntityPersister.insert(AbstractEntityPersister.java:2643) at org.hibernate.action.EntityIdentityInsertAction.execute(EntityIdentityInsertAction.java:51) at org.hibernate.engine.ActionQueue.execute(ActionQueue.java:279) at org.hibernate.event.def.AbstractSaveEventListener.performSaveOrReplicate(AbstractSaveEventListener.java:298) at org.hibernate.event.def.AbstractSaveEventListener.performSave(AbstractSaveEventListener.java:181) at org.hibernate.event.def.AbstractSaveEventListener.saveWithGeneratedId(AbstractSaveEventListener.java:107) at org.hibernate.ejb.event.EJB3PersistEventListener.saveWithGeneratedId(EJB3PersistEventListener.java:49) at org.hibernate.event.def.DefaultPersistEventListener.entityIsTransient(DefaultPersistEventListener.java:131) at org.hibernate.event.def.DefaultPersistEventListener.onPersist(DefaultPersistEventListener.java:87) at org.hibernate.event.def.DefaultPersistEventListener.onPersist(DefaultPersistEventListener.java:38) at org.hibernate.impl.SessionImpl.firePersist(SessionImpl.java:618) at org.hibernate.impl.SessionImpl.persist(SessionImpl.java:592) at org.hibernate.impl.SessionImpl.persist(SessionImpl.java:596) at org.hibernate.ejb.AbstractEntityManagerImpl.persist(AbstractEntityManagerImpl.java:220) ... 9 more Caused by: com.mysql.jdbc.exceptions.jdbc4.CommunicationsException: Communications link failure Last packet sent to the server was 0 ms ago. at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method) at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:39) at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:27) at java.lang.reflect.Constructor.newInstance(Constructor.java:513) at com.mysql.jdbc.Util.handleNewInstance(Util.java:406) at com.mysql.jdbc.SQLError.createCommunicationsException(SQLError.java:1074) at com.mysql.jdbc.MysqlIO.reuseAndReadPacket(MysqlIO.java:2985) at com.mysql.jdbc.MysqlIO.reuseAndReadPacket(MysqlIO.java:2871) at com.mysql.jdbc.MysqlIO.checkErrorPacket(MysqlIO.java:3414) at com.mysql.jdbc.MysqlIO.sendCommand(MysqlIO.java:1936) at com.mysql.jdbc.MysqlIO.sqlQueryDirect(MysqlIO.java:2060) at com.mysql.jdbc.ConnectionImpl.execSQL(ConnectionImpl.java:2536) at com.mysql.jdbc.ConnectionImpl.setAutoCommit(ConnectionImpl.java:4874) at org.apache.commons.dbcp.DelegatingConnection.setAutoCommit(DelegatingConnection.java:331) at org.apache.commons.dbcp.PoolingDataSource$PoolGuardConnectionWrapper.setAutoCommit(PoolingDataSource.java:317) at org.springframework.jdbc.datasource.LazyConnectionDataSourceProxy$LazyConnectionInvocationHandler.getTargetConnection(LazyConnectionDataSourceProxy.java:405) at org.springframework.jdbc.datasource.LazyConnectionDataSourceProxy$LazyConnectionInvocationHandler.invoke(LazyConnectionDataSourceProxy.java:364) at $Proxy15.prepareStatement(Unknown Source) at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25) at java.lang.reflect.Method.invoke(Method.java:597) at org.hibernate.util.GetGeneratedKeysHelper.prepareStatement(GetGeneratedKeysHelper.java:49) at org.hibernate.jdbc.AbstractBatcher.getPreparedStatement(AbstractBatcher.java:495) at org.hibernate.jdbc.AbstractBatcher.prepareStatement(AbstractBatcher.java:94) at org.hibernate.id.IdentityGenerator$GetGeneratedKeysDelegate.prepare(IdentityGenerator.java:69) at org.hibernate.id.insert.AbstractReturningDelegate.performInsert(AbstractReturningDelegate.java:30) ... 24 more Caused by: java.net.SocketException: Software caused connection abort: recv failed at java.net.SocketInputStream.socketRead0(Native Method) at java.net.SocketInputStream.read(SocketInputStream.java:129) at com.mysql.jdbc.util.ReadAheadInputStream.fill(ReadAheadInputStream.java:113) at com.mysql.jdbc.util.ReadAheadInputStream.readFromUnderlyingStreamIfNecessary(ReadAheadInputStream.java:160) at com.mysql.jdbc.util.ReadAheadInputStream.read(ReadAheadInputStream.java:188) at com.mysql.jdbc.MysqlIO.readFully(MysqlIO.java:2428) at com.mysql.jdbc.MysqlIO.reuseAndReadPacket(MysqlIO.java:2882) ... 44 more
やはり、8時間の監視をしてるっぽいですね。
(めんどいので、今はソースレベルでの確認はしないことにするw)
validationQuery 設定ありの場合
8時間進めても、問題なく動作しました。(さらに1日進めても問題なく動作しました)
以下、SQL ログ。
SELECT 1 SET autocommit=0 insert into User (name) values ('Kumapiyo_0') commit SET autocommit=1
以下、beans.xmlの設定抜粋
<!-- Actual DataSource --> <bean id="rawDataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close"> <property name="driverClassName" value="com.mysql.jdbc.Driver" /> <property name="url" value="jdbc:mysql://localhost:3306/test" /> <property name="username" value="root" /> <property name="password" value="" /> <property name="validationQuery" value="SELECT 1" /> <!-- for optimization <property name="initialSize" value="10" /> <property name="maxIdle" value="10" /> <property name="minIdle" value="10" /> <property name="maxActive" value="20" /> <property name="maxWait" value="100" /> --> <!-- Don't use the following properties because of introducing extra complexity <property name="testWhileIdle" value="true" /> <property name="timeBetweenEvictionRunsMillis" value="60000" /> --> </bean>
性能テスト
validationQuery を用いた場合、DataSource から取得したコネクションを安心して使えるようになる反面、コネクションとるたびに、毎回 "SELECT 1" のようなクエリを吐くというのも、結構性能的にキビシイかもしれないと思う今日この頃。
ということで、validationQuery あり版となし版の性能比較をしてみました。
テスト環境
テスト環境は、以下のような感じです。(スゲーやる気なさそーな環境w)
テストプログラムの抜粋
long startTime = System.currentTimeMillis(); for (int i = 0; i < 10000; i++) { // User を新規作成 User user = userService.newUser(); user.setName("Kumapiyo_" + i); // User を DB に保存。 userService.registerUser(user); } System.out.println("Time : " + (System.currentTimeMillis() - startTime));
validationQuery なし版
SET autocommit=0 insert into User (name) values ('Kumapiyo_0') commit SET autocommit=1
3回実行した結果
Time : 46656 ms
Time : 46344 ms
Time : 47125 ms
-
-
-
-
-
-
-
-
-
-
-
-
-
- -
-
-
-
-
-
-
-
-
-
-
-
-
avg. : 46708 ms
validationQuery あり版
SELECT 1 SET autocommit=0 insert into User (name) values ('Kumapiyo_0') commit SET autocommit=1
3回実行した結果
Time : 52610 ms
Time : 52031 ms
Time : 51297 ms
-
-
-
-
-
-
-
-
-
-
-
-
-
- -
-
-
-
-
-
-
-
-
-
-
-
-
avg. : 51979 ms
考察
ということで、validationQuery で 'SELECT 1' を発行するオーバーヘッドは、上記のサンプルでは約11%です。
この設定を行うことにより、DBが正常に動作しているかつDBの負荷が飽和していない状態である限り、コネクションタイムアウトの例外はほぼ防げるといってよいと思います。
性能的には、11%のオーバーヘッドは結構大きいと考える人が多いかもしれません。しかし、おいら的には、「少し高いハードウェア買えばいいじゃん」と思います。
これしきのオーバーヘッドを嫌って validationQuery を使わないという選択肢をとると、結局、テストフェーズや運用フェーズで、割に合わないコスト(金額や社会的信用等)を支払うことになりますカラ。
同様の理由から、timeBetweenEvictionRunsMillis 等のオプションも、おいら的には推奨しません。効率はよいと思いますが、システムとしての複雑さの増加に見合う価値があるとは考えられません。(と、おいらは思う。)
links
第1回 Spring+HibernateEntityManager(HibernateEntityManager単体編)
第2回 Spring+HibernateEntityManager(とりあえずSpring編)
第3回 Spring+HibernateEntityManager(宣言的トランザクション編)
第4回 Spring+HibernateEntityManager(Spring+DDDっぽく編)
第5回 Spring+HibernateEntityManager(Spring+DDDっぽく編 その2)
第6回 Spring+HibernateEntityManager(Spring+DDDっぽく編 その3)
第7回 Spring+HibernateEntityManager(Spring+DDDっぽく編 その4)
第8回 Spring+HibernateEntityManager(@Transactionalアノテーション編)
第9回 Spring+HibernateEntityManager(@Required編)
第10回 Spring+HibernateEntityManager(XMLからの外部リソース参照編)
第11回 Spring+HibernateEntityManager(AspectJ AOP with Load-Time-Weaver編)
第12回 Spring+HibernateEntityManager(DBCPのvalidationQuery編)
第13回 Spring+HibernateEntityManager(@Resource編)
第14回 Spring+HibernateEntityManager(コンポーネント自動検出 with アノテーション編)
第15回 Spring+HibernateEntityManager(コンポーネント自動検出 without アノテーション編)