Amazon-Aurora

Amazon AuroraのZDPがどれだけ”ゼロダウンタイム”なのか試してみた

ご挨拶

お疲れさまです、寺井です。

最近コミュニティの参加レポートばかり上げてたので、そろそろ技術要素のあるブログを書きたいと思っていたところ、挙動が気になる機能があったので、今回はガッツリ検証してみることにしました。💪

経緯と目的

Auroraの機能の1つとして、『ダウンタイムのないパッチ適用 (Zero-downtime patching[ZDP])』なるものがあることを先日知りました。

この機能がどこまで"ゼロダウンタイム"なのかを確認してみたいと思います。

ZDPについて

引用:ダウンタイムのないパッチ適用 (ZDP) - Amazon Aurora
ダウンタイムのないパッチ適用 (ZDP) 機能では、ベストエフォートに基づいて、Aurora アップグレード中のクライアント接続を維持するよう試みます。ZDP が正常に完了されると、アプリケーションのセッションが保持され、アップグレードの進行中にデータベースエンジンが再起動します。データベースエンジンの再起動により、数秒から約 1 分間スループットが低下する可能性があります。

関西弁で要約すると、
「ちょっとだけ処理が遅くなってまうことはあるかもしれへんけど、接続が切れへんように"できるだけ"頑張るでぇ」
ってことのようですね。

あと「ZDP」って必殺技みたいでカッコいいですけど、読み方によってカッコよさ変わってきますね。
ズィーディーピー?ゼットディーピー?

私は「ゼットデーピー」派です。響き的に。

検証内容

ZDPが有効なエンジンバージョンを使用したAurora(PostgreSQL)で、ダウンタイムが発生する作業を行い、作業期間中にAuroraの各エンドポイントへの[接続/読み/書き]において、どれだけダウンタイムが発生するか試します。

参考リンク:Aurora PostgreSQL のアップグレードで ZDP を利用できる条件とエンジンバージョンの詳細

先に今回の検証のまとめ

マイナー/メジャーアップグレードにおいて、リードレプリカの有り無しで挙動が変わるか試してみたかったので、以下の4通りを表にまとめました。

リードレプリカ アップグレード種類 ZDP ダウンタイム 所要時間
無し マイナーバージョン 読み取りにおいて 5秒間 のスループット低下が発生 5分
無し メジャーバージョン 読み書き共に 10分間 のダウンタイム 12分
有り マイナーバージョン 書き込み5秒、読み取り1分のダウンタイム
読み取り機能は実質ダウンタイム無し(*)
5分
有り メジャーバージョン 読み書き共に 10分間 のダウンタイム 12分

※先に書き込みエンドポイントが数秒ダウン、その後に読み取りエンドポイントがダウンして、うまいこと被らないようにやってくれてるみたいでした。

ZDPが発動した場合でも、数秒程度のダウンタイムは発生するようですね。
ただ何回か試行したところ、挙動にバラつきがあったので、あくまでもベストエフォートであるという感じでした。
(メンテナンスウインドウは任意に発生させることができないため、検証しきれていないです)

よって、「ZDPが有効だからダウンタイムはゼロです!!」と言い切るのは難しいですね。

とはいえ、コネクションを切らなかったり、若干のスループット低下に抑えたりと、ZDPの有効性はしっかりと確認できたので、Auroraを推奨する1つの理由になるなと感じました。

ちなみに、以下の変更だとどうなるのかなと思って一緒に試してみましたけど、どれもインスタンス再起動を伴うものばかりだったので、ZDPは発動せず当然のようにダウンタイムが発生しました。

  1. インスタンスタイプの変更
  2. Aurora タイプ変更[Provisioned → Serverless v2]
  3. Aurora タイプ変更[Serverless v2 → Provisioned]
  4. パラメータグループの変更

検証内容

以降は具体的にどのような検証を行ったかを記載していきます。

少し長く、絵的にもしんどくなりますので、興味のある方だけ見てってください!!🙏

