S3 の Multi-Region Access Points でクロスリージョン参照性能が改善するか試してみた

S3 の Multi-Region Access Points でクロスリージョン参照性能が改善するか試してみた

複数リージョンの Lambda からバージニア北部(us-east-1)の S3 を参照する構成で、Amazon S3 Multi-Region Access Points(MRAP)を使うと取得時間がどう変わるかを測りました。us-east-1 以外のリージョンでは p50 が 58〜83%(S3 API)、76〜92%(CloudFront 経由、キャッシュなし)短くなりました。
2026.10.21

はじめに

前回の記事では、Lambda Web Functions を複数リージョンに展開した構成を扱いました。バージニアの S3 を CloudFront 経由で参照するときの遅延には、Origin Shield で対処しました。

https://dev.classmethod.jp/articles/cloudfront-origin-shield-s3-cross-region-latency/

Origin Shield は、キャッシュヒット率の向上とネットワーク性能の改善で遅延を抑える対策です。今回は根本対策の候補として、Amazon S3 Multi-Region Access Points(以下、MRAP)を評価しました。

バージニアの S3 を東京(ap-northeast-1)、オレゴン(us-west-2)、フランクフルト(eu-central-1)へレプリケーションして MRAP を作りました。4リージョンの Lambda Event Function(従来の Lambda)から取得する時間を比較しています。比較したのは、S3 API と CloudFront 経由のそれぞれで、取得先を S3 単一バケットにした場合と MRAP にした場合です。更新がレプリカに届くまでの時間と、追加費用の見積もりもあわせて紹介します。

MRAP の仕組み

MRAP は、複数リージョンのバケットをまとめた1つのグローバルなエンドポイントです。リクエストは AWS Global Accelerator を通って AWS のネットワークに入り、ネットワーク遅延に基づいて最寄りのバケットへ振り分けられます。バケット間のデータは、S3 のクロスリージョンレプリケーションで別途そろえます。

利用にあたって押さえておく点は次の3つです。

  • リクエストには SigV4A 署名が必要です。
  • MRAP のポリシーと各バケットのバケットポリシーの両方で許可されたときにアクセスできます。
  • MRAP に含めるバケットは作成後に追加・変更・削除できなくなります。

https://docs.aws.amazon.com/ja_jp/AmazonS3/latest/userguide/MultiRegionAccessPoints.html

構築した構成

us-east-1、ap-northeast-1、us-west-2、eu-central-1 の4リージョンにバージョニングを有効にしたバケットを作り、us-east-1 から他の3つへレプリケーションルールを設定しました。次に、us-east-1 のバケットに前回と同じ200ファイル(gzip 圧縮、128〜714バイト)をコピーし、完了から約23秒後に確認すると、3つのバケットすべてにそろっていました。

MRAP は us-west-2 の s3control API で作ります。作成は非同期で、READY になるまで数分かかりました。

MRAP の作成コマンド(mrap-create.json)
aws s3control create-multi-region-access-point \
  --region us-west-2 \
  --account-id <ACCOUNT_ID> \
  --client-token <トークン> \
  --details file://mrap-create.json
{
  "Name": "oss-mrap-verify",
  "PublicAccessBlock": {"BlockPublicAcls": true, "IgnorePublicAcls": true, "BlockPublicPolicy": true, "RestrictPublicBuckets": true},
  "Regions": [
    {"Bucket": "oss-mrap-us-east-1-<SUFFIX>"},
    {"Bucket": "oss-mrap-ap-northeast-1-<SUFFIX>"},
    {"Bucket": "oss-mrap-us-west-2-<SUFFIX>"},
    {"Bucket": "oss-mrap-eu-central-1-<SUFFIX>"}
  ]
}

Lambda から MRAP を読む

Node.js 22 ランタイムに同梱の AWS SDK(v3)では SigV4A の実装を読み込めず、そのままでは MRAP から取得できませんでした。@aws-sdk/client-s3 と @aws-sdk/signature-v4a をアプリに同梱しました。コードの先頭で @aws-sdk/signature-v4a を import すると取得できました。

https://docs.aws.amazon.com/sdkref/latest/guide/feature-s3-mrap.html

呼び出しは通常の GetObjectCommand で、Bucket にバケット名の代わりに MRAP の ARN を渡します。<MRAP_ALIAS> には、aws s3control get-multi-region-access-point が返す Alias の値(末尾の .mrap を含む)を入れます。

import { S3Client, GetObjectCommand } from "@aws-sdk/client-s3";
import "@aws-sdk/signature-v4a";

const s3 = new S3Client({ region: "us-east-1" });
const response = await s3.send(new GetObjectCommand({
  Bucket: "arn:aws:s3::<ACCOUNT_ID>:accesspoint/<MRAP_ALIAS>",
  Key: "path/to/object"
}));

