Amazon MemoryDB のシャードを書き込み中にフェイルオーバーさせ、止まった時間と再試行の結果を確認してみた

Amazon MemoryDB のシャードを書き込み中にフェイルオーバーさせ、止まった時間と再試行の結果を確認してみた

AWS MemoryDB でプライマリノードのフェイルオーバーが発生したとき、アプリケーションの書き込みが実際に何秒止まるのか、どんなエラーが返されるのか、再試行でデータを取りこぼさないようにするにはどうすればいいのかを、実際に検証してみました。
2026.11.01

はじめに

テクニカルサポートの 片方 です。
MemoryDB は、プライマリノードに障害が起きるとレプリカにフェイルオーバーし、通常は数秒で書き込みを再開できます。では、書き込みを続けているアプリケーションから見ると、何秒止まり、どんなエラーが返り、再試行すれば取りこぼさずに済むのでしょうか。
Valkey のクラスターに、Lambda 関数から平均で毎秒 30 件以上の SET を送り続けながら、failover-shard でフェイルオーバーを起こしました。止まった時間、エラーの種類、再試行の結果を確かめています。

構成

東京リージョンのデフォルト VPC に、次のリソースを AWS CLI で作りました。

リソース 内容
MemoryDB クラスター order-counter-cache Valkey 7.3、db.t4g.small、シャード 1、レプリカ 1、TLS あり、ACL は open-access(検証のための設定)
サブネットグループ order-counter-cache-subnets デフォルト VPC のサブネット
セキュリティグループ order-counter-cache-sg クラスターと Lambda 関数の両方に付ける
Lambda 関数 order-counter-writer Python 3.13、VPC 内、valkey-py 6.2.0 を同梱。タイムアウトは実行時間(約 7 分半)より長くする

プライマリの 0001-001 が ap-northeast-1a、レプリカの 0001-002 が ap-northeast-1c です。

先に結論から

  • 書き込みが止まったのは 17.9 秒でした。返ったエラーは、TimeoutError が 1 回、ClusterDownError が 8 回です。途中の接続し直しは、タイムアウト、Error 104(Connection reset by peer)、Connection refused の順に失敗し、5 回目で接続できました。
  • 再試行で書き込み直し、15,822 件すべてを欠落なく書き込めました。ただし、止まった分が通ったのは、試行の上限 10 回のうち 10 回目で、余裕はありませんでした。
  • イベントでは、フェイルオーバーの開始から完了まで 48 秒かかりました。一方、書き込みは開始から約 20 秒で再開しました。
  • 接続し直しに再試行を入れていなかった最初の Lambda 関数は、フェイルオーバーの後、接続し直しの 1 回目の失敗でエラーになって終わりました。書き込みだけでなく、接続し直しにも再試行が必要です。

やってみた

次の順に進めました。

  1. クラスターと Lambda 関数を作る
  2. フェイルオーバーを起こす(最初の関数はエラーで終わった)
  3. 接続し直しにも再試行を入れて、もう一度試す
  4. イベントで進み方を見る
  5. 書き込み側の結果を見る

1. クラスターと Lambda 関数を作る

クラスターは create-cluster で作り、available になるまで 14 分かかりました。Lambda 関数の接続先は、クラスターエンドポイント(clustercfg. で始まるアドレス)です。

Lambda 関数は、キー order:{n} に値 n を順番に SET し、OK が返った n を記録します。エラーが出たら、同じ n を、フルジッター付きの指数バックオフ(100 ms から 2 倍、上限 2 秒)で書き込み直します。試行は初回を含めて最大 10 回で、それでも書き込めなかった n は「未確定」として別に記録します。同じキーに同じ値を書き込むだけなので、何度書き込み直しても結果は変わりません(カウンターの INCR は使いません)。

エラーが出るたびにクライアントを作り直します。その接続にも、フルジッター付きの指数バックオフ(0.5 秒から 2 倍、上限 8 秒)で、初回を含めて最大 12 回の再試行を入れています。この接続の再試行は、手順 2 の結果を受けて後から入れたものです。

"""MemoryDB(Valkey)へ order:{n} を順に SET し続け、OK が返った n を記録する。

- 接続:クラスターエンドポイント(TLS)。valkey-py の ValkeyCluster を使う。
- 再試行:接続エラー・タイムアウト・MOVED/CLUSTERDOWN・READONLY などのエラーは、フルジッター付きの指数バックオフ
  (100 ms から 2 倍、上限 2 秒)で最大 10 回まで同じ n を書き直す。上限を超えた n は「未確定」として別に記録する。
- 冪等性:キー order:{n} に値 n を書くだけなので、同じ n を何度書いても結果は変わらない(カウンターの INCR は使わない)。
- 終了:DURATION 秒(環境変数、既定 420 秒)経ったら止まり、書けた n の一覧・エラーの種類と回数・書けなかった時間帯を返す。
"""
import json
import os
import random
import time

