Open9

redis-py / django-redis

taxintaxin

概要

  • django-redis / redis-pyのコードリーディングのメモ
  • 下記のような部分を見ていく
    • redis clientのオプションがどのように渡されるか
    • get, setなどの処理がどのように実装されているか
taxintaxin

django-redis - option

CACHES = {
    "default": {
        "BACKEND": "django_redis.cache.RedisCache",
        "LOCATION": "rediss://127.0.0.1:6379/1",
        "OPTIONS": {
            "CLIENT_CLASS": "django_redis.client.DefaultClient",
            "CONNECTION_POOL_KWARGS": {"ssl_cert_reqs": None}
        }
    }
}

CONNECTION_POOL_KWARGSをdjango-redisで参照している部分から追ってみる

django-redisのConnectionFactoryclassの初期化のタイミングでoption (pool_cls_kwargs) を取り出す
https://github.com/jazzband/django-redis/blob/master/django_redis/pool.py#L12-L34

get_connection_pool()のメソッドが呼ばれるタイミングで、connection poolの生成のタイミングでオプションを渡す
生成時には、pool_clsクラスのfrom_url()が呼ばれる
https://github.com/jazzband/django-redis/blob/master/django_redis/pool.py#L115-L125

pool_clsはユーザー側の指定がないと、redis.connection.ConnectionPoolが指定される
https://github.com/jazzband/django-redis/blob/master/django_redis/pool.py#L23-L26

taxintaxin

redis-py - ConnectionPool

ConnectionPool classには、from_urlのメソッドがある
https://github.com/redis/redis-py/blob/master/redis/connection.py#L1235-L1252

上記のメソッドでは下記の処理を行なっている

  • redis接続時のURL (redis://[[username]:[password]]@localhost:6379/0) のparse
  • オプションのoverride

つまり、django-redis側で指定したパラメータを利用して、redis-pyのconnectionの設定をoverrideする
https://github.com/redis/redis-py/blob/master/redis/connection.py#L1289-L1296

taxintaxin

redis-py - connection / client

  • redis-py側のsample codeを改めて見てみると、connectionではなくredis clientの生成時に ssl_cert_reqs のオプションを渡している
    • ssl_cert_reqsに関しては、client / connectionの両方のclassで指定できるっぽい
  • これを踏まえると、connection classとclient classの関係性を整理する必要がある
ssl_cert_conn = redis.Redis(
    host="localhost",
    port=6666,
    ssl=True,
    ssl_certfile=ssl_certfile,
    ssl_keyfile=ssl_keyfile,
    ssl_cert_reqs="required",
    ssl_ca_certs=ssl_ca_certs,
)

redis client側の処理をみるとconnection_poolの有無をチェックして、生成されていない場合はredis clientを生成しています
https://github.com/redis/redis-py/blob/master/redis/client.py#L949-L956

この際に、client側から渡したオプションは反映されます
https://github.com/redis/redis-py/blob/master/redis/client.py#L991-L1020

実装を踏まえると、connection poolとclientの両方で、同じオプションで異なる設定値を渡したとしても、connection pool側が優先されるということになるはず

その後は、connectionを初期化した上で、connection_poolからconnectionを取り出します

taxintaxin

redis-py - get_connection

  • get_connection()の処理を確認する

最初に、PIDのチェック処理を行う
https://github.com/redis/redis-py/blob/master/redis/connection.py#L1377-L1383

PIDのチェックで何を見ているかは説明があるので、ざっくりまとめると下記のようになる

  • fork-safeな状態を実現するための仕組み
  • 現在のPID と ConnectionPool インスタンスに保存されているPID を比較する
    • 同一なら何もせずに終了
    • 異なる場合は、self.reset()を呼び出す

https://github.com/redis/redis-py/blob/master/redis/connection.py#L1328

self.reset()を呼び出すと、connection側のプロセスに紐づくインスタンスの変数を初期化します
(この時、_checkpid()はprocessがforkして、現在子プロセスで動作していると仮定します)

https://github.com/redis/redis-py/blob/master/redis/connection.py#L1311-L1326

その後、Availableなconnectionがあるかを確認して、なければconnectionを生成する
https://github.com/redis/redis-py/blob/master/redis/connection.py#L1383-L1387

make_connection()では、各種引数を渡してconnectionを返す
ここでconnectionの上限と比較して、上限を超える数のconnectionを張ろうとしていたらexceptionをraiseする
https://github.com/redis/redis-py/blob/master/redis/connection.py#L1422-L1427

その後は、connection.connect()を呼び出して、redisへの接続を試みる
https://github.com/redis/redis-py/blob/master/redis/connection.py#L1390-L1404

taxintaxin

redis-py - connect()

  • redisの接続方式について簡単におさらいする
  • connect()self._connect()の内部処理についてみていく

redisの接続はTCP/IPもしくはUnix domain socketでの接続の2種類の方式が存在する

Redis Configurationから参照できるredis.confを確認すると、両方の形式に対応した設定項目が存在していた
(defaultでは、TCP/IPでの接続を想定しており、Unix socketでの接続はdisableされている)

# TCP listen() backlog.
#
# In high requests-per-second environments you need a high backlog in order
# to avoid slow clients connection issues. Note that the Linux kernel
# will silently truncate it to the value of /proc/sys/net/core/somaxconn so
# make sure to raise both the value of somaxconn and tcp_max_syn_backlog
# in order to get the desired effect.
tcp-backlog 511

# Unix socket.
#
# Specify the path for the Unix socket that will be used to listen for
# incoming connections. There is no default, so Redis will not listen
# on a unix socket when not specified.
#
# unixsocket /run/redis.sock
# unixsocketperm 700

これを踏まえて、connect()の処理に戻る

すでにsock (socket) が生成されていればreturnするが、初期状態であれば call_with_retryを呼び出す
https://github.com/redis/redis-py/blob/master/redis/connection.py#L606-L617

この処理自体は、retry classに定義されているが、doで渡されたラムダ式 (self._connect())を実行して、失敗した場合は指定回数を上限として一定間隔でのbackoff (再実行) が走る
https://github.com/redis/redis-py/blob/master/redis/retry.py#L35-L41

ラムダ式で渡されている _connect()を確認すると、TCP socketの生成処理を行なっている

ここでは、ソケット通信で利用されるsocketのlibraryのgetaddrinfo()を呼び出して、サービスに接続されたソケットを作成するために必要な全ての引数が入った要素のtupleに変換します
https://docs.python.org/ja/3/library/socket.html#socket.getaddrinfo

その後は各種設定した上で、address で示されるリモートソケットに接続します
ここまでが、ラムダ式で渡されるself. _connect()の処理です
https://github.com/redis/redis-py/blob/771109e/redis/connection.py#L664-L665

そして、生成されたsocketを利用、self.on_connect()を実行する
https://github.com/redis/redis-py/blob/771109e/redis/connection.py#L619-L630

on_connect()では、下記のような処理を実施した上でRedisに接続できるかどうかを確認します

  • connectionの初期化
  • Redisへの接続時の認証 (AUTH command)

接続できない場合は、disconnect()が呼び出されて、socketのshutdownなどの処理を行います
https://github.com/redis/redis-py/blob/771109e/redis/connection.py#L627-L631

taxintaxin

django-redis - get

ここまでが、get_connection()の全容でredis clientの初期化のタイミングで実行される処理の流れ
https://github.com/redis/redis-py/blob/master/redis/client.py#L1024-L1025

では、実際の処理が呼ばれるまでの流れはどうなっているか?

cache backendの指定箇所では、django_redis.cache.RedisCacheというdjango-redisのclassが指定されています

CACHES = {
    "default": {
        "BACKEND": "django_redis.cache.RedisCache",
        "LOCATION": "redis://127.0.0.1:6379/1",
        "OPTIONS": {
            "CLIENT_CLASS": "django_redis.client.DefaultClient",
        }
    }
}

内部的には、オプションでCLIENT_CLASSの指定がなければ、django_redis.client.DefaultClientを利用するような実装になっています
https://github.com/jazzband/django-redis/blob/master/django_redis/cache.py#L43-L52

CLIENT_CLASSで別の種類のclient classを指定することも可能です
https://github.com/jazzband/django-redis#use-the-sentinel-connection-factory

DefaultClientの初期化のタイミングでは、serializer / compressorをセットした上でget_connection_factory()を呼び出して、connection factoryを生成する

ん、connection factory...? となったので、メモを振り返ると最初の方に ConnectionFactoryクラスが登場していた
https://zenn.dev/link/comments/17a05cf6e1e061

これを踏まえて、getの処理を見ていく
https://github.com/jazzband/django-redis/blob/master/django_redis/client/default.py#L240-L265

まず最初に、redis-pyの処理を利用するためにredis-pyのclient (raw redis clientと呼ばれている) のinstanceを取得する
https://github.com/jazzband/django-redis/blob/master/django_redis/client/default.py#L95-L110

実際にclientを返しているのは、このあたりの実装
https://github.com/jazzband/django-redis/blob/master/django_redis/client/default.py#L104-L105
https://github.com/jazzband/django-redis/blob/master/django_redis/client/default.py#L112-L118

その上で、raw redis clientのget()メソッドを呼び出している
redis-pyのclient classでは各コマンドの処理は実装されていないが、CoreCommandsなどの各コマンドごとの処理を実装したクラスを継承している
https://github.com/redis/redis-py/blob/master/redis/client.py#L842

core commandsとして各種類のコマンドクラスを継承していて、DataAccessCommands > BasicKeyCommands として多重継承している
https://github.com/redis/redis-py/blob/master/redis/commands/core.py#L5946-L5955

BasicKeyCommands classでのget()の実装は下記で、self.execute_command("GET", name)を実行している
https://github.com/redis/redis-py/blob/master/redis/commands/core.py#L1722-L1728

execute_command()の処理の実態としては、渡されたコマンドをsocket経由で実行してreponseをparseしている
https://github.com/redis/redis-py/blob/master/redis/client.py#L1233-L1248
https://github.com/redis/redis-py/blob/master/redis/client.py#L1212-L1217
https://github.com/redis/redis-py/blob/master/redis/connection.py#L797-L801

send_packed_command()の実行時に、self._sock.sendall(item)を呼び出している
https://github.com/redis/redis-py/blob/master/redis/connection.py#L770-L781

socket.sendallはsocketへデータを送信するだけ
itemとして渡されているのは、GETを引数として渡したself.pack_command(*args)である
https://docs.python.org/ja/3/library/socket.html#socket.socket.sendall

taxintaxin

redis-py - pack_command(*args)

pack_commandでは、RESPに沿った形でコマンドのデータを整形している
https://github.com/redis/redis-py/blob/master/redis/connection.py#L842

redis clientが利用するprotocolはRedis serialization protocol (RESP)と呼ばれており、下記のリンクからprotocolの詳細仕様については確認できる
https://redis.io/docs/reference/protocol-spec/

redis-py側の実装に関係する部分の一例を挙げると、文字列のbytes数を表現する箇所のためにSYM_DOLLARが定義されている

A "$" byte followed by the number of bytes composing the string (a prefixed length), terminated by CRLF.

https://github.com/redis/redis-py/blob/master/redis/connection.py#L867-L872