【Cassandra】データ整合性を維持するオペレーション

# はじめに
Cassandraである程度一貫性を担保して運用していくためには、実施すべきオペレーションがいくつかあります。
この記事では整合性に関連するCassandraの概要とデータ整合性維持に重要なオペレーションをいくつか紹介します。

# Cassandraの概要
## Cassandraと整合性
Cassandraでは `可用性(Availability)` や `分断耐性(Partition-tolerance)` を実現するために `一貫性 (Consistency)` として `結果整合性(Eventual Consistency)` を採用しています。

## Cassandraの読み書き
Keyspace単位で設定された `REPLICATION` にしたがって特定のデータを担当するノードを決定します。production環境では論理DCごとにレプリカ数を設定する `NetworkTopologyStrategy` が推奨されています。
```
CREATE KEYSPACE example 
  WITH REPLICATION = { 
   'class' : 'NetworkTopologyStrategy', 
   'dc1' : 3,
   'dc2' : 3,
} ;
```

### consistency level
REPLICATION によって決定された担当ノードの中から `consistency level` に設定されたノード数のread/writeが成功すればクライアントに正常な結果を返すように設定することができます。
一貫性を重視した設定をすることでデータ整合性をある程度担保することが可能ですが、その分可用性が失われるため、ユースケースに応じた設定が必要です。
以下が代表的なものになります。
- `ONE`: 最初に返却したノードのデータをクライアントに返します
- `QUORUM`: 最初に返却した (レプリカ数/2)+1、つまり過半数のノードのデータを中から最新のものをクライアントに返します
- `ALL`: 全てのノードのデータの中からTimestampが最新のものをクライアントに返します

