Amazon Aurora DSQL を使ってみる (2) リージョン間同期の遅延確認

# 1. はじめに
- 前回の記事「[Amazon Aurora DSQL を使ってみる (1) 基本的な操作](https://qiita.com/mksamba/items/ddf3d819adcf92c199f2)」にて、Aurora DSQLの基本的な操作手順(DSQLクラスターへの接続およびSQL文実行)を確認した。
- Aurora DSQLの気になるポイントとして、「us-east-1で書き込まれた内容がus-east-2にどれくらい速く反映されるのか？」がある。
- すごく速く同期される仕組みの中身を理解するのは難しいが、まずは自分なりの方法でどれくらい速いのか測定してみる。

:::note warn
以下、記事内に遅延の値などを記載しているが、あくまで「このスクリプトを実行したらそのタイミングではこういう値だった」的なレベルのもので、サービスの性能などを厳密に測定したりしているものではない。
:::

# 2. やったこと

1. リージョン間遅延(ping)
    - そもそもus-east-1とus-east-2が、ネットワーク的にどれくらい離れているのか確認する(単純にEC2インスタンス間のping)。
2. EC2インスタンス - Aurora DSQL間の遅延
    - us-east-1のEC2インスタンスから同リージョンのDSQLクラスター、及び別リージョン(us-east-2)のDSQLクラスターへINSERTした時の遅延を確認する。
3. Aurora DSQL クラスター間のデータ同期遅延
    - 【今回の本題】us-east-1のEC2インスタンスからus-east-1のDSQLクラスターへINSERTした後、us-east-2のEC2インスタンスからus-east-2のDSQLクラスターへSELECTして、リージョン間でどれくらい速く同期されているのかを確認する。
4. Aurora Global Database の非同期レプリケーション遅延
    - 比較対象として、us-east-1のEC2インスタンスからus-east-1のAurora Global Database(Primary)へINSERTした後、us-east-2のEC2インスタンスからus-east-2のAurora Global Database(Secondary)へSELECTして、リージョン間でどれくらい速くレプリケーションされているのかを確認する。

# 3. 構成図

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/189302/77082075-ffdd-4e4c-bdcf-2c8016c1c478.png)

- Aurora DSQL を作成したのと同じリージョンにSQL文実行用のEC2インスタンス(Amazon Linux 2023, t2.micro)をそれぞれ用意する。
- 比較参考用に、Aurora DSQL を作成したのと同じリージョンに、Aurora Global Database(PostgreSQL 16.4, r5.large) を、us-east-1(primary), us-east-2(secondary)として作成する。

# 4. 手順

## 4.1 リージョン間遅延(ping)

- そもそもus-east-1とus-east-2がどれくらい離れているかを確認するため、それぞれのリージョンにあるインスタンス間でのpingを行う(EIP利用)。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/189302/214df1da-194e-4422-8dc2-70d43112a84c.png)

```
[ec2-user@ip-10-0-0-110 ~]$ ping x.x.x.x (us-east-2インスタンスのEIP)
PING x.x.x.x (x.x.x.x) 56(84) bytes of data.
64 bytes from x.x.x.x: icmp_seq=1 ttl=121 time=13.7 ms
64 bytes from x.x.x.x: icmp_seq=2 ttl=121 time=13.0 ms
64 bytes from x.x.x.x: icmp_seq=3 ttl=121 time=13.0 ms
64 bytes from x.x.x.x: icmp_seq=4 ttl=121 time=12.4 ms
64 bytes from x.x.x.x: icmp_seq=5 ttl=121 time=12.9 ms
64 bytes from x.x.x.x: icmp_seq=6 ttl=121 time=12.9 ms
64 bytes from x.x.x.x: icmp_seq=7 ttl=121 time=12.6 ms
64 bytes from x.x.x.x: icmp_seq=8 ttl=121 time=13.2 ms
64 bytes from x.x.x.x: icmp_seq=9 ttl=121 time=13.5 ms
64 bytes from x.x.x.x: icmp_seq=10 ttl=121 time=13.2 ms
^C
--- x.x.x.x ping statistics ---
10 packets transmitted, 10 received, 0% packet loss, time 9014ms
rtt min/avg/max/mdev = 12.420/13.049/13.719/0.360 ms
```

