SQLite on EFS はどの程度実用的か

TL;DR;

TPC-C 相当の workload に対して、

であればSQLite on EFS でいける。

SQLite on EFS

小規模に SQL を利用したい場合の選択肢として、AWS EFS (NFS) 上で SQLite を使うという選択肢がある。

AWS EFS の課金体系

2025年 春時点で、EFS では 3通りの課金体系を選べる。

価格表をみてもらうとよいが、Elastic throughput の読み書き課金や provisioned throughput の throughput 課金は高額になりがち1。 小規模な SQL を安価に運用することを狙う場合、ほぼ Bursting throughput 一択で、その制限内でどの程度運用できそうか確認する。

Bursting Throughput の枠で収まらない場合は、EFS ではなく RDS PostgreSQL/MySQL や、neon.tech、TiDB などの managed database を検討したほうが cost/performance の面でよいだろう。

1

Bursting throughput 1ヶ月分の credit 相当である、write 75GiB + read 150GiB を Elastic throughput で読み書きすると、$11.25 (2025-04, Tokyo region) かかる。

EFS Bursting Throughput mode

Bursting Throughput mode の場合、保存容量に応じて throughput が割当られるが、保存 1GiB 未満の場合には一律以下が付与される。 SQLite を載せて使う場合、1GiB こえるような SQLite file を EFS で使うのはあまり現実的ではなく、実質この1GiB未満保存の throughput が制限になる。

Burst credit を使い切った場合の 1MiB/sec は実用にならないため、付与される burst credit の範囲内で利用する。 そのため、1ヶ月の I/O が平均 125GiB 以内ということが制限となる。 (※ read は 1/3 扱いなので、例えば write 75GiB + read 150GiB は範囲内)

ここから、月間 request 上限の目安がわかる。 SQLite file の読み書き量は workload によって変わるため実際の用途ごとに測定してもらうのが良いが、参考に TPC-C benchmark 相当の場合、約 1MiB/req. (read を1/3換算した合計) が目安。 125k req./month までであれば、burst credit の範囲に収まりそうということがわかる。

Perk performance

月間の合計 request は上記でわかるので、request が集中したときの短時間の性能も確認する。 上記同様、TPC-C benchmark 相当の場合でみると、同時読み書き時で 2 req./sec 程度になる。

性能は workload に大きく依存する。 TPC-C の場合、New Order, Payment, Order Status, Delivery, Stock Level の 5つの transaction 処理の複合になっているが、そのうち New Order, Payment, Delivery の 3つが書き込み処理で、書き込み比率は高めである。 SQLite は同時に並列読み書きできない制限があるが、読み込みのみなら多数並列で動く。たとえば、blog のような read がほとんどの workload であれば同時にもっと多くの request を処理できるだろう。

性能を他の database と比較すると、かなり遅い。

※ tpm : TPC-C の New Order transaction を 1分間に実行できた数

かなり遅いが、この程度でも成り立つ用途はそれなりにあるだろう。 平均10秒に一回操作(request)する使い方で、20 user が同時に使っているぐらいの規模感であり、個人開発であれば現実的ではなかろうか。

もう少し性能が欲しい場合

TPC-C 相当 2 req./sec よりもう少しだけ性能が欲しい場合、EFS one zone にすると可用性・耐久性と引き換えに 5 req./sec 相当程度まで性能が伸びる。 1つの availability zone 内のみでの冗長になる代わりに、IO 遅延、特に lock 取得が短縮されて性能が改善される。

Zone 障害時には service が停まりうる、また data center 1棟を火災で消失のような場合には database を失うことになるが、十分低い確率と許容できれば EFS one zone を選ぶのも良いだろう。 EFS replication で例えば東京-大阪で非同期 replication しておき全損は避けるというような risk の取り方もできる(非同期なので災害直前15分程度の書き込みは失われる)。

EFS (NFS) で SQLite を利用する場合の注意点

SQLite の FAQ にも書かれているが、NFS 上で SQLite を使うのは推奨されていない。 正しい使い方をしないと、性能がでないだけでなく SQLite file が壊れるため SQLite の document をよく読んだ上で使おう。

SQLite file を壊さないための注意点

まず最初に、SQLite 3.7.0 以降で実装された WAL mode は使えない。 SQLite の WAL mode は共有 memory を必要とし、NFS 上で SQLite を使う場合には別の host で動いているSQLite と共有 memory を同期できないためである。 具体的に、起動時に明示的に以下のように SQL PRAGMA を発行して、従来の rollback journal の利用を強制する。

PRAGMA journal_mode=DELETE;

続けて、SQLite の FAQ で、NFS 上で SQLite を使うのは推奨しない旨が書かれている。 大きく 2つ理由があり、

ということが書かれている。 特に前者は file 破損につながるため、EFS 以外の NFS 上で利用する場合には lock 実装を注意深く確認する。

性能を確保するための注意点

NFS で SQLite を走らせる場合の性能上の bottle neck は FAQ にもあるように file lock の取得待ち時間である。 EFS 上での benchmark をみると、競合がない状態でも lock 取得に 30ms 程度かかってしまう。 さらに複数 process が同時に lock を取り合う状態になると lock 取得時間が平均 200ms 程度まで伸びる。 2

そのため lock の取得&解放 回数をなるべく少なくするようにすることが性能を出すためのコツになる。 具体的には BEGIN TRANSACTION ... COMMIT でまとめると、transaction 単位での lock 取得になるため、HTTP request 1つに対する処理を 1回の transaction にまとめるようにするとよい。

-- この例だと file lock 取得-解放を3回繰り返す
SELECT * FROM users WHERE id=$1;
SELECT * FROM posts WHERE user_id=$1;
INSERT INTO posts SET (user_id,title) VALUES ($1,$2);
-- この例だと file lock は1回で済む
BEGIN IMMEDIATE TRANSACTION;
SELECT * FROM users WHERE id=$1;
SELECT * FROM posts WHERE user_id=$1;
INSERT INTO posts SET (user_id,title) VALUES ($1,$2);
COMMIT;
2

参考に、EFSではなく EC2+EBS であれば lock 取得の遅延は競合なし時で 0.06 ms。500倍違う。

そのほかの注意点

SQLite は default の挙動として file lock 取得ができなかった場合には直ちに SQLITE_BUSY を返して処理を中断してしまう。 これだと application code の中で再試行処理をいたるところに書かなければならないため、あまり使い勝手がよくない。 PRAGMA busy_timeout を設定しておくと、指定した ms 間は SQLite 側で lock 再取得を試みるようになるので、設定しておくと良い。

-- 最大 2秒 lock 確保待ち
PRAGMA busy_timeout = 2000;

まとめ

上記のように coding する上でいくつか注意は必要、また性能も限られるが、EFS で SQLite を利用することで、小規模な用途に安価に SQL database を運用することができる。