著者は、 Apache Software Foundation を選択して、 Write forDOnationsプログラムの一環として寄付を受け取りました。

序章

Redis( Re mote Di ctionary S erver)は、メモリ内のオープンソースソフトウェアです。 これは、サーバーのRAMを使用するデータ構造ストアであり、最速のソリッドステートドライブ(SSD)よりも数倍高速です。 これにより、Redisの応答性が高くなるため、レート制限に適しています。

レート制限は、ユーザーがサーバーにリソースを要求できる回数に上限を設けるテクノロジーです。 多くのサービスは、ユーザーがサーバーに過度の負荷をかけようとしたときにサービスが悪用されるのを防ぐために、レート制限を実装しています。

たとえば、 PHP を使用してWebアプリケーションにパブリックAPI(アプリケーションプログラミングインターフェイス)を実装する場合、何らかの形式のレート制限が必要です。 その理由は、APIを公開するときに、アプリケーションユーザーが特定の時間枠でアクションを繰り返すことができる回数を制御したいからです。 制御がないと、ユーザーはシステムを完全に停止させる可能性があります。

特定の制限を超えるユーザーの要求を拒否すると、アプリケーションをスムーズに実行できます。 顧客が多い場合、レート制限により、各顧客がアプリケーションに高速でアクセスできるようにする公正な使用ポリシーが適用されます。 レート制限は、帯域幅のコストを削減し、サーバーの輻輳を最小限に抑えるのにも役立ちます。

MySQLのようなデータベースにユーザーアクティビティを記録することにより、レート制限モジュールをコーディングすることは実用的かもしれません。 ただし、データをディスクからフェッチして設定された制限と比較する必要があるため、多くのユーザーがシステムにアクセスする場合、最終製品はスケーラブルではない可能性があります。 これは遅いだけでなく、リレーショナルデータベース管理システムはこの目的のために設計されていません。

Redisはインメモリデータベースとして機能するため、レートリミッターを作成するための適格な候補であり、この目的で信頼できることが証明されています

このチュートリアルでは、Ubuntu20.04サーバーでRedisを使用してレート制限するためのPHPスクリプトを実装します。

前提条件

始める前に、次のものが必要です。

ステップ1—PHP用のRedisライブラリをインストールする

まず、Ubuntuサーバーパッケージリポジトリインデックスを更新することから始めます。 次に、php-redis拡張機能をインストールします。 これは、PHPコードにRedisを実装できるようにするライブラリです。 これを行うには、次のコマンドを実行します。

  1. sudo apt update
  2. sudo apt install -y php-redis

次に、Apacheサーバーを再起動して、php-redisライブラリをロードします。

  1. sudo systemctl restart apache2

ソフトウェア情報インデックスを更新し、PHP用のRedisライブラリをインストールしたら、IPアドレスに基づいてユーザーのアクセスを制限するPHPリソースを作成します。

ステップ2—レート制限のためのPHPWebリソースの構築

この手順では、Webサーバーのルートディレクトリ(/var/www/html/)にtest.phpファイルを作成します。 このファイルは一般に公開され、ユーザーはWebブラウザにそのアドレスを入力して実行できます。 ただし、このガイドの基礎として、後でcurlコマンドを使用してリソースへのアクセスをテストします。

サンプルリソースファイルを使用すると、ユーザーは10秒の時間枠で3回アクセスできます。 制限を超えようとすると、レート制限されていることを通知するエラーが発生します。

このファイルのコア機能は、Redisサーバーに大きく依存しています。 ユーザーが初めてリソースをリクエストすると、ファイル内のPHPコードは、ユーザーのIPアドレスに基づいてRedisサーバー上にキーを作成します。

ユーザーがリソースに再度アクセスすると、PHPコードはユーザーのIPアドレスをRedisサーバーに保存されているキーと照合し、キーが存在する場合は値を1つインクリメントしようとします。 PHPコードは、増分された値が設定された最大制限に達しているかどうかをチェックし続けます。

ユーザーのIPアドレスに基づくRedisキーは、10秒後に期限切れになります。 この期間が経過すると、ユーザーのWebリソースへのアクセスのログ記録が再開されます。

まず、/var/www/html/test.phpファイルを開きます。

  1. sudo nano /var/www/html/test.php

次に、次の情報を入力してRedisクラスを初期化します。 REDIS_PASSWORDに適切な値を入力することを忘れないでください。

/var/www/html/test.php
<?php

