Ubuntu 22.04でPython/MySQLアプリケーションのセッション処理をRedisで高速化する方法

はじめに

認証は、ログイン要求中にユーザーの身元を確認するプロセスです。認証プロセスでは、ユーザーはユーザー名とパスワードの形式で資格情報を提出します。その後、アプリケーションはそれらのログイン資格情報を保存されたデータベースエントリと照合します。一致がある場合、アプリケーションはユーザーにシステムへのアクセスを許可します。

キャッシュメカニズムのないMySQLやPostgreSQLのようなリレーショナルデータベースにログイン資格情報を保存することは、まだ一般的で実用的な手法ですが、以下の制限があります:

  • データベースの過負荷。ユーザーがログイン要求を提出するたびに、アプリケーションはデータベースサーバーへの往復を行い、データベーステーブルからユーザーの資格情報を確認する必要があります。データベースは他の読み書き要求も処理する可能性があるため、このプロセス全体でデータベースに過負荷がかかり、遅くなります。

  • 従来のディスクベースのデータベースはスケーラビリティの問題を抱えています。アプリケーションが1秒あたり数千のリクエストを受け取る場合、ディスクベースのデータベースでは最適なパフォーマンスが得られません。

上記の課題を克服するために、Redisを使用してユーザーのログイン資格情報をキャッシュすることができます。これにより、ログインリクエストごとにバックエンドデータベースに接続する必要がありません。Redisは、コンピュータのRAMを使用してデータをキーと値のペアで格納する、最も人気のある超高速データストアの1つです。このガイドでは、Ubuntu 22.04サーバー上のPython/MySQLアプリケーションでセッション処理の高速化にRedisデータベースを使用します。

前提条件

このチュートリアルを開始する前に、以下の設定が必要です:

ステップ1 — RedisとMySQLのPythonデータベースドライバのインストール

このアプリケーションは、ユーザーの名前やパスワードなどの認証情報をMySQLデータベースサーバーに永続的に保存します。ユーザーがアプリケーションにログインすると、PythonスクリプトがMySQLデータベースをクエリし、詳細を保存された値と照合します。その後、Pythonスクリプトはユーザーのログイン情報をRedisデータベースにキャッシュし、他の将来のリクエストに対応します。このロジックを完了するために、PythonスクリプトにはMySQLとRedisサーバーとの通信に必要なデータベースドライバ(Pythonモジュール)が必要です。以下の手順に従ってドライバをインストールしてください:

  1. パッケージ情報インデックスを更新し、以下のコマンドを実行してPythonパッケージマネージャーであるpython3-pipをインストールします。これにより、Python標準ライブラリに含まれていない追加のモジュールをインストールできるようになります。
sudo apt install python3-pip
  1. PythonのMySQLドライバをインストールします:
pip install mysql-connector-python
  1. PythonのRedisドライバをインストールします:
pip install redis

MySQLとRedisとの通信に必要なドライバをインストールした後、次のステップに進み、MySQLデータベースを初期化します。

ステップ2 — サンプルMySQLデータベースの設定

