话不多说,进入正题,有一个bug,一直提示找不到文件夹,一顿排查后发现是file.mkdir()只能在app容器中创建目录,无法在宿主机中创建,于是我将目录与宿主机做了一个卷映射。但现在最新的问题来了:
备份语句执行失败
可疑点一:由于之前是可以执行的,尝试将卷映射去掉看看
失败:难道之前只是在命令行执行,并没有部署上去执行?果然不记录就会开始怀疑(想起来了,确实没有部署过)
找到原因:其实想想为什么提示docker不存在?是因为springboot的jar包中的容器中没有这个命令,
那么其实只要将这个命令放到宿主机中运行即可
可疑点二:代码添加了一行时区,虽然可能性不大,但我决定试试(现在想想,其实也是没有办法的办法了哈哈,但凡有点想法,也不至于这样排查啊)
借助docker in docker 的思路应该可以解决
参考链接:在docker容器中调用和执行宿主机的docker
经过验证,还是不太行
尝试一:在容器内部运行命令(前置条件,需要对docker命令进行卷映射)
docker exec -it app bash
命令:
docker exec -it mysql mysqldump -u用户名 -p密码 r_blog > /mnt/docker/mysql_backup/zzz.sql
发现可行,那具体什么原因呢?
怀疑是没有指定数据库端口号造成的,那就试试吧
docker exec -i mysql mysqldump -h81.71.87.241 -P端口号 -u用户名 -p密码 r_blog > /mnt/docker/mysql_backup/abca.sql
结果:还是不行
尝试二 : 使用Thread.sleep(10);睡眠10秒试试,等待命令执行完毕,感觉不太可行,因为exec.waitFor() 已经在等待进程了,总之试试吧
发现是由于
尝试三: 发现是由于Runtime.getRuntime().exec(command)没有成功执行导致的,那么能不能想办法打印执行的具体信息呢?当然可以!
参考文档:java执行系统命令
//读取命令的输出信息
InputStream inputStream = process.getInputStream();
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
process.waitFor();
if (process.exitValue() != 0) {
log.info("命令执行失败");
//说明命令执行失败
//可以进入到错误处理步骤中
}
log.info("打印进程输出信息====================================================");
String s = null;
while ((s = bufferedReader.readLine()) != null){
log.info(s);
}
log.info("打印进程输出信息结束=====================================================");
结果发现,命令直接执行失败,没有任何信息打印出来
更换ProcessBuilder 的方式执行命令
ProcessBuilder processBuilder = new ProcessBuilder(lcommand);
processBuilder.redirectErrorStream(true);
Process process = processBuilder.start();
报错Caused by: java.io.IOException: error=2, No such file or directory
,可能是容器内部缺少某个命令,先在win10中运行验证一下
发现是参数传错了…
ProcessBuilder processBuilder = new ProcessBuilder(lcommand);
参数应该是List或者String []的形式传入,改变参数发现windows执行成功,重新部署尝试运行…
还是不行,但有了新的发现,打印信息:the input device is not a TTY,开始调用百度api查阅信息…
真相渐渐浮出水面…发现执行定时任务是服务器的直接调用,不需要可交互的终端
-i : 可以进入容器内部
-t : 提供一个伪客户端
参考文档: 报错解决:the input device is not a TTY
去掉参数重试…
错误信息:Couldn't find table: ">"
,这又是为啥嘞?继续找寻原因…,怀疑是需要-i参数,进入容器内部,怀疑错误,发现是容器无法识别">"导致,换成参数-r即可
参考文档:报错: Couldn’t find table: “>”
执行后果然可行,不过新的问题出现:
mysqldump: Can't create/write to file '/mnt/docker/mysql_backup/2022-02-21-13-13-56.sql' (OS errno 2 - No such file or directory)
难道是权限问题?,我emo了…
将命令在服务器终端执行,发现报同样错误,然后确定是 -r参数导致的,这时候就要去看mysqldump文档了
首先查看下mysqldump的版本是否最新:
经过不断尝试,发现问题出在ProcessBuilder方法上面,它会将 数据库后的>字符识别成表,那么如何解决呢?
不断尝试后,决定去寻求大佬帮助!!!
错误总结:
目的背景:将springboot项目部署进docker容器中,备份docker中mysql容器中blog数据库
java中相关代码命令:
ProcessBuilder processBuilder = new ProcessBuilder("docker","exec","mysql","mysqldump","-h81.71.87.241","-P3306","-uroot","-proot","blog",">","/mnt/docker/backup/aaa.sql");
错误码:
mysqldump: Couldn't find table: ">"
请教各位大佬如何解决这个问题。
找到一个解决思路:
https://blog.csdn.net/weixin_43625121/article/details/109203934
https://blog.51cto.com/u_14299052/2986120
估计是glib的坑,后续再解决吧
最终办法:改写成shell脚本,ProcessBuilder去调用它
将命令存放在shell脚本,然后映射进springboot容器中,让springboot项目去调用它
services:
nginx:
image: nginx
container_name: nginx
ports:
- 80:80
- 443:443
links:
- app
depends_on:
- app
volumes:
- /mnt/docker/nginx/:/etc/nginx/
- /mnt/raxcl/blog_admin:/raxcl/blog_admin
- /mnt/raxcl/blog_view:/raxcl/blog_view
network_mode: "bridge"
app:
image: app
container_name: app
volumes:
# 映射shell脚本和mysql备份存放路径
- /mnt/docker/mysql_backup:/mnt/docker/mysql_backup
# 映射docker命令
- /var/run/docker.sock:/var/run/docker.sock
- /usr/bin/docker:/usr/bin/docker
expose:
- "8090"
network_mode: "bridge"
#!/bin/bash
backName=$1
echo backName is $backName
docker exec mysql mysqldump -h81.71.87.241 -P3306 -uroot -proot r_blog > /mnt/docker/mysql_backup/sql_file/${backName}
bash -n script_name.sh
使用下面的命令来执行并调试 Shell 脚本-x:
bash -x script_name.sh
chmod 777 mysqlDump.sh
package cn.raxcl.task;
import cn.raxcl.exception.NotFoundException;
import com.alibaba.fastjson.JSON;
import com.qiniu.http.Response;
import com.qiniu.storage.Configuration;
import com.qiniu.storage.Region;
import com.qiniu.storage.UploadManager;
import com.qiniu.storage.model.DefaultPutRet;
import com.qiniu.util.Auth;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.io.*;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.text.SimpleDateFormat;
import java.util.*;
/**
* mysql相关任务
* @author c-long.chan
* @date 2022/2/17 12:37
*/
@Component
@Slf4j
public class MysqlBackupScheduleTask {
@Value("${mysql-backup.host}")
private String host;
@Value("${mysql-backup.port}")
private Integer port;
@Value("${spring.datasource.username}")
private String username;
@Value("${spring.datasource.password}")
private String password;
@Value("${mysql-backup.qi-niu-yun.path}")
private String filePath;
@Value("${mysql-backup.qi-niu-yun.accessKey}")
private String accessKey;
@Value("${mysql-backup.qi-niu-yun.secretKey}")
private String secretKey;
@Value("${mysql-backup.qi-niu-yun.bucket}")
private String bucket;
/**
* 提取mysql数据并同步到七牛云
*/
public void syncMysqlToCloud() throws IOException, InterruptedException {
//1. 提取mysql数据
log.info("开始备份数据库");
String backName = new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss").format(new Date()) + ".sql";
dataBaseDump(backName);
//2. 备份数据至七牛云
String localFilePath = filePath + File.separator + backName;
boolean upload = upload(localFilePath, backName);
if (!upload){
throw new NotFoundException("数据上传失败,请联系管理员");
}
}
/**
* 提取mysql数据操作
* @param backName sql备份名
*/
private void dataBaseDump(String backName) throws IOException, InterruptedException {
//非Linux下判断目录是否存在
log.warn("如果项目部署在docker等容器中,请将目录与宿主机进行映射!!!");
if(!isOsLinux()){
Path path = Paths.get(filePath);
//判断目录是否存在
//File存在创建文件夹的缺陷,改用Files
log.info("判断目录是否存在:{}",path);
if (Files.notExists(path)){
log.info("目录不存在,创建它~");
Path directories = Files.createDirectories(path);
log.info("创建目录成功:{}",directories);
}else{
log.info("目录已存在");
}
}
File datafile = new File(filePath + File.separator + backName);
if (datafile.exists()){
log.error("文件名已存在,请更换:{}",backName);
throw new NotFoundException("文件名已存在,请更换");
}
//拼接cmd命令
String command = isOsLinux() ?
"sh /mnt/docker/mysql_backup/shell/mysqlDump.sh " +backName :
"cmd /c mysqldump -h" + host + " -P" + port + " -u" + username + " -p" + password + " r_blog > " + datafile;
log.info("备份命令为:{}",command);
List<String> commandList = new ArrayList<>();
Collections.addAll(commandList,command.split(" "));
log.info("commandList为:{}",commandList);
log.info("开始执行备份操作");
//执行备份命令
ProcessBuilder processBuilder = new ProcessBuilder(commandList);
processBuilder.redirectErrorStream(true);
Process process = processBuilder.start();
log.info("执行完成:{}",processBuilder);
//读取命令的输出信息
InputStream inputStream = process.getInputStream();
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
process.waitFor();
if (process.exitValue() != 0) {
log.error("命令执行失败");
throw new NotFoundException("命令执行失败");
}
log.info("打印进程输出信息====================================================");
String info;
while ((info = bufferedReader.readLine()) != null){
log.info(info);
}
log.info("打印进程输出信息结束=====================================================");
if (process.waitFor() == 0){
log.info("数据库备份成功,备份路径为:{}",datafile);
}
}
/**
* 判断项目运行环境是否为Linux
* @return boolean
*/
private boolean isOsLinux() {
Properties properties = System.getProperties();
String os = properties.getProperty("os.name");
return os != null && os.toLowerCase().contains("linux");
}
/**
* 将sql备份上传至七牛云
* @param localFilePath 文件路径(包含文件名)
* @param fileName 文件名
* @return boolean
*/
public boolean upload(String localFilePath,String fileName){
//1. 构建一个带指定Region对象的配置类(指定七牛云的机房区域)
Configuration configuration = new Configuration(Region.huanan());
//其他参数参考类注释
UploadManager uploadManager = new UploadManager(configuration);
//准备上传
try {
Auth auth = Auth.create(accessKey,secretKey);
String upToken = auth.uploadToken(bucket);
//上传文件
Response response = uploadManager.put(localFilePath, fileName, upToken);
//解析上传成功的结果
DefaultPutRet defaultPutRet = JSON.parseObject(response.bodyString(), DefaultPutRet.class);
log.info("上传文件到七牛云成功:{}",defaultPutRet);
return true;
} catch (IOException e) {
log.info("上传文件至七牛云失败:{}",e.toString());
return false;
}
}
}