TL;DR
-
redis-py
はRedis Sentinelに対応したモジュールがあるので、そちらを使ってRedis Sentinelにアクセスが可能 - Redis Sentinel経由で取得したRedisへの接続はコネクションプールであり、通常のRedisへの操作と同様に利用可能
- マスターがダウンした際に使っていた接続(コネクションプール)は、フェイルオーバーに追従してくれる
環境
今回の環境は、こちら。
$ 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をインストールします。
$ 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
のデフォルト設定から以下のように変更。
※デフォルト設定の全体は、最後に記載します
bind 0.0.0.0
レプリカ側は、/etc/redis.conf
のデフォルト設定から以下のように変更。
bind 0.0.0.0
replicaof 192.168.33.10 6379
Redisのマスター、レプリカをそれぞれ起動して
$ sudo systemctl start redis
レプリケーションが動作していることを確認。
マスター側。
$ 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
レプリカ側。
$ 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
のデフォルト設定から、以下のように変更します。
※デフォルト設定の全体は、最後に記載します
sentinel monitor mymaster 192.168.33.10 6379 2
Redis Sentinel起動(3台で行います)。
$ sudo systemctl start redis-sentinel
確認。
$ 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
からアクセスしてみます。まずはインストール。
$ pip3 install redis
今回のredis-py
のバージョンです。
$ pip3 freeze
redis==3.3.8
redis-py
からRedis Sentinelへのアクセスですが、以下を参考に
コマンドラインプログラムを書いてみました。
sentinel_client.py
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インスタンスを作成することで行います。
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
で行います。
redis = sentinel.master_for('mymaster')
redis = sentinel.slave_for('mymaster')
各メソッドの戻り値は、SentinelConnectionPool
のインスタンスです。
あとは、SentinelConnectionPool
に対してRedisコマンドを実行すればOKです。
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))
あとは、情報取得系のコマンドを…。
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の情報です。
では、実行してみます。
$ python3 sentinel_client.py
start sentinel client
>
マスター、レプリカの情報。
> info master
('192.168.33.10', 6379)
> info replicas
[('192.168.33.11', 6379)]
マスターに接続。
> use master
set、get。
Redis<SentinelConnectionPool<service=mymaster(master)>: > set key1 value1
set key1 = value1
Redis<SentinelConnectionPool<service=mymaster(master)>: > get key1
get[key1] = value1
レプリカにつなぎなおして、データ取得。
Redis<SentinelConnectionPool<service=mymaster(master)>: > use replica
Redis<SentinelConnectionPool<service=mymaster(slave)>: > get key1
get[key1] = value1
レプリカ側では、データの更新は不可です。
Redis<SentinelConnectionPool<service=mymaster(slave)>: > set key2 value2
ReadOnlyError: You can't write against a read only replica.
ここで、再度マスターに接続。
Redis<SentinelConnectionPool<service=mymaster(slave)>: > use master
マスターのRedisを停止してみます。
$ sudo systemctl stop redis
redis-cli
で、マスターが切り替わったことを確認します。
$ 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
この状態で、先程のマスターにつないだコマンドからデータを取得してみます。
Redis<SentinelConnectionPool<service=mymaster(master)>: > get key1
get[key1] = value1
なんか、動きました…。
このRedisの情報を見てみます。
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}}
レプリカがいないことになっていますね。
'connected_slaves': 0
ということは、接続しているのはフェイルオーバーしたレプリカのようです。
この状態で、レプリカに接続してデータを取得してみると、なんと動きます。
Redis<SentinelConnectionPool<service=mymaster(master)>: > use replica
Redis<SentinelConnectionPool<service=mymaster(slave)>: > get key1
get[key1] = value1
discover〜
で見てみると、こういう状態です。
Redis<SentinelConnectionPool<service=mymaster(slave)>: > info master
('192.168.33.11', 6379)
Redis<SentinelConnectionPool<service=mymaster(slave)>: > info replicas
[]
となると、この場合はレプリカにつないでも、つなぐべきレプリカがいないのでマスターに接続することになるわけですね。
実際、更新も可能です。
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
のデフォルト値。
$ 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
のデフォルト設定。
$ 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