その手の平は尻もつかめるさ

ギジュツ的な事をメーンで書く予定です

Redis の Slave (Replica) の Expire は 4.0 RC3 以降信用して良くなっている

maedama.hatenablog.com
trapezoid.hatenablog.com

上記のブログには今から6年ほど前の当時の情報が記されていますが,Redis 4.0 RC3 以降の Slave (replica) の Expire は信用して良くなっているようです.
Redis の公式ドキュメント (Replication – Redis) を参照すると,

However note that writable replicas before version 4.0 were incapable of expiring keys with a time to live set. This means that if you use EXPIRE or other commands that set a maximum TTL for a key, the key will leak, and while you may no longer see it while accessing it with read commands, you will see it in the count of keys and it will still use memory. So in general mixing writable replicas (previous version 4.0) and keys with TTL is going to create issues.

Redis 4.0 RC3 and greater versions totally solve this problem and now writable replicas are able to evict keys with TTL as masters do, with the exceptions of keys written in DB numbers greater than 63 (but by default Redis instances only have 16 databases).

とあり,どうやらExpire が信用できない挙動は Redis 4.0 RC3 から修正されているようです.
どういうことかと言うと,「Slave 側ではデータは Purge しない。Master で データの Purge の Replication をまつ」という挙動が解消され,slave (replica) 側でも能動的に TTL の判断ができるようになっています.
つまり,slave (replica) に対して TTL 切れのアイテムに問い合わせたとしても,正しく expire することとなります.


というわけで検証してみます.Dockerを使ってやってみましょう.なお検証に用いたバージョンは 4.0.14 および 5.0.7 です (記載上は 4.0.14 を使用します).

まずは master (primary) 側の Redis server の Docker container を用意します.

FROM redis:4.0.14
COPY primary.redis.conf /usr/local/etc/redis/redis.conf
CMD [ "redis-server", "/usr/local/etc/redis/redis.conf" ]

この時の primary.redis.conf は以下の通り:

bind 0.0.0.0

コンテナ間通信をさせる必要があるので network を作っておきます.

$ docker network create -d bridge redis-test

作った master (primary) の Redis server を起動しましょう.

$ docker run --net=redis-test -p 6379:6379 primary-redis:latest

コンテナの IP アドレスを確認しておきます:

$ docker network inspect redis-test
[
    {
        "Name": "redis-test",
        "Id": "379ae24b35454bc119d80f35d3dd40e3db6e1ec72579ddbba32ca322cec4293a",
        "Created": "2020-05-08T02:53:19.955015821Z",
        "Scope": "local",
        "Driver": "bridge",
        "EnableIPv6": false,
        "IPAM": {
            "Driver": "default",
            "Options": {},
            "Config": [
                {
                    "Subnet": "172.18.0.0/16",
                    "Gateway": "172.18.0.1"
                }
            ]
        },
        "Internal": false,
        "Attachable": false,
        "Ingress": false,
        "ConfigFrom": {
            "Network": ""
        },
        "ConfigOnly": false,
        "Containers": {
            "e75cbce6bce2e1a797780da5c7500aaebbc1b4cf0e28865854e6ba5c46e78b80": {
                "Name": "amazing_greider",
                "EndpointID": "a3ddb49a5406447b3f52757b7b9c6be91baa2d5d3a221ec3ef27af697860f595",
                "MacAddress": "02:42:ac:12:00:02",
                "IPv4Address": "172.18.0.2/16",
                "IPv6Address": ""
            }
        },
        "Options": {},
        "Labels": {}
    }
]

172.18.0.2 了解.

それでは slave (replica) 側の Redis server の Docker container も作ってしまいましょう.

FROM redis:4.0.14
COPY replica.redis.conf /usr/local/etc/redis/redis.conf
CMD [ "redis-server", "/usr/local/etc/redis/redis.conf" ]

この時の replica.redis.conf は以下の通り:

bind 0.0.0.0
slaveof 172.18.0.2 6379

slaveof に先ほど取得した master (primary) の Redis server の IP アドレスを入れておきます.

そしてこのコンテナも起動.

