当前位置:论坛首页 > BUG提交 > Linux面板

【已解答】计划任务中的数据库备份,对于PgSQL,无法备份...

发表在 BUG提交4 天前 [复制链接] 3 64

面板版本:宝塔Linux面板11.3.0

系统版本:Debian 12

问题描述:

我的后端服务使用了多个不同的 Schema,但不使用默认的名为 public 的 schema.
在计划任务中创建的 备份pgsql数据库[所有] 任务,执行时只得到了一个空的zip压缩包,内部只有个空的sql文件。


我看了一下宝塔的代码,在 class/panelBackup.py 中定义的 pgsql_backup_database 函数是负责执行该备份操作的,但是看上去没有考虑周全。

  1. db_name = db_find["name"]
  2. isinstance                             # <-- 1479行
  3. db_user = "postgres"
  4. db_host = "127.0.0.1"
复制代码
1470行莫名其妙地出现一行 isinstance 不知道是何用意。


  1. if "ALL" in table_list:
  2.     tb_l = pgsql_obj.query("SELECT tablename FROM pg_tables WHERE schemaname = 'public';")
  3.     if isinstance(tb_l, list) and tb_l:
  4.            table_list = [i[0] for i in tb_l]
复制代码
1523行的SQL, 限定了查找表时只查 public 下的表。而且,如果tb_l 为空,那么 table_list 就仍是 ['ALL'],没有把其中的 'ALL' 移除。


  1.         # if storage_type == "db":  # 导出单个文件     <---- 1560行
  2.         #     file_name = file_name + ".sql.gz"
  3.         #     backup_path = os.path.join(db_backup_dir, file_name)
  4.         #     table_shell = ""
  5.         #     if len(table_list) != 0:
  6.         #         table_shell = "--table='" + "' --table='".join(table_list) + "'"
  7.         #     shell += " {table_shell} | gzip > '{backup_path}'".format(table_shell=table_shell, backup_path=backup_path)
  8.         #     public.ExecShell(shell, env={"PGPASSWORD": db_password})
  9.         # else:  # 按表导出
  10.         export_dir = os.path.join(db_backup_dir, file_name)
  11.         if not os.path.isdir(export_dir):
  12.                os.makedirs(export_dir)
复制代码
1560行处的 storage_type 判断被注释掉了,说明 pgsql的备份任务只支持 public schema 下的表的按表导出。
如果存在与数据库用户名同名的schema, 其中有和 public 中同名的表,
由于 pg_dump 默认使用数据库的默认 search_path(通常是 "$user", public),$user 会更优先,
那就会导致备份的表不是 public 下的,这显然应该注意。最好显式指定要备份的表的 schema。

