###### tags: `memo` `systems`
# LEC 15 Logging
Slide URL: http://web.mit.edu/6.033/www/lec/s17-partial.pdf
## 前回の話
- 以下の機能を提供するトランザクションを実装する方法を検討
- 原子性
- 分離
- shadow copies で原子性を実現してみた
- ただし、パフォーマンス面は…
## 今回の話
- 原子性を実現する別の方法、logging
## Logging
### :one: 基本
以下の一連の処理をログに記録する
```sql
begin
write(A, 100)
write(A, 50)
commit
begin
write(A, read(A) - 20)
write(Bm read(B) + 20)
commit
begin
write(A, read(A) + 30)
crash!
```
ログは以下の通り
|No|TransactionID|Type|OldValue|NewValue|
|-|-|-|-|-|
|1|T1|UPDATE|A = 0|A = 100|
|2|T1|UPDATE|B = 0|B = 50|
|3|T1|COMMIT|||
|4|T2|UPDATE|A = 100|A = 80|
|5|T2|UPDATE|B = 50|B = 70|
|6|T2|COMMIT|||
|7|T3|UPDATE|A = 80|A = 110|
そして、ログから特定の値を再現するコードは以下の通り
```python
def read(logs, var):
commits = []
# ログを逆順にスキャン(新しいログから見ていきたいため)
for log in reversed(logs):
if log.type == "COMMIT": # コミット済みトランザクションの tid を記録
commits.append(log.tid)
elif log.type == "UPDATE" # 目的の変数が最後に更新された値を探す
and log.var == var
and (log.tid in commits or log.tid == current_tid):
return log.new_value
```
コミット前にクラッシュすると、COMMIT ログが残らないので read() では引っかからない
**欠点: read が遅くなる場合がある**
- 例えば、以下の場合は遅い
- ログが膨大、かつ
- 目的変数の最後の更新がログの比較的先頭部分にある
### :two: cell storage を使った read() のパフォーマンス改善
ログに加えて、on disk に各変数の最新値を書くこと read() のパフォーマンスを改善できる。ただし、今回は cell storage の値の正しさを保証するために cell storage に対して recover 処理を実行する必要がある
```python
def read(var):
return cell_read(var)
def write(var, value):
logs.append(Log(current_tid, "UPDATE", var, read(var), value))
cell_write(var, value)
def recover(logs):
commits = []
# 未コミットの処理を破棄する (undo)
for log in reversed(logs):
if log.type == "COMMIT":
commits.append(log.tid)
if log.type == "UPDATE" and log.tid not in commits:
# コミットされていないログであれば、古い値に復元する
cell_write(log.var, log.old_value)
```
**利点: read が早くなった**
**欠点: write, recover が遅くなる場合がある**
- write: ログの記録 + cell storage への書き込みが増えたため
- recover: 必ずログ全体を操作するため、ログが膨大だと遅くなる
### :three: cache を使った write() のパフォーマンス改善
cell storage に加えて、on memory なキャッシュを用意する。最新値は cache に記録するようにする。ここで大事なことが 2 つあって、
1. cache の値を定期的に cell storage に書き出す必要がある
- recover() は cell storage をベースにしているため
2. cache が cell storage に書き出される前にクラッシュした場合も想定する必要がある
- recover() で undo だけではなく、redo の処理も必要になる
```python
def read(var):
if var in cache:
return cache[var]
else:
cache[var] = cell_read(var)
return cache[var]
def write(var, value):
logs.append(Log(current_tid, "UPDATE", var, read(var), value))
cache[var] = value
def flush(): # 時々呼び出して、cache を cell storage に書き出す
for var in all_vars:
cell_write(var, cache[var])
def recover(logs):
commit = []
# 未コミットの処理を破棄する (undo)
for log in reversed(logs):
if log.type == "COMMIT":
commits.append(log.tid)
if log.type == "UPDATE" and log.tid not in commits:
# コミットされていないログであれば、古い値に復元する
cell_write(log.var, log.old_value)
# コミット済みの値を反映 (redo)
for log in logs:
if log.type == "UPDATE" and log.tid in commits:
cell_write(log.var, log.new_value)
```
**利点: read, write が早くなった**
**欠点: recover が遅くなる場合がある**
- recover: 必ずログ全体を操作するため、ログが膨大だと遅くなる
## :four: checkpoints とログの truncate による recover() のパフォーマンス改善
recover() が遅いのはログが膨大になるから、であれば以下の方法でログを短くすればいい
- cache の中身を全て flush する
- CHECKPOINT を作成する
- その時点での最新値
- CHECKPOINT 以前のログを削除する
## メモ
- 今回の Logging は Write-Achead Logging (ログ先行書き込み) と言うらしい
- https://ja.wikipedia.org/wiki/%E3%83%AD%E3%82%B0%E5%85%88%E8%A1%8C%E6%9B%B8%E3%81%8D%E8%BE%BC%E3%81%BF
- 実際の変更を加える前に必ずログを残す方式
- よく知られたログ先行書き込みアルゴリズムに ARIES がある
- https://qiita.com/kumagi/items/14b6593a2e8ae0c56546 とか
- https://dl.acm.org/doi/10.1145/128765.128770 を見ると
- 結構今回の話に似ている気がする?
- ファイルシステムではジャーナリングという