開発

SQLite WALモードの盲点、読み取り専用でもロック競合

SQLiteのWALモードでは、読み取り専用の短命コネクションでもロック競合が発生する。WALの接続ライフサイクルが原因で、書き込みが全くない状況でも"database is locked"エラーが生じ得る。

7分で読める SINGULISM 編集チームが確認・編集

SQLite WALモードの盲点、読み取り専用でもロック競合
Photo from Unsplash

SQLiteで発生する「database is locked」エラーは、書き込み操作にのみ関連すると一般に考えられている。しかし、読み取り専用の短命コネクションでも、このエラーが発生し得ることが明らかになった。

Lobstersで hynek.me via ubernostrum が報告した内容によれば、WAL(Write-Ahead Logging)モードにおける接続のライフサイクルが原因で、アプリケーションデータの書き込みが一切行われていない状況でもロック競合が生じる。この現象は、データベースへの書き込みが稀で、読み取りが高頻度で行われるワークロードにおいて特に顕在化する。

WALモードの一般的な誤解

SQLiteのロック問題に対する定型的なアドバイスは、次の3点に集約される。

  • WALモードを使用する
  • 0以外のビジータイムアウトを設定する
  • 書き込みトランザクションには BEGIN IMMEDIATE を使用する

このアドバイスは、長命のデータベース接続を前提とする典型的なWebアプリケーションのワークロードでは正しい。しかし、hynek.meが直面したのは特殊なパターンだった。データベースへの書き込みは月に1回未満と極めて稀だが、読み取りは独立したプロセス群によって毎秒数十回から数百回実行される。各プロセスはデータベースを読み取り専用(SQLITE_OPEN_READONLY)で開き、1回のSELECTを実行し、すぐに閉じるという運用である。

書き込みが読み取りをブロックするのを避けるため、データベースをWALモードに設定した。ところが、この構成で「database is locked」エラーが散発的に発生した。SQLiteのCライブラリにおけるデフォルトの接続タイムアウト値が0であることも、問題の顕在化に拍車をかけた。

読み取りでも発生する書き込み競合

WALモードでは、接続同士が-shmファイルを通じて調整を行う。空のWALデータベースを開いたり閉じたりする際に、瞬間的に排他ロックが必要になる。このタイミングで新たな読み取り専用接続が到着すると、アプリケーションデータの書き込みが全く行われていない状況でも SQLITE_BUSY エラーが返される。

hynek.meが提示したファイルタイムスタンプの比較が状況を明確に示している。

$ ls -la /vmws/config/config.db*
-rw-r----- 1 root root 294912 Jul 21 09:49 /vmws/config/config.db
-rw-r----- 1 root root 32768 Jul 24 18:26 /vmws/config/config.db-shm
-rw-r----- 1 root root 0 Jul 24 18:26 /vmws/config/config.db-wal

データベース本体(config.db)は7月21日から更新されていない。しかし、-walファイルと-shmファイルは7月24日まで読み取り専用プロセスによって書き換えられていた。読み取り専用であるはずのシステムで、予想外の書き込みが発生している。

回避策とトレードオフ

hynek.meは、書き込みが極めて稀であることを考慮し、データベースをWALモードからDELETEモードに切り替えることで問題を解決した。この変更以降、「database is locked」エラーは発生していない。

同氏は「デフォルトのビジータイムアウトが0でなければ、この問題を学ぶことはなかった。エラーが明確に発生したことは幸運だった」と振り返る。問題が静かに進行するよりも、明確に失敗する方が対処しやすいというジレンマだ。

ただし、この選択は常に正しいとは限らない。DELETEモードでは書き込み時にテーブル全体のロックが発生するため、書き込み頻度が高いワークロードには適さない。トレードオフを理解した上での選択が求められる。

再現可能なテストスクリプト

hynek.meは、標準ライブラリのみを使用したPythonスクリプトで、この問題の再現方法も公開している。スクリプトは3つのシナリオを並行プロセスで実行する。

  • A: ビジータイムアウトなし、WALモード
  • B: 1秒のビジータイムアウト、WALモード
  • C: ビジータイムアウトなし、WALモードなし(DELETEモード)

2023年製MacBook ProとPython 3.14の環境では、シナリオAで1~10回の失敗が確認され、BとCでは0回だった。この結果は、WALモードとビジータイムアウトの関係を明確に示している。

編集部の見解

今回の発見は、SQLiteのWALモードに潜む盲点を浮き彫りにした。短期的には、読み取り専用の短命コネクションを多用するシステムの開発者は、WALモードの選択を再評価する必要がある。hynek.meが示したように、DELETEモードへの切り替えや適切なビジータイムアウトの設定が有効な対策となる。また、この問題は接続プールを使用しない独立プロセスアーキテクチャで顕在化しやすい。接続プールの採用やコネクションの再利用を検討することで、ロック競合の発生頻度を低減できる可能性がある。 長期的視点では、SQLiteのWAL実装そのものの改善が期待される。短命な読み取り専用コネクションのロック調停をより効率的に行う最適化が加われば、この問題は根本的に解決するかもしれない。また、SQLiteのドキュメントにおいても、WALモードの注意点として「読み取り専用コネクションでもロック競合が発生し得る」ことが明記されるべきだろう。 編集部としては、データベースエンジンの選択時に、想定するワークロードのパターンをより厳密に検討することが問われていると考える。

参考

よくある質問

WALモードで読み取り専用接続がロックされる理由は何か
WALモードでは接続同士が-shmファイルを通じて調整を行う。空のWALデータベースを開いたり閉じたりする際に瞬間的に排他ロックが必要になり、このタイミングで新たな読み取り専用接続が到着するとSQLITE_BUSYエラーが返される。アプリケーションデータの書き込みが全く行われていない状況でも発生する。
この問題を回避する方法はあるか
主な回避策として、ビジータイムアウトに0以外の値を設定する方法がある。また、書き込みが稀なワークロードではWALモードからDELETEモードに切り替えることも有効。接続プールの使用や、コネクションの再利用による読み取り専用コネクションの総数を減らすことも効果的だ。
DELETEモードに切り替えることで別の問題は発生しないか
DELETEモードでは書き込み時にテーブル全体のロックが発生するため、書き込み頻度が高いワークロードでは並行性が低下する。読み取りと書き込みの比率や頻度を考慮し、適切なモードを選択する必要がある。hynek.meのように書き込みが月に1回未満という極端に稀なケースでは、DELETEモードが安全な選択となる。
出典: Lobsters

コメント

← トップへ戻る