ElastiCache Redisを使うべきタイミング:RDS読み取り負荷をキャッシュ層で解消する実践ガイド

RDSのスロークエリログを眺めていると、同じSELECT文が毎秒何十回も実行されているのに気づくことがある。商品カタログ、ユーザープロファイル、設定値など、更新頻度が低いのに読み取り頻度が高いデータがRDSを圧迫しているケースだ。ElastiCache Redisをキャッシュ層として導入することで、そのような読み取りクエリの大半をデータベースに到達させずに処理できる。

TL;DR:ElastiCache Redis導入の判断基準

観点内容
主な用途読み取り頻度が高く、更新頻度が低いデータのキャッシュ
効果が出やすいパターン同一クエリの繰り返し実行、セッション管理、ランキング集計
効果が薄いパターン書き込み主体のワークロード、データが常に変化するケース
主要な構成要素レプリケーショングループ、プライマリエンドポイント、リーダーエンドポイント
キャッシュ戦略Cache-Aside(Lazy Loading)が最も一般的
注意点キャッシュ無効化(TTL設計)とコールドスタート対策が必要

ElastiCache Redisの仕組みとアーキテクチャ

ElastiCache for Redisは、AWSが管理するインメモリデータストアだ。RDSへのクエリがキャッシュ層でヒットすれば、ディスクI/Oもネットワークラウンドトリップも発生しない。レイテンシはミリ秒単位からサブミリ秒単位に短縮される。

クラスターモード無効(Cluster Mode Disabled)構成では、1つのプライマリノードと最大5つのリードレプリカで構成されるレプリケーショングループを作成する。プライマリエンドポイントへの書き込みは非同期でレプリカに伝播し、リーダーエンドポイントはラウンドロビンでリードレプリカに読み取りを分散する。

graph LR App["アプリケーション"] --> Redis["ElastiCache Redis
リーダーエンドポイント"] Redis -->|"キャッシュヒット"| App Redis -->|"キャッシュミス"| App App -->|"キャッシュミス時のみ"| RDS["Amazon RDS"] RDS --> App App -->|"結果をキャッシュに書き込み
プライマリエンドポイント"| RedisW["ElastiCache Redis
プライマリエンドポイント"] style Redis fill:#c8e6c9 style RedisW fill:#c8e6c9 style RDS fill:#ffccbc
  1. アプリケーションがまずElastiCache Redisに問い合わせる
  2. キャッシュヒットの場合、Redisが直接レスポンスを返す(RDSへのクエリは発生しない)
  3. キャッシュミスの場合、アプリケーションがRDSに問い合わせ、取得したデータをRedisに書き込んでからクライアントに返す
  4. 次回以降の同一リクエストはキャッシュヒットとなり、RDSの負荷が低減される

ElastiCache Redisが有効なユースケース

すべての読み取りクエリをキャッシュすべきではない。効果が出るのは、データの変化頻度に対してアクセス頻度が著しく高い場合だ。

  • 商品カタログ・マスターデータ:1日数回の更新に対して毎秒数千回の読み取りが発生するケース
  • セッション管理:RDSにセッションデータを保存しているとスケールアウトが困難になる。Redisへの移行でステートレスなアプリケーション層を実現できる
  • ランキング・集計結果:RedisのSorted Setを使えば、RDSで重いGROUP BY集計を実行せずにランキングを維持できる
  • APIレスポンスキャッシュ:外部APIや重いJOINクエリの結果をシリアライズしてキャッシュする
Redisはデータベースの代替ではなく、データベースへのアクセスを減らすためのバッファだ。キャッシュが消えても(ノード障害、TTL期限切れ)、システムはRDSから正しいデータを取得できなければならない。

ElastiCache Redisクラスターの構築手順

Step 1: サブネットグループの作成

ElastiCacheはVPC内に配置する。まずキャッシュサブネットグループを作成し、プライベートサブネットを指定する。RDSと同じVPC内のプライベートサブネットを使うことで、アプリケーションからの通信がAWSネットワーク内で完結する。

aws elasticache create-cache-subnet-group \
  --cache-subnet-group-name my-redis-subnet-group \
  --cache-subnet-group-description "Redis subnet group for app tier" \
  --subnet-ids subnet-0a1b2c3d4e5f6a7b8 subnet-0b2c3d4e5f6a7b8c9 \
  --region us-east-1

Step 2: セキュリティグループの設定

ElastiCacheノードへのアクセスはポート6379に限定し、アプリケーション層のセキュリティグループからのインバウンドのみを許可する。インターネットからの直接アクセスは絶対に許可しない。

aws ec2 create-security-group \
  --group-name redis-sg \
  --description "Security group for ElastiCache Redis" \
  --vpc-id vpc-0123456789abcdef0 \
  --region us-east-1

aws ec2 authorize-security-group-ingress \
  --group-id sg-0redis1234567890a \
  --protocol tcp \
  --port 6379 \
  --source-group sg-0app1234567890b \
  --region us-east-1

Step 3: レプリケーショングループの作成

クラスターモード無効でレプリケーショングループを作成する。--num-cache-clustersにはプライマリ1台+リードレプリカ数を合計した値を指定する。本番環境では最低でもリードレプリカを1台用意してフェイルオーバーに備える。

aws elasticache create-replication-group \
  --replication-group-id my-redis-rg \
  --replication-group-description "Redis cache for app read offload" \
  --num-cache-clusters 2 \
  --cache-node-type cache.r7g.large \
  --engine redis \
  --engine-version 7.1 \
  --cache-subnet-group-name my-redis-subnet-group \
  --security-group-ids sg-0redis1234567890a \
  --at-rest-encryption-enabled \
  --transit-encryption-enabled \
  --region us-east-1

Step 4: エンドポイントの確認

作成完了後、プライマリエンドポイントとリーダーエンドポイントを取得する。アプリケーションは書き込みにプライマリエンドポイント、読み取りにリーダーエンドポイントを使い分けることで、リードレプリカへの負荷分散が機能する。

aws elasticache describe-replication-groups \
  --replication-group-id my-redis-rg \
  --query 'ReplicationGroups[0].NodeGroups[0].{Primary:PrimaryEndpoint,Reader:ReaderEndpoint}' \
  --output json \
  --region us-east-1

Cache-Aside戦略の実装(Python例)

Cache-Aside(Lazy Loading)は最もシンプルで実績のあるキャッシュ戦略だ。アプリケーションがキャッシュを明示的に管理し、ミスした場合のみデータベースに問い合わせる。書き込みはキャッシュを無効化(削除)するか、TTLの自然期限切れに任せる。

🔽 Pythonによるキャッシュ実装例(クリックで展開)
import redis
import json
import psycopg2
from typing import Optional

# リーダーエンドポイント:読み取りクエリをリードレプリカに分散させる
redis_reader = redis.Redis(
    host='my-redis-rg-ro.xxxxxx.ng.0001.use1.cache.amazonaws.com',
    port=6379,
    ssl=True,
    decode_responses=True
)

# プライマリエンドポイント:キャッシュへの書き込みはプライマリノードに向ける
redis_writer = redis.Redis(
    host='my-redis-rg.xxxxxx.ng.0001.use1.cache.amazonaws.com',
    port=6379,
    ssl=True,
    decode_responses=True
)

CACHE_TTL_SECONDS = 300  # 5分間キャッシュを保持

def get_product(product_id: str) -> Optional[dict]:
    cache_key = f'product:{product_id}'

    # Step 1: リーダーエンドポイント経由でキャッシュを確認
    cached = redis_reader.get(cache_key)
    if cached:
        return json.loads(cached)  # キャッシュヒット

    # Step 2: キャッシュミス時のみRDSに問い合わせ
    conn = psycopg2.connect(host='rds-endpoint', dbname='mydb',
                            user='app_user', password='secret')
    with conn.cursor() as cur:
        cur.execute('SELECT id, name, price FROM products WHERE id = %s',
                    (product_id,))
        row = cur.fetchone()
    conn.close()

    if row is None:
        return None

    product = {'id': row[0], 'name': row[1], 'price': str(row[2])}

    # Step 3: プライマリエンドポイント経由でキャッシュに書き込む
    redis_writer.setex(cache_key, CACHE_TTL_SECONDS, json.dumps(product))

    return product

def invalidate_product_cache(product_id: str) -> None:
    # 商品データが更新された際にキャッシュを明示的に削除する
    cache_key = f'product:{product_id}'
    redis_writer.delete(cache_key)

ElastiCache Redisの運用監視

キャッシュ層を導入した後、最初に確認すべきメトリクスはキャッシュヒット率だ。ヒット率が低い場合、TTL設計の見直しかキャッシュキーの設計に問題がある可能性が高い。

graph TD M1["CacheHits / CacheMisses
ヒット率の監視"] --> D1{"ヒット率 < 80%?"} D1 -->|"Yes"| A1["キャッシュキー設計
TTL設定を見直す"] D1 -->|"No"| M2["Evictions
強制削除数の監視"] M2 --> D2{"Evictions増加?"} D2 -->|"Yes"| A2["ノードタイプの
スケールアップを検討"] D2 -->|"No"| M3["CurrConnections
接続数の監視"] M3 --> D3{"接続数が急増?"} D3 -->|"Yes"| A3["接続プール設定を
確認・調整する"] D3 -->|"No"| OK["正常運用"]
  1. CacheHits / CacheMisses:ヒット率の計算に使う。ヒット率 = CacheHits / (CacheHits + CacheMisses)
  2. CurrConnections:接続数の急増はアプリケーション側の接続プール設定の問題を示すことが多い
  3. Evictions:メモリ不足でキャッシュが強制削除されている場合に増加する。ノードタイプのスケールアップを検討する
  4. DatabaseMemoryUsagePercentage:メモリ使用率が高い状態が続く場合、Evictionsが増加し始める前にスケールアップを判断する

CacheHitsメトリクスをCloudWatchで確認するコマンド例:

aws cloudwatch get-metric-statistics \
  --namespace AWS/ElastiCache \
  --metric-name CacheHits \
  --dimensions Name=CacheClusterId,Value=my-redis-rg-001 \
  --start-time 2024-01-15T00:00:00Z \
  --end-time 2024-01-15T01:00:00Z \
  --period 300 \
  --statistics Sum \
  --region us-east-1

よくある失敗パターン:「キャッシュを入れたのにRDSが遅いまま」

ElastiCacheを導入したのにRDSの負荷が下がらないという相談を受けたことがある。CloudWatchを確認すると、CacheMissesがCacheHitsを大幅に上回っていた。

原因を調べると、キャッシュキーにユーザーIDとタイムスタンプを含めていた。タイムスタンプが秒単位で変化するため、同一ユーザーの同一データに対してキャッシュが一切ヒットしていなかった。

キャッシュキーはデータの同一性を表すパラメータのみで構成しなければならない。リクエストのたびに変化する値(タイムスタンプ、リクエストIDなど)をキーに含めると、キャッシュは実質的に機能しない。修正後、CacheHits率は85%を超え、RDSのCPU使用率は半分以下に低下した。

キャッシュキーの設計ミスは、CloudWatchのCacheMissesメトリクスを見るまで気づきにくい。導入直後に必ずヒット率を確認する習慣をつけること。

IAMポリシー:最小権限でElastiCacheを操作する

ElastiCacheの管理操作に必要なIAMポリシーを示す。Describe*などの読み取り系アクションはリソースレベルの制限をサポートしていないためResource: "*"が必要だ。作成系アクションも同様にリソースARNを事前に特定できないためResource: "*"を使用する。変更・削除系アクションは特定のレプリケーショングループARNに限定できる。

🔽 ElastiCache管理用IAMポリシー(クリックで展開)
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "ElastiCacheReadOperations",
      "Effect": "Allow",
      "Action": [
        "elasticache:Describe*",
        "elasticache:ListTagsForResource"
      ],
      "Resource": "*"
    },
    {
      "Sid": "ElastiCacheModifyDeleteOperations",
      "Effect": "Allow",
      "Action": [
        "elasticache:ModifyReplicationGroup",
        "elasticache:DeleteReplicationGroup"
      ],
      "Resource": "arn:aws:elasticache:us-east-1:123456789012:replicationgroup:my-redis-rg"
    },
    {
      "Sid": "ElastiCacheCreateOperations",
      "Effect": "Allow",
      "Action": [
        "elasticache:CreateReplicationGroup",
        "elasticache:CreateCacheSubnetGroup"
      ],
      "Resource": "*"
    }
  ]
}

ElastiCache Redisを使うべきタイミング:まとめと次のステップ

ElastiCache Redisが効果を発揮するのは、同一データへの読み取りが繰り返し発生し、そのデータが短期間では変化しないケースだ。RDSのスロークエリログで同一クエリが頻出しているなら、まずキャッシュ対象の候補として検討する価値がある。

導入後は必ずCacheHits/CacheMissesメトリクスを監視し、ヒット率が期待値を下回る場合はキャッシュキー設計とTTL設定を見直す。Evictionsが増加し始めたらメモリ不足のサインであり、ノードタイプのスケールアップを検討する。

用語集

用語説明
レプリケーショングループElastiCache for Redisにおける、プライマリノードとリードレプリカで構成されるクラスター単位
Cache-Aside(Lazy Loading)アプリケーションがキャッシュを明示的に管理し、ミス時のみDBに問い合わせるキャッシュ戦略
TTL(Time To Live)キャッシュエントリの有効期限。期限切れ後は自動的に削除される
Evictionメモリ不足時にRedisが古いキャッシュエントリを強制削除する動作
リーダーエンドポイントクラスターモード無効構成でリードレプリカへのラウンドロビン分散を行うエンドポイント

コメント

このブログの人気の投稿

LambdaからプライベートサブネットのRDSに接続できない — VPC設定の正しい理解と診断手順

SQS Visibility Timeoutの完全解説:メッセージ二重処理の原因と対策

Lambda タイムアウト設定を変更する方法:上限・注意点・実運用パターン