Skip to content

Commit de04579

Browse files
committed
update backup strategy
1 parent 64c91f3 commit de04579

File tree

4 files changed

+153
-41
lines changed

4 files changed

+153
-41
lines changed

internal/application/backup.go

Lines changed: 21 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -402,53 +402,42 @@ func (bs *BackupScheduler) backupCouchDB(tempDir string, info *CouchDBBackupInfo
402402
return nil
403403
}
404404

405-
// backupCouchDBDatabase 备份单个 CouchDB 数据库
405+
// backupCouchDBDatabase 备份单个 CouchDB 数据库(优化内存使用)
406406
func (bs *BackupScheduler) backupCouchDBDatabase(dir string, dbName string) (DatabaseBackupInfo, error) {
407407
start := time.Now()
408-
408+
409409
// 获取数据库信息
410410
dbInfo, err := bs.getCouchDBDatabaseInfo(dbName)
411411
if err != nil {
412412
return DatabaseBackupInfo{}, err
413413
}
414-
415-
bs.log.Printf("Backing up: %s (%d docs)", dbName, dbInfo["doc_count"])
416-
417-
// 导出所有文档
418-
docs, err := bs.exportCouchDBDocs(dbName)
419-
if err != nil {
420-
return DatabaseBackupInfo{}, err
421-
}
422-
423-
// 构建备份数据
424-
backup := map[string]interface{}{
425-
"db_name": dbName,
426-
"update_seq": dbInfo["update_seq"],
427-
"doc_count": dbInfo["doc_count"],
428-
"timestamp": time.Now().Format(time.RFC3339),
429-
"docs": docs,
414+
415+
// 修复类型转换问题
416+
docCount := 0
417+
if dc, ok := dbInfo["doc_count"].(float64); ok {
418+
docCount = int(dc)
430419
}
431-
432-
// 保存为 JSON 文件
420+
421+
bs.log.Printf("Backing up: %s (%d docs)", dbName, docCount)
422+
423+
// 直接流式写入文件,避免大量文档占用内存
433424
outputPath := filepath.Join(dir, dbName+".json")
434-
data, err := json.Marshal(backup)
425+
426+
// 使用流式导出(避免 OOM)
427+
fileSize, err := bs.streamExportCouchDBDocs(dbName, dbInfo, outputPath)
435428
if err != nil {
436-
return DatabaseBackupInfo{}, fmt.Errorf("failed to marshal backup: %w", err)
437-
}
438-
439-
if err := os.WriteFile(outputPath, data, 0644); err != nil {
440-
return DatabaseBackupInfo{}, fmt.Errorf("failed to write backup file: %w", err)
429+
return DatabaseBackupInfo{}, err
441430
}
442-
431+
443432
info := DatabaseBackupInfo{
444-
DocCount: int(dbInfo["doc_count"].(float64)),
433+
DocCount: docCount,
445434
UpdateSeq: fmt.Sprintf("%v", dbInfo["update_seq"]),
446-
Size: int64(len(data)),
435+
Size: fileSize,
447436
BackupTime: time.Since(start).Milliseconds(),
448437
}
449-
450-
bs.log.Printf("✓ %s backed up: %.2f KB", dbName, float64(info.Size)/1024)
451-
438+
439+
bs.log.Printf("✓ %s backed up: %.2f KB in %d ms", dbName, float64(info.Size)/1024, info.BackupTime)
440+
452441
return info, nil
453442
}
454443

internal/application/backup_helpers.go

Lines changed: 130 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -643,42 +643,165 @@ func (bs *BackupScheduler) getCouchDBDatabaseInfo(dbName string) (map[string]int
643643
// exportCouchDBDocs 导出数据库所有文档
644644
func (bs *BackupScheduler) exportCouchDBDocs(dbName string) ([]interface{}, error) {
645645
url := fmt.Sprintf("%s/%s/_all_docs?include_docs=true", bs.couchdbClient.GetURL(), dbName)
646-
646+
647647
req, err := http.NewRequest(http.MethodGet, url, nil)
648648
if err != nil {
649649
return nil, err
650650
}
651-
651+
652652
bs.setBasicAuth(req)
653-
653+
654654
resp, err := http.DefaultClient.Do(req)
655655
if err != nil {
656656
return nil, err
657657
}
658658
defer resp.Body.Close()
659-
659+
660660
if resp.StatusCode != http.StatusOK {
661661
body, _ := io.ReadAll(resp.Body)
662662
return nil, fmt.Errorf("unexpected status code: %d, body: %s", resp.StatusCode, string(body))
663663
}
664-
664+
665665
var result struct {
666666
Rows []struct {
667667
Doc interface{} `json:"doc"`
668668
} `json:"rows"`
669669
}
670-
670+
671671
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
672672
return nil, err
673673
}
674-
674+
675675
docs := make([]interface{}, 0, len(result.Rows))
676676
for _, row := range result.Rows {
677677
if row.Doc != nil {
678678
docs = append(docs, row.Doc)
679679
}
680680
}
681+
682+
return docs, nil
683+
}
681684

685+
// streamExportCouchDBDocs 流式导出数据库文档(分批处理,避免 OOM)
686+
func (bs *BackupScheduler) streamExportCouchDBDocs(dbName string, dbInfo map[string]interface{}, outputPath string) (int64, error) {
687+
// 创建输出文件
688+
file, err := os.Create(outputPath)
689+
if err != nil {
690+
return 0, fmt.Errorf("failed to create output file: %w", err)
691+
}
692+
defer file.Close()
693+
694+
// 写入文件头部(元数据)
695+
docCount := 0
696+
if dc, ok := dbInfo["doc_count"].(float64); ok {
697+
docCount = int(dc)
698+
}
699+
700+
header := fmt.Sprintf(`{"db_name":"%s","update_seq":"%v","doc_count":%d,"timestamp":"%s","docs":[`,
701+
dbName,
702+
dbInfo["update_seq"],
703+
docCount,
704+
time.Now().Format(time.RFC3339),
705+
)
706+
707+
if _, err := file.WriteString(header); err != nil {
708+
return 0, err
709+
}
710+
711+
// 分批导出文档,避免 OOM
712+
// 每批处理 1000 个文档
713+
batchSize := 1000
714+
totalExported := 0
715+
716+
for skip := 0; skip < docCount; skip += batchSize {
717+
// 获取一批文档
718+
limit := batchSize
719+
if skip+limit > docCount {
720+
limit = docCount - skip
721+
}
722+
723+
docs, err := bs.exportCouchDBDocsBatch(dbName, skip, limit)
724+
if err != nil {
725+
return 0, fmt.Errorf("failed to export batch at skip=%d: %w", skip, err)
726+
}
727+
728+
// 写入文档
729+
for i, doc := range docs {
730+
if totalExported > 0 || i > 0 {
731+
file.WriteString(",")
732+
}
733+
734+
// 将文档序列化为 JSON 并写入
735+
docJSON, err := json.Marshal(doc)
736+
if err != nil {
737+
bs.log.Warnf("Failed to marshal doc in %s: %v", dbName, err)
738+
continue
739+
}
740+
741+
file.Write(docJSON)
742+
totalExported++
743+
}
744+
745+
// 输出进度(每 10 批)
746+
if (skip/batchSize)%10 == 0 && skip > 0 {
747+
progress := float64(totalExported) / float64(docCount) * 100
748+
bs.log.Printf(" Progress: %d/%d docs (%.1f%%)", totalExported, docCount, progress)
749+
}
750+
}
751+
752+
// 写入文件尾部
753+
file.WriteString("]}")
754+
755+
// 获取文件大小
756+
stat, _ := os.Stat(outputPath)
757+
758+
if totalExported != docCount {
759+
bs.log.Warnf("Expected %d docs, exported %d docs for %s", docCount, totalExported, dbName)
760+
}
761+
762+
return stat.Size(), nil
763+
}
764+
765+
// exportCouchDBDocsBatch 分批导出文档
766+
func (bs *BackupScheduler) exportCouchDBDocsBatch(dbName string, skip, limit int) ([]interface{}, error) {
767+
url := fmt.Sprintf("%s/%s/_all_docs?include_docs=true&skip=%d&limit=%d",
768+
bs.couchdbClient.GetURL(), dbName, skip, limit)
769+
770+
req, err := http.NewRequest(http.MethodGet, url, nil)
771+
if err != nil {
772+
return nil, err
773+
}
774+
775+
bs.setBasicAuth(req)
776+
777+
resp, err := http.DefaultClient.Do(req)
778+
if err != nil {
779+
return nil, err
780+
}
781+
defer resp.Body.Close()
782+
783+
if resp.StatusCode != http.StatusOK {
784+
body, _ := io.ReadAll(resp.Body)
785+
return nil, fmt.Errorf("unexpected status code: %d, body: %s", resp.StatusCode, string(body))
786+
}
787+
788+
var result struct {
789+
Rows []struct {
790+
Doc interface{} `json:"doc"`
791+
} `json:"rows"`
792+
}
793+
794+
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
795+
return nil, err
796+
}
797+
798+
docs := make([]interface{}, 0, len(result.Rows))
799+
for _, row := range result.Rows {
800+
if row.Doc != nil {
801+
docs = append(docs, row.Doc)
802+
}
803+
}
804+
682805
return docs, nil
683806
}
684807

internal/interfaces/cli/vercurr.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,6 @@ package cli
33
var CurrentVersion = Version{
44
Major: 0,
55
Minor: 2,
6-
PatchLevel: 37,
6+
PatchLevel: 40,
77
Suffix: "",
88
}

manifest.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"version": "0.2.37",
2+
"version": "0.2.40",
33
"name": "Hugoverse",
44
"description": "Headless CMS for Hugo",
55
"author": "sunwei",

0 commit comments

Comments
 (0)