はじめに
業務で開発しているオンライン申請系のシステムで、Spring Batchを利用しています。
Spring Batchを本格的に使ったのは初めてだったのですが、バッチアプリケーションに必要な機能が豊富に提供されており、かなり便利だと感じました。
しかし、仕組みをちゃんと知らずに使ってハマった点があったので、書いてみます。
実行環境
- Java11
- Spring Boot 2.4.3
- Spring Batch 4.3.1
- PostgreSQL 42.2.18
- MyBatis 3.5.5
使い方
Spring Batchには、バッチ処理(ジョブ)を起動する方法として、2つの方法がサポートされています。
私達のシステムでは、それぞれ以下のユースケースで利用しています。
- 夜間バッチなど、スケジューラから起動する処理(from コマンドライン)
- SaaSなどの外部サービスと通信する処理(from Webコンテナー)
なぜ後者をバッチ処理にしているかと言うと、いわゆる「Retryパターン」を実装できるためです。Spring Batchは、ジョブの実行履歴や実行時のパラメータなどが、全てデータベース等に保存されるため、ジョブのIDを指定して失敗した処理のリトライをかんたんに実装できます。 個人的には、この機能がかなり便利で魅力を感じています。
ハマったコト1:コマンドラインからのジョブ実行でリソース不足
問題
コマンドラインからジョブ起動する方法では、一般的にシェルスクリプトからJavaプロセスを起動することになります。私達のシステムでも、OSのスケジューラ->シェルスクリプト->Javaを起動しています。
ジョブを起動するスクリプトは Java 仮想マシンを開始する必要があるため、プライマリエントリポイントとして機能するメインメソッドを持つクラスが必要です。Spring Batch は、まさにこの目的に役立つ実装を提供します
#!/bin/bash
(略)
# 起動ジョブ名取得
if [ $# = 0 ]; then
echo "Please enter the job name."
exit 1
else
JOBNAME=$1
fi
echo "Batch launch Name is [ $JOBNAME ]"
# 起動パラメータの取得
if [ ! -n "$2" ]; then
PARAMS=''
echo "Params is not set."
else
PARAMS=$2
echo "Params is $PARAMS"
fi
(略)
# バッチ起動
/usr/bin/java -server -Dspring.main.web-application-type=none -jar ${APP_HOME}/batch-sample-application.jar $JOBNAME $PARAMS
# バッチ処理結果ステータス
EXITCODE=$?
echo "batch exit code is $EXITCODE"
exit $EXITCODE
考えれば当たり前ですが、仮想マシンのリソースやコネクションプールを毎回使用するため、複数のジョブを同時に起動すると、リソース不足に陥りやすくなります。
特に、Spring BootでSpring Batchを動かす場合、コネクションプールHikariCPのプールサイズのデフォルト設定はmaximum-pool-size
=minimum-idle
=10 となっており、起動のたびに10コネクションを使用するため、一部のプロセスで起動エラーになってしまいました。
2021-12-05 01:35:13.735 ERROR [batch-examination,,] 11736 --- [main] com.zaxxer.hikari.pool.HikariPool : HikariPool-1 - Exception during pool initialization.
org.postgresql.util.PSQLException: 接続試行は失敗しました。
at org.postgresql.core.v3.ConnectionFactoryImpl.openConnectionImpl(ConnectionFactoryImpl.java:315)
at org.postgresql.core.ConnectionFactory.openConnection(ConnectionFactory.java:51)
at org.postgresql.jdbc.PgConnection.<init>(PgConnection.java:225)
at org.postgresql.Driver.makeConnection(Driver.java:465)
at org.postgresql.Driver.connect(Driver.java:264)
at com.zaxxer.hikari.util.DriverDataSource.getConnection(DriverDataSource.java:138)
at com.zaxxer.hikari.pool.PoolBase.newConnection(PoolBase.java:358)
at com.zaxxer.hikari.pool.PoolBase.newPoolEntry(PoolBase.java:206)
at com.zaxxer.hikari.pool.HikariPool.createPoolEntry(HikariPool.java:477)
at com.zaxxer.hikari.pool.HikariPool.checkFailFast(HikariPool.java:560)
at com.zaxxer.hikari.pool.HikariPool.<init>(HikariPool.java:115)
at com.zaxxer.hikari.HikariDataSource.getConnection(HikariDataSource.java:112)
(略)
at org.springframework.boot.loader.MainMethodRunner.run(MainMethodRunner.java:49)
at org.springframework.boot.loader.Launcher.launch(Launcher.java:107)
at org.springframework.boot.loader.Launcher.launch(Launcher.java:58)
at org.springframework.boot.loader.JarLauncher.main(JarLauncher.java:88)
Caused by: java.io.EOFException: null
at org.postgresql.core.PGStream.receiveChar(PGStream.java:443)
at org.postgresql.core.v3.ConnectionFactoryImpl.doAuthentication(ConnectionFactoryImpl.java:598)
at org.postgresql.core.v3.ConnectionFactoryImpl.tryConnect(ConnectionFactoryImpl.java:161)
at org.postgresql.core.v3.ConnectionFactoryImpl.openConnectionImpl(ConnectionFactoryImpl.java:213)
... 97 common frames omitted
解決策(回避策)
- ジョブの起動タイミングをずらす
- コネクションプールの数を減らす(
minimum-idle
=1) - ヒープサイズをチューニングする
ハマったコト2:Spring Batchメタデータテーブルの削除
Spring Batchのメタデータテーブル
リファレンスに詳しい説明が記載されていますが、6つのメタデータテーブルから構成されています。これにより、先述したジョブのリトライが可能となっています。
https://spring.pleiades.io/spring-batch/docs/current/reference/html/images/meta-data-erd.png
テーブルのDDLは、各プラットフォームに合わせたsqlファイルがSpring Batchのjarに内包されています。私達はPostgreSQLを使用しているので、以下のsqlをそのまま使用しています。
- schema-postgresql
- schema-drop-postgresql.sql
問題
これらのメタデータは、ジョブが実行されるたびに情報がINSERTされていくため、放置すると肥大化してしまいます。
定期的なメンテナンス作業ができないシステムでは、以下のようなテーブルのクリーンアップを実装する必要があるみたいです。
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="xxx.xxx.MetaDataCleanUpMapper">
<!-- 削除条件 -->
<sql id="delete_where">
where
CREATE_TIME < #{deltime,jdbcType=TIMESTAMP}
</sql>
<!-- BATCH_STEP_EXECUTION_CONTEXT の削除 -->
<sql id="delete_step_execution_context">
DELETE FROM
BATCH_STEP_EXECUTION_CONTEXT
WHERE
STEP_EXECUTION_ID IN (
SELECT
STEP_EXECUTION_ID
FROM
BATCH_STEP_EXECUTION
WHERE
JOB_EXECUTION_ID IN (
SELECT
JOB_EXECUTION_ID
FROM
BATCH_JOB_EXECUTION
<include refid="delete_where"/>
)
)
</sql>
<!-- BATCH_STEP_EXECUTION の削除 -->
<sql id="delete_step_execution">
DELETE FROM
BATCH_STEP_EXECUTION
WHERE
JOB_EXECUTION_ID IN (
SELECT
JOB_EXECUTION_ID
FROM
BATCH_JOB_EXECUTION
<include refid="delete_where"/>
)
</sql>
<!-- BATCH_JOB_EXECUTION_CONTEXT の削除 -->
<sql id="delete_job_execution_context">
DELETE FROM
BATCH_JOB_EXECUTION_CONTEXT
WHERE
JOB_EXECUTION_ID IN (
SELECT
JOB_EXECUTION_ID
FROM
BATCH_JOB_EXECUTION
<include refid="delete_where"/>
)
</sql>
<!-- BATCH_JOB_EXECUTION_PARAMS の削除 -->
<sql id="delete_job_execution_params">
DELETE FROM
BATCH_JOB_EXECUTION_PARAMS
WHERE
JOB_EXECUTION_ID IN (
SELECT
JOB_EXECUTION_ID
FROM
BATCH_JOB_EXECUTION
<include refid="delete_where"/>
)
</sql>
<!-- BATCH_JOB_EXECUTION の削除 -->
<sql id="delete_job_execution">
DELETE FROM
BATCH_JOB_EXECUTION
<include refid="delete_where"/>
</sql>
<!-- BATCH_JOB_INSTANCE の削除 -->
<sql id="delete_job_instance">
DELETE FROM
BATCH_JOB_INSTANCE
WHERE
JOB_INSTANCE_ID IN (
SELECT
BATCH_JOB_INSTANCE.JOB_INSTANCE_ID
FROM
BATCH_JOB_INSTANCE
LEFT JOIN
BATCH_JOB_EXECUTION
ON BATCH_JOB_INSTANCE.JOB_INSTANCE_ID = BATCH_JOB_EXECUTION.JOB_INSTANCE_ID
WHERE
BATCH_JOB_EXECUTION.JOB_EXECUTION_ID is null
)
</sql>
<!-- メタデータテーブル削除SQL -->
<delete id="deleteBatchStepExecutionContext" parameterType="map">
<include refid="delete_step_execution_context"/>
</delete>
<delete id="deleteBatchStepExecution" parameterType="map">
<include refid="delete_step_execution"/>
</delete>
<delete id="deleteBatchJobExecutionContext" parameterType="map">
<include refid="delete_job_execution_context"/>
</delete>
<delete id="deleteBatchJobExecutionParams" parameterType="map">
<include refid="delete_job_execution_params"/>
</delete>
<delete id="deleteBatchJobExecution" parameterType="map">
<include refid="delete_job_execution"/>
</delete>
<delete id="deleteBatchJobInstance" parameterType="map">
<include refid="delete_job_instance"/>
</delete>
</mapper>
クリーンアップが終わらない…
サービス開始からしばらくすると、クリーンアップに長時間かかるようになりました。1日あたり約6,000〜7,000件ほどのジョブを実行していたのですが、クリーンアップのSQLが24時間経過しても返って来なくなりました。
解決策(回避策)
遅い原因として、BATCH_JOB_EXECUTION
とBATCH_JOB_INSTANCE
に対する外部キー参照が疑われたので、外部キー参照の無効化(削除でも良い)を行いました。
ALTER TABLE BATCH_JOB_EXECUTION DISABLE TRIGGER ALL;
ALTER TABLE BATCH_JOB_INSTANCE DISABLE TRIGGER ALL;
その結果、数時間かかっていたSQLが、数秒で完了するにようになりました。
この問題については、他の解決方法が分かっていません…
最後に
ということで、実システムでSpring Batchを使ってみた上で、直面した問題や注意点を書いてみました。
他にも注意すべき点や、ここに書いた問題の良い解決策があれば、ぜひご意見いただければ嬉しいです!