redis-pyからRedis Sentinelにアクセスしてみる

# TL;DR

* `redis-py`はRedis Sentinelに対応したモジュールがあるので、そちらを使ってRedis Sentinelにアクセスが可能
* Redis Sentinel経由で取得したRedisへの接続はコネクションプールであり、通常のRedisへの操作と同様に利用可能
* マスターがダウンした際に使っていた接続（コネクションプール）は、フェイルオーバーに追従してくれる


# 環境

今回の環境は、こちら。

```shell
$ cat /etc/redhat-release 
CentOS Linux release 7.6.1810 (Core)


$ uname -a
Linux localhost.localdomain 3.10.0-957.12.2.el7.x86_64 #1 SMP Tue May 14 21:24:32 UTC 2019 x86_64 x86_64 x86_64 GNU/Linux


$ redis-server -v
Redis server v=5.0.6 sha=00000000:0 malloc=jemalloc-5.1.0 bits=64 build=c3d7ebb6b1a2844b


$ redis-sentinel -v
Redis server v=5.0.6 sha=00000000:0 malloc=jemalloc-5.1.0 bits=64 build=c3d7ebb6b1a2844b


$ python -V
Python 3.6.8


$ pip3 freeze
redis==3.3.8
```

CentOS 7、Redis 5.0.6、Python 3.6.8、redis-py 3.3.8です。

各サーバーは、以下のように用意します。

* マスター … `192.168.33.10`
* レプリカ … `192.168.33.11`
* Sentinel 1〜3 … `192.168.33.12`〜`194.168.33.14`
* クライアント … `192.168.33.15`



# Redis Sentinelの構築

まずは、Redis Sentinalを構築しましょう。

EPELから、Redisをインストールします。

```shell
$ sudo yum install epel-release
$ sudo yum install https://rpms.remirepo.net/enterprise/remi-release-7.rpm
$ sudo yum --enablerepo=remi install redis
```

まずは、Redisのレプリケーションを構成します。

Redisのマスターは、`/etc/redis.conf`のデフォルト設定から以下のように変更。  
※デフォルト設定の全体は、最後に記載します

```shell
bind 0.0.0.0
```

レプリカ側は、`/etc/redis.conf`のデフォルト設定から以下のように変更。

```shell
bind 0.0.0.0

replicaof 192.168.33.10 6379
```

Redisのマスター、レプリカをそれぞれ起動して

```shell
$ sudo systemctl start redis
```

レプリケーションが動作していることを確認。

マスター側。

```shell
$ redis-cli -h 192.168.33.10
192.168.33.10:6379> info replication
# Replication
role:master
connected_slaves:1
slave0:ip=192.168.33.11,port=6379,state=online,offset=28,lag=0
master_replid:54810ab6453ffdf12f21ff34157e2e0d655f8a81
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:28
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:28
```

レプリカ側。

```shell
$ redis-cli -h 192.168.33.11
192.168.33.11:6379> info replication
# Replication
role:slave
master_host:192.168.33.10
master_port:6379
master_link_status:up
master_last_io_seconds_ago:6
master_sync_in_progress:0
slave_repl_offset:56
slave_priority:100
slave_read_only:1
connected_slaves:0
master_replid:54810ab6453ffdf12f21ff34157e2e0d655f8a81
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:56
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:56
```

続いて、Redis Sentinelを構成します。`/etc/redis-sentinel.conf`のデフォルト設定から、以下のように変更します。  
※デフォルト設定の全体は、最後に記載します

```shell
sentinel monitor mymaster 192.168.33.10 6379 2
```

Redis Sentinel起動（3台で行います）。

```shell
$ sudo systemctl start redis-sentinel
```

確認。

```shell
$ redis-cli -h 192.168.33.12 -p 26379
192.168.33.12:26379> info sentinel
# Sentinel
sentinel_masters:1
sentinel_tilt:0
sentinel_running_scripts:0
sentinel_scripts_queue_length:0
sentinel_simulate_failure_flags:0
master0:name=mymaster,status=ok,address=192.168.33.10:6379,slaves=1,sentinels=3
```

