Upgrade to Pro — share decks privately, control downloads, hide ads and more …

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

forrep
October 25, 2021

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

forrep

October 25, 2021
Tweet

More Decks by forrep

Other Decks in Programming

Transcript

  1. しくじり先生
    ~ NFS+sqliteで苦労した話から学ぶ、問題解決の考え方 ~
    2021年10月

    ラクーンホールディングス 羽山 純
    しくじり先生 「NFS+sqliteで苦労した話から学ぶ、問題解決の考え方」 1

    View full-size slide

  2. なぜ、「しくじり先生」?
    問題解決の過程はエンジニアごとに個性が表れやすかったり、ノウハウの塊だったりと少なからず参考になることがあ
    るはずです。
    一方で他のエンジニアがどのように問題解決しているのかは共有される機会が少ない状況にあります。
    そこで、とあるマイクロサービスを Docker化する案件で発生した問題から、

    それを 解決していく過程を一緒になぞる ことで問題解決の過程を疑似体験してもらいます。
    しくじり先生 「NFS+sqliteで苦労した話から学ぶ、問題解決の考え方」 2

    View full-size slide

  3. そのマイクロサービスのシステム構成
    アプリサーバは2台のVMで構成、ロードバランサーで振り分け
    データストアとして sqlite を採用している
    アプリサーバの ノード間でデータストアを共有しない
    各ノードがローカルファイルシステムに sqlite DBファイルを持つ
    sqlite DBの更新頻度は1日1回、更新後に各ノードへsqliteファイルを配布する
    クリティカルなデータではないため緩い管理でOKだった(破損しても再作成が可能)
    しくじり先生 「NFS+sqliteで苦労した話から学ぶ、問題解決の考え方」 3

    View full-size slide

  4. Docker化するために必要なだったこと
    データストアを複数の Docker コンテナから共有すること
    (良い悪いは別として)今までは 各サーバのローカルストレージは永続化可能なストレージとして利用可能 でしたが、
    Docker化するとコンテナ内にデータを永続化するわけにはいきません。

    そこでなんらかの共有可能なデータストアが必要になります。
    しくじり先生 「NFS+sqliteで苦労した話から学ぶ、問題解決の考え方」 4

    View full-size slide

  5. 発生した問題(この問題を解決する)
    アプリ用コンテナ 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

    View full-size slide

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

    View full-size slide

  7. データストアを共有するにあたっての選択肢
    Docker化によって データストアの共有 が必要になったため、以下の選択肢から選定することとしました。
    MySQL, PostgreSQL などの RDBMS を利用する
    NFS で sqlite DBファイルを共有する
    最終的には後者の NFS+sqlite を選択することになりますが、

    この選定を行うには sqlite と他の RDBMS の違いを理解する必要があります。
    しくじり先生 「NFS+sqliteで苦労した話から学ぶ、問題解決の考え方」 7

    View full-size slide

  8. sqlite と他の RDBMS(MySQL等)の違いとは?
    アーキテクチャ面から両者の違いを説明できるでしょうか?

    一見すると似ている両者ですが動作原理は大きく異なります。
    イモリとヤモリは似ているけど両生類と爬虫類でそもそもの成り立ちに違いがあります。

    コーヒーフレッシュと牛乳、モロッコヨーグルトとヨーグルト、ラクトアイスとアイスクリームくらいの違いがありま
    す。
    それくらいに両者の動作原理は異なります。
    しくじり先生 「NFS+sqliteで苦労した話から学ぶ、問題解決の考え方」 8

    View full-size slide

  9. MySQLはデーモンが集中管理する
    MySQL はサーバ上でデーモンとして起動して、利用するアプリは通信でDBデーモンとやりとりをする
    DBとアプリのプロセスは別になる
    アプリ側はDBデーモンにデータの読み込み・更新を依頼するだけ
    単一のデーモンがデータファイルを読み込み・更新を集中管理するのでデータ整合性の保証が容易
    複数アプリから同時に接続しても、処理を担当するのは単一デーモンなので内部で排他制御ができる
    維持管理コストは高い、サーバの用意からデーモンの死活監視やデータのバックアップなど一連のコストが必要
    DBだけ死ぬ状況が発生しうる
    DBが死ぬと大抵はアプリもサービスを提供できなくなる
    しくじり先生 「NFS+sqliteで苦労した話から学ぶ、問題解決の考え方」 9

    View full-size slide

  10. 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

    View full-size slide

  11. 単一 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

    View full-size slide

  12. ファイルが開かれた様子を確認
    ファイルディスクリプタを監視するコンソールの出力内容からそれぞれのプロセスが 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

    View full-size slide

  13. sqlite の排他制御はどうやっているのか?
    独立した複数プロセスが同じDBファイルを同時に開くことのできる(ということは・・・)

    ⇒ 各プロセスが好きなタイミングで書き込むとファイルが壊れてしまいます。
    単一のデーモンならばプロセス内で排他処理ができますが、それをできない sqlite は 排他制御をOS側に頼っています

    実際に試してみましょう。
    begin;

    insert into t values ('a');

    トランザクションを開始してレコードを挿入するとファイルがロックされている様子を確認できます。
    $ cat /proc//fdinfo/

    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

    View full-size slide

  14. sqlite の排他制御はOS側の仕組みに頼っている
    次に Python 側からレコードを挿入しようとするとデータベースがロックされているのでエラーになります。
    cur = conn.cursor()

    cur.execute('insert into t values (?)', ('b',))

    Traceback (most recent call last):

    File "", line 1, in

    sqlite3.OperationalError: database is locked

    sqlite は排他制御をOS側に頼っていることもあって他のRDBMSのような複雑なロック機構は実装されておらず、更新の
    際はデータベース全体の排他ロックを取得します。
    常に全体の排他ロックとなるのは更新が多いデータベースではパフォーマンス的な問題になりえます。

    一方でOS側の仕組みでロックを取得してため、 プログラム言語を問わず同じ動作ができる というメリットがありま
    す。
    実際に sqlite のCUIツールと Python から同時に更新してみたら正しく排他制御されました。
    しくじり先生 「NFS+sqliteで苦労した話から学ぶ、問題解決の考え方」 14

    View full-size slide

  15. 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

    View full-size slide

  16. OSの仕組みを利用するとはどういうことか?
    OSの仕組みを利用する ことは一般的に システムコール と呼ばれる
    fcntl はファイルに対して排他制御を行うためのシステムコール
    起動されたプログラム内のコードだけでは、できることが限られている
    単純な計算処理くらいしかできない
    単純な計算処理を膨大に行うことはできるので、ビットコインの採掘(の演算部分)は可能
    ファイルを開くことはできない、システムコールでOSに「○○ファイルを開いてください」とお願いする
    (ざっくり表すと) 外界に影響をおよぼす操作はシステムコール で実現する
    システムコールというのは、プログラムで言うところのOS側に用意された関数
    見分け方
    「その関数を使えなくても(超上級エンジニアなら)自力でも実装できそう」かどうか。
    「絶対に無理」となるのがシステムコール。 open()
    関数をシステムコールを使わずに実装することは不可
    能。
    しくじり先生 「NFS+sqliteで苦労した話から学ぶ、問題解決の考え方」 16

    View full-size slide

  17. システムアーキテクトの役割
    システム設計において高レイヤは問題領域に近いので自然と考慮されます。

    今回の例では「SQLが使えるデータストアが欲しい」「複数ノードで共有できるデータストアが欲しい」などが高レイ
    ヤにあたります。
    これらの要件を満たすのは当然ですが、システムアーキテクトの役割にはそこからさらにレイヤを下って考慮する所に
    もあります。低レイヤまで考慮された技術選定はプロダクトの完成度を高くしてくれます。
    そして選定理由を言語化しましょう。
    ○○のために□□を採用
    特に考慮せず、サイコロを転がした(これでも問題なし)
    全知全能の神ではないのですべてを考慮することは不可能で必ず抜けはあります。その上で考慮されたポイントや理由
    が言語化されていれば、あとからアーキテクチャに修正を入れる場合の参考になります。
    しくじり先生 「NFS+sqliteで苦労した話から学ぶ、問題解決の考え方」 17

    View full-size slide

  18. 結果として今回は sqlite を選定する
    選定において NFS で sqlite DBファイルを共有する点については懸念を感じつつも NFS側の進化もあるはずで、ロックは
    設定すれば実現できると考えました。(これ自体は間違っていない)
    その結果、消極的に sqlite を選定しましたが、のちにこれが原因で苦労することになります。
    以降はおおよそ問題の発生後の時系列でスライドが進行します。
    しくじり先生 「NFS+sqliteで苦労した話から学ぶ、問題解決の考え方」 18

    View full-size slide

  19. DBファイルが破損した問題について、分解して考える
    sqlite は複数プロセスからの更新に対応しているから、sqlite 開発者の想定する使い方をすれば壊れないはずです。

    (※ドキュメントが誤っている可能性も考える必要があるが、高い可能性から検討すべき)
    sqliteは同時更新に対応している
    結果としてNFSサーバ上のファイルが壊れた
    この二つの前提条件から必然的に
    「sqliteは同時更新に対応しているけどなんらかの理由でそれが正しく動いていない」
    という状況が導き出されます。
    さらに冒頭で引用した NFS のドキュメントには NFS における排他制御の問題がまさに指摘されていました。

    元々想定していた問題が発生したという状況なので、「ファイルの排他制御」にあたりを付けて検証をすすめます。
    しくじり先生 「NFS+sqliteで苦労した話から学ぶ、問題解決の考え方」 19

    View full-size slide

  20. NFSのマウントオプションを確認
    マウントされたファイルシステムはマウントオプション次第で動きが変わります。

    /proc/mounts
    からマウント状況を確認します。
    $ cat /proc/mounts

    ...

    : 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= 0 0

    ...

    vers=3
    で nolock
    が指定されていると分かります。
    しくじり先生 「NFS+sqliteで苦労した話から学ぶ、問題解決の考え方」 20

    View full-size slide

  21. 【余談】知識の精度を高める
    現在のマウント情報を取得したい場合に /etc/fstab
    を参照するのは正しいでしょうか?

    大抵はそれでも正しいけどサーバの状況によってはマウント状態が fstab に反映されていない可能性もあります。
    誤った情報を元に判断するとすれ違いが累積して、最終的に大きな問題になることがあります。

    そうならないためにも、 持っている知識の精度を高めましょう。
    「fstab はマウント状態を表す」は精度が低い状態です。

    一方で「fstab はOS起動時にマウントするための設定ファイル」と認識していれば、必ずしもリアルタイムな情報を表
    していないことに気づくことができます。
    現在のマウント状況をより正しく得るなら /proc/mounts
    を参照するべきです。

    このわずかな精度の差は徐々に積み重なって大きな差となって表れます。
    しくじり先生 「NFS+sqliteで苦労した話から学ぶ、問題解決の考え方」 21

    View full-size slide

  22. 【余談】知識の精度はなぜ必要か?
    サイコロは一見すると完全な立方体に見えます。

    しかしサイコロピラミッドを積み上げるとわずかな精度の差があり、それ故に沢山積み上げることが難しいことが分か
    ります。
    システムも同様で知識を元に積み上げていくものです。

    単純なシステムなら問題は起きにくいですが、大規模になって様々な知識を積み重ねると、それぞれの精度の低さが累
    積してシステム全体としては破綻することがあります。
    障害対応の場合は調査精度のわずかな低下が累積した結果、答えにたどり着けないケースもあります。
    知識の精度を高める ことで、サイコロを沢山積み上げる状況でも問題を避けることができます。
    しくじり先生 「NFS+sqliteで苦労した話から学ぶ、問題解決の考え方」 22

    View full-size slide

  23. 直接の原因が判明
    話題を戻して、改めてマウントオプションのドキュメントを確認してみます。
    $ 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

    View full-size slide

  24. マウントオプションを lock に変更できるか検討
    man nfs
    をさらに確認すると NFSv3 では NFS とは別に NLM(Network Lock Manager) というプロトコルでロックを管理
    していて、ロックをする場合はその設定も必要になるとのことです。

    そもそもネットワーク越しでのロック状況の管理は難易度が高くなりがちです。なぜなら意図しないシャットダウンが
    発生した場合にロックが正常に解放されるのかなどを担保する必要があるためです。
    インフラ側と協議の結果、マウントオプションを lock
    に変更する案は採用しないことになりました。
    該当マイクロサービスのためだけに NLM用のデーモンを管理した上でシステムの健全性を担保するくらいならなら、ノ
    ウハウのある MySQL を利用した方が管理上楽だと考えると「ですよね」としか言えない状況です。
    ロック処理が改善されている NFSv4 はというと、利用機器に既知の不具合が報告されているため採用不可能でした。
    しくじり先生 「NFS+sqliteで苦労した話から学ぶ、問題解決の考え方」 24

    View full-size slide

  25. 方針決定
    nolock
    を維持する
    sqlite DBファイルへの書き込みタイミングをアプリ側で排他制御する
    方針決定の理由は以下でした。
    依然として MySQL で管理するほどのデータではない
    書き込み頻度が少ないためアプリ側で排他制御すれば十分安全
    しかしここで1つ考慮漏れをしていました。
    それは、 本番環境は書き込み頻度が低くてもテスト時は過度な同時書き込みテストを実施する ということです。
    しくじり先生 「NFS+sqliteで苦労した話から学ぶ、問題解決の考え方」 25

    View full-size slide

  26. ロックファイルを利用した独自ロックの仕組みを用意
    本題とは関係ありませんが、アプリ側で実装したロック機構は以下のルールとしました。
    前提: 共有ディレクトリ上にロックディレクトリを用意
    1. ロックファイルが存在しないことを確認、すでにある場合はロック失敗
    2. ファイル名に100ナノ秒精度の時刻を付与したロックファイルを生成
    3. 0.5秒待つ
    4. ロックを確認、ロックディレクトリ内で自然順の先頭で返却されるロックファイルが有効なロックとする
    複数ノードが同時にロックファイルを生成しても、0.5秒後に判断して片方のロックが勝つことを担保できる
    ディレクトリエントリの更新が遅い環境で利用しても0.5秒以内に反映されれば正しく排他制御できる
    各ノードの時刻のずれが少なければ、先にロックを取った方が優先される可能性が高い
    ファイル生成完了から他のNFSクライアントへの反映が0.25秒以上遅延すると正しいロックを取得できないパターンは
    ありますが、今回の利用用途では十分な精度だと判断しました。
    しくじり先生 「NFS+sqliteで苦労した話から学ぶ、問題解決の考え方」 26

    View full-size slide

  27. テストの実施、しかしうまくいかず
    アプリ側とバッチの複数並行稼働で負荷をかけたところ、残念ながら sqlite DBファイルが壊れる現象が再発しました。
    これは想定外だったのでさらにレイヤを下げて原因を探ることにします。
    しくじり先生 「NFS+sqliteで苦労した話から学ぶ、問題解決の考え方」 27

    View full-size slide

  28. 【余談】障害対応では問題の再現条件を重視する
    発生する問題によってはその再現が難しい場合もあります。

    問題に対処する場合に最も工数をかけるべき部分は 問題の再現 です。
    問題への対処フロー
    1. 問題の発生(報告を受ける、目視で確認など)
    2. 問題の再現
    3. 問題を修正
    4. 問題の修正の確認(再現)
    問題の報告を受けると つい再現方法の確認をスキップして修正に入ってしまいがち です。

    そして修正後に確認してクローズしたら、実は実施したテストが不適切だったというケースはよくあります。
    問題へ対処する際には必ず再現方法を確認してから行うようにしましょう。
    しくじり先生 「NFS+sqliteで苦労した話から学ぶ、問題解決の考え方」 28

    View full-size slide

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

    View full-size slide

  30. 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

    View full-size slide

  31. 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

    View full-size slide

  32. 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

    View full-size slide

  33. nfs のマウントオプションに noac を追加
    ドキュメントによると noac は sync と actimeo=0 を指定した効果をもつようなので同期書き込みする上にキャッシュも
    無効になるので問題の解決になるはずと考えました。

    しかしドキュメントの読み込みと理解が甘く、各オプションの動作原理をしっかり把握せずに利用していました。
    マウントオプションの変更後に再度テストするとファイル破損は発生しなくなりました。
    このように認識違いのまま偶然解決してしまうケースは後からより大きな問題となることもあるので要注意です。ここ
    に至るまでに本当の問題に気づけるチャンスはあったので反省点です。
    この時点では問題が起きなくなったので 本人の認識としてはクローズした案件 となりました。
    しくじり先生 「NFS+sqliteで苦労した話から学ぶ、問題解決の考え方」 33

    View full-size slide

  34. インフラチームから不穏な情報共有あり
    「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

    View full-size slide

  35. この情報で大きな見落としに気づく
    tcpdump の結果から今までうっすら感じていたモヤモヤが何だったのかようやく分かりました。

    NFS にはサーバとクライアントがいますが、サーバ側の考慮が抜け落ちていたことに気づいてしまったのです。正確に
    言うと意識のどこかにはありましたが、あえて考えないようにしていました。

    しかし実際のパケットレベルでのやりとり内容が出てくると否応なしにNFSサーバの存在を認識せざるを得ません。
    この時点で分かっていることは以下です。
    mount オプションはNFSクライアント側でそのマウントポイントをどう扱うかの指定
    クライアント側のマウントオプションはNFSサーバ側の設定には直接影響しない(※間接的には影響あり)
    サーバ側が同期書き込みするかどうかは、今回の問題に関係なかった(後述)
    偶然問題が解消しただけ だと判明したので追加調査が必要になりました。
    しくじり先生 「NFS+sqliteで苦労した話から学ぶ、問題解決の考え方」 35

    View full-size slide

  36. サーバ側の同期書き込みと今回の問題には関係がなかった
    よく考えれば当たり前の事ですが、NFSサーバ側は 単一のデーモンがデータを集中管理するので書き込みが同期か非同
    期かに関わらず最新の情報を常に返却することができる はずです。仮にまだストレージに書き込んでないファイルへ読
    み込み要求があったら、メモリ上のバッファから返却すればいいだけです。
    非同期で問題になるのはサーバの意図しないシャットダウンが発生した際に書き込まれていない情報が失われるという
    バックアップと復元の話でした。
    しくじり先生 「NFS+sqliteで苦労した話から学ぶ、問題解決の考え方」 36

    View full-size slide

  37. 問題が発生しうる場所を改めて整理
    システムは様々な仕組みの上で動いているので、その仕組みがそれぞれレイヤとなって表れます。
    今回の問題において、開発者から近い順に挙げると以下のレイヤに分けて考えることができます。
    1. 開発したプログラムの問題
    2. (開発したプログラムを動作させる)Pythonインタプリタの問題
    3. (Pythonインタプリタが内部で利用する)sqliteライブラリの問題
    4. (sqliteライブラリがシステムコール経由で呼び出す)Linuxカーネルの問題
    5. (Linuxカーネルがシステムコール内で利用する)NFSクライアントの問題
    6. (NFSクライアントがネットワーク経由でコールする)NFSサーバの問題(※ネットワークの問題も含む)
    ここで重要なのは問題を確認するレイヤがどこなのかを意識しながら対応することです。

    1~4 はこれまでの情報から問題の原因である可能性が低いことが分かっています。なぜならばローカルファイルシステ
    ムでは正常に動いていたからです。
    しくじり先生 「NFS+sqliteで苦労した話から学ぶ、問題解決の考え方」 37

    View full-size slide

  38. 問題が発生しうる場所を改めて整理
    5. (Linuxカーネルがシステムコール内で利用する)NFSクライアントの問題
    6. (NFSクライアントがネットワーク経由でコールする)NFSサーバの問題(※ネットワークの問題も含む)
    6 は状況を整理した結果、単一デーモンでデータの不整合が発生する論理的な理由がないので可能性が低くなりまし
    た。

    ネットワークの問題も有線のローカルネットワークなので低く見積もることができます。
    となると次に検証すべきポイントは 5 のレイヤです。
    しくじり先生 「NFS+sqliteで苦労した話から学ぶ、問題解決の考え方」 38

    View full-size slide

  39. 対象レイヤに限定して検証をする
    5. (Linuxカーネルがシステムコール内で利用する)NFSクライアントの問題
    調査対象を Linux の NFSクライアントに絞れたのでまずは情報収集をします。
    クライアント側の問題ということは、 サーバ側では更新されたデータがあるのに依然として古い情報を使ってしまう 状
    況が考えられます。

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

    View full-size slide

  40. 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

    View full-size slide

  41. ようやく問題の全貌が分かる
    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

    View full-size slide

  42. 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

    View full-size slide

  43. 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

    View full-size slide

  44. 古いキャッシュが利用された原因を考える
    問題のアプリはファイルを開くタイミングをロックに含めていませんでした。
    1. sqlite DBファイルを開く
    2. ロックをする
    3. 書き込む
    4. ロックを解放する
    5. sqlite DBファイルを閉じる
    ファイルを開いた後、ロックを取得する前に他のクライアントから書き込みがあるとキャッシュが破棄されず古いデー
    タを使ってしまいそうです。

    さらに sqlite は後述しますがファイルを閉じる動作に癖があるので、利用する側が厳密にファイルの開閉を制御するこ
    とが難しい状況もあります。
    しくじり先生 「NFS+sqliteで苦労した話から学ぶ、問題解決の考え方」 44

    View full-size slide

  45. 古いキャッシュが利用された原因を考える
    Weak cache consistency は各オペレーションの前にファイル属性を取得して更新を検出してくれるので、それならば問
    題の発生を防ぐことができそうです。

    しかしここで問題になるのがファイル属性のキャッシュです。
    ファイル属性の取得にはキャッシュが効くので一定時間内の再取得は古い属性情報のままとなり、結果として「ファイ
    ルは更新されていない」と判断されてしまいます。
    ここまで調べると noac で問題が解決した理由が分かります。

    noac には actimeo=0 を暗黙で含むため属性キャッシュが無効化されます。
    都度サーバーから GETATTR で属性情報を取得することで最新の更新を検出可能となり、キャッシュの破棄ができるよう
    になったようです。
    しくじり先生 「NFS+sqliteで苦労した話から学ぶ、問題解決の考え方」 45

    View full-size slide

  46. 検証方法を考える
    原因の仮説ができたので次は実際にそれが合っているか検証する必要があります。
    3. (Pythonインタプリタが内部で利用する)sqliteライブラリの問題
    4. (sqliteライブラリがシステムコール経由で呼び出す)Linuxカーネルの問題
    5. (Linuxカーネルがシステムコール内で利用する)NFSクライアントの問題
    検証したいのは 5 レイヤなので、可能な限り対象レイアのみ切り出して検証する方法を考えます。
    sqlite がファイルの読み書きに利用するのはシステムコールなので、sqlite が使うのと同じシステムコールを利用すれば
    sqlite を利用せずに 5 レイヤだけを検証可能なはずです。
    しくじり先生 「NFS+sqliteで苦労した話から学ぶ、問題解決の考え方」 46

    View full-size slide

  47. sqlite の動作を確認する
    システムコールは「OS側へのお願い」なので、OS側から観測する方法がいくつも用意されています。

    例えば現在プログラムが呼び出しているシステムコールをリアルタイムで観測する strace
    コマンドがあります。
    strace -p

    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

    View full-size slide

  48. 検証する
    2台のPCから同じNFS領域をマウントして検証します。同じPCから試すとローカルPC内では fcntl が効いてしまうので別
    環境から接続します。
    クライアントA
    1. ファイルAをオープン
    2. ファイルAを読み込んでsha256ハッシュ取得
    3. ファイルAを先頭にシーク
    4. 2に戻る
    クライアントA はファイルを開いたまま、先頭までシークと読み込みとハッシュ計算を繰り返します。
    しくじり先生 「NFS+sqliteで苦労した話から学ぶ、問題解決の考え方」 48

    View full-size slide

  49. 検証する
    クライアントB
    任意のタイミングでファイルAにランダムな文字列を書き込む
    クライアントAが常時ハッシュを出力しつづけている間にクライアントBが書き込みます。

    クライアントBの書き込み完了からクライアントA側のハッシュが変化するまでにどれくらいのタイムラグがあるかを検
    証します。
    しくじり先生 「NFS+sqliteで苦労した話から学ぶ、問題解決の考え方」 49

    View full-size slide

  50. 検証結果
    想定通りクライアント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

    View full-size slide

  51. クライアント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

    View full-size slide

  52. クライアントB(任意タイミングでランダムデータを書き込む)
    import random

    def random_write():

    fp = open('test.db', 'w')

    fp.write(str(random.random()))

    fp.close()

    random_write()

    しくじり先生 「NFS+sqliteで苦労した話から学ぶ、問題解決の考え方」 52

    View full-size slide

  53. クライアント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

    View full-size slide

  54. ようやく解決する
    いくつかの考慮漏れと想定ミスから寄り道になってしまいましたが、途中からは仕切り直してなんとか無難なゴールを
    迎えることができました。
    開発初期はローカルストレージを利用していたので、早期に NFS を利用できたらもう少し理想的な対処ができたはずで
    すが、今回は新規 NFS 環境の準備が必要な案件だったのでどうしても後手に回ってしまう状況でした。

    振り返ると諸々反省点はありますが、各ポイントでの判断など参考になる部分があればなと思っています。
    しくじり先生 「NFS+sqliteで苦労した話から学ぶ、問題解決の考え方」 54

    View full-size slide

  55. 【余談】オープンソースの中身は楽しい
    /******************************************************************************

    *************************** 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

    View full-size slide