環境

  • 検査対象: Amazon Aurora(PostgreSQL)
  • スクリプト実行用: EC2(Alma LinuxOS 8.9[Python 3.11])

検証用のPythonスクリプト

Auroraは複数のエンドポイントを持ってるので、全てのエンドポイントに対して高速にアクセスして、どれだけエラーが発生するかログに記録するようなスクリプトを考えました。

ザックリとした流れは以下の通りです。

  1. 接続試行: 各エンドポイントへの接続を試み、結果をログに記録。
  2. 書き込み判断: 接続成功時、エンドポイントが書き込み可能か判断。
  3. 読み書き処理: 書き込み可能ならデータ挿入、全エンドポイントでデータ読み取り。
  4. ログ記録: 操作の成否と所要時間をログに記録。
  5. 繰り返し処理: 全エンドポイントに対して上記を一定間隔で実行。
import psycopg2
import time
from datetime import datetime

# ログファイルの設定。スクリプトが起動した日時でファイル名を生成し、ログの初期設定を行います。
start_time = datetime.now().strftime("%Y%m%d_%H%M%S")
log_filename = f"major-up_replica-2_log_{start_time}.csv"
with open(log_filename, "w") as log_file:
    log_file.write("timestamp,event_type,endpoint,result,error_message,elapsed_time\n")

# ログをCSVフォーマットで記録する関数。各イベントの詳細をファイルに書き出します。
def log_message(event_type, endpoint, result, error_message="", elapsed_time=None):
    error_message = error_message.replace("\n", " ").replace('"', '""')
    timestamp = datetime.now()
    elapsed_time_str = f"{elapsed_time}" if elapsed_time else ""
    log_entry = f'"{timestamp}","{event_type}","{endpoint}","{result}","{error_message}",{elapsed_time_str}\n'
    with open(log_filename, "a") as log_file:
        log_file.write(log_entry)

# 接続情報と認証情報の設定。ここにAuroraの接続情報を設定してください。
endpoints = {
    "cluster_writer": {"endpoint": "your-cluster-writer-endpoint.amazonaws.com", "write": True},
    "cluster_reader": {"endpoint": "your-cluster-reader-endpoint.amazonaws.com", "write": False},
    "writer_instance": {"endpoint": "your-writer-instance-endpoint.amazonaws.com", "write": True},
    "reader_instance": {"endpoint": "your-reader-instance-endpoint.amazonaws.com", "write": False}
}
username, password, database = "your_username", "your_password", "your_database"

# データベースへの接続を試みる関数。
def try_connect(endpoint):
    start = time.time()
    try:
        conn = psycopg2.connect(host=endpoint, user=username, password=password, dbname=database, connect_timeout=5)
        log_message("connect", endpoint, "success", "", time.time() - start)
        return conn
    except Exception as e:
        log_message("connect", endpoint, "fail", str(e), time.time() - start)

# データベースへの書き込みを試みる関数。書き込み可能なエンドポイントで使用します。
def try_write(conn, endpoint):
    start = time.time()
    try:
        with conn.cursor() as cursor:
            cursor.execute('INSERT INTO test_table (value) VALUES (%s)', (1,))
            conn.commit()
            log_message("write", endpoint, "success", "", time.time() - start)
    except Exception as e:
        log_message("write", endpoint, "fail", str(e), time.time() - start)

# データベースからの読み取りを試みる関数。すべてのエンドポイントで使用します。
def try_read(conn, endpoint):
    start = time.time()
    try:
        with conn.cursor() as cursor:
            cursor.execute('SELECT 1 FROM test_table')
            cursor.fetchall()
            log_message("read", endpoint, "success", "", time.time() - start)
    except Exception as e:
        log_message("read", endpoint, "fail", str(e), time.time() - start)

# メイン実行ループ。各エンドポイントに対して接続、書き込み、読み取りを繰り返し実行します。
while True:
    for role, details in endpoints.items():
        conn = try_connect(details["endpoint"])
        if conn:
            if details["write"]: try_write(conn, details["endpoint"])
            try_read(conn, details["endpoint"])
            conn.close()
    time.sleep(0.1)  # 待機時間の設定。サーバーへの負荷を考慮して調整してください。

生成されるログ形式例

"2024-04-09 19:14:38.419510","connect","auroraname.cluster-xxxx.region.rds.amazonaws.com","success","",0.059619903564453125
"2024-04-09 19:14:38.428108","write","auroraname.cluster-xxxx.region.rds.amazonaws.com","success","",0.008398771286010742
"2024-04-09 19:14:38.432143","read","auroraname.cluster-xxxx.region.rds.amazonaws.com","success","",0.00382232666015625
"2024-04-09 19:14:38.467704","connect","auroraname.cluster-ro-xxxx.region.rds.amazonaws.com","success","",0.03521466255187988
"2024-04-09 19:14:38.469122","read","auroraname.cluster-ro-xxxx.region.rds.amazonaws.com","success","",0.0012824535369873047

全ての検証が終わって集計してるときに気づいたんですけど、1つのリクエストで待ちが発生している間、後続の処理も止まっちゃうので、ガチで検証するならリクエストを非同期にしないといけないなと思いました。(諦め

結果

スクリプトログからの結果一覧表

ログをCSVで出力しているので、Excelに喰わせてまとめて計算しました。
表は、特定のAuroraインスタンス群に対して定期的に実施された[接続・読み取り・書き込み]のテストの結果をまとめたものです。

CSVをS3に保存するようにしてAthena+QuickSightで可視化まで組めたら楽しそう🤤

※以下表は数値を見るのが気持ちいい人向け

[レプリカ無し] マイナーバージョンアップグレード

エンドポイント種類 最小応答時間 最大応答時間 平均応答時間 最大と平均の差分 ダウンタイム
cluster_writer
接続 0.02804 0.16758 0.04045 0.13 0:00:00
書き込み 0.00414 0.05995 0.00541 0.05 0:00:00
読み取り 0.00090 0.01098 0.00173 0.01 0:00:00
cluster_reader
接続 0.02805 0.17719 0.04011 0.14 0:00:00
書き込み 0:00:00
読み取り 0.00121 6.44962 0.00211 6.45 0:00:00
writer_instance
接続 0.02702 0.09002 0.03874 0.05 0:00:00
書き込み 0.00419 0.03761 0.00543 0.03 0:00:00
読み取り 0.00093 0.01870 0.00181 0.02 0:00:00

[レプリカ無し] メジャーバージョンアップグレード

エンドポイント種類 最小応答時間 最大応答時間 平均応答時間 最大と平均の差分 ダウンタイム
cluster_writer
接続 0.00120 1.02714 0.00896 1.02 0:09:46
書き込み 0.00406 0.08789 0.00546 0.08 0:00:00
読み取り 0.00348 0.04781 0.00461 0.04 0:00:00
cluster_reader
接続 0.00105 0.20750 0.00869 0.20 0:09:46
書き込み 0:00:00
読み取り 0.00390 0.04367 0.00510 0.04 0:00:00
writer_instance
接続 0.00047 0.23578 0.00783 0.23 0:09:46
書き込み 0.00428 0.06127 0.00545 0.06 0:00:00
読み取り 0.00346 0.06339 0.00478 0.06 0:00:00

[レプリカ有り]マイナーバージョンアップグレード

エンドポイント種類 最小応答時間 最大応答時間 平均応答時間 最大と平均の差分 ダウンタイム
cluster_writer
接続 0.00221 1.05160 0.04788 1.00 0:00:05.465
書き込み 0.00779 0.09244 0.00930 0.08
読み取り 0.00385 0.04193 0.00488 0.04
cluster_reader
接続 0.00063 0.98435 0.03600 0.95 0:00:34.715
書き込み
読み取り 0.00119 0.03623 0.00286 0.03
writer_instance
接続 0.00206 0.11068 0.04751 0.06 0:00:05.518
書き込み 0.00776 0.06059 0.00925 0.05
読み取り 0.00378 0.01755 0.00484 0.01
reader_instance
接続 0.00063 1.08770 0.02973 1.06 0:01:08.306
書き込み
読み取り 0.00120 0.09974 0.00224 0.10
エンドポイント種類 最小応答時間 最大応答時間 平均応答時間 最大と平均の差分 ダウンタイム
cluster_writer
接続 0.00266 1.04552 0.00825 1.04 0:10:10.714
書き込み 0.00589 0.08961 0.00929 0.08
読み取り 0.00000 0.04294 0.00551 0.04
cluster_reader
接続 0.00256 0.14971 0.00814 0.14 0:10:10.861
書き込み
読み取り 0.00274 0.02829 0.00600 0.02
writer_instance
接続 0.00193 0.12869 0.00734 0.12 0:10:10.888
書き込み 0.00762 0.10820 0.00920 0.10
読み取り 0.00451 0.04275 0.00555 0.04
reader_instance
接続 0.00058 0.85836 0.00369 0.85 0:11:14.157
書き込み
読み取り 0.00210 0.08907 0.00332 0.09
  • 極端に遅いクエリは除いて計算
  • 小数点は適度に丸めてます

RDSイベントから見る

マイナーバージョンアップグレード(14.x -> 14.x)

11:42   The parameter max_wal_senders was set to a value incompatible with replication. It has been adjusted from 10 to 20.
11:42   Aurora fast startup isn't supported for this upgrade path.
11:42   Attempting to upgrade the database with zero downtime.
11:43   Attempt to upgrade the database with zero downtime finished. The process took 6134929 ms, 4 connections preserved, 0 connections dropped. See the database error log for details.

イベントログからもZDPが適用されたことが確認できますね!

最後のイベントを見ると、
「プロセスには 6134929 ms かかり、4 つの接続が維持され、0 つの接続がドロップされました」
とありますが、秒単位をms(ミリ秒)じゃなくてμs(マイクロ秒)として解釈してます。誤記?🤔
(本当に「6134929 ms(ミリ秒)」だとすると、1時間半ぐらいかかった計算になる…。)

ややこしい話は置いといて分かりやすくいうと、
「6秒ぐらいかかったけど接続は維持したよ💪」ということかと思います。

スクリプトで生成したログと照らし合わせたところ、一回だけ読み取り処理に6秒ぐらいかかってるものがあったので、イベントの内容と相違は無さそうです。

平均値と比べると大幅なスループットの低下が発生したと言えるかもしれませんが、接続を維持してくれたうえでエラー吐かないってのは、アプリ側としては助かりそうな気がします。

メジャーバージョンアップグレード(14.x -> 15.x)

12:31   DB instance shutdown
12:31   The parameter max_wal_senders was set to a value incompatible with replication. It has been adjusted from 10 to 20.
12:40   The parameter max_wal_senders was set to a value incompatible with replication. It has been adjusted from 10 to 20.
12:40   DB instance shutdown
12:40   The parameter max_wal_senders was set to a value incompatible with replication. It has been adjusted from 10 to 20.
12:41   DB instance shutdown
12:41   The parameter max_wal_senders was set to a value incompatible with replication. It has been adjusted from 10 to 20.
12:41   DB instance restarted

やはりメジャーバージョンのアップグレードではZDPはしなかったようですね。
ダウンタイムを抑えたいなら、大人しくブルー/グリーンデプロイを検討したほうが良さそうです。

感想

初めにZDPを目にしたときは
「ダウンタイム無しでやってくれるんや、へぇ~すご😯」
ぐらいにしか捉えてなかったので、検証したことによって認識が異なっていることに気づけましたし、説明するときにも自信をもって話すことができそうです。

やっぱり何事も検証してみるというのは大事だと感じました。

Auroraのさらなる強みを知れた良い機会でした。
ありがとうございました~!👏

返信を残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

CAPTCHA