redis-py / django-redis

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

django-redis
- Django側で、cache用のbackendとして利用される
CACHES = {
"default": {
"BACKEND": "django_redis.cache.RedisCache",
"LOCATION": "redis://127.0.0.1:6379/1",
"OPTIONS": {
"CLIENT_CLASS": "django_redis.client.DefaultClient",
}
}
}

django-redis - option
- redis-pyでは下記リンク先のようにクライアント生成時に、オプションを指定できる
- 一方で、同様のオプションをdjango-redisを利用して指定すると、
CACHES.<backend_name>.OPTIONS.CONNECTION_POOL_KWARGS
に指定する
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のConnectionFactory
classの初期化のタイミングでoption (pool_cls_kwargs
) を取り出す
get_connection_pool()
のメソッドが呼ばれるタイミングで、connection poolの生成のタイミングでオプションを渡す
生成時には、pool_cls
クラスのfrom_url()
が呼ばれる
pool_cls
はユーザー側の指定がないと、redis.connection.ConnectionPool
が指定される

redis-py - ConnectionPool
-
redis.connection.ConnectionPool
の実装を確認する
ConnectionPool classには、from_url
のメソッドがある
上記のメソッドでは下記の処理を行なっている
- redis接続時のURL (
redis://[[username]:[password]]@localhost:6379/0
) のparse - オプションのoverride
つまり、django-redis側で指定したパラメータを利用して、redis-pyのconnectionの設定をoverrideする

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を生成しています
この際に、client側から渡したオプションは反映されます
実装を踏まえると、connection poolとclientの両方で、同じオプションで異なる設定値を渡したとしても、connection pool側が優先されるということになるはず
その後は、connectionを初期化した上で、connection_poolからconnectionを取り出します

redis-py - get_connection
-
get_connection()
の処理を確認する
最初に、PIDのチェック処理を行う
PIDのチェックで何を見ているかは説明があるので、ざっくりまとめると下記のようになる
- fork-safeな状態を実現するための仕組み
- 現在のPID と ConnectionPool インスタンスに保存されているPID を比較する
- 同一なら何もせずに終了
- 異なる場合は、
self.reset()
を呼び出す
self.reset()
を呼び出すと、connection側のプロセスに紐づくインスタンスの変数を初期化します
(この時、_checkpid()はprocessがforkして、現在子プロセスで動作していると仮定します)
その後、Availableなconnectionがあるかを確認して、なければconnectionを生成する
make_connection()
では、各種引数を渡してconnectionを返す
ここでconnectionの上限と比較して、上限を超える数のconnectionを張ろうとしていたらexceptionをraiseする
その後は、connection.connect()
を呼び出して、redisへの接続を試みる

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
を呼び出す
この処理自体は、retry classに定義されているが、do
で渡されたラムダ式 (self._connect()
)を実行して、失敗した場合は指定回数を上限として一定間隔でのbackoff (再実行) が走る
ラムダ式で渡されている _connect()
を確認すると、TCP socketの生成処理を行なっている
ここでは、ソケット通信で利用されるsocket
のlibraryのgetaddrinfo()
を呼び出して、サービスに接続されたソケットを作成するために必要な全ての引数が入った要素のtupleに変換します
その後は各種設定した上で、address で示されるリモートソケットに接続します
ここまでが、ラムダ式で渡されるself. _connect()
の処理です
そして、生成されたsocketを利用、self.on_connect()
を実行する
on_connect()
では、下記のような処理を実施した上でRedisに接続できるかどうかを確認します
- connectionの初期化
- Redisへの接続時の認証 (AUTH command)
接続できない場合は、disconnect()
が呼び出されて、socketのshutdownなどの処理を行います

django-redis - get
ここまでが、get_connection()
の全容でredis clientの初期化のタイミングで実行される処理の流れ
では、実際の処理が呼ばれるまでの流れはどうなっているか?
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
を利用するような実装になっています
CLIENT_CLASS
で別の種類のclient classを指定することも可能です
DefaultClient
の初期化のタイミングでは、serializer / compressorをセットした上でget_connection_factory()
を呼び出して、connection factoryを生成する
ん、connection factory...? となったので、メモを振り返ると最初の方に ConnectionFactory
クラスが登場していた
これを踏まえて、getの処理を見ていく
まず最初に、redis-pyの処理を利用するためにredis-pyのclient (raw redis client
と呼ばれている) のinstanceを取得する
実際にclientを返しているのは、このあたりの実装
その上で、raw redis clientのget()
メソッドを呼び出している
redis-pyのclient classでは各コマンドの処理は実装されていないが、CoreCommands
などの各コマンドごとの処理を実装したクラスを継承している
core commandsとして各種類のコマンドクラスを継承していて、DataAccessCommands
> BasicKeyCommands
として多重継承している
BasicKeyCommands classでのget()
の実装は下記で、self.execute_command("GET", name)
を実行している
execute_command()
の処理の実態としては、渡されたコマンドをsocket経由で実行してreponseをparseしている
send_packed_command()
の実行時に、self._sock.sendall(item)
を呼び出している
socket.sendall
はsocketへデータを送信するだけ
itemとして渡されているのは、GETを引数として渡したself.pack_command(*args)
である

redis-py - pack_command(*args)
pack_command
では、RESPに沿った形でコマンドのデータを整形している
redis clientが利用するprotocolはRedis serialization protocol (RESP)
と呼ばれており、下記のリンクからprotocolの詳細仕様については確認できる
redis-py側の実装に関係する部分の一例を挙げると、文字列のbytes数を表現する箇所のためにSYM_DOLLAR
が定義されている
A "$" byte followed by the number of bytes composing the string (a prefixed length), terminated by CRLF.