Oracle GoldenGate自動競合検出および解消機能の紹介 その2

# はじめに
前回から引き続き、Oracle GoldenGateの自動競合検出および解消機能(Automatic Conflict Detection and Resolution)(以降Auto CDRと記載)を紹介します。本記事では、実機を用いて、Auto CDRの有効化方法と競合解決履歴データの確認方法、運用時の考慮事項を紹介します。

# 検証環境構成
検証環境の構成を下記に示します。本環境では、データベース、Oracle GoldenGateのプロセスは作成済みとなります。本検証環境では、2台のOracle Linux7.9のサーバー(ホスト名はdb11とdb31)を、Oracle GoldenGateのソース兼ターゲットとして使用しています。以降の手順において、こちらのホスト名が度々登場するため、頭の片隅においておくと理解がしやすいと思います。
![img02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3330820/7e985c6a-9faf-ed2a-0083-abb085c5c273.png)


# Auto CDR動作検証
Auto CDRの動作確認を下記の流れで進めていきます。
下記アスタリスクをつけた箇所がAuto CDR固有の手順となります。

* テーブル作成
* サプリメンタルロギング設定
* パラメータファイル修正
* Auto CDR有効化(*)
* プロセス起動
* 競合発生および解決確認(*)
* 競合履歴の確認(*)

以降に実行した際のログを記載していますが、冗長な表現にならないよう、出力メッセージは適宜修正しています。予め、ご了承ください。

## テーブル作成
sqlplusを起動しPDBに接続後、anglerスキーマを作成し、権限を付与します。(db11とdb31にて実施する事)
```
SQL> CREATE USER angler;
ユーザーが作成されました。

SQL> PASSWORD angler
anglerに対するパスワードを変更しています。
新規パスワード:
新規パスワードを再入力してください:
パスワードが変更されました。

SQL> GRANT connect, resource, unlimited tablespace TO angler;
権限付与が成功しました。
```
続いてanglerスキーマにfishテーブルを作成します。(db11とdb31にて実施する事)
```
SQL> CREATE TABLE angler.fish (
  2      id NUMBER,
  3      name VARCHAR2(100),
  4      CONSTRAINT pk_fish PRIMARY KEY(id)
  5  );
表が作成されました。
```

## サプリメンタルロギング設定
ggsciを起動し、DBLOGINコマンドにてPDBにログインします。本環境では、gg_pdbというエイリアスを使用して、PDBに接続しています。PDB接続後、ADD TRANDATAコマンドを用いて、fishテーブルのサプリメンタルロギングを有効化します。(db11とdb31にて実施する事)
```
$ cd $GG_HOME
$ ./ggsci
GGSCI 1> DBLOGIN USERIDALIAS gg_pdb
Successfully logged into database.

GGSCI 2> ADD TRANDATA angler.fish
INFO    OGG-15132  Logging of supplemental redo data enabled for table ANGLER.FISH.
INFO    OGG-15133  TRANDATA for scheduling columns has been added on table ANGLER.FISH.
INFO    OGG-15135  TRANDATA for instantiation CSN has been added on table ANGLER.FISH.
INFO    OGG-10471  ***** Oracle Goldengate support information on table ANGLER.FISH *****
Oracle Goldengate support native capture on table ANGLER.FISH.
Oracle Goldengate marked following column as key columns on table ANGLER.FISH: ID.
```

### パラメータファイル修正
Oracle GoldenGateの各プロセスのパラメータファイルを修正します。

##### Primary Extract(キャプチャ)用パラメータ
下記パラメータを追加します。 (db11のpe1.prmとdb31のpe3.prmに追加する事)
| # | 設定値 | 説明 |
|:-|:-|:-|
|1   |TABLE angler.fish;    |anglerスキーマのfishテーブルをレプリケーション対象として設定します。   |

##### Data Pump用パラメータ
下記パラメータを追加します。 (db11のdp1.prmとdb31のdp3.prmに追加する事)
| # | 設定値 | 説明 |
|:-|:-|:-|
|1   |TABLE angler.fish;    |anglerスキーマのfishテーブルをレプリケーション対象として設定します。   |

##### Replicat用パラメータ
下記パラメータを追加します。 (db11のrp1.prmとdb31のrp3.prmに追加する事)
| # | 設定値 | 説明 |
|:-|:-|:-|
|1   |MAP angler.fish, TARGET angler.fish;    |anglerスキーマのfishテーブルをレプリケーション対象として設定します。   |

### Auto CDR有効化
sqlplusを起動し、PDBに接続後、dbms_goldengate_admパッケージのadd_auto_cdrプロシージャを実行します。対象テーブルをAuto CDR対象に設定します。(db11とdb31にて実施する事)
* schema_nameパラメータに、対象スキーマのanglerを指定します。
* table_nameパラメータに、対象テーブルのfishを指定します。
* record_conflictsパラメータに、TRUEを設定します。こちらは競合が発生した際にデータディクショナリに競合のログを記録させるためとなります。
```
SQL> BEGIN
  2      dbms_goldengate_adm.add_auto_cdr(
  3          schema_name => 'ANGLER',
  4          table_name => 'FISH',
  5          record_conflicts => TRUE
  6      );
  7  END;
  8  /
PL/SQLプロシージャが正常に完了しました。
```
なお、データが格納されている既存テーブルをAuto CDR対象として追加する場合は、existing_data_timestamp => TO_TIMESTAMP(‘任意の日付’, ‘yyyy-mm-dd hh24:mi:ss’)パラメータを追加する必要があります。理由は後述します。

Auto CDR対象のテーブル一覧は、dba_gg_auto_cdr_tablesにて確認可能です。
```
SQL> COL table_owner FORM a15
SQL> COL table_name FORM a15
SQL> COL tombstone_table FORM a15
SQL> COL row_resolution_column FORM a20
SQL> SELECT table_owner, table_name, tombstone_table, row_resolution_column, existing_data_timestamp FROM dba_gg_auto_cdr_tables;

TABLE_OWNER     TABLE_NAME      TOMBSTONE_TABLE ROW_RESOLUTION_COLUM EXISTING_DATA_TIMESTAMP
--------------- --------------- --------------- -------------------- ---------------------------------------------------------------------------
ANGLER          FISH            DT$_FISH        CDRTS$ROW            21-10-01 15:00:00.000000
```

Auto CDRを有効化した事により、非表示のタイムスタンプ列が追加されている事を確認します。 sqlplusのシステム変数COLINVISIBLEにON を設定すると、DESCRIBEコマンド実行時に非表示列を表示させる事ができます。
```
SQL> SET colinvisible ON
SQL> DESC angler.fish
 名前                                    NULL?    型
 ----------------------------------------- -------- ----------------------------
 ID                                        NOT NULL NUMBER
 NAME                                               VARCHAR2(100)
 CDRTS$ROW (INVISIBLE)                     NOT NULL TIMESTAMP(6)
```
また、ツースムストンテーブルが作成されている事を確認します。
```
SQL> DESC angler.dt$_fish;
 名前                                    NULL?    型
 ----------------------------------------- -------- ----------------------------
 ID                                                 NUMBER
 DELTIME$$                                          TIMESTAMP(6)
```
### プロセス起動
ggsciを起動し、Oracle GoldenGateのプロセスを起動します。(db11とdb31にて実施する事)
下記はdb11環境の実行例です
```
$ ./ggsci
GGSCI 1> START ER *
Sending START request to MANAGER ...
EXTRACT DP1 starting

Sending START request to MANAGER ...
EXTRACT PE1 starting

Sending START request to MANAGER ...
REPLICAT RP1 starting

GGSCI 2> INFO ALL
Program     Status      Group       Lag at Chkpt  Time Since Chkpt
MANAGER     RUNNING
EXTRACT     RUNNING     DP1         00:00:00      00:00:04
EXTRACT     RUNNING     PE1         00:00:00      00:00:03
REPLICAT    RUNNING     RP1         00:00:00      00:00:04
```
### 競合発生および解決確認
INSERT ROW EXISTS競合を発生させるため、db11とdb31のそれぞれに同一キーのデータをINSERTします。Oracle GoldenGateがターゲットDBへレプリケーションする前に、INSERTする必要があります。
db11にて実行します。
```
SQL> INSERT INTO angler.fish (id, name) VALUES (1,'セイゴ');
1行が作成されました。
```
続いて、db31にて実行します。
```
SQL> INSERT INTO angler.fish (id, name) VALUES (1,'フッコ');
1行が作成されました。
```
db11とdb31の両方で、できるだけ同時に実行します。
```
SQL> COMMIT;
コミットが完了しました。
```
db31のINSERT処理が、あとに実行されているため、db11の更新データが破棄されて、db31の更新データが採用される動作となるはずです。想定通りの結果となっているかどうか、fishテーブルを確認します。
```
SQL> SET LINES 120
SQL> COL name FORM a15
SQL> SELECT id, name, cdrts$row FROM angler.fish;

        ID NAME            CDRTS$ROW
---------- --------------- -----------------------------
         1 フッコ          21-10-02 01:15:00.450450
```
想定通り、db31のデータが採用されて、db11とdb31のデータが一致している事が確認できました。

### 競合履歴の確認
競合履歴は、下記データディクショナリビューから確認可能です。

* dba_apply_error_messages
* dba_apply_error

db11からdb31へのレプリケーション時の競合履歴は、db31側のデータディクショナリビューに格納されます。
db31からdb11へのレプリケーション時の競合履歴は、db11側のデータディクショナリビューに格納されます。

db11のdba_apply_error_messagesを確認すると以下の事がわかります。

* OBJECT_OWNERおよびOBJECT_NAME列の情報から、angler.fishテーブルにて競合が発生した。
* APPLIED_STATE列の情報から、db31のデータが採用(WON)された。
* ERROR_MESSAGE列の情報から、キー違反のエラーが発生した。

```
SQL> SET LINES 180
SQL> COL local_transaction_id FORM a20
SQL> COL error_message FORM a50
SQL> COL object_owner FORM a12
SQL> COL object_name FORM a11
SQL> COL conflict_type FORM a17
SQL> COL applied_state FORM a13
SQL> SELECT local_transaction_id,
  2         object_owner,
  3         object_name,
  4         conflict_type,
  5         applied_state,
  6         error_message
  7    FROM dba_apply_error_messages;

LOCAL_TRANSACTION_ID OBJECT_OWNER OBJECT_NAME CONFLICT_TYPE     APPLIED_STATE ERROR_MESSAGE
-------------------- ------------ ----------- ----------------- ------------- --------------------------------------------------
1.33.1008            ANGLER       FISH                          WON           ORA-00001: 一意制約(ANGLER.PK_FISH)に反しています

```
同様に、db31のdba_apply_error_messagesのSQL実行結果を下記表に記載します。
* OBJECT_OWNERおよびOBJECT_NAME列の情報から、angler.fishテーブルにて競合が発生した。
* CONFLICT_TYPE列の情報から、INSERT ROW EXISTSが発生した。
* APPLIED_STATE列の情報から、db11のデータが破棄(LOST)された。
* ERROR_MESSAGE列の情報から、キー違反のエラーが発生した。

```
LOCAL_TRANSACTION_ID OBJECT_OWNER OBJECT_NAME CONFLICT_TYPE     APPLIED_STATE ERROR_MESSAGE
-------------------- ------------ ----------- ----------------- ------------- --------------------------------------------------
5.9.2585             ANGLER       FISH        INSERT ROW EXISTS LOST          ORA-26799: 列値ANGLERを持つ表PK_FISH.ANGLERで、一
                                                                              意制約FISH.("ID") = (1)に違反しました
                                                                              ORA-00001: 一意制約(ANGLER.PK_FISH)に反しています
```

# Auto CDR運用の考慮事項
Auto CDRを運用するにあたっての考慮事項をいくつか紹介します。

### 競合履歴のメンテナンス
前述の通り、競合の履歴は、データディクショナリビューを用いて参照可能ですが、蓄積され続けるため、定期的に削除する運用が必要となります。dbms_apply_admパッケージのプロシージャを実行する事により、競合履歴を任意のタイミングで削除可能です。

競合履歴を全部削除する場合は、dbms_apply_adm.delete_all_errors(apply_name=>'OGG$<Replicat名>')を実行します。
db31の実行例を示します。
```
SQL> EXECUTE dbms_apply_adm.delete_all_errors(apply_name=>'OGG$rp3')
PL/SQLプロシージャが正常に完了しました。
```
特定トランザクションの履歴のみを削除する場合は、dbms_apply_adm.delete_error(local_transaction_id=>'dba_apply_error_messages.local_transaction_id列値')を実行します。
db11の実行例を示します。
```
SQL> EXECUTE dbms_apply_adm.delete_error(local_transaction_id=>'1.33.1008')
PL/SQLプロシージャが正常に完了しました。
```
なお、ご使用のOracle GoldenGateのバージョンによっては、Replicat起動時に競合履歴が削除される動作となるようです。競合履歴を保管する場合は、データディクショナリビューのデータを定期的に退避する運用が必要になります。なお、本検証環境のOracle GoldenGate19.1.0.0.210720では、Replicat起動時に競合履歴が削除される動作は確認できませんでした。また、指定した時刻以前の履歴を削除できると便利ですが、残念ながらOracle Database 19c 19.12.0.0.0においては、実装されていませんでした。

### ツームストンテーブルのメンテナンス
Auto CDR対象テーブルのデータを削除すると、キー値とタイムスタンプがツームストンテーブルに蓄積されるため、ツームストンテーブルの不要データを削除する運用が必要になります。
削除方法は、dbms_goldengate_admパッケージのpurge_tombstonesプロシージャを実行する事により、指定した時刻以前のデータが削除されます。
```
SQL> EXECUTE dbms_goldengate_adm.purge_tombstones('21-10-02 02:00:00.000000 GMT');
PL/SQLプロシージャが正常に完了しました。
```

### 既存テーブルをAuto CDR対象に追加する際の考慮事項
データが格納されているテーブルをAuto CDRの対象に追加する場合は注意が必要です。前述の通り、dbms_goldengate_adm.add_auto_cdrプロシージャを実行する事により、指定したテーブルをAuto CDR対象に追加可能ですが、プロシージャのデフォルト動作では、非表示のタイムスタンプ列の初期値は、プロシージャ実行時のタイムスタンプが設定されてしまいます。
例えば、add_auto_cdrプロシージャを、db11では、2021-10-02 02:10に実行し、db31では、2021-10-02 02:11に実行した場合は下記が、CDRTS\\$ROW列の初期値に設定されます。

db11側
```
SQL> SELECT id, name, cdrts$row FROM angler.fish;

        ID NAME                 CDRTS$ROW
---------- -------------------- ------------------------
         1 アジ                 21-10-02 02:10:27.481142
         2 サバ                 21-10-02 02:10:27.481142
         3 マグロ               21-10-02 02:10:27.481142
```
db31側
```
SQL> SELECT id, name, cdrts$row FROM angler.fish;

        ID NAME                 CDRTS$ROW
---------- -------------------- ------------------------
         1 アジ                 21-10-02 02:11:09.289390
         2 サバ                 21-10-02 02:11:09.289390
         3 マグロ               21-10-02 02:11:09.289390
```
上記の通り、CDRTS\\$ROW列値が異なる状態にて、db11のid=1のデータをUPDATEすると、競合を誤検出してしまいます。以降に競合誤検出の例を示します。

| 時刻 | db11 | db31 |
|:-|:-|:-|
|10:00:00   |UPDATE angler.fish SET name = 'スズキ' WHERE id = 1;<BR>1行が更新されました。<BR><BR>COMMIT;<BR>コミットが完了しました。<BR><BR>[補足]<BR>上記SQL実行により、非表示のタイムスタンプ列が、2:10:27から10:00:00へ更新される。   |   |
|10:00:02   |   |[競合の誤検出]<BR>db11の更新前タイムスタンプ列値02:10:27とdb31のタイムスタンプ列値02:11:09と異なるため、UPDATE ROW EXISTS競合が発生する。<BR><BR>[競合の解消]<BR>db11の更新後タイムスタンプ列値10:00:00が、db31のタイムスタンプ列値2:11:09よりも新しいため、db31のデータをUPDATEする。   |
|10:00:04   |SELECT name FROM angler.fish WHERE id = 1;<BR> NAME <BR> ----------<BR> スズキ   |SELECT name FROM angler.fish WHERE id = 1;<BR> NAME <BR> ----------<BR> スズキ   |

上記例では、データの不整合は発生しませんが、競合が発生した履歴が、データディクショナリビューに記録されてしまい、混乱を招く可能性があります。

競合の誤検出を防止するため、データ格納済みの既存テーブルをAuto CDRの対象に追加する際は、dbms_goldengate_adm.add_auto_cdrプロシージャのexisting_data_timestampパラメータに、各DBに同じタイムスタンプを明示的に設定する事を推奨します。
db11とdb31に同一値を設定する例(CDRTS\\$ROW列値は、UTC管理のため、-9:00が表示されます)
```
SQL> BEGIN
  2      dbms_goldengate_adm.add_auto_cdr(
  3          schema_name => 'ANGLER',
  4          table_name => 'FISH',
  5          record_conflicts => TRUE,
  6          existing_data_timestamp => TO_TIMESTAMP('2021-10-02 00:00:00','yyyy-mm-dd hh24:mi:ss')
  7      );
  8  END;
  9  /
PL/SQLプロシージャが正常に完了しました。

SQL> SELECT id, name, cdrts$row FROM angler.fish;
        ID NAME                 CDRTS$ROW
---------- -------------------- ------------------------
         1 スズキ               21-10-01 15:00:00.000000
         2 サバ                 21-10-01 15:00:00.000000
         3 マグロ               21-10-01 15:00:00.000000
```
# おわりに
本記事では、Auto CDRの有効化方法と競合解決履歴データの確認方法、運用時の考慮事項を紹介しました。
従来のCDRを採用した場合においても列値の大小を比較した競合解決が可能ですが、比較対象列がテーブルに存在する必要がありました。Auto CDRでは、非表示のタイムスタンプ列による時刻ベースの競合解決が可能であるため、比較的導入しやすい機能であると考えます。