- us-east-1とus-east-2間のpingは往復13ms程度。
- 参考まで、東京(ap-northeast-1)と大阪(ap-northeast-3)のインスタンス間でpingすると往復10ms程度なので、将来的にDSQLが日本でリリースされ、東京と大阪で利用する場合にも同程度の遅延レベル感になると想定。

## 4.2 EC2インスタンス - Aurora DSQL間の遅延

- EC2インスタンス(Python)からDSQLへINSERTする際の遅延を測定する(同一リージョン内での遅延及び別リージョン間での遅延の2パターン。構成図は以下の通り)。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/189302/73c0582a-4702-4b53-a8b5-8853a8795ff5.png)

### 実施方法
- 事前に遅延測定用のテーブルを作成する(列はid, EC2インスタンス時刻, DSQL時刻の3つ)。

```
postgres=> CREATE TABLE time_list (id INTEGER not null, instance_time TIMESTAMP not null, dsql_time TIMESTAMP not null, PRIMARY KEY(id));
```

- 以下のPythonスクリプトで遅延を測定する。
    - まずEC2インスタンスで自分のOSの時刻を取得し、次にDSQLの時刻をSQLコマンドで取得し、それらを同じ行にINSERTする。AUTO COMMITなので1行INSERT毎にCOMMITも行われ、DSQL内でLinked Regionへの同期も行われる。
    - 10ms毎に繰り返し処理を行う。
    - us-east-1のEC2インスタンスで実行する。スクリプト内のregion設定およびcluster_endpointを切り替えることで、DSQLのus-east-1及びus-east-2それぞれへのINSERTを実行する。

``` dsql-insert.py
import psycopg
import boto3
import os, sys
import time
from datetime import datetime

def main(cluster_endpoint):
    region = 'us-east-1'
    #region = 'us-east-2'

    # Generate a password token
    client = boto3.client("dsql", region_name=region)
    password_token = client.generate_db_connect_admin_auth_token(cluster_endpoint, region)

    # connection parameters
    dbname = "dbname=postgres"
    user = "user=admin"
    host = f'host={cluster_endpoint}'
    sslmode = "sslmode=require"
    #sslrootcert = "sslrootcert=system"
    password = f'password={password_token}'

    # Make a connection to the cluster
    conn = psycopg.connect('%s %s %s %s %s' % (dbname, user, host, sslmode, password))
    conn.set_autocommit(True)
    cur = conn.cursor()

    num = 1
    while num < 401:
        itime = datetime.now()
        # Insert some rows
        cur.execute("INSERT INTO time_list(id, instance_time, dsql_time) VALUES(%s, %s, CURRENT_TIMESTAMP)",(num, itime))
        num += 1
        time.sleep(0.01)

    cur.execute("SELECT * FROM time_list")
    results = cur.fetchall()  # すべての行を取得
    print(f"{'ID':<5} {'Instance Time':<30} {'DSQL Time':<30}")

    for row in results:
        print(f"{row[0]:<5} {row[1]} {row[2]}")

if __name__ == "__main__":
    # Replace with your own cluster's endpoint
    cluster_endpoint = "xxxxxxxxxxxxxxxxxxxxxxlt4.dsql.us-east-1.on.aws"
    #cluster_endpoint = "xxxxxxxxxxxxxxxxxxxxxxycq.dsql.us-east-2.on.aws"
    main(cluster_endpoint)
```

### 結果

- 同一リージョン内での遅延

```
[ec2-user@ip-10-0-0-110 ~]$ python3 insert-us-east-1.py
ID    Instance Time                  DSQL Time
1     2025-02-10 01:53:02.047760 2025-02-10 01:53:02.046821
2     2025-02-10 01:53:02.270401 2025-02-10 01:53:02.269634
3     2025-02-10 01:53:02.304991 2025-02-10 01:53:02.304108
4     2025-02-10 01:53:02.338620 2025-02-10 01:53:02.337666
5     2025-02-10 01:53:02.371345 2025-02-10 01:53:02.370401
6     2025-02-10 01:53:02.403060 2025-02-10 01:53:02.402765
7     2025-02-10 01:53:02.435984 2025-02-10 01:53:02.434993
8     2025-02-10 01:53:02.468278 2025-02-10 01:53:02.467330
9     2025-02-10 01:53:02.500519 2025-02-10 01:53:02.499536
10    2025-02-10 01:53:02.531806 2025-02-10 01:53:02.530837
```

