読者です 読者をやめる 読者になる 読者になる

set setting reset

インフラ関連の小ネタと備忘録

redis-py で expire が付与されていない key を削除する

メモリが逼迫してきた redis で、本来存在しないはずの expire が付与されていない key を削除することになったので記録として。
key は session id として使われています。

環境

  • 作業環境

  • サーバ環境

    • centos6.4
    • redis 2.6.14

前提

  • session id は英数小文字と数字が含まれていて、規則性がない
    こんなの => csodes5b5wenrvn0llbfcmme

使ったもの

インストールは pip で一発です。

スクリプトの概要

  • 小文字と数字のランダムな文字列を生成してワイルドカードで keys をたたく

    • keys * だと負荷がちょっとこわかったため、ちょっとずつやりたかったのです
      *ランダム文字列の生成は こちら のものを使わせていただきました。
  • DELETE_COUNT の数だけ削除するが、keys コマンドでひっぱってくる数にバラつきがあるので、正確ではない

    • 現在の keys - expires で TTL のない keys 数を取得
    • TTL のない keys 数から DELETE_COUNT を引き、削除後の TTL のない keys 数を取得
    • keys で取得した key を 1 つずつ ttl コマンドで expire が付与されていないことをチェックし、なければ削除する
    • これを 削除後の TTL のない keys 数 まで繰り返す
      • DELETE 数を正確にするには削除コマンド実行前に check_delete_limit をするとよいでしょう
      • ただし、その場合はコマンド発行回数が増加するので注意(infoだけなので速いのは速いですが)

スクリプト

#!/usr/bin/env/python
# -*- coding: utf-8 -*-

import os, sys, string, random, time
import redis
from logging import getLogger
import logging

logging.basicConfig(format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', level=logging.INFO)
logger = getLogger(os.path.basename(__file__))

# configration
HOST = sys.argv[1]
PORT = 6379
DB = 0
RANDOM_COUNT = 3
DELETE_COUNT = 200000

def connect_redis(h=HOST, p=PORT, d=DB):
    try:
        r = redis.StrictRedis(host=h, port=p, db=d)
    except redis.ConnectionError:
        logger.error("failed to connect %s" % HOST)
        logger.exception(e)
        sys.exit(1)
    except Exception, e:
        logger.exception(e)
        sys.exit(2)
    finally:
        return r

def get_redis_info(arg):
    r = connect_redis()
    info = r.info(arg)
    return info

def check_delete_limit(DB,DELETE_LIMIT):
    i = get_redis_info("keyspace")
    if i.has_key("db%s" % DB) == True:
        d = i.get("db%s" % DB)
        k = d.get("keys") - d.get("expires")
        if k <= DELETE_LIMIT:
            logger.info("limit arrived. stop process.")
            sys.exit(0)

def get_delete_limit(DB,DELETE_COUNT):
    i = get_redis_info("keyspace")
    if i.has_key("db%s" % DB) == True:
        d = i.get("db%s" % DB)
        k = d.get("keys") - d.get("expires")
        if k >= DELETE_COUNT:
            k -= DELETE_COUNT
            return k
        elif k == DELETE_COUNT:
            return k
        else:
            logger.info("nothing to delete any more.")
            sys.exit(0)

def gen_rand_str(length, chars=None):
    if chars is None:
        chars = string.digits + string.lowercase
    return ''.join([random.choice(chars) for i in range(length)])

def delete_session_keys(HOST,PORT,DB,RANDOM_COUNT,DELETE_LIMIT):
    sample_key = "*" + gen_rand_str(RANDOM_COUNT) + '*'
    # get sample keys
    r = connect_redis()
    keys = r.keys(sample_key)
    if keys is None:
        logger.info("not found expire keys from sample key.")

    for k in keys:
        # skip key "RedisSessionStateStore:LockedSessions"
        if "LockedSessions" in k:
            logger.debug("skip delete. key => %s" % k)
            continue

        # check ttl each key
        logger.debug("ttl start")
        ttl = r.ttl(k)
        logger.debug("ttl end")
        if ttl == -1:
            # delete key
            r.delete(k)
            logger.info("delete key => %s" % k)
            time.sleep(0.1)
        else:
            logger.debug("this key has ttl. key  => %s , ttl => %s" % (k, ttl))

if __name__ == '__main__':
    DELETE_LIMIT = get_delete_limit(DB,DELETE_COUNT)
    while True:
        check_delete_limit(DB, DELETE_LIMIT)
        delete_session_keys(HOST,PORT,DB,RANDOM_COUNT,DELETE_LIMIT)

実行

$  python delete_session_keys.py redisのIPアドレスなど
delete key => s5zhfytghgdq5bjmkaaqhkf2
delete key => sedhrhftw13jblpawgbvrh4k
delete key => s4rewrbhex04fdujbytp3rw5
delete key => skwcm0jigx2arwqwf52sj21n
this key has ttl. key  => uxxprmpwyjpxqeots1q334lo , ttl => 10503
this key has ttl. key  => djfw2py52croafz4gigun0l0 , ttl => 3606

以上です。