---
# System prepended metadata

title: LEC 15 Logging
tags: [memo, systems]

---

###### 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 を見ると
    - 結構今回の話に似ている気がする？
- ファイルシステムではジャーナリングという
