package com.sohu.cache.stats.app.impl; import java.io.BufferedWriter; import java.io.File; import java.io.IOException; import java.nio.charset.Charset; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.text.SimpleDateFormat; import java.util.Arrays; import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import org.apache.commons.collections.CollectionUtils; import org.apache.commons.collections.MapUtils; import org.apache.commons.lang.StringUtils; import org.apache.commons.lang.math.NumberUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import redis.clients.jedis.Jedis; import com.sohu.cache.constant.AppDataMigrateEnum; import com.sohu.cache.constant.AppDataMigrateResult; import com.sohu.cache.constant.AppDataMigrateStatusEnum; import com.sohu.cache.constant.CommandResult; import com.sohu.cache.constant.ErrorMessageEnum; import com.sohu.cache.constant.RedisMigrateToolConstant; import com.sohu.cache.dao.AppDataMigrateStatusDao; import com.sohu.cache.entity.AppDataMigrateSearch; import com.sohu.cache.entity.AppDataMigrateStatus; import com.sohu.cache.entity.MachineInfo; import com.sohu.cache.exception.SSHException; import com.sohu.cache.machine.MachineCenter; import com.sohu.cache.protocol.MachineProtocol; import com.sohu.cache.redis.RedisCenter; import com.sohu.cache.ssh.SSHUtil; import com.sohu.cache.stats.app.AppDataMigrateCenter; import com.sohu.cache.util.ConstUtils; import com.sohu.cache.web.service.AppService; /** * 数据迁移(使用唯品会的开源工具redis-migrate-tool进行迁移) * * @author leifu * @Date 2016-6-8 * @Time 下午2:54:33 */ public class AppDataMigrateCenterImpl implements AppDataMigrateCenter { private Logger logger = LoggerFactory.getLogger(AppDataMigrateCenterImpl.class); private AppService appService; private RedisCenter redisCenter; private MachineCenter machineCenter; private AppDataMigrateStatusDao appDataMigrateStatusDao; @Override public AppDataMigrateResult check(String migrateMachineIp, AppDataMigrateEnum sourceRedisMigrateEnum, String sourceServers, AppDataMigrateEnum targetRedisMigrateEnum, String targetServers, String redisSourcePass, String redisTargetPass) { // 1. 检查migrateMachineIp是否安装 AppDataMigrateResult migrateMachineResult = checkMigrateMachine(migrateMachineIp); if (!migrateMachineResult.isSuccess()) { return migrateMachineResult; } // 2. 检查源配置 AppDataMigrateResult sourceResult = checkMigrateConfig(migrateMachineIp, sourceRedisMigrateEnum, sourceServers, redisSourcePass, true); if (!sourceResult.isSuccess()) { return sourceResult; } // 3. 检查目标 AppDataMigrateResult targetResult = checkMigrateConfig(migrateMachineIp, targetRedisMigrateEnum, targetServers, redisTargetPass, false); if (!targetResult.isSuccess()) { return targetResult; } return AppDataMigrateResult.success(); } /** * 检查迁移的机器是否正常 * * @param migrateMachineIp * @return */ private AppDataMigrateResult checkMigrateMachine(String migrateMachineIp) { if (StringUtils.isBlank(migrateMachineIp)) { return AppDataMigrateResult.fail("redis-migrate-tool所在机器的IP不能为空"); } // 1. 检查机器是否存在在机器列表中 try { MachineInfo machineInfo = machineCenter.getMachineInfoByIp(migrateMachineIp); if (machineInfo == null) { return AppDataMigrateResult.fail(migrateMachineIp + "没有在机器管理列表中"); } else if (machineInfo.isOffline()) { return AppDataMigrateResult.fail(migrateMachineIp + ",该机器已经被删除"); } } catch (Exception e) { logger.error(e.getMessage(), e); return AppDataMigrateResult.fail("检测发生异常,请观察日志"); } // 2. 检查是否安装redis-migrate-tool try { String cmd = ConstUtils.getRedisMigrateToolCmd(); String response = SSHUtil.execute(migrateMachineIp, cmd); if (StringUtils.isBlank(response) || !response.contains("source") || !response.contains("target")) { return AppDataMigrateResult.fail(migrateMachineIp + "下," + cmd + "执行失败,请确保redis-migrate-tool安装正确!"); } } catch (Exception e) { logger.error(e.getMessage(), e); return AppDataMigrateResult.fail("检测发生异常,请观察日志"); } // 3. 检查是否有运行的redis-migrate-tool // 3.1 从数据库里检测,每次迁移记录迁移的详情,状态不太好控制,暂时去掉 // try { // int count = appDataMigrateStatusDao.getMigrateMachineStatCount(migrateMachineIp, AppDataMigrateStatusEnum.START.getStatus()); // if (count > 0) { // return AppDataMigrateResult.fail(migrateMachineIp + "下有redis-migrate-tool进程,请确保只有一台机器只有一个迁移任务进行"); // } // } catch (Exception e) { // logger.error(e.getMessage(), e); // } // 3.2 查看进程是否存在 try { String cmd = "/bin/ps -ef | grep redis-migrate-tool | grep -v grep | grep -v tail"; String response = SSHUtil.execute(migrateMachineIp, cmd); if (StringUtils.isNotBlank(response)) { return AppDataMigrateResult.fail(migrateMachineIp + "下有redis-migrate-tool进程,请确保只有一台机器只有一个迁移任务进行"); } } catch (Exception e) { logger.error(e.getMessage(), e); return AppDataMigrateResult.fail("检测发生异常,请观察日志"); } return AppDataMigrateResult.success(); } /** * 检测配置 * * @param migrateMachineIp * @param redisMigrateEnum * @param servers * @param redisSourcePass 源密码 * @return */ private AppDataMigrateResult checkMigrateConfig(String migrateMachineIp, AppDataMigrateEnum redisMigrateEnum, String servers, String redisPassword, boolean isSource) { //target如果是rdb是没有路径的,不需要检测 if (isSource || !AppDataMigrateEnum.isFileType(redisMigrateEnum)) { if (StringUtils.isBlank(servers)) { return AppDataMigrateResult.fail("服务器信息不能为空!"); } } List<String> serverList = Arrays.asList(servers.split(ConstUtils.NEXT_LINE)); if (CollectionUtils.isEmpty(serverList)) { return AppDataMigrateResult.fail("服务器信息格式有问题!"); } for (String server : serverList) { if (AppDataMigrateEnum.isFileType(redisMigrateEnum)) { if (!isSource) { continue; } // 检查文件是否存在 String filePath = server; String cmd = "head " + filePath; try { String headResult = SSHUtil.execute(migrateMachineIp, cmd); if (StringUtils.isBlank(headResult)) { return AppDataMigrateResult.fail(migrateMachineIp + "上的rdb:" + filePath + "不存在或者为空!"); } } catch (Exception e) { logger.error(e.getMessage()); return AppDataMigrateResult.fail(migrateMachineIp + "上的rdb:" + filePath + "读取异常!"); } } else { // 1. 检查是否为ip:port格式(简单检查一下,无需正则表达式) // 2. 检查Redis节点是否存在 String[] instanceItems = server.split(":"); if (instanceItems.length != 2) { return AppDataMigrateResult.fail("实例信息" + server + "格式错误,必须为ip:port格式"); } String ip = instanceItems[0]; String portStr = instanceItems[1]; boolean portIsDigit = NumberUtils.isDigits(portStr); if (!portIsDigit) { return AppDataMigrateResult.fail(server + "中的port不是整数"); } int port = NumberUtils.toInt(portStr); boolean isRun = redisCenter.isRun(ip, port, redisPassword); if (!isRun) { return AppDataMigrateResult.fail(server + "不是存活的或者密码错误!"); } } } return AppDataMigrateResult.success(); } @Override public boolean migrate(String migrateMachineIp, AppDataMigrateEnum sourceRedisMigrateEnum, String sourceServers, AppDataMigrateEnum targetRedisMigrateEnum, String targetServers, long sourceAppId, long targetAppId, String redisSourcePass, String redisTargetPass, long userId) { // 1. 生成配置 int migrateMachinePort = ConstUtils.REDIS_MIGRATE_TOOL_PORT; String configContent = generateConfig(migrateMachinePort, sourceRedisMigrateEnum, sourceServers, targetRedisMigrateEnum, targetServers, redisSourcePass, redisTargetPass); // 2. 上传配置 String timestamp = new SimpleDateFormat("yyyyMMddHHmmss").format(new Date()); String confileFileName = "rmt-" + timestamp + ".conf"; String logFileName = "rmt-" + timestamp + ".log"; boolean uploadConfig = createRemoteFile(migrateMachineIp, confileFileName, configContent); if (!uploadConfig) { return false; } // 3. 开始执行: 指定的配置名、目录、日志名 String cmd = ConstUtils.getRedisMigrateToolCmd() + " -c " + ConstUtils.getRedisMigrateToolDir() + confileFileName + " -o " + ConstUtils.getRedisMigrateToolDir() + logFileName + " -d"; logger.warn(cmd); try { SSHUtil.execute(migrateMachineIp, cmd); } catch (Exception e) { logger.error(e.getMessage(), e); return false; } // 4. 记录执行记录 AppDataMigrateStatus appDataMigrateStatus = new AppDataMigrateStatus(); appDataMigrateStatus.setMigrateMachineIp(migrateMachineIp); appDataMigrateStatus.setMigrateMachinePort(migrateMachinePort); appDataMigrateStatus.setStartTime(new Date()); appDataMigrateStatus.setSourceMigrateType(sourceRedisMigrateEnum.getIndex()); appDataMigrateStatus.setSourceServers(sourceServers); appDataMigrateStatus.setTargetMigrateType(targetRedisMigrateEnum.getIndex()); appDataMigrateStatus.setTargetServers(targetServers); appDataMigrateStatus.setLogPath(ConstUtils.getRedisMigrateToolDir() + logFileName); appDataMigrateStatus.setConfigPath(ConstUtils.getRedisMigrateToolDir() + confileFileName); appDataMigrateStatus.setUserId(userId); appDataMigrateStatus.setSourceAppId(sourceAppId); appDataMigrateStatus.setTargetAppId(targetAppId); appDataMigrateStatus.setStatus(AppDataMigrateStatusEnum.START.getStatus()); appDataMigrateStatusDao.save(appDataMigrateStatus); return true; } /** * 生成配置 * * @param sourceRedisMigrateEnum * @param sourceServers * @param targetRedisMigrateEnum * @param targetServers * @return */ public String generateConfig(int listenPort, AppDataMigrateEnum sourceRedisMigrateEnum, String sourceServers, AppDataMigrateEnum targetRedisMigrateEnum, String targetServers, String redisSourcePass, String redisTargetPass) { // source StringBuffer config = new StringBuffer(); config.append("[source]" + ConstUtils.NEXT_LINE); config.append("type: " + sourceRedisMigrateEnum.getType() + ConstUtils.NEXT_LINE); config.append("servers:" + ConstUtils.NEXT_LINE); List<String> sourceServerList = Arrays.asList(sourceServers.split(ConstUtils.NEXT_LINE)); for (String server : sourceServerList) { config.append(" - " + server + ConstUtils.NEXT_LINE); } if (StringUtils.isNotBlank(redisSourcePass)) { config.append("redis_auth: " + redisSourcePass + ConstUtils.NEXT_LINE); } config.append(ConstUtils.NEXT_LINE); // target config.append("[target]" + ConstUtils.NEXT_LINE); config.append("type: " + targetRedisMigrateEnum.getType() + ConstUtils.NEXT_LINE); if (!AppDataMigrateEnum.isFileType(targetRedisMigrateEnum)) { config.append("servers:" + ConstUtils.NEXT_LINE); List<String> targetServerList = Arrays.asList(targetServers.split(ConstUtils.NEXT_LINE)); for (String server : targetServerList) { config.append(" - " + server + ConstUtils.NEXT_LINE); } if (StringUtils.isNotBlank(redisTargetPass)) { config.append("redis_auth: " + redisTargetPass + ConstUtils.NEXT_LINE); } config.append(ConstUtils.NEXT_LINE); } // common:使用最简配置 config.append("[common]" + ConstUtils.NEXT_LINE); config.append("listen: 0.0.0.0:" + listenPort + ConstUtils.NEXT_LINE); config.append("dir: " + ConstUtils.getRedisMigrateToolDir()); return config.toString(); } /** * 创建远程文件 * * @param host * @param fileName * @param content */ public boolean createRemoteFile(String host, String fileName, String content) { /** * 1. 创建本地文件 */ // 确认目录 String localAbsolutePath = MachineProtocol.TMP_DIR + fileName; File tmpDir = new File(MachineProtocol.TMP_DIR); if (!tmpDir.exists()) { if (!tmpDir.mkdirs()) { logger.error("cannot create /tmp/cachecloud directory."); } } Path path = Paths.get(MachineProtocol.TMP_DIR + fileName); // 将配置文件的内容写到本地 BufferedWriter bufferedWriter = null; try { bufferedWriter = Files.newBufferedWriter(path, Charset.forName(MachineProtocol.ENCODING_UTF8)); bufferedWriter.write(content); } catch (IOException e) { logger.error("write rmt file error, ip: {}, filename: {}, content: {}", host, fileName, content, e); return false; } finally { if (bufferedWriter != null) { try { bufferedWriter.close(); } catch (IOException e) { logger.error(e.getMessage(), e); } } } /** * 2. 将配置文件推送到目标机器上 */ try { SSHUtil.scpFileToRemote(host, localAbsolutePath, ConstUtils.getRedisMigrateToolDir()); } catch (SSHException e) { logger.error("scp rmt file to remote server error: ip: {}, fileName: {}", host, fileName, e); return false; } /** * 3. 删除临时文件 */ File file = new File(localAbsolutePath); if (file.exists()) { file.delete(); } return true; } @Override public List<AppDataMigrateStatus> search(AppDataMigrateSearch appDataMigrateSearch) { try { return appDataMigrateStatusDao.search(appDataMigrateSearch); } catch (Exception e) { logger.error(e.getMessage(), e); return Collections.emptyList(); } } @Override public String showDataMigrateLog(long id, int pageSize) { AppDataMigrateStatus appDataMigrateStatus = appDataMigrateStatusDao.get(id); if (appDataMigrateStatus == null) { return ""; } String logPath = appDataMigrateStatus.getLogPath(); String host = appDataMigrateStatus.getMigrateMachineIp(); StringBuilder command = new StringBuilder(); command.append("/usr/bin/tail -n").append(pageSize).append(" ").append(logPath); try { return SSHUtil.execute(host, command.toString()); } catch (SSHException e) { logger.error(e.getMessage(), e); return ""; } } @Override public String showDataMigrateConf(long id) { AppDataMigrateStatus appDataMigrateStatus = appDataMigrateStatusDao.get(id); if (appDataMigrateStatus == null) { return ""; } String configPath = appDataMigrateStatus.getConfigPath(); String host = appDataMigrateStatus.getMigrateMachineIp(); String command = "cat " + configPath; try { return SSHUtil.execute(host, command); } catch (SSHException e) { logger.error(e.getMessage(), e); return ""; } } @Override public Map<RedisMigrateToolConstant, Map<String, Object>> showMiragteToolProcess(long id) { AppDataMigrateStatus appDataMigrateStatus = appDataMigrateStatusDao.get(id); if (appDataMigrateStatus == null) { return Collections.emptyMap(); } String info = ""; String host = appDataMigrateStatus.getMigrateMachineIp(); int port = appDataMigrateStatus.getMigrateMachinePort(); Jedis jedis = null; try { jedis = new Jedis(host, port, 5000); info = jedis.info(); } catch (Exception e) { logger.error(e.getMessage(), e); } finally { if (jedis != null) { jedis.close(); } } if (StringUtils.isBlank(info)) { return Collections.emptyMap(); } return processRedisMigrateToolStats(info); } /** * 处理迁移工具状态 * @param statResult * @return */ private Map<RedisMigrateToolConstant, Map<String, Object>> processRedisMigrateToolStats(String statResult) { Map<RedisMigrateToolConstant, Map<String, Object>> redisStatMap = new HashMap<RedisMigrateToolConstant, Map<String, Object>>(); String[] data = statResult.split("\r\n"); String key; int i = 0; int length = data.length; while (i < length) { if (data[i].contains("#")) { int index = data[i].indexOf('#'); key = data[i].substring(index + 1); ++i; RedisMigrateToolConstant redisMigrateToolConstant = RedisMigrateToolConstant.value(key.trim()); if (redisMigrateToolConstant == null) { continue; } Map<String, Object> sectionMap = new LinkedHashMap<String, Object>(); while (i < length && data[i].contains(":")) { String[] pair = data[i].split(":"); sectionMap.put(pair[0], pair[1]); i++; } redisStatMap.put(redisMigrateToolConstant, sectionMap); } else { i++; } } return redisStatMap; } @Override public CommandResult sampleCheckData(long id, int nums) { AppDataMigrateStatus appDataMigrateStatus = appDataMigrateStatusDao.get(id); if (appDataMigrateStatus == null) { return null; } String ip = appDataMigrateStatus.getMigrateMachineIp(); String configPath = appDataMigrateStatus.getConfigPath(); String sampleCheckDataCmd = ConstUtils.getRedisMigrateToolCmd() + " -c " + configPath + " -C" + " 'redis_check " + nums + "'"; logger.warn("sampleCheckDataCmd: {}", sampleCheckDataCmd); try { return new CommandResult(sampleCheckDataCmd, SSHUtil.execute(ip, sampleCheckDataCmd)); } catch (Exception e) { logger.error(e.getMessage(), e); return new CommandResult(sampleCheckDataCmd, ErrorMessageEnum.INNER_ERROR_MSG.getMessage()); } } @Override public AppDataMigrateResult stopMigrate(long id) { // 获取基本信息 AppDataMigrateStatus appDataMigrateStatus = appDataMigrateStatusDao.get(id); if (appDataMigrateStatus == null) { return AppDataMigrateResult.fail("id=" + id + "迁移记录不存在!"); } // 获取进程号 String migrateMachineIp = appDataMigrateStatus.getMigrateMachineIp(); String migrateMachineHostPort = migrateMachineIp + ":" + appDataMigrateStatus.getMigrateMachinePort(); Map<RedisMigrateToolConstant, Map<String, Object>> redisMigrateToolStatMap = showMiragteToolProcess(id); if (MapUtils.isEmpty(redisMigrateToolStatMap)) { return AppDataMigrateResult.fail("获取" + migrateMachineHostPort + "相关信息失败,可能是进程不存在或者客户端超时,请查找原因或重试!"); } Map<String, Object> serverMap = redisMigrateToolStatMap.get(RedisMigrateToolConstant.Server); int pid = MapUtils.getInteger(serverMap, "process_id", -1); if (pid <= 0) { return AppDataMigrateResult.fail("获取" + migrateMachineHostPort + "的进程号" + pid + "异常"); } // 确认进程号是redis-migrate-tool进程 Boolean exist = checkPidWhetherIsRmt(migrateMachineIp, pid); if (exist == null) { return AppDataMigrateResult.fail("执行过程中发生异常,请查看系统日志!"); } else if (exist.equals(false)) { return AppDataMigrateResult.fail(migrateMachineIp + "进程号" + pid + "不存在,请确认!"); } // kill掉进程 try { String cmd = "kill " + pid; SSHUtil.execute(migrateMachineIp, cmd); exist = checkPidWhetherIsRmt(migrateMachineIp, pid); if (exist == null) { return AppDataMigrateResult.fail(ErrorMessageEnum.INNER_ERROR_MSG.getMessage()); } else if (exist.equals(false)) { // 更新记录完成更新 appDataMigrateStatusDao.updateStatus(id, AppDataMigrateStatusEnum.END.getStatus()); return AppDataMigrateResult.success("已经成功停止了id=" + id + "的迁移任务"); } else { return AppDataMigrateResult.fail(migrateMachineIp + "进程号" + pid + "仍然存在,没有kill掉,请确认!"); } } catch (Exception e) { logger.error(e.getMessage()); return AppDataMigrateResult.fail(ErrorMessageEnum.INNER_ERROR_MSG.getMessage()); } } /** * 检查pid是否是redis-migrate-tool进程 * @param migrateMachineIp * @param pid * @return * @throws SSHException */ private Boolean checkPidWhetherIsRmt(String migrateMachineIp, int pid){ try { String cmd = "/bin/ps -ef | grep redis-migrate-tool | grep -v grep | grep " + pid; String response = SSHUtil.execute(migrateMachineIp, cmd); if (StringUtils.isNotBlank(response)) { return true; } else { return false; } } catch (SSHException e) { logger.error(e.getMessage(), e); return null; } } public void setRedisCenter(RedisCenter redisCenter) { this.redisCenter = redisCenter; } public void setMachineCenter(MachineCenter machineCenter) { this.machineCenter = machineCenter; } public void setAppService(AppService appService) { this.appService = appService; } public void setAppDataMigrateStatusDao(AppDataMigrateStatusDao appDataMigrateStatusDao) { this.appDataMigrateStatusDao = appDataMigrateStatusDao; } }