- 別リージョン間での遅延

```
[ec2-user@ip-10-0-0-110 ~]$ python3 insert-us-east-2.py
ID    Instance Time                  DSQL Time
1     2025-02-10 02:10:20.543390 2025-02-10 02:10:20.548469
2     2025-02-10 02:10:20.813969 2025-02-10 02:10:20.819066
3     2025-02-10 02:10:20.883989 2025-02-10 02:10:20.889074
4     2025-02-10 02:10:20.953425 2025-02-10 02:10:20.958444
5     2025-02-10 02:10:21.002607 2025-02-10 02:10:21.007618
6     2025-02-10 02:10:21.050416 2025-02-10 02:10:21.068457
7     2025-02-10 02:10:21.113137 2025-02-10 02:10:21.118171
8     2025-02-10 02:10:21.161655 2025-02-10 02:10:21.166648
9     2025-02-10 02:10:21.209806 2025-02-10 02:10:21.214818
10    2025-02-10 02:10:21.258429 2025-02-10 02:10:21.263434
```

- Instance Timeの取得を先に行うため、理論上はInstance Time の時刻が DSQL Time より前になっていないといけないが、同一リージョン内での検証結果では、DSQL Timeのほうが早くなってしまっている。これはEC2インスタンスやDSQLにおけるTimeSync Serviceとの時刻同期の精度や、PythonやSQLで時刻を取得する際の精度にも限界があり、ミリ/マイクロ秒レベルでの正確なタイムスタンプの取得は難しいのかなという想定。同一リージョン内であればほぼ遅延0で書き込み可能な様子、といったん認識することとした。
- 別リージョン間の場合、概ね5ms程度の遅延があり、事前にリージョン間ping遅延を測定した際の値(RTT 13ms)を考慮して、そんなものかなという感じ。

## 4.3 Aurora DSQL クラスター間のデータ同期遅延

- 今回の検証の本題として、us-east-1 のEC2インスタンスからus-east-1のDQSLクラスターに連続的にINSERTし、us-east-2のEC2インスタンスからus-east-2のDSQLクラスターにSELECTして、SELECTしたタイミングでどこまでのレコードが同期されてきているのかを確認する。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/189302/78b28aa6-71a1-4179-8430-30d0421d914d.png)

### 実施方法

- us-east-1 のEC2インスタンスから、10ms間隔でインスタンスの時刻、DSQLの時刻を含むレコードを1行ずつINSERT(AUTO COMMIT)する。スクリプトは「4.2 EC2インスタンス - Aurora DSQL間の遅延」で使用しているものと同じ。
- us-east-2側のスクリプトは以下。上記のus-east-1側でのINSERTが継続している状態で、us-east-2 のEC2インスタンスから、us-east-2のDSQLクラスターに対してSELECTを行い、SELECTを実行したタイミングで取得できた最新のレコードを確認して遅延を判断する。

``` dsql-select.py
import psycopg
import boto3
import os, sys
import time
from datetime import datetime

def main(cluster_endpoint):
    region = 'us-east-2'

    # Generate a password token
    client = boto3.client("dsql", region_name=region)
    password_token = client.generate_db_connect_admin_auth_token(cluster_endpoint, region)

    # connection parameters
    dbname = "dbname=postgres"
    user = "user=admin"
    host = f'host={cluster_endpoint}'
    sslmode = "sslmode=require"
    #sslrootcert = "sslrootcert=system"
    password = f'password={password_token}'

    # Make a connection to the cluster
    conn = psycopg.connect('%s %s %s %s %s' % (dbname, user, host, sslmode, password))
    conn.set_autocommit(True)
    cur = conn.cursor()

    itime = datetime.now()
    print(f"Instance Time(start) : {itime}")

    cur.execute("select NOW()")
    results = cur.fetchone()[0]
    formatted_time = results.strftime("%Y-%m-%d %H:%M:%S.%f")  # %fはマイクロ秒を表示
    print(f"Database Time(finish): {formatted_time}")

    cur.execute("SELECT * FROM time_list")
    
    itime = datetime.now()
    print(f"Instance Time(finish): {itime}")

    results = cur.fetchall()  
    print(f"{'ID':<5} {'Instance Time':<30} {'DSQL Time':<30}")

    for row in results:
        print(f"{row[0]:<5} {row[1]} {row[2]}")

if __name__ == "__main__":
    # Replace with your own cluster's endpoint
    cluster_endpoint = "xxxxxxxxxxxxxxxxxxxx.ycq.dsql.us-east-2.on.aws"
    main(cluster_endpoint)
```

