fix: support MySQL backup GTID options (#13407)

* fix: support MySQL backup GTID options

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
ssongliu
2026-07-29 10:42:21 +08:00
committed by GitHub
co-authored by Copilot Autofix powered by AI
parent 97d383ed12
commit 380033dfe0
20 changed files with 83 additions and 5 deletions
+1
View File
@@ -31,4 +31,5 @@ type BackupRecord struct {
Status string `json:"status"`
Message string `json:"message"`
Description string `json:"description"`
Args string `gorm:"not null;default:''" json:"args"`
}
+41
View File
@@ -2,6 +2,7 @@ package service
import (
"context"
"encoding/json"
"fmt"
"os"
"path"
@@ -39,6 +40,7 @@ func (u *BackupService) MysqlBackup(req dto.CommonBackup) error {
TaskID: req.TaskID,
Status: constant.StatusWaiting,
Description: req.Description,
Args: encodeBackupArgs(req.Args),
}
if err := backupRepo.CreateRecord(record); err != nil {
global.LOG.Errorf("save backup record failed, err: %v", err)
@@ -143,6 +145,14 @@ func handleMysqlRecover(req dto.CommonRecover, parentTask *task.Task, isRollback
if !isRollback {
rollbackFile := path.Join(global.Dir.TmpDir, fmt.Sprintf("database/%s/%s_%s.sql.gz", req.Type, req.DetailName, time.Now().Format(constant.DateTimeSlimLayout)))
var rollbackArgs []string
if req.BackupRecordID != 0 {
record, err := backupRepo.GetRecord(repo.WithByID(req.BackupRecordID))
if err != nil {
return err
}
rollbackArgs = decodeBackupArgs(record.Args)
}
if err := cli.Backup(client.BackupInfo{
Name: req.DetailName,
Type: req.Type,
@@ -150,6 +160,7 @@ func handleMysqlRecover(req dto.CommonRecover, parentTask *task.Task, isRollback
Format: dbInfo.Format,
TargetDir: path.Dir(rollbackFile),
FileName: path.Base(rollbackFile),
Args: rollbackArgs,
}); err != nil {
return fmt.Errorf("backup mysql db %s for rollback before recover failed, err: %v", req.DetailName, err)
}
@@ -242,6 +253,36 @@ func doMysqlBackup(db DatabaseHelper, targetDir, fileName, secret string) error
return nil
}
func encodeBackupArgs(args []string) string {
var items []string
for _, arg := range args {
if len(arg) != 0 {
items = append(items, arg)
}
}
if len(items) == 0 {
return ""
}
data, err := json.Marshal(items)
if err != nil {
global.LOG.Warnf("marshal backup args failed: %v", err)
return ""
}
return string(data)
}
func decodeBackupArgs(value string) []string {
if len(value) == 0 {
return nil
}
var args []string
if err := json.Unmarshal([]byte(value), &args); err != nil {
global.LOG.Warnf("unmarshal backup args failed: %v", err)
return nil
}
return args
}
func loadSqlFile(file string) (string, error) {
if !strings.HasSuffix(file, ".tar.gz") && !strings.HasSuffix(file, ".zip") {
return file, nil
+1
View File
@@ -163,6 +163,7 @@ func (u *CronjobService) handleDatabase(cronjob model.Cronjob, startTime time.Ti
record.Name = dbInfo.Database
record.DetailName = dbInfo.Name
record.DownloadAccountID, record.SourceAccountIDs = cronjob.DownloadAccountID, cronjob.SourceAccountIDs
record.Args = encodeBackupArgs(dbInfo.Args)
backupDir := path.Join(global.Dir.LocalBackupDir, fmt.Sprintf("tmp/database/%s/%s/%s", dbInfo.DBType, record.Name, dbInfo.Name))
switch dbInfo.DBType {
+1
View File
@@ -93,6 +93,7 @@ func InitAgentDB() {
migrations.AddMcpServerGatewayArgs,
migrations.InitFirewallPortWhiteList,
migrations.AddDatabaseUserTable,
migrations.AddBackupRecordArgs,
})
if err := m.Migrate(); err != nil {
global.LOG.Error(err)
+7
View File
@@ -1676,3 +1676,10 @@ var AddMcpServerGatewayArgs = &gormigrate.Migration{
return tx.AutoMigrate(&model.McpServer{})
},
}
var AddBackupRecordArgs = &gormigrate.Migration{
ID: "20260729-add-backup-record-args",
Migrate: func(tx *gorm.DB) error {
return tx.AutoMigrate(&model.BackupRecord{})
},
}
+3 -3
View File
@@ -129,9 +129,9 @@
</el-checkbox>
<span class="input-help">{{ $t('database.mongodbRecoverDropAllCollectionsHelper') }}</span>
</el-form-item>
<el-form-item v-if="supportMysqlBackupArgs()" :label="$t('cronjob.backupArgs')">
<el-form-item v-if="isBackup && supportMysqlBackupArgs()" :label="$t('cronjob.backupArgs')">
<el-select v-model="args" filterable allow-create multiple>
<el-option v-for="item in mysqlArgs" :key="item.arg" :value="item.arg" :label="item.arg">
<el-option v-for="item in loadMysqlArgs(type)" :key="item.arg" :value="item.arg" :label="item.arg">
{{ item.arg }}
<span class="ml-2">{{ item.description }}</span>
</el-option>
@@ -199,7 +199,7 @@ import { MsgSuccess } from '@/utils/message';
import TaskLog from '@/components/log/task/index.vue';
import { routerToFileWithPath } from '@/utils/router';
import { useGlobalStore } from '@/composables/useGlobalStore';
import { mysqlArgs } from '@/views/cronjob/cronjob/helper';
import { loadMysqlArgs } from '@/views/cronjob/cronjob/helper';
const { currentNode } = useGlobalStore();
const emit = defineEmits(['close']);
+2
View File
@@ -1821,6 +1821,8 @@ const message = {
singleTransaction: 'Backup InnoDB tables using a single transaction, suitable for large-volume data backups',
quick: 'Read data row by row instead of loading the entire table into memory, suitable for large-volume data and low-memory machine backups',
skipLockTables: 'Backup without locking all tables, suitable for highly concurrent databases',
setGtidPurgedOff:
'Do not export GTID information, suitable for database recovery in Group Replication environments',
missBackupAccount: 'The backup account could not be found',
syncDate: 'Synchronization time ',
clean: 'Cache clean',
+2
View File
@@ -1855,6 +1855,8 @@ const message = {
quick: 'Lee datos fila por fila en lugar de cargar la tabla completa en memoria, adecuado para copias de seguridad de datos de gran volumen y máquinas con poca memoria',
skipLockTables:
'Copia de seguridad sin bloquear todas las tablas, adecuada para bases de datos altamente concurrentes',
setGtidPurgedOff:
'No exportar información GTID, adecuado para restaurar bases de datos en entornos de Group Replication',
missBackupAccount: 'No se pudo encontrar la cuenta de respaldo',
syncDate: 'Hora de sincronización',
clean: 'Limpiar caché',
+1
View File
@@ -1810,6 +1810,7 @@ const message = {
'پشتیبان‌گیری از جداول InnoDB با استفاده از یک تراکنش واحد، مناسب برای پشتیبان‌گیری داده‌های حجیم',
quick: 'خواندن داده‌ها سطر به سطر به جای بارگذاری کل جدول در حافظه، مناسب برای پشتیبان‌گیری داده‌های حجیم و ماشین‌های با حافظه کم',
skipLockTables: 'پشتیبان‌گیری بدون قفل کردن همه جداول، مناسب برای پایگاه‌های داده با هم‌روندی بالا',
setGtidPurgedOff: 'اطلاعات GTID صادر نشود؛ مناسب برای بازیابی پایگاه داده در محیط‌های Group Replication',
missBackupAccount: 'حساب پشتیبان پیدا نشد',
syncDate: 'زمان همگام‌سازی',
clean: 'پاک‌سازی کش',
+2
View File
@@ -1820,6 +1820,8 @@ const message = {
'単一トランザクションを使用して InnoDB テーブルをバックアップします大容量データのバックアップに適しています',
quick: 'テーブル全体をメモリにロードする代わりにデータを行単位で読み取ります大容量データや低メモリマシンのバックアップに適しています',
skipLockTables: 'すべてのテーブルをロックせずにバックアップします高並列データベースに適しています',
setGtidPurgedOff:
'バックアップ時に GTID 情報をエクスポートしませんグループレプリケーション環境でのデータベース復元に適しています',
missBackupAccount: 'バックアップアカウントは見つかりませんでした',
syncDate: '同期時間',
clean: 'キャッシュクリーン',
+1
View File
@@ -1791,6 +1791,7 @@ const message = {
singleTransaction: '단일 트랜잭션을 사용하여 InnoDB 테이블을 백업하며, 대용량 데이터 백업에 적합합니다',
quick: '전체 테이블을 메모리에 로드하는 대신 데이터를 행별로 읽습니다. 대용량 데이터 저메모리 시스템 백업에 적합합니다',
skipLockTables: '모든 테이블을 잠그지 않고 백업합니다. 높은 동시성 데이터베이스에 적합합니다',
setGtidPurgedOff: '백업 GTID 정보를 내보내지 않으며 그룹 복제 환경에서 데이터베이스를 복구할 적합합니다',
missBackupAccount: '백업 계정을 찾을 없습니다',
syncDate: '동기화 시간',
clean: '캐시 정리',
+1
View File
@@ -1789,6 +1789,7 @@ const message = {
singleTransaction: 'ສຳຮອງຕາຕະລາງ InnoDB ໂດຍໃຊ້ single transaction, ເໝາະສຳລັບຂໍ້ມູນຂະໜາດໃຫຍ່',
quick: 'ອ່ານຂໍ້ມູນເທື່ອລະແຖວແທນການໂຫຼດທັງໝົດລົງ memory, ເໝາະສຳລັບຂໍ້ມູນໃຫຍ່ ແລະ ເຄື່ອງທີ່ມີ memory ຕ່ຳ',
skipLockTables: 'ສຳຮອງໂດຍບໍ່ລັອກຕາຕະລາງ, ເໝາະສຳລັບຖານຂໍ້ມູນທີ່ມີການໃຊ້ງານສູງ',
setGtidPurgedOff: 'ບໍ່ສົ່ງອອກຂໍ້ມູນ GTID, ເໝາະສຳລັບການກູ້ຄືນຖານຂໍ້ມູນໃນສະພາບແວດລ້ອມ Group Replication',
missBackupAccount: 'ກວດບໍ່ພບບັນຊີສຳຮອງຂໍ້ມູນ',
syncDate: 'ເວລາຊິງໂຄໄນ ',
clean: 'ລ້າງ Cache',
+2
View File
@@ -1838,6 +1838,8 @@ const message = {
'Sandaran jadual InnoDB menggunakan transaksi tunggal, sesuai untuk sandaran data isipadu besar',
quick: 'Baca data baris demi baris daripada memuatkan keseluruhan jadual ke dalam ingatan, sesuai untuk sandaran data isipadu besar dan mesin ingatan rendah',
skipLockTables: 'Sandaran tanpa mengunci semua jadual, sesuai untuk pangkalan data konkuren tinggi',
setGtidPurgedOff:
'Jangan eksport maklumat GTID, sesuai untuk pemulihan pangkalan data dalam persekitaran Group Replication',
missBackupAccount: 'Akaun sandaran tidak dijumpai',
syncDate: 'Waktu penyelarasan',
clean: 'Bersihkan cache',
+2
View File
@@ -1840,6 +1840,8 @@ const message = {
'Faz backup de tabelas InnoDB usando uma única transação, adequado para backups de dados de grande volume',
quick: ' dados linha por linha em vez de carregar toda a tabela na memória, adequado para backups de dados de grande volume e máquinas com pouca memória',
skipLockTables: 'Backup sem bloquear todas as tabelas, adequado para bancos de dados altamente concorrentes',
setGtidPurgedOff:
'Não exportar informações de GTID, adequado para restaurar bancos de dados em ambientes de Group Replication',
missBackupAccount: 'A conta de backup não foi encontrada',
syncDate: 'Data de sincronização',
clean: 'Limpeza de cache',
+2
View File
@@ -1827,6 +1827,8 @@ const message = {
'Резервное копирование таблиц InnoDB с использованием одной транзакции, подходит для резервного копирования данных большого объема',
quick: 'Чтение данных построчно вместо загрузки всей таблицы в память, подходит для резервного копирования данных большого объема и машин с низкой памятью',
skipLockTables: 'Резервное копирование без блокировки всех таблиц, подходит для высококонкурентных баз данных',
setGtidPurgedOff:
'Не экспортировать информацию GTID, подходит для восстановления базы данных в среде Group Replication',
missBackupAccount: 'Не удалось найти учетную запись резервного копирования',
syncDate: 'Время синхронизации',
clean: 'Очистка кэша',
+2
View File
@@ -1839,6 +1839,8 @@ const message = {
'InnoDB tablolarını tek bir işlem kullanarak yedekler, büyük hacimli veri yedeklemeleri için uygundur',
quick: 'Tüm tabloyu belleğe yüklemek yerine verileri satır satır okur, büyük hacimli veri ve düşük bellekli makine yedeklemeleri için uygundur',
skipLockTables: 'Tüm tabloları kilitlemeden yedekleme, yüksek eşzamanlılığa sahip veritabanları için uygundur',
setGtidPurgedOff:
'GTID bilgilerini dışa aktarmayın; Group Replication ortamlarında veritabanı kurtarma için uygundur',
missBackupAccount: 'Yedekleme hesabı bulunamadı',
syncDate: 'Senkronizasyon zamanı ',
clean: 'Önbellek temizleme',
+1
View File
@@ -1713,6 +1713,7 @@ const message = {
singleTransaction: '使用單一事務備份 InnoDB 適用於大資料量的備份',
quick: '逐行讀取資料而不是將整個表載入到記憶體中適用於大資料量和低記憶體機器的備份',
skipLockTables: '不鎖定所有表進行備份適用於高併發的資料庫',
setGtidPurgedOff: '備份時不匯出 GTID 資訊適用於群組複寫環境中的資料庫還原',
missBackupAccount: '未能找到備份帳號',
syncDate: '同步時間 ',
clean: '快取清理',
+1
View File
@@ -1723,6 +1723,7 @@ const message = {
singleTransaction: '使用单一事务备份 InnoDB 适用于大数据量的备份',
quick: '逐行读取数据而不是将整个表加载到内存中适用于大数据量和低内存机器的备份',
skipLockTables: '不锁定所有表进行备份适用于高并发的数据库',
setGtidPurgedOff: '备份时不导出 GTID 信息适用于组复制环境中的数据库恢复',
missBackupAccount: '未能找到备份账号',
syncDate: '同步时间 ',
clean: '缓存清理',
@@ -24,7 +24,15 @@ export const mysqlArgs = [
{ arg: '--single-transaction', description: i18n.global.t('cronjob.singleTransaction') },
{ arg: '--quick', description: i18n.global.t('cronjob.quick') },
{ arg: '--skip-lock-tables', description: i18n.global.t('cronjob.skipLockTables') },
{
arg: '--set-gtid-purged=OFF',
description: i18n.global.t('cronjob.setGtidPurgedOff'),
supportedTypes: ['mysql', 'mysql-cluster'],
},
];
export const loadMysqlArgs = (dbType: string) => {
return mysqlArgs.filter((item) => !item.supportedTypes || item.supportedTypes.includes(dbType));
};
function loadWeek(i: number) {
for (const week of weekOptions) {
if (week.value === i) {
@@ -356,7 +356,7 @@
<el-form-item :label="$t('cronjob.backupArgs')">
<el-select v-model="form.argItems" filterable allow-create multiple>
<el-option
v-for="item in mysqlArgs"
v-for="item in loadMysqlArgs(form.dbType)"
:key="item.arg"
:value="item.arg"
:label="item.arg"
@@ -860,7 +860,7 @@ import {
transSpecToObj,
weekOptions,
cronjobTypes,
mysqlArgs,
loadMysqlArgs,
} from '../helper';
import { loadUsers } from '@/api/modules/toolbox';
import { loadContainerUsers } from '@/api/modules/container';