Upgrade to PRO for Only $50/Year—Limited-Time Offer! 🔥

しくじり先生 - NFS+sqliteで苦労した話から学ぶ、問題解決の考え方 / proble...

forrep
October 25, 2021

しくじり先生 - NFS+sqliteで苦労した話から学ぶ、問題解決の考え方 / problem-solving approach

forrep

October 25, 2021
Tweet

More Decks by forrep

Other Decks in Programming

Transcript

  1. そのマイクロサービスのシステム構成 アプリサーバは2台のVMで構成、ロードバランサーで振り分け データストアとして sqlite を採用している アプリサーバの ノード間でデータストアを共有しない 各ノードがローカルファイルシステムに sqlite DBファイルを持つ

    sqlite DBの更新頻度は1日1回、更新後に各ノードへsqliteファイルを配布する クリティカルなデータではないため緩い管理でOKだった(破損しても再作成が可能) しくじり先生 「NFS+sqliteで苦労した話から学ぶ、問題解決の考え方」 3
  2. 発生した問題(この問題を解決する) アプリ用コンテナ x2、バッチ用コンテナ x1 から共有フォルダをNFSでマウント 共有フォルダに sqlite DBファイルを配置 この状態で 複数のサーバから同時に

    sqlite DBファイルを更新をするとデータ構造が壊れて しまいました。 ちなみに sqlite側のNFSでの運用に対するスタンスは以下の通り。 Locking mechanism might not work correctly if the database file is kept on an NFS filesystem. This is because fcntl() file locking is broken on many NFS implementations. You should avoid putting SQLite database files on NFS if multiple processes might try to access the file at the same time. ⇒ NFSではロック機構が正しく動かない可能性があります。これは fcntl() のファイルロックが多くのNFSの実装で壊 れているからです。もし複数のプロセスから同時にデータベースファイルへアクセスするなら、NFSにデータベースフ ァイルを配置するのは避けるべきです。 しくじり先生 「NFS+sqliteで苦労した話から学ぶ、問題解決の考え方」 5
  3. なぜ sqlite を採用していたのか?(Docker化の以前の話) 保持するデータがシンプルで更新頻度が低くパフォーマンス要件も緩かった 複雑・厳密なデータ構造やパフォーマンスの担保が必要ならば MySQL を選択していた可能性 必要ないのに MySQL などを利用すると管理コストがかかる

    sqlite はDBファイル1つを管理するだけで済む シンプルなデータ構造ならテキストファイル等に保存すれば良かったのでは? シンプルなデータ構造でもORマッパー相当のコーディングを自力で行うのは望ましくない sqlite は SQL を介してデータを取得できるため、ORマッパーが対応していることが多い ORマッパー導入済みなら必要に応じて MySQL などへ置き換えしやすい 1カラムに20MB超のテキストをいくつか保持する要件があった 1カラムに巨大テキストを保存するのは MySQL の得意分野ではない、使い方としてしっくりこない感 今後の拡張で200MBなどに巨大化する可能性もあり MySQL を選定しづらい状況 以上から 今後のあらゆる選択肢に対して最も動きやすい一歩として sqlite を採用 しくじり先生 「NFS+sqliteで苦労した話から学ぶ、問題解決の考え方」 6
  4. データストアを共有するにあたっての選択肢 Docker化によって データストアの共有 が必要になったため、以下の選択肢から選定することとしました。 MySQL, PostgreSQL などの RDBMS を利用する NFS

    で sqlite DBファイルを共有する 最終的には後者の NFS+sqlite を選択することになりますが、 この選定を行うには sqlite と他の RDBMS の違いを理解する必要があります。 しくじり先生 「NFS+sqliteで苦労した話から学ぶ、問題解決の考え方」 7
  5. sqlite にはデーモンがない sqlite の実態は単なるライブラリにすぎない(CUIツールも同梱) 例えば Python は sqlite のライブラリを利用した sqlite3

    モジュールから sqlite の機能を利用できる 例えば Java には sqlite のライブラリを内部含んだ sqlite用の JDBCドライバがあって、JDBCドライバ自体に sqlite を扱う実装が含まれる JDBCドライバは本来DBサーバとの通信を中継する役割だが、sqliteはJDBCドライバがDBファイルを更新 アプリのプロセスにDBの処理自体が含まれて、同一プロセス内で sqlite ファイルを直接更新する sqlite はプロセス内の処理が実態なので、DBだけが死ぬということ自体が起きえない(ファイル破損は発生する) しくじり先生 「NFS+sqliteで苦労した話から学ぶ、問題解決の考え方」 10
  6. 単一 sqlite ファイルを複数プロセスから開いてみる まずは sqlite DBファイルを準備します。 sqlite3 test.db "create table

    t ( val varchar )" python, sqlite3 プロセスが開いているファイルディスクリプタを監視します。 watch -tn 1 'bash -c '"'"'while read f; do echo -e "PID:$f $(ps -p $f -o comm=)\n$(ls -l /proc/$f/fd)\n$(ls /proc/$f/fdinfo |tail -n+4 \ |while read fd; do echo "fd: $fd\n$(tail -n+4 /proc/$f/fdinfo/$fd)"; done)\n\n"; done < <(pgrep python; pgrep sqlite3)'"'" Python と sqlite CUIツールから同じDBファイルを開いてみます。 import sqlite3 conn = sqlite3.connect('test.db') sqlite3 test.db しくじり先生 「NFS+sqliteで苦労した話から学ぶ、問題解決の考え方」 11
  7. ファイルが開かれた様子を確認 ファイルディスクリプタを監視するコンソールの出力内容からそれぞれのプロセスが 3 番で test.db を開く様子を確 認できます。 --PID:22764 python-- total

    0 lrwx------ 1 jun jun 64 Oct 4 11:34 0 -> /dev/pts/25 lrwx------ 1 jun jun 64 Oct 4 11:34 1 -> /dev/pts/25 lrwx------ 1 jun jun 64 Oct 4 11:34 2 -> /dev/pts/25 lrwx------ 1 jun jun 64 Oct 4 11:35 3 -> /home/jun/works/sqlite/test.db fd: 3 --PID:17833 sqlite3-- total 0 lrwx------ 1 jun jun 64 Oct 4 14:00 0 -> /dev/pts/19 lrwx------ 1 jun jun 64 Oct 4 14:00 1 -> /dev/pts/19 lrwx------ 1 jun jun 64 Oct 4 14:00 2 -> /dev/pts/19 lrwx------ 1 jun jun 64 Oct 4 14:00 3 -> /home/jun/works/sqlite/test.db fd: 3 しくじり先生 「NFS+sqliteで苦労した話から学ぶ、問題解決の考え方」 12
  8. sqlite の排他制御はどうやっているのか? 独立した複数プロセスが同じDBファイルを同時に開くことのできる(ということは・・・) ⇒ 各プロセスが好きなタイミングで書き込むとファイルが壊れてしまいます。 単一のデーモンならばプロセス内で排他処理ができますが、それをできない sqlite は 排他制御をOS側に頼っています 。

    実際に試してみましょう。 begin; insert into t values ('a'); トランザクションを開始してレコードを挿入するとファイルがロックされている様子を確認できます。 $ cat /proc/<pid>/fdinfo/<fd> lock: 1: POSIX ADVISORY WRITE 17833 08:10:16995 1073741825 1073741825 lock: 2: POSIX ADVISORY READ 17833 08:10:16995 1073741826 1073742335 しくじり先生 「NFS+sqliteで苦労した話から学ぶ、問題解決の考え方」 13
  9. sqlite の排他制御はOS側の仕組みに頼っている 次に Python 側からレコードを挿入しようとするとデータベースがロックされているのでエラーになります。 cur = conn.cursor() cur.execute('insert into

    t values (?)', ('b',)) Traceback (most recent call last): File "<stdin>", line 1, in <module> sqlite3.OperationalError: database is locked sqlite は排他制御をOS側に頼っていることもあって他のRDBMSのような複雑なロック機構は実装されておらず、更新の 際はデータベース全体の排他ロックを取得します。 常に全体の排他ロックとなるのは更新が多いデータベースではパフォーマンス的な問題になりえます。 一方でOS側の仕組みでロックを取得してため、 プログラム言語を問わず同じ動作ができる というメリットがありま す。 実際に sqlite のCUIツールと Python から同時に更新してみたら正しく排他制御されました。 しくじり先生 「NFS+sqliteで苦労した話から学ぶ、問題解決の考え方」 14
  10. sqlite が使っているOS側の排他制御とは? OS側の排他制御の仕組みは fcntl() と呼ばれています。 sqlite だけの専用の仕組みではなく OS側に用意された汎用的な排他制御機構 なのでプログラムから直接利用すること ができます。新しいコンソールから以下を実行してみます。

    import fcntl fp = open('test.db', 'r+b') fcntl.lockf(fp, fcntl.LOCK_EX) しかし実行しても応答は返ってきません。なぜなら先ほどの sqlite CUIのプロセスがそのファイルに対して排他ロック をまだ保持しているからです。放置していたトランザクションをコミットしてロックを解放してみます。 commit; その瞬間に fcntl.lockf() の応答が返ってきてロックを取得できたはずです。 その状態で別のコンソールから新規レコードを挿入してみると失敗することも確認できます。 insert into t values ('c'); Error: database is locked しくじり先生 「NFS+sqliteで苦労した話から学ぶ、問題解決の考え方」 15
  11. OSの仕組みを利用するとはどういうことか? OSの仕組みを利用する ことは一般的に システムコール と呼ばれる fcntl はファイルに対して排他制御を行うためのシステムコール 起動されたプログラム内のコードだけでは、できることが限られている 単純な計算処理くらいしかできない 単純な計算処理を膨大に行うことはできるので、ビットコインの採掘(の演算部分)は可能

    ファイルを開くことはできない、システムコールでOSに「◦◦ファイルを開いてください」とお願いする (ざっくり表すと) 外界に影響をおよぼす操作はシステムコール で実現する システムコールというのは、プログラムで言うところのOS側に用意された関数 見分け方 「その関数を使えなくても(超上級エンジニアなら)自力でも実装できそう」かどうか。 「絶対に無理」となるのがシステムコール。 open() 関数をシステムコールを使わずに実装することは不可 能。 しくじり先生 「NFS+sqliteで苦労した話から学ぶ、問題解決の考え方」 16
  12. 結果として今回は sqlite を選定する 選定において NFS で sqlite DBファイルを共有する点については懸念を感じつつも NFS側の進化もあるはずで、ロックは 設定すれば実現できると考えました。(これ自体は間違っていない)

    その結果、消極的に sqlite を選定しましたが、のちにこれが原因で苦労することになります。 以降はおおよそ問題の発生後の時系列でスライドが進行します。 しくじり先生 「NFS+sqliteで苦労した話から学ぶ、問題解決の考え方」 18
  13. NFSのマウントオプションを確認 マウントされたファイルシステムはマウントオプション次第で動きが変わります。 /proc/mounts からマウント状況を確認します。 $ cat /proc/mounts ... :<mount_from> <mount_to>

    nfs rw,sync,noatime,vers=3,rsize=32768,wsize=32768,namlen=255,acregmin=0,acregmax=0,acdirmin=0,acdirmax=0,soft,nolock, proto=tcp,timeo=30,retrans=6,sec=null,mountaddr=10.100.5.212,mountvers=3,mountproto=tcp,local_lock=all,addr=<ip> 0 0 ... vers=3 で nolock が指定されていると分かります。 しくじり先生 「NFS+sqliteで苦労した話から学ぶ、問題解決の考え方」 20
  14. 【余談】知識の精度を高める 現在のマウント情報を取得したい場合に /etc/fstab を参照するのは正しいでしょうか? 大抵はそれでも正しいけどサーバの状況によってはマウント状態が fstab に反映されていない可能性もあります。 誤った情報を元に判断するとすれ違いが累積して、最終的に大きな問題になることがあります。 そうならないためにも、 持っている知識の精度を高めましょう。

    「fstab はマウント状態を表す」は精度が低い状態です。 一方で「fstab はOS起動時にマウントするための設定ファイル」と認識していれば、必ずしもリアルタイムな情報を表 していないことに気づくことができます。 現在のマウント状況をより正しく得るなら /proc/mounts を参照するべきです。 このわずかな精度の差は徐々に積み重なって大きな差となって表れます。 しくじり先生 「NFS+sqliteで苦労した話から学ぶ、問題解決の考え方」 21
  15. 直接の原因が判明 話題を戻して、改めてマウントオプションのドキュメントを確認してみます。 $ man nfs lock / nolock Selects whether

    to use the NLM sideband protocol to lock files on the server. If neither op‐ tion is specified (or if lock is specified), NLM locking is used for this mount point. When using the nolock option, applications can lock files, but such locks provide exclusion only against other applications running on the same client. Remote applications are not affected by these locks. 「nolock でもロック可能だけど同じNFSクライアント上で動作するアプリケーションにのみ作用する」 とのことで す。 これで問題に対する直接の原因が分かりました。 sqlite が NFS 上のファイルを fcntl() でロックすることは可能だけど同一のNFSクライアント(同サーバ)のみに作用 して、別サーバからは同時にロック取得できてしまうことが分かりました。 これでは書き込み要求が競合するとファイルが破損するのは必然です。 しくじり先生 「NFS+sqliteで苦労した話から学ぶ、問題解決の考え方」 23
  16. マウントオプションを lock に変更できるか検討 man nfs をさらに確認すると NFSv3 では NFS とは別に

    NLM(Network Lock Manager) というプロトコルでロックを管理 していて、ロックをする場合はその設定も必要になるとのことです。 そもそもネットワーク越しでのロック状況の管理は難易度が高くなりがちです。なぜなら意図しないシャットダウンが 発生した場合にロックが正常に解放されるのかなどを担保する必要があるためです。 インフラ側と協議の結果、マウントオプションを lock に変更する案は採用しないことになりました。 該当マイクロサービスのためだけに NLM用のデーモンを管理した上でシステムの健全性を担保するくらいならなら、ノ ウハウのある MySQL を利用した方が管理上楽だと考えると「ですよね」としか言えない状況です。 ロック処理が改善されている NFSv4 はというと、利用機器に既知の不具合が報告されているため採用不可能でした。 しくじり先生 「NFS+sqliteで苦労した話から学ぶ、問題解決の考え方」 24
  17. 方針決定 nolock を維持する sqlite DBファイルへの書き込みタイミングをアプリ側で排他制御する 方針決定の理由は以下でした。 依然として MySQL で管理するほどのデータではない 書き込み頻度が少ないためアプリ側で排他制御すれば十分安全

    しかしここで1つ考慮漏れをしていました。 それは、 本番環境は書き込み頻度が低くてもテスト時は過度な同時書き込みテストを実施する ということです。 しくじり先生 「NFS+sqliteで苦労した話から学ぶ、問題解決の考え方」 25
  18. ロックファイルを利用した独自ロックの仕組みを用意 本題とは関係ありませんが、アプリ側で実装したロック機構は以下のルールとしました。 前提: 共有ディレクトリ上にロックディレクトリを用意 1. ロックファイルが存在しないことを確認、すでにある場合はロック失敗 2. ファイル名に100ナノ秒精度の時刻を付与したロックファイルを生成 3. 0.5秒待つ

    4. ロックを確認、ロックディレクトリ内で自然順の先頭で返却されるロックファイルが有効なロックとする 複数ノードが同時にロックファイルを生成しても、0.5秒後に判断して片方のロックが勝つことを担保できる ディレクトリエントリの更新が遅い環境で利用しても0.5秒以内に反映されれば正しく排他制御できる 各ノードの時刻のずれが少なければ、先にロックを取った方が優先される可能性が高い ファイル生成完了から他のNFSクライアントへの反映が0.25秒以上遅延すると正しいロックを取得できないパターンは ありますが、今回の利用用途では十分な精度だと判断しました。 しくじり先生 「NFS+sqliteで苦労した話から学ぶ、問題解決の考え方」 26
  19. 【余談】障害対応では問題の再現条件を重視する 発生する問題によってはその再現が難しい場合もあります。 問題に対処する場合に最も工数をかけるべき部分は 問題の再現 です。 問題への対処フロー 1. 問題の発生(報告を受ける、目視で確認など) 2. 問題の再現

    3. 問題を修正 4. 問題の修正の確認(再現) 問題の報告を受けると つい再現方法の確認をスキップして修正に入ってしまいがち です。 そして修正後に確認してクローズしたら、実は実施したテストが不適切だったというケースはよくあります。 問題へ対処する際には必ず再現方法を確認してから行うようにしましょう。 しくじり先生 「NFS+sqliteで苦労した話から学ぶ、問題解決の考え方」 28
  20. NFS についてさらに調査 Linux NFS-HOWTO のドキュメントを確認して以下の記述から NFS の書き込みが同期化されていないことが原因だと考 えました。書き込み完了後にロックを解放しても、NFSサーバ側へはまだ反映されていないのかもと。 しかし後にこの推測が誤っていたことが発覚、よく読むと意味が違うことを事実と取り違えていました。(後述) Linux

    NFS-HOWTO - 5.9. NFS の同期動作と非同期動作 NFSversion3プロトコルのリクエストでは、ファイルをクローズするときやfsync()の時にNFSクライアントから「後 出し」のcommitリクエストが発行されますが、これによってサーバは以前書き込みを完了していなかったデー タ・メタデータをディスクに書き込むよう強制されます。そしてサーバは、sync動作に従うのであれば、この書き 込みが終了するまでクライアントに応答しません。一方 もしasyncが用いられている場合は、commitは基本的に no-op(何も行なわない動作)です。なぜならサーバは再びクライアントに対して、データは既に永続的なストレー ジに送られた、と嘘をつくからです。 するとクライアントはサーバがデータを永続的なストレージに保存したと信 じて自分のキャッシュを捨ててしまうので、これはやはりクライアントとサーバをデータ破壊の危険に晒すことに なります。 そこで同期書き込みへの変更をするオプションを調べました。 しくじり先生 「NFS+sqliteで苦労した話から学ぶ、問題解決の考え方」 29
  21. man nfs ac / noac Selects whether the client may

    cache file attributes. If neither option is specified (or if ac is specified), the client caches file attributes. To improve performance, NFS clients cache file attributes. Every few seconds, an NFS client checks the server's version of each file's attributes for updates. Changes that occur on the server in those small intervals remain undetected until the client checks the server again. The noac option prevents clients from caching file attributes so that applications can more quickly detect file changes on the server. In addition to preventing the client from caching file attributes, the noac option forces ap‐ plication writes to become synchronous so that local changes to a file become visible on the server immediately. That way, other clients can quickly detect recent writes when they check the file's attributes. Using the noac option provides greater cache coherence among NFS clients accessing the same files, but it extracts a significant performance penalty. As such, judicious use of file lock‐ ing is encouraged instead. The DATA AND METADATA COHERENCE section contains a detailed discus‐ sion of these trade-offs. The noac option is a combination of the generic option sync, and the NFS-specific option actimeo=0. しくじり先生 「NFS+sqliteで苦労した話から学ぶ、問題解決の考え方」 30
  22. man nfs acregmin=n The minimum time (in seconds) that the

    NFS client caches attributes of a regular file before it requests fresh attribute information from a server. If this option is not specified, the NFS client uses a 3-second minimum. See the DATA AND METADATA COHERENCE section for a full discussion of attribute caching. acregmax=n The maximum time (in seconds) that the NFS client caches attributes of a regular file before it requests fresh attribute information from a server. If this option is not specified, the NFS client uses a 60-second maximum. See the DATA AND METADATA COHERENCE section for a full discussion of attribute caching. acdirmin=n The minimum time (in seconds) that the NFS client caches attributes of a directory before it requests fresh at‐ tribute information from a server. If this option is not specified, the NFS client uses a 30-second minimum. See the DATA AND METADATA COHERENCE section for a full discussion of attribute caching. acdirmax=n The maximum time (in seconds) that the NFS client caches attributes of a directory before it requests fresh at‐ tribute information from a server. If this option is not specified, the NFS client uses a 60-second maximum. See the DATA AND METADATA COHERENCE section for a full discussion of attribute caching. actimeo=n Using actimeo sets all of acregmin, acregmax, acdirmin, and acdirmax to the same value. If this option is not specified, the NFS client uses the defaults for each of these options listed above. しくじり先生 「NFS+sqliteで苦労した話から学ぶ、問題解決の考え方」 31
  23. man mount async All I/O to the filesystem should be

    done asynchronously. (See also the sync option.) sync All I/O to the filesystem should be done synchronously. In the case of media with a limited number of write cycles (e.g. some flash drives), sync may cause life-cycle shortening. nfs 固有のマウントオプションと mount 全体の汎用オプションで参照するドキュメントが違います。 sync, async は mount 側のドキュメントに記載があります。 しくじり先生 「NFS+sqliteで苦労した話から学ぶ、問題解決の考え方」 32
  24. nfs のマウントオプションに noac を追加 ドキュメントによると noac は sync と actimeo=0

    を指定した効果をもつようなので同期書き込みする上にキャッシュも 無効になるので問題の解決になるはずと考えました。 しかしドキュメントの読み込みと理解が甘く、各オプションの動作原理をしっかり把握せずに利用していました。 マウントオプションの変更後に再度テストするとファイル破損は発生しなくなりました。 このように認識違いのまま偶然解決してしまうケースは後からより大きな問題となることもあるので要注意です。ここ に至るまでに本当の問題に気づけるチャンスはあったので反省点です。 この時点では問題が起きなくなったので 本人の認識としてはクローズした案件 となりました。 しくじり先生 「NFS+sqliteで苦労した話から学ぶ、問題解決の考え方」 33
  25. インフラチームから不穏な情報共有あり 「tcpdump で分析したところマウント時の sync, async で動作に多少の違いはあるものの、どちらでも妥当なタイミ ングで FILE_SYNC フラグ付きのパケットがNFSサーバに送信されているようです」 RFC

    1813 - NFS Version 3 Protocol If stable is FILE_SYNC, the server must commit the data written plus all file system metadata to stable storage before returning results. NFSv3 の仕様を規定した RFC1813 には FILE_SYNC フラグが付いていると応答前にディスクへの永続化を保証すること が記載されています。 NFS のように Linux やら Solaris やら AIX などさまざまな環境で実装された機能の仕様を調べるには RFC が役に立ちま す。 (注) RFC は策定された仕様というだけなので各プラットフォームで RFC 通り実装されている保証はありません。 しくじり先生 「NFS+sqliteで苦労した話から学ぶ、問題解決の考え方」 34
  26. 問題が発生しうる場所を改めて整理 システムは様々な仕組みの上で動いているので、その仕組みがそれぞれレイヤとなって表れます。 今回の問題において、開発者から近い順に挙げると以下のレイヤに分けて考えることができます。 1. 開発したプログラムの問題 2. (開発したプログラムを動作させる)Pythonインタプリタの問題 3. (Pythonインタプリタが内部で利用する)sqliteライブラリの問題 4.

    (sqliteライブラリがシステムコール経由で呼び出す)Linuxカーネルの問題 5. (Linuxカーネルがシステムコール内で利用する)NFSクライアントの問題 6. (NFSクライアントがネットワーク経由でコールする)NFSサーバの問題(※ネットワークの問題も含む) ここで重要なのは問題を確認するレイヤがどこなのかを意識しながら対応することです。 1~4 はこれまでの情報から問題の原因である可能性が低いことが分かっています。なぜならばローカルファイルシステ ムでは正常に動いていたからです。 しくじり先生 「NFS+sqliteで苦労した話から学ぶ、問題解決の考え方」 37
  27. 対象レイヤに限定して検証をする 5. (Linuxカーネルがシステムコール内で利用する)NFSクライアントの問題 調査対象を Linux の NFSクライアントに絞れたのでまずは情報収集をします。 クライアント側の問題ということは、 サーバ側では更新されたデータがあるのに依然として古い情報を使ってしまう 状

    況が考えられます。 つまりクライアント側のキャッシュがどのように使われているかを調べることで欲しい情報を得ることができそうで す。 キャッシュの影響で最新情報を取得できない事象はネットワーク経由のやりとりでは当たり前にあることですが、NFS が普通のストレージ然として振る舞うのでつい忘れてしまっていたことに気づかされました。 しくじり先生 「NFS+sqliteで苦労した話から学ぶ、問題解決の考え方」 39
  28. NFSクライアントの情報収集 (man nfs) DATA AND METADATA COHERENCE Some modern cluster

    file systems provide perfect cache coherence among their clients. Perfect cache coherence among disparate NFS clients is expensive to achieve, especially on wide area networks. As such, NFS settles for weaker cache coherence that satisfies the requirements of most file sharing types. Close-to-open cache consistency Typically file sharing is completely sequential. First client A opens a file, writes something to it, then closes it. Then client B opens the same file, and reads the changes. When an application opens a file stored on an NFS version 3 server, the NFS client checks that the file exists on the server and is permitted to the opener by sending a GETATTR or ACCESS request. The NFS client sends these requests regardless of the freshness of the file's cached attributes. When the application closes the file, the NFS client writes back any pending changes to the file so that the next opener can view the changes. This also gives the NFS client an opportunity to report write errors to the application via the return code from close(2). The behavior of checking at open time and flushing at close time is referred to as close-to-open cache consistency, or CTO. It can be dis‐ abled for an entire mount point using the nocto mount option. Weak cache consistency There are still opportunities for a client's data cache to contain stale data. The NFS version 3 protocol introduced "weak cache consistency" (also known as WCC) which provides a way of efficiently checking a file's attributes before and after a single request. This allows a client to help identify changes that could have been made by other clients. When a client is using many concurrent operations that update the same file at the same time (for example, during asynchronous write behind), it is still difficult to tell whether it was that client's updates or some other client's updates that altered the file. Attribute caching Use the noac mount option to achieve attribute cache coherence among multiple clients. Almost every file system operation checks file attri‐ bute information. The client keeps this information cached for a period of time to reduce network and server load. When noac is in effect, a client's file attribute cache is disabled, so each operation that needs to check a file's attributes is forced to go back to the server. This permits a client to see changes to a file very quickly, at the cost of many extra network operations. Be careful not to confuse the noac option with "no data caching." The noac mount option prevents the client from caching file metadata, but there are still races that may result in data cache incoherence between client and server. The NFS protocol is not designed to support true cluster file system cache coherence without some type of application serialization. If abso‐ lute cache coherence among clients is required, applications should use file locking. Alternatively, applications can also open their files with the O_DIRECT flag to disable data caching entirely. しくじり先生 「NFS+sqliteで苦労した話から学ぶ、問題解決の考え方」 40
  29. ようやく問題の全貌が分かる DATA AND METADATA COHERENCE Some modern cluster file systems

    provide perfect cache coherence among their clients. Perfect cache coherence among disparate NFS clients is expensive to achieve, especially on wide area networks. As such, NFS settles for weaker cache coherence that satisfies the requirements of most file sharing types. ドキュメントから NFS は 大抵の用途に適合する弱いキャッシュの一貫性 を採用しているとのことが分かりました。 DATA AND METADATA COHERENCE とは DATA ⇒ ファイル内容のキャッシュ一貫性 METADATA ⇒ ファイルやディレクトリの属性情報のキャッシュ一貫性 しくじり先生 「NFS+sqliteで苦労した話から学ぶ、問題解決の考え方」 41
  30. Close-to-open cache consistency A8. What is close-to-open cache consistency? にも別の表現で解説があったので参照しました。

    Linux implements close-to-open cache consistency by comparing the results of a GETATTR operation done just after the file is closed to the results of a GETATTR operation done when the file is next opened. If the results are the same, the client will assume its data cache is still valid; otherwise, the cache is purged. 大抵の利用用途がクライアントAがファイルを開いて書き込んで閉じ、次にクライアントBが開いて書き込んで閉じ るというシーケンシャルな流れであるという想定 前回ファイルを閉じた際に取得したタイムスタンプと、次回開く際のタイムスタンプを比較して違いがあればファ イル内容のキャッシュを破棄する 逆に言うとファイルを開き直さないとファイル属性の強制取得は行われないのでキャッシュは破棄されません。 ドキュメントに出てくる GETATTR は RFC で定義される NFS サーバからファイル・ディレクトリの情報を取得するプロ シージャです。 しくじり先生 「NFS+sqliteで苦労した話から学ぶ、問題解決の考え方」 42
  31. Weak cache consistency There are still opportunities for a client's

    data cache to contain stale data. The NFS version 3 protocol introduced "weak cache consistency" (also known as WCC) which provides a way of efficiently checking a file's attributes before and after a single request. This allows a client to help identify changes that could have been made by other clients. When a client is using many concurrent operations that update the same file at the same time (for example, during asynchronous write behind), it is still difficult to tell whether it was that client's updates or some other client's updates that altered the file. Close-to-open だけではファイルを開いた後に更新されたデータを取得できないので Weak cache consistency という機 能もあるようです。 各オペレーションの前にファイル属性を取得(GETATTR)して認識していない属性情報の更新があれば他のクライアント から更新されたと判断してキャッシュを破棄するという動作のようです。 しくじり先生 「NFS+sqliteで苦労した話から学ぶ、問題解決の考え方」 43
  32. 古いキャッシュが利用された原因を考える 問題のアプリはファイルを開くタイミングをロックに含めていませんでした。 1. sqlite DBファイルを開く 2. ロックをする 3. 書き込む 4.

    ロックを解放する 5. sqlite DBファイルを閉じる ファイルを開いた後、ロックを取得する前に他のクライアントから書き込みがあるとキャッシュが破棄されず古いデー タを使ってしまいそうです。 さらに sqlite は後述しますがファイルを閉じる動作に癖があるので、利用する側が厳密にファイルの開閉を制御するこ とが難しい状況もあります。 しくじり先生 「NFS+sqliteで苦労した話から学ぶ、問題解決の考え方」 44
  33. 古いキャッシュが利用された原因を考える Weak cache consistency は各オペレーションの前にファイル属性を取得して更新を検出してくれるので、それならば問 題の発生を防ぐことができそうです。 しかしここで問題になるのがファイル属性のキャッシュです。 ファイル属性の取得にはキャッシュが効くので一定時間内の再取得は古い属性情報のままとなり、結果として「ファイ ルは更新されていない」と判断されてしまいます。 ここまで調べると

    noac で問題が解決した理由が分かります。 noac には actimeo=0 を暗黙で含むため属性キャッシュが無効化されます。 都度サーバーから GETATTR で属性情報を取得することで最新の更新を検出可能となり、キャッシュの破棄ができるよう になったようです。 しくじり先生 「NFS+sqliteで苦労した話から学ぶ、問題解決の考え方」 45
  34. 検証方法を考える 原因の仮説ができたので次は実際にそれが合っているか検証する必要があります。 3. (Pythonインタプリタが内部で利用する)sqliteライブラリの問題 4. (sqliteライブラリがシステムコール経由で呼び出す)Linuxカーネルの問題 5. (Linuxカーネルがシステムコール内で利用する)NFSクライアントの問題 検証したいのは 5

    レイヤなので、可能な限り対象レイアのみ切り出して検証する方法を考えます。 sqlite がファイルの読み書きに利用するのはシステムコールなので、sqlite が使うのと同じシステムコールを利用すれば sqlite を利用せずに 5 レイヤだけを検証可能なはずです。 しくじり先生 「NFS+sqliteで苦労した話から学ぶ、問題解決の考え方」 46
  35. sqlite の動作を確認する システムコールは「OS側へのお願い」なので、OS側から観測する方法がいくつも用意されています。 例えば現在プログラムが呼び出しているシステムコールをリアルタイムで観測する strace コマンドがあります。 strace -p <pid> Python

    のプロセスに対して strace を実行した状態で sqlite の操作を一通りやってみたところ、DBファイルのオープン から読み書きクローズまで想定通りの動作を確認できました。 例えば sqlite DBファイルのオープンをしたら以下のようなシステムコールが実行されます。 conn = sqlite3.connect('test.db') # Python コード openat(AT_FDCWD, "/home/jun/works/sqlite/test.db", O_RDWR|O_CREAT|O_CLOEXEC, 0644) = 3 # strace の出力 しくじり先生 「NFS+sqliteで苦労した話から学ぶ、問題解決の考え方」 47
  36. 検証する 2台のPCから同じNFS領域をマウントして検証します。同じPCから試すとローカルPC内では fcntl が効いてしまうので別 環境から接続します。 クライアントA 1. ファイルAをオープン 2. ファイルAを読み込んでsha256ハッシュ取得

    3. ファイルAを先頭にシーク 4. 2に戻る クライアントA はファイルを開いたまま、先頭までシークと読み込みとハッシュ計算を繰り返します。 しくじり先生 「NFS+sqliteで苦労した話から学ぶ、問題解決の考え方」 48
  37. 検証結果 想定通りクライアントBが書き込み完了してから数秒経過後にクライアントAの結果へ反映されるケースが散見されまし た。これでは排他制御してもファイルが破損するのは納得です。 またクライアントA側の処理方法を「ファイルを開きっぱなしで都度先頭にシーク」から「都度開いて閉じる」に変更 したら想定通りリアルタイムで最新データを取得できるようになりました。 もしくは属性キャッシュを無効化する noac を付けても問題は発生しなくなりました。 ... 0044:

    a6c283ae87881fa14788baaadc8df1854daacd140eb71274ca8c997d8ddaf8e4 0045: a6c283ae87881fa14788baaadc8df1854daacd140eb71274ca8c997d8ddaf8e4 0046: a6c283ae87881fa14788baaadc8df1854daacd140eb71274ca8c997d8ddaf8e4 0047: a6c283ae87881fa14788baaadc8df1854daacd140eb71274ca8c997d8ddaf8e4 0048: a6c283ae87881fa14788baaadc8df1854daacd140eb71274ca8c997d8ddaf8e4 0049: a6c283ae87881fa14788baaadc8df1854daacd140eb71274ca8c997d8ddaf8e4 0050: a6c283ae87881fa14788baaadc8df1854daacd140eb71274ca8c997d8ddaf8e4 0051: a6c283ae87881fa14788baaadc8df1854daacd140eb71274ca8c997d8ddaf8e4 0052: a6c283ae87881fa14788baaadc8df1854daacd140eb71274ca8c997d8ddaf8e4 0053: b05dcdb7adb24011b3fcb776eb74df7e90507c5a6527b2fdd00fc2edd0993e49 ← 更新してから数秒後にハッシュが変化 0054: b05dcdb7adb24011b3fcb776eb74df7e90507c5a6527b2fdd00fc2edd0993e49 しくじり先生 「NFS+sqliteで苦労した話から学ぶ、問題解決の考え方」 50
  38. クライアントA(ファイルを開いたままsha256ハッシュを計算し続ける) 詳しい説明は省略しますが検証に利用したコードを掲載します。 import hashlib import time fp = open('test.db', 'rb')

    def sha256hash1(fp): fp.seek(0) return hashlib.sha256(fp.read()).hexdigest() for i in range(1000): print('{:04}: {}'.format(i, sha256hash1(fp))) time.sleep(0.1) fp.close() しくじり先生 「NFS+sqliteで苦労した話から学ぶ、問題解決の考え方」 51
  39. クライアントB(任意タイミングでランダムデータを書き込む) import random def random_write(): fp = open('test.db', 'w') fp.write(str(random.random()))

    fp.close() random_write() しくじり先生 「NFS+sqliteで苦労した話から学ぶ、問題解決の考え方」 52
  40. クライアントA'(都度ファイルを開いて閉じながらハッシュ計算) import hashlib import time def sha256hash2(): fp = open('test.db',

    'rb') return hashlib.sha256(fp.read()).hexdigest() fp.close() for i in range(1000): print('{:04}: {}'.format(i, sha256hash2())) time.sleep(0.1) しくじり先生 「NFS+sqliteで苦労した話から学ぶ、問題解決の考え方」 53
  41. 【余談】オープンソースの中身は楽しい /****************************************************************************** *************************** Posix Advisory Locking **************************** ** ** POSIX

    advisory locks are broken by design. ANSI STD 1003.1 (1996) ** section 6.5.2.2 lines 483 through 490 specify that when a process ** sets or clears a lock, that operation overrides any prior locks set ... ** ** This means that we cannot use POSIX locks to synchronize file access ** among competing threads of the same process. POSIX locks will work fine ** to synchronize access for threads in separate processes, but not ** threads within the same process. ** ... ** ** But wait: there are yet more problems with POSIX advisory locks. ** ... sqliteのソースコードの中にはPOSIXのロック機構に対する愚痴が90行近くコメントで書かれていたりします。 例えば同じファイルを2回開いた状態で片方をクローズすると、そのファイルに対するロックを失います。それ故に sqlite はファイルの開閉で少し特殊な処理が必要になっています。 しくじり先生 「NFS+sqliteで苦労した話から学ぶ、問題解決の考え方」 55