### 結果

#### 10ms間隔での連続INSERTの場合
- 上記のスクリプトの実行結果は以下の通り(10ms間隔で400回INSERTしている間に1回SELECT実施)。

```
[ec2-user@ip-10-0-15-4 ~]$ python3 dsql-select.py　※10ms間隔
Instance Time(start) : 2025-02-10 02:03:14.326589
Database Time(finish): 2025-02-10 02:03:14.325649
Instance Time(finish): 2025-02-10 02:03:14.458220
ID    Instance Time                  DSQL Time

1     2025-02-10 02:03:11.161942 2025-02-10 02:03:11.160994
2     2025-02-10 02:03:11.343008 2025-02-10 02:03:11.342087
3     2025-02-10 02:03:11.377308 2025-02-10 02:03:11.376310
～中略～
94    2025-02-10 02:03:14.303955 2025-02-10 02:03:14.303032
95    2025-02-10 02:03:14.335718 2025-02-10 02:03:14.334773
96    2025-02-10 02:03:14.368579 2025-02-10 02:03:14.367585
```

|| 処理内容 | timestamp |
| --- | --- | --- |
|a| us-east-2 EC2インスタンスで処理を開始(SELECT開始前) | 02:03:14.326589 |
|b| us-east-2 DSQLでSELECTが完了　| 02:03:14.325649|
|c| us-east-2 EC2インスタンスで処理が完了(SELECT結果受領) | 02:03:14.458220 |
|d| 取得できた最後のレコード内のus-east-1のEC2インスタンス時刻 | 02:03:14.368579 |
|e| 取得できた最後のレコード内のus-east-1のDSQL時刻 | 02:03:14.367585 |

- 取得できたtimestamp値について確認する。us-east-2のEC2インスタンスにおけるタイミングa (02:03:14.326589) で実行したSELECTで取得した最新のレコードに含まれる、us-east-1側で取得した時刻(タイミングd/e)の値が、タイミングaよりも未来の値になり、論理的におかしい結果となってしまった。ミリ秒/マイクロ秒レベルの測定をこの方法で行うのはやはり厳しいのかもしれない。

#### 500ms間隔での連続INSERT
- もう少し分かりやすい結果とするため、us-east-1側EC2インスタンスでのINSERT実行間隔を500msに変更し、同じ検証を実施する。

```
[ec2-user@ip-10-0-15-4 ~]$ python3 dsql-select.py 　※500ms間隔
Instance Time(start) : 2025-02-12 07:57:16.100630
Database Time(finish): 2025-02-12 07:57:16.099583
Instance Time(finish): 2025-02-12 07:57:16.246476
ID    Instance Time                  DSQL Time                     
1     2025-02-12 07:57:07.569067 2025-02-12 07:57:07.568210
2     2025-02-12 07:57:08.249154 2025-02-12 07:57:08.248505
3     2025-02-12 07:57:08.772726 2025-02-12 07:57:08.772051
～中略～
15    2025-02-12 07:57:15.046144 2025-02-12 07:57:15.045614
16    2025-02-12 07:57:15.567858 2025-02-12 07:57:15.567327
17    2025-02-12 07:57:16.089342 2025-02-12 07:57:16.088828
```

