vacuum.go 17 KB

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