このガイドでは、1つのMySQLテーブルが必要です。本番環境では、他のリクエストに対応するために数十のテーブルを持つことができます。次のコマンドを実行してデータベースを設定し、テーブルを作成してください:

  1. MySQLデータベースサーバーにrootユーザーとしてログインします:

    sudo mysql -u root -p
    
  2. パスワードが求められたら、MySQLサーバーのrootパスワードを入力し、ENTERキーを押して進みます。その後、次のコマンドを実行して、サンプルのcompanyデータベースとcompany_userアカウントを作成します。強力なパスワードであるexample-mysql-passwordを入力してください:

  1. CREATE DATABASE company;
  2. CREATE USER 'company_user'@'localhost' IDENTIFIED WITH mysql_native_password BY 'example-mysql-password';
  3. GRANT ALL PRIVILEGES ON company.* TO 'company_user'@'localhost';
  4. FLUSH PRIVILEGES;
  1. 以下の出力が表示されることを確認して、前のコマンドが正常に実行されたことを確認してください:

    出力
    Query OK, 1 row affected (0.01 sec)
  2. 新しいcompanyデータベースに切り替えます:

    1. USE company;
  3. 次の出力を確認して、新しいデータベースに接続されていることを確認します:

    出力
    Database changed
  4. system_usersテーブルを作成します。 user_id列は、各ユーザーを一意に識別するためのPRIMARY KEYとして機能します。 usernameおよびpassword列は、ユーザーがアプリケーションにログインするために提出するログイン資格情報です。 first_nameおよびlast_name列には、ユーザーの名前が格納されます:

    custom_prefix(mysql>)
    CREATE TABLE system_users (
        user_id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY,
        username VARCHAR(50),
        first_name VARCHAR(50),
        last_name VARCHAR(50),
        password VARCHAR(50)
    ) ENGINE = InnoDB;
    
  5. 次の出力を確認して、新しいテーブルが作成されていることを確認してください:

    出力
    Query OK、0行が影響を受けました(0.03秒)
  6. サンプルデータでsystem_usersテーブルを埋める。セキュリティのためにMySQLの組み込み関数MD5(...)を使用してパスワードをハッシュ化する。

    1. INSERT INTO system_users (username, first_name, last_name, password) VALUES ('john_doe', 'JOHN', 'DOE', MD5('password_1'));
    2. INSERT INTO system_users (username, first_name, last_name, password) VALUES ('mary_henry', 'MARY', 'HENRY', MD5('password_2'));
    3. INSERT INTO system_users (username, first_name, last_name, password) VALUES ('peter_jade', 'PETER', 'JADE', MD5('password_3'));
  7. 以下は出力の確認です。

    出力
    クエリOK、1行が変更されました (0.00秒)
  8. system_usersテーブルをクエリしてデータが入っていることを確認します。

    1. SELECT
    2. user_id,
    3. first_name,
    4. last_name,
    5. password
    6. FROM system_users;
  9. 以下の出力を確認してください:

    出力
    +---------+------------+-----------+----------------------------------+ | user_id | first_name | last_name | password | +---------+------------+-----------+----------------------------------+ | 1 | JOHN | DOE | 57210b12af5e06ad2e6e54a93b1465aa | | 2 | MARY | HENRY | 259640f97ac2b4379dd540ff4016654c | | 3 | PETER | JADE | 48ef85c894a06a4562268de8e4d934e1 | +---------+------------+-----------+----------------------------------+ 3in set (0.00 sec)
  10. MySQLデータベースからログアウトする:

    1. QUIT;

これでアプリケーションに適切なMySQLデータベースが設定されました。次のステップでは、サンプルデータベースと通信するPythonモジュールを作成します。

ステップ3 — Python用の中央MySQLゲートウェイモジュールを作成する

Pythonプロジェクトをコーディングする際には、各タスクごとに別のモジュールを作成してコードの再利用性を高めるべきです。このステップでは、PythonスクリプトからMySQLデータベースに接続してクエリを実行するための中央モジュールを設定します。以下の手順に従ってください:

  1. projectディレクトリを作成します。このディレクトリはPythonソースコードファイルを他のシステムファイルから分離します:

    1. mkdir project
  2. 新しいprojectディレクトリに切り替えます。

    1. cd project
  3. nanoテキストエディタを使用して、新しいmysql_db.pyファイルを開きます。このファイルはMySQLデータベースと通信するPythonモジュールをホストします。

    nano mysql_db.py
    
  4. 以下の情報をmysql_db.pyファイルに入力してください。 company_userアカウントの正しいMySQLパスワードでexample-mysql-passwordを置き換えてください:

    ~/project/mysql_db.py
    
    import mysql.connector
    
    class MysqlDb:
    
    def db_con(self):
    
        mysql_con = mysql.connector.connect(
            host     = "localhost",
            user     = "company_user",
            password = "example-mysql-password",
            database = "company",
            port     = "3306"
        )
    
        return mysql_con
    
    def query(self, username, password):
    
        db = self.db_con()
        db_cursor = db.cursor()
    
        db_query  = "select username, password from system_users where username = %s and password = md5(%s)"
        db_cursor.execute(db_query, (username, password))
    
        result = db_cursor.fetchone()
        row_count = db_cursor.rowcount
    
        if  row_count < 1:
            return False
        else:
            return result[1]
    
  5. mysql_db.pyファイルを保存して閉じます。