|| 処理内容 | timestamp |
| --- | --- | --- |
|a| us-east-2 EC2インスタンスで処理を開始(SELECT開始前) | 07:57:16.100630 |
|b| us-east-2 DSQLでSELECTが完了　| 07:57:16.099583|
|c| us-east-2 EC2インスタンスで処理が完了(SELECT結果受領) | 07:57:16.246476 |
|d| 取得できた最後のレコード内のus-east-1のEC2インスタンス時刻 | 07:57:16.089342 |
|e| 取得できた最後のレコード内のus-east-1のDSQL時刻 | 07:57:16.088828 |

- 取得できたtimestamp値について再確認する。us-east-2のEC2インスタンスにおけるタイミングa (07:57:16.100630) で実行したSELECTで取得した最新のレコードに含まれる、us-east-1側で取得した時刻(タイミングd/e)の値が、約120ms前の値となっている。この結果から、500ms間隔で実施しているINSERTに関して、DSQLクラスター間では少なくとも500ms以内には同期が完了し、us-east-2側で最新のINSERT結果を取得できていると考えられる。

## 4.4 Aurora Global Database の非同期レプリケーション遅延

- 参考として、Aurora Global Databaseで同様の検証を実施する。Aurora Global Databaseの設定内容は以下。
    - エンジン: PostgreSQL 16.4 (Aurora DSQL(Preview)に近いもの)
    - インスタンスタイプ: db.r5.large (Global Databaseにするための最小構成)
    - 冗長構成: us-east-1がPrimary, us-east-2がSecondary(通常時は読み取りのみ可) 

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/189302/bd3b5423-ef2f-4c32-983e-7f8c9e2636eb.png)

### 実施方法
- 「4.2 EC2インスタンス - Aurora DSQL間の遅延」及び「4.3 Aurora DSQL クラスター間のデータ同期遅延」と同じ内容を実施する。(接続するエンドポイントをAurora DSQLのものからAurora Global Databaseのものに変更)
- us-east-1のEC2インスタンスからus-east-2のAurora Global Database(Secondary)へのアクセスは書き込みができないため省略する。

### 結果
#### EC2インスタンス - Aurora Global Database(Primary)間の遅延(同一リージョン内)

```
[ec2-user@ip-10-0-0-110 ~]$ python3 aurora-insert.py
ID    Instance Time                  Aurora Time
1     2025-02-10 00:35:22.082938 2025-02-10 00:35:22.083393
2     2025-02-10 00:35:22.096639 2025-02-10 00:35:22.097042
3     2025-02-10 00:35:22.109740 2025-02-10 00:35:22.110143
4     2025-02-10 00:35:22.123324 2025-02-10 00:35:22.123743
5     2025-02-10 00:35:22.136899 2025-02-10 00:35:22.137323
6     2025-02-10 00:35:22.149976 2025-02-10 00:35:22.151701
7     2025-02-10 00:35:22.164577 2025-02-10 00:35:22.165006
8     2025-02-10 00:35:22.178305 2025-02-10 00:35:22.178722
9     2025-02-10 00:35:22.191660 2025-02-10 00:35:22.192067
10    2025-02-10 00:35:22.204872 2025-02-10 00:35:22.205272
```
- Aurora DSQLの結果と大差はないが、常にインスタンス時刻 < Aurora時刻 となっており、同一リージョン内では数msレベルでのINSERTが可能。

####  Aurora Global Database 間の非同期レプリケーション遅延
- Aurora DSQLの検証と同じく、us-east-1側で10ms/500ms間隔で連続INSERTしている間に、us-east-2側でSELECTを実施する。
- なおAurora Global Databaseは非同期レプリケーションであり、同期処理しているAurora DSQLとは仕様が異なる。

``` 
[ec2-user@ip-10-0-15-4 ~]$ python3 aurora-select.py　※10ms間隔
Instance Time(start) : 2025-02-10 00:47:57.652112
Database Time(finish): 2025-02-10 00:47:57.652392
Instance Time(finish): 2025-02-10 00:47:57.655129
ID    Instance Time                  Aurora Time
1     2025-02-10 00:47:54.465371 2025-02-10 00:47:54.465772
2     2025-02-10 00:47:54.479214 2025-02-10 00:47:54.479581
3     2025-02-10 00:47:54.492804 2025-02-10 00:47:54.493156
～中略～
238   2025-02-10 00:47:57.599869 2025-02-10 00:47:57.600225
239   2025-02-10 00:47:57.612945 2025-02-10 00:47:57.613302
240   2025-02-10 00:47:57.625803 2025-02-10 00:47:57.626162
[ec2-user@ip-10-0-15-4 ~]$
```

