Redis::Objects を使ったサービス改善と新しい gem を作ったお話
半年ぶりくらいに会社の勉強会で発表しました。
Redis::Objects という gem を使って RDS にかかっていた IO 負荷を改善した、という話と、 Redis::Objects を拡張する gem を作ったという話(後述)です。
Redis を使って DB の負荷を下げる系のお話って 2010 年代にされ尽くした感があって、今更な気もするんですが、一周回って最近聞かない話だなーと思ってます。 フロントエンドエンジニアで最近サーバーサイドも触る機会が増えたような方々にウケが良かった感じがありますね。
一方で社内のインフラエンジニアからは Redis ってメンテナンスの手間が掛かって評判悪いですね。 使うだけなら手軽で楽なんですが、AWS のアップデートでダウンタイムが発生する時に利用者向けにアナウンスしないといけなかったり。 ダウンタイムと言っても深夜に数秒〜数分なので影響は少ないと思うんですが、エンタープライズ向けのサービスとかやってるとこの辺は厳しいですね。
SRE のエラーバジェットの概念 が日本全体に浸透すればソフトウェア開発の生産性は底上げされるんじゃないかと思うのですが、この辺は時間を掛けて少しずつ啓蒙していくしか無いですね。例えば日本の生活インフラ(電気・水道・ガスとか電車とか)ってめっちゃ高水準なので、ソフトウェアの世界でも100%稼働することが当たり前という価値観になってしまうんじゃないかな、と思うのですが、この辺の価値観は表裏一体というか、当たり前と感じているから高品質になる一方で、生産性は下がってしまうという構図だと思っています。
https://www.nic.ad.jp/ja/materials/iw/2017/proceedings/s15/s15-fujisaki.pdf
一応メンテナンスの手間、という点では夏頃に AWS から Amazon MemoryDB for Redis というフルマネージドな Redis が発表されています。 東京リージョンは未対応で、書き込み速度が遅くなっていたり、料金も若干高くなっていたりしますが、気になるサービスではありますね。
さて、この記事のメインは冒頭で触れた Redis::Objects を拡張する gem を作ったよ、という話です。
発端は Redis::Objects の counter
を使ってサービスの効果測定(メール配信数、CV 数、ログイン数など)をしたいなーと考えたことなんですが、効果測定って一定の期間毎に集計して改善した・していないの指標にしたいやつじゃないですか?
counter
でカウントアップしつつ、日次のバッチ処理で集計すれば良いんですが、それだとあまり気軽じゃないというか、測定対象となるサービス開発の後、測定ロジックとバッチ処理、保存先の DB テーブル作成までセットで実装しないと実現できないですよね。
これはサクッと実装するには counter
が自動で日次で保存先を切り替えてくれれば良さそう、ということで、 redis-objects-daily-counter
という gem は daily_counter
という日次で保存先を切り替えるカウンター機能を提供します。
Redis::Objects を拡張して作ってあるので、使い方はほぼ同じで、対象としたい Class に include Redis::Objects
を追加すると使えるようになります。
# Gemfile gem 'redis-objects-daily-counter'
class Homepage include Redis::Objects daily_counter :pv, expireat: -> { Time.now + 2_678_400 } # about a month def id 1 end end # 2021-04-01 homepage = Homepage.new homepage.id # 1 homepage.pv.increment homepage.pv.increment homepage.pv.increment puts homepage.pv.value # 3 # 2021-04-02 (next day) puts homepage.pv.value # 0 homepage.pv.increment homepage.pv.increment puts homepage.pv.value # 2 start_date = Date.new(2021, 4, 1) end_date = Date.new(2021, 4, 2) homepage.pv.range(start_date, end_date) # [3, 2]
#increment
#decrement
は counter
と同じ使い勝手です。ただ、日付が変わると自動的に保存先が切り替わります。
これは Redis で保存している Key 名が以下のフォーマットになっているためです。
model_name:id:field_name:yyyy-mm-dd
日付が変わった後も過去のレコードは削除されていないので、以下のコードでアクセス出来ます。この使い方は同じく Redis::Objects の list
と似たような使い勝手になっています。
# 2021-04-01 homepage.pv.increment(3) # 2021-04-02 (next day) homepage.pv.increment(2) # 2021-04-03 (next day) homepage.pv.increment(5) homepage.pv[Date.new(2021, 4, 1)] # => 3 homepage.pv[Date.new(2021, 4, 1), 3] # => [3, 2, 5] homepage.pv[Date.new(2021, 4, 1)..Date.new(2021, 4, 2)] # => [3, 2] homepage.pv.delete(Date.new(2021, 4, 1)) homepage.pv.range(Date.new(2021, 4, 1), Date.new(2021, 4, 3)) # => [0, 2, 5] homepage.pv.at(Date.new(2021, 4, 2)) # => 2
これで測定対象となるサービスを開発した後、 daily_counter
で測定処理を実装しておくだけで OK。
この状態でリリースすればデータは溜まっていくので、週次や月次で集計するバッチ処理を後でゆっくり実装すれば良いです。
バッチ処理実装前にサーバー内で $ bin/rails console
を実行して daily_counter
の値を確認しながらバッチ処理の実装を考える、とかも出来ますね。
具体的なデータを見ながらの方が開発難易度は下がります。
ちなみにですが、 expireat
オプションを指定しておくことをオススメします。
gem の仕様上、どんどん Redis 上のメモリを圧迫していくので、一定期間後に自動的に削除するようにしましょう。
daily_counter
があるなら週次、月次、年次カウンターがあっても良いよね、なんだったら毎時、毎分カウンターもあったって良いじゃないか、という事で v0.2.0
ではそれらが追加されています。
毎時・毎分のカウンターは API rate limit 機能の実装にも良いかもしれないですね。
annual_counter
- Key format:
model_name:id:field_name:yyyy
- Redis is a highly volatile key-value store, so I don't recommend using it.
- Key format:
monthly_counter
- Key format:
model_name:id:field_name:yyyy-mm
- Key format:
weekly_counter
- Key format:
model_name:id:field_name:yyyyWw
- Key format:
daily_counter
- Key format:
model_name:id:field_name:yyyy-mm-dd
- Key format:
hourly_counter
- Key format:
model_name:id:field_name:yyyy-mm-ddThh
- Key format:
minutely_counter
- Key format:
model_name:id:field_name:yyyy-mm-ddThh:mi
- Key format:
一応実装はしましたが、 annual_counter
のように期間の長いカウンターの利用はあまりオススメしません。
Redis ってふとした拍子にデータが飛んでしまう可能性があるので、あくまでキャッシュ的な位置付けで利用するのが良いと思います。
daily_counter
も週次・月次で RDS に集計結果を保存する運用を想定していますので。
というわけで久々のブログ更新では新しく作った redis-objects-daily-counter
という gem の紹介をさせて頂きました。
これから弊社プロダクトでも利用していく予定ですし、色々なサービスでも活用頂けると嬉しいです。