Redisを使用したリクエスト数の削減の概要[パート1]

最近、私はRedisを使用してクエリの数を制限するいくつかの異なる方法を書きました。 商業プロジェクトと個人プロジェクトの両方で。 この出版物の2つのパートでは、クエリの数を制限するための2つの異なるが関連する方法(標準のRedisコマンドとLuaスクリプトの使用)を取り上げます。 説明されている各メソッドは、新しいユースケースを追加し、前のメソッドの欠陥を解決します。



この投稿では、PythonとRedis、およびそれほどではないがLuaにある程度の経験があることを前提としていますが、この経験がない人にも興味があります。



リクエストの数を制限する理由



たとえば、 TwitterはAPIへのリクエストの数を制限していますが、 RedditStackOverflowは投稿とコメントの数に制限を使用しています。



リソースの利用を最適化するために誰かがリクエストの数を制限し、誰かがスパマーと格闘しています。 つまり、現代のインターネットでは、プラットフォームへのリクエストの数を制限することは、ユーザーが持つことができる影響を制限することを目的としています。 理由に関係なく、ユーザーのアクションを数え、ユーザーが特定の制限に達した、または超えた場合にそれらを防ぐ必要があるという事実から話を進めましょう。 最初に、特定のAPIへのリクエストの数を、ユーザーあたり1時間あたり最大240リクエストに制限します。



アクションをカウントし、ユーザーを制限する必要があることを知っているため、小さなヘルパーコードが必要です。 まず、アクションを実行するユーザーの1つ以上の識別子を提供する関数が必要です。 ユーザーのIPだけである場合もあれば、識別子である場合もあります。 可能であれば両方を使用することを好みます。 ユーザーが許可されていない場合は、少なくともIP。 以下は、 Flask Flask-Loginプラグインを使用してIPとユーザーIDを取得する関数です。



from flask import g, request def get_identifiers(): ret = ['ip:' + request.remote_addr] if g.user.is_authenticated(): ret.append('user:%s'%g.user.get_id()) return ret
      
      







カウンターを使用するだけ