我的目的是想备份整个数据库,所以简单照抄了比较完善的 mysql 的备份逻辑,将这个函数改为如下代码:


  1.     # pgsql 备份数据库
  2.     def pgsql_backup_database(self, db_find: dict, args: dict) -> Tuple[bool, str]:
  3.         from databaseModel.pgsqlModel import panelPgsql
  4.         
  5.         storage_type = args.get("storage_type", "db")  # 备份的文件数量, 按照数据库 | 按照表
  6.         table_list = args.get("table_list", [])  # 备份的集合
  7.         
  8.         db_name = db_find["name"]
  9.         db_user = "postgres"
  10.         db_host = "127.0.0.1"
  11.         if db_find["db_type"] == 0:
  12.             db_port = panelPgsql.get_config_options("port", int, 5432)
  13.             
  14.             t_path = os.path.join(public.get_panel_path(), "data/postgresAS.json")
  15.             if not os.path.isfile(t_path):
  16.                 error_msg = "管理员密码未设置!"
  17.                 self.echo_error(error_msg)
  18.                 return False, error_msg
  19.             db_password = json.loads(public.readFile(t_path)).get("password", "")
  20.             if not db_password:
  21.                 error_msg = "数据库密码为空!请先设置数据库密码!"
  22.                 self.echo_error(error_msg)
  23.                 return False, error_msg
  24.         
  25.         elif db_find["db_type"] == 1:
  26.             # 远程数据库
  27.             conn_config = json.loads(db_find["conn_config"])
  28.             db_host = conn_config["db_host"]
  29.             db_port = conn_config["db_port"]
  30.             db_user = conn_config["db_user"]
  31.             db_password = conn_config["db_password"]
  32.         elif db_find["db_type"] == 2:
  33.             conn_config = public.M("database_servers").where("id=? AND LOWER(db_type)=LOWER('pgsql')", db_find["sid"]).find()
  34.             db_host = conn_config["db_host"]
  35.             db_port = conn_config["db_port"]
  36.             db_user = conn_config["db_user"]
  37.             db_password = conn_config["db_password"]
  38.         else:
  39.             error_msg = "未知的数据库类型"
  40.             self.echo_error(error_msg)
  41.             return False, error_msg
  42.         
  43.         pgsql_obj = panelPgsql().set_host(host=db_host, port=db_port, database=db_name, user=db_user, password=db_password)
  44.         status, err_msg = pgsql_obj.connect()
  45.         if status is False:
  46.             error_msg = "连接数据库[{}:{}]失败".format(db_host, int(db_port))
  47.             self.echo_error(error_msg)
  48.             return False, error_msg
  49.         
  50.         db_size = 0
  51.         db_data = pgsql_obj.query("SELECT pg_database_size('{}') AS database_size;".format(db_name))
  52.         if isinstance(db_data, list) and len(db_data) != 0:
  53.             db_size = db_data[0][0]
  54.         
  55.         if db_size == 0:
  56.             error_msg = '指定数据库 `{}` 没有任何数据!'.format(db_name)
  57.             self.echo_error(error_msg)
  58.             return False, error_msg
  59.         
  60.         try:
  61.             if "ALL" in table_list:
  62.                 table_list=[]
  63.                 # tb_l = pgsql_obj.query("SELECT tablename FROM pg_tables WHERE schemaname = 'public';")
  64.                 # if isinstance(tb_l, list) and tb_l:
  65.                 #     table_list = [i[0] for i in tb_l]
  66.                 # else:
  67.                 #     table_list=[]
  68.         except:
  69.             table_list=[]
  70.         
  71.         self.echo_info('备份PgSQL数据库:{}'.format(db_name))
  72.         self.echo_info("数据库大小:{}".format(public.to_size(db_size)))
  73.         self.echo_info("备份的table_list:{}".format(table_list))
  74.         self.echo_info("备份的类型:{}".format(storage_type))
  75.         
  76.         disk_path, disk_free, disk_inode = self.get_disk_free(self._PGSQL_BACKUP_DIR)
  77.         self.echo_info("分区{}可用磁盘空间为:{},可用Inode为:{}".format(disk_path, public.to_size(disk_free), disk_inode))
  78.         if disk_path:
  79.             if disk_free < db_size:
  80.                 error_msg = "目标分区可用的磁盘空间小于{},无法完成备份,请增加磁盘容量,或在设置页面更改默认备份目录!".format(public.to_size(db_size))
  81.                 self.echo_error(error_msg)
  82.                 return False, error_msg
  83.             if disk_inode < self._inode_min:
  84.                 error_msg = "目标分区可用的Inode小于{},无法完成备份,请增加磁盘容量,或在设置页面更改默认备份目录!".format(self._inode_min)
  85.                 self.echo_error(error_msg)
  86.                 return False, error_msg
  87.         stime = time.time()
  88.         self.echo_info("开始导出数据库:{}".format(public.format_date(times=stime)))
  89.         # 调用 get_backup_dir 函数来获取备份目录的路径
  90.         pgsql_backup_dir = self.get_backup_dir(db_find, args, "pgsql")
  91.         # 使用获取的路径来构建备份文件的路径
  92.         db_backup_dir = os.path.join(pgsql_backup_dir, db_name)
  93.         if not os.path.exists(db_backup_dir):
  94.             os.makedirs(db_backup_dir)
  95.         
  96.         file_name = "{db_name}_{backup_time}_pgsql_data".format(db_name=db_name, backup_time=time.strftime("%Y-%m-%d_%H-%M-%S", time.localtime()))
  97.         
  98.         shell = "'{pgdump_bin}' --host='{db_host}' --port={db_port} --username='{db_user}' --dbname='{db_name}' --clean".format(
  99.             pgdump_bin=self._PGDUMP_BIN,
  100.             db_host=db_host,
  101.             db_port=int(db_port),
  102.             db_user=db_user,
  103.             db_name=db_name,
  104.         )
  105.         
  106.         if storage_type == "db":  # 导出单个文件
  107.             if not os.path.exists("/usr/bin/gzip") and not os.path.exists("/bin/gzip") and not os.path.exists("/usr/sbin/gzip"):
  108.                 self.echo_info("备份异常!压缩工具gzip不存在,请在终端执行安装后再执行备份")
  109.                 if os.path.exists("/usr/bin/apt-get"):
  110.                     self.echo_info("安装命令:apt-get install gzip -y")
  111.                 elif os.path.exists("/usr/bin/yum"):
  112.                     self.echo_info("安装命令:yum install gzip -y")
  113.                 return False, "gzip命令不存在,请先安装gzip"
  114.             file_name = file_name + ".sql.gz"
  115.             backup_path = os.path.join(db_backup_dir, file_name)
  116.             table_shell = ""
  117.             if len(table_list) != 0:
  118.                 table_shell = "--table='" + "' --table='".join(table_list) + "'"
  119.             
  120.             # shell += " {table_shell} | gzip > '{backup_path}'".format(table_shell=table_shell, backup_path=backup_path)
  121.             shell += " {table_shell} 2> '{err_log}' | gzip > '{backup_path}'".format(table_shell=table_shell, err_log=self._err_log, backup_path=backup_path)
  122.             self.echo_info("备份语句:{}".format(shell))
  123.             public.ExecShell(shell, env={"PGPASSWORD": db_password})
  124.         else:  # 按表导出
  125.             export_dir = os.path.join(db_backup_dir, file_name)
  126.             if not os.path.isdir(export_dir):
  127.                 os.makedirs(export_dir)
  128.             
  129.             for table_name in table_list:
  130.                 tb_backup_path = os.path.join(export_dir, "{table_name}.sql".format(table_name=table_name))
  131.                 tb_shell = shell + " --table='{table_name}' > '{tb_backup_path}'".format(table_name=table_name, tb_backup_path=tb_backup_path)
  132.                 public.ExecShell(tb_shell, env={"PGPASSWORD": db_password})
  133.             backup_path = "{export_dir}.zip".format(export_dir=export_dir)
  134.             public.ExecShell("cd '{backup_dir}' && zip -m '{backup_path}' -r '{file_name}'".format(backup_dir=db_backup_dir, backup_path=backup_path, file_name=file_name))
  135.             if not os.path.exists(backup_path):
  136.                 public.ExecShell("rm -rf {}", format(export_dir))
  137.         
  138.         # public.ExecShell(shell, env={"PGPASSWORD": db_password})
  139.         if not os.path.exists(backup_path):
  140.             error_msg = "数据库备份失败!"
  141.             self.echo_error(error_msg)
  142.             self.echo_info(public.readFile(self._err_log))
  143.             return False, error_msg
  144.         gz_size = os.path.getsize(backup_path)
  145.         # self.check_disk_space(gz_size,self._PGSQL_BACKUP_DIR,type=1)
  146.         self.echo_info("数据库备份完成,耗时{:.2f}秒,压缩包大小:{}".format(time.time() - stime, public.to_size(gz_size)))
  147.         return True, backup_path
  148.    
