🥰

SQLite on EFSは書き込みロックできる

2025/01/26に公開

はじめに

こんにちは。がれっとです。

先日ECSとRDSをやめて、AWSコストを9割削減しましたという記事を投稿したところ、興味深いコメントを見つけたので検証してみました。

結論

EFS上のファイルには通常のSQLite相当のロックを行うことができ、SQLiteの書き込みが競合してバイナリファイルが壊れるといったことは基本的にない。

検証内容

SQLiteは書き込みロックをOSによるファイルロックを使用して実現しているため、Network File System上のファイルに対してうまくいかないというコメントを発見しました。

たしかに、SQLite公式ドキュメントにもその旨が記載されています。

https://sqlite.org/faq.html#q5

そのため、本当にEFS上のSQLiteは書き込みが競合して壊れるのか、検証していきます。

AWS 構成図

AWS構成図

マウントポイントによってロックのかかり方が異なる可能性を否定できなかったため、念の為アベイラビリティゾーンを分けて検証しました。

Lambda

API Gatewayに対して、GETリクエストをするとSELECT文が、POSTリクエストをするとINSERT文が走るPythonのスクリプトを簡易的に用意しました。

以下抜粋です

def lambda_handler(event, context): 
    if event["requestContext"]["http"]["method"] == "POST":
        # データ挿入
        try: 
            insert_data() # 1行レコードをインサート
        except Exception as e:
            return {
                "statusCode": 500,
                "body": json.dumps({
                    "error": "Failed to insert data",
                    "message": str(e),
                })
            }

    # データ取得
    try:
        body = get_data() # 最新のレコードを取得
    except Exception as e:
        return {
            "statusCode": 500,
            "body": json.dumps({
                "error": "Failed to fetch data",
                "message": str(e),
            })
        }

    return {
        "statusCode": 200,
        "body": json.dumps(body)
    }

テーブル定義は以下のものを使用しています。

CREATE TABLE posts (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  content TEXT
);

bodyにはテーブルにある最新レコードが返ってくるようにしています。

検証

以下の内容を検証しました。

  • Lambdaへの並列リクエスト
    • GET
    • POST
  • EC2でトランザクション中にLambdaへリクエスト
    • GET
    • POST

Lambdaへの並列リクエスト

並列リクエストは、abコマンドを利用しました。

GET

READ系は特に競合せずに捌くことができるはずですが、念の為検証します。

$ ab -n 10 -c 10 -m GET https://xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/dev/

This is ApacheBench, Version 2.3 <$Revision: 1903618 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/

Benchmarking xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com (be patient).....done


Server Software:        
Server Hostname:        xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com
Server Port:            443
SSL/TLS Protocol:       TLSv1.2,ECDHE-RSA-AES128-GCM-SHA256,2048,128
Server Temp Key:        ECDH X25519 253 bits
TLS Server Name:        xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com

Document Path:          /dev/
Document Length:        38 bytes

