Redis Sentinelで自動フェイルオーバー

この投稿は自分のブログ記事からRedis Sentinel関連の項目だけ抜粋したものです。追記事項があればブログの方に書いていきます。
__「Redisの監視/分析系ツールまとめ」__ http://rest-term.com/archives/3045/

# [Redis Sentinel](http://redis.io/topics/sentinel)
Redis本家プロジェクトで開発されている、Redisサーバの死活監視/通知および自動フェイルオーバー機能を提供する管理サーバ(redis-sentinel)です。v2.4.16または2.6.0-rc6以降のバージョンから利用可能になりました。公式ドキュメントを参考にしつつ動作確認をします。

- 環境: 
CentOS 5.9 (x86_64), Redis 2.6.10

## 構成
ここでは2つのホストでSlaveを2プロセス、Sentinelを3プロセスの構成で試します。

- Master db0:6379
- Slave db0:6380, db1:6379
- Sentinel db0:26379, db0:23680, db1:26379

## 設定

```py:/etc/redis/sentinel.conf
# port <sentinel-port>
port 26379
# sentinel monitor <master-name> <ip> <redis-port> <quorum>
sentinel monitor mymaster db0 6379 2
# sentinel down-after-milliseconds <master-name> <milliseconds>
sentinel down-after-milliseconds mymaster 5000
# sentinel failover-timeout <master-name> <milliseconds>
sentinel failover-timeout mymaster 900000
# sentinel can-failover <master-name> <yes|no>
sentinel can-failover mymaster yes
# sentinel parallel-syncs <master-name> <numslaves>
sentinel parallel-syncs mymaster 1
```

各設定項目について表にしています。Sentinelによるクラスタ監視はいわゆるQuorumベース投票の方式を採ります。複数のSentinelがMasterを監視していて、その内しきい値数以上のSentinelがMasterのダウンを検知したらフェイルオーバー処理を開始します。

| 設定項目 | 概要 |
|:---|:---|
| monitor | Masterのホストとポートおよび状態が**ODOWN(objectively down)**に移行するための定足数(quorum) |
| down-after-milliseconds	| Master/Slaveのダウン検知後、状態が**SDOWN(subjectively down)**に移行するまでの時間(ms) |
| failover-timeout | フェイルオーバー処理のタイムアウト(ms) |
| can-failover | フェイルオーバー処理が実行可能か(yes/no) |
| parallel-syncs | SlaveをMasterに昇格させた後、いくつのSlaveと同期させるか |

SentinelはMasterとSlave両方の死活監視をしてくれますが、状態がODOWNに遷移するのはMasterのみなので注意してください。can-failover を no にして起動したSentinelプロセスはフェイルオーバー処理自体は行わず(他のSentinelに任せる)、自身はダウン検知と投票処理のみを行います。

## 起動
Sentinel起動前にレプリケーションが動作していることを確認しておきます。

```py:redis-cli
redis db0:6379> info replication
# Replication
role:master
connected_slaves:1
slave0:db0,6380,online
slave1:db1,6379,online
```

続けて3つのSentinelプロセスを立ち上げます。

```py:redis-sentinel
$ redis-sentinel /etc/redis/sentinel.conf
## または redis-server をsentinelモードで起動する
$ redis-server /etc/redis/sentinel.conf --sentinel
```

ログにて各SlaveおよびSentinelと接続したことを確認できます。

```
[19069] 14 May 16:30:31.909 * +slave slave db1:6379 db1 6379 @ mymaster db0 6379
[19069] 14 May 16:30:31.909 * +slave slave db0:6380 db0 6380 @ mymaster db0 6379
[19069] 14 May 16:30:38.320 * +sentinel sentinel db0:26380 db0 26380 @ mymaster db0 6379
[19069] 14 May 16:30:39.655 * +sentinel sentinel db1:26379 db1 26379 @ mymaster db0 6379
```

また、Sentinel起動後はINFOコマンドでSentinel関連の情報を確認ができます。

```py:redis-cli
redis db0:26379> info sentinel
# Sentinel
sentinel_masters:1
sentinel_tilt:0
sentinel_running_scripts:0
sentinel_scripts_queue_length:0
master0:name=mymaster,status=ok,address=db0:6379,slaves=2,sentinels=3
```

Sentinelプロセス自体の監視は一番下の項目を見るようにすれば良いかと思います。

フェイルオーバー処理の動作確認の為、ここでMasterを落とします。

```py:redis-cli
redis db0:6379> shutdown
```

