AWSでRedisを使う(Elasticache + Redis + ioredis)
はじめに
アプリケーションを構築する際、キャッシュを導入することでパフォーマンスの向上やユーザビリティの向上といった点が期待できます。
今回はAWSリソースで構築されたシステムに対してAWS Elasticacheを用いてサーバーサイドキャッシュを構築したのでその手順をまとめます。
キャッシュの種類
キャッシュは読み込み頻度が高く、書き込み頻度が低いデータに対して利用されることが多いです。
また、消去が許容されるものであることも重要な基準になります。
しかし、キャッシュとして保持しておくデータは性質によって保持する場所を考えなくてはなりません。
保持する場所はユーザー側で保持するか、それ以外の場所で保持するかの二つに大別できます。
ユーザー側でのキャッシュ保持
ブラウザキャッシュ
webブラウザのストレージでキャッシュを保持することを指します。
静的コンテンツやユーザー情報を含むデータをキャッシュデータとして保持することで2回目以降のアクセスの際に高速で表示することが可能になります。
ユーザー側以外でのキャッシュの保持
サーバーサイドキャッシュ
コンテンツを配信するサーバー側でキャッシュデータを保持することを指します。
ユーザーに関するデータをキャッシュする場合もありますが、主には複数ユーザーに共有されるデータや動的コンテンツを保持します。
例えば、都道府県の一覧をDBに保持している都道府県一覧webページがあるとします。
ユーザーAが都道府県一覧ページを訪れた際にwebサーバーはDBまで都道府県一覧を取得しに行き、ユーザーAへレスポンスを送ります。
次にユーザーBが都道府県一覧ページを訪れた際、webサーバーはDBに問い合わせるのではなく、自身に保持していた都道府県一覧データをユーザーBに返却します。
そうすることでユーザーBは通常より速いレスポンスをwebサーバーから受け取ることができます。
ここでの都道府県一覧データは、ユーザーによって変化がない共有することができるデータのためサーバーサイドのキャッシュに適していると言えます。
サーバーサイドキャッシュのフロー例
以上のように例ではwebアプリケーションのサーバーキャッシュについて挙げましたが、他にもDNSサーバーキャッシュなどビジネスロジックを処理するサーバー以外でのサーバーでのキャッシュもサーバーサイドキャッシュに含まれます。
AWS Elasticache
AWS ElasticacheはRedis、Memcachedをサポートしているインメモリデータベースです。
インメモリデータベースとは揮発性メモリにデータを管理し、書き込み/読み込み速度を向上させたデータベースのことを指します。
そのため、Webサーバーのストレージを利用するキャッシュシステムより処理速度が速いことが特徴です。
今回はRedisエンジンを利用します。
揮発性メモリと不揮発性メモリ
メモリには揮発性メモリと不揮発性メモリの2種類があります。
揮発性メモリはread/write速度が速いことが特徴です。
ただ、電圧がないとデータを保持できないためPCのメインメモリとして利用されています。
RAM(Random Access Memory)と称され、その中でも構造が容易で安価なDRAMやCPUキャッシュメモリとして利用されるSRAMが代表で挙げられます。
不揮発性メモリは揮発性メモリと比較してread/write速度は劣りますが、電圧がない状況でもデータを保持することができるメモリです。
そのためPCのストレージとして利用されています。
ROM(Read Only Memory)と称され、SSDやハードディスクが該当します。
Redis
RedisはNoSQLに分類されるでデータベースです。
NoSQLとは簡単にいうとRDBのようにテーブル同士のリレーションを用いてデータを管理するのではなく、単純なKey:Valueのように一つのKeyに対して1つのValueが対応するデータ形式を管理するデータベースです。
特徴として、RDBより処理速度が速いのでリアルタイム性が必要なデータの管理などで利用される場面が多いです。
Redisは複数コマンドが一気に与えられてもキュー方式で処理を行うシングルスレッド方式で処理を実施します。
RDSとNoSQL
データベースの形式には2種類あり、SQLを利用してデータを取得するRDB(Relational Data Base)とkeyを元にvalueを取得するNoSQLがあります。
RDSの特徴としてはデータを意味のあるかたまり(table)として保存することと、table同士の関係を用いて複雑なデータ構造を表すことができるのが特徴です。
一方、NoSQLのデータベースはkey-valueといった1:1の形式でデータを保持しており、しデータの取得速度が速いが、複雑なデータ構造を持つことができないのが特徴です。
下例のように李信の位をデータとして持つ場合、NoSQLでは1:1で、RDBでは2つのtableからcategoryを元にtable同士をくっつけて取得します。このようにRDBはtable同士を疎結合に作ることでデータ管理に拡張性を持たせることができますが、NoSQLよりかは処理速度が劣ってしまいます。
NoSQLとRDBのデータ構造の違い
Redisのデータ構造
Redis特有の機能としてRedis ClusterとSlotという概念があります。
Redis ClusterとはRedisがデータを分割して保持する管理機能です。
Redis Clusterの中に複数のRedis nodeが存在し、nodeごとにデータは保存されます。
このようにデータを分割して保持することをシャーディングと呼び、そのシャーディングを管理するのがRedis Clusterです。
slotはnode間で重複しないキーです。
setする際にslotが割り当てられます。(下図の赤線)
リクエストの際はkeyだけでなく、keyに基づいた slot番号をRedisが処理します。そのslot番号をもとにnode内を順次探索していきます。(下図の青線)
※下図のslot番号はテキトーです
Redisのデータ構造
Redisで扱えるデータ型
Redisでは扱えるデータ型が9つあります。
単純な文字列からオブジェクト、ビットマップまでも扱うことができます。
詳しい使い方はRedis公式ドキュメントを参照ください
構築
今回は表題の通りAWS ElasticacheのRedisエンジンを用いてシングルノードの単純なキャッシュサーバーを立てます。
手動でも構築はできますが、Terraformから作成します。
全体構成としては、下図のようになっています。
パブリックサブネットの一つにElasticacheを立ち上げ、2つのパブリックサブネットからキャッシュサーバーにアクセスできるように設定します。
アーキテクチャ図
1.Elasticacheの設定
resource "aws_elasticache_subnet_group" "サブネットリソース名" {
name = <任意の名称>
subnet_ids = [
<配置するサブネットのID>
]
}
resource "aws_elasticache_cluster" "クラスターリソース名" {
cluster_id = <任意のID>
engine = "redis"
engine_version = "7.1"
node_type = "cache.t2.micro"
num_cache_nodes = 1
parameter_group_name = "default.redis7"
subnet_group_name = <配置サブネット名>
security_group_ids = <アタッチセキュリティグループID>
port = 6379
apply_immediately = true
snapshot_retention_limit = 0 //スナップショットを取らない(データの永続性をオフにする)
}
terraformからAWS Elasticacheリソースを作成する場合は
aws_elasticache_cluster
aws_elasticache_subnet_group
が必要です。
aws_elasticache_subnet_group
ではElasticacheを立てるサブネットの指定を、
aws_elasticache_cluster
ではElasticacheのエンジンなどサーバー本体の設定を指定します。
ちなみに、今回はシングルノード構成なのでaws_elasticache_cluster
を指定しましたが、レプリケーション構成の場合はaws_elasticache_replication_group
を指定します。
aws_elasticache_cluster
での主な設定項目は以下です。
-
cluster_id
任意のidを指定します。 -
engine
動作するエンジンのオプションでRedisまたは、Memcachedを指定できます。 -
engine_version
エンジンバージョンを指定します。
バージョンを指定せずに起動した場合、最新バージョンで起動します。 -
node_type
インスタンスのスペックです。
利用要件に合わせてスペックを選択します。
このオプションによって時間あたりの課金金額が異なります。
種類の詳細はこちらです。https://docs.aws.amazon.com/ja_jp/AmazonElastiCache/latest/red-ug/CacheNodes.SupportedTypes.html
-
num_cache_nodes
ノード数を選択します。
今回はシングルノードのため1を設定しています。 -
parameter_group_name
メモリ使用量の最大値やバックアップ頻度、接続のタイムアウト設定などエンジンの詳細な設定パラメータになります。
engine_versionで指定したエンジンに合わせてパラメータグループを選択する必要があります。 -
security_group_ids
Elasticacheにアタッチするセキュリティグループのを指定します。 -
port
portはデフォルトで6379になります。 -
apply_immediately
こちらを有効にしておくことでリソースの再作成を行った際に属性の変更が即座に適用されるようになります。 -
snapshot_retention_limit
スナップショットを保持するかの設定フラグになります。
インスタンスのスペックによっては利用できません。
インスタンスが対応しているかの確認はこちらを参照ください。https://docs.aws.amazon.com/ja_jp/AmazonElastiCache/latest/red-ug/CacheNodes.SupportedTypes.html
2.Security Groupの設定
Elcasticacheに適用するセキュリティグループを作成します。
今回キャッシュサーバーはECSからのアクセスのみ許可したいため、IN方向のルールはECSセキュリティグループをsource_security_group_idに設定します。
# ElastiCacheのセキュリティグループ
resource "aws_security_group" "elasticache_sg" {
vpc_id = <VPC ID>
tags = {
Name = "elasticache-sg"
}
}
# OUT方向のセキュリティグループのルールを作成
resource "aws_security_group_rule" "elasticache_to_ecs" {
security_group_id = <ElastcacheのセキュリティグループID>
type = "egress"
description = "Allow to Any"
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
# IN方向のセキュリティグループのルールを作成
resource "aws_security_group_rule" "ecs_for_elasticache" {
security_group_id = <ElastcacheのセキュリティグループID>
type = "ingress"
from_port = 6379
to_port = 6379
protocol = "tcp"
# ECSのセキュリティグループからの通信のみ許可
source_security_group_id = <ECSセキュリティグループのID>
}
3.ECSの設定
AWS Elasticacheは任意のエンドポイントを決めることができず、リソースが作成される時点でエンドポイントが生成されます。
そのため、エンドポイントをバックエンドサーバー側に伝播させる必要があります。
伝播方法はいろいろあると思いますが、今回はECSコンテナの環境変数にエンドポイントを設定してあげます。
ECSのタスク定義で環境変数を設定する方法は割愛しますが、AWS Elasticacheのエンドポイントの取得は
aws_elasticache_cluster.<クラスターリソース名>.cache_nodes[0].address
で取得できます。
ポートはaws_elasticache_cluster.<クラスターリソース名>.port
です。
以上の設定でインフラ側のAWS Elasticacheを利用する準備はできました🎉
実装
今回はバックエンドサーバーがtypescriptでの実装だったのでioredisを利用しました。
ioredis
ioredisはRedisサーバーへの操作をソースコードから行えるライブラリです。
GitHubのREADMEに利用方法の詳細が載っていますので全ての説明は割愛させていただきます。
接続
インストールしたredisパッケージをインポートし、Redisインスタンスを生成します。
インポート方式ですが、import { Redis } from 'ioredis'
かconst Redis = require(
ioredis)`でインポートできます。
インポートしたRedisパッケージに対してエンドポイント、ポート名を渡してインスタンス生成するとconnectionが作成されます。
import { Redis } from "ioredis"
//引数なしの場合はlocalhost:6379が自動設定
const redis = new Redis(host:<生成されたエンドポイント>, port:<ポート番号>);
set
キャッシュサーバーに対してデータをセットするメソッドです。
データのセットが完了するとOK
の文字列を返却します。
//キーmykeyに対してhogeをセット
redis.set("mykey", "hoge")
//返り値はセットの成功/失敗を返す(OK/NG)
const res = redis.set("mykey", "hogehoge")
また、EX
オプションを利用することで対象キーの保持期間を設けることも可能です。
redis.set("hoge", "fuga", 'EX', 300); //キー:hogeは300秒後に消去される
get
設定したキーをもとにvalueを取得するメソッドです。
問い合わせたキーがある場合はvalueを、存在しない場合はnullが返却されます。
また、第二引数にコールバック関数を設定することでエラーハンドリングも行えます。
redis.get('hoge') // => 'fuga'
redis.get("mykey", (err, result) => {
if (err) {
console.error(err);
} else {
console.log(result); // Prints "value"
}
})
今回は特に複雑なデータを保持するだけでなかったんどえstringの簡単なset,getで実装しましたが、
簡単な演算を実施して格納することやトランザクションを設定することも可能なようでいろいろ幅広く利用できそうです。
ハマったとこ
キャッシュサーバーの中身を見たい
Eelasticacheは直接キャッシュされているデータを見る機能はありません。
そのため、踏み台のEC2を構築してredis-cliをインストールしてEelasticacheに対してコマンドを送るしかないです。
動作確認の際は踏み台EC2を用意しておきましょう。
キャッシュサーバーにデータは登録されているのにキャッシュを返してこない
Elasticacheを構築する自体で難しいことはありませんでしたが、動作確認で全然キャッシュが使われていないことに気付きました。
原因としてはクライアント側でキャッシュを持ってしまっており、そもそもサーバー側に2回目の処理が行われていなかったため、リクエストが飛んでなかったです。。
ブラウザ側でキャッシュを持たないようにしたのにキャッシュを返さない
前項のチョンボを修正して今度こそ2回目のリクエストでキャッシュを返してくれるだろうとワクワクしながら動作確認をしました。
しかしやはりキャッシュを返してこない。。
ブラウザからのリクエストは飛ぶように修正したのに何故だろうと思うと、、今度はCloudFrontでキャッシュを持っていました。
CloudFrontの設定を修正するとしっかりElasticacheにあるキャッシュデータを返してくれるようになりました。
環境のキャッシュしうる箇所を最初から把握しておくべきでした。
まとめ
今回はAWS Elasticacheを立てるための設定にフォーカスを置いてまとめてみました。
構築自体は非常に簡単ですが、Redisの動作原理を忠実に再現している機能であるためRedis自体を知らないといけないなと感じるところがありました。
私自身Redisを触るのが初めてだったのでもっと使いこなせていけたらと思います。
実装面ではまだまだ使えてない機能がたくさんあるので機会があれば使ってみたいです。
参考
terraform
ioredis
Discussion