マスター、レプリカがそれぞれひとつ、Sentinelが3つある状態ですね。


# `redis-py`で、Redis Sentinelにアクセスする

それでは、今度はRedis Sentinelに`redis-py`からアクセスしてみます。まずはインストール。

```shell
$ pip3 install redis
```

今回の`redis-py`のバージョンです。

```shell
$ pip3 freeze
redis==3.3.8
```

`redis-py`からRedis Sentinelへのアクセスですが、以下を参考に

[Sentinel support](https://github.com/andymccurdy/redis-py#sentinel-support)

コマンドラインプログラムを書いてみました。

`sentinel_client.py`

```python
import re
from redis.exceptions import ReadOnlyError
from redis.sentinel import Sentinel
import sys

try:
    print('start sentinel client')

    sentinels = [('192.168.33.12', 26379), ('192.168.33.13', 26379), ('192.168.33.14', 26379)]
    sentinel = Sentinel(sentinels, decode_responses = True)

    redis = None

    while True:
        if redis != None:
            print('{}: > '.format(redis), end = '', flush = True)
        else:
            print('> ', end = '', flush = True)

        command = sys.stdin.readline().strip()

        if command == 'info master':
            print(sentinel.discover_master('mymaster'))
        elif command == 'info replicas':
            print(sentinel.discover_slaves('mymaster'))
        elif command == 'use master':
            redis = sentinel.master_for('mymaster')
        elif command == 'use replica':
            redis = sentinel.slave_for('mymaster')
        elif command == 'info':
            print(str(redis.info()))
        elif command.startswith('set '):
            tokens = re.split('\s+', command)
            key = tokens[1]
            value = tokens[2]

            try:
                redis.set(key, value)
                print('set {} = {}'.format(key, value))
            except ReadOnlyError as e:
                print('{}: {}'.format(e.__class__.__name__, e))

        elif command.startswith('get '):
            tokens = re.split('\s+', command)
            key = tokens[1]
            print('get[{}] = {}'.format(key, redis.get(key)))
        elif command.startswith('del '):
            tokens = re.split('\s+', command)
            key = tokens[1]

            try:
                redis.delete(key)
                print('del, {}'.format(key))
            except ReadOnlyError as e:
                print('{}: {}'.format(e.__class__.__name__, e))
        elif command == 'exit':
            print('bye bye!!')
            break
        elif not command:
            pass
        else:
            print('unknown command = {}'.format(command))

except KeyboardInterrupt:
    print('bye bye!!')
```

Sentinelへのアクセスは、各Sentinelプロセスへのアクセス先を使って、Sentinelインスタンスを作成することで行います。

```python
    sentinels = [('192.168.33.12', 26379), ('192.168.33.13', 26379), ('192.168.33.14', 26379)]
    sentinel = Sentinel(sentinels, decode_responses = True)
```

マスターおよびレプリカへのアクセスは、`Sentinel#master_for`または`Sentinel#slave_for`で行います。

```shell
            redis = sentinel.master_for('mymaster')

            redis = sentinel.slave_for('mymaster')
```

各メソッドの戻り値は、`SentinelConnectionPool`のインスタンスです。

あとは、`SentinelConnectionPool`に対してRedisコマンドを実行すればOKです。

```python
        elif command.startswith('set '):
            tokens = re.split('\s+', command)
            key = tokens[1]
            value = tokens[2]

            try:
                redis.set(key, value)
                print('set {} = {}'.format(key, value))
            except ReadOnlyError as e:
                print('{}: {}'.format(e.__class__.__name__, e))

        elif command.startswith('get '):
            tokens = re.split('\s+', command)
            key = tokens[1]
            print('get[{}] = {}'.format(key, redis.get(key)))
        elif command.startswith('del '):
            tokens = re.split('\s+', command)
            key = tokens[1]

            try:
                redis.delete(key)
                print('del, {}'.format(key))
            except ReadOnlyError as e:
                print('{}: {}'.format(e.__class__.__name__, e))
```

あとは、情報取得系のコマンドを…。

```python
        if command == 'info master':
            print(sentinel.discover_master('mymaster'))
        elif command == 'info replicas':
            print(sentinel.discover_slaves('mymaster'))


        elif command == 'info':
            print(str(redis.info()))
```

`discover〜`はSentinelにおけるマスター、レプリカの情報で、`info`は接続しているRedisの情報です。

では、実行してみます。

```shell
$ python3 sentinel_client.py
start sentinel client
> 
```

マスター、レプリカの情報。

```shell
> info master
('192.168.33.10', 6379)
> info replicas
[('192.168.33.11', 6379)]
```

マスターに接続。

```shell
> use master
```

set、get。

```shell
Redis<SentinelConnectionPool<service=mymaster(master)>: > set key1 value1
set key1 = value1
Redis<SentinelConnectionPool<service=mymaster(master)>: > get key1
get[key1] = value1
```

レプリカにつなぎなおして、データ取得。

```shell
Redis<SentinelConnectionPool<service=mymaster(master)>: > use replica
Redis<SentinelConnectionPool<service=mymaster(slave)>: > get key1
get[key1] = value1
```

レプリカ側では、データの更新は不可です。

```shell
Redis<SentinelConnectionPool<service=mymaster(slave)>: > set key2 value2
ReadOnlyError: You can't write against a read only replica.
```

ここで、再度マスターに接続。

```shell
Redis<SentinelConnectionPool<service=mymaster(slave)>: > use master
```

マスターのRedisを停止してみます。

```shell
$ sudo systemctl stop redis
```

`redis-cli`で、マスターが切り替わったことを確認します。

```shell
$ redis-cli -h 192.168.33.12 -p 26379
192.168.33.12:26379> info sentinel
# Sentinel
sentinel_masters:1
sentinel_tilt:0
sentinel_running_scripts:0
sentinel_scripts_queue_length:0
sentinel_simulate_failure_flags:0
master0:name=mymaster,status=ok,address=192.168.33.11:6379,slaves=1,sentinels=3
```

この状態で、先程のマスターにつないだコマンドからデータを取得してみます。

```shell
Redis<SentinelConnectionPool<service=mymaster(master)>: > get key1
get[key1] = value1
```

なんか、動きました…。

このRedisの情報を見てみます。

```shell
Redis<SentinelConnectionPool<service=mymaster(master)>: > info
{'redis_version': '5.0.6', 'redis_git_sha1': 0, 'redis_git_dirty': 0, 'redis_build_id': 'c3d7ebb6b1a2844b', 'redis_mode': 'standalone', 'os': 'Linux 3.10.0-957.12.2.el7.x86_64 x86_64', 'arch_bits': 64, 'multiplexing_api': 'epoll', 'atomicvar_api': 'atomic-builtin', 'gcc_version': '4.8.5', 'process_id': 24544, 'run_id': 'ad1e763f073c8fcce092a773f2bbd2c5933d2bf9', 'tcp_port': 6379, 'uptime_in_seconds': 440050, 'uptime_in_days': 5, 'hz': 10, 'configured_hz': 10, 'lru_clock': 9707634, 'executable': '/usr/bin/redis-server', 'config_file': '/etc/redis.conf', 'connected_clients': 7, 'client_recent_max_input_buffer': 2, 'client_recent_max_output_buffer': 0, 'blocked_clients': 0, 'used_memory': 2070088, 'used_memory_human': '1.97M', 'used_memory_rss': 4554752, 'used_memory_rss_human': '4.34M', 'used_memory_peak': 2173256, 'used_memory_peak_human': '2.07M', 'used_memory_peak_perc': '95.25%', 'used_memory_overhead': 2024062, 'used_memory_startup': 791416, 'used_memory_dataset': 46026, 'used_memory_dataset_perc': '3.60%', 'allocator_allocated': 2637720, 'allocator_active': 3035136, 'allocator_resident': 7573504, 'total_system_memory': 510861312, 'total_system_memory_human': '487.20M', 'used_memory_lua': 37888, 'used_memory_lua_human': '37.00K', 'used_memory_scripts': 0, 'used_memory_scripts_human': '0B', 'number_of_cached_scripts': 0, 'maxmemory': 0, 'maxmemory_human': '0B', 'maxmemory_policy': 'noeviction', 'allocator_frag_ratio': 1.15, 'allocator_frag_bytes': 397416, 'allocator_rss_ratio': 2.5, 'allocator_rss_bytes': 4538368, 'rss_overhead_ratio': 0.6, 'rss_overhead_bytes': -3018752, 'mem_fragmentation_ratio': 2.25, 'mem_fragmentation_bytes': 2526672, 'mem_not_counted_for_evict': 0, 'mem_replication_backlog': 1048576, 'mem_clients_slaves': 0, 'mem_clients_normal': 183998, 'mem_aof_buffer': 0, 'mem_allocator': 'jemalloc-5.1.0', 'active_defrag_running': 0, 'lazyfree_pending_objects': 0, 'loading': 0, 'rdb_changes_since_last_save': 0, 'rdb_bgsave_in_progress': 0, 'rdb_last_save_time': 1569988537, 'rdb_last_bgsave_status': 'ok', 'rdb_last_bgsave_time_sec': 0, 'rdb_current_bgsave_time_sec': -1, 'rdb_last_cow_size': 221184, 'aof_enabled': 0, 'aof_rewrite_in_progress': 0, 'aof_rewrite_scheduled': 0, 'aof_last_rewrite_time_sec': -1, 'aof_current_rewrite_time_sec': -1, 'aof_last_bgrewrite_status': 'ok', 'aof_last_write_status': 'ok', 'aof_last_cow_size': 0, 'total_connections_received': 26, 'total_commands_processed': 2737329, 'instantaneous_ops_per_sec': 3, 'total_net_input_bytes': 202595677, 'total_net_output_bytes': 1041974292, 'instantaneous_input_kbps': 0.2, 'instantaneous_output_kbps': 0.54, 'rejected_connections': 0, 'sync_full': 0, 'sync_partial_ok': 0, 'sync_partial_err': 0, 'expired_keys': 0, 'expired_stale_perc': 0.0, 'expired_time_cap_reached_count': 0, 'evicted_keys': 0, 'keyspace_hits': 3, 'keyspace_misses': 2, 'pubsub_channels': 1, 'pubsub_patterns': 0, 'latest_fork_usec': 277, 'migrate_cached_sockets': 0, 'slave_expires_tracked_keys': 0, 'active_defrag_hits': 0, 'active_defrag_misses': 0, 'active_defrag_key_hits': 0, 'active_defrag_key_misses': 0, 'role': 'master', 'connected_slaves': 0, 'master_replid': '56d4bc022b7a089800bfe723201c821f570f2fe4', 'master_replid2': '54810ab6453ffdf12f21ff34157e2e0d655f8a81', 'master_repl_offset': 91797500, 'second_repl_offset': 91786762, 'repl_backlog_active': 1, 'repl_backlog_size': 1048576, 'repl_backlog_first_byte_offset': 90748925, 'repl_backlog_histlen': 1048576, 'used_cpu_sys': 1280.370212, 'used_cpu_user': 41.081586, 'used_cpu_sys_children': 0.004907, 'used_cpu_user_children': 0.0, 'cluster_enabled': 0, 'db0': {'keys': 1, 'expires': 0, 'avg_ttl': 0}}
```

レプリカがいないことになっていますね。

```python
'connected_slaves': 0
```

ということは、接続しているのはフェイルオーバーしたレプリカのようです。

この状態で、レプリカに接続してデータを取得してみると、なんと動きます。

```shell
Redis<SentinelConnectionPool<service=mymaster(master)>: > use replica
Redis<SentinelConnectionPool<service=mymaster(slave)>: > get key1
get[key1] = value1
```

`discover〜`で見てみると、こういう状態です。

```shell
Redis<SentinelConnectionPool<service=mymaster(slave)>: > info master
('192.168.33.11', 6379)
Redis<SentinelConnectionPool<service=mymaster(slave)>: > info replicas
[]
```

となると、この場合はレプリカにつないでも、つなぐべきレプリカがいないのでマスターに接続することになるわけですね。

実際、更新も可能です。

```shell
Redis<SentinelConnectionPool<service=mymaster(slave)>: > use replica
Redis<SentinelConnectionPool<service=mymaster(slave)>: > set key2 value2
set key2 = value2
```

というわけで、マスターを使っている時に接続先がダウンした場合、フェイルオーバーしたマスターに昇格した旧レプリカにつないでくれるということですね（Sentinelが検知するまでの間の挙動にについては、今回は見ていませんが）。

とりあえず、挙動はなんとなくわかった感じです。

# 付録：EPELからインストールした、RedisおよびRedis Sentinelのデフォルト値

EPELからインストールした、`/etc/redis.conf`のデフォルト値。

```shell
$ sudo grep -v '#' /etc/redis.conf | grep -v '^$'
bind 127.0.0.1
protected-mode yes
port 6379
tcp-backlog 511
timeout 0
tcp-keepalive 300
daemonize no
supervised no
pidfile /var/run/redis_6379.pid
loglevel notice
logfile /var/log/redis/redis.log
databases 16
always-show-logo yes
save 900 1
save 300 10
save 60 10000
stop-writes-on-bgsave-error yes
rdbcompression yes
rdbchecksum yes
dbfilename dump.rdb
dir /var/lib/redis
replica-serve-stale-data yes
replica-read-only yes
repl-diskless-sync no
repl-diskless-sync-delay 5
repl-disable-tcp-nodelay no
replica-priority 100
lazyfree-lazy-eviction no
lazyfree-lazy-expire no
lazyfree-lazy-server-del no
replica-lazy-flush no
appendonly no
appendfilename "appendonly.aof"
appendfsync everysec
no-appendfsync-on-rewrite no
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb
aof-load-truncated yes
aof-use-rdb-preamble yes
lua-time-limit 5000
slowlog-log-slower-than 10000
slowlog-max-len 128
latency-monitor-threshold 0
notify-keyspace-events ""
hash-max-ziplist-entries 512
hash-max-ziplist-value 64
list-max-ziplist-size -2
list-compress-depth 0
set-max-intset-entries 512
zset-max-ziplist-entries 128
zset-max-ziplist-value 64
hll-sparse-max-bytes 3000
stream-node-max-bytes 4096
stream-node-max-entries 100
activerehashing yes
client-output-buffer-limit normal 0 0 0
client-output-buffer-limit replica 256mb 64mb 60
client-output-buffer-limit pubsub 32mb 8mb 60
hz 10
dynamic-hz yes
aof-rewrite-incremental-fsync yes
rdb-save-incremental-fsync yes
```

EPELからインストールした、`/etc/redis-sentinel.conf`のデフォルト設定。

```shell
$ sudo grep -v '#' /etc/redis-sentinel.conf | grep -v '^$'
port 26379
daemonize no
pidfile "/var/run/redis-sentinel.pid"
logfile "/var/log/redis/sentinel.log"
dir "/tmp"
sentinel myid 88cdee281108c92337965c782f60653f4f5d00fe
sentinel deny-scripts-reconfig yes
sentinel monitor mymaster 127.0.0.1 6379 2
sentinel config-epoch mymaster 0
sentinel leader-epoch mymaster 0
protected-mode no
supervised systemd
sentinel known-replica mymaster 192.168.33.11 6379
sentinel known-sentinel mymaster 192.168.33.14 26379 4cdd5e38eccc965513db0d8be46b9eb6614418da
sentinel known-sentinel mymaster 192.168.33.13 26379 22249d49a0b530709b7897cbbb88a2db6da6a1b1
sentinel current-epoch 0
```