package com.sohu.cache.redis; import com.sohu.cache.entity.InstanceInfo; import com.sohu.cache.redis.ReshardProcess.ReshardStatusEnum; import com.sohu.cache.util.IdempotentConfirmer; import org.apache.commons.lang.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import redis.clients.jedis.*; import redis.clients.jedis.exceptions.JedisException; import java.util.*; import java.util.concurrent.TimeUnit; /** * 水平扩容重构 * @author leifu * @Date 2016年12月7日 * @Time 上午10:13:00 */ public class RedisClusterReshard { private final Logger logger = LoggerFactory.getLogger(this.getClass()); /** * migrate超时时间 */ private int migrateTimeout = 10000; /** * 普通jedis操作超时时间 */ private int defaultTimeout = Protocol.DEFAULT_TIMEOUT * 5; /** * 每次迁移key个数 */ private int migrateBatch = 10; /** * 所有有效节点 */ private Set<HostAndPort> hosts; /** * redis操作封装 */ private RedisCenter redisCenter; /** * 迁移状态 */ private final ReshardProcess reshardProcess = new ReshardProcess(); public RedisClusterReshard(Set<HostAndPort> hosts, RedisCenter redisCenter) { this.hosts = hosts; this.redisCenter = redisCenter; } /** * 加入主从分片 */ public boolean joinCluster(long appId, String masterHost, int masterPort, final String slaveHost, final int slavePort) { //1. 确认主从节点是否正常 final Jedis masterJedis = redisCenter.getJedis(appId, masterHost, masterPort, defaultTimeout, defaultTimeout); boolean isRun = redisCenter.isRun(appId, masterHost, masterPort); if (!isRun) { logger.error(String.format("joinCluster: master host=%s,port=%s is not run", masterHost, masterPort)); return false; } boolean hasSlave = StringUtils.isNotBlank(slaveHost) && slavePort > 0; final Jedis slaveJedis = hasSlave ? redisCenter.getJedis(appId, slaveHost, slavePort, defaultTimeout, defaultTimeout) : null; if (hasSlave) { isRun = redisCenter.isRun(appId, slaveHost, slavePort); if (!isRun) { logger.error(String.format("joinCluster: slave host=%s,port=%s is not run", slaveHost, slavePort)); return false; } } //2. 对主从节点进行meet操作 //获取所有主节点 List<HostAndPort> masterHostAndPostList = getMasterNodeList(appId); //meet master boolean isClusterMeet = clusterMeet(appId, masterHostAndPostList, masterHost, masterPort); if (!isClusterMeet) { logger.error("master isClusterMeet failed {}:{}", masterHost, masterPort); return false; } if (hasSlave) { isClusterMeet = clusterMeet(appId, masterHostAndPostList, slaveHost, slavePort); if (!isClusterMeet) { logger.error("slave isClusterMeet failed {}:{}", slaveHost, slavePort); return false; } } //3.复制 if (hasSlave) { final String masterNodeId = getNodeId(appId, masterJedis); if (masterNodeId == null) { logger.error(String.format("joinCluster:host=%s,port=%s nodeId is null", masterHost, masterPort)); return false; } return new IdempotentConfirmer() { @Override public boolean execute() { try { //等待广播节点 TimeUnit.SECONDS.sleep(2); } catch (Exception e) { logger.error(e.getMessage(), e); } String response = slaveJedis.clusterReplicate(masterNodeId); logger.info("clusterReplicate-{}:{}={}", slaveHost, slavePort, response); return response != null && response.equalsIgnoreCase("OK"); } }.run(); } else { return true; } } /** * 节点meet * @param masterHostAndPostList * @param host * @param port * @return */ private boolean clusterMeet(long appId, List<HostAndPort> masterHostAndPostList, final String host, final int port) { boolean isSingleNode = redisCenter.isSingleClusterNode(appId, host, port); if (!isSingleNode) { logger.error("{}:{} isNotSingleNode", host, port); return false; } else { logger.warn("{}:{} isSingleNode", host, port); } for (HostAndPort hostAndPort : masterHostAndPostList) { String clusterHost = hostAndPort.getHost(); int clusterPort = hostAndPort.getPort(); final Jedis jedis = redisCenter.getJedis(appId, clusterHost, clusterPort, defaultTimeout, defaultTimeout); try { boolean isClusterMeet = new IdempotentConfirmer() { @Override public boolean execute() { //将新节点添加到集群当中,成为集群中已知新节点 String meet = jedis.clusterMeet(host, port); return meet != null && meet.equalsIgnoreCase("OK"); } }.run(); if (isClusterMeet) { return true; } } catch (Exception e) { logger.error(e.getMessage(), e); } finally { if (jedis != null) jedis.close(); } } return false; } /** * 将source中的startSlot到endSlot迁移到target * */ public boolean migrateSlot(long appId, InstanceInfo sourceInstanceInfo, InstanceInfo targetInstanceInfo, int startSlot, int endSlot, boolean isPipelineMigrate) { long startTime = System.currentTimeMillis(); //上线类型 reshardProcess.setType(0); //迁移的总slot个数 reshardProcess.setTotalSlot(endSlot - startSlot + 1); //源和目标Jedis Jedis sourceJedis = redisCenter.getJedis(appId, sourceInstanceInfo.getIp(), sourceInstanceInfo.getPort(), defaultTimeout, defaultTimeout); Jedis targetJedis = redisCenter.getJedis(appId, targetInstanceInfo.getIp(), targetInstanceInfo.getPort(), defaultTimeout, defaultTimeout); //逐个slot迁移 boolean hasError = false; for (int slot = startSlot; slot <= endSlot; slot++) { long slotStartTime = System.currentTimeMillis(); try { //num是迁移key的总数 int num = migrateSlotData(appId, sourceJedis, targetJedis, slot, isPipelineMigrate); reshardProcess.addReshardSlot(slot, num); logger.warn("clusterReshard:{}->{}, slot={}, keys={}, costTime={} ms", sourceInstanceInfo.getHostPort(), targetInstanceInfo.getHostPort(), slot, num, (System.currentTimeMillis() - slotStartTime)); } catch (Exception e) { logger.error(e.getMessage(), e); hasError = true; break; } } if (reshardProcess.getStatus() != ReshardStatusEnum.ERROR.getValue()) { reshardProcess.setStatus(ReshardStatusEnum.FINISH.getValue()); } long endTime = System.currentTimeMillis(); logger.warn("clusterReshard:{}->{}, slot:{}->{}, costTime={} ms", sourceInstanceInfo.getHostPort(), targetInstanceInfo.getHostPort(), startSlot, endSlot, (endTime - startTime)); if (hasError) { reshardProcess.setStatus(ReshardStatusEnum.ERROR.getValue()); return false; } else { reshardProcess.setStatus(ReshardStatusEnum.FINISH.getValue()); return true; } } /** * 迁移slot数据,并稳定slot配置 * @throws Exception */ private int moveSlotData(final long appId, final Jedis source, final Jedis target, final int slot, boolean isPipelineMigrate) throws Exception { int num = 0; while (true) { final Set<String> keys = new HashSet<String>(); boolean isGetKeysInSlot = new IdempotentConfirmer() { @Override public boolean execute() { List<String> perKeys = source.clusterGetKeysInSlot(slot, migrateBatch); if (perKeys != null && perKeys.size() > 0) { keys.addAll(perKeys); } return true; } }.run(); if (!isGetKeysInSlot) { throw new RuntimeException(String.format("get keys failed slot=%d num=%d", slot, num)); } if (keys.isEmpty()) { break; } for (final String key : keys) { boolean isKeyMigrate = new IdempotentConfirmer() { // 失败后,迁移时限加倍 private int migrateTimeOutFactor = 1; @Override public boolean execute() { String response = source.migrate(target.getClient().getHost(), target.getClient().getPort(), key, 0, migrateTimeout * (migrateTimeOutFactor++)); return response != null && (response.equalsIgnoreCase("OK") || response.equalsIgnoreCase("NOKEY")); } }.run(); if (!isKeyMigrate) { throw new RuntimeException("migrate key=" + key + failedInfo(source, slot)); } else { num++; logger.info("migrate key={};response=OK", key); } } } final String targetNodeId = getNodeId(appId, target); boolean isClusterSetSlotNode; //设置 slot新归属节点 isClusterSetSlotNode = new IdempotentConfirmer() { @Override public boolean execute() { boolean isOk = false; List<HostAndPort> masterNodesList = getMasterNodeList(appId); for (HostAndPort hostAndPort : masterNodesList) { Jedis jedis = null; try { jedis = redisCenter.getJedis(appId, hostAndPort.getHost(), hostAndPort.getPort()); String response = jedis.clusterSetSlotNode(slot, targetNodeId); isOk = response != null && response.equalsIgnoreCase("OK"); if (!isOk) { logger.error("clusterSetSlotNode-{}={}", getNodeId(appId, target), response); break; } } catch (Exception e) { logger.error(e.getMessage(), e); } finally { if (jedis != null) jedis.close(); } } return isOk; } }.run(); if (!isClusterSetSlotNode) { throw new RuntimeException("clusterSetSlotNode:" + failedInfo(target, slot)); } return num; } /** * 指派迁移节点数据 * CLUSTER SETSLOT <slot> IMPORTING <node_id> 从 node_id 指定的节点中导入槽 slot 到本节点。 * CLUSTER SETSLOT <slot> MIGRATING <node_id> 将本节点的槽 slot 迁移到 node_id 指定的节点中。 * CLUSTER GETKEYSINSLOT <slot> <count> 返回 count 个 slot 槽中的键。 * MIGRATE host port key destination-db timeout [COPY] [REPLACE] * CLUSTER SETSLOT <slot> NODE <node_id> 将槽 slot 指派给 node_id 指定的节点,如果槽已经指派给另一个节点,那么先让另一个节点删除该槽>,然后再进行指派。 */ private int migrateSlotData(long appId, final Jedis source, final Jedis target, final int slot, boolean isPipelineMigrate) { int num = 0; final String sourceNodeId = getNodeId(appId, source); final String targetNodeId = getNodeId(appId, target); boolean isError = false; if (sourceNodeId == null || targetNodeId == null) { throw new JedisException(String.format("sourceNodeId = %s || targetNodeId = %s", sourceNodeId, targetNodeId)); } boolean isImport = new IdempotentConfirmer() { @Override public boolean execute() { String importing = target.clusterSetSlotImporting(slot, sourceNodeId); logger.info("slot={},clusterSetSlotImporting={}", slot, importing); return importing != null && importing.equalsIgnoreCase("OK"); } }.run(); if (!isImport) { isError = true; logger.error("clusterSetSlotImporting" + failedInfo(target, slot)); } boolean isMigrate = new IdempotentConfirmer() { @Override public boolean execute() { String migrating = source.clusterSetSlotMigrating(slot, targetNodeId); logger.info("slot={},clusterSetSlotMigrating={}", slot, migrating); return migrating != null && migrating.equalsIgnoreCase("OK"); } }.run(); if (!isMigrate) { isError = true; logger.error("clusterSetSlotMigrating" + failedInfo(source, slot)); } try { num = moveSlotData(appId, source, target, slot, isPipelineMigrate); } catch (Exception e) { isError = true; logger.error(e.getMessage(), e); } if (!isError) { return num; } else { String errorMessage = "source=%s target=%s slot=%d num=%d reShard failed"; throw new RuntimeException(String.format(errorMessage, getNodeKey(source), getNodeKey(target), slot, num)); } } private String failedInfo(Jedis jedis, int slot) { return String.format(" failed %s:%d slot=%d", jedis.getClient().getHost(), jedis.getClient().getPort(), slot); } /** * 获取所有主节点 * @return */ private List<HostAndPort> getMasterNodeList(long appId) { List<HostAndPort> masterNodeList = new ArrayList<HostAndPort>(); //获取RedisCluster所有节点 JedisCluster jedisCluster = new JedisCluster(hosts, defaultTimeout); Collection<JedisPool> allNodes = jedisCluster.getConnectionHandler().getNodes().values(); try { for (JedisPool jedisPool : allNodes) { String host = jedisPool.getHost(); int port = jedisPool.getPort(); if (!redisCenter.isMaster(appId, host, port)) { continue; } masterNodeList.add(new HostAndPort(host, port)); } } finally { jedisCluster.close(); } return masterNodeList; } private final Map<String, String> nodeIdCachedMap = new HashMap<String, String>(); public String getNodeId(final long appId, final Jedis jedis) { String nodeKey = getNodeKey(jedis); if (nodeIdCachedMap.get(nodeKey) != null) { return nodeIdCachedMap.get(nodeKey); } else { String nodeId = redisCenter.getNodeId(appId, jedis.getClient().getHost(), jedis.getClient().getPort()); nodeIdCachedMap.put(nodeKey, nodeId); return nodeId; } } protected String getNodeKey(Jedis jedis) { return jedis.getClient().getHost() + ":" + jedis.getClient().getPort(); } public void setMigrateTimeout(int migrateTimeout) { this.migrateTimeout = migrateTimeout; } public void setDefaultTimeout(int defaultTimeout) { this.defaultTimeout = defaultTimeout; } public ReshardProcess getReshardProcess() { return reshardProcess; } public RedisCenter getRedisCenter() { return redisCenter; } public void setRedisCenter(RedisCenter redisCenter) { this.redisCenter = redisCenter; } }