WebSphere Libertyデータソース管理POC

# 本記事の目的
WebSphere Libertyでデータソースを構成する手順とサンプルコード、データベース接続検証関連パラメータの製品挙動についてまとめている。

JBoss編、Tomcat編についてはこちらを参照ください。
[JBoss データソース管理（DBはAurora PostgreSQL）POC](https://qiita.com/thashi/items/8b30041be752235196b3)
[Tomcat DBCP POC](https://qiita.com/thashi/items/e7b8a70fe382ca3d68c2)

# 背景
とある超重要システムでDb2障害時にWebSphereを再起動していた。
ミッションクリティカルなシステムではAP再起動はアプリ全体の停止につながるため、対応方法としてはありえない。影響範囲を極小化するためにせめてコネクションプールのリフレッシュだけにできないか？と思ったのがこの記事のモチベーション。

# 検証環境
- WebSphere Liberty Jakarta EE 8 24.0.0.8
- OpenJDK 17.0.6
- Red Hat Enterprise Linux release 9.3 (Plow)
- Db2 11.5.9.0
- JMXコマンドラインツール(https://qiita.com/uzresk/items/9142c24f218003a4b2a6)

# WebSphere Libertyのインストールと設定
https://www.ibm.com/support/pages/websphere-liberty-developers
からzipファイルをダウンロードして解凍するだけ。
```
$ JAVA_HOME=/usr/lib/jvm/java-17-openjdk-17.0.10.0.7-2.0.1.el9.x86_64
$ export JAVA_HOME
$ PATH=$JAVA_HOME/bin:$PATH
$ export PATH
$ pwd
/opt/ibm/wlp/bin
$ ls
auditUtility      binaryLog.bat  collective      configUtility.bat  featureManager      featureUtility.bat  jaxb   pluginUtility      productInfo.bat  securityUtility      server.bat           start.sh
auditUtility.bat  client         collective.bat  ddlGen             featureManager.bat  installUtility      jaxrs  pluginUtility.bat  schemaGen        securityUtility.bat  serverSchemaGen      tools
binaryLog         client.bat     configUtility   ddlGen.bat         featureUtility      installUtility.bat  jaxws  productInfo        schemaGen.bat    server               serverSchemaGen.bat
$ ./server create
サーバー defaultServer が作成されました。
$ ./server start
サーバー defaultServer を始動中です。
サーバー defaultServer がプロセス ID 1132789 で始動しました。
$ ./server stop
サーバー defaultServer を停止中です。
サーバー defaultServer は停止しました。
```

効率的に検証を行うため、二回目以降はコンソールに標準出力させて観察する「server run」を使用
```
$ ./setEnv.sh（上記の環境変数セット用のshell）
$ ./server run
```

### リモートアクセス許可
インストール直後はローカルアクセスのみに制限かけられているためリモートホストからのアクセス許可を設定する。下記のとおりserver.xmlにhost="*"を追加。再起動は不要。
```
    <httpEndpoint id="defaultHttpEndpoint"
                  httpPort="9080"
                  httpsPort="9443" host="*" />
```

### JDBCドライバの設定
Db2のJDBCドライバをセットする。下記のようにresourcesディレクトリにファイルコピーする。
```
$ pwd
/opt/ibm/db2/V11.5_01/java
$ ls
Common.jar  db2dbgm.jar  db2java.zip  db2jcc_license_cisuz.jar  db2policy.jar  db2umplugin.jar
db2c2c.jar  db2ext.jar   db2jcc4.jar  db2jcc_license_cu.jar     db2qgjdbc.jar  jdk64
$ pwd
/opt/ibm/wlp/usr/shared/resources
$ ls
db2jcc4.jar
```

### データソース関連の挙動をみるための設定
https://www.ibm.com/docs/ja/was-liberty/core?topic=10-connectionpool-monitoring
で丁寧に解説されている。
server.xmlに必要featureを追加する。
```
    <featureManager>
        <feature>javaee-8.0</feature>
        <feature>localConnector-1.0</feature>
        <feature>monitor-1.0</feature>
    </featureManager>
```    
defaultServerフォルダの下にjvm.optionsファイルを作成し、下記のように設定する。
```
-Dfile.encoding=UTF-8(Windows開発環境のみ。文字化け防止目的)

-Dcom.sun.management.jmxremote.port=9024

-Dcom.sun.management.jmxremote.rmi.port=9025

-Dcom.sun.management.jmxremote.ssl=false

-Dcom.sun.management.jmxremote.authenticate=false
```
コネクションオブジェクトの作成と破棄を観察するため、メトリクスは下記のように設定する。
metricsファイルの中身
```
"WebSphere:type=ConnectionPoolStats,name=jdbc/db2" "CreateCount"
"WebSphere:type=ConnectionPoolStats,name=jdbc/db2" "DestroyCount"
```
実行方法は下記の通り
```
java -Dpath=./metrics -jar jmx-cmdclient-0.1.1.jar localhost:9024 1
```
一秒おきに標準出力される。出力例は下記の通り。（CreateCount=2, DestroyCount=2の例）
```
2024-08-24 15:19:12.380,2,2
2024-08-24 15:19:13.383,2,2
2024-08-24 15:19:14.385,2,2
```

注意：LibertyのデータソースのMBean属性は動的に生成される。そのため上記の属性をみるためには一度アプリケーションを実行してDBアクセスする必要がある。Liberty起動直後に上記のツールを実行してもターゲットとなる属性がない旨のエラーとなるため注意が必要。

### データソースの設定
```
    <featureManager>
            <feature>javaee-8.0</feature>
            <feature>localConnector-1.0</feature>
            <feature>monitor-1.0</feature>
    </featureManager>
    <basicRegistry id="basic" realm="BasicRealm">
        <!-- <user name="yourUserName" password="" />  -->
    </basicRegistry>
    <httpEndpoint id="defaultHttpEndpoint"
                  httpPort="9080"
                  httpsPort="9443" host="*" />
    <applicationManager autoExpand="true"/>
    <library id="db2lib">
        <fileset dir="${shared.resource.dir}" includes="*.jar"/>
    </library>
    <dataSource id="db2ds" jndiName="jdbc/db2" onConnect="SELECT * FROM SYSIBM.SYSDUMMY1" validationTimeout="5s" connectionManagerRef="pool1">
        <connectionManager id="pool1" maxPoolSize="100" minPoolSize="100" connectionTimeout="10s" agedTimeout="30m" purgePolicy="ValidateAllConnections" />
        <jdbcDriver libraryRef="db2lib"/>
        <properties.db2.jcc databaseName="testdb" serverName="localhost" portNumber="25010" user="db2inst1" password="db2inst1" />
    </dataSource>
```
ポイントは下記の三つ。
- onConnect
- validationTimeout
- purgePolicy

onConnectで負荷のかからないsysdummy1表へのクエリーを設定する。
validationTimeoutは重要な項目。これを設定しないとpurgePolicyだけではねらった効果とならない。
- ねらった効果
DB障害が発生し、DB復帰後に無効となったコネクションオブジェクトが接続検証によって破棄されてアプリケーションとしてはエラーとならずに正常実行すること。

validationTimeoutの設定をしないとDB復帰後の初回アクセスでエラーとなる。
TomcatやJBossと異なり、バックグラウンドでの定期validation機能はドキュメントを見るかぎりなかった。ただし、Tomcat編とJBoss編で言及したとおり、定期validationはなくてもやりたことは実現できるし、無効オブジェクトクリアの即時性はある。
参考：[データソース関連のドキュメント](https://www.ibm.com/docs/ja/was-liberty/core?topic=SSD28V_liberty/com.ibm.websphere.liberty.autogen.nd.doc/ae/rwlp_config_dataSource.htm)

参考：Db2ポートの確認方法は下記の通り。
```
$ db2 get dbm cfg | grep SVCENAME
 TCP/IP サービス名                            (SVCENAME) = db2c_db2inst1
 SSL サービス名                           (SSL_SVCENAME) =
$ grep db2c_db2inst1 /etc/services
db2c_db2inst1   25010/tcp
$ grep db2 /etc/services
ibm-db2         523/tcp                 # IBM-DB2
ibm-db2         523/udp                 # IBM-DB2
questdb2-lnchr  5677/tcp                # Quest Central DB2 Launchr
questdb2-lnchr  5677/udp                # Quest Central DB2 Launchr
qdb2service     45825/tcp               # Qpuncture Data Access Service
qdb2service     45825/udp               # Qpuncture Data Access Service
DB2_db2inst1    20016/tcp
DB2_db2inst1_1  20017/tcp
DB2_db2inst1_2  20018/tcp
DB2_db2inst1_3  20019/tcp
DB2_db2inst1_4  20020/tcp
DB2_db2inst1_END        20021/tcp
db2c_db2inst1   25010/tcp
```

## アプリケーションのデプロイ
今回使用したコードは下記のとおり
```
package test;

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.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.sql.DataSource;

/**
 * Servlet implementation class TestServlet
 */
@WebServlet("/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 {
		try {
			String sql = "select col1 from test";
			long start = System.nanoTime();
			Context envContext = new InitialContext();
			DataSource ds = (DataSource)envContext.lookup("jdbc/db2");
			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("result:" + 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;
	}

}
```

デプロイはwarをコピーするだけ
```
[root@localhost dropins]# pwd
/opt/ibm/wlp/usr/servers/defaultServer/dropins
[root@localhost dropins]# ls
db2.war
```

### Db2の設定（実行されたSQLとその回数をみる目的）
Db2の設定方法は[ドキュメント](https://www.ibm.com/support/pages/db2-%E4%BB%BB%E6%84%8F%E3%81%AE%E6%9C%9F%E9%96%93%E3%81%AB%E5%AE%9F%E8%A1%8C%E3%81%95%E3%82%8C%E3%81%9F-sql-%E3%81%A8%E3%81%9D%E3%81%AE%E5%AE%9F%E8%A1%8C%E5%9B%9E%E6%95%B0%E3%82%92%E5%8F%96%E5%BE%97%E3%81%99%E3%82%8B%E6%96%B9%E6%B3%95
)の通りに設定する。

```
db2 "select count(st.stmtid) as num_exec, varchar(st.stmt_text,512) as txt from ACTIVITY_ACTEVMON a, ACTIVITYSTMT_ACTEVMON st where a.activity_id = st.activity_id and a.uow_id = st.uow_id and a.appl_id = st.appl_id and a.time_started between '2024-08-23-09.00.00.000000' and '2024-08-25-12.00.00.000000' group by st.stmtid, varchar(st.stmt_text,512) order by 1 desc"
```
実行結果は下記のような結果となり、validation用のSQLが実行されたことを確認できる。この例ではselect * from sysibm.sysdummy1の部分がカウントアップされる。

```
         19 select col1 from test                                     
          5 SELECT * FROM SYSIBM.SYSDUMMY1                            
          5 select CURRENT SCHEMA from SYSIBM.SYSDUMMY1               
          1 SET EVENT MONITOR ACTEVMON STATE 1 
```

## 検証：Db2障害
Db2をdb2stop forceで強制停止して起動する。
```
$ db2stop force
2024-08-24 14:28:16     0   0   SQL1064N  DB2STOP の処理が正常に終了しました。
SQL1064N  DB2STOP の処理が正常に終了しました。
$ db2start
08/24/2024 14:28:23     0   0   SQL1063N  DB2START の処理が正常に終了しました。
SQL1063N  DB2START の処理が正常に終了しました。
$ curl localhost:9080/db2/TestServlet
db2
```
結果は初回アクセス時に下記のメッセージが標準出力に出力されたものの、アプリケーションとしてはエラーとならずに正常実行された。
このとき、データソースのCreateCountとDestroyCountがカウントアップされた。
```
[監査      ] J2CA0056I: 接続マネージャーは、リソース jdbc/db2 のリソース・アダプターから致命的接続エラーを受け取りました。  例外: state STATE_ACTIVE_FREE com.ibm.db2.jcc.am.DisconnectNonTransientConnectionException: [jcc][t4][2030][11211][4.33.31] 接続の基礎となるソケット、ソケット入力ストリーム、またはソケット出力ストリーム上での操作中に、
通信エラーが発生しました。  エラーのロケーション: Reply.fill() - insufficient data (-1)。  メッセージ: データが不十分です。 ERRORCODE=-4499, SQLSTATE=08001
```
なお、今回の検証ではDb2のcatalog/uncatalog設計はいれていない。
理由はそもそも不要なのと、障害からのリカバリ手順が増えるため。

## まとめ
WebSphere Libertyを利用する際には、onConnect,validationTimeout,purgePolicyを適切に設定することでDB障害復帰後にWASを再起動する必要はなく、無効なDB接続は確実に破棄されDB接続は自然回復する。

## おまけ
全ての設定変更を観察したわけではないが、設定ファイルを修正したあとに動的に変更検知がなされてその旨が標準出力に出力された。再起動の要らない動的変更はIBMに限らず、各製品で年々進化してきているが今回の検証レベルではすべて動的に変更された。
また、Libertyの起動時間はDELLのノートPCで約8秒だった。重くて寝起きの悪いWebLogicやWebSphere traditionalと比較すると昨今のelasticityを目指すコンテナ環境との相性はよい製品だと感じた。
20年以上前に会社のオンボロレンタルサーバーでWebSphere上のアプリ開発した時には再起動するたびに喫煙ルームにいって煙草を吸った記憶がある。タバコ一本の時間＝WebSphere起動時間が相場だったころからすると隔世の感がある。