Lambda の実行ロールには、MRAP の arn:aws:s3::<ACCOUNT_ID>:accesspoint/<MRAP_ALIAS>/object/* と4つのバケットの /* に対する s3:GetObject を許可しました。

CloudFront から MRAP を読む

CloudFront のオリジンに MRAP を指定するときは、S3 オリジンではなく Custom Origin として設定します。ドメイン名は <MRAP_ALIAS>.accesspoint.s3-global.amazonaws.com です。

OAC(Origin Access Control)は、署名プロトコルを sigv4a にして作ります。OAC の種類は、オリジンが Custom Origin でも s3 を指定します。

{
  "Name": "oss-mrap-sigv4a-oac",
  "SigningProtocol": "sigv4a",
  "SigningBehavior": "always",
  "OriginAccessControlOriginType": "s3"
}
オリジンの設定
{
  "Id": "MRAP-oss-mrap",
  "DomainName": "<MRAP_ALIAS>.accesspoint.s3-global.amazonaws.com",
  "OriginAccessControlId": "<OAC_ID>",
  "CustomOriginConfig": {
    "HTTPPort": 80,
    "HTTPSPort": 443,
    "OriginProtocolPolicy": "https-only",
    "OriginSslProtocols": {"Quantity": 1, "Items": ["TLSv1.2"]}
  }
}

4つのバケットのバケットポリシーと MRAP のポリシーの両方で、CloudFront のサービスプリンシパルに s3:GetObject を許可しました。いずれも aws:SourceArn でこのディストリビューションに限定しました。

https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/private-content-restricting-access-to-s3-mrap.html

バケットポリシー(4バケット共通)
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "AllowCloudFrontViaMRAP",
      "Effect": "Allow",
      "Principal": {"Service": "cloudfront.amazonaws.com"},
      "Action": "s3:GetObject",
      "Resource": "arn:aws:s3:::<BUCKET>/*",
      "Condition": {
        "StringEquals": {
          "aws:SourceArn": "arn:aws:cloudfront::<ACCOUNT_ID>:distribution/<DIST_ID>"
        }
      }
    }
  ]
}

キャッシュポリシーなどその他の設定は、前回の S3 オリジンのディストリビューションと同じです。

計測方法

計測は前回と同じ方法です。4リージョンに置いた Event Function(Node.js 22、512MB)が、200ファイルを1件ずつ順に取得し、取得ごとの時間を記録します。8ラウンド(各リージョン・各経路1,600件)を実行しました。

CloudFront 経由はラウンドごとにクエリ文字列 k を変え、エッジにキャッシュがない状態から取得しました。Origin Shield は使っていません。S3 API は AWS SDK for JavaScript v3 で取得しました。S3 単一バケットの場合は us-east-1 のバケット名を、MRAP の場合は MRAP の ARN を Bucket に指定しました。

計測用 Event Function のコード(index.mjs)
// event: { mode: "cf" | "s3" | "mrap", base, bucket, prefix, k, files: [..] }
//   cf:   https://<ディストリビューション>/<prefix><file>?k=<k> を順に取得する
//   s3:   bucket を S3 API で直接取得する
//   mrap: bucket に MRAP の ARN を渡して S3 API(SigV4A)で取得する
import { S3Client, GetObjectCommand } from "@aws-sdk/client-s3";
import "@aws-sdk/signature-v4a";

const now = () => performance.now();

export const handler = async (event) => {
  const rows = [];
  if (event.mode === "s3" || event.mode === "mrap") {
    const s3 = new S3Client({ region: "us-east-1" });
    const get = async (key) => {
      const r = await s3.send(new GetObjectCommand({ Bucket: event.bucket, Key: key }));
      return (await r.Body.transformToByteArray()).length;
    };
    await get(event.prefix + event.files[0]); // 接続の確立(計測に含めない)
    for (const f of event.files) {
      const t0 = now();
      let status = 200, bytes = 0;
      try { bytes = await get(event.prefix + f); } catch (e) { status = e.$metadata?.httpStatusCode ?? 0; }
      const t = now() - t0;
      rows.push([f, status, +t.toFixed(1), +t.toFixed(1), "", "", bytes]);
    }
  } else {
    const url = (f, k) => `${event.base}/${event.prefix}${f}?k=${k}`;
    try { const w = await fetch(url(event.files[0], "warm-" + event.k)); await w.arrayBuffer(); } catch (e) {}
    for (const f of event.files) {
      const t0 = now();
      let status = 0, ttfb = 0, xc = "", pop = "", bytes = 0;
      try {
        const r = await fetch(url(f, event.k), { signal: AbortSignal.timeout(10000) });
        ttfb = now() - t0;
        status = r.status; xc = r.headers.get("x-cache") ?? ""; pop = r.headers.get("x-amz-cf-pop") ?? "";
        bytes = (await r.arrayBuffer()).byteLength;
      } catch (e) { status = -1; xc = String(e.name); }
      const t = now() - t0;
      rows.push([f, status, +ttfb.toFixed(1), +t.toFixed(1), xc, pop, bytes]);
    }
  }
  return { region: process.env.AWS_REGION, mode: event.mode, k: event.k, columns: ["file", "status", "ttfb_ms", "total_ms", "x_cache", "pop", "bytes"], rows };
};

計測結果

単位は ms です。

S3 API で直接取得

p50

呼び出し元 S3 単一バケット MRAP 短縮率(%)
ap-northeast-1 173.6 30.0 83
eu-central-1 116.4 28.3 76
us-west-2 89.8 37.6 58
us-east-1 28.6 29.8 −

p95

呼び出し元 S3 単一バケット MRAP 短縮率(%)
ap-northeast-1 193.3 35.6 82
eu-central-1 129.7 34.3 74
us-west-2 105.0 49.9 52
us-east-1 42.1 44.4 −

S3 単一バケットでは us-east-1 から離れるほど取得時間が延びましたが、MRAP ではどのリージョンからも p50 で 28〜38ms に収まりました。us-east-1 からの取得は、S3 単一バケットと MRAP でほぼ同じでした。

CloudFront 経由(エッジにキャッシュなし)

p50

呼び出し元 S3 単一バケット MRAP 短縮率(%)
ap-northeast-1 492.0 38.2 92
eu-central-1 312.0 37.0 88
us-west-2 233.6 55.3 76
us-east-1 44.7 38.7 −

p95

呼び出し元 S3 単一バケット MRAP 短縮率(%)
ap-northeast-1 519.2 53.0 90
eu-central-1 337.4 54.5 84
us-west-2 270.8 83.6 69
us-east-1 78.0 76.7 −

ap-northeast-1 では p50 で 492ms から 38ms になりました。前回の Origin Shield(p50 159.5ms)と比べても短く、MRAP を S3 API で直接取得した場合(p50 30.0ms)と同程度の値です。

更新が反映されるまでの時間

us-east-1 のバケットの同じキーを5回上書きしました。他の3バケットに新しい内容が届くまでの時間と、ap-northeast-1 の Event Function から MRAP で新しい内容が読めるまでの時間を測りました。レプリケーションは標準の設定で、S3 Replication Time Control は使っていません。

回 上書きから3バケットすべてに届くまで 上書きから ap-northeast-1 の MRAP で読めるまで
1 22.7秒 22.3秒
2 21.7秒 21.3秒
3 22.6秒 22.2秒
4 22.7秒 21.7秒
5 22.6秒 22.2秒

5回とも約22秒でした。この間に ap-northeast-1 から MRAP で読むと、古い内容が HTTP 200 で返りました。

追加費用の見積もり

MRAP を使うと、S3 単一バケットの構成に対して次の費用が追加になります。

  • レプリケーション先のバケットへの PUT リクエスト(宛先リージョンの単価)
  • リージョン間のデータ転送(1GB あたり $0.02)
  • レプリカのストレージ
  • MRAP のデータルーティング(1GB あたり $0.0033)

本番で対象にしている著者ページの JSON の構成を想定して計算しました(1,200ファイル、1ファイル約1KB、1日1回全ファイルを上書き、ダウンロードは1日1万回)。バージョニングの旧バージョンは削除する想定で、そのストレージ費用は含めていません。

  • レプリケーション PUT:1,200件/日 × 30日 = 3.6万件/月。宛先3リージョンの単価(1,000件あたり東京 $0.0047、オレゴン $0.005、フランクフルト $0.0054)で $0.17 + $0.18 + $0.19 ≒ $0.54/月
  • リージョン間のデータ転送:1.2MB × 30日 × 3リージョン ≒ 108MB → $0.002/月
  • レプリカのストレージ:1.2MB × 3リージョン → $0.001 未満/月
  • MRAP データルーティング:1KB × 1万回 × 30日 = 300MB → $0.001/月

合計で約 $0.54/月で、ほとんどがレプリケーション先への PUT リクエストです。

https://aws.amazon.com/jp/s3/pricing/

まとめ

MRAP を使うことで、S3 から離れたリージョンの Lambda でも、同一リージョンの S3 とほぼ同じ水準の参照性能が得られました。

ただ、本番で対象にしている著者ページの JSON は1,200ファイルを毎日すべて更新するため、毎日全ファイルがレプリケーションの対象になります。また、MRAP の導入で管理の複雑化が予想されるため、当面必要な性能が得られる Origin Shield の利用を継続する予定です。

MRAP は認証などの理由で CloudFront を挟まず S3 を直接参照したい場合や、大容量のファイルを Lambda や EC2 から高頻度で参照してクロスリージョン転送の課金が問題になりそうな場合に、あらためて採用を検討したいと思います。

この記事をシェアする

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

関連記事