mysql_db.pyモジュールファイルには、次の2つのメソッドを持つ1つのクラス(MysqlDb:)があります:
db_con(self)::以前に作成したサンプルのcompanyデータベースに接続し、return mysql_conステートメントを使用して再利用可能なMySQL接続を返します。
query(self, username, password)::ユーザー名とパスワードを受け取り、system_usersテーブルをクエリして一致するかどうかを調べます。条件付きのif row_count < 1: ... else: return result[1]ステートメントは、ユーザーがテーブルに存在しない場合はブール値Falseを返し、アプリケーションが一致を見つけた場合はユーザーのパスワード(result[1])を返します。

MySQLモジュールが準備できたら、次のステップに進んでRedisキーバリューストアに通信する類似のRedisモジュールをセットアップします。

ステップ4 – Pythonの中央Redisモジュールの作成

このステップでは、Redisサーバーに接続するモジュールを作成します。次の手順を実行してください。

  1. 新しいredis_db.pyファイルを開きます:

    nano redis_db.py
    
  2. redis_db.pyファイルに以下の情報を入力してください。Redisサーバーの正しいパスワードであるexample-redis-passwordを置き換えてください:

    ~/project/redis_db.py
    import redis
    class RedisDb:
        def db_con(self):
            r_host = 'localhost'
            r_port = 6379
            r_pass = 'example-redis-password'
            redis_con = redis.Redis(host = r_host, port = r_port, password = r_pass)
            return redis_con
    
  3. redis_db.pyファイルを保存して閉じます。

  • 上記のファイルには、1つのクラス(RedisDb:)があります。

  • このクラスの下には、db_con(self):メソッドがあり、提供された認証情報を使用してRedisサーバーに接続し、return redis_conステートメントを使用して再利用可能な接続を返します。

Redisクラスを設定した後、次のステップでプロジェクトのメインファイルを作成します。

ステップ5 — アプリケーションのエントリーポイントの作成

すべてのPythonアプリケーションには、エントリーポイントまたはアプリケーションの実行時に実行されるメインファイルが必要です。このファイルでは、認証されたユーザーの現在のサーバーの時刻を表示するコードを作成します。このファイルでは、作成したカスタムのMySQLおよびRedisモジュールを使用してユーザーを認証します。以下の手順に従って、ファイルを作成してください。

  1. 新しいindex.pyファイルを開きます:

    nano index.py
    
  2. index.pyファイルに以下の情報を入力してください:

    ~/project/index.py
    from encodings import utf_8
    import base64
    from hashlib import md5
    import json
    import datetime
    import http.server
    from http import HTTPStatus
    import socketserver
    import mysql_db
    import redis_db
    
    class HttpHandler(http.server.SimpleHTTPRequestHandler):
        def do_GET(self):
            self.send_response(HTTPStatus.OK)
            self.send_header('Content-type', 'application/json')
            self.end_headers()
            authHeader = self.headers.get('Authorization').split(' ');
            auth_user, auth_password = base64.b64decode(authHeader[1]).decode('utf8').split(':')
            mysql_server = mysql_db.MysqlDb()
            redis_server = redis_db.RedisDb()
            redis_client =  redis_server.db_con()
            now = datetime.datetime.now()
            current_time = now.strftime("%Y-%m-%d %H:%M:%S")
            resp = {}
            if redis_client.exists(auth_user):
                if md5(auth_password.encode('utf8')).hexdigest() != redis_client.get(auth_user).decode('utf8'):
                    resp = {"error": "無効なユーザー名/パスワードです。"}
                else:
                    resp = {"time": current_time, "authorized by": "Redisサーバー"}
            else:
                mysql_resp  = mysql_server.query(auth_user, auth_password)
                if mysql_resp == False:
                     resp =  {"error": "無効なユーザー名/パスワードです。"}
                else:
                    resp = {"time": current_time, "authorized by": "MySQLサーバー"}
                    redis_client.set(auth_user, mysql_resp)
            self.wfile.write(bytes(json.dumps(resp, indent = 2) + "\r\n", "utf8"))
    httpd = socketserver.TCPServer(('', 8080), HttpHandler)
    print("ウェブサーバーがポート8080で実行中です...")
    
    try:
        httpd.serve_forever()
    except KeyboardInterrupt:
        httpd.server_close()
        print("ウェブサーバーが停止しました。")
    
  3. index.pyファイルを保存して閉じます。

  • index.pyファイルでは、import...セクションに以下のモジュールがプロジェクトに追加されます。

    • utf_8base64md5、およびjson、テキストエンコーディングおよびフォーマットモジュール。

    • http.serverHTTPStatus、およびsocketserver、Webサーバーモジュール。

    • datetime、時刻/日付モジュール。

    • mysql_dbおよびredis_db、以前に作成したMySQLおよびRedisサーバーへのアクセスのためのカスタムモジュール。

  • HttpHandler(http.server.SimpleHTTPRequestHandler)は、HTTPサーバーのためのハンドラークラスです。クラス内では、do_GET(self)メソッドがHTTPGETリクエストを処理し、認証済みユーザーに対してシステムの日時を表示します。

  • if ... : else: ...のロジックでは、Pythonスクリプトは論理的なif redis_client.exists(auth_user):文を実行して、Redisサーバーにユーザーの資格情報が存在するかどうかを確認します。ユーザーの詳細が存在し、Redisに保存されているパスワードがユーザーが送信したパスワードと一致しない場合、アプリケーションは{"error": "Invalid username/password."}エラーを返します。