ログにてフェイルオーバー処理の進捗を確認できます。

```
[19069] 14 May 16:30:51.170 # +sdown master mymaster db0 6379
[19069] 14 May 16:30:52.386 # +odown master mymaster db0 6379 #quorum 2/2
[19069] 14 May 16:30:52.386 # +failover-triggered master mymaster db0 6379
[19069] 14 May 16:30:52.386 # +failover-state-wait-start master mymaster db0 6379 #starting in 13910 milliseconds
[19069] 14 May 16:31:06.379 # +failover-state-select-slave master mymaster db0 6379
[19069] 14 May 16:31:06.480 # +selected-slave slave db1:6379 db1 6379 @ mymaster db0 6379
[19069] 14 May 16:31:06.480 * +failover-state-send-slaveof-noone slave db1:6379 db1 6379 @ mymaster db0 6379
[19069] 14 May 16:31:06.583 * +failover-state-wait-promotion slave db1:6379 db1 6379 @ mymaster db0 6379
[19069] 14 May 16:31:06.901 # +promoted-slave slave db1:6379 db1 6379 @ mymaster db0 6379
[19069] 14 May 16:31:06.902 # +failover-state-reconf-slaves master mymaster db0 6379
[19069] 14 May 16:31:06.991 * +slave-reconf-sent slave db0:6380 db0 6380 @ mymaster db0 6379
[19069] 14 May 16:31:07.294 * +slave-reconf-inprog slave db0:6380 db0 6380 @ mymaster db0 6379
[19069] 14 May 16:31:08.316 * +slave-reconf-done slave db0:6380 db0 6380 @ mymaster db0 6379
[19069] 14 May 16:31:08.417 # +failover-end master mymaster db0 6379
[19069] 14 May 16:31:08.417 # +switch-master mymaster db0 6379 db1 6379
[19069] 14 May 16:31:08.548 * +slave slave db0:6380 db0 6380 @ mymaster db1 6379
[19069] 14 May 16:31:09.068 * +sentinel sentinel db0:26380 db0 26380 @ mymaster db1 6379
[19069] 14 May 16:31:12.258 * +sentinel sentinel db1:26379 db1 26379 @ mymaster db1 6379
```

SentinelがMasterのダウンを検知すると状態がSDOWNに、quorumパラメータで指定した数のSDOWNが揃うと次はODOWNへと遷移、その後フェイルオーバー処理が開始されます。

最後に新しいMasterとSlaveで正常稼働していることを確認します。

```py:redis-cli
redis db1:6379> info replication
# Replication
role:master
connected_slaves:1
slave0:db0,6380,online
```

以上、Sentinelによるフェイルオーバー機能の動作確認まで試してみました。

## Sentinel API
SentinelはいくつかのAPIを提供しているので簡単に紹介したいと思います。

```py:redis-cli
## SENTINEL masters
# 監視対象のMasterに関する情報を確認
redis 127.0.0.1:26379> sentinel masters
1)  1) "name"
    2) "mymaster"
    3) "ip"
    4) "127.0.0.1"
    5) "port"
    6) "6379"
    7) "runid"
    8) "b3b31e6c6d1fbb0cec5d179fd666ce00ea103746"
    9) "flags"
   10) "master"
   11) "pending-commands"
   12) "0"
   13) "last-ok-ping-reply"
   14) "568"
   15) "last-ping-reply"
   16) "568"
   17) "info-refresh"
   18) "9745"
   19) "num-slaves"
   20) "1"
   21) "num-other-sentinels"
   22) "1"
   23) "quorum"
   24) "2"

## SENTINEL slaves <master name>
# Slaveに関する情報を確認
redis 127.0.0.1:26379> sentinel slaves mymaster
1)  1) "name"
    2) "127.0.0.1:6380"
    3) "ip"
    4) "127.0.0.1"
    5) "port"
    6) "6380"
    7) "runid"
    8) "17c0f7e7e4d2af0f5f6140ea0ceb0d82d0010aed"
    9) "flags"
   10) "s_down,slave,disconnected"
   11) "pending-commands"
   12) "0"
   13) "last-ok-ping-reply"
   14) "709377"
   15) "last-ping-reply"
   16) "709377"
   17) "s-down-time"
   18) "704333"
   19) "info-refresh"
   20) "712808"
   21) "master-link-down-time"
   22) "0"
   23) "master-link-status"
   24) "ok"
   25) "master-host"
   26) "127.0.0.1"
   27) "master-port"
   28) "6379"
   29) "slave-priority"
   30) "100"

## SENTINEL is-master-down-by-addr <ip> <port>
# Masterの死活確認
redis 127.0.0.1:26379> sentinel is-master-down-by-addr 127.0.0.1 6379
1) (integer) 0  ## 0:UP, 1:DOWN
2) "397dec99760d5d30043941fe4c1c35ba99e99ebf"  ## Sentinel(subjective leader)のrun_id

## SENTINEL get-master-addr-by-name <master name>
# MasterのIP, Portを確認
redis 127.0.0.1:26379> sentinel get-master-addr-by-name mymaster
1) "127.0.0.1"
2) "6379"

## SENTINEL reset
# Sentinelの現在の状態をリセット(フェイルオーバー処理中であっても)
redis 127.0.0.1:26379> sentinel reset mymaster
(integer) 1
```