設定できるLevelの詳細については[こちら](https://docs.datastax.com/en/cassandra-oss/3.x/cassandra/dml/dmlConfigConsistency.html)などを参考にしてください。

## Cassandraと削除
削除を実施した場合、`tombstone` という削除データを書き込むことで論理削除を行なっております。readした最新データがtombstoneであった場合、削除されたデータとしてクライアントに返却されます。

---
ここまでがCassandra概要になりますが、read/writeともに `QUORUM` などの一貫性を重視した consistency levelを設定していた場合でも、適切な運用を実施していない場合には予期しないデータを返却してしまうケースも存在します。
今回はCassandraの整合性を維持する上で必要とされる運用オペレーションをいくつか紹介いたします。

# 整合性維持のためのオペレーション
## [Repair](https://cassandra.apache.org/doc/3.11/cassandra/tools/nodetool/repair.html)の実施
ノード間のデータを比較し、全てのノードのデータを最新データへと修復するコマンドです。
```
# データ全体をRepairしたい場合

# 全台に実行する場合
$ nodetool repair --full -pr

# 1ラックに実行する場合
$ nodetool repair --full
```
コマンドに指定するオプションよって実行が必要なノード数や時間が異なるため、詳細については[こちら](https://cassandra.apache.org/doc/3.11/cassandra/tools/nodetool/repair.html)などを参考にしてください。
tombstoneもこちらのコマンドによって同期されます。

### Repair未実施による不整合
#### 削除データの復活
tombstoneが削除されるタイミングは[TABLEオプション](https://cassandra.apache.org/doc/3.11/cassandra/cql/ddl.html#create-table-general-options)の `gc_grace_seconds` にて指定が可能で、デフォルトは864000秒(10日)に設定されています。
しかし、Repairがgc_grace_seconds時間以上未実施の場合には、tombstoneのwriteに失敗しているノードが存在する場合にデータが復活します。

##### gc_grace_seconds経過前
過半数の2ノードでtombstoneが存在しているため、QUORUMでreadを行った場合、データ1は削除されているデータとしてクライアントに返却されます。
![repair_before.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3647913/bdc50cc2-9116-10f7-2bb6-0e36539590d8.png)

##### gc_grace_seconds経過後
gc_grace_secondsが経過すると、node1とnode2ではCompaction実行時にtombstoneとデータ1が物理削除されます。
しかし、node3についてはtombstoneが存在しないため、物理削除が行われずにデータ1が残ったままになってしまいます。これによりnode3から返却された場合は消したはずのデータが復活してしまう事象が発生します。
また、その後にRepairが実行されると全てのノードへデータ1が伝播するため、全てのリクエストでデータが復活することになります。
![repair_after.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3647913/8e9f1558-ec36-2d60-44d7-d42cd3e576cf.png)

以上のようなRepair未実施によるデータ復活を防ぐため、
__gc_grace_seconds経過前にRepairを実行することで、tombstoneを全てのレプリカノードに同期させておく必要があります__。

## [Cleanup](https://cassandra.apache.org/doc/3.11/cassandra/tools/nodetool/cleanup.html)の実施
クラスタへのノードの追加や削除により担当するデータの範囲に変更があった場合に、すでに担当ではなくなった不要データを該当ノードから削除するコマンドです
```
# すべてのkeyspace/tableに実行する場合
$ nodetool cleanup
```

### Cleanup未実施による不整合
#### 古いデータの読み込み・削除データの復活
Cleanupを実施しないまま担当データ範囲に変更があった場合に、該当ノードでの古いデータの読み込みや削除データの復活をする場合があります。
##### ノード追加前
まずはnode1, node2, node3にデータ1が存在している状態です。
![before_add.drawio.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3647913/41e8bb67-e41a-9408-c603-4eeb31c99a33.png)

##### ノード追加後
node4が追加されたことにより、データ1の担当がnode3からnode4へ変更され、
node3では物理データはあるが参照されないようになります。
![after_add.drawio.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3647913/210602b1-5238-fd39-5c9a-ae6b31925203.png)

##### データ削除
データ1の削除が実施されnode1, node2, node3からデータ1が削除されます。
node3はデータが残ったままですが、データ担当範囲ではなく参照されないため、データ1はクライアントには削除状態が返却されます。
![after_delete.drawio.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3647913/2fc8ed9e-38ad-98a8-2068-6caaeb1cde65.png)

##### ノード削除後
node4がクラスタから削除された場合、node3が再びデータの担当範囲になるためデータ1がクライアントから参照されるようになってしまいます。
![after_redelete.drawio.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3647913/7c725d92-d994-361c-76e6-3c6df12c8751.png)

以上のようなCleanup未実施によるデータ復活や該当ノードに対する古いデータのreadを防ぐため、
__ノード追加を実施の際にはCleanupを実行する必要があります__。

## [Ring](https://cassandra.apache.org/doc/3.11/cassandra/tools/nodetool/ring.html)情報を一致させる
クラスタの `Token Ring(データ担当範囲)情報` を表示するコマンドです
```
# すべてのkeyspaceに実行する場合
$ nodetool ring
```

### Ring情報の伝播について
CassandraではRing(データ担当範囲)情報を `Gossip` を使ってやりとりしています。

#### Gossip
クラスタに属する各エンドポイントのメタ情報を拡散しています。
各ノードは定期的にクラスタ内の他のノードをランダムに選定してやりとりを行い、ハートビートの世代数を確認して更新が必要であればメタ情報の更新を行います。非常に素早く情報を伝播させることができますが、変更順序を厳密に保証していないため、問題を発生させる場合があります。

### Ring情報ずれによる不整合
GossipではRing情報のやりとりも行っており、ノード間でRing情報がずれることで読み書きの不整合が発生します。Ring情報のノード間差分を監視することでデータ不整合の発生可能性の検知を行うことができます。
以下がデータ不整合の一例です。

クラスタ内で以下の2つのRing情報が混在している場合
![latest_ring_before.drawio.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3647913/ff3b7ddf-e7bd-2952-500b-3dd4d751f257.png)
このような場合にnodeBを含むRingAを認識しているCoordinatorノードが最新のデータをnodeBに書き込んだ後、nodeBを含まないRingBを認識しているCoordinatorノードがデータを読み込むとnodeAから古いデータを読み込むことで一部データの不整合が発生します。

※ Cassandra 5.xではGossipから [Transactional Cluster Metadata(TCM)](https://cwiki.apache.org/confluence/display/CASSANDRA/CEP-21%3A+Transactional+Cluster+Metadata) での管理が行われるため、こうした不整合の解消が予定されております。

以上のようなRing情報ずれによる該当ノードに対する古いデータのreadを防ぐため、
__ノード間のRing情報を定期的に監視し、差異が発生していた場合にはノードの再起動などでGossipの再伝播を行う必要があります__。

## ダウンノードの起動
ノードがダウンしていた際に `Hinted Handoff` によって復旧時にデータが再送信されることによりデータの一貫性を確保します。

### Hinted Handoff
cassandra.yamlによって [hinted_handoff_enabled](https://cassandra.apache.org/doc/3.11/cassandra/configuration/cass_yaml_file.html#hinted_handoff_enabled) を有効化することで、書き込みのデータ担当となっているノードがダウンした場合にCoordinatorノードで一時的に書き込みを保管し、復旧する際にノードへデータを再送信することでデータの一貫性を確保します。

### ダウン時の書き込み欠損
Hinted Handoffを有効化した場合も注意が必要になります。Hinted Handoffは `max_hint_window_in_ms`を超過したデータについてはCoordinatorで保持されなくなるため、max_hint_window_in_msを超過した場合に整合性を維持するためにはダウンノードに対する[Removenode](https://cassandra.apache.org/doc/3.11/cassandra/tools/nodetool/removenode.html)や[Replace](https://cassandra.apache.org/doc/3.11/cassandra/operating/topo_changes.html#replacing-a-dead-node)といった対応が必要になります。

※ Schema versionが異なるバージョンアップ時には、デプロイ後の起動でHintの伝播をしないため注意が必要になります。

# 最後に
ここで紹介した以外にも注意すべき事項などはあると思いますが、皆さんのCassandra運用の一助になればと思います。