$redis = new Redis();
$redis->connect('127.0.0.1', 6379);
$redis->auth('REDIS_PASSWORD');

$redis->authは、Redisサーバーへのプレーンテキスト認証を実装します。 これは、ローカルで(localhostを介して)作業している間は問題ありませんが、リモートRedisサーバーを使用している場合は、SSL認証の使用を検討してください。

次に、同じファイルで、次の変数を初期化します。

/var/www/html/test.php
. . .
$max_calls_limit  = 3;
$time_period      = 10;
$total_user_calls = 0;

あなたが定義した:

  • $max_calls_limit:ユーザーがリソースにアクセスできる呼び出しの最大数です。
  • $time_period$max_calls_limitに従って、ユーザーがリソースにアクセスできる時間枠を秒単位で定義します。
  • $total_user_calls:指定された時間枠内にユーザーがリソースへのアクセスを要求した回数を取得する変数を初期化します。

次に、次のコードを追加して、Webリソースを要求しているユーザーのIPアドレスを取得します。

/var/www/html/test.php
. . .
if (!empty($_SERVER['HTTP_CLIENT_IP'])) {
    $user_ip_address = $_SERVER['HTTP_CLIENT_IP'];
} elseif (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
    $user_ip_address = $_SERVER['HTTP_X_FORWARDED_FOR'];
} else {
    $user_ip_address = $_SERVER['REMOTE_ADDR'];
}

このコードはデモンストレーションの目的でユーザーのIPアドレスを使用しますが、認証が必要な保護されたリソースがサーバー上にある場合は、ユーザー名またはアクセストークンを使用してユーザーのアクティビティをログに記録できます。

このようなシナリオでは、システムに認証されたすべてのユーザーが一意の識別子(たとえば、顧客ID、開発者ID、ベンダーID、さらにはユーザーID)を持ちます。 (これを構成する場合は、$user_ip_addressの代わりにこれらの識別子を使用することを忘れないでください。)

このガイドでは、ユーザーIPアドレスで概念を証明できます。 したがって、前のコードスニペットでユーザーのIPアドレスを取得したら、次のコードブロックをファイルに追加します。

/var/www/html/test.php
. . .
if (!$redis->exists($user_ip_address)) {
    $redis->set($user_ip_address, 1);
    $redis->expire($user_ip_address, $time_period);
    $total_user_calls = 1;
} else {
    $redis->INCR($user_ip_address);
    $total_user_calls = $redis->get($user_ip_address);
    if ($total_user_calls > $max_calls_limit) {
        echo "User " . $user_ip_address . " limit exceeded.";
        exit();
    }
}

echo "Welcome " . $user_ip_address . " total calls made " . $total_user_calls . " in " . $time_period . " seconds";

このコードでは、if...elseステートメントを使用して、RedisサーバーにIPアドレスで定義されたキーがあるかどうかを確認します。 キーif (!$redis->exists($user_ip_address)) {...}が存在しない場合は、キーを設定し、コード$redis->set($user_ip_address, 1);を使用してその値を1に定義します。

$redis->expire($user_ip_address, $time_period);は、期間内(この場合は10秒)に有効期限が切れるキーを設定します。

ユーザーのIPアドレスがRedisキーとして存在しない場合は、変数$total_user_calls1に設定します。

...else {...}...ステートメントブロックで、$redis->INCR($user_ip_address);コマンドを使用して、各IPアドレスキーに設定されたRedisキーの値を1ずつインクリメントします。 これは、キーがRedisサーバーにすでに設定されており、リピートリクエストとしてカウントされる場合にのみ発生します。

ステートメント$total_user_calls = $redis->get($user_ip_address);は、RedisサーバーでIPアドレスベースのキーをチェックすることにより、ユーザーが行うリクエストの総数を取得します。

ファイルの終わりに向かって、...if ($total_user_calls > $max_calls_limit) {... }..ステートメントを使用して、制限を超えているかどうかを確認します。 その場合は、echo "User " . $user_ip_address . " limit exceeded.";でユーザーに警告します。 最後に、echo "Welcome " . $user_ip_address . " total calls made " . $total_user_calls . " in " . $time_period . " seconds";ステートメントを使用して、その期間に行われた訪問についてユーザーに通知します。

すべてのコードを追加すると、/var/www/html/test.phpファイルは次のようになります。

/var/www/html/test.php
<?php
$redis = new Redis();
$redis->connect('127.0.0.1', 6379);
$redis->auth('REDIS_PASSWORD');

