Caffeineでキャッシュのエントリのキャッシュ有効時間に揺らぎを入れる方法

こんにちは。今日はCaffeineでキャッシュのエントリに対して キャッシュ有効時間に対してランダムな揺らぎを導入する方法をここに書いておきます。

キャッシュに対してランダムな揺らぎを入れる必要性に関してはこちらの記事を読むと一部書いてあると思います。

また、AWS Architecture Blogにある「Exponential Backoff and Jitter」を読むと良いかもしれません。

こちらはこちらで面白いですが、キャッシュの話は関係ないです。 アクセス負荷を平滑する手法について、シミュレーションとその結果から生成されるグラフについて紹介してくれています。

この記事ではキャッシュそのものについて説明しません。

Caffeineとは

CaffeineとはGuavaにあるCacheライクなAPIを持つインメモリキャッシュのためのライブラリです。 高速らしいです。

Caffeine自体の使い方については以下のブログを参照してください

実装方針

Caffeineにはいつからか、Expiryというインターフェースが用意されており以下のようなインターフェースになっています。

public interface Expiry<K, V> {
  // currentDurationは現在時点から見てexpireするまでの時間です。
  // 全ての単位は nanosecondsです。

 // エントリが作成された時点からどのくらいでexpireするかを戻り値で返す
  long expireAfterCreate(@NonNull K key, @NonNull V value, long currentTime);
 // エントリが更新された時点からどのくらいでexpireするかを戻り値で返す
  long expireAfterUpdate(@NonNull K key, @NonNull V value,
      long currentTime, @NonNegative long currentDuration);
 // エントリが読み込みされた時点からどのくらいでexpireするかを戻り値で返す
  long expireAfterRead(@NonNull K key, @NonNull V value,
      long currentTime, @NonNegative long currentDuration);
}

また、以下のように、CacheのビルダーにexpireAfterにExpiryを渡すことが出来るようになっています。

Caffeine.newBuilder()
   .expireAfter(new MyExpiry<>())
   .build();

実装してみます

今回の10分をベースに25%の範囲(7.5分〜12.5分)で揺らぎのあるExpiryを書いてみます。

以下のような形になりました。

/**
 * 7.5分〜12.5分の間で揺らぎのあるExpiry
 */
public class MyExpiry<K, V> implements Expiry<K, V> {

    public final Random = new Random();
    public final long base = TimeUnit.NANOSECONDS.convert(10, TimeUnit.MINUTES);
    public final double jitterFactor = 0.25;
    
    
    @Override
    public long expireAfterCreate(K key, V value, long currentTime) {
        return return (long) (base + jitterFactor * base * (2 * rand.nextDouble() - 1));;
    }
    
    @Override
    public long expireAfterUpdate(K key, V value, long currentTime,
            long currentDuration) {
        return currentDuration;
    }
    
    @Override
    public long expireAfterRead(K key, V value, long currentTime,
            long currentDuration) {
        return currentDuration;
    }

expireAfterCreateを実装して他はcurrentDurationを返すだけです。

今回はLRUではなく固定サイズ、固定期間キャッシュするようなイメージの物を実装したので expireAfterReadはcurrentDurationを返すようにしています また、今回自分の用途ではupdateをしないので、expireAfterUpdateもcurrentDurationを返すようにしています。

利用する際は、必要に応じてこれらのメソッドを適切に実装してください。

まとめ

今回は簡単にCaffeineでキャッシュの有効期間に対してランダム性をもたせることで 負荷を平滑化しようとしてこの記事で書いたような実装を書きました。

メモ書きに近いですが、誰かの訳に立てると幸いです。

負荷試験はこれからです。

reactor-logbackを試した。+ LMAX Disruptorの Technical Paperを読んだメモ

以下のモジュールを試した。

Reactor LogbackはLMAX Disruptorの上に作られたReactorのアドオンだ。 このモジュールはアプリケーションのための高速で非同期なロギングの機能を提供する。パフォーマンスもロギングも諦めたくないあなたにおすすめだ。

利用方法はリポジトリに書かれているが、簡単だ。リポジトリに書いてあるサンプルを下に示しているが logback-classicに入っているAsyncAppenderと同じ形で適用可能だ。

<configuration>

  <!-- The underlying appender will be the standard console one. -->
  <appender name="stdout" class="ch.qos.logback.core.ConsoleAppender">
    <encoder>
      <pattern>
        %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n
      </pattern>
    </encoder>
  </appender>

  <!-- Wrap calls to the console logger with async dispatching to Disruptor. -->
  <appender name="async" class="reactor.logback.AsyncAppender">
    <appender-ref ref="stdout"/>
  </appender>

  <!-- Direct all logging through the AsyncAppender. -->
  <root level="info">
    <appender-ref ref="async"/>
  </root>

</configuration>

適用した結果はあまり代わり映えしなかったが この状態で負荷試験をしてみたい。

なぜ試したか

ある本を読んでいて、Reactorのモジュールの紹介の中にreactor-logbackが紹介されていた。

気になったのでリポジトリを覗いてみたところ LMAX Disruptorを使っていると書かれており、試したくなった。

LMAX Disruptorとは

詳細については別途調べてほしいが 以下のテクニカルペーパーを読むと非常に興味深い。

彼ら曰く、高速なFXのトランザクション処理システムを作っていたところ パフォーマンスの追求の末、Queueが遅いことが分かったようだ。

非常に高速なQueueの開発をしていたが、結果としてはQueueでは駄目だ、となったようだ。 従来の構造では、ロックによる競合が激しくこれによってスループットが下がってしまうらしい。

上記に示したテクニカルペーパーにはロックなどの技術に関して オーバーヘッドがどのくらいか、といった話が記載されている。

Abstractにはこう記載されている 「Testing has shown that the mean latency using the Disruptor for a three-stage pipeline is 3 orders of magnitude lower than an equivalent queue-based approach. In addition, the Disruptor handles approximately 8 times more throughput for the same configuration.」

3ステージあるパイプラインをQueueベースのものと比べたところ、3桁低いレイテンシを計測し 8倍のスループットを弾き出したようだ。

非常に興味内容が他にも色々書かれている。 ぜひハイパフォーマンスを求める並列処理を書きたいあなたに読んでほしい。

なぜLMAX Disruptorを調べたか

これはElasticのAPMのコードを見たときに依存関係に見たことのないツールが入っていたからだ。

この他にも興味深い依存関係が書かれているので 非常にコードとしても面白いリポジトリだった。

ただ、disruptorは全然聞いたことないので 調べてみたところ、日本語でいくつかのブログが見つかった。 そのブログの中にあるリンクを辿るといくつかの英語のブログにもリンクがあった。 その中には有名なMartin Fowlerも記事を書いて紹介していた。

まとめ

最近見つけたreactor-addonである、reactor-logbackの紹介記事でした。 また、その中で使われているLMAX DisruptorのTechnical Paperを読んだメモを調べた経緯と共に書いた。

OSSのツールの中身を見るのは楽しい。 まだ見たことのないライブラリに出会いたい。

そういえば、途中だがTechnical Paperの和訳を自分用メモとして書いたので ここにリンク

Disruptor · GitHub

を残しておく

SpringfoxはComponentScanを利用しているので spring-context-indexerでComponentのindexをすることは出来ない

出来ない。 理由はタイトルに書いた通り、springfox (現在最新 2.9.2) ではComponentScanを利用している。

なぜか

spring-context-indexerを有効にする際は、ComponentScanを利用しているjarが全て対応していないといけない。

じゃあどうするか。

indexerでindexすることは出来ないので ここで紹介されている とおり spring.propertiesに以下の記述を追加しましょう。

spring.index.ignore=true

まとめ

諦めた。

じゃあライブラリとしてはどうするべきか

AutoConfigurationを用意するのが正しいはず。