これで、ユーザー識別子を返す関数が作成され、アクションのカウントを開始できます。 Redisで利用できる最も簡単な方法の1つは、時間範囲のキーを計算し、対象のアクションが発生するたびにその中のカウンターをインクリメントすることです。 カウンターの数値が必要な値を超える場合、アクションの実行は許可されません。 以下は、1時間の範囲(および有効期間)で自動的に減衰するキーを使用する関数です。



 import time def over_limit(conn, duration=3600, limit=240): bucket = ':%i:%i'%(duration, time.time() // duration) for id in get_identifiers(): key = id + bucket count = conn.incr(key) conn.expire(key, duration) if count > limit: return True return False
      
      





これはかなり単純な関数です。 各識別子について、Redisの対応するキーを増やし、その有効期間を1時間に設定します。 カウンター値が制限を超えた場合、Trueを返します。 それ以外の場合は、Falseを返します。



以上です。 まあ、またはほぼ。 これにより、問題を解決できます-各ユーザーの1時間あたりのリクエスト数を240に制限します。 ただし、実際には、ユーザーは1時間ごとに制限がリセットされることにすぐに気付くでしょう。 そして、時間の始まりの数秒で240のリクエストを送信することを止めるものは何もありません。 その後、私たちの仕事はスマルカに行きます。



異なる範囲を使用します



リクエストを1時間ごとに制限するという主な目標は成功しましたが、ユーザーはすべてのAPIリクエストをできるだけ早く(1時間ごとに)送信し始めます。 1時間ごとの制限に加えて、1秒ごとおよび1分ごとの制限を導入して、リクエストのピーク数の状況をスムーズにする必要があるようです。



ユーザーには1秒あたり10リクエスト、1分あたり120リクエスト、1時間あたり240リクエストで十分であり、時間の経過とともにリクエストをより適切に分散できると判断したとします。



これを行うには、単にover_limit()関数を使用します。



 def over_limit_multi(conn, limits=[(1, 10), (60, 120), (3600, 240)]): for duration, limit in limits: if over_limit(conn, duration, limit): return True return False
      
      





これは期待どおりに機能します。 ただし、over_limit()の3つの呼び出しのそれぞれは、2つのRedisコマンドを実行できます。1つはカウンターを更新し、2つ目はキーのライフタイムを設定します。 IPおよびユーザーIDに対して実行します。 その結果、1人のユーザーが1つの操作の制限を超えたことを単に伝えるために、Redisへの最大12の要求が必要になる場合があります。 Redisリクエストの数を最小限に抑える最も簡単な方法は、 パイプラインを使用することです。 このようなクエリは、Redisではトランザクションと呼ばれます。 Redisのコンテキストでは、これは1つのリクエストで多くのコマンドを送信することを意味します。



over_limit()関数が作成されているため、 MULTIを使用してINCRおよびEXPIRE呼び出しを単一の要求に簡単に置き換えることができます。 この変更により、 over_limit_multi()とともにRedisを使用するときに、Redisへのリクエストの数を12から6に減らすことができます。



 def over_limit(conn, duration=3600, limit=240): pipe = conn.pipeline(transaction=True) bucket = ':%i:%i'%(duration, time.time() // duration) for id in get_identifiers(): key = id + bucket pipe.incr(key) pipe.expire(key, duration) if pipe.execute()[0] > limit: return True return False
      
      





Redisの呼び出し回数を半分に減らすことは素晴らしいことですが、ユーザーがAPI呼び出しを行うことができるかどうかを確認するために、まだ6つのリクエストを行います。 over_limit_multi()の別のバージョンを書くことができます。これはすべての操作を一度に実行し、その後制限をチェックしますが、実装にいくつかのエラーがあることは明らかです。 ユーザーを制限し、1時間あたり240を超えないリクエストを許可することができますが、最悪の場合、1時間あたり10リクエストのみです。 はい、エラーはRedisに別のリクエストを行うことで修正できます。または、単純にすべてのロジックをRedisに転送できます!



正しく考える



以前の実装を修正する代わりに、Redis内で実行するLUAスクリプトに転送しましょう。 このスクリプトでは、上記と同じことを行います。制限のリストを確認し、各識別子についてカウンターを増やし、ライフタイムを更新し、カウンターが制限を超えたかどうかを確認します。



 import json def over_limit_multi_lua(conn, limits=[(1, 10), (60, 125), (3600, 250)]): if not hasattr(conn, 'over_limit_multi_lua'): conn.over_limit_multi_lua = conn.register_script(over_limit_multi_lua_) return conn.over_limit_multi_lua( keys=get_identifiers(), args=[json.dumps(limits), time.time()]) over_limit_multi_lua_ = ''' local limits = cjson.decode(ARGV[1]) local now = tonumber(ARGV[2]) for i, limit in ipairs(limits) do local duration = limit[1] local bucket = ':' .. duration .. ':' .. math.floor(now / duration) for j, id in ipairs(KEYS) do local key = id .. bucket local count = redis.call('INCR', key) redis.call('EXPIRE', key, duration) if tonumber(count) > limit[2] then return 1 end end end return 0 '''
      
      





'local bucket'の直後のコードを見てください。 Luaスクリプトは以前のソリューションのように見え、元のover_limit()と同じ操作を実行します。



おわりに



1つの時間間隔で開始し、最終的に、いくつかのレベルの制限で機能し、単一のユーザーの異なる識別子で機能し、Redisに対して1つのリクエストのみを実行できるリクエストの数を制限する方法があります。



実際、当社のリミッターのオプションは、さまざまなアプリケーションで役立ちます。



サンドボックスの記事に対して、それが翻訳であることを正しく示す方法が見つかりませんでした。




All Articles