|| 処理内容 | timestamp |
| --- | --- | --- |
|a| us-east-2 EC2インスタンスで処理を開始(SELECT開始前) | 00:47:57.652112 |
|b| us-east-2 Aurora(Secondary)でSELECTが完了　| 00:47:57.652392|
|c| us-east-2 EC2インスタンスで処理が完了(SELECT結果受領) | 00:47:57.655129 |
|d| 取得できた最後のレコード内のus-east-1のEC2インスタンス時刻 | 00:47:57.625803 |
|e| 取得できた最後のレコード内のus-east-1のAurora(Primary)時刻 | 00:47:57.626162 |


```
[ec2-user@ip-10-0-15-4 ~]$ python3 aurora-select.py  ※500ms間隔 
Instance Time(start) : 2025-02-12 13:34:12.982551
Database Time(finish): 2025-02-12 13:34:12.982938
Instance Time(finish): 2025-02-12 13:34:12.986455
ID    Instance Time                  Aurora Time                     
1     2025-02-12 13:34:08.100501 2025-02-12 13:34:08.101002
2     2025-02-12 13:34:08.604676 2025-02-12 13:34:08.605241
3     2025-02-12 13:34:09.109176 2025-02-12 13:34:09.109742
4     2025-02-12 13:34:09.613155 2025-02-12 13:34:09.613710
5     2025-02-12 13:34:10.117047 2025-02-12 13:34:10.117648
6     2025-02-12 13:34:10.621244 2025-02-12 13:34:10.623301
7     2025-02-12 13:34:11.128629 2025-02-12 13:34:11.129190
8     2025-02-12 13:34:11.637546 2025-02-12 13:34:11.638114
9     2025-02-12 13:34:12.141808 2025-02-12 13:34:12.142391
10    2025-02-12 13:34:12.646207 2025-02-12 13:34:12.646776
```

|| 処理内容 | timestamp |
| --- | --- | --- |
|a| us-east-2 EC2インスタンスで処理を開始(SELECT開始前) | 13:34:12.982551 |
|b| us-east-2 Aurora(Secondary)でSELECTが完了　| 13:34:12.982938|
|c| us-east-2 EC2インスタンスで処理が完了(SELECT結果受領) | 13:34:12.986455 |
|d| 取得できた最後のレコード内のus-east-1のEC2インスタンス時刻 | 13:34:12.646207 |
|e| 取得できた最後のレコード内のus-east-1のAurora(Primary)時刻 | 13:34:12.646776 |

- 10ms間隔でINSERT実施時、us-east-2側でSELECTした時刻(a)と最後のレコード内の時刻(d/e)の差が約26msであり、500ms間隔でのINSERT実施時は時刻aとd/eの差が約335ms。数十msレベルの遅延でレプリケーションできてはいそうだが、小さい値の厳密な測定は難しいため、今回はざっくり500ms以内には収まっていると考えることにする。

## 4.5 全体のまとめ
- ここまでに測定した値を図に記入した(あくまでも今回やった測定方法、タイミングで取得した値であり、厳密なものではない)。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/189302/1b0edf2d-50b1-4daa-bf28-c873a3fea0ed.png)


- そもそもus-east-1/us-east-2のリージョン間でRTT 13ms程度のネットワーク遅延がある。
- Aurora DSQL/Aurora Global Database とも、リージョン内でのINSERT/SELECTに要する時間は無視できるくらい小さい。
- Aurora DSQL/Aurora Global Database とも、リージョン間の同期/非同期レプリケーションは数十msレベルでは行われているようだが、今回の方法では厳密な測定はできなかった。ただ今回の条件(レコード1行だけのINSERT)であれば、500ms以内で同期/非同期レプリケーションは完了した。

# 5. 所感
- 初心者のため、測定方法が適切なのかなどの課題はあるが、かなり低遅延での同期が行われていそうということは分かった。re:Invent発表資料などを確認して、なぜ低遅延が実現可能なのかなど仕組みを理解できるようにしたい。