Sentinel関連の管理ツールを作る際にはこのSentinel APIをサポートしたクライアントライブラリを使うと楽かと思います。

以下、フェイルオーバー処理の挙動に関する部分をピックアップして紹介します。

## 昇格させるSlaveの選択
当然ですがSlaveとして正常に稼働しているプロセスが対象となります。細かい条件を省いてざっくり言うと、Masterとの接続が安定していて(ダウンタイムが少ない)、SentinelからのPING/INFOコマンドに直近5000ms以内に応答しているSlaveが対象になります(詳細は公式ドキュメントと src/sentinel.c を参照)。

対象となるSlaveが複数ある場合は **slave_priority** の値が小さいSlaveを選択します。

```py:redis-cli
redis db0:6380> info replication
# Replication
role:slave
master_host:127.0.0.1
master_port:6379
master_link_status:up
master_last_io_seconds_ago:0
master_sync_in_progress:0
slave_priority:100  ## <- この値
slave_read_only:1
connected_slaves:0
```

同じ slave_priority のSlaveが存在する場合は **run_id** が小さいSlaveを選択します。

```py:redis-cli
redis db0:6380> info
# Server
redis_version:2.6.10
redis_git_sha1:00000000
redis_git_dirty:0
redis_mode:standalone
os:Linux 2.6.18-194.26.1.el5 x86_64
arch_bits:64
multiplexing_api:epoll
gcc_version:4.1.2
process_id:17468
run_id:17c0f7e7e4d2af0f5f6140ea0ceb0d82d0010aed  ## <- この値
tcp_port:6380
uptime_in_seconds:1738
uptime_in_days:0
lru_clock:538965
```

2013/06現在は slave_priority の変更を行うAPIは公開されていないようですので、単に run_id が若いSlaveが選択されることになります。Slave選択のアルゴリズムは今後変更されることもあるでしょう。

## Sentinels/Slavesの自動検出
Sentinelの設定にSlaveのリストを持たせる必要がないのは、SentinelがPub/Subによる情報共有を行っているからです。途中で別のSentinelやSlaveをオンラインで追加しても他のSentinelにその情報は伝わります。

```py:redis-cli
## Sentinelは "__sentinel__:hello" チャンネルを利用している
redis db0:6379> subscribe __sentinel__:hello
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "__sentinel__:hello"
3) (integer) 1
1) "message"
2) "__sentinel__:hello"
3) "127.0.0.1:26380:77887a367e1c76ee9dc15f67a2fb3dc49bb00bf4:1"
1) "message"
2) "__sentinel__:hello"
3) "127.0.0.1:26379:3a92da7269b69fa1dcce5915068c4af6e8caf161:1"
1) "message"
2) "__sentinel__:hello"
3) "127.0.0.1:26380:77887a367e1c76ee9dc15f67a2fb3dc49bb00bf4:1"
1) "message"
2) "__sentinel__:hello"
3) "127.0.0.1:26379:3a92da7269b69fa1dcce5915068c4af6e8caf161:1"
```

メッセージの内容は **host:port:run_id:can-failover** となっていて、\_\_sentinel\_\_:hello チャンネルに5秒毎にpublish、全てのSentinelはこのチャンネルをsubscribeして情報共有しています。

## レプリケーションの注意点
RedisのレプリケーションはMySQLなどのRDBMSのレプリケーションとは異なる部分が多いです。Redisはレプリケーション開始時に全てのデータをディスクに書き出してからSlaveに転送するのでI/Oやネットワーク帯域には注意してください。ただし、Redis v2.8ではPSYNC(Partial Resynchronization)が実装されるとのことなのでこの問題は解消されると思われます。

引き続きSentinelの検証を続けていきたいと思います。