ユーザーの詳細がRedisサーバーに存在しない場合、アプリケーションはmysql_resp = mysql_server.query(auth_user, auth_password)ステートメントを使用してMySQLデータベースサーバーにクエリを送信します。ユーザーが提供したパスワードがデータベースに保存されている値と一致しない場合、アプリケーションは{"error": "Invalid username/password."}エラーを返します。それ以外の場合、アプリケーションはredis_client.set(auth_user, mysql_resp)ステートメントを使用してユーザーの詳細をRedisサーバーにキャッシュします。

  • ユーザーの資格情報がRedis/MySQLの詳細と一致する場合、アプリケーションは{"time": current_time, ...}ステートメントを使用してシステムの現在の日付/時刻を表示します。出力のauthorized byエントリでは、アプリケーションでユーザーを認証するデータベースサーバーが表示されます。

      if redis_client.exists(auth_user):
          if md5(auth_password.encode('utf8')).hexdigest() != redis_client.get(auth_user).decode('utf8'):
              resp = {"error": "ユーザー名/パスワードが無効です。"}
          else:
              resp = {"time": current_time, "authorized by": "Redisサーバー"}
      else:
          mysql_resp  = mysql_server.query(auth_user, auth_password)
          if mysql_resp == False:
               resp =  {"error": "ユーザー名/パスワードが無効です。"}
          else:
              resp = {"time": current_time, "authorized by": "MySQLサーバー"}
              redis_client.set(auth_user, mysql_resp)   
    

これでアプリケーションのメインファイルの設定が完了しました。次のステップでは、アプリケーションをテストします。

ステップ6 – アプリケーションのテスト

このステップでは、Redisのキャッシュメカニズムが機能するかどうかを確認するためにアプリケーションを実行します。以下のコマンドを実行してアプリケーションをテストしてください。

  1. 以下のpython3コマンドを使用してアプリケーションを実行します:

    python3 index.py
    
  2. アプリケーションのカスタムWebサーバーが実行されていることを確認してください:

    出力
    Webサーバーはポート8080..で実行されています。
  3. 新しいターミナルウィンドウでサーバへの別のSSH接続を確立し、以下のcurlコマンドを実行して、john_doeの認証情報を使用して4つのGETリクエストを送信してください。4つのリクエストを単一のコマンドで送信するために、http://localhost:8080/のURLの末尾に[1-4]を追加します:

    curl -X GET -u john_doe:password_1  http://localhost:8080/[1-4]
    
  4. 以下の出力を確認してください。MySQLサーバーは最初の認証リクエストのみ処理します。その後、Redisデータベースが次の3つのリクエストを処理します。

    出力
    [1/4] { "time": "2023-11-07 10:04:38", "authorized by": "MySQL server" } [4/4] { "time": "2023-11-07 10:04:38", "authorized by": "Redis server" }

アプリケーションのロジックは予想通りに動作しています。

結論

このガイドでは、Redisサーバーを使用して、ユーザーのログイン資格情報をキャッシュするPythonアプリケーションを構築しました。Redisは高可用性でスケーラブルなデータベースサーバーであり、秒間数千のトランザクションを実行することができます。アプリケーション内のRedisキャッシュメカニズムにより、バックエンドのデータベースサーバーへのトラフィックを大幅に削減することができます。Redisアプリケーションの詳細については、当社のRedisチュートリアルを参照してください。

Source:
https://www.digitalocean.com/community/tutorials/how-to-speed-up-python-mysql-application-session-handling-with-redis-on-ubuntu-22-04