vacuum.go 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545
  1. package core
  2. import (
  3. "cmp"
  4. "fmt"
  5. "os"
  6. "path/filepath"
  7. "slices"
  8. "time"
  9. "git.ycl.pub/yl32940/TsDB2/DB/core/page"
  10. "git.ycl.pub/yl32940/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. // rowStore 行存储索引 (主键/时序) 的原始定义; nil = 源表没有
  78. // (core API 允许不建行存储索引, 此时重建也不能凭空建一个)。
  79. rowStore *vacuumSrcIndex
  80. // fieldRemap 旧 master 下标 → 新活跃下标。新表剔除了 DROP 列,
  81. // 列下标整体前移; 不映射就拿旧 FieldIDs 建新索引 → 越界/错位。
  82. fieldRemap map[byte]byte
  83. rows []vacuumSrcRow
  84. // tsKeyName 时序表的时间键列名 (rowStore.typ == Time 时非空),
  85. // 重建前按它排序行, 避免乱序时间戳行触发 ErrOutOfOrder 被静默跳过。
  86. tsKeyName string
  87. }
  88. // vacuumRowTS 取行快照里时间键列的 uint64 值 (时序表重建排序用);
  89. // 缺列或类型不符返回 0。
  90. func vacuumRowTS(vals []types.FieldValue, keyName string) uint64 {
  91. for _, fv := range vals {
  92. if string(fv.FieldName) != keyName {
  93. continue
  94. }
  95. switch v := fv.Value.(type) {
  96. case uint64:
  97. return v
  98. case int64:
  99. return uint64(v)
  100. }
  101. return 0
  102. }
  103. return 0
  104. }
  105. // remapIndexFields 把索引分量的旧 master 下标换算到新表的活跃下标;
  106. // 映射缺失的分量原样保留 (不该发生, 保住可见错误而非静默错位)。
  107. func remapIndexFields(fields []byte, remap map[byte]byte) []byte {
  108. out := make([]byte, len(fields))
  109. for i, f := range fields {
  110. if nf, ok := remap[f]; ok {
  111. out[i] = nf
  112. } else {
  113. out[i] = f
  114. }
  115. }
  116. return out
  117. }
  118. // ============================================================================
  119. // 主入口
  120. // ============================================================================
  121. // VACUUM 整库重写, 让数据库文件真正缩小。
  122. //
  123. // 拒绝条件:
  124. // - 当前有活跃事务 (db.currentTxn != nil)
  125. // - db 路径不可用
  126. //
  127. // 调用后 db 句柄会被关闭并重新打开, 调用者继续使用即可。
  128. func (db *DB) VACUUM(opts ...VACUUMOptions) (*VACUUMResult, error) {
  129. opt := VACUUMOptions{Mode: VACUUMModeSync}
  130. if len(opts) > 0 {
  131. opt = opts[0]
  132. }
  133. if db.currentTxn.Load() != nil {
  134. return nil, fmt.Errorf("%w: VACUUM", ErrTxnActive)
  135. }
  136. if db.path == "" || db.path == os.DevNull {
  137. return nil, fmt.Errorf("%w (%q)", ErrVACUUMNoPath, db.path)
  138. }
  139. // 手动与自动调度可能并发触发: 双开会对同一文件 Close/Open 交错, 必须串行。
  140. db.vacuumMu.Lock()
  141. defer db.vacuumMu.Unlock()
  142. // 操作级串行锁: VACUUM 全程 Close/Open/rename, 必须持有到 adoptEngineState 之后,
  143. // 防止其他 goroutine 在切换间隙操作句柄。内部 db.Close()/db.Commit() 同 goroutine 重入,
  144. // 可重入锁保证它们不会误交这把锁。
  145. db.accMu.Lock()
  146. defer db.accMu.Unlock()
  147. progress := opt.Progress
  148. if progress == nil {
  149. progress = func(string, int64, int64) {}
  150. }
  151. start := time.Now()
  152. // 1. 把 WAL / 脏页全部落盘, 拿到一致快照
  153. if err := db.Commit(); err != nil {
  154. return nil, fmt.Errorf("VACUUM: 提交源库失败: %w", err)
  155. }
  156. srcStat, _ := os.Stat(db.path)
  157. var oldSize int64
  158. if srcStat != nil {
  159. oldSize = srcStat.Size()
  160. }
  161. // 2. 扫描旧库
  162. progress("scan", 0, 0)
  163. sources, totalRows, err := db.vacuumScan(progress)
  164. if err != nil {
  165. return nil, fmt.Errorf("VACUUM: 扫描源库失败: %w", err)
  166. }
  167. // 3. 关闭旧 db (确保所有页都落盘)
  168. if err := db.Close(); err != nil {
  169. _ = os.Remove(db.path + ".vacuum.tmp")
  170. return nil, fmt.Errorf("VACUUM: 关闭源库失败: %w", err)
  171. }
  172. // 4. 在临时路径上重建
  173. tmpPath := db.path + ".vacuum.tmp"
  174. _ = os.Remove(tmpPath) // 清理残留
  175. progress("build", 0, totalRows)
  176. newDB, err := Open(tmpPath)
  177. if err != nil {
  178. return db.reopenAfterVacuum(nil, fmt.Errorf("VACUUM: 打开临时库失败: %w", err))
  179. }
  180. // 5. 重建表 + 索引 + 插入行
  181. var rowsCopied int64
  182. for _, src := range sources {
  183. // 5a. 建表
  184. newTbl, err := newDB.CreateTable(src.name, src.fields)
  185. if err != nil {
  186. _ = newDB.Close()
  187. _ = os.Remove(tmpPath)
  188. return db.reopenAfterVacuum(nil, fmt.Errorf("VACUUM: 重建表 %q 失败: %w", src.name, err))
  189. }
  190. // 行存储索引按【原始名字/原始类型】重建: 旧实现无条件建成
  191. // ("pk", Primary) —— ORM 按 pk_<表名> 寻址, 重建后 Find/Delete/Update
  192. // 全部 ErrFieldNotFound; 时序表则被静默改成主键表, 分段语义丢失。
  193. if src.rowStore != nil {
  194. rsFields := remapIndexFields(src.rowStore.fields, src.fieldRemap)
  195. if _, err := newTbl.CreateIndex(src.rowStore.name, src.rowStore.typ, rsFields); err != nil {
  196. _ = newDB.Close()
  197. _ = os.Remove(tmpPath)
  198. return db.reopenAfterVacuum(nil, fmt.Errorf("VACUUM: 重建行存储索引 %q 失败: %w", src.rowStore.name, err))
  199. }
  200. }
  201. // 5b. 插入全部行 (走主键索引)
  202. for ri, row := range src.rows {
  203. if _, err := newTbl.InsertRowWithID(row.pkID, row.vals); err != nil {
  204. // 单行失败: 跳过 (deserializeRow 失败/罕见约束冲突)
  205. continue
  206. }
  207. rowsCopied++
  208. if ri%1000 == 0 {
  209. progress("build", rowsCopied, totalRows)
  210. }
  211. }
  212. // 周期 commit, 减少 WAL 大小
  213. if err := newDB.Commit(); err != nil {
  214. _ = newDB.Close()
  215. _ = os.Remove(tmpPath)
  216. return db.reopenAfterVacuum(nil, fmt.Errorf("VACUUM: 提交 %q 失败: %w", src.name, err))
  217. }
  218. progress("build", rowsCopied, totalRows)
  219. // 5c. 重建二级索引 (自动批量回填已存在的行)。行存储索引已按
  220. // 原始定义建好, 跳过; FieldIDs 经 fieldRemap 换算到新表下标。
  221. progress("index", rowsCopied, totalRows)
  222. for _, idx := range src.indexes {
  223. if isRowStoreType(idx.typ) {
  224. continue // 行存储索引已在 CreateTable 后手动建好
  225. }
  226. if _, err := newTbl.CreateIndex(idx.name, idx.typ, remapIndexFields(idx.fields, src.fieldRemap), idx.unique); err != nil {
  227. _ = newDB.Close()
  228. _ = os.Remove(tmpPath)
  229. return db.reopenAfterVacuum(nil, fmt.Errorf("VACUUM: 重建索引 %q 失败: %w", idx.name, err))
  230. }
  231. }
  232. progress("index", rowsCopied, totalRows)
  233. }
  234. // 6. 关闭新 db
  235. if err := newDB.Commit(); err != nil {
  236. _ = newDB.Close()
  237. _ = os.Remove(tmpPath)
  238. return db.reopenAfterVacuum(nil, fmt.Errorf("VACUUM: 最终提交失败: %w", err))
  239. }
  240. if err := newDB.Close(); err != nil {
  241. _ = os.Remove(tmpPath)
  242. return db.reopenAfterVacuum(nil, fmt.Errorf("VACUUM: 关闭新库失败: %w", err))
  243. }
  244. // 7. fsync 父目录 (确保 rename 的目录项落盘)
  245. if dir := filepath.Dir(db.path); dir != "" {
  246. if d, derr := os.Open(dir); derr == nil {
  247. _ = d.Sync()
  248. _ = d.Close()
  249. }
  250. }
  251. // 8. 原子替换
  252. progress("swap", 0, 0)
  253. if err := os.Rename(tmpPath, db.path); err != nil {
  254. if !opt.KeepTempOnError {
  255. _ = os.Remove(tmpPath)
  256. }
  257. return db.reopenAfterVacuum(nil, fmt.Errorf("VACUUM: 替换文件失败: %w", err))
  258. }
  259. // 9. 重新打开
  260. progress("done", 0, 0)
  261. newDB2, err := Open(db.path)
  262. if err != nil {
  263. return nil, fmt.Errorf("VACUUM: 重新打开失败: %w", err)
  264. }
  265. db.adoptEngineState(newDB2)
  266. // 10. 统计结果
  267. newStat, _ := os.Stat(db.path)
  268. var newSize int64
  269. if newStat != nil {
  270. newSize = newStat.Size()
  271. }
  272. result := &VACUUMResult{
  273. OldFileSize: oldSize,
  274. NewFileSize: newSize,
  275. SavedBytes: oldSize - newSize,
  276. Tables: len(sources),
  277. RowsCopied: rowsCopied,
  278. Duration: time.Since(start),
  279. Mode: opt.Mode,
  280. }
  281. if oldSize > 0 {
  282. result.SavedPercent = float64(oldSize-newSize) * 100 / float64(oldSize)
  283. }
  284. return result, nil
  285. }
  286. // ============================================================================
  287. // 扫描源库
  288. // ============================================================================
  289. // vacuumScan 扫描源库, 拿到 schema + 索引定义 + 行快照
  290. func (db *DB) vacuumScan(progress VACUUMProgress) ([]vacuumSrcTable, int64, error) {
  291. tables := db.GetAllTables()
  292. sources := make([]vacuumSrcTable, 0, len(tables))
  293. // 先统计总行数 (用于进度回调的 total)
  294. totalRows := int64(0)
  295. for _, meta := range tables {
  296. tbl, ok := db.FindTable(meta.Name)
  297. if !ok {
  298. continue
  299. }
  300. totalRows += int64(tbl.TotalRows())
  301. }
  302. progress("scan", 0, totalRows)
  303. var scanned int64
  304. for _, meta := range tables {
  305. tbl, ok := db.FindTable(meta.Name)
  306. if !ok {
  307. continue
  308. }
  309. // 字段定义 (剔除已 DROP 的列)。同时建立 旧 master 下标 → 新活跃下标
  310. // 的映射: 新表 registry 只含活跃列, 下标整体前移, 索引的 FieldIDs
  311. // 必须按映射换算, 否则"删过列"的表上重建索引必然越界/错位。
  312. fields := tbl.GetFields()
  313. activeFields := make([]types.FieldInfo, 0, len(fields))
  314. fieldRemap := make(map[byte]byte, len(fields))
  315. for i, f := range fields {
  316. if f.IsDropped() {
  317. continue
  318. }
  319. fieldRemap[byte(i)] = byte(len(activeFields))
  320. activeFields = append(activeFields, f)
  321. }
  322. // 索引定义
  323. var srcIndexes []vacuumSrcIndex
  324. var rowStore *vacuumSrcIndex
  325. for _, im := range tbl.IndexMeta().GetIndexes() {
  326. idx := vacuumSrcIndex{
  327. name: im.Name(),
  328. typ: im.IndexType,
  329. fields: append([]byte(nil), im.Fields()...),
  330. unique: im.Unique(),
  331. }
  332. srcIndexes = append(srcIndexes, idx)
  333. if isRowStoreType(im.IndexType) {
  334. captured := idx
  335. rowStore = &captured
  336. }
  337. }
  338. // 行快照 (保留 NULL: deserializeRow 会跳过 NULL 列, VACUUM 会把
  339. // NULL 物化成类型零值 —— 语义漂移且不可逆)。
  340. rows := make([]vacuumSrcRow, 0, tbl.TotalRows())
  341. q := tbl.Query()
  342. scanErr := q.fullTableScanRaw(func(pkID uint64, rowData []byte) bool {
  343. vals := tbl.deserializeRowKeepNull(rowData)
  344. if vals == nil {
  345. return true // 解析失败, 跳过
  346. }
  347. rows = append(rows, vacuumSrcRow{pkID: pkID, vals: vals})
  348. scanned++
  349. if scanned%10000 == 0 {
  350. progress("scan", scanned, totalRows)
  351. }
  352. return true
  353. })
  354. if scanErr != nil {
  355. return nil, 0, fmt.Errorf("扫描表 %q: %w", meta.Name, scanErr)
  356. }
  357. // 时序表: 重建前按时间键排序。时序索引断言时间有序, 按 pkID 序
  358. // 插入乱序时间戳行会 ErrOutOfOrder 被静默跳过 (数据丢失)。
  359. tsKeyName := ""
  360. if rowStore != nil && rowStore.typ == page.IndexTypeTime && len(rowStore.fields) > 0 {
  361. fi := int(rowStore.fields[0])
  362. if fi < len(fields) {
  363. tsKeyName = string(fields[fi].FieldName)
  364. }
  365. slices.SortStableFunc(rows, func(a, b vacuumSrcRow) int {
  366. return cmp.Compare(vacuumRowTS(a.vals, tsKeyName), vacuumRowTS(b.vals, tsKeyName))
  367. })
  368. }
  369. sources = append(sources, vacuumSrcTable{
  370. name: meta.Name,
  371. fields: activeFields,
  372. indexes: srcIndexes,
  373. rowStore: rowStore,
  374. fieldRemap: fieldRemap,
  375. tsKeyName: tsKeyName,
  376. rows: rows,
  377. })
  378. }
  379. progress("scan", totalRows, totalRows)
  380. return sources, totalRows, nil
  381. }
  382. // ============================================================================
  383. // 自动调度
  384. // ============================================================================
  385. // SetAutoVACUUM 开启自动 VACUUM, 周期触发。
  386. //
  387. // interval: 两次 VACUUM 的最小间隔 (例如 1*time.Hour)
  388. // options: 每次 VACUUM 使用的选项
  389. //
  390. // 关闭: SetAutoVACUUM(0)
  391. func (db *DB) SetAutoVACUUM(interval time.Duration, opts ...VACUUMOptions) {
  392. db.autoVacuumMu.Lock()
  393. defer db.autoVacuumMu.Unlock()
  394. if interval <= 0 {
  395. if db.autoVacuumTimer != nil {
  396. db.autoVacuumTimer.Stop()
  397. db.autoVacuumTimer = nil
  398. }
  399. db.autoVacuumEnabled = false
  400. return
  401. }
  402. opt := VACUUMOptions{Mode: VACUUMModeSync}
  403. if len(opts) > 0 {
  404. opt = opts[0]
  405. }
  406. db.autoVacuumOpts = opt
  407. db.autoVacuumEnabled = true
  408. db.lastVACUUM = time.Now()
  409. run := func() {
  410. db.autoVacuumMu.Lock()
  411. enabled := db.autoVacuumEnabled
  412. curOpt := db.autoVacuumOpts
  413. db.autoVacuumMu.Unlock()
  414. if !enabled {
  415. return
  416. }
  417. // VACUUM 本体拿 vacuumMu 串行: 与并发手动 VACUUM 排队执行,
  418. // 不会双开对同一文件 Close/Open 交错。
  419. // 自动调度: 失败也吞掉 (用户可通过 Progress 回调处理)
  420. _, _ = db.VACUUM(curOpt)
  421. db.autoVacuumMu.Lock()
  422. db.lastVACUUM = time.Now()
  423. stillEnabled := db.autoVacuumEnabled
  424. // Reset 必须在锁内: SetAutoVACUUM(0) 会并发把 timer 置 nil,
  425. // 锁外读/重置是 data race (race detector 实测抓到)。
  426. if stillEnabled && db.autoVacuumTimer != nil {
  427. db.autoVacuumTimer.Reset(interval)
  428. }
  429. db.autoVacuumMu.Unlock()
  430. }
  431. db.autoVacuumTimer = time.AfterFunc(interval, run)
  432. }
  433. // LastVACUUM 返回上次自动 VACUUM 的时间
  434. func (db *DB) LastVACUUM() time.Time {
  435. db.autoVacuumMu.Lock()
  436. defer db.autoVacuumMu.Unlock()
  437. return db.lastVACUUM
  438. }
  439. // ============================================================================
  440. // 辅助
  441. // ============================================================================
  442. // reopenAfterVacuum VACUUM 失败时尝试重新打开 db.path, 让 db 句柄继续可用
  443. //
  444. // 返回 (*VACUUMResult, error): 即使失败也返回 nil result + 错误,
  445. // 调用方处理逻辑一致。
  446. func (db *DB) reopenAfterVacuum(_ *VACUUMResult, origErr error) (*VACUUMResult, error) {
  447. newDB, err := Open(db.path)
  448. if err != nil {
  449. return nil, fmt.Errorf("%w; 且重新打开源库也失败: %v", origErr, err)
  450. }
  451. db.adoptEngineState(newDB)
  452. return nil, origErr
  453. }
  454. // adoptEngineState 用 other (刚重新打开的句柄) 的引擎状态替换 db 的,
  455. // 保留 db 上用户配置的运行时参数 (autoCommitDelay / autoVacuum 调度 / lastVACUUM)。
  456. //
  457. // VACUUM 完成后 db.path 已被替换成重建的文件, 必须换引擎状态才能继续使用句柄;
  458. // 但不能整体 `*db = *other`: DB 含多把互斥锁, 结构体整体赋值会把锁的内部状态
  459. // 一并按值拷贝 (go vet copylocks 报错), 更糟的是会把用户配置覆盖成零值 ——
  460. // autoVacuumEnabled 变回 false 导致自动 VACUUM 首轮之后不再重排,
  461. // autoCommitDelay 被重置成 0 导致防抖自动提交静默失效。
  462. func (db *DB) adoptEngineState(other *DB) {
  463. db.file = other.file
  464. db.header = other.header
  465. db.tables = other.tables
  466. db.currentTxn.Store(nil)
  467. db.pendingFree = nil
  468. db.pendingFreeChain = nil
  469. db.freePage = other.freePage
  470. db.freeChain = other.freeChain
  471. // other 的表树在 Open 时把内部 db 指针绑到了 other (bindDB 只补 nil, 不会改绑),
  472. // 必须强制改绑到本句柄, 否则后续表操作会读到 other 的幽灵状态
  473. // (独立的锁、永远为 nil 的事务指针)。
  474. db.tables.db = db
  475. for _, tbl := range db.tables.tables {
  476. tbl.db = db
  477. }
  478. // 运行期回调与句柄闭包重绑到本句柄: other 的 onPageWrite / autoCommit /
  479. // handle 闭包捕获的是 other —— 不重绑的话, VACUUM 之后开事务,
  480. // markTouched 查的是 other.currentTxn (恒 nil), Rollback 静默失效;
  481. // 新页登记到 other.allocated (回滚后成孤儿页); 延迟回收进 other.pendingFree
  482. // (永不 drain, 页永久泄漏)。
  483. db.wireRuntime()
  484. }
  485. // isRowStoreType 索引是否为行存储索引 (主键 / 时序)
  486. //
  487. // 行存储索引在 step 5a 已由 VACUUM 自己手动建好, 重建二级索引时跳过, 避免重复。
  488. func isRowStoreType(typ page.IndexType) bool {
  489. return typ == page.IndexTypePrimary || typ == page.IndexTypeTime
  490. }