Skip to content

Redis

FastAPI から使うために、 RockyLinux9 に Redis をインストールする。 レートリミット(SlowAPI) のストレージとして使う。


Redis サーバーのセットアップ

cf. Install Redis on Red Hat / Rocky

dnf でインストールして、 systemctl で起動する。

# dnf install redis -y

# systemctl enable redis

# systemctl start redis

# redis-cli ping
PONG

redis-cli pingPONG が返ってくれば動いてる: 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 状態の確認。

ss -lntp | grep 6379LISTEN 0 511 127.0.0.1:6379 0.0.0.0:* users:(("redis-server",pid=14116,fd=6))
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 で変えられる。

127.0.0.1:6379> SELECT 1
OK
127.0.0.1:6379[1]>

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 に書く。

127.0.0.1:6379> CONFIG GET maxmemory
1) "maxmemory"
2) "0"
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 なので、削除せずにエラーを返す。

127.0.0.1:6379> CONFIG GET maxmemory-policy
1) "maxmemory-policy"
2) "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 個をランダムで選んで比較して、その中から削除する。

127.0.0.1:6379> CONFIG GET maxmemory-samples
1) "maxmemory-samples"
2) "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 か。

127.0.0.1:6379> CONFIG GET maxmemory-eviction-tenacity
1) "maxmemory-eviction-tenacity"
2) "10"
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 しないなら無視していい。

127.0.0.1:6379> CONFIG GET replica-ignore-maxmemory
1) "replica-ignore-maxmemory"
2) "yes"
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 まで上げられるみたい。

127.0.0.1:6379> CONFIG GET active-expire-effort
1) "active-expire-effort"
2) "1"
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

redis-benchmark -qPING_INLINE: 70621.47 requests per second, p50=0.359 msec
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 前後・ p50 0.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) で使うのは INCREXPIRE みたいな O(1) のコマンドなので、この結果を見る限り Redis 側がボトルネックになる心配はなさそう。

TCP と UNIX ドメインソケットの比較

FastAPI と Redis を同じホストに置くなら、 TCP の代わりに UNIX ドメインソケットでも繋げる。 /etc/redis/redis.conf に 2 行足して設定する。

unixsocket /tmp/redis.sock
unixsocketperm 700

redis-benchmark を TCP( -h / -p )と UNIX ソケット( -s )で、同じ回数・コマンドで比べてみる。

redis-benchmark -h 127.0.0.1 -p 6379 -n 100000 -t set,get,ping -qPING_INLINE: 61842.92 requests per second, p50=0.399 msec
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
redis-benchmark -s /tmp/redis.sock -n 100000 -t set,get,ping -qPING_INLINE: 96246.39 requests per second, p50=0.255 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_uriredis+unix:// スキームにする。 limits が redis-py 用の unix:// に変換して from_url に渡す。

limiter = Limiter(
    key_func=get_remote_address,
    storage_uri="redis+unix:///tmp/redis.sock",
)

ソケットのパーミッション

unixsocketperm 700 だと、ソケットファイルは Redis を動かしてるユーザー(ふつう redis )と root しか触れない。 root でやった redis-benchmark が通るのはこのため。 FastAPI アプリを別ユーザーで動かしてると PermissionError になって繋がらない。アプリのユーザーから使うなら 770 にして、そのユーザーを redis グループに入れる。

# /etc/redis/redis.conf
unixsocketperm 770
# アプリを動かすユーザー(例: alice)を redis グループに追加してから、アプリを再起動する
# usermod -aG redis alice

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