Couchbase Mobile データレプリケーションプロトコル解説



# 歴史

Couchbase MobileのV1.xでは、RESTベースのプロトコルを使用してレプリケーションが実装されていました。つまり、レプリケーションロジックはHTTPを介したAPIコールとして成立していました。

# レプリケーションプロトコル概要
### WebSocket
Couchbase Mobileのレプリケーションは、WebSocket上のメッセージングプロトコルとして実装されています。

WebSocketプロトコルは、単一のTCPソケット接続を介してリモートホスト間で全二重メッセージを渡すことを可能にします。WebSocketプロトコルはHTTP/S接続として開始され、リモートホストがサポートしている場合は、WebSocketに切り替わります。


Couchbase Mobileのメッセージングプロトコルは、WebSocketレイヤーを用いた階層化アーキテクチャにより、レプリケーションロジックと、その基盤となるメッセージングトランスポートの間の「関心の分離」を実現しています。


#### WebSocketの利点
WebSocketプロトコルは(RESTベースのプロトコルより)高速で、必要となる帯域幅とソケットリソースを削減します。ソケットリソースの節約により、サーバー側での同時接続数を増やすことができます。


### レプリケーションモード
レプリケーションプロセスには、以下の２つのモードがあります。

- **「継続」モード**では、変更はクライアントとSync Gateway間で継続的に同期されます。
- **「ワンショット」モード**では、変更は「ワンショット」で同期され、クライアントとサーバー間の接続は一旦切断されます。その後の変更をプッシュアップまたはプルダウンする必要がある場合、クライアントは新たにレプリケーションを明示的に実行します。

# キーコンセプト
プロトコルの詳細へ進む前に、いくつかのキーとなるコンセプトを解説します。

## リビジョンツリー
Couchbaseは、ドキュメントの管理にMulti Version Concurrency Control（MVCC）システムを使用しています。このようなシステムでは、すべてのドキュメントが一連のリビジョンとして保存されます。ドキュメントが作成時に、はじめのリビジョンが自動的に作成され、編集であれ削除であれ、ドキュメントが更新されるたびに、そのドキュメントの次のリビジョンが追加されます。ドキュメントの削除に対応するリビジョンは、トゥームストーン（墓石）リビジョンと呼ばれます。


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/176567/e1430a4c-44f3-0831-a570-0b9319024d41.png)


### 変更　Change
以下の解説で「変更」と言われる時、それは、ソースデータベースには存在するがターゲットデータベースには存在しないリビジョンと対応しています。

## シーケンスID
すべての変更は、時系列順のシーケンスIDに一意に関連付けられています。これは、最終更新時刻タイムスタンプに似ていますが、実時間ではなく、自動的に増分されるカウンターである点が異なります。

注： Couchbase LiteのシーケンスIDは単純な整数ですが、SyncGatewayのシーケンスIDは長いbase64文字列である可能性があります。理由は複雑で、CouchbaseServerクラスター内のノード間の内部同時実行性に関係しています。シーケンスIDの内容は内部の実装に依存するため、クライアントはその内容について想定を置いた使い方をしてはなりません。

## レプリケーター

レプリケーション実行に関わるレプリケーターには、以下の種類があります。

プッシュレプリケーションに用いられる：

- ソースレプリケーター：データの変更を送信するレプリケーター
- ターゲットレプリケーター：データの変更を受信するレプリケーター

プルレプリケーションに用いられる：

- アクティブレプリケーター：データの変更を自動的にプッシュ/プルするCouchbase Liteレプリケーター
- パッシブレプリケーター：変更のプッシュ/プルリクエストに応答するSync Gatewayレプリケーター

## チェックポイント

レプリケーションの最新の進行状況を記録することによって、レプリケーションの中断・障害を経て再開する際に、確実に処理を継続することを可能にする仕組みをチェックポイントと呼びます。

チェックポイントは、レプリケーターによって複製された最後のシーケンスIDの記録です。すべてのレプリケーションサイクルの終わりに、（ソース）レプリケーターはターゲットに送信された最後の変更に対応するソースシーケンスIDをチェックポイントにします。最初のレプリケーションサイクルにはチェックポイントがありません。

## レプリケーション
レプリケーションとは、ソースレプリケーターが最後のチェックポイントよりシーケンスIDが大きいすべての変更をターゲットレプリケーターに送信するプロセスです。

レプリケーションによって、現在のリビジョンのリビジョン本体、関連するBLOB/添付ファイル、およびリビジョン履歴が複製されます。


