こんにちは、タイミーでバックエンドのテックリードをしている新谷 (@euglena1215) です。
タイミーのバックエンドでは、Feature Flagの管理にFlipperを使用しており、そのデータストアとしてRedisを利用しています。本記事では、Flipperの値をインメモリキャッシュする機構を導入することで、Redisへのアクセスを90%削減し、毎時発生していた突発的なスパイクを解消した事例を紹介します。
本記事が、Feature Flagシステムの負荷やパフォーマンスに課題を抱えているチームの参考になれば幸いです。
Redisで突発的なspikeとエラーが発生していた
タイミーのモノリスRailsリポジトリでは、Feature Flagを活用して新機能の段階的なリリースやA/Bテストを実施しています。しかし、運用を続ける中で、いくつかの問題が顕在化してきました。
毎時0分台に発生する突発的なspike
DatadogでRedisのメトリクスを監視していたところ、毎時0分台に突発的なspikeが発生していることが確認されました。このspikeでは、平常時の約5倍ものアクセスが集中していました。

これはどうやら毎時0分に実行されるcronにより非同期ジョブが大量にenqueueされ、各ジョブの処理でFeature Flagが参照されることによる RedisへのN+1アクセスが発生しているようでした。
そもそもFeature Flagの値はほとんどのケースで変化せず、毎回データストアであるRedisにアクセスする必要はないのではないかと考えました。数秒くらいの期間であれば各プロセスでFlagをキャッシュしておいても、実用上は問題ないはずです。
そこで、プロセス内キャッシュとRedisの値を同期してくれる便利な仕組みを実現しようと考えました。
解決策:Flipper::Adapters::Syncの導入
Flipperには、remoteのデータとlocalのデータをいい感じに同期してくれるFlipper::Adapters::Syncが存在します。このアダプターを使うことで、指定した間隔(interval)でのみremoteからデータを取得し、それ以外はlocalのキャッシュから値を読み取る、という振る舞いを実現できます。
実装の詳細
実装の主なポイントは以下の通りです:
1. Sync Adapterの導入
# config/initializers/flipper.rb require 'connection_pool' require 'flipper/adapters/sync' Flipper.configure do |config| redis_pool = ConnectionPool.new(size: 5, timeout: 5) do Redis.new(url: REDIS_URL) end remote_adapter = Flipper::Adapters::Redis.new(redis_pool) local_adapter = Flipper::Adapters::Memory.new sync_adapter = Flipper::Adapters::Sync.new(local_adapter, remote_adapter, interval: 10) config.adapter do sync_adapter end end
Sync Adapterは、localのMemory adapterとremoteのRedis adapterを組み合わせて使用します。interval: 10と指定することで、10秒間はlocalのキャッシュから値を読み取り、10秒経過後に再度Redisから最新の値を取得します。
2. Adapter初期化の最適化
重要なポイントとして、Sync Adapterのインスタンスをconfig.adapter do ~ endブロックの外で作成しています。これには理由があります:
- ブロック内に記述すると、リクエスト毎にアダプターが初期化されてしまう
- 外で作成することで、プロセス内で単一のインスタンスを共有できる
- 結果として、プロセス内で同じローカルキャッシュが使われる
3. Redis Connection Poolの導入
gem 'connection_pool'
これまでは毎リクエストでRedis接続を確立していましたが、Sync Adapterを導入することで接続が長期間保持されるようになります。そのため、接続が切れた場合の耐障害性を高めるため、Flipper 1.3.3から導入されたRedis Connection Poolsの機能を使ってconnection poolを持つようにしました。
4. リクエスト毎のメモ化を無効化
Rails.application.configure do # Sync adapter の @local (Memory) がプロセス内キャッシュとして機能するため、 # リクエストごとのメモ化は不要 config.flipper.memoize = false end
Flipperではデフォルトでリクエスト毎に同一flagを参照した際にメモ化する最適化が有効になっていますが、これを無効にしました。メモ化が有効になっていると、リクエスト毎にSync adapterのlocalデータが揮発してしまう問題があったためです。Sync adapterが導入されたことで、そもそもメモ化は不要になりました。
導入前後の効果
検証環境での検証結果
本番導入前に、demo環境で動作確認を行いました。社内管理画面に全てのfeature flagに対してFlipper.enabled?を呼び出すテストボタンを用意し、導入前後の挙動を比較しました。
Before:全リクエストでFlag数回のRedisアクセス

After:10秒中の最初のリクエストのみRedisアクセス

元の実装では、全てのリクエストでFlag数回(21回)のRedisアクセスが発生していましたが、変更後は同一プロセスにおける10秒中の最初のリクエストを除き、Redisアクセスが発生しなくなったことを確認しました。
本番環境での効果
本番環境に導入した結果、以下の効果が確認できました:
- 平常時のRedisアクセスが90%削減
- リクエスト毎のRedisアクセスがほぼ不要になり、大幅なアクセス削減を実現
- 毎時0分台のspikeが完全に解消
- 平常時の5倍のアクセスが発生していたspikeが消滅
- Redisの負荷が安定し、インスタンスサイズダウンの可能性も見えてきた

まとめ
FlipperにFlipper::Adapters::Syncを導入することで、Feature FlagシステムのRedis負荷を90%削減し、毎時発生していた突発的なspikeを解消することができました。
この改善により、以下のメリットが得られました:
- 安定性の向上:timeout errorの減少、spike解消による安定した動作
- パフォーマンスの向上:Redis I/Oの削減による応答速度の改善
- コスト削減の可能性:Redis負荷の削減によるインスタンスサイズダウンの検討
Feature Flagの値が頻繁に変更されない特性を活かし、適切にキャッシュすることで、大きな改善効果を得ることができました。同様の課題を抱えているチームの参考になれば幸いです。