Redis¶
FastAPI から使うために、 RockyLinux9 に Redis をインストールする。 レートリミット(SlowAPI) のストレージとして使う。
Redis サーバーのセットアップ¶
cf. Install Redis on Red Hat / Rocky
dnf でインストールして、 systemctl で起動する。
redis-cli ping に PONG が返ってくれば動いてる: cf. Redis CLI
Note
ローカルホストから使うだけなら firewalld の設定は不要。
バインドアドレス¶
デフォルトでローカルホストからの受付のみ許可されてる( /etc/redis/redis.conf より)。
# IF YOU ARE SURE YOU WANT YOUR INSTANCE TO LISTEN TO ALL THE INTERFACES
# JUST COMMENT OUT THE FOLLOWING LINE.
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
bind 127.0.0.1 -::1
ポート番号¶
デフォルトのポート番号は 6379( /etc/redis/redis.conf より)。
# Accept connections on the specified port, default is 6379 (IANA #815344).
# If port 0 is specified Redis will not listen on a TCP socket.
port 6379
LISTEN 状態の確認。
LISTEN 0 511 [::1]:6379 [::]:* users:(("redis-server",pid=14116,fd=7))
データベースの選択¶
デフォルトで 16 個の DB が用意されてて、デフォルトは 0 番( /etc/redis/redis.conf より)。
# Set the number of databases. The default database is DB 0, you can select
# a different one on a per-connection basis using SELECT <dbid> where
# dbid is a number between 0 and 'databases'-1
databases 16
使う DB の番号は SELECT で変えられる。
Redis が使うメモリの設定¶
/etc/redis/redis.conf の MEMORY MANAGEMENT の部分で設定できる項目を見ていく。 KEY の削除については Key eviction も参照。
使用状況や設定値の一覧は INFO memory で確認できる。
INFO memory の出力
# redis-cli
127.0.0.1:6379> INFO memory
# Memory
used_memory:874704
used_memory_human:854.20K
used_memory_rss:16351232
used_memory_rss_human:15.59M
used_memory_peak:1018624
used_memory_peak_human:994.75K
used_memory_peak_perc:85.87%
used_memory_overhead:832904
used_memory_startup:812064
used_memory_dataset:41800
used_memory_dataset_perc:66.73%
allocator_allocated:1003128
allocator_active:1277952
allocator_resident:3514368
total_system_memory:801439744
total_system_memory_human:764.31M
used_memory_lua:33792
used_memory_lua_human:33.00K
used_memory_scripts:328
used_memory_scripts_human:328B
number_of_cached_scripts:1
maxmemory:0
maxmemory_human:0B
maxmemory_policy:noeviction
allocator_frag_ratio:1.27
allocator_frag_bytes:274824
allocator_rss_ratio:2.75
allocator_rss_bytes:2236416
rss_overhead_ratio:4.65
rss_overhead_bytes:12836864
mem_fragmentation_ratio:19.61
mem_fragmentation_bytes:15517536
mem_not_counted_for_evict:0
mem_replication_backlog:0
mem_clients_slaves:0
mem_clients_normal:20512
mem_aof_buffer:0
mem_allocator:jemalloc-5.1.0
active_defrag_running:0
lazyfree_pending_objects:0
lazyfreed_objects:0
maxmemory(最大使用メモリ容量)¶
デフォルトは 0 なので使う量は無制限。設定するなら /etc/redis/redis.conf に書く。
redis.conf の説明
# Set a memory usage limit to the specified amount of bytes.
# When the memory limit is reached Redis will try to remove keys
# according to the eviction policy selected (see maxmemory-policy).
#
# If Redis can't remove keys according to the policy, or if the policy is
# set to 'noeviction', Redis will start to reply with errors to commands
# that would use more memory, like SET, LPUSH, and so on, and will continue
# to reply to read-only commands like GET.
#
# This option is usually useful when using Redis as an LRU or LFU cache, or to
# set a hard memory limit for an instance (using the 'noeviction' policy).
#
# WARNING: If you have replicas attached to an instance with maxmemory on,
# the size of the output buffers needed to feed the replicas are subtracted
# from the used memory count, so that network problems / resyncs will
# not trigger a loop where keys are evicted, and in turn the output
# buffer of replicas is full with DELs of keys evicted triggering the deletion
# of more keys, and so forth until the database is completely emptied.
#
# In short... if you have replicas attached it is suggested that you set a lower
# limit for maxmemory so that there is some free RAM on the system for replica
# output buffers (but this is not needed if the policy is 'noeviction').
#
# maxmemory <bytes>
maxmemory-policy(メモリ不足時に何を削除するか)¶
デフォルトは noeviction なので、削除せずにエラーを返す。
redis.conf の説明
# MAXMEMORY POLICY: how Redis will select what to remove when maxmemory
# is reached. You can select one from the following behaviors:
#
# volatile-lru -> Evict using approximated LRU, only keys with an expire set.
# allkeys-lru -> Evict any key using approximated LRU.
# volatile-lfu -> Evict using approximated LFU, only keys with an expire set.
# allkeys-lfu -> Evict any key using approximated LFU.
# volatile-random -> Remove a random key having an expire set.
# allkeys-random -> Remove a random key, any key.
# volatile-ttl -> Remove the key with the nearest expire time (minor TTL)
# noeviction -> Don't evict anything, just return an error on write operations.
#
# LRU means Least Recently Used
# LFU means Least Frequently Used
#
# Both LRU, LFU and volatile-ttl are implemented using approximated
# randomized algorithms.
#
# Note: with any of the above policies, when there are no suitable keys for
# eviction, Redis will return an error on write operations that require
# more memory. These are usually commands that create new keys, add data or
# modify existing keys. A few examples are: SET, INCR, HSET, LPUSH, SUNIONSTORE,
# SORT (due to the STORE argument), and EXEC (if the transaction includes any
# command that requires memory).
#
# The default is:
#
# maxmemory-policy noeviction
maxmemory-samples(削除するものを決めるサンプリング数)¶
デフォルトでは、メモリ不足で削除するものを決めるときに 5 個をランダムで選んで比較して、その中から削除する。
redis.conf の説明
# LRU, LFU and minimal TTL algorithms are not precise algorithms but approximated
# algorithms (in order to save memory), so you can tune it for speed or
# accuracy. By default Redis will check five keys and pick the one that was
# used least recently, you can change the sample size using the following
# configuration directive.
#
# The default of 5 produces good enough results. 10 Approximates very closely
# true LRU but costs more CPU. 3 is faster but not very accurate.
#
# maxmemory-samples 5
maxmemory-eviction-tenacity(削除処理をどれくらい頑張るか)¶
デフォルトは 10 でバランス型。 0 にすると削除処理よりも遅延抑制を優先するので削除は控えめ、 100 にすると遅延してでも削除処理を進める。デフォルトでうまく動くように設計されてるそうなので、基本そのままで OK か。
redis.conf の説明
# Eviction processing is designed to function well with the default setting.
# If there is an unusually large amount of write traffic, this value may need to
# be increased. Decreasing this value may reduce latency at the risk of
# eviction processing effectiveness
# 0 = minimum latency, 10 = default, 100 = process without regard to latency
#
# maxmemory-eviction-tenacity 10
replica-ignore-maxmemory(Replication に関する設定)¶
デフォルトでは Replica は maxmemory 設定を無視する。 Master とのデータ不整合対策のため。 Replication しないなら無視していい。
redis.conf の説明
# Starting from Redis 5, by default a replica will ignore its maxmemory setting
# (unless it is promoted to master after a failover or manually). It means
# that the eviction of keys will be just handled by the master, sending the
# DEL commands to the replica as keys evict in the master side.
#
# This behavior ensures that masters and replicas stay consistent, and is usually
# what you want, however if your replica is writable, or you want the replica
# to have a different memory setting, and you are sure all the writes performed
# to the replica are idempotent, then you may change this default (but be sure
# to understand what you are doing).
#
# Note that since the replica by default does not evict, it may end using more
# memory than the one set via maxmemory (there are certain buffers that may
# be larger on the replica, or data structures may sometimes take more memory
# and so forth). So make sure you monitor your replicas and make sure they
# have enough memory to never hit a real out-of-memory condition before the
# master hits the configured maxmemory setting.
#
# replica-ignore-maxmemory yes
active-expire-effort(期限切れ Key の削除をどれくらい頑張るか)¶
デフォルトは 1 で、いい感じにやってくれるっぽい。もし不都合があれば、もっと積極的に削除するようにレベルを 10 まで上げられるみたい。
redis.conf の説明
# Redis reclaims expired keys in two ways: upon access when those keys are
# found to be expired, and also in background, in what is called the
# "active expire key". The key space is slowly and interactively scanned
# looking for expired keys to reclaim, so that it is possible to free memory
# of keys that are expired and will never be accessed again in a short time.
#
# The default effort of the expire cycle will try to avoid having more than
# ten percent of expired keys still in memory, and will try to avoid consuming
# more than 25% of total memory and to add latency to the system. However
# it is possible to increase the expire "effort" that is normally set to
# "1", to a greater value, up to the value "10". At its maximum value the
# system will use more CPU, longer cycles (and technically may introduce
# more latency), and will tolerate less already expired keys still present
# in the system. It's a tradeoff between memory, CPU and latency.
#
# active-expire-effort 1
ベンチマーク¶
redis-benchmark で手元の Redis がどれくらい捌けるか測ってみる。 -q(quiet mode)を付けると、コマンドごとに requests per second(1秒あたりの処理数)と p50(レイテンシの中央値)だけの 1行サマリーが出る。
cf. Redis benchmark
PING_MBULK: 71581.96 requests per second, p50=0.351 msec
SET: 72780.20 requests per second, p50=0.351 msec
GET: 71942.45 requests per second, p50=0.351 msec
INCR: 72727.27 requests per second, p50=0.351 msec
LPUSH: 71530.76 requests per second, p50=0.351 msec
RPUSH: 72358.90 requests per second, p50=0.351 msec
LPOP: 73367.57 requests per second, p50=0.343 msec
RPOP: 73046.02 requests per second, p50=0.351 msec
SADD: 72202.16 requests per second, p50=0.351 msec
HSET: 72306.58 requests per second, p50=0.351 msec
SPOP: 71736.01 requests per second, p50=0.351 msec
ZADD: 57537.40 requests per second, p50=0.351 msec
ZPOPMIN: 71684.59 requests per second, p50=0.351 msec
LPUSH (needed to benchmark LRANGE): 71377.59 requests per second, p50=0.351 msec
LRANGE_100 (first 100 elements): 43572.98 requests per second, p50=0.575 msec
LRANGE_300 (first 300 elements): 20028.04 requests per second, p50=1.239 msec
LRANGE_500 (first 500 elements): 14210.60 requests per second, p50=1.743 msec
LRANGE_600 (first 600 elements): 12335.02 requests per second, p50=2.007 msec
MSET (10 keys): 74183.98 requests per second, p50=0.343 msec
Info
数値は手元のマシンでの実測値で、環境によって変わる。見るべきは絶対値ではなく傾向。
考察:
SET/GET/INCR/LPUSH/HSETみたいな単純なコマンドは、だいたい 7万 requests/sec 前後・p500.35 msec 前後で横並び。これらは O(1) なので、データ量に関係なく一定の速さで返ってくる。ZADDだけ 5.7万 requests/sec で少し遅い。 Sorted Set は要素を順序付きで保持する(内部はスキップリスト)ので、挿入のコストが他の書き込みより高い。LRANGEは取り出す要素数が増えるほどはっきり遅くなる。 O(N) で、返すデータ量とシリアライズのコストが要素数に比例するから。
| LRANGEの要素数 | requests/sec | p50 |
|---|---|---|
| 100 | 約 43,600 | 0.575 msec |
| 300 | 約 20,000 | 1.239 msec |
| 500 | 約 14,200 | 1.743 msec |
| 600 | 約 12,300 | 2.007 msec |
レートリミット(SlowAPI) で使うのは INCR や EXPIRE みたいな O(1) のコマンドなので、この結果を見る限り Redis 側がボトルネックになる心配はなさそう。
TCP と UNIX ドメインソケットの比較¶
FastAPI と Redis を同じホストに置くなら、 TCP の代わりに UNIX ドメインソケットでも繋げる。 /etc/redis/redis.conf に 2 行足して設定する。
redis-benchmark を TCP( -h / -p )と UNIX ソケット( -s )で、同じ回数・コマンドで比べてみる。
PING_MBULK: 65019.51 requests per second, p50=0.391 msec
SET: 65231.57 requests per second, p50=0.383 msec
GET: 64808.82 requests per second, p50=0.391 msec
PING_MBULK: 95877.28 requests per second, p50=0.263 msec
SET: 98716.68 requests per second, p50=0.255 msec
GET: 99502.48 requests per second, p50=0.247 msec
| 接続方法 | requests/sec | p50 |
|---|---|---|
TCP( 127.0.0.1:6379 ) |
約 62,000〜65,000 | 約 0.39 msec |
UNIX ソケット( /tmp/redis.sock ) |
約 96,000〜99,000 | 約 0.25 msec |
UNIX ソケットの方が速いのは、ローカルでも TCP/IP スタック(パケット化・チェックサム・プロトコル処理・ループバック)を通る TCP に対して、 UNIX ソケットはカーネル内の IPC でそれを省けるから。別マシンの Redis には使えないけど、同居構成なら有利。
SlowAPI から UNIX ソケットで繋ぐ¶
レートリミット(SlowAPI) で UNIX ソケットを使うなら、 storage_uri を redis+unix:// スキームにする。 limits が redis-py 用の unix:// に変換して from_url に渡す。
ソケットのパーミッション
unixsocketperm 700 だと、ソケットファイルは Redis を動かしてるユーザー(ふつう redis )と root しか触れない。 root でやった redis-benchmark が通るのはこのため。 FastAPI アプリを別ユーザーで動かしてると PermissionError になって繋がらない。アプリのユーザーから使うなら 770 にして、そのユーザーを redis グループに入れる。
redis-py 8.0 + RESP3 + UNIX ソケットの不具合
redis-py 8.0.0 だと、 UNIX ソケット接続でそのまま繋ごうとすると ValueError: Cannot enable maintenance notifications for connection object that doesn't have a host attribute. で Internal Server Error になる。 storage_options={"protocol": 2} で RESP2 にすれば回避できる。 redis-py 側は修正済み(#4097 マージ済み)なので、 後のリリースでは直るはず: cf. redis-py #4086