# プロトコル
## 接続の確立

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/176567/1a401d68-4b3c-3ecc-954f-72769fa2c14e.png)


1. レプリケーションを開始するために、クライアントはHTTPを介してサーバーにWebSocketハンドシェイク要求を送信し、WebSocketに切り替えたいことを伝えます。
2. サーバーは、プロトコルの切り替えに同意したことを示して応答します。
3. WebSocketによるハンドシェイクが行われると、ソケットはHTTPのための使用を停止し、すべての通信はWebSocketのメッセージになります。

1つのソケットで、プッシュレプリケーションとプルレプリケーションの両方を同時にサポートできます。

レプリケーションは、実行のためチェックポイントを前提としているため、まずチェックポイントの管理について説明します。

## チェックポイント管理
レプリケーションは（プッシュとプルのいずれも）、サーバーにチェックポイントを保存 store および取得 fetch します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/176567/3397fbf0-9e0d-f6d6-61a0-9b25d4cade0d.png)


1. 接続の確立後、サーバーで最後の既知の送信元シーケンスIDを判別するために、クライアントからサーバーに対して`getCheckpoint`要求メッセージを送信します。このリクエストには次のものが含まれます。
  - クライアントを識別するクライアントID
2. `getCheckpoint`要求への応答には、クライアントに対して最後に記録されたチェックポイントが含まれます。チェックポイントは、クライアントによって以前に作成および保存されたJSONオブジェクトです。これには、クライアントが必要とするデータを含めることができますが、現在の形式は次のとおりです。
  - クライアントによってプッシュされた最後の既知のシーケンスである**ローカルシーケンスID**
  - クライアントが受信した最後のシーケンスIDである**リモートシーケンスID**
3. シーケンスが正常に複製されると、クライアントは定期的に`setCheckpoint`メッセージを送信して、（送信された最後の）ローカルシーケンスIDと（受信された最後の）リモートシーケンスIDを記録します。

## プッシュレプリケーション
プッシュレプリケーションでは、一連の変更がアクティブレプリケーターによってパッシブレプリケーターに自動的に送信されます

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/176567/bec540a3-b2fb-af49-92e6-beb4a5e2ec5d.png)

1. チェックポイントが取得された後、クライアント側のレプリケーターがローカルシーケンスID以降のローカルデータベースへの変更を検出すると、クライアントは、変更された現在の各リビジョンに対応する`change`オブジェクトの配列を含む`proposeChanges`メッセージをサーバーに送信します。これには、トゥームストーンリビジョン（ドキュメント削除情報）も含まれます。`change`オブジェクトは、JSONとしてエンコードされ、以下の内容が含まれます。
  - 変更されたドキュメントのドキュメントID
  - ドキュメントの現在のリビジョンのリビジョンID
  - 既知のサーバーリビジョンのリビジョンID（既に存在する場合、既知のサーバーリビジョンIDがない場合は省略）
  - オプションとして、ドキュメント本文のおおよそのサイズ
2. `proposeChanges`リクエストに対するサーバーの応答には、以下を含むJSONオブジェクトが含まれます。
  - ステータスコードの配列。各エントリは、`proposeChanges`リクエストで指定されたリビジョンIDに対応します。このステータスによって、そのリビジョンの状態（競合が発生しているかどうか等）が判別されます。