$max_calls_limit  = 3;
$time_period      = 10;
$total_user_calls = 0;

if (!empty($_SERVER['HTTP_CLIENT_IP'])) {
    $user_ip_address = $_SERVER['HTTP_CLIENT_IP'];
} elseif (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
    $user_ip_address = $_SERVER['HTTP_X_FORWARDED_FOR'];
} else {
    $user_ip_address = $_SERVER['REMOTE_ADDR'];
}

if (!$redis->exists($user_ip_address)) {
    $redis->set($user_ip_address, 1);
    $redis->expire($user_ip_address, $time_period);
    $total_user_calls = 1;
} else {
    $redis->INCR($user_ip_address);
    $total_user_calls = $redis->get($user_ip_address);
    if ($total_user_calls > $max_calls_limit) {
        echo "User " . $user_ip_address . " limit exceeded.";
        exit();
    }
}

echo "Welcome " . $user_ip_address . " total calls made " . $total_user_calls . " in " . $time_period . " seconds";

/var/www/html/test.phpファイルの編集が終了したら、保存して閉じます。

これで、test.phpWebリソースのユーザーをレート制限するために必要なロジックがコーディングされました。 次のステップでは、スクリプトをテストします。

ステップ3—Redisレート制限のテスト

このステップでは、curlコマンドを使用して、ステップ2でコーディングしたWebリソースを要求します。 スクリプトを完全にチェックするには、1つのコマンドで5回リソースをリクエストします。 これを行うには、test.phpファイルの最後にプレースホルダーURLパラメーターを含めます。 ここでは、リクエストの最後に値?[1-5]を使用して、curlコマンドを5回実行します。

次のコマンドを実行します。

  1. curl -H "Accept: text/plain" -H "Content-Type: text/plain" -X GET http://localhost/test.php?[1-5]

コードを実行すると、次のような出力が表示されます。

Output
[1/5]: http://localhost/test.php?1 --> <stdout> --_curl_--http://localhost/test.php?1 Welcome 127.0.0.1 total calls made 1 in 10 seconds [2/5]: http://localhost/test.php?2 --> <stdout> --_curl_--http://localhost/test.php?2 Welcome 127.0.0.1 total calls made 2 in 10 seconds [3/5]: http://localhost/test.php?3 --> <stdout> --_curl_--http://localhost/test.php?3 Welcome 127.0.0.1 total calls made 3 in 10 seconds [4/5]: http://localhost/test.php?4 --> <stdout> --_curl_--http://localhost/test.php?4 User 127.0.0.1 limit exceeded. [5/5]: http://localhost/test.php?5 --> <stdout> --_curl_--http://localhost/test.php?5 User 127.0.0.1 limit exceeded.

お気づきのとおり、最初の3つのリクエストは問題なく実行されました。 ただし、スクリプトでは4番目と5番目のリクエストのレートが制限されています。 これは、Redisサーバーがユーザーのリクエストをレート制限していることを確認します。

このガイドでは、次の2つの変数に低い値を設定しました。

/var/www/html/test.php
...
$max_calls_limit  = 3;
$time_period      = 10;
...

実稼働環境でアプリケーションを設計する場合、ユーザーがアプリケーションにアクセスする頻度に応じて、より高い値を検討できます。

これらの値を設定する前に、リアルタイムの統計を確認することをお勧めします。 たとえば、サーバーログに、平均的なユーザーが60秒ごとに1,000回アプリケーションにアクセスしたことが示されている場合は、それらの値をユーザーを制限するためのベンチマークとして使用できます。

より良い視点で物事を置くために、ここにレート制限の実装のいくつかの実際の例があります(2021年現在):

結論

このチュートリアルでは、Ubuntu 20.04サーバーでRedisを使用してレート制限するためのPHPスクリプトを実装し、Webアプリケーションが不注意または悪意のある乱用から保護されるようにしました。 ユースケースに応じて、ニーズに合わせてコードを拡張できます。

本番環境で使用するためにApacheサーバーを保護することをお勧めします。 Ubuntu20.04でLet’sEncryptを使用してApacheを保護する方法チュートリアルに従ってください。

また、Redisがデータベースキャッシュとしてどのように機能するかを読むことも検討してください。 Ubuntu20.04チュートリアルでPHPを使用してMySQLのキャッシュとしてRedisを設定する方法を試してください。

その他のリソースは、PHPおよびRedisトピックページにあります。