复制代码
经测试可以正常备份。 但这毕竟是临时改的面板的代码,写法也没怎么推敲,希望官方能重视起来这种基础的备份功能,毕竟谁也不希望自己设定的备份任务只能得到一堆空白压缩包。





使用道具 举报 只看该作者 回复
发表于 3 天前 | 显示全部楼层
感谢反馈,下个版本会进行处理
使用道具 举报 回复 支持 反对
发表于 昨天 21:43 | 显示全部楼层
跟帖一个,看咱们这边后续有修复这个问题的计划吗:https://www.bt.cn/bbs/thread-151737-1-1.html
同名远程数据库的问题
使用道具 举报 回复 支持 反对
发表于 昨天 22:28 | 显示全部楼层
阿珂 发表于 2025-12-3 16:32
感谢反馈,下个版本会进行处理

这不得奖励点宝塔币?
使用道具 举报 回复 支持 反对
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

普通问题处理

论坛响应时间:72小时

问题处理方式:排队(仅解答)

工作时间:白班:9:00 - 18:00

紧急运维服务

响应时间:3分钟

问题处理方式:宝塔专家1对1服务

工作时间:工作日:9:00 - 18:30

宝塔专业团队为您解决服务器疑难问题

点击联系技术分析

工作时间:09:00至18:30

快速回复 返回顶部 返回列表