[TOC]
Redis Sentinel
Redis Sentinel 是用于監控Redis集群中Master狀態的工具 , Sentinel 只在 Server 端做主從切換 , 客戶端需要額外的開發 , 所以我們的介紹 , 會分為Redis服務端 , 和 客戶端兩部分.
Sentinel的作用 :
1.Master的狀態檢測
2.如果Master出現異常 , 則會進行Master-Slave切換, 將其中一個Slave作為Master , 而將之前的Master實例切換為Slave .
3.Master-Slave切換后 , master_redis.conf , slave_redis.conf 和 sentinel.conf的內容都會發生改變 , 即 master_redis.conf中會多出一行slaveof的配置 , sentinel.conf的監控目標會隨著變更
服務端工作方式:
Sentinel 對于不可用有兩種定義
主觀不可用(SDOWN) : SDOWN 是單個Sentinel實例檢測到redis實例的狀態 , 自己主觀上判斷為不可用
客觀不可用(ODOWN) : ODOWN 需要Sentinel集群內一定數量(配置文件)的實例達成一致 , 才會認為Master節點不可用 , 然后執行failover策略 .
1.每個Sentinel實例以每秒一次的頻率向自己所監控的Master,Slave以及其他Sentinel實例發送一個PING命令
2.如果一個實例距離最后一次有效回復PING命令的時間超過 down-after-millisenconds配置所指定的值 , 則這個實例會被Sentinel標記為主觀不可用(SDOWN) .
3.如果一個Master實例被標記為主觀不可用 , 則Sentinel 集群會進行投票 , 通過SENTINEL is_master_down_by_addr 命令 來獲得其他Sentinel對Master的檢測結果 , 如果超過指定數量的Sentinel認為該實例不可用 , 則Master會被標記為客觀不可用(ODOWN) .
4.從SDOWN切換到ODOWN狀態 , 不需要使用一致性算法 , 只使用gossip協議.
5.ODOWN狀態只適用于Master節點 , Slave節點和Sentinel節點不會存在ODOWN狀態 , 也不需要進行投票
客戶端工作方式:
對于客戶端的工作方式 , 我們以分析Java版本客戶端 Jedis的源碼為主。
構造器 :
public JedisSentinelPool(String masterName, Set<String> sentinels,
final GenericObjectPoolConfig poolConfig,
final int connectionTimeout, final int soTimeout,
final String password,
final int database,
final String clientName) {
this.poolConfig = poolConfig;
this.connectionTimeout = connectionTimeout;
this.soTimeout = soTimeout;
this.password = password;
this.database = database;
this.clientName = clientName;
//初始化 Sentinels
HostAndPort master = initSentinels(sentinels, masterName);
// 初始化連接池
initPool(master);
}
初始化方法 (initSentinels) :
private HostAndPort initSentinels(Set<String> sentinels, final String masterName) {
HostAndPort master = null;
boolean sentinelAvailable = false;
log.info("Trying to find master from available Sentinels...");
// 遍歷Sentinel集群
for (String sentinel : sentinels) {
// 解析 Sentinel 地址
final HostAndPort hap = toHostAndPort(Arrays.asList(sentinel.split(":")));
log.fine("Connecting to Sentinel " + hap);
Jedis jedis = null;
try {
// 創建Sentinel 連接
jedis = new Jedis(hap.getHost(), hap.getPort());
// 根據MasterName獲取Master地址 , 返回一個集合 , 下標 0 是地址 , 下標 1 是端口
List<String> masterAddr = jedis.sentinelGetMasterAddrByName(masterName);
// connected to sentinel...
sentinelAvailable = true;
if (masterAddr == null || masterAddr.size() != 2) {
log.warning("Can not get master addr, master name: " + masterName + ". Sentinel: " + hap + ".");
continue;
}
// 實例化地址
master = toHostAndPort(masterAddr);
log.fine("Found Redis master at " + master);
// 如果在任何一個Sentinel中找到了master , 跳出循環
break;
} catch (JedisException e) {
// resolves #1036, it should handle JedisException there's another chance
// of raising JedisDataException
log.warning("Cannot get master address from sentinel running @ " + hap + ". Reason: " + e + ". Trying next one.");
} finally {
if (jedis != null) {
jedis.close();
}
}
}
if (master == null) {
if (sentinelAvailable) {
// can connect to sentinel, but master name seems to not
// monitored
throw new JedisException("Can connect to sentinel, but " + masterName + " seems to be not monitored...");
} else {
throw new JedisConnectionException("All sentinels down, cannot determine where is " + masterName + " master is running...");
}
}
log.info("Redis master running at " + master + ", starting Sentinel listeners...");
// 遍歷Sentinel集群地址 , 針對每一個實例 , 啟動一個監聽器
for (String sentinel : sentinels) {
final HostAndPort hap = toHostAndPort(Arrays.asList(sentinel.split(":")));
MasterListener masterListener = new MasterListener(masterName, hap.getHost(), hap.getPort());
// whether MasterListener threads are alive or not, process can be stopped
masterListener.setDaemon(true);
masterListeners.add(masterListener);
masterListener.start();
}
return master;
}
在initSentinels方法中 , 遍歷Sentinel集群 , 并通過與Jedis綁定的客戶端 , 發送一個 get-master-addr-by-name命令 ,來詢問master節點的地址 。 直到找到master節點的地址 , 或者確認不存在指定名字的master節點。
在這段代碼的最后 , 我們可以看到針對每一個Sentinel實例 都啟動了一個監聽器 , 我們來分析一下 MasterListener做了什么。
MasterListener
protected class MasterListener extends Thread {
protected String masterName;
protected String host;
protected int port;
protected long subscribeRetryWaitTimeMillis = 5000;
// 因監聽器可能被多個線程訪問 , 所以jedis對象被修飾為**可見的** ,
// 即一個線程修改了Jedis實例 , 其他的線程也可以得到最新的實例
protected volatile Jedis j;
protected AtomicBoolean running = new AtomicBoolean(false);
protected MasterListener() { }
public MasterListener(String masterName, String host, int port) {
super(String.format("MasterListener-%s-[%s:%d]", masterName, host, port));
this.masterName = masterName;
this.host = host;
this.port = port;
}
public MasterListener(String masterName, String host, int port, long subscribeRetryWaitTimeMillis) {
this(masterName, host, port);
this.subscribeRetryWaitTimeMillis = subscribeRetryWaitTimeMillis;
}
public void run() {
running.set(true);
while (running.get()) {
j = new Jedis(host, port);
try {
// double check that it is not being shutdown
if (!running.get()) {
break;
}
// 訂閱 channelName 為 "+switch-master" 的消息
j.subscribe(new JedisPubSub() {
@Override
public void onMessage(String channel, String message) {
log.fine("Sentinel " + host + ":" + port + " published: " + message + ".");
String[] switchMasterMsg = message.split(" ");
if (switchMasterMsg.length > 3) {
if (masterName.equals(switchMasterMsg[0])) {
// 初始連接池
initPool(toHostAndPort(Arrays.asList(switchMasterMsg[3], switchMasterMsg[4])));
} else {
log.fine("Ignoring message on +switch-master for master name "+ switchMasterMsg[0] + ", our master name is " + masterName);
}
} else {
log.severe("Invalid message received on Sentinel " + host + ":" + port + " on channel +switch-master: " + message);
}
}
//channelName
}, "+switch-master");
} catch (JedisConnectionException e) {
if (running.get()) {
log.log(Level.SEVERE, "Lost connection to Sentinel at " + host + ":" + port + ". Sleeping 5000ms and retrying.",e);
try {
Thread.sleep(subscribeRetryWaitTimeMillis);
} catch (InterruptedException e1) {
log.log(Level.SEVERE, "Sleep interrupted: ", e1);
}
} else {
log.fine("Unsubscribing from Sentinel at " + host + ":" + port);
}
} finally {
j.close();
}
}
}
protected volatile Jedis j;
protected AtomicBoolean running = new AtomicBoolean(false);
從成員變量中我們可以看到 為了保障線程安全 , 代碼中使用了volatile(可見性關鍵字) , 和布爾的原子變量 , 我會在另外的文章里 , 描述他們在使用上的區別 。
在 MasterListener 這個監聽器里 , 我們可以看到這里訂閱了一個名為 "+switch-master" 的事件 , 當得到這個事件的時候,調用 initPool 方法 , 用來更新Master節點的地址 , 并且初始化連接池 。