vacuum.go 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434
  1. package core
  2. import (
  3. "errors"
  4. "fmt"
  5. "os"
  6. "path/filepath"
  7. "sync"
  8. "time"
  9. "TsDB2/DB/core/page"
  10. "TsDB2/DB/core/types"
  11. )
  12. // ============================================================================
  13. // VACUUM 整库重写 —— 回收整库碎片, 让文件真正缩小
  14. //
  15. // 工作原理:
  16. // 扫描旧库 schema + 全部行 → 在临时文件 <path>.vacuum.tmp 上重建 →
  17. // 原子 rename 替换旧文件 → 重新打开。
  18. //
  19. // 与自动 compact (40% 失效字节触发, 详见 datapage.go) 的区别:
  20. // - 自动 compact: 页内墓碑回收, 文件大小几乎不变
  21. // - VACUUM: 整库重写, 文件真正缩小, 归还所有空闲页给 OS
  22. //
  23. // 失败回滚: 任何阶段失败 → 删除临时文件, 旧库保持不变。
  24. // ============================================================================
  25. // VACUUMMode VACUUM 工作模式
  26. type VACUUMMode int
  27. const (
  28. // VACUUMModeSync 同步模式: 锁写后单步完成, 阻塞所有读写。
  29. VACUUMModeSync VACUUMMode = iota
  30. // VACUUMModeOnline 在线模式: 扫描期允许读 (但不允许写),
  31. // 切换期短暂阻塞。当前 MVP 实现退化为 Sync (Close-Open),
  32. // 保留 API 形状供后续接入增量捕获。
  33. VACUUMModeOnline
  34. )
  35. // VACUUMProgress 进度回调类型。
  36. // phase: "scan" | "build" | "index" | "swap" | "done" | "fail"
  37. // copied: 已复制行数 (scan/build/index 阶段有效)
  38. // total: 总行数 (估计, 0 表示未知)
  39. type VACUUMProgress func(phase string, copied, total int64)
  40. // VACUUMOptions VACUUM 选项
  41. type VACUUMOptions struct {
  42. // Progress 进度回调 (nil = 不调用)
  43. Progress VACUUMProgress
  44. // Mode 工作模式, 默认 VACUUMModeSync
  45. Mode VACUUMMode
  46. // KeepTempOnError 失败时保留临时文件供分析, 默认 false
  47. KeepTempOnError bool
  48. }
  49. // VACUUMResult VACUUM 结果
  50. type VACUUMResult struct {
  51. OldFileSize int64
  52. NewFileSize int64
  53. SavedBytes int64
  54. SavedPercent float64
  55. Tables int
  56. RowsCopied int64
  57. Duration time.Duration
  58. Mode VACUUMMode
  59. }
  60. // vacuumSrcRow 源行快照 (deserializeRow 的产物, 跨 Copy 安全)
  61. type vacuumSrcRow struct {
  62. pkID uint64
  63. vals []types.FieldValue
  64. }
  65. // vacuumSrcIndex 源索引定义
  66. type vacuumSrcIndex struct {
  67. name string
  68. typ page.IndexType
  69. fields []byte
  70. unique bool
  71. }
  72. // vacuumSrcTable 源表信息 (schema + 索引 + 行快照)
  73. type vacuumSrcTable struct {
  74. name string
  75. fields []types.FieldInfo
  76. indexes []vacuumSrcIndex
  77. rows []vacuumSrcRow
  78. }
  79. // ============================================================================
  80. // 主入口
  81. // ============================================================================
  82. // VACUUM 整库重写, 让数据库文件真正缩小。
  83. //
  84. // 拒绝条件:
  85. // - 当前有活跃事务 (db.currentTxn != nil)
  86. // - db 路径不可用
  87. //
  88. // 调用后 db 句柄会被关闭并重新打开, 调用者继续使用即可。
  89. func (db *DB) VACUUM(opts ...VACUUMOptions) (*VACUUMResult, error) {
  90. opt := VACUUMOptions{Mode: VACUUMModeSync}
  91. if len(opts) > 0 {
  92. opt = opts[0]
  93. }
  94. if db.currentTxn != nil {
  95. return nil, fmt.Errorf("%w: VACUUM", ErrTxnActive)
  96. }
  97. if db.path == "" || db.path == os.DevNull {
  98. return nil, fmt.Errorf("%w (%q)", ErrVACUUMNoPath, db.path)
  99. }
  100. progress := opt.Progress
  101. if progress == nil {
  102. progress = func(string, int64, int64) {}
  103. }
  104. start := time.Now()
  105. // 1. 把 WAL / 脏页全部落盘, 拿到一致快照
  106. if err := db.Commit(); err != nil {
  107. return nil, fmt.Errorf("VACUUM: 提交源库失败: %w", err)
  108. }
  109. srcStat, _ := os.Stat(db.path)
  110. var oldSize int64
  111. if srcStat != nil {
  112. oldSize = srcStat.Size()
  113. }
  114. // 2. 扫描旧库
  115. progress("scan", 0, 0)
  116. sources, totalRows, err := db.vacuumScan(progress)
  117. if err != nil {
  118. return nil, fmt.Errorf("VACUUM: 扫描源库失败: %w", err)
  119. }
  120. // 3. 关闭旧 db (确保所有页都落盘)
  121. if err := db.Close(); err != nil {
  122. _ = os.Remove(db.path + ".vacuum.tmp")
  123. return nil, fmt.Errorf("VACUUM: 关闭源库失败: %w", err)
  124. }
  125. // 4. 在临时路径上重建
  126. tmpPath := db.path + ".vacuum.tmp"
  127. _ = os.Remove(tmpPath) // 清理残留
  128. progress("build", 0, totalRows)
  129. newDB, err := Open(tmpPath)
  130. if err != nil {
  131. return db.reopenAfterVacuum(nil, fmt.Errorf("VACUUM: 打开临时库失败: %w", err))
  132. }
  133. // 5. 重建表 + 索引 + 插入行
  134. var rowsCopied int64
  135. for _, src := range sources {
  136. // 5a. 建表
  137. newTbl, err := newDB.CreateTable(src.name, src.fields)
  138. if err != nil {
  139. _ = newDB.Close()
  140. _ = os.Remove(tmpPath)
  141. return db.reopenAfterVacuum(nil, fmt.Errorf("VACUUM: 重建表 %q 失败: %w", src.name, err))
  142. }
  143. // CreateTable 不自动建主键索引, 必须显式建; 否则 InsertRowWithID 找不到行存储索引,
  144. // resolveIndexes() 返回空, 插入路径无副作用通过, 但 TotalRows++ 谎报数据, Count() 拿到 0。
  145. if _, err := newTbl.CreateIndex("pk", page.IndexTypePrimary, []byte{0}); err != nil {
  146. _ = newDB.Close()
  147. _ = os.Remove(tmpPath)
  148. return db.reopenAfterVacuum(nil, fmt.Errorf("VACUUM: 建主键索引 %q 失败: %w", src.name, err))
  149. }
  150. // 5b. 插入全部行 (走主键索引)
  151. for ri, row := range src.rows {
  152. if _, err := newTbl.InsertRowWithID(row.pkID, row.vals); err != nil {
  153. // 单行失败: 跳过 (deserializeRow 失败/罕见约束冲突)
  154. continue
  155. }
  156. rowsCopied++
  157. if ri%1000 == 0 {
  158. progress("build", rowsCopied, totalRows)
  159. }
  160. }
  161. // 周期 commit, 减少 WAL 大小
  162. if err := newDB.Commit(); err != nil {
  163. _ = newDB.Close()
  164. _ = os.Remove(tmpPath)
  165. return db.reopenAfterVacuum(nil, fmt.Errorf("VACUUM: 提交 %q 失败: %w", src.name, err))
  166. }
  167. progress("build", rowsCopied, totalRows)
  168. // 5c. 重建二级索引 (自动批量回填已存在的行)
  169. progress("index", rowsCopied, totalRows)
  170. for _, idx := range src.indexes {
  171. if isRowStoreType(idx.typ) {
  172. continue // 行存储索引已在 CreateTable 时建好
  173. }
  174. if _, err := newTbl.CreateIndex(idx.name, idx.typ, idx.fields, idx.unique); err != nil {
  175. _ = newDB.Close()
  176. _ = os.Remove(tmpPath)
  177. return db.reopenAfterVacuum(nil, fmt.Errorf("VACUUM: 重建索引 %q 失败: %w", idx.name, err))
  178. }
  179. }
  180. progress("index", rowsCopied, totalRows)
  181. }
  182. // 6. 关闭新 db
  183. if err := newDB.Commit(); err != nil {
  184. _ = newDB.Close()
  185. _ = os.Remove(tmpPath)
  186. return db.reopenAfterVacuum(nil, fmt.Errorf("VACUUM: 最终提交失败: %w", err))
  187. }
  188. if err := newDB.Close(); err != nil {
  189. _ = os.Remove(tmpPath)
  190. return db.reopenAfterVacuum(nil, fmt.Errorf("VACUUM: 关闭新库失败: %w", err))
  191. }
  192. // 7. fsync 父目录 (确保 rename 的目录项落盘)
  193. if dir := filepath.Dir(db.path); dir != "" {
  194. if d, derr := os.Open(dir); derr == nil {
  195. _ = d.Sync()
  196. _ = d.Close()
  197. }
  198. }
  199. // 8. 原子替换
  200. progress("swap", 0, 0)
  201. if err := os.Rename(tmpPath, db.path); err != nil {
  202. if !opt.KeepTempOnError {
  203. _ = os.Remove(tmpPath)
  204. }
  205. return db.reopenAfterVacuum(nil, fmt.Errorf("VACUUM: 替换文件失败: %w", err))
  206. }
  207. // 9. 重新打开
  208. progress("done", 0, 0)
  209. newDB2, err := Open(db.path)
  210. if err != nil {
  211. return nil, fmt.Errorf("VACUUM: 重新打开失败: %w", err)
  212. }
  213. *db = *newDB2
  214. // 10. 统计结果
  215. newStat, _ := os.Stat(db.path)
  216. var newSize int64
  217. if newStat != nil {
  218. newSize = newStat.Size()
  219. }
  220. result := &VACUUMResult{
  221. OldFileSize: oldSize,
  222. NewFileSize: newSize,
  223. SavedBytes: oldSize - newSize,
  224. Tables: len(sources),
  225. RowsCopied: rowsCopied,
  226. Duration: time.Since(start),
  227. Mode: opt.Mode,
  228. }
  229. if oldSize > 0 {
  230. result.SavedPercent = float64(oldSize-newSize) * 100 / float64(oldSize)
  231. }
  232. return result, nil
  233. }
  234. // ============================================================================
  235. // 扫描源库
  236. // ============================================================================
  237. // vacuumScan 扫描源库, 拿到 schema + 索引定义 + 行快照
  238. func (db *DB) vacuumScan(progress VACUUMProgress) ([]vacuumSrcTable, int64, error) {
  239. tables := db.GetAllTables()
  240. sources := make([]vacuumSrcTable, 0, len(tables))
  241. // 先统计总行数 (用于进度回调的 total)
  242. totalRows := int64(0)
  243. for _, meta := range tables {
  244. tbl, ok := db.FindTable(meta.Name)
  245. if !ok {
  246. continue
  247. }
  248. totalRows += int64(tbl.TotalRows())
  249. }
  250. progress("scan", 0, totalRows)
  251. var scanned int64
  252. for _, meta := range tables {
  253. tbl, ok := db.FindTable(meta.Name)
  254. if !ok {
  255. continue
  256. }
  257. // 字段定义 (剔除已 DROP 的列)
  258. fields := tbl.GetFields()
  259. activeFields := make([]types.FieldInfo, 0, len(fields))
  260. for _, f := range fields {
  261. if f.IsDropped() {
  262. continue
  263. }
  264. activeFields = append(activeFields, f)
  265. }
  266. // 索引定义
  267. var srcIndexes []vacuumSrcIndex
  268. for _, im := range tbl.IndexMeta().GetIndexes() {
  269. srcIndexes = append(srcIndexes, vacuumSrcIndex{
  270. name: im.Name(),
  271. typ: im.IndexType,
  272. fields: append([]byte(nil), im.Fields()...),
  273. unique: im.Unique(),
  274. })
  275. }
  276. // 行快照
  277. rows := make([]vacuumSrcRow, 0, tbl.TotalRows())
  278. q := tbl.Query()
  279. scanErr := q.fullTableScanRaw(func(pkID uint64, rowData []byte) bool {
  280. vals := tbl.deserializeRow(rowData)
  281. if vals == nil {
  282. return true // 解析失败, 跳过
  283. }
  284. rows = append(rows, vacuumSrcRow{pkID: pkID, vals: vals})
  285. scanned++
  286. if scanned%10000 == 0 {
  287. progress("scan", scanned, totalRows)
  288. }
  289. return true
  290. })
  291. if scanErr != nil {
  292. return nil, 0, fmt.Errorf("扫描表 %q: %w", meta.Name, scanErr)
  293. }
  294. sources = append(sources, vacuumSrcTable{
  295. name: meta.Name,
  296. fields: activeFields,
  297. indexes: srcIndexes,
  298. rows: rows,
  299. })
  300. }
  301. progress("scan", totalRows, totalRows)
  302. return sources, totalRows, nil
  303. }
  304. // ============================================================================
  305. // 自动调度
  306. // ============================================================================
  307. // SetAutoVACUUM 开启自动 VACUUM, 周期触发。
  308. //
  309. // interval: 两次 VACUUM 的最小间隔 (例如 1*time.Hour)
  310. // options: 每次 VACUUM 使用的选项
  311. //
  312. // 关闭: SetAutoVACUUM(0)
  313. func (db *DB) SetAutoVACUUM(interval time.Duration, opts ...VACUUMOptions) {
  314. db.autoVacuumMu.Lock()
  315. defer db.autoVacuumMu.Unlock()
  316. if interval <= 0 {
  317. if db.autoVacuumTimer != nil {
  318. db.autoVacuumTimer.Stop()
  319. db.autoVacuumTimer = nil
  320. }
  321. db.autoVacuumEnabled = false
  322. return
  323. }
  324. opt := VACUUMOptions{Mode: VACUUMModeSync}
  325. if len(opts) > 0 {
  326. opt = opts[0]
  327. }
  328. db.autoVacuumOpts = opt
  329. db.autoVacuumEnabled = true
  330. db.lastVACUUM = time.Now()
  331. run := func() {
  332. db.autoVacuumMu.Lock()
  333. enabled := db.autoVacuumEnabled
  334. curOpt := db.autoVacuumOpts
  335. db.autoVacuumMu.Unlock()
  336. if !enabled {
  337. return
  338. }
  339. // 自动调度: 失败也吞掉 (用户可通过 Progress 回调处理)
  340. _, _ = db.VACUUM(curOpt)
  341. db.autoVacuumMu.Lock()
  342. db.lastVACUUM = time.Now()
  343. stillEnabled := db.autoVacuumEnabled
  344. db.autoVacuumMu.Unlock()
  345. if stillEnabled && db.autoVacuumTimer != nil {
  346. db.autoVacuumTimer.Reset(interval)
  347. }
  348. }
  349. db.autoVacuumTimer = time.AfterFunc(interval, run)
  350. }
  351. // LastVACUUM 返回上次自动 VACUUM 的时间
  352. func (db *DB) LastVACUUM() time.Time {
  353. db.autoVacuumMu.Lock()
  354. defer db.autoVacuumMu.Unlock()
  355. return db.lastVACUUM
  356. }
  357. // ============================================================================
  358. // 辅助
  359. // ============================================================================
  360. // reopenAfterVacuum VACUUM 失败时尝试重新打开 db.path, 让 db 句柄继续可用
  361. //
  362. // 返回 (*VACUUMResult, error): 即使失败也返回 nil result + 错误,
  363. // 调用方处理逻辑一致。
  364. func (db *DB) reopenAfterVacuum(_ *VACUUMResult, origErr error) (*VACUUMResult, error) {
  365. newDB, err := Open(db.path)
  366. if err != nil {
  367. return nil, fmt.Errorf("%w; 且重新打开源库也失败: %v", origErr, err)
  368. }
  369. *db = *newDB
  370. return nil, origErr
  371. }
  372. // isRowStoreType 索引是否为行存储索引 (主键 / 时序)
  373. //
  374. // 行存储索引在 step 5a 已由 VACUUM 自己手动建好, 重建二级索引时跳过, 避免重复。
  375. func isRowStoreType(typ page.IndexType) bool {
  376. return typ == page.IndexTypePrimary || typ == page.IndexTypeTime
  377. }
  378. // 编译期检查 (用到的 import 别漏)
  379. var (
  380. _ = errors.New
  381. _ sync.Mutex
  382. )