この記事は アイスタイル Advent Calendar 2022 7日目の記事です。
こんにちは。アイスタイルにて会員基盤の運用保守や機能改修を担当しているyukawacです。
Spring BootでElasticache for Memcachedのキャッシュ操作をする機会があったので、
キャッシュ操作するまでに実施したことやコード例を書きます。
⚠️注意⚠️
※本記事ではElasticache for Memcachedへ疎通するためのネットワーク周りの設定については記載しません。
※ローカル環境にて、JavaとDockerの実行環境は構築済みとします。
※ElastiCache for Memcachedクラスターは作成済みとします。
一連の流れ
- ローカル環境でmemcachedを立ち上げる(dockerコンテナ)
- Spring Bootでローカル環境のmemcachedのキャッシュ操作をするコードを作成する
- Elasticache for Memcachedへ接続できるように設定を書き換える
筆者のローカル環境
- OS : Windows10
- IDE : IntelliJ IDEA 2021.3.1
- Docker環境 : Rancher Desktop 1.6.2
- OpenJDK : Amazon Corretto 17
それでは、進めます。
1. ローカル環境でmemcachedを立ち上げる(dockerコンテナ)
いきなりElastiCache for Memcachedへ接続はせず、
ローカル環境構築も兼ねて、まずはローカル環境でmemcachedを立ち上げ、接続を試みました。
memcached公式イメージを利用します。
ローカル環境でdockerコマンドを実行し、memcachedを立ち上げます。
# 初回起動
docker run --name demo-memcached --env TZ=Asia/Tokyo -d -p 11211:11211 memcached:latest
telnetコマンドにて接続確認できます。
telnet localhost 11211
※ちなみに、停止と2回目以降の起動コマンドは下記です。
# 停止
docker stop demo-memcached
# 2回目以降の起動
docker start demo-memcached
2. Spring Bootでローカル環境のmemcachedのキャッシュ操作をするコードを作成する
2-1. Spring Bootプロジェクトを作成する
Spring InitializrでSpring Bootプロジェクトを生成します。
主要なものは
- Spring Boot:2.7.6 (作成時の最新2系)
- Java:17
- Spring Web (動作確認デモ用に追加)
- Lombok (動作確認デモ用に追加)
を選択しました。
「GENERATE」をクリックしてdemo.zipをダウンロードし、展開します。
展開したプロジェクトはIntelliJ IDEAで開きます。
2-2. Memcachedクライアントライブラリの依存関係をbuild.gradleに記述しダウンロードする
JavaのMemcachedクライアントはAmazon ElastiCache Cluster Clientを利用します。
その他のクライアントライブラリでもキャッシュ操作することはできますが、
AWSのElastiCache for Memcachedの機能
等を利用するためにはAWSのライブラリの利用が必要となります。
build.gradleファイルのdependenciesに下記を追加し、ダウンロードします。
implementation 'com.amazonaws:elasticache-java-cluster-client:1.2.0'
2-3. Cacheインターフェースを実装する
キャッシュ操作のためのCacheインターフェースを実装します。
今回は最低限、@Cacheableを利用したキャッシュの登録、取得が操作できるような実装例を記載します。
Cacheインターフェースの実装例
package com.example.demo;
import java.util.concurrent.Callable;
import lombok.extern.slf4j.Slf4j;
import net.spy.memcached.MemcachedClient;
import org.springframework.cache.Cache;
import org.springframework.cache.support.SimpleValueWrapper;
@Slf4j
public class Memcached implements Cache {
private final String name;
private final MemcachedClient memcachedClient;
public Memcached(String name, MemcachedClient memcachedClient) {
this.name = name;
this.memcachedClient = memcachedClient;
}
@Override
public String getName() {
return this.name;
}
@Override
public Object getNativeCache() {
return this.memcachedClient;
}
@Override
public ValueWrapper get(Object key) {
Object value = this.memcachedClient.get(key.toString());
if (value == null) {
log.debug("cache miss for key: " + key); // 動作確認用DEBUGログ
return null;
}
log.debug("cache hit for key: " + key); // 動作確認用DEBUGログ
return new SimpleValueWrapper(value);
}
@Override
public <T> T get(Object key, Class<T> type) { return null; } // 実装省略
@Override
public <T> T get(Object key, Callable<T> valueLoader) { return null; } // 実装省略
@Override
public void put(Object key, Object value) {
if (value == null) {
return;
}
this.memcachedClient.set(key.toString(), 24 * 60 * 60 /* expiration 1 day */, value);
log.debug("cache put for key: " + key); // 動作確認用DEBUGログ
}
@Override
public void evict(Object key) { } // 実装省略
@Override
public void clear() { } // 実装省略
}
2-4. Memcached接続設定クラスを作成する
CachingConfigurerSupportを継承したMemcached接続設定クラスを作成します。
CachingConfigurerSupportを継承したMemcached接続設定クラス実装例
package com.example.demo;
import java.io.*;
import java.util.*;
import lombok.extern.slf4j.*;
import net.spy.memcached.*;
import org.springframework.beans.factory.annotation.*;
import org.springframework.cache.*;
import org.springframework.cache.annotation.*;
import org.springframework.cache.interceptor.*;
import org.springframework.cache.support.*;
import org.springframework.context.annotation.*;
@EnableCaching
@Configuration
@Slf4j
public class CachingConfiguration extends CachingConfigurerSupport {
@Value("${cache.hostname}")
private String hostname;
@Value("${cache.port}")
private String port;
@Bean
MemcachedClient memcachedClient() {
String memcachedAddress = hostname + ":" + port;
/* ※1 */
ClientMode clientMode = hostname.contains("cache.amazonaws.com") ? ClientMode.Dynamic : ClientMode.Static;
/* ※2 */
ConnectionFactory connectionFactory = new ConnectionFactoryBuilder(new KetamaConnectionFactory())
.setClientMode(clientMode)
.build();
try {
return new MemcachedClient(connectionFactory, AddrUtil.getAddresses(memcachedAddress));
} catch (IOException e) {
throw new RuntimeException(e);
}
}
@Override
@Bean
public CacheManager cacheManager() {
SimpleCacheManager cacheManager = new SimpleCacheManager();
// 複数のCacheインスタンスをセットできますが、今回は1つのみセットします。
cacheManager.setCaches(Collections.singletonList(new Memcached("cache", memcachedClient())));
cacheManager.afterPropertiesSet();
return cacheManager;
}
@Override
public CacheErrorHandler errorHandler() {
return new SimpleCacheErrorHandler();
}
}
ローカル用接続のapplication.yml設定例
cache:
hostname: localhost
port: 11211
logging:
level:
com.example.demo: DEBUG # ログ出力用
※補足
- クライアントモードを決定しています。
ClientMode clientMode = hostname.contains("cache.amazonaws.com") ? ClientMode.Dynamic : ClientMode.Static;
オートディスカバリ機能を利用するためにはClientMode.Dynamic
を利用する必要があります。
Elasticache for Memcachedクラスターへ接続するときのみClientMode.Dynamic
を利用し、
オートディスカバリ機能がないmemcachedへ接続する場合はClientMode.Static
を利用するように分岐させています。
2. 効率的な負荷分散のための ElastiCache クライアントの設定をするため、KetamaConnectionFactoryインスタンスを指定しています。
new ConnectionFactoryBuilder(new KetamaConnectionFactory())
2-5. キャッシュ操作のための、動作確認デモ用のエンドポイントを用意する
動作確認デモ用にエンドポイントを用意します。
コントローラクラス
package com.example.demo;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequiredArgsConstructor
public class DemoController {
final DemoService demoService;
@GetMapping("/demo")
public ResponseEntity<String> demo() {
String value = demoService.getValue("keyName");
return new ResponseEntity<>(value, HttpStatus.OK);
}
}
サービスクラス
package com.example.demo;
import lombok.RequiredArgsConstructor;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
public class DemoService {
/**
* @Cacheableでキャッシュ機能の有効化をしています。
* キャッシュがなければ、return文の"Value!!"値が返却され、keyNameをキーとしてキャッシュ登録されます。
* キャッシュがあれば、キャッシュに登録されている"Value!!"値が返却されます。
*/
@Cacheable(cacheNames = "cache", key = "#keyName")
public String getValue(final String keyName) {
return "Value!!";
}
}
以上のクラスや設定ファイルを用意したら、Spring Bootアプリケーションを起動します。
http://localhost:8080/demo
へGETリクエストすると、ローカル環境にてキャッシュ操作の動作確認ができます。
数回リクエストすると動作確認用DEBUGログにより、キャッシュ登録・取得操作の様子がうかがえます。
2022-12-05 14:17:19.783 DEBUG 25796 --- [nio-8080-exec-7] com.example.demo.Memcached : cache miss for key: keyName
2022-12-05 14:17:19.788 DEBUG 25796 --- [nio-8080-exec-7] com.example.demo.Memcached : cache put for key: keyName
2022-12-05 14:17:25.890 DEBUG 25796 --- [nio-8080-exec-8] com.example.demo.Memcached : cache hit for key: keyName
2022-12-05 14:17:26.624 DEBUG 25796 --- [nio-8080-exec-9] com.example.demo.Memcached : cache hit for key: keyName
3. Elasticache for Memcachedへ接続できるように設定を書き換える
ElastiCache for Memcachedクラスターの接続設定をapplication.ymlに記載します。
AWSマネジメントコンソールから作成済のクラスターの詳細画面を開き、「設定エンドポイント」に記載されているホスト名とポートを設定します。
ElastiCache for Memcachedクラスターの設定エンドポイントを記載したapplication.yml例
# 設定エンドポイントに「demo-memcached.xxxxx.cfg.apne1.cache.amazonaws.com:11211」と記載されている場合
cache:
hostname: demo-memcached.xxxxx.cfg.apne1.cache.amazonaws.com
port: 11211
※ホスト名は必ず「.cfg」が入っているものを指定すること(重要)
設定変更後、Spring BootアプリケーションをElastiCache for Memcachedクラスターに接続できるネットワーク上(AWS上であればEC2やFargate等)に置いて起動すると、キャッシュ操作ができます。
以上、Spring BootでElasticache for Memcachedのキャッシュ操作をするまでの一連の流れでした。
memcachedサーバーに接続できているアプリケーションは、接続をElasticache for Memcachedクラスターに向けるよう設定変更すれば接続可能ということがわかりました。
(おまけ)オートディスカバリ機能の動作確認
ElastiCache for MemcachedクラスターへClientMode.Dynamic
モードで接続するとオートディスカバリ機能が利用できます。
クラスターへ定期的に問い合わせをし、ノード数の増減が有るか無いかを確認し、
クラスターの状態を把握し接続設定を更新するようになります。
ElastiCache for Memcachedクラスターのノード増減によってconfigurationが変更される様子はログで確認できます。(WARNレベルで出力されます。)
クラスターのノード数を1から4に増やしたとき
Level:WARN Logger:n.s.m.ConfigurationPoller:90 Message:Change in configuration - Existing configuration: Version:1
CacheNode List:
demo-memcached.xxxxxx.0001.apne1.cache.amazonaws.com:11211
New configuration:Version:2
CacheNode List:
demo-memcached.xxxxxx.0001.apne1.cache.amazonaws.com:11211
demo-memcached.xxxxxx.0002.apne1.cache.amazonaws.com:11211
demo-memcached.xxxxxx.0003.apne1.cache.amazonaws.com:11211
demo-memcached.xxxxxx.0004.apne1.cache.amazonaws.com:11211
configurationのversionが1から2に上がり、0001~0004ノードが認識されるようになりました。
クラスターのノード数を4から2に減らしたとき
Level:WARN Logger:n.s.m.ConfigurationPoller:90 Message:Change in configuration - Existing configuration: Version:2
CacheNode List:
demo-memcached.xxxxxx.0001.apne1.cache.amazonaws.com:11211
demo-memcached.xxxxxx.0002.apne1.cache.amazonaws.com:11211
demo-memcached.xxxxxx.0003.apne1.cache.amazonaws.com:11211
demo-memcached.xxxxxx.0004.apne1.cache.amazonaws.com:11211
New configuration:Version:3
CacheNode List:
demo-memcached.xxxxxx.0001.apne1.cache.amazonaws.com:11211
demo-memcached.xxxxxx.0002.apne1.cache.amazonaws.com:11211
configurationのversionが2から3に上がり、0003と0004のノードは認識されなくなりました。
自動検出されるため、ノードを追加してもアプリケーション側の修正は必要ありません。便利です。
参考文献
- Spring Boot
- Elasticache for Memcached
- memcached公式イメージ
- Amazon ElastiCache Cluster Client (Memcached for Java)
- MVN REPOSITORY ElastiCacheJavaClusterClient
- オートディスカバリ
- 転送時の暗号化 (TLS)
最後までお読みいただきありがとうございました。