Concurrency Level:      10
Time taken for tests:   0.421 seconds
Complete requests:      10
Failed requests:        0
Total transferred:      2000 bytes
HTML transferred:       380 bytes
Requests per second:    23.75 [#/sec] (mean)
Time per request:       420.979 [ms] (mean)
Time per request:       42.098 [ms] (mean, across all concurrent requests)
Transfer rate:          4.64 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:       27   56  14.8     64      67
Processing:    71  211  61.3    244     256
Waiting:       69  211  61.7    244     256
Total:         98  266  75.5    309     323

Percentage of the requests served within a certain time (ms)
  50%    309
  66%    311
  75%    314
  80%    319
  90%    323
  95%    323
  98%    323
  99%    323
 100%    323 (longest request)

問題ないようです。

ついでなので性能測定もしておきます。
下記にConnection Timesの合計を記します。

リクエスト数 並列数 最低 中央 最大
10 10 98 309 323
100 10 118 271 375
500 10 125 264 387
20 20 132 459 802
100 20 140 600 779
500 20 107 598 804
100 100 138 5162 5368
500 100 156 6761 7558

並列リクエストには弱いようです。

POST

$ ab -n 10 -c 10 -m POST https://xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/dev/

This is ApacheBench, Version 2.3 <$Revision: 1903618 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/

Benchmarking xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com (be patient).....done


Server Software:        
Server Hostname:        xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com
Server Port:            443
SSL/TLS Protocol:       TLSv1.2,ECDHE-RSA-AES128-GCM-SHA256,2048,128
Server Temp Key:        ECDH X25519 253 bits
TLS Server Name:        xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com

Document Path:          /dev/
Document Length:        38 bytes

Concurrency Level:      10
Time taken for tests:   1.906 seconds
Complete requests:      10
Failed requests:        0
Total transferred:      2000 bytes
HTML transferred:       380 bytes
Requests per second:    5.25 [#/sec] (mean)
Time per request:       1906.476 [ms] (mean)
Time per request:       190.648 [ms] (mean, across all concurrent requests)
Transfer rate:          1.02 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:       53   60   2.4     61      62
Processing:   208  921 434.3    999    1584
Waiting:      207  921 434.5    999    1584
Total:        261  981 435.8   1059    1646

Percentage of the requests served within a certain time (ms)
  50%   1059
  66%   1199
  75%   1309
  80%   1449
  90%   1646
  95%   1646
  98%   1646
  99%   1646
 100%   1646 (longest request)

成功したようです。リクエスト数を増やしてみます。

ab -n 50 -c 10 -m POST https://xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/dev/
This is ApacheBench, Version 2.3 <$Revision: 1903618 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/

Benchmarking xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com (be patient).....done


Server Software:        
Server Hostname:        xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com
Server Port:            443
SSL/TLS Protocol:       TLSv1.2,ECDHE-RSA-AES128-GCM-SHA256,2048,128
Server Temp Key:        ECDH X25519 253 bits
TLS Server Name:        xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com

Document Path:          /dev/
Document Length:        38 bytes

Concurrency Level:      10
Time taken for tests:   10.750 seconds
Complete requests:      50
Failed requests:        39
   (Connect: 0, Receive: 0, Length: 39, Exceptions: 0)
Total transferred:      10039 bytes
HTML transferred:       1939 bytes
Requests per second:    4.65 [#/sec] (mean)
Time per request:       2149.955 [ms] (mean)
Time per request:       214.995 [ms] (mean, across all concurrent requests)
Transfer rate:          0.91 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:       24   37  14.0     32      73
Processing:   190 1943 2425.9   1000   10010
Waiting:      188 1942 2426.0   1000   10010
Total:        218 1980 2430.4   1026   10078

Percentage of the requests served within a certain time (ms)
  50%   1026
  66%   1379
  75%   2101
  80%   2448
  90%   5966
  95%   8904
  98%  10078
  99%  10078
 100%  10078 (longest request)

39件のリクエストに失敗したようです。
SQLiteファイルが破損していれば失敗するはずのGETリクエストをcurlで送ってみます。

$ curl -X GET https://xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/dev/
{"id": 138, "content": "Hello, World!"}

正常に動いているようです。少なくとも、SQLiteファイルが破損していることはなさそうです。
では、一体どのようなエラーが発生しているのでしょうか?
CloudWatchのログを確認してみます。

sqlite3.OperationalError: database is locked というエラーが表示されていることから、SQLiteがロックされて書き込めていないことが確認できます。

クラウドウォッチにdatabase is lockedエラーが表示されている

※ API Gatewayが503を返すケースもあります。

EC2でトランザクション中にLambdaへリクエスト

下記手順で行います。

  1. EC2にSSH
  2. EC2にEFSをマウント
  3. sqlite3コマンドでEFS上のSQLiteファイルにアクセス
  4. 任意のトランザクション処理を行う

なお、SQLiteのトランザクションに関しては下記ページを参照させていただきました。

https://kurokawh.blogspot.com/2013/11/sqlite-sqlite.html

GET

下記について確認します。

  • 書き込みロック中にREADできるか
    • SHAREDロック
    • RESERVEDロック
  • 読み込みロック中にREADが妨げられるか
    • EXCLUSIVEロック
書き込みロック中のREAD
  1. BEGIN DEFERRED;後にSELECT文を発行し、SHAREDロックを取った後GETリクエストを行います。
$ curl -X GET https://xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/dev/
{"id": 282, "content": "Hello, World!"}
  1. BEGIN IMMEDIATE; でRESERVEDロックを取った後curlでGETリクエストを行います。
$ curl -X GET https://xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/dev/
{"id": 282, "content": "Hello, World!"}

問題なく読み込めます。

読み込みロック中のREAD

BEGIN EXCLUSIVE; でEXCLUSIVEロックを取った後curlでGETリクエストを行います。

$ curl -X GET https://xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/dev/
{"error": "Failed to fetch data", "message": "database is locked"}

読み込みがロックにより阻まれました。

POST

下記について確認します。

  • 書き込みロック中にWRITEが妨げられるか
    • SHAREDロック
    • RESERVEDロック
    • EXCLUSIVEロック
  1. BEGIN DEFERRED;後にSELECT文を発行し、SHAREDロックを取った後POSTリクエストを行います。
$ curl -X POST https://xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/dev/
{"error": "Failed to insert data", "message": "database is locked"}
  1. BEGIN IMMEDIATE; でRESERVEDロックを取った後curlでGETリクエストを行います。
$ curl -X POST https://xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/dev/
{"error": "Failed to insert data", "message": "database is locked"}
  1. BEGIN EXCLUSIVE;でEXCLUSIVEロックを取った後curlでGETリクエストを行います。
$ curl -X POST https://xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/dev/
{"error": "Failed to insert data", "message": "database is locked"}

いずれもロックにより書き込めないようです。

結果

EFS上のSQLiteファイルには、通常のSQLiteと同様にロックを取って読み書きできるようです。

考察

ではなぜNFS上のSQLiteではロックが取れず、書き込みが失敗するという意見があるのでしょうか?

もちろん公式ドキュメントにもありますし、実際にそのような例もあるようです。

https://techblog.raccoon.ne.jp/archives/1635140633.html

上記のページではNFSのマウントにNFSv3を使用していますが、EFSではNFSv4.0をサポートしています。

https://docs.aws.amazon.com/ja_jp/efs/latest/ug/whatisefs.html

NFSv4ではロック周りの処理が改善されているため、問題が生じなかったものと考えられます。

まとめ

簡易的なSQLite on EFSの検証を通じて、NFSプロトコルについて少し詳しくなることができました。

ご自身の環境でこちらの構成を扱われる際は、要件にあった性能検証を行った上での導入をおすすめいたします。

Discussion