diary

日記です

redis 2.2を複数slave構成かつevictedありで運用してるとマジヤバイ話

結論

Redis 2.2使ってる奴はさっさとアップデートしろ

タイトルの条件で使用メモリがmaxmemoryに達するとキーがまるごと全部消し飛ぶ(evictedする)可能性があります。2.4ではとっく対策されているので、さっさと2.4以降、というかもうstableは2.6だし2.6にしろ

概要

わかりやすい全容(ぶっちゃけこっち見たほうが多分速い)。

だいたい下記のような感じのことが書いてあります

  • Redis 2.2でMaster-Slave構成(特に多数のslaveを接続している)を取っており
  • maxmemory-policyにnoeviction以外を指定している状態で
  • メモリ使用量がmaxmemoryに到達してキーのevictが発生し、
  • 格納されているキー1つあたりのの平均サイズが小さい (2013/04/23 21:47 紛らわしいので追記)

上記の条件で、maxmemory時のキー削除ロジックが暴走して、(ほぼ)すべてのキーがmasterから削除される

説明

まず前提として、

  • redisはキーを消すときにslaveに対して出力バッファ、AOF用のバッファにDELコマンドを書き足す
  • maxmemory到達時の押し出しに関しても、同様にふるまう

という事実があり、Redis 2.2ではmaxmemory到達時のキー削除に関しては色々省略するとまあだいたいこんなかんじで実装されていて、何らかのコマンドが走る度にチェックされる。

while (server.maxmemory && zmalloc_used_memory() > server.maxmemory) {

    /* 
     * maxmemoryポリシーに従って消すべきbestkeyを探すコードが
     * モリモリ書いてあったりするけど今回は全く関係ないオブジイヤーなので中略
     */

    /* Finally remove the selected key. */
    if (bestkey) {
        robj *keyobj = createStringObject(bestkey,sdslen(bestkey));
        propagteExpire(db,keyobj);
        dbDelete(db,keyobj);
        server.stat_evictedkeys++;
        decrRefCount(keyobj);
        freed++;
    }

    /* だるいので省略 */
}

要するに消したいものを探して、メモリ消費量がmaxmemoryを下回るまで消しまくる。

中で何をしているかというと、実際にdbからキーを消してるのはdbDeleteなんですが、消す前にpropagteExpireの呼び出しによって各DELコマンドの発行とそれの接続中の各スレーブへのレプリケーション、そしてAOFへの追記のための出力バッファへの書き出しが行われています。

この時、あくまで出力バッファにそれぞれ書き出されるだけで、その場では実際にAOFへの追記やslaveへのレプリケーションは行われない。また、接続中の各slave毎に出力バッファは確保されます。

このため、

  • propagteExpireはメモリ解放ロジック内において(少なくとも、イベントループ側に処理を返すまで)は、出力バッファ分のメモリを専有する
  • 専有する出力バッファは、接続中のslaveの数に比例して増大する

加えて、出力バッファもdbと同じくzmalloc(redisの厨二ネーミングなmalloc。名前に反して確保した容量を管理する程度のことしか自らはしていない、薄いwrapper)で確保している為、

  • 出力バッファの分のメモリ消費も上記コードのzmalloc_used_memoryの返り値に加算される

すると、

  • dbDeleteで開放される容量 < replication,aof用の出力バッファが専有する量

となった時、zmalloc_used_memoryの返り値は解放前後で減るどころか増えるようになり、この解放ロジックはキーを全て捨て去るまで止まらない勢いで走り続けるようになります。

でかい容量を食っているキーが偶然削除されれば、運が良ければそこで止まるかもしれないですが、まあ大体ダメであろうと思われます。大人しく2.6を使え

対策は実行時に確保されてる出力バッファぶんをmaxmemoryから差っ引いた上で、dbDeleteで実際に確保されるメモリ量が目標解放量に達するまでキーを削除し続ける、というロジックに変更することでなされたみたいです。

最後に

なんか成り行きで調べてたらちょっと面白くなってきたのでまとめてみましたが、もう解決してるので2.2使わなきゃ微塵も問題ないです。

とりあえず2.6使っときましょう