WALとチェックポイントによる復旧
残高100のDBをWALモードで作り、150への変更が本体へ反映される前後を調べます。ログのヘッダー、ページ番号、確定を示すDBサイズ、チェックサムを解析します。続いて専用の子プロセスをCOMMIT後・次の変更の確定前に終了させ、再接続で残る値を確認します。
説明
WAL(Write-Ahead Log)方式では、変更したページを本体DBとは別のログへ追記して確定します。本体へ反映するチェックポイントと、トランザクションのCOMMITは同じ操作ではありません。読み取りでは本体と有効なWALを合わせた状態を使います。
残高100のDBをWALモードで作り、150への変更が本体へ反映される前後を調べます。ログのヘッダー、ページ番号、確定を示すDBサイズ、チェックサムを解析します。続いて専用の子プロセスをCOMMIT後・次の変更の確定前に終了させ、再接続で残る値を確認します。
1本体へ残高100を保存する
WALと同期設定を指定し、自動チェックポイントを止めて手順を観察します。
import sqlite3,tempfile,shutil
from pathlib import Path
from wal_tools import wal_info,crash_update
tmp = tempfile.TemporaryDirectory()
path = Path(tmp.name)/'wal.db'
c = sqlite3.connect(path,isolation_level=None)
print(c.execute('PRAGMA journal_mode=WALWAL方式へ変更').fetchone())
c.execute('PRAGMA synchronous=FULL確定時の同期を要求')
c.execute('PRAGMA wal_autocheckpoint=0自動反映を止めて観察')
c.executescript('CREATE TABLE balance(id INTEGER PRIMARY KEY,amount INTEGER); INSERT INTO balance VALUES(1,100);')
print(c.execute('PRAGMA wal_checkpoint(TRUNCATE)本体へ反映しWALを縮める').fetchone())WALモードになり、手動チェックポイントで本体へ100を反映しました。TRUNCATEはチェックポイント完了後にWALを0バイトへ縮めます。戻り値(0,0,0)はbusyなし、残るログと処理対象が0である状態です。
2150への変更をWALへ確定する
一文のUPDATEを確定してログを解析します。
c.execute('UPDATE balance SET amount=150変更を一文で確定 WHERE id=1')
print(c.execute('SELECT amount FROM balance').fetchone())
print(wal_info(path)WALの実バイト列を解析)接続からは150が見えます。WALは32バイトのヘッダーと、24バイトのフレームヘッダー+4096バイトのページからなります。この一回の更新では一フレームで4152バイトです。wal_infoは実ファイルの構造とチェックサムを検査する補助関数です。
3WAL内のページ番号とコミット記録
解析したフレームからページ番号と確定時DBページ数を取り出します。
info = wal_info(path)
for number,(page,db_pages) in enumerate(info['frames']各フレームのページ番号とDBサイズ,1):
print('frame',number,'page',page,'db_pages',db_pages,'commit',db_pages!=00以外なら確定フレーム)ページ番号2、DBページ数2、commit Trueです。フレームのDBページ数が0でなければ、そのフレームはトランザクションの確定を示します。値は変更されたページ番号とは別に、確定時のDB全体のページ数を表します。
4本体ファイルだけのコピーには150がない
開いたDBの本体だけを別名へコピーして読みます。
copy_path = Path(tmp.name)/'main_only.db'
shutil.copyfile(path,copy_path)本体だけをコピー
copy = sqlite3.connect(copy_path)
print('main only:'WALなしのコピーから読む,copy.execute('SELECT amount FROM balance').fetchone())
copy.close()
print('live:'本体とWALを合わせて読む,c.execute('SELECT amount FROM balance').fetchone())本体だけのコピーは100、元の接続は150です。この例では未チェックポイントの確定変更がWALにあるためです。稼働中のDBは本体だけをコピーして一貫した最新バックアップが得られるとは限らず、バックアップAPIなどで整合した状態を取得します。
5チェックポイント後の本体を確認する
WALを本体へ反映し、もう一度コピーします。
print(c.execute('PRAGMA wal_checkpoint(TRUNCATE)確定したページを本体へ反映').fetchone())
print(wal_info(path))
shutil.copyfile(path,copy_path)反映後の本体を読む
copy = sqlite3.connect(copy_path)
print('main after checkpoint:',copy.execute('SELECT amount FROM balance').fetchone())
copy.close()WALは0バイトとなり、本体だけでも150です。COMMITは変更の確定、チェックポイントは確定したページを本体へ反映する作業で、役割と時点が異なります。
6200をコミットし、999のコミット前に子プロセスを終了する
現在の接続を閉じ、固定手順の子プロセスを起動します。
c.close()親の接続を閉じる
print("child exit:",crash_update(path)固定位置で子プロセスを終了)
print(wal_info(path)残ったログを解析)子は200へUPDATEして確定し、次にBEGINして999へ変更したところで終了コード23で終わります。接続を通常終了する処理は通らないため、WALが残ります。crash_updateの処理順は、確定200→未確定999→プロセス終了です。
7再接続するとコミット済みの200が復元される
本体だけの値と、元のDBへの再接続の値を比べます。
shutil.copyfile(path,copy_path)復旧前の本体を取得
copy = sqlite3.connect(copy_path)
print('main before recovery:',copy.execute('SELECT amount FROM balance').fetchone())
copy.close()
c = sqlite3.connect(path元のWALも含めて再接続,isolation_level=None)
print('reopened:',c.execute('SELECT amount FROM balance').fetchone())本体だけは150、再接続では200です。SQLiteは有効なWALの確定済みページを使います。未確定だった999は保存された取引として採用されません。プロセス終了時に直近で計算した値と、DBが確定した値を分けて判断します。
8復旧したDBを検査して本体へ反映する
整合性検査とチェックポイントを実行します。
print(c.execute('PRAGMA integrity_checkDB構造の整合性検査').fetchone())
print(c.execute('PRAGMA wal_checkpoint(TRUNCATE)復旧した確定状態を本体へ反映').fetchone())
c.close()
c = sqlite3.connect(path,isolation_level=None)
print(c.execute('SELECT * FROM balance最終的な保存値').fetchall())整合性検査はok、再接続後の残高は200です。WALのヘッダーと各フレームにはソルト値と連鎖チェックサムがあり、別世代や壊れたログの採用を防ぐ検査に使われます。確定境界とログの有効性の両方を確認して復旧する仕組みです。
まとめ
COMMITとチェックポイントは別で、確定した変更がWALだけにある場合があります。
WALの確定印・ソルト値・チェックサムを検査して、有効なページを採用します。
再接続で採用するのは確定済みの値で、終了前の未確定変更とは異なります。
操作履歴と解説を振り返れます。
Python / SQLite
import sqlite3,tempfile,shutil
from pathlib import Path
from wal_tools import wal_info,crash_update
tmp = tempfile.TemporaryDirectory()
path = Path(tmp.name)/'wal.db'
c = sqlite3.connect(path,isolation_level=None)
print(c.execute('PRAGMA journal_mode=WAL').fetchone())
c.execute('PRAGMA synchronous=FULL')
c.execute('PRAGMA wal_autocheckpoint=0')
c.executescript('CREATE TABLE balance(id INTEGER PRIMARY KEY,amount INTEGER); INSERT INTO balance VALUES(1,100);')
print(c.execute('PRAGMA wal_checkpoint(TRUNCATE)').fetchone())('wal',)
(0, 0, 0)c.execute('UPDATE balance SET amount=150 WHERE id=1')
print(c.execute('SELECT amount FROM balance').fetchone())
print(wal_info(path))(150,)
{'header_0_11': '37 7f 06 82 00 2d e2 18 00 00 10 00', 'version': 3007000, 'page_size': 4096, 'bytes': 4152, 'frames': [(2, 2)], 'checksums': 'ok'}info = wal_info(path)
for number,(page,db_pages) in enumerate(info['frames'],1):
print('frame',number,'page',page,'db_pages',db_pages,'commit',db_pages!=0)frame 1 page 2 db_pages 2 commit True
copy_path = Path(tmp.name)/'main_only.db'
shutil.copyfile(path,copy_path)
copy = sqlite3.connect(copy_path)
print('main only:',copy.execute('SELECT amount FROM balance').fetchone())
copy.close()
print('live:',c.execute('SELECT amount FROM balance').fetchone())main only: (100,) live: (150,)
print(c.execute('PRAGMA wal_checkpoint(TRUNCATE)').fetchone())
print(wal_info(path))
shutil.copyfile(path,copy_path)
copy = sqlite3.connect(copy_path)
print('main after checkpoint:',copy.execute('SELECT amount FROM balance').fetchone())
copy.close()(0, 0, 0)
{'bytes': 0, 'frames': 0}
main after checkpoint: (150,)c.close()
print("child exit:",crash_update(path))
print(wal_info(path))child exit: 23
{'header_0_11': '37 7f 06 82 00 2d e2 18 00 00 10 00', 'version': 3007000, 'page_size': 4096, 'bytes': 4152, 'frames': [(2, 2)], 'checksums': 'ok'}shutil.copyfile(path,copy_path)
copy = sqlite3.connect(copy_path)
print('main before recovery:',copy.execute('SELECT amount FROM balance').fetchone())
copy.close()
c = sqlite3.connect(path,isolation_level=None)
print('reopened:',c.execute('SELECT amount FROM balance').fetchone())main before recovery: (150,) reopened: (200,)
print(c.execute('PRAGMA integrity_check').fetchone())
print(c.execute('PRAGMA wal_checkpoint(TRUNCATE)').fetchone())
c.close()
c = sqlite3.connect(path,isolation_level=None)
print(c.execute('SELECT * FROM balance').fetchall())('ok',)
(0, 0, 0)
[(1, 200)]演習完了