3. クライアントは、手順2の応答`proposeChanges`で要求されたリビジョンごとに`rev`メッセージを送信します。`rev`メッセージの本文にはJSON形式のドキュメントが含まれ、メッセージのヘッダーには以下のメタデータが含まれます。
  - `id`: ドキュメントID
  - `rev`: リビジョンID
  - `sequence`: シーケンスID
  - `history`: changes`応答で指定された既知の祖先リビジョン以降のリビジョンIDのコンマ区切りリスト

4. すべての`rev`メッセージが送信された後、連続モードでは、クライアントはローカルデータベースが変更されるのを待ち、ステップ1に戻ります。ワンショットモードでは、接続が切断され、レプリケーションが終了します。

## プルレプリケーション
プルレプリケーションでは、クライアントのアクティブレプリケーターからのプルリクエストに応答して、サーバーのパッシブレプリケーターによって一連の変更が送信されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/176567/0dc77be3-33e4-f57d-705e-29a8b91391fe.png)

1. チェックポイントが取得された後、クライアントは以下のヘッダーを含む`subchanges`メッセージをサーバーに送信します。
  - `since`: リモートシーケンスID
  - `continuous`: 連続モードかどうかを示すフィールド。`true`の値は、クライアントが変更について継続的に通知されることを望んでいることを示す。
  - `batch`: 1つのメッセージで送信される変更の最大数
2. サーバーは、変更された現在の各リビジョンに対応する`change`オブジェクトの配列`changes`を含むメッセージをクライアントに送信します。これには、トゥームストーンリビジョン（ドキュメント削除情報）が含まれます。変更自体はネストされたJSON配列としてエンコードされ、次のものが含まれます。
  - リモートシーケンスID（サーバー側での変更のシーケンスID）
  - 変更されたドキュメントのドキュメントID
  - ドキュメントの現在のリビジョンのリビジョンID
  - `isDeleted`フラグ（リビジョンがトゥームストーンであるかどうかを示すフラグ）。値が`1`の場合、トゥームストーンリビジョンであることを示す
  - オプションとして、ドキュメント本文のおおよそのサイズ
3. `changes` リクエストに対するクライアントの応答には、以下を含むJSONオブジェクトが含まれます。
  - `maxHistory`: クライアントが受け入れる履歴の最大サイズ
  - 既知の祖先の配列: 　`changes`リクエストで指定されたリビジョンIDごとに1つのエントリ。`null`値は、クライアントが対応するリビジョンに関心がないことを示す。
4. サーバーは、手順3の`changes`リクエストへの応答で要求されたリビジョンごとに`rev`メッセージを送信します。`rev`メッセージの本文にはJSON形式のドキュメントが含まれ、メッセージのヘッダーには以下のメタデータが含まれます。
  - `id`: 本文が含まれているドキュメントのドキュメントID
  - `rev`: 含まれているリビジョン
  - `sequence`: 変更のシーケンスID
  - `history`: `changes`応答で指定された既知の祖先リビジョン以降のリビジョンIDのコンマ区切りリスト
 
5. 変更の送信が完了すると、サーバーは空の`changes`メッセージを送信して、送信する変更がこれ以上ないことを示します。
6. すべての変更が送信された後、連続モードでは、サーバーが変更を待機している間、接続は開いたままになり、ステップ2に戻ります。ワンショットモードでは、接続が切断され、レプリケーションが終了します。

手順2〜4は、「クライアント」と「サーバー」の役割が入れ替わっているだけで、プッシュレプリケーションの手順1〜3と同じです。クライアントとサーバーの役割が完全に異なるコードを必要とするHTTPベースのAPIとは異なり、レプリケーションプロトコルは対称的です。

# 競合の処理
複数のソースから同じドキュメントリビジョンに対して、「同時」に更新がある場合、競合が発生する可能性があります。ここで、「同時」とは「前のレプリケーションから次のレプリケーションまでの間」を意味します。クライアントが1時間オフラインになり、戻ってきた場合、その時間中にそのクライアントに加えられた変更は、他のすべてのクライアントによって行われた変更と実質的に「同時」に行われたものとみなされます。分散システムでは、このような並行性は決して稀なものではなく、現実的なものです。

Couchbase Mobile (2.0移行)は、Conflict Free(競合のない)モードをサポートしています。このモードでは、リビジョンの保存中にクライアントが競合に遭遇すると、ドキュメントに関連付けられた競合リゾルバーコールバックが呼び出され、結果としてマージされたリビジョンが使用されます。したがって、ドキュメントが競合状態で目に見える形で存在することはありません。

# 接続ロストへの対応

レプリケーション中に、クライアントがサーバーに接続できない場合、またはサーバーへの接続が切断された場合、クライアントは指数バックオフアルゴリズムを使用して再接続を試みます。このアルゴリズムでは、再試行するたびに待機時間が長くなります。

- ワンショットレプリケーションの場合、最大2回の再試行が行われます。
- 連続レプリケーションの場合には、クライアントは無期限に再接続しようとしますが、再試行の間隔は10分を超えて増加しません。

接続ロストを検出することも一筋縄ではいかない場合があります。TCP接続がアイドル状態の場合、どちらの方向にもパケットは送信されないため、どちらのピアも、もう一方のピアが突然切断されたかどうかを判断できません。これを回避するために、レプリケーター間で、定期的に「ハートビート」メッセージを送信します。送信者がメッセージに応答して数秒以内にTCP ACKパケットを受信しない場合、送信者は接続がダウンしていることを認識します。

# 参考情報
[Introducing the New Data Replication Protocol in Couchbase Mobile 2.0](https://blog.couchbase.com/data-replication-couchbase-mobile/)