备份恢复方案有以下几个东西需要备份:主要是需要将数据备份下来
- 首先查找数据存放的地方
- 存入consul的数据
- 存入keycloak的资源及权限控制
- 数据库表及数据
- ...
- 怎么备份?:针对数据存放的位置不同,备份的方式也不同 数据备份,方式如下:
- consul数据--> consul-kv.json
- DB ---> ispeco.sql
- keycloak resources ---> keycloak-resources.json
将上述文件压缩成压缩包:以时间(ispeco-backup-resources年-月-日-时-分-秒)命名;存放在用户设定的存放路径(若用户未指定,则存放在默认路径/etc/ispeco/backup/路径下);
- 恢复数据 (1) 查询默认路径下的备份文件,ui展示
(2) 根据用户选择的压缩文件进行恢复
- keycloak
- 恢复consul数据
- 恢复keycloak数据
备份恢复流程
备份consul数据
通过命令备份
备份数据
// 导出所有kv键值对,注意prefix是导出键值对的前缀,为空字符串或不填说明要导出所有
consul kv export prefix > path/filename;
consul kv export test > kv.json; //导出consul中以test为前缀的所有key到当前路径的kv.json文件中
备注: 导出的数据value是base64编码的字符串。 使用base64 -d命令编码就可以看到原始的value值,如:
$ echo 'eyJ2ZXJzaW9uX3RpbWVzdGFtcCI6IC0xfQ==' | base64 -d
{"version_timestamp": -1}
恢复数据
//(1)从文件导入
consul kv import @kv.json;
//(2) 从stdin导入
cat values.json | consul kv import -
//(3) 直接传递JSON
consul kv import "$(cat values.json)"
//导入
consul kv import @kv.json;
备注: 导入的value必须是base64编码过的值。
通过api备份/恢复数据
备份数据
/v1/txn PUT
[
{
"KV": {
"Verb": "<verb>", get-tree
"Key": "<key>", //可以设置成前缀
"Value": "<Base64-encoded blob of data>",
"Flags": <flags>,
"Index": <index>,
"Session": "<session id>"
}
}
]
恢复数据
/v1/txn PUT
[
{
"KV": {
"Verb": "<verb>", set
"Key": "<key>", //key名称1
"Value": "<Base64-encoded blob of data>",
"Flags": <flags>,
"Index": <index>,
"Session": "<session id>"
}
},
.....
{
"KV": {
"Verb": "<verb>", set
"Key": "<key>", //key名称n
"Value": "<Base64-encoded blob of data>",
"Flags": <flags>,
"Index": <index>,
"Session": "<session id>"
}
}
]
注意事项
consul请求:/v1/txn 一次事务只允许操作64个key
备份DB数据
目前采用mysqldump命令进行备份数据
mysqldump
备份mysql表结构及数据
远程备份语法:
mysqldump -h 服务器 -u用户名 -p密码 数据库名 >备份文件.sql
本地备份语法:
mysqldump -u用户名 -p密码 数据库名 > 备份文件.sql
//示例
mysqldump -uroot -ppassword databasesName >C:\/\/databaseFile\/database.sql
恢复DB数据库表结构与数据
恢复备份语法:
mysql -uroot -p'密码' 数据库名 < /home/back/company.sql(备份文件的路径)
//示例
mysql -uroot -ppassword databasesName <C:\/\/databaseFile\/database.sql
注意事项
使用 mysqldump 备份数据库时避免锁表 可加参数--single-transaction, --skip-opt解决
- 导出表时默认会枷锁
对一个正在运行的数据库进行备份请慎重!! 如果一定要在服务运行期间备份,请添加 --skip-opt选项,
类似执行:
mysqldump --skip-opt -u root --password=123456 dbname >mysql.SQL
2.开启一致性事务
--opt 会lock 本次需要备份的所有表,因为本次备份的是 dbname数据库,所以会锁住dbname的所有表。
--single-transaction参数的作用,设置事务的隔离级别为可重复读,即REPEATABLE READ,这样能保证在一个事务中所有相同的查询读取到同样的数据,也就大概保证了在dump期间,如果其他innodb引擎的线程修改了表的数据并提交,对该dump线程的数据并无影响
java实现代码
/**
* MySQL备份还原工具类
*/
public class MySqlBackupRestoreUtils {
/**
* 备份数据库
* @param host host地址,可以是本机也可以是远程
* @param userName 数据库的用户名
* @param password 数据库的密码
* @param savePath 备份的路径
* @param fileName 备份的文件名
* @param databaseName 需要备份的数据库的名称
* @return
* @throws IOException
*/
public static boolean backup(String host, String userName, String password, String backupFolderPath, String fileName,
String database) throws Exception {
File backupFolderFile = new File(backupFolderPath);
if (!backupFolderFile.exists()) {
// 如果目录不存在则创建
backupFolderFile.mkdirs();
}
if (!backupFolderPath.endsWith(File.separator) && !backupFolderPath.endsWith("/")) {
backupFolderPath = backupFolderPath + File.separator;
}
// 拼接命令行的命令
String backupFilePath = backupFolderPath + fileName;
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append("mysqldump --opt").append(" --add-drop-database").append(" --add-drop-table");
stringBuilder.append(" -h").append(host).append(" -u").append(userName).append(" -p").append(password);
stringBuilder.append(" --result-file=").append(backupFilePath).append(".sql").append(" --default-character-set=utf8 ").append(database);
System.out.println(stringBuilder.toString());
// 调用外部执行 exe 文件的 Java API
Process process = Runtime.getRuntime().exec(getCommand(stringBuilder.toString()));
if (process.waitFor() == 0) {
// 0 表示线程正常终止
System.out.println("数据已经备份到 " + backupFilePath + " 文件中");
return true;
}
return false;
}
/**
* 还原数据库
* @param restoreFilePath 数据库备份的脚本路径
* @param host IP地址
* @param database 数据库名称
* @param userName 用户名
* @param password 密码
* @return
*/
public static boolean restore(String restoreFilePath, String host, String userName, String password, String database)
throws Exception {
File restoreFile = new File(restoreFilePath);
if (restoreFile.isDirectory()) {
for (File file : restoreFile.listFiles()) {
if (file.exists() && file.getPath().endsWith(".sql")) {
restoreFilePath = file.getAbsolutePath();
break;
}
}
}
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append("mysql -h").append(host).append(" -u").append(userName).append(" -p").append(password);
stringBuilder.append(" ").append(database).append(" < ").append(restoreFilePath);
try {
Process process = Runtime.getRuntime().exec(getCommand(stringBuilder.toString()));
if (process.waitFor() == 0) {
System.out.println("数据已从 " + restoreFilePath + " 导入到数据库中");
}
} catch (IOException e) {
e.printStackTrace();
return false;
}
return true;
}
private static String[] getCommand(String command) {
String os = System.getProperty("os.name");
String shell = "/bin/bash";
String c = "-c";
if(os.toLowerCase().startsWith("win")){
shell = "cmd";
c = "/c";
}
String[] cmd = { shell, c, command };
return cmd;
}
public static void main(String[] args) throws Exception {
String host = "localhost";
String userName = "root";
String password = "123456";
String database = "jansens";
System.out.println("开始备份");
String backupFolderPath = "d:/dev/";
String fileName = "mdh";
backup(host, userName, password, backupFolderPath, fileName, database);
System.out.println("备份成功");
/*System.out.println("开始还原");
String restoreFilePath = "d:/dev/mdh.sql";
restore(restoreFilePath, host, userName, password, database);
System.out.println("还原成功");*/
}
}
备份恢复keycloak数据
备份
REST API: POST /{realm}/partial-export?exportGroupsAndRoles=true&exportClients=true
JAVA API:
//获取RealmResource
RealmResource realm = getKeycloak().realm(realmName);
//获得导出的realm对象realmRepresentation 参数:exportGroupsAndRoles=true&exportClients=true
RealmRepresentation realmRepresentation = realm.partialExport(true, true);
List<ClientRepresentation> clients = realmRepresentation.getClients();
//处理clients,设置client:gateway的secret为"0cc2e58b-8a91-432b-bae7-6cffda154cd9",删除不需要备份的client
//导出realmRepresentation为realm-export.json
还原
REST API: POST /{realm}/partialImport -d @realm-export.json
JAVA API:
//解析realm-export.json获得RealmRepresentation对象 method(File file)方法为解析json文件获取RealmRepresentation对象
RealmRepresentation realmRepresentation = method(File file);
//获取所需还原的Roles、Users、clients信息,封装PartialImportRepresentatio对象,设置导入时若资源已存在执行策略为"OVERWRITE"
List<ClientRepresentation> clients = realmRepresentation.getClients();
List<UserRepresentation> users = realmRepresentation.getUsers();
RolesRepresentation roles = realmRepresentation.getRoles();
PartialImportRepresentation partialImportRepresentation = new PartialImportRepresentation();
partialImportRepresentation.setClients(clients);
partialImportRepresentation.setUsers(users);
partialImportRepresentation.setRoles(roles);
partialImportRepresentation.setIfResourceExists("OVERWRITE");
//获取RealmResource
RealmResource realm = getKeycloak().realm(realmName);
//导入
realm.partialImport(partialImportRepresentation)
导入策略:
- SKIP: 直接跳过,无法回滚
- OVERWRITE:重新导入(覆盖)所有资源(符合需求)
- FAIL:直接失败返回409
参考文档
API
- 备份:/dashboard/web/api/backup POST
{
"filepath": "",
"type": "" //默认为ALL全部备份,可以选择备份哪些内容
}
- 恢复:/dashboard/web/api/backup/restore POST
content-type:form-data
{
"backupFile": "路径/文件名.zip"
}