from valkey.cluster import ValkeyCluster
from valkey.exceptions import ConnectionError, TimeoutError, ClusterDownError, ValkeyError

HOST = os.environ["MEMORYDB_HOST"]
DURATION = int(os.environ.get("DURATION", "420"))
INTERVAL = float(os.environ.get("INTERVAL", "0.02"))

def connect():
    """クラスターに接続する。失敗したらフルジッター付きの指数バックオフ(0.5 秒から 2 倍、上限 8 秒)で最大 12 回まで試す。"""
    last = None
    for attempt in range(12):
        try:
            t0 = time.time()
            client = ValkeyCluster(host=HOST, port=6379, ssl=True, socket_timeout=1.0, socket_connect_timeout=2.0,
                                   cluster_error_retry_attempts=0)
            print(json.dumps({"connected": True, "attempt": attempt + 1, "seconds": round(time.time() - t0, 1)}))
            return client
        except Exception as e:
            last = e
            print(json.dumps({"connect_error": type(e).__name__, "attempt": attempt + 1,
                              "seconds": round(time.time() - t0, 1), "message": str(e)[:160]}))
            time.sleep(random.uniform(0, min(8.0, 0.5 * 2 ** attempt)))
    raise last

def lambda_handler(event, context):
    start_n = int(event.get("start", 1))
    t_start = time.time()
    client = connect()
    ok = []
    unresolved = []
    errors = {}
    outages = []
    n = start_n
    outage_start = None
    while time.time() - t_start < DURATION:
        key = f"order:{n}"
        done = False
        for attempt in range(10):
            try:
                if client.set(key, str(n)):
                    done = True
                    break
            except (ConnectionError, TimeoutError, ClusterDownError, ValkeyError) as e:
                name = type(e).__name__
                errors[name] = errors.get(name, 0) + 1
                if outage_start is None:
                    outage_start = time.time()
                time.sleep(random.uniform(0, min(2.0, 0.1 * 2 ** attempt)))
                try:
                    client.close()
                except Exception:
                    pass
                client = connect()
        if done:
            ok.append(n)
            if outage_start is not None:
                outages.append({"from_n": n, "seconds": round(time.time() - outage_start, 2), "at": round(time.time() - t_start, 1)})
                outage_start = None
        else:
            unresolved.append(n)
        n += 1
        time.sleep(INTERVAL)
    # 検算:OK を記録した n がすべて読めるか
    missing = []
    for m in ok:
        try:
            if client.get(f"order:{m}") != str(m).encode():
                missing.append(m)
        except ValkeyError:
            missing.append(m)
    return {
        "start": start_n, "last_n": n - 1, "ok_count": len(ok), "unresolved": unresolved, "errors": errors,
        "outages": outages, "missing_after_check": missing, "elapsed": round(time.time() - t_start, 1),
        "ok_first": ok[:3], "ok_last": ok[-3:],
    }

2. フェイルオーバーを起こす

Lambda 関数を AWS CLI で同期で呼び出し、書き込みが始まってから、別の端末で次のコマンドを実行しました。

aws memorydb failover-shard --cluster-name order-counter-cache --shard-name 0001

failover-shard は、フェイルオーバーが起きたときのアプリケーションの動作を試すための API です。24 時間に 5 回まで使えます(2026 年 10 月時点)。

Lambda 関数の実行は 7 分半ほどかかり、AWS CLI の読み取りタイムアウトのデフォルト(60 秒)より長くなります。同じ手順で試すときは、aws lambda invoke に --cli-read-timeout を付けて、当該関数の実行時間より長い値か 0 にしてください。
最初の 2 回は、接続し直しに再試行を入れていない関数で試しました。どちらも ValkeyClusterException で、Lambda 関数がエラーで終わりました。メッセージは、1 回目が「Timeout connecting to server」、2 回目が「Connection reset by peer」です。イベントの「Starting failover」は、呼び出しからそれぞれ 79 秒後と 94 秒後でした。

エラーのスタックトレースは、どちらも、SET が失敗した後に接続し直す行を指していました。フェイルオーバーかその後の旧プライマリの回復の間に SET が失敗し、続く接続し直しの 1 回目でも失敗して、そのまま Lambda 関数が終わったと判断しました。
セキュリティグループやサブネットの設定の問題も疑いました。DURATION を 20 秒にして呼び出したところ、22 秒で 775 件を書き込めたので、設定の問題ではないと判断しました。
この 2 回のフェイルオーバーで、プライマリは 1 回目で 0001-002 に替わり、2 回目で 0001-001 に戻りました。

3. 接続し直しにも再試行を入れて、もう一度試す

手順 1 のコードのとおり、接続し直しにも再試行を入れて、3 回目の呼び出しを行いました。最初の接続は 1 回目の試行(0.4 秒)で成功し、Lambda 関数の開始から約 54 秒後に failover-shard を実行しました。この回が 3 回目のフェイルオーバーで、直前のプライマリとレプリカの配置は作成時と同じです。

以降の結果は、すべてこの 3 回目のものです。

