maedamaのブログ

アプリケーションエンジニアです。最近は主に設計を担当しています。

今更だが、RedisのSlaveのExpireは信用してはいけない

また、障害起因でのブログです。忘れないようにこちらにメモります。

http://trapezoid.hatenablog.com/entry/2013/02/10/035020

にも詳しくかいてあります。

とあるreplication をくんでいるRedisの系統があった。Replication をくんではいるものの、とある事情により Slaveには参照していなかったが、突如 Masterのロードがあがったので Slave に負荷分散する必要が生まれ、あれ?大丈夫だっけ?とある事情ってなんだっけ?ってなったのでそのとある事情についてまとめます。

Slave の expireは信用できない。

結論だけ述べると、 MASTER に 以下のようなコマンドうち

SETEX foo 3 1 #key Fooに3秒のexpireで1というvalueをset

10秒後にSlaveにGetコマンドをうつと、本来は Expireしてるので、空がかえってくる事をきたいしそうですが、本来 Expireしてるデータがかえってしまう可能性が多分にあります。

GET foo  #nilを期待するけど、1がかえってくる可能性有り

どうしてこのような事がおきるのでしょうか以下想像と事実の混合ですが。。

前提として Redis の replication は http://redis.io/topics/replication の下記の記述にあるとおり、コマンドベースの Replication をとってる模様です

The master then starts background saving, and starts to buffer all new commands received that will modify the dataset. When the background saving is complete, the master transfers the database file to the slave, which saves it on disk, and then loads it into memory. The master will then send to the slave all buffered commands. This is done as a stream of commands and is in the same format of the Redis protocol itself.

コマンドには、いわゆる冪等性みたいな概念は全くないので このような戦略をとる場合は Master で そのコマンドが実行された時の状態と Slave で コマンドが実行されたときの状態を完全に一致させないとデータの不整合がうまれてしまうという問題があります。

先ほどの例で考えましょう

まず、MASTERに以下のようなコマンドがながれたとします

SETEX foo 3 1 # Master から replicationされたコマンド
# 2秒経過
EXPIRE  foo 3 # fooのExpireが延長された
# 2秒経過
INCR foo 1  # Valueが 2 になる

さて、ここで最初のSETEXのコマンドの後にMasterとSlaveの間のReplicationが2秒間遅延した場合にSlaveからはどのようにみえるのだろうか?

SETEX foo 3 1 # Master から replicationされたコマンド
# Master側では2秒経過 してるが、Replicationが2秒遅延してるので 4秒経過してる
EXPIRE  foo 3
# 2秒経過
INCR foo 1 

本来、MASTERではSETEXの2秒後にEXPIREがのばされたようにみえるがSLAVEではSETEXの4秒後にEXPIREがのばされたように見えます。

このような状態でSLAVE側で、よしなにExpireしようとするとどのような事がおきるでしょうか?

SETEX foo 3 1 # Master から replicationされたコマンド
# Slave側で3.5秒経過
GET foo  # fooはSETEXから3秒経ってるな。よーしExpireだ
# Master側では2秒経過 したあとにうたれたコマンドが Replicationが2秒遅延してるので 4秒経過してる
EXPIRE  foo 3 # もう fooはexpireしてるよーん
# 2秒経過 
INCR foo 1  # fooはnilだぜ

みたいな感じで不整合が生まれてしまいます。実際には、Masterの時刻を含めてreplicateしてもらい、Slave側で Expires at判定する場合は、自分のサーバーの時刻からみた経過時間ではなく Masterからreplicateされてきてる時間で経過時間をみるみたいな事をすればもう少しうまくいくようになるかもしれませんが、Redisはコマンドいっぱいあるので、様々なコマンドの事をかんがえるとそんなにシンプルではないかもしれません。 ちなみに、Redisはこれを非常にシンプルな方法で解決してます。

Slave側ではデータはPurgeしない。Masterで データの PurgeのReplicationをまつ

order to obtain a correct behavior without sacrificing consistency, when a key expires, a DEL operation is synthesized in both the AOF file and gains all the attached slaves. This way the expiration process is centralized in the master instance, and there is no chance of consistency errors. However while the slaves connected to a master will not expire keys independently (but will wait for the DEL coming from the master), they'll still take the full state of the expires existing in the dataset, so when a slave is elected to a master it will be able to expire the keys independently, fully acting as a master.

これだけです。じゃあ、MasterっていつデータをExpireするの?となると以下のようになります

http://redis.io/commands/expire

How Redis expires keys Redis keys are expired in two ways: a passive way, and an active way. A key is actively expired simply when some client tries to access it, and the key is found to be timed out. Of course this is not enough as there are expired keys that will never be accessed again. This keys should be expired anyway, so periodically Redis test a few keys at random among keys with an expire set. All the keys that are already expired are deleted from the keyspace. Specifically this is what Redis does 10 times per second: Test 100 random keys from the set of keys with an associated expire. Delete all the keys found expired. If more than 25 keys were expired, start again from step 1. This is a trivial probabilistic algorithm, basically the assumption is that our sample is representative of the whole key space, and we continue to expire until the percentage of keys that are likely to be expired is under 25% This means that at any given moment the maximum amount of keys already expired that are using memory is at max equal to max amount of write operations per second divided by 4.

MasterにGetしたタイミングで明示的に確認する以外は、わりとadhocです。expireされたデータが残ってる可能性は十分にあります。以上のような理由により SlaveのGet時にはExpireは信用できないのです。

じゃあどうするかというと、expires_at みたいな値をvalueにつっこんでおいて アプリケーション側でexpire判定ロジックをすればokです。また、以上のような理由があったとしてもMasterのexpireは設定しておいた方がいいです。そうしないとRedisがデータをPurgeしてくれないので、無駄にデータが増える事になります。 以上のような理由で、RedisのexpireとSlaveを運用する場合は、データ量をいい感じにへらしてもらうためにExpireをつけるくらいのイメージのほうがよし。

間違ってたり、もっといいアイディアあれば教えてください