一覧DB-09-1 · 保存・複製・復旧

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

1
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)
2
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'}
3
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
4
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,)
5
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,)
6
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'}
7
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,)
8
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)]

演習完了