4. イベントで進み方を見る

describe-events で取ったイベントです。開始から完了まで 48 秒で、その後に旧プライマリ 0001-001 の回復が始まりました。1 行目は、手順 2 の 2 回目のフェイルオーバーの後に始まった、0001-002 の回復の完了です。

01

フェイルオーバー先は、レプリカだった 0001-002 です。MemoryDB コンソールのクラスターの画面では、最終イベントに旧プライマリ 0001-001 の回復の完了が出ています。ノードがプライマリかレプリカかは、この画面には出ません。

02

5. 書き込み側の結果を見る

Lambda 関数の戻り値です。

03

項目 値
書き込んだ件数(ok_count) 15,822
エラー(errors) TimeoutError 1 回、ClusterDownError 8 回
書き込みが止まった時間(outages) 17.94 秒(関数の開始から 60.5 秒後に止まり、78.4 秒後に order:2170 で再開)
試行の上限でも書き込めなかった件数(unresolved) 0(画像では件数の unresolved_count に整形)
終了後に GET して読めなかった件数(missing_after_check) 0

elapsed(457.0 秒)は、書き込みの後の検算の GET も含めた、当該関数の中の経過時間です。

書き込みが止まり始めたのは、failover-shard の実行から約 6 秒後、イベントの「Starting failover」から約 2 秒後でした。再開は、イベントの「completed」より 28 秒早く、書き込み側はイベントの完了を待たずに戻りました。

次は、接続し直したときのログです。

04

最初の SET が TimeoutError になった後、接続し直しは、タイムアウト、Error 104、Connection refused(2 回)と失敗し、5 回目で接続できました。Error 104 のメッセージは、コードで 160 文字に切っているため、途中までしか出ていません。

その後の ClusterDownError(8 回)は、接続はできても書き込めない状態です。この間に、新しいプライマリへの切り替えが進んでいたと考えています。9 回のエラーはすべて order:2170 のもので、10 回目の試行で書き込めました。上限ちょうどです。

確認してみた

データは失われなかったか

終了後に、OK が返った 15,822 件すべてを GET して値を確かめ、欠落は 0 件でした。MemoryDB は、整合性のとれたレプリカにフェイルオーバーし、その間にデータを失わない設計です。

https://docs.aws.amazon.com/ja_jp/memorydb/latest/devguide/autofailover.html

プライマリノードに障害が発生すると、整合性のとれたレプリカへのフェイルオーバーがトリガーされ、フェイルオーバー中にデータが失われることはありません。これで、書き込みは新しいプライマリノードから処理されます。

止まった時間はどのくらいか

https://docs.aws.amazon.com/ja_jp/memorydb/latest/devguide/autofailover.html

通常は数秒で、クラスターへの書き込みが再び可能になります。

今回は 17.9 秒で、「数秒」より長くなりました。ただし、この値には、クライアント側の再試行の待ち(1 回あたり最大 2 秒)と、接続し直しの待ちが含まれています。クラスターが書き込みを受け付けなかった時間そのものではありません。

一方、最初の SET が TimeoutError になるまでの待ち(socket_timeout の 1 秒まで)は含まれていません。止まった時間は、例外を受け取った時刻から数えているためです。測ったのは 1 回だけなので、毎回この秒数になるとは限りません。

イベントの文言

開始と完了のイベントは次のメッセージです。

https://docs.aws.amazon.com/ja_jp/memorydb/latest/devguide/autofailover.html

  1. クラスターメッセージ: FailoverShard API called for shard <shard-id>
  2. クラスターメッセージ: Failover from primary node <primary-node-id> to replica node <node-id> completed

今回のクラスターで出たのは、「Starting failover for shard 0001」「Failover to replica node order-counter-cache-0001-002 completed」「Recovering node order-counter-cache-0001-001」「Finished recovery for node order-counter-cache-0001-001」でした。開始と完了の 2 つは、同じページの CLI の例の JSON と同じ文言です。回復の 2 つは、ドキュメントの一覧では「Recovering nodes」「Finished recovery for nodes」(複数形)になっています。イベントの文字列で判定する場合は、実際の文言を確かめてから使ってください。

まとめ

本ブログが誰かの参考になれば幸いです。

参考資料

クラスメソッドオペレーションズ株式会社について

クラスメソッドグループのオペレーション企業です。
運用・保守開発・サポート・情シス・バックオフィスの専門チームが、IT・AIをフル活用した「しくみ」を通じて、お客様の業務代行から課題解決や高付加価値サービスまでを提供するエキスパート集団です。
当社は様々な職種でメンバーを募集しています。
「オペレーション・エクセレンス」と「らしく働く、らしく生きる」を共に実現するカルチャー・しくみ・働き方にご興味がある方は、クラスメソッドオペレーションズ株式会社 コーポレートサイト をぜひご覧ください。※2026年1月 アノテーション㈱から社名変更しました

この記事をシェアする

AWSのお困り事はクラスメソッドへ

関連記事