本記事の目的
JBossでデータソースを構成する手順とサンプルコード、データベース接続検証関連パラメータの製品挙動についてまとめている。
Tomcat編についてはこちらを参照ください。
Tomcat DBCP POC
背景
とあるシステムでRDSインスタンス障害時にAPサーバーを再起動していた。
ミッションクリティカルなシステムではAP再起動はアプリ全体の停止につながるため、対応方法としてはありえない。影響範囲を極小化するためにせめてコネクションプールのリフレッシュだけにできないか?と思ったのがこの記事のモチベーション。
検証環境
- JBoss EAP 8.0.0.GA
- OpenJDK 17.0.6
- Red Hat Enterprise Linux release 9.3 (Plow)
- Aurora PostgreSQL 15.4 db.t3.medium
JBossの設定
/opt/jboss-eap-8.0/standalone/configuration/standalone.xmlの設定内容
<subsystem xmlns="urn:jboss:domain:datasources:7.0">
<datasources>
<datasource jndi-name="java:jboss/datasources/ExampleDS" pool-name="ExampleDS"
・・・省略・・・
</datasource>
<datasource jndi-name="java:/PostgresDS" pool-name="PostgresDS" statistics-enabled="true">
<connection-url>jdbc:postgresql://aurora-cluster.cluster-xxxxxxx.ap-northeast-1.rds.amazonaws.com:5432/test</connection-url>
<driver>postgres</driver>
<pool>
<min-pool-size>50</min-pool-size>
<initial-pool-size>50</initial-pool-size>
<max-pool-size>50</max-pool-size>
<fair>false</fair>
<flush-strategy>FailingConnectionOnly</flush-strategy>
</pool>
<security>
<user-name>postgres</user-name>
<password>postgres</password>
</security>
<validation>
<check-valid-connection-sql>SELECT 1</check-valid-connection-sql>
<validate-on-match>true</validate-on-match>
<background-validation>false</background-validation>
<background-validation-millis>1000</background-validation-millis>
<use-fast-fail>false</use-fast-fail>
</validation>
</datasource>
<drivers>
<driver name="h2" module="com.h2database.h2">
<xa-datasource-class>org.h2.jdbcx.JdbcDataSource</xa-datasource-class>
</driver>
<driver name="postgres" module="org.postgres">
<driver-class>org.postgresql.Driver</driver-class>
<xa-datasource-class>org.postgresql.xa.PGXADataSource</xa-datasource-class>
</driver>
</drivers>
</datasources>
</subsystem>
- check-valid-connection-sqlで接続検証用のSQLを設定する
- validate-on-matchがtrueに設定されている場合は、コネクションオブジェクトが、コネクションプールからチェックアウトされるたびに検証される。SQLを実行する都度、確実に事前検証がなされる
- background-validationをfalseにしているのはvalidate-on-matchとの併用ができないため。JBossのドキュメントに併用できないことは制約として明記されている。Tomcatだと両方のメカニズムを同時利用できるだけに残念な仕様。尚、background-validationのほうはバックグラウンドでの定期検証処理となるため。検証されるまでの間にアプリケーションが無効なコネクションオブジェクトをつかんでしまうとアプリケーションエラーとなる。DB障害時のアプリケーションへの影響を最小化する観点ではvalidation-on-matchのみ設定するのがよい
- use-fast-failをfalseにするとDB障害直後のDBアクセスで全ての無効なコネクションオブジェクトが破棄される挙動となる。trueにすると破棄されるオブジェクトはSQL実行の都度1つのみ。プール上の無効オブジェクトが仮に50だとすると50回SQLが実行されるまで無効オブジェクトはゼロにはならない。今回の検証では50の無効オブジェクトを破棄するのに約700msec必要とした。プール数が1000を超えてアプリケーションのレスポンス要件が厳しい場合のみuse-fast-fail=trueを検討するでよい
因みに、接続検証によって無効オブジェクトが破棄される場合にはJBossはエラーではなくワーニング扱いとなっている。DBエラーや復帰中のアプリケーションエラーはエラー監視でひっかけられるので特にこのワーニングにたいする監視設計上の考慮は不要
WARN [org.jboss.jca.core.connectionmanager.pool.strategy.OnePool] (default task-1) IJ000621: Destroying connection that could not be validated: org.jboss.jca.core.connectionmanager.listener.TxConnectionListener@5b4cef66[state=NORMAL managed connection=org.jboss.jca.adapters.jdbc.local.LocalManagedConnection@7549fc8 connection handles=0 lastReturned=1719919623095 lastValidated=1719919623095 lastCheckedOut=1719919623095 trackByTx=false pool=org.jboss.jca.core.connectionmanager.pool.strategy.OnePool@1d07889 mcp=SemaphoreConcurrentLinkedQueueManagedConnectionPool@613ec535[pool=PostgresDS] xaResource=LocalXAResourceImpl@1786af36[connectionListener=5b4cef66 connectionManager=340fc8d1 warned=false currentXid=null productName=PostgreSQL productVersion=16.3 jndiName=java:/PostgresDS] txSync=null]
JDBCドライバの設定
/opt/jboss-eap-8.0/modules/org/postgres/main配下のファイル
- module.xml
- postgresql-42.7.3.jar
module.xmlの内容
<?xml version="1.0" ?>
<module xmlns="urn:jboss:module:1.1" name="org.postgres">
<resources>
<resource-root path="postgresql-42.7.3.jar"/>
</resources>
<dependencies>
<module name="javax.api"/>
<module name="javax.transaction.api"/>
</dependencies>
</module>
コネクションプールの状態参照方法
今回のPOCではJBoss cliを利用した。下記はその実行例。CreatedCountやDestrypedCountを参照することでJBossの挙動がつかめる。
[standalone@localhost:9990 statistics=pool] pwd
/subsystem=datasources/data-source=PostgresDS/statistics=pool
[standalone@localhost:9990 statistics=pool] ls
ActiveCount=50 IdleCount=50 TotalBlockingTime=1 XAEndAverageTime=0 XAPrepareMaxTime=0 XAStartAverageTime=0
AvailableCount=50 InUseCount=0 TotalCreationTime=13635 XAEndCount=0 XAPrepareTotalTime=0 XAStartCount=0
AverageBlockingTime=1 MaxCreationTime=907 TotalGetTime=0 XAEndMaxTime=0 XARecoverAverageTime=0 XAStartMaxTime=0
AverageCreationTime=136 MaxGetTime=0 TotalPoolTime=134788636 XAEndTotalTime=0 XARecoverCount=0 XAStartTotalTime=0
AverageGetTime=0 MaxPoolTime=2699042 TotalUsageTime=0 XAForgetAverageTime=0 XARecoverMaxTime=0 statistics-enabled=true
AveragePoolTime=2695772 MaxUsageTime=0 WaitCount=0 XAForgetCount=0 XARecoverTotalTime=0
AverageUsageTime=0 MaxUsedCount=1 XACommitAverageTime=0 XAForgetMaxTime=0 XARollbackAverageTime=0
BlockingFailureCount=0 MaxWaitCount=0 XACommitCount=0 XAForgetTotalTime=0 XARollbackCount=0
CreatedCount=100 MaxWaitTime=1 XACommitMaxTime=0 XAPrepareAverageTime=0 XARollbackMaxTime=0
DestroyedCount=50 TimedOut=50 XACommitTotalTime=0 XAPrepareCount=0 XARollbackTotalTime=0
コネクションプールの強制リフレッシュ(フラッシュ)方法
何らかトラブルの際に、アプリケーションのSQL実行を待たずしてコマンド実行でフラッシュする方法。下記のようにJBoss cliを利用する。
この機能を利用するトラブルの例:アプリケーションの特定機能で性能問題が発生し、コネクションオブジェクトがロングトランザクションに占有されてしまって、他のトランザクションが実行できない状態に陥ったときにその状況を解消する目的。
/subsystem=datasources/data-source=PostgresDS:flush-all-connection-in-pool
即時リフレッシュする挙動となる。リフレッシュ直後のアプリケーションエラーはない。コネクションオブジェクト取得も通常通りのレスポンスでレスポンス劣化なし。自明の結果ではあるが、アプリケーションのSQL実行後、commit前に実行するとアプリケーションはエラーとなった。
/subsystem=datasources/data-source=PostgresDS:flush-gracefully-connection-in-pool
上記(flush-all-connection-in-pool)との違いは、commit前のトランザクションがすべて処理完了するのを待ってリフレッシュする挙動となる。仕掛中のトランザクションを救いたい時のオプションだが実際のユースケースは不明。もしかするとOracle RAC利用時に狙った効果を得ることができるかもしれない。が、RACで高度な設計を狙うならJBossではなく、WebLogic(マルチデータソース)を組み合わせるのがよい。
アプリケーションのコード(DB参照)
PostgreSQLでは接続インスタンスのIPアドレスを取得する手段があるためTomcat(Aurora MySQL)のPOCで利用したコードを一部変更している。後述するAuroraクラスタのフェイルオーバーの挙動をみる目的。
- 変更ポイント
実行するSQLをselect inet_server_addr()にした。
package test;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import javax.sql.DataSource;
/**
* Servlet implementation class TestServlet
*/
public class TestServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
/**
* @see HttpServlet#HttpServlet()
*/
public TestServlet() {
super();
// TODO Auto-generated constructor stub
}
/**
* @see HttpServlet#doGet(HttpServletRequest request, HttpServletResponse response)
*/
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
Context initContext;
try {
String sql = "select inet_server_addr()"; // PostgreSQL return IP address.
initContext = new InitialContext();
long start = System.nanoTime();
Context envContext = new InitialContext();
DataSource ds = (DataSource)envContext.lookup("java:/PostgresDS");
Connection conn = ds.getConnection();
long end = System.nanoTime();
System.out.println((end - start)/1000);
String result = executeQuery(conn, sql, 1);
conn.close();
PrintWriter out = response.getWriter();
out.println(result);
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* @see HttpServlet#doPost(HttpServletRequest request, HttpServletResponse response)
*/
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
doGet(request, response);
}
private String executeQuery(Connection conn, String sql) {
String result = "";
Statement stmt;
try {
stmt = conn.createStatement();
ResultSet rset = stmt.executeQuery(sql);
while (rset.next()) {
result = result + rset.getString(1);
}
rset.close();
stmt.close();
} catch (SQLException e) {
e.printStackTrace();
}
System.out.println("IP Address:" + result);
return result;
}
private String executeQuery(Connection conn, String sql, int count) {
String result = "";
for(int i = 0; i < count; i++) {
result = result + executeQuery(conn, sql);
}
return result;
}
}
アプリケーションのコード(更新トランザクション)
アプリケーションの処理途中でDB障害やコネクションプールのリフレッシュを実行する目的
※標準出力を見ながら障害発生させたいところで障害発生コマンドを実行するために適宜sleepを埋め込んでいる。
package test;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.sql.DataSource;
/**
* Servlet implementation class TestServlet
*/
public class LongTransactionServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
/**
* @see HttpServlet#HttpServlet()
*/
public LongTransactionServlet() {
super();
}
/**
* @see HttpServlet#doGet(HttpServletRequest request, HttpServletResponse response)
*/
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
Context initContext;
try {
String now = String.valueOf(System.currentTimeMillis());
System.out.println(now);
String sql = "insert into test values('" + now + "')";
initContext = new InitialContext();
Context envContext = new InitialContext();
DataSource ds = (DataSource)envContext.lookup("java:/PostgresDS");
Connection conn = ds.getConnection();
conn.setAutoCommit(false);
executeQuery(conn, sql, 1);
System.out.println("sleep start.");
Thread.sleep(10000);
System.out.println("sleep end.");
System.out.println("before commit().");
Thread.sleep(10000);
conn.commit();
System.out.println("commit() end.");
System.out.println("before close().");
Thread.sleep(10000);
conn.close();
System.out.println("close() end.");
PrintWriter out = response.getWriter();
out.println("success.");
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* @see HttpServlet#doPost(HttpServletRequest request, HttpServletResponse response)
*/
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// TODO Auto-generated method stub
doGet(request, response);
}
private void executeQuery(Connection conn, String sql) {
Statement stmt;
try {
stmt = conn.createStatement();
stmt.executeUpdate(sql);
stmt.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
private void executeQuery(Connection conn, String sql, int count) {
for(int i = 0; i < count; i++) {
executeQuery(conn, sql);
}
}
}
検証:Aurora PostgreSQL障害(障害挿入クエリ)
Aurora MySQL同様に、製品側で意図的に障害挿入する機能が提供されている。詳細は下記のドキュメントを参照ください。
障害挿入クエリ
- インスタンスのクラッシュ
test=> SELECT aurora_inject_crash ('instance');
SSL SYSCALLエラー: EOFを検出
サーバーへの接続が失われました。リセットしています: 失敗。
サーバーへの接続が失われました。リセットしています: 失敗。
!?>
下記のワーニングが出力されるが、インスタンス復帰直後のアプリケーション実行においてエラーは発生しない。
WARN [org.jboss.jca.adapters.jdbc.local.LocalManagedConnectionFactory] (default task-1) IJ030027: Destroying connection that is not valid, due to the following exception: org.postgresql.jdbc.PgConnection@633ab0fc: org.postgresql.util.PSQLException: バックエンドへの送信中に、入出力エラーが起こりました。
ワーニングは発生するものの、アプリケーションはエラーとはならない。
この検証ではuse-fast-fail=falseを採用しているため、アプリケーションの初回アクセス時間が727msec(その次は7msec)となった。
本検証ではプールサイズを50にしているため、初回アクセス時。下記メッセージが50回出力された。
WARN [org.jboss.jca.core.connectionmanager.pool.strategy.OnePool] (default task-1) IJ000621: Destroying connection that could not be validated: org.jboss.jca.core.connectionmanager.listener.TxConnectionListener@614dc6ca[state=NORMAL managed connection=org.jboss.jca.adapters.jdbc.local.LocalManagedConnection@3f63b4cd connection handles=0 lastReturned=1722323003581 lastValidated=1722323003581 lastCheckedOut=1722323003581 trackByTx=false pool=org.jboss.jca.core.connectionmanager.pool.strategy.OnePool@35a1dc4 mcp=SemaphoreConcurrentLinkedQueueManagedConnectionPool@de4ec27[pool=PostgresDS] xaResource=LocalXAResourceImpl@60ca87a8[connectionListener=614dc6ca connectionManager=75370dbf warned=false currentXid=null productName=PostgreSQL productVersion=15.4 jndiName=java:/PostgresDS] txSync=null]
- ディスパッチャーのクラッシュ
test=> SELECT aurora_inject_crash ('dispatcher');
aurora_inject_crash
------------------------------------------------
fault injection has been executed successfully
(1 行)
インスタンスのクラッシュ同様の結果となる。
- ノードのクラッシュ
test=> SELECT aurora_inject_crash ('node');
WARNING: terminating connection because of crash of another server process
DETAIL: The postmaster has commanded this server process to roll back the current transaction and exit, because another server process exited abnormally and possibly corrupted shared memory.
HINT: In a moment you should be able to reconnect to the database and repeat your command.
SSL SYSCALLエラー: EOFを検出
サーバーへの接続が失われました。リセットしています: 成功。
psql (16.3、サーバー 15.4)
SSL接続(プロトコル: TLSv1.3、暗号化方式: TLS_AES_256_GCM_SHA384、圧縮: オフ)
PostgreSQLクライアントからみえるエラーパターンとして2パターンあった。
test=> SELECT aurora_inject_crash ('node');
WARNING: terminating connection because of crash of another server process
DETAIL: The postmaster has commanded this server process to roll back the current transaction and exit, because another server process exited abnormally and possibly corrupted shared memory.
HINT: In a moment you should be able to reconnect to the database and repeat your command.
SSL SYSCALLエラー: EOFを検出
サーバーへの接続が失われました。リセットしています: 失敗。
サーバーへの接続が失われました。リセットしています: 失敗。
!?>
インスタンスのクラッシュ同様の結果となる。
ディスク障害系については記事投稿時点では何も起きなかったが深堀せずとした。
ディスク障害
SELECT * FROM aurora_show_volume_status();で値を取得し、この値よりも小さい値を指定する。↓の80
-
SELECT aurora_inject_disk_failure(100, 80, true, 180);
-
SELECT aurora_inject_disk_failure(100, 80, false, 180);
いずれもクエリーを実行しても何も起きなかった。 -
SELECT aurora_inject_disk_congestion(100, 80, true, 180, 50, 50);
-
SELECT aurora_inject_disk_congestion(100, 80, false, 180, 50, 50);
いずれのクエリーを実行しても何も起きなかった。
検証:Aurora PostgreSQL障害時挙動(マネコン操作)
リーダーとライターが入れ替わり、AZも変更となる。フェイルオーバー後の初回アクセス時はエラーとならない。
クライアントへはフェイルオーバー完了後に別IPアドレス(select inet_server_addr())が返る。
エンドポイントについてはフェイルオーバー後にIPアドレス(nslookup endpoint)が変わる。
記事執筆で利用した環境では下記のとおり、digによる観察では必ず5秒でDNSキャッシュが切れる挙動となった。RDSのフェイルオーバーは1分かかることもあるため、この5秒は誤差にすぎないレベルだが、自分の環境がどういうDNS環境になっているのかは把握しておいたほうがよい。
- watch -dc -n 1 dig aurora-cluster.xxx.ap-northeast-1.rds.amazonaws.comの実行結果
; <<>> DiG 9.16.23-RH <<>> aurora-cluster.cluster-xxx.ap-northeast-1.rds.amazonaws.com
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 9101
;; flags: qr rd ra; QUERY: 1, ANSWER: 3, AUTHORITY: 0, ADDITIONAL: 1
;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; MBZ: 0x0005, udp: 4096
;; QUESTION SECTION:
;aurora-cluster.cluster-xxx.ap-northeast-1.rds.amazonaws.com. IN A
;; ANSWER SECTION:
aurora-cluster.cluster-xxx.ap-northeast-1.rds.amazonaws.com. 5 IN CNAME ・・・省略・・・
reader.xxx.ap-northeast-1.rds.amazonaws.com. 5 IN CNAME ec2-xxx.ap-northeast-1.compute.amazonaws.com.
ec2-54-248-128-58.ap-northeast-1.compute.amazonaws.com. 5 IN A 54.248.xxx.xxx
;; Query time: 6 msec
;; SERVER: 192.168.47.2#53(192.168.47.2)
;; WHEN: Sat Aug 10 13:57:50 JST 2024
;; MSG SIZE rcvd: 247
検証:Aurora PostgreSQL障害時挙動(Fault Injection Service)
- Fault Injection Serviceでのフェイルオーバー
アクション名:failover_db_cluster / aws:rds:failover-db-cluster
マネコンでのフェイルオーバーと同じ挙動となる。 - Fault Injection Serviceでのインスタンス再起動
アクション名:reboot / aws:rds:reboot-db-instances
IPアドレスそのまま、インスタンス再起動直後のアプリケーションエラーはなし。フェイルオーバー中はエラーとなる。エラーとなるのは約1秒間。
本記事投稿時点ではFIS(RDS)の提供機能についてはこの二つのみ。FISは有償のサービスであるが、いずれもマネコンやRDSの障害挿入機能により代替できるため利用するメリットはない。
まとめ
Aurora PostgreSQLクラスタのようにAZ跨ぎのフェイルオーバーが発生する製品であっても、JBossを利用する際には、validate-on-match=trueにすることでDB障害復帰後にJBossを再起動する必要はなく、無効なDB接続は確実に破棄されDB接続は自然回復する。