SOMPO Digital Lab ソフトウェアエンジニアの木村です。
最近行ったサービスの性能改善について記事にしたいと思います。
課題
運用しているサービスの利用ユーザ増加に伴い、DB(Amazon Aurora)のCPU使用率が常に80%に張り付くようになってしまっていました。 特にピーク時には90%を超えることもあり、早々に対策を打たないとサービスがダウンする可能性があります。
この問題に対する改善を行ったことで、CPU使用率を30%前後まで下げることができましたというのが今回の記事です。
サービスの構成
今回改善の対象としているサービスは次のような構成になっています(本稿に関係ない部分は省略)。
アプリケーションはPython + Djangoで実装されており、Fargate上で稼働しています。
DBへのアクセスはDjangoのQuerysetやModelを用いて行っています。一部複雑なクエリについてはカスタムSQLで直接実行していることもあります。
改善策
今回のようなケースの場合にまず思いつく解決策としては以下の2つがあるかと思います
- SQLのパフォーマンスチューニング
- キャッシュレイヤの導入によるDBアクセスの軽減
記事のタイトルからも分かるとおり、今回は後者を選択することにしたのですが、それにはいくつか理由があります。
- 前者の方法はこれまで何度か繰り返していて、その度にSQLの記述自体が複雑化していっていること
- 問題となっているクエリがアプリケーションでも上位を争うほど頻繁に発行されているものであり、単純に発行回数の問題でDBを逼迫させていること
- キャッシュ導入により画面表示が現在よりも数十ミリ〜数百ミリ秒早くなることがで期待できること。それによりユーザ体験の向上が見込めること
- 運用担当者(=私)が他にもサービスを運用していて、サービス改善にかけられる時間が多くないこと
- 今後DBパフォーマンスで問題が発生した場合に、アクセスを一時的にキャッシュへ逃がす選択ができるようになること
- DBの費用のうちI/Oの占める割合が増加しており、DBアクセスを減らすことができるとコストが大きく削減されること。また浮いたコストがキャッシュ運用費用と比較しても大きくなる計算のため、サービス全体でのコスト削減ができること
主に上記の理由から、キャッシュを導入しDBへのアクセス回数を減らすことでの改善に踏み切りました。
構築手順
対策が決まったのでキャッシュを導入していきます。
テスト環境・本番環境が稼働するクラウド環境はもちろん、開発時の動作チェックも行いたいためローカル環境にも同等の環境を構築していきます。
ライブラリインストール
まずは必要なライブラリを導入していきます。
Django 3.xを利用している & DjangoのキャッシュシステムのバックエンドをRedisに変更したいので、django-redis
を利用します。
python -m pip install django-redis
docker-composeの設定
普段アプリケーションを開発するときには、docker-composeを用いてアプリケーションコンテナ、DBコンテナがコマンド1つで立ち上がるようにしています。
コマンドで立ち上がるコンテナ群にRedisコンテナを追加します
redis: image: redis:7.0.15 container_name: redis ports: - 6379:6379 volumes: - ./cache/data:/data
後ほどクラウド環境構築の際に説明しますが、今回クラウド環境ではElastiCache ServerlessのElastiCache for Redis バージョン 7.1を利用したいため、 ローカルで立ち上げるRedisのバージョンを互換性のある7.0としています。
また、アプリケーションコンテナがキャッシュコンテナより先に立ち上がると困るため、アプリケーションコンテナの設定で依存関係を指定しておくと良いでしょう。
depends_on
- redis
Djangoの設定
次にDjangoのキャッシュシステムを変更していきます。
設定は多くの場合settings.py
ファイルに記載されています。既存のDjangoのキャッシュ設定は次のようになっていました
CACHES = { "default": { "BACKEND": "django.core.cache.backends.db.DatabaseCache", "LOCATION": "django_cache_table", } }
これを次のように変更していきます。
CACHES = { "default": { "BACKEND": "django_redis.cache.RedisCache", "LOCATION": env("REDIS_LOCATION"), "OPTIONS": { "CLIENT_CLASS": "django_redis.client.DefaultClient", } } }
アプリケーションの環境変数の設定(ローカル)
アプリケーションの環境変数を設定して、Redisの接続先を指定します。
REDIS_LOCATION=redis://redis:6379/0
ローカル環境での動作確認
ここまで実施するとローカル環境ではキャッシュが利用できるようになります。
DjangoはViewベースのキャッシングやテンプレートベースのキャッシングができる機能を持っていますが、 既存の処理への影響範囲が見極めづらいことや、そもそもテンプレート機能は利用していない(フロントエンドはDjangoからは独立させてReactで開発しています)ため、一旦はこれらの機能は利用しないことにしました。 単純にDBアクセスの手前でキャッシュ有無を確認し、キャッシュがあればキャッシュから値を取得、キャッシュがなければDBからデータを取得しキャッシュへ保存という手段をとります。
アプリケーション内部からキャッシュへ愚直にアクセスするにはdjango.core.cache
を利用します。
データ取得処理を疑似コード的に書くと以下の様になります。
from django.core.cache import cache # キャッシュからデータ取得 data = cache.get("test_key") # キャッシュからデータが取得できた場合 if data is not None: return data # キャッシュからデータが取得できなかった場合、DBから取得した後、キャッシュに保存する data_from_db = fetch_from_db() cache.set("test_key", data_from_db, 60) # キャッシュの有効期限を60秒として保存 return data_from_db
コード実行後にredis-cli
などでRedisサーバに接続してキャッシュデータベースを確認すると、データが保存されていることが分かるでしょう。
クラウド環境でのキャッシュ構築
ローカルでの動作確認ができたのでクラウド環境でもキャッシュを用意していきます。
前述したとおり、ElastiCache ServerlessのElastiCache for Redis バージョン 7.1を構築していきます。ElastiCacheのサーバレス版は2023年のre:Inventで発表されたものになります。re:Inventの参加録も記事になっていますので是非過去記事をご覧下さい。
インフラ環境は全てTerraformを使ってコード化しているため、Terraformでリソースを記述します。
# ElastiCache Serverlessリソース resource "aws_elasticache_serverless_cache" "app_cache" { name = "app-cache" description = "Cache for app" engine = "redis" cache_usage_limits { data_storage { maximum = 10 unit = "GB" } ecpu_per_second { maximum = 5000 } } daily_snapshot_time = "11:00" kms_key_id = aws_kms_key.cache_key.arn major_engine_version = "7" snapshot_retention_limit = 1 security_group_ids = [module.app_cache_security_group.security_group_id] subnet_ids = data.terraform_remote_state.infra.outputs.private_subnet_ids } # KMS resource "aws_kms_key" "cache_key" { description = "Key for ElastCache" } # セキュリティグループ resource "aws_security_group" "app_cache_security_group" { name = "app-cache-security-group" description = "Security group for app cache" vpc_id = data.terraform_remote_state.infra.outputs.vpc.id } # セキュリティグループのインバウンドルール resource "aws_security_group_rule" "app_cache_security_group_ingress" { type = "ingress" from_port = 6379 to_port = 6379 protocol = "tcp" security_group_id = aws_security_group.app_cache_security_group.id source_security_group_id = module.app_security_group.security_group_id # アプリの属するセキュリティグループを指定 } # セキュリティグループのアウトバウンドルール resource "aws_security_group_rule" "app_cache_security_group_egress" { type = "egress" from_port = 0 to_port = 0 protocol = "-1" cidr_blocks = ["0.0.0.0/0"] security_group_id = aws_security_group.app_cache_security_group.id }
簡単に解説すると以下のリソースを作成しています。
- ElastiCache Serverless
- 今回メインとなるキャッシュインスタンス
- KMS
- キャッシュを暗号化するための鍵
- セキュリティグループ
- キャッシュインスタンスが属するセキュリティグループ。インバウンドルールでアプリの属するセキュリティグループからのみアクセスできるようにしている
これをterraform apply
すると、次のような構成ができあがります。
アプリケーションの環境変数の設定(クラウド)
AWSコンソールを開いて、AWSコンソール > ElastiCache > Redisキャッシュと見ていくと、Redisインスタンスへ接続するためのエンドポイントが表されています。
ここに表示されたエンドポイントをクラウド環境上のアプリケーションの環境変数に設定します。
REDIS_LOCATION=rediss://*****.serverless.apne1.cache.amazonaws.com
上記の通りにRedisを構築すると、データ転送中の暗号化が有効になると思います。スキーマをrediss
としてTLS(SSL)接続とするのを忘れないようにしましょう。
クラウド環境での動作確認
さてアプリケーションをクラウド環境にデプロイしていきましょう。
デプロイ後、DBインスタンスのCPU使用率を監視していると次の画像のようになりました。
11:30頃にリリース作業を実施しましたが、リリース前に80%前後で推移していたCPU使用率がリリース後に30%前後まで下がったことが確認できました。上手くキャッシュが効いてそうですね。
コストについてはもう少し長期的に見る必要がありますが、I/Oも落ち着いて来ていて改善がされていそうです。
まとめ
DBの負荷権限のためにElastiCacheを導入し、CPU使用率を80%→30%と改善することができました。
ElastiCache Serverlessが登場したことで、キャッシュインスタンスも自分たちで管理する必要がなくなり、更に効率の良い運用ができるようになりそうです。
ビジネスでサービスを作る以上、当然キャッシュ導入によるコストと導入しない場合のコストを鑑みつつ検討する必要はありますが、費用対効果が見込めるのであれば選択肢として考えられるのではないでしょうか。
また今回は一旦キャッシュインスタンスのスペックパラメータを適当に設定しましたので最適化の余地がまだあると思います。 今後キャッシュヒット率やストレージ占有率を監視しつつパラメータを最適化することで、より効率的にElastiCache Serverlessを利用できると思います。
SOMPO Digital Labでは一緒に働くソフトウェアエンジニアを募集しています
SOMPO Digital Labでは費用対効果を熟慮しながら、サービスアーキテクチャを考えることのできるエンジニアを募集しています。
以下のリンクからカジュアル面談の応募ができますので、興味を持った方は是非話を聞きに来て下さい