main.go 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146
  1. // orphanaudit — VRTX 孤儿页取证工具。
  2. //
  3. // orphanaudit -db <副本路径> [-samples 3] [-noengine]
  4. //
  5. // 输出两套口径并排:
  6. //
  7. // - analyzer 口径: 与 analyzer/audit.go 完全同批可达根 (不走 blob 链)。
  8. // 与 analyzer 报告对账用 —— 两个数字必须相等, 不等说明本工具移植有 bug。
  9. // - blob 感知口径: 再走行内 BlobRef → blob 链 (Type=9)。剩下的才是真实泄漏。
  10. //
  11. // 绝不直接对原件跑: 先用 cp 复制数据库文件, 只对副本运行。
  12. // 引擎只读打开 (O_RDONLY + 共享锁, 跳过 WAL 恢复), 不会写盘。
  13. package main
  14. import (
  15. "encoding/json"
  16. "flag"
  17. "fmt"
  18. "os"
  19. "git.ycl.pub/yl32940/TsDB2/internal/orphancheck"
  20. )
  21. func main() {
  22. db := flag.String("db", "", "VRTX 文件路径 (必须是副本!)")
  23. samples := flag.Int("samples", 3, "每种类型的样例页数")
  24. noEngine := flag.Bool("noengine", false, "不用引擎只读扫描 blob 链 (纯 analyzer 口径)")
  25. jsonOut := flag.Bool("json", false, "JSON 输出")
  26. flag.Parse()
  27. if *db == "" {
  28. fmt.Fprintln(os.Stderr, "用法: orphanaudit -db <副本路径> [-samples N] [-noengine] [-json]")
  29. os.Exit(2)
  30. }
  31. R, err := orphancheck.Run(*db, orphancheck.Options{
  32. EngineBlobScan: !*noEngine,
  33. SamplesPerType: *samples,
  34. })
  35. if err != nil {
  36. fmt.Fprintln(os.Stderr, "取证失败:", err)
  37. os.Exit(2)
  38. }
  39. if *jsonOut {
  40. enc := map[string]any{
  41. "file": R.File, "totalPages": R.TotalPages, "nextFreePage": R.NextFreePage,
  42. "freeCount": R.FreeCount, "freeDeclared": R.FreeDeclared, "freeVerdict": R.FreeVerdict,
  43. "walPending": R.WALPending, "reachableNoBlob": R.ReachableNoBlob, "blobPages": R.BlobPages,
  44. "analyzerOrphans": len(R.AnalyzerOrphans), "analyzerByType": R.AnalyzerByType,
  45. "analyzerWalLeftover": R.AnalyzerWalLeft,
  46. "orphans": len(R.Orphans), "byType": R.ByType, "walLeftover": R.WalLeft,
  47. "brokenChains": R.BrokenChains, "notes": R.Notes,
  48. "orphanIds": ids(R.Orphans), "analyzerOrphanIds": ids(R.AnalyzerOrphans),
  49. "samples": hexSamples(R),
  50. }
  51. writeJSON(enc)
  52. return
  53. }
  54. printReport(R)
  55. }
  56. func writeJSON(v any) {
  57. enc := json.NewEncoder(os.Stdout)
  58. enc.SetIndent("", " ")
  59. _ = enc.Encode(v)
  60. }
  61. func ids(ps []orphancheck.PageInfo) []uint32 {
  62. out := make([]uint32, len(ps))
  63. for i, p := range ps {
  64. out[i] = p.ID
  65. }
  66. return out
  67. }
  68. func printReport(R *orphancheck.Result) {
  69. fmt.Printf("== 文件: %s\n", R.File)
  70. fmt.Printf("总页数=%d NextFreePage=%d 空闲链=%d 页 (header 声明 %d, 判定: %s)\n",
  71. R.TotalPages, R.NextFreePage, R.FreeCount, R.FreeDeclared, R.FreeVerdict)
  72. if R.WALPending > 0 {
  73. fmt.Printf("未恢复 WAL 链: %d 页 (只读打开跳过恢复)\n", R.WALPending)
  74. }
  75. fmt.Printf("结构可达 (analyzer 口径, 含页 1)=%d 页; 引擎扫描认领 blob 链页=%d\n\n",
  76. R.ReachableNoBlob, R.BlobPages)
  77. fmt.Printf("-- analyzer 口径孤儿页 = %d\n", len(R.AnalyzerOrphans))
  78. printHist(R.AnalyzerByType, R.AnalyzerWalLeft)
  79. fmt.Printf("-- blob 感知口径真正孤儿页 = %d\n", len(R.Orphans))
  80. printHist(R.ByType, R.WalLeft)
  81. if len(R.Chains) > 0 {
  82. fmt.Printf("\nblob 链: 共 %d 条, 断裂 %d 条\n", len(R.Chains), R.BrokenChains)
  83. }
  84. for _, n := range R.Notes {
  85. fmt.Println("备注:", n)
  86. }
  87. if len(R.Orphans) > 0 && len(R.Samples) > 0 {
  88. fmt.Println("\n== 孤儿页样例 (头 64B hex) ==")
  89. for _, tn := range orphancheck.SortedTypes(R.ByType) {
  90. for _, p := range R.Samples[tn] {
  91. fmt.Printf("-- 页 %d 类型 %s UseSize=%d NextPage=%d\n", p.ID, tn, p.UseSize, p.Next)
  92. fmt.Print(orphancheck.SampleHex(p))
  93. }
  94. }
  95. }
  96. if d := len(R.AnalyzerOrphans) - len(R.Orphans); d > 0 {
  97. fmt.Printf("\n结论: analyzer 口径有 %d 页『孤儿』, 其中 %d 页是【活 blob 链页】"+
  98. " —— analyzer 不走 blob 链造成的误报; 真正泄漏 %d 页。\n", len(R.AnalyzerOrphans), d, len(R.Orphans))
  99. } else if len(R.Orphans) > 0 {
  100. fmt.Printf("\n结论: %d 页真实泄漏 (blob 走查后仍不可达), 按上方类型直方图指认路径。\n", len(R.Orphans))
  101. } else {
  102. fmt.Println("\n结论: 无真实孤儿页。")
  103. }
  104. }
  105. func printHist(m map[string]int, walLeft int) {
  106. if len(m) == 0 {
  107. fmt.Println(" (无)")
  108. return
  109. }
  110. for _, tn := range orphancheck.SortedTypes(m) {
  111. fmt.Printf(" %-24s %d\n", tn, m[tn])
  112. }
  113. if walLeft > 0 {
  114. fmt.Printf(" 其中 WAL(7) 页 = 已提交 WAL 链未回收 (引擎泄漏)\n")
  115. }
  116. }
  117. func hexSamples(R *orphancheck.Result) map[string]any {
  118. out := map[string]any{}
  119. for tn, ps := range R.Samples {
  120. var rows []map[string]any
  121. for _, p := range ps {
  122. rows = append(rows, map[string]any{
  123. "id": p.ID, "useSize": p.UseSize, "next": p.Next,
  124. "head64": orphancheck.SampleHex(p),
  125. })
  126. }
  127. out[tn] = rows
  128. }
  129. return out
  130. }