$ docker run --net=redis-test -p 16379:6379 replica-redis:latest
$ docker ps
CONTAINER ID        IMAGE                  COMMAND                  CREATED             STATUS              PORTS                     NAMES
f3efc8e79267        replica-redis:latest   "docker-entrypoint.s…"   3 seconds ago       Up 2 seconds        0.0.0.0:16379->6379/tcp   interesting_wright
52e66b665fe2        primary-redis:latest   "docker-entrypoint.s…"   4 seconds ago       Up 3 seconds        0.0.0.0:6379->6379/tcp    wonderful_tereshkova

上記のようにホストマシンのポート 6379 は master (primary) に,そして 16379 は slave (replica) の Redis server に繋がっていることとなります.


それでは Expire を利用するコードをホストマシンから流し込んで検証してみましょう.

$ redis-cli -p 6379 SET foo 1 EX 5 && \
  redis-cli -p 6379 GET foo && \
  redis-cli -p 16379 GET foo && \
  sleep 6 && \
  redis-cli -p 16379 GET foo && \
  redis-cli -p 6379 GET foo
  • master (primary) にセット
  • master (primary) と slave (replica) でゲット
  • expire duration が経過するまで sleep
  • slave (replica) 側から再度ゲット (master (primary) が先ではないのが重要)
  • master (primary) 側から再度ゲット

というシナリオです.

結果としては

OK
"1"
"1"
# 6 秒経過
(nil)
(nil)

というふうになり,slave (replica) 側から GET を試みたとしてもちゃんとデータが purge されています.したがって「Slave 側ではデータは Purge しない。Master で データの Purge の Replication をまつ」という挙動は解消していることがわかります.

よかったですね!


おまけ

さて,前述のブログで id:maedama さんが指摘している,replication 遅延が生じた時の挙動についても見てみましょう.

tc を使って擬似的なパケット遅延を表現するので,master (primary) 側の Docker file に手を加えて iproute2 をインストールしておきます.

FROM redis:4.0.14
COPY primary.redis.conf /usr/local/etc/redis/redis.conf
RUN apt-get -y update && apt-get install -y iproute2
CMD [ "redis-server", "/usr/local/etc/redis/redis.conf" ]

そしてコンテナを立ち上げてみます.tc で制御するためには --cap-add NET_ADMIN で権限を与える必要あり.

$ docker run --net=redis-test -p 6379:6379 --cap-add NET_ADMIN primary-redis:latest

次に tc で擬似的に master (primary) から slave (replica) 宛て (この場合 172.18.0.3/32) のパケットを 2000ms 遅延させる設定を加えてみます (678a54eb5d91 は primary-redis の container ID).

docker exec 678a54eb5d91 tc qdisc add dev eth0 root handle 1: prio
docker exec 678a54eb5d91 tc filter add dev eth0 parent 1:0 protocol ip prio 1 u32 match ip dst 172.18.0.3/32 flowid 2:1
docker exec 678a54eb5d91 tc qdisc add dev eth0 parent 1:1 handle 2: netem delay 2000ms

ホストマシンから redis-cli で検証コマンドを流し込んでみましょう.

redis-cli -p 6379 SET foo 1 EX 3 && \
  sleep 2 && \
  redis-cli -p 6379 EXPIRE foo 3 && \
  sleep 2 && \
  redis-cli -p 6379 INCR foo && \
  redis-cli -p 6379 GET foo && \
  redis-cli -p 16379 GET foo

このときの出力は以下のようになります:

OK
# 2秒後
(integer) 1
# 2秒後
(integer) 2
"2"
"1"

slave (replica) 側では EXPIRE foo 3 までは適用されている一方,2000ms の遅延によって INCR foo が反映されていないことがわかります.

さらに2秒待ってみましょう

redis-cli -p 6379 SET foo 1 EX 3 && \
  sleep 2 && \
  redis-cli -p 6379 EXPIRE foo 3 && \
  sleep 2 && \
  redis-cli -p 6379 INCR foo && \
  redis-cli -p 6379 GET foo && \
  redis-cli -p 16379 GET foo && \
  sleep 2 && \
  redis-cli -p 6379 GET foo && \
  redis-cli -p 16379 GET foo

出力はこのような感じ

OK
# 2秒後
(integer) 1
# 2秒後
(integer) 2
"2"
"1"
# 2秒後
(nil)
"2"

INCR foo まで slave (replica) に反映されたことが見て取れます.slave (replica) 側は自前で TTL を持っているような感じに見えますね.この場合は素朴なレプリケーション遅延が起きるような感じがします.