Spring Sessionのサポート対応事例
Springアプリ使用時にOracleの一時表領域が枯渇する問題の調査と対策
この問題は、以下のようなものでした。
- Springベースのアプリを長時間稼働していると、Oracleの一時表領域が枯渇してSQLException (ORA-01652) がスローされる
- 一時表領域を確認すると、大量のLOBが含まれており、APサーバを再起動するまでは開放されない
- Spring SessionのGitHub Issueに同様の問題があった
問題が解決するまでは、APサーバの定期的な再起動などで回避してもらうことにして、調査を開始します。 調べてみると、Spring Sessionが使用するSPRING_SESSION_ATTRIBUTESテーブルのBLOB型のATTRIBUTE_BYTES列があり、 このテーブルへのINSERTのたびにこのテーブルだけでなく、一時表領域も増えていくことが分かりました。 Oracleのドキュメントを見ると、以下のような記述があります。
一時表領域に登録された「一時LOB」を解放するには、java.sql.Blobのfree()メソッドを呼ぶ必要がりますが、呼ばれていない可能性が高そうです。
ということで、ソースコードをチェックします。SPRING_SESSION_ATTRIBUTESテーブルにINSERTしているのはこのメソッドです。
private void insertSessionAttributes(JdbcSession session, List attributeNames) {
Assert.notEmpty(attributeNames, "attributeNames must not be null or empty");
if (attributeNames.size() > 1) {
this.jdbcOperations.batchUpdate(this.createSessionAttributeQuery, new BatchPreparedStatementSetter() {
@Override
public void setValues(PreparedStatement ps, int i) throws SQLException {
String attributeName = attributeNames.get(i);
ps.setString(1, attributeName);
getLobHandler().getLobCreator().setBlobAsBytes(ps, 2,
serialize(session.getAttribute(attributeName)));
ps.setString(3, session.getId());
}
@Override
public int getBatchSize() {
return attributeNames.size();
}
});
}
else {
this.jdbcOperations.update(this.createSessionAttributeQuery, (ps) -> {
String attributeName = attributeNames.get(0);
ps.setString(1, attributeName);
getLobHandler().getLobCreator().setBlobAsBytes(ps, 2, serialize(session.getAttribute(attributeName)));
ps.setString(3, session.getId());
});
}
}
このコードのgetLobHandler().getLobCreator()で取得されるTemporaryLobCreatorには以下のclose()メソッドがありましたが、 これを呼び出している個所はinsertSessionAttributes()メソッドにはありません。
@Override
public void close() {
for (Blob blob : this.temporaryBlobs) {
try {
blob.free();
}
catch (SQLException ex) {
logger.warn("Could not free BLOB", ex);
}
}
for (Clob clob : this.temporaryClobs) {
try {
clob.free();
}
catch (SQLException ex) {
logger.warn("Could not free CLOB", ex);
}
}
}
TemporaryLobCreatorはjava.io.Closeableを継承するLobCreatorを実装し、一時LOBを解放するclose()メソッドを持っていますが、 TemporaryLobCreatorはtry-with-resourcesステートメントなしで生成されるため、close()メソッドが呼び出されず、一時LOBは解放されないと考えられます。
try-with-resourcesステートメントを使用すると、ステートメント内の処理終了後に自動的にjava.io.Closeableを 実装しているオブジェクトのclose()メソッドが呼び出されます。
ということで、バグの可能性が高いことを顧客に伝え、検証のために簡単なサンプルアプリをつくってみることにしました。Spring Sessionを利用してOracleにセッション情報を保存するSpring Bootアプリです。Oracle DBのインストールに時間を奪われつつも、再現環境を構築し、検証です。
JMeterでアプリケーションにアクセスし続けると、ORA-01652が発生し、SQLExceptionがスローされます。再現しました。そして、以下のSQLを発行すると、
select * from V$TEMPORARY_LOBS;
CACHE_LOBSが増え続けているのが分かります。
SID CACHE_LOBS NOCACHE_LOBS ABSTRACT_LOBS CON_ID
---------- ---------- ------------ ------------- ----------
18 371 0 0 0
23 0 0 0 0
25 229 0 0 0
173 144 0 0 0
178 723 0 0 0
346 533 0 0 0
では、以下のようにtry-with-resourcesステートメント(try (LobCreator lobCreator = lobHandler.getLobCreator()) {・・・})を付加してから、同じことをしてみましょう。
private void insertSessionAttributes(JdbcSession session, List attributeNames) {
Assert.notEmpty(attributeNames, "attributeNames must not be null or empty");
try (LobCreator lobCreator = lobHandler.getLobCreator()) {
if (attributeNames.size() > 1) {
this.jdbcOperations.batchUpdate(this.createSessionAttributeQuery, new BatchPreparedStatementSetter() {
@Override
public void setValues(PreparedStatement ps, int i) throws SQLException {
String attributeName = attributeNames.get(i);
ps.setString(1, attributeName);
lobCreator.setBlobAsBytes(ps, 2,
serialize(session.getAttribute(attributeName)));
ps.setString(3, session.getId());
}
@Override
public int getBatchSize() {
return attributeNames.size();
}
});
} else {
this.jdbcOperations.update(this.createSessionAttributeQuery, (ps) -> {
String attributeName = attributeNames.get(0);
ps.setString(1, attributeName);
lobCreator.setBlobAsBytes(ps, 2, serialize(session.getAttribute(attributeName)));
ps.setString(3, session.getId());
});
}
}
}
SQLExceptionがスローされず、CACHE_LOBSも増えていません。
SID CACHE_LOBS NOCACHE_LOBS ABSTRACT_LOBS CON_ID
---------- ---------- ------------ ------------- ----------
23 0 0 0 0
Spring Sessionプロジェクトにもプルリクエストを提出しました。
レビューに時間はかかりましたが、修正が正しいと判断され、マージされました。