0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

GETSETがRedisキーのTTLを黙って消していた話

0
Posted at

はじめに

私は、通話にボットを送り込まず画面上に常駐するミーティングアシスタント、LiveSuggest を開発しています。この記事で扱うのは製品の紹介ではなく、課金まわりのRedis実装で踏んだバグです。

先に結論を書きます。Redisのバグではありません。原因は、私たちがコマンドの副作用を見落としていたことでした。

前提: セッションキーと課金カウンタ

セッションごとにRedisのキーを持ち、TTLは15分にしています。セッションが動いている間はTTLが更新され続け、止まれば15分後に自然に消える、という設計です。

課金対象の秒数は、このキーのカウンタに積み上げています。加算はLuaスクリプトで INCRBY と EXPIRE をまとめて実行するので、加算とTTLの更新は必ずセットでアトミックに行われます。

読み取りとリセットにGETSETを使っていた

積み上がった秒数は、別の処理が定期的に回収します。ここで使っていたのが GETSET です。現在値を読み、同時に0へ戻す処理を1コマンドで書けるので、アトミックで手軽に見えました。

見落としていたのは、GETSET が値を上書きするときにTTLも消してしまう点です。回収した直後のキーは、TTLのない永続キーになります。

なぜ普段は表に出なかったのか

セッションが生きていれば、次の加算でLuaスクリプトが EXPIRE を実行し、TTLは元に戻ります。消えたTTLはすぐ付け直されるので、通常の流れでは問題が見えません。

問題になるのは、回収の直後にセッションが止まった場合です。そのあと EXPIRE が一度も呼ばれないゾンビセッションになると、課金秒数のキー(billableSecondsPending)はTTLを失ったまま残り続けます。15分で消えるはずのキーが、いつまでも消えません。

原因の特定

issue #601 で、このリークの原因が GETSET であることを突き止めました。加算側のスクリプトは正しく、TTLを外していたのは回収側でした。

修正: GETとSET KEEPTTLをLuaで

回収処理もLuaスクリプトに置き換えました。中身は次の2ステップだけです。

  1. GET で現在値を読む
  2. SET に KEEPTTL を付けて値を戻す

KEEPTTL を付けると、値を書き換えても既存のTTLは残ります。Luaスクリプト内で実行するので、読み取りと書き戻しの間に加算が割り込むこともありません。

もう一つ決めたのが、キーが存在しなければ何もしない、というルールです。セッションのキーがすでに期限切れで消えているのに回収側が SET すると、TTLのないキーを新しく作ってしまいます。これでは同じリークがまた起きます。GET の結果が空なら書き込まずに終了します。

修正後の整理

  • 加算: Luaで INCRBY と EXPIRE をアトミックに実行し、TTLを更新する
  • 回収: Luaで GET と SET KEEPTTL を実行し、TTLには触れない
  • キーがなければ作らない

TTLを更新するのは加算側だけになりました。回収側はTTLに一切影響しません。

学んだこと

TTLで寿命を管理しているキーに書き込むときは、そのコマンドがTTLを残すのか消すのかを確認する必要があります。GETSET のように、読み取りとリセットを1コマンドで済ませられる便利なコマンドほど、副作用を見落としやすいです。

今回のリークは正常なセッションでは起きず、止まったセッションでだけ起きていました。TTLに頼った設計では「次の更新が来ない場合」を想定し、そのときキーにTTLが残っているかを確認するようにしています。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?