Oracle GoldenGateにおけるReplicatのパフォーマンスチューニング

この記事はOracle GoldenGate(OGG)におけるReplicatのパフォーマンスチューニングについてのtipsとなります。

以下の環境で確認したものとなりますので、環境による差異がある場合はコマンドがそのまま使用できるか等は確認いただければと思います。
| 項目 |　環境詳細 |
| --- | --- | 
| Database | Oracle DB EE 19c / 3node RAC構成 |
| GoldenGate | OGG 19c / Integrated Replicat / classic architecture |

# サマリ
- 高負荷な処理ではstreams poolがネックとなるため、streams poolが枯渇しない程度にトランザクションサイズの判定閾値や並列度のチューニングする必要があります
- replicatの性能を上げるためにはシリアル処理が必須なトランザクションをうまく分離するようにプロセスを分割し、高いパラレル度で処理させることが重要です

# メモリチューニング（streams pool）
replicatはOracle DBのメモリのうちstreams poolを使用します。streams poolが不足した場合、replicatは `ORA-4031` によりABENDしてしまうことがあるためある程度余裕を持った状態での運用が必要となります。

streams poolの使用状況は`gv$streams_pool_statistics`で確認できるため、replicatの実行中や特に処理量が多いと予想されるタイミング（初期同期時など）ではこのビューから使用状況を確認するのがおすすめです。
```sql
-- 確認用SQL
set line 300 pages 300 feed off
select
  INST_ID,
  TOTAL_MEMORY_ALLOCATED/1024/1024 as TOTAL_MEMORY_ALLOCATED,
  CURRENT_SIZE/1024/1024 as CURRENT_SIZE,
  TOTAL_MEMORY_ALLOCATED/CURRENT_SIZE * 100 as percentage
from gv$streams_pool_statistics
order by 1;
 
   INST_ID TOTAL_MEMORY_ALLOCATED CURRENT_SIZE PERCENTAGE
---------- ---------------------- ------------ ----------
         1                    466         3072       15.2
         2                   2796         3072       91.0
         3                   2669         3072       86.9
```

## replicatのメモリ使用傾向
replicatによるstreams poolの使用量は、基本的には処理レコード数に比例して伸びる傾向があります。そのため、レコード数の多いトランザクションをパラレルで処理する場合に枯渇しやすく注意が必要です。

トランザクションの大きさ、処理の並列度を考えるには、replicatにおけるlarge transactionという考え方を理解するがあります。

replicatではあるサイズ以上（`eager_size`、デフォルトで15100レコード）のトランザクションはlarge transactionに分類され、シリアルで処理されるという挙動をします。シリアルの処理とパラレルの処理では以下のような挙動の差があるため注意が必要です。
- シリアル処理
    - メモリ不足時は`ORA-4031`でエラーになりやすい
- パラレル処理
    - メモリ不足時は`wait for memory allocation`により待機する傾向がある

`eager_size`付近の大きさのトランザクションが多数ある場合、総処理量は少ないが同時処理される量は多くなる、といった状況が発生しえます。
特に`eager_size`より少し小さいトランザクションが多数ある場合はメモリ不足でreplicatがエラーになりやすく、`eager_size`や並列度の調整を行うことが必要かもしれません。
```sh
# 総処理量が少ないのに同時処理量は多い例

# 仮定
- replicatの並列度は4に設定されている
- eager_size=15100 # デフォルトの設定
DBOPTIONS INTEGRATEDPARAMS (PARALLELISM 4, EAGER_SIZE 15100)

# case 1（総処理量は少ないが、同時処理量は大きい）
総処理量 : 100,000レコード（10,000レコード * 10トランザクション）
トランザクションのレコード数はeager_size以下のため、パラレルで処理される
--> 同時処理量は 40,000レコード（10,000レコード * 4並列）となる

# case 2（総処理量は多いが、同時処理量は小さい）
総処理量 : 200,000レコード（20,000レコード * 10トランザクション）
トランザクションのレコード数はeager_size以上のため、シリアルで処理される
--> 同時処理量は 20,000レコード（20,000レコード * 1並列）となる
```

## RACにおける接続先ノードの分散
replicatは接続先インスタンスのstreams poolしか使用できないため、単一replicatでは単一インスタンスのstreams poolしか使用できません。
RAC環境である場合はreplicatを複数プロセスに分けて接続先インスタンスを別々にすることで各インスタンスのstreams poolを効率的に使用できます。

例として以下のようにRACの各インスタンスへの接続先を定義してそれに対応したreplicatプロセスを定義することで、各インスタンスのstreams poolを使用させてメモリを有効に活用することができます（プロセス分割のコマンド例は後述します）。
```sh
# 3node RACにて各インスタンスの接続用のcredentialを作成する
$ ./ggsci
GGSCI> ALTER CREDENTIALSTORE ADD USER ggadm@dbserver1:1521/instance1, ALIAS ggadm_instance1
GGSCI> ALTER CREDENTIALSTORE ADD USER ggadm@dbserver2:1521/instance2, ALIAS ggadm_instance2
GGSCI> ALTER CREDENTIALSTORE ADD USER ggadm@dbserver3:1521/instance3, ALIAS ggadm_instance3

# 各replicatに上述のcredentialを割り当てる
------------------------------------------
REPLICAT rep1
USERIDALIAS ggadm_instance1
...（略）...
------------------------------------------
REPLICAT rep2
USERIDALIAS ggadm_instance2
...（略）...
------------------------------------------
REPLICAT rep3
USERIDALIAS ggadm_instance3
...（略）...
------------------------------------------
```

# パラレル実行のチューニング
## パラレル実行の仕様
replicatはパラレル実行が可能であり基本的に`PARALLELISM`のパラメータで制御されます。具体的には、`PARALLELISM`の値に応じて以下のように実際のプロセス数が変動します。

- `PARALLELISM`が1の場合
    - パラレル実行にはならず常にシリアルでの実行になる
- `PARALLELISM`が1より大きい場合
    - 負荷を加味しつつ`PARALLELISM`（デフォルト4）以上`MAX_PARALLELISM`（デフォルト50）以下のプロセス数で動作する。これは`PARALLELISM_INTERVAL`（デフォルト5秒）毎に再計算され、プロセスの増減がされる
    - 適用するトランザクションのレコード数が`eager_size`（デフォルト15100）を超える場合、large transaction扱いなり常にシリアルで適用される
    - 例外としてシーケンス操作、DDLなどは常にシリアルで適用される（詳細はMOSのDoc 2349663.1を参照ください） 

replicatのパフォーマンスを上げるにはパラレル度を上げて並列で動作させることは重要となります。
並列度を高く設定してもパラレル実行不可なトランザクションが混在するとシリアル実行に切り替わってしまいパフォーマンスが上がらないため、上述のようなパラレル実行できない処理（large transaction, シーケンス操作, DDL）はそれ専用のプロセスに分けられると効率が良くなります。

### large transactionの補足
通常、replicatはパラレルで適用するために各トランザクション（LCR）の末尾のCommit recordを見て各トランザクションの依存関係を確認する必要があります。しかし、large transactionではCommit recordを見ずに先頭から適用しています（これがDirect modeでの適用と呼ばれる）。

このような挙動は、おそらくはトランザクションのサイズが大きすぎる場合にCommit recordを見て依存関係を確認する部分のオーバーヘッドが高いためのものと思われます。

## replicat処理状況の確認方法
replicatの動作状況は以下のように`gv$gg_apply_server`ビューにて確認可能です。

以下の例では、`SERVER_ID`が各replicatのパラレル実行される子プロセス、`TOTAL_MESSAGES_APPLIED`が処理されたメッセージ数となります。この例では
REP1は各プロセスの処理メッセージ数が均等に伸びておりうまくパラレルで動作できていることが読み取れますが、一方でREP2, 3は処理メッセージ数がほぼ1-2プロセスに偏っておりほとんどシリアルで動作してしまっており非効率なことがわかります。
```sql
set line 200 pages 200
break on inst_id on apply_name
select inst_id, apply_name, server_id, state, total_messages_applied
from gv$gg_apply_server order by 2,3;
 
INST_ID APPLY_NAME   SERVER_ID STATE                TOTAL_MESSAGES_APPLIED
------- ----------- ---------- -------------------- ----------------------
      1 OGG$REP1             0 IDLE                                9535516
                             1 IDLE                                 329179
                             2 IDLE                                3884629
                             3 IDLE                                3517435
                             4 IDLE                                3711645
      2 OGG$REP2             0 IDLE                                      0
                             1 EXECUTE TRANSACTION                16279952  -- トランザクション実行中
                             2 IDLE                                      0
                             3 IDLE                                      0
                             4 IDLE                                      0
                             5 INACTIVE                                  0
      3 OGG$REP3             0 IDLE                                      0
                             1 IDLE                                9408427
                             2 EXECUTE TRANSACTION                 9010171  -- トランザクション実行中
                             3 IDLE                                      0
                             4 IDLE                                      0
                             5 INACTIVE                                  0
                             6 INACTIVE                                  0
```
### シリアル処理（large transaction）の調査
replicatの処理性能が出ないケースでは上記のように特定の子プロセスに処理が偏り、並列度が低くなってしまっていることが多々あります。処理が偏ってしまう際はlarge transactionがネックになっている可能性が高いため、large transactionの有無及び頻度、サイズの確認が重要です。

もっとも簡単なlarge transactionの確認方法としては、以下のようにログ（ggserr.log）にlarge transactionの情報が出力されるためこちらで確認するのが良いでしょう。ログを確認することでどの程度の大きさのlarge transactionが含まれているかがわかります。
```
2022-08-22T19:43:38.607+0900  INFO    OGG-25221  Oracle GoldenGate Delivery for Oracle, rep1.prm:  Processing transaction (XID: 20.28.41565525, Seqno: 70258, RBA: 18628234) larger than eager size (15,100).
2022-08-22T19:52:53.451+0900  INFO    OGG-25222  Oracle GoldenGate Delivery for Oracle, rep1.prm:  Large transaction completed. Total records: 1,482,854.
```

より詳細には`gv$gg_apply_server`での確認するのが良いと思われます。もしlarge transactionがある場合、シリアルで適用されるため1つのプロセスのみが`EXECUTE TRANSACTION`になり、他のプロセスは`WAIT DEPENDENCY`で待機させられます。

例として、以下のようなSTATEになっている場合はlarge transactionが処理されており他の子プロセスが待機してしまっている状態の可能性が高いです。
```sql
set line 200 pages 200
break on inst_id on apply_name
select inst_id, apply_name, server_id, state
from gv$gg_apply_server
where inst_id = 2
order by 2,3;
 
INST_ID APPLY_NAME   SERVER_ID STATE              
------- ----------- ---------- --------------------
      2 OGG$REP2             0 WAIT DEPENDENCY     -- large transaction待ち
                             1 EXECUTE TRANSACTION
                             2 WAIT DEPENDENCY     -- large transaction待ち
                             3 WAIT DEPENDENCY     -- large transaction待ち
                             4 WAIT DEPENDENCY     -- large transaction待ち
                             5 INACTIVE           
```

`WAIT DEPENDENCY`で待機している/させているオブジェクトの特定は以下のように`gv$goldengate_table_stats`の統計値を見ることで確認が可能です。

`gv$goldengate_table_stats`のwait_dependenciesが大きい場合、他の子プロセスのlarge transactionの実行に阻害されて待機させられています。性能を改善したい場合は待機の原因となっているオブジェクトを別プロセスに分離することで、待機を解消し並列度を上げるといった方法が考えられます。
```sql
set tab off line 1000 pages 200 feed off
select
  apply_name,
  source_table_owner,
  source_table_name,
  destination_table_owner,
  destination_table_name,
  sum(total_inserts + total_updates+total_deletes) Total_Operations,
  sum(wait_dependencies) Wait_Dependencies
from gv$goldengate_table_stats
group by apply_name, source_table_owner, source_table_name, destination_table_owner, destination_table_name
order by 1,2,3,4,5;

APPLY_NAME  SOURCE_TABLE_OWNER  SOURCE_TABLE_NAME  DESTINATION_TABLE_OWNER  DESTINATION_TABLE_NAME  TOTAL_OPERATIONS WAIT_DEPENDENCIES
----------- ------------------- ------------------ ------------------------ ----------------------- ---------------- -----------------
OGG$REP1    FINANCE             TABLE_1            HR                       TABLE_1                         20597978                 0
OGG$REP2    FINANCE             TABLE_2            HR                       TABLE_2                         17227673             45872 -- wait dependenciyでの待機が増えており競合がありうる
OGG$REP2    FINANCE             TABLE_3            HR                       TABLE_3                         19574793                 0

-- 上記以外にも様々な統計値が確認できるため、以下のような項目も調べるとよいかと思います
set tab off line 1000 pages 200 feed off
select
  apply_name,
  source_table_owner,
  source_table_name,
  destination_table_owner,
  destination_table_name,
  sum(total_inserts + total_updates+total_deletes) Total_Operations,
  sum(total_inserts)     Total_Inserts,
  sum(total_updates)     Total_Updates,
  sum(total_deletes)     Total_Deletes,
  sum(insert_collisions) Insert_Collisions,
  sum(update_collisions) Update_Collisions,
  sum(delete_collisions) Delete_Collisions,
  sum(reperror_records)  Reperror_Discards,
  sum(reperror_ignores)  Reperror_Ignores,
  sum(wait_dependencies) Wait_Dependencies
from gv$goldengate_table_stats
group by apply_name, source_table_owner, source_table_name, destination_table_owner, destination_table_name
order by 1,2,3,4,5;

APPLY_NAME  SOURCE_TABLE_OWNER  SOURCE_TABLE_NAME  DESTINATION_TABLE_OWNER  DESTINATION_TABLE_NAME  TOTAL_OPERATIONS TOTAL_INSERTS TOTAL_UPDATES TOTAL_DELETES INSERT_COLLISIONS UPDATE_COLLISIONS DELETE_COLLISIONS REPERROR_DISCARDS REPERROR_IGNORES WAIT_DEPENDENCIES
----------- ------------------- ------------------ ------------------------ ----------------------- ---------------- ------------- ------------- ------------- ----------------- ----------------- ----------------- ----------------- ---------------- -----------------
OGG$REP1    FINANCE             TABLE_1            HR                       TABLE_4                         20597978      20597978             0             0                 0                 0                 0                 0                0                 0
OGG$REP2    FINANCE             TABLE_2            HR                       TABLE_5                         17227673      17227673             0             0                 0                 0                 0                 0                0                 0
OGG$REP3    FINANCE             TABLE_3            HR                       TABLE_6                         19574793        906089      18668704             0                 0                 0                 0                 0                0                 0
```
### シリアル処理（シーケンス操作、DDL）の調査
シーケンスやDDLも強制的にシリアル処理となる処理ですが、これらが原因で待機が発生しているかは`gv$gg_apply_receiver`に手がかりがあるケースが多いです。

私がこれまで構築した中ではプロセスが`Waiting for Replicat flush request to complete`での待機はシーケンスやDDLで待たされているケースが多く、これらの待機が見受けられる場合はシーケンスやDDLがネックになっていないか疑った方が良いでしょう。
```sql
set line 400 pages 200 tab off
select
  apply_name,
  total_messages_received,
  total_available_messages,
  state rcvstate,
  last_received_msg_position,
  acknowledgement_position
from gv$gg_apply_receiver
order by 1;
 
APPLY_NAME  TOTAL_MESSAGES_RECEIVED TOTAL_AVAILABLE_MESSAGES RCVSTATE                                       LAST_RECEIVED_MSG_PO ACKNOWLEDGEMENT_POSI
----------- ----------------------- ------------------------ ---------------------------------------------- -------------------- --------------------
OGG$REP1                   21837584                        0 Waiting for message from client                96272047041          96272046238
OGG$REP2                   19056004                       37 Waiting for Replicat flush request to complete 96271648329          96271648329  -- シーケンスかDDLによる待機
OGG$REP3                   20589910                        0 Waiting for message from client                96271972353          96271662925
```

上記の方法以外には、DDLが適用されている場合はggserr.logに該当DDL文が出力されるためログの確認が最も楽な手段となります。

シーケンスやDDLがネックになるケースでもlarge transactionの際と同様にプロセスを分割して対象オブジェクトを分けることで性能改善できる可能性があります。

## その他のパラレル実行時の注意
ワークロードによってはパラレル実行する際にdead lockが発生するケースがありえます。このケースは厄介で、replicatがdead lockで待機してハングしたような状態になってしまいます（おそらくreplicatはstopも受け付けないでしょう）。

dead lockが出ている場合はアラートログにdead lockが出た旨のエラーが出ているはずですので、アラートログの確認が有効です。ある程度は並列度を下げることでdead lockは抑止できるため、並列度の調整が必要になるでしょう。
性能面で許容できるならシリアルにしてしまえば確実に発生は防げます。

# Replicatの操作例
## パラメータの変更（PARALLELISM, EAGER_SIZE）
この記事で紹介した`PARALLELISM`や`eager_size`を変更する場合、replicatのパラメータファイルに以下のように記載します。
```
DBOPTIONS INTEGRATEDPARAMS (PARALLELISM 8, EAGER_SIZE 25000)
```

`DBOPTIONS INTEGRATEDPARAMS`のその他のパラメータは以下を参照ください。

> https://docs.oracle.com/cd/F22974_01/oracle-db/optional-parameters-integrated-modes.html#GUID-7910D870-20C6-4842-A709-9A89111140F2

## プロセスの分割例
以下では例として単一プロセス構成でのreplicatを3プロセスに分割します。各プロセスはRACの別々のインスタンスに接続する形とします。

まず、各ノード指定で接続するためのcredentialを追加します。
```sh
$ ./ggsci
GGSCI> ALTER CREDENTIALSTORE ADD USER ggadm@dbserver1:1521/instance1, ALIAS ggadm_instance1
GGSCI> ALTER CREDENTIALSTORE ADD USER ggadm@dbserver2:1521/instance2, ALIAS ggadm_instance2
GGSCI> ALTER CREDENTIALSTORE ADD USER ggadm@dbserver3:1521/instance3, ALIAS ggadm_instance3

# 各replicatに上述のcredentialを割り当てる
------------------------------------------
REPLICAT rep1
USERIDALIAS ggadm_instance1
...（略）...
```

続いて既存のreplicatを停止して静止点を作り、Trail, RBAの位置を確認します。確認した値でreplicatプロセスの位置を調整しつつプロセス追加します。
```sh
$ ./ggsci
GGSCI> stop replicat rep
GGSCI> info replicat rep
  
# ここではLog Read Checkpointのトレイルファイルの番号とRBAを確認する。以下のように出力されているはず
REPLICAT   REP       Last Started 2022-07-01 11:18   Status RUNNING
INTEGRATED
Checkpoint Lag       142:03:51 (updated 00:30:29 ago)
Process ID           75261
Log Read Checkpoint  File /gg_trail/trails/tr000012575         # <-- ここのファイルの番号の部分（12575）
                     2022-06-28 20:10:03.310164  RBA 26483779  # <-- ここのRBAの値（26483779）
  
GGSCI> add replicat rep1, integrated, exttrail /gg_trail/trails/tr, extseqno [trailファイル番号], extrba [RBA番号]
GGSCI> add replicat rep2, integrated, exttrail /gg_trail/trails/tr, extseqno [trailファイル番号], extrba [RBA番号]
GGSCI> add replicat rep3, integrated, exttrail /gg_trail/trails/tr, extseqno [trailファイル番号], extrba [RBA番号]
 
例）
GGSCI> add replicat rep1, integrated, exttrail /gg_trail/trails/tr, extseqno 12575, extrba 26483779
```

分割元のreplicatの適用済みSCNを確認し、aftercsn指定で分割後のreplicatを起動します。
```sql
$ sqlplus / as sysdba
SQL> alter session set container=[pdbname];
SQL> col SERVER_NAME for a15
     col APPLIED_HIGH_POSITION for a20
     select SERVER_NAME, APPLIED_HIGH_POSITION from dba_gg_inbound_progress;
 
SERVER_NAME  APPLIED_HIGH_POSITIO
------------ --------------------
OGG$REP      96272266383
```
```sh
# 上記で確認したAPPLIED_HIGH_POSITIONを適用済みのSCNとして使う
# afterscn付きでstartしないと適用するトランザクションの重複等でエラーになるため注意する
$ ./ggsci
GGSCI> start replicat rep1, afterscn [上記のAPPLIED_HIGH_POSITIO]
GGSCI> start replicat rep2, afterscn [上記のAPPLIED_HIGH_POSITIO]
GGSCI> start replicat rep3, afterscn [上記のAPPLIED_HIGH_POSITIO]
 
例）
GGSCI> start replicat rep1, afterscn 96272266383
```