You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

397 lines
11 KiB

  1. // Copyright (c) 2017 Couchbase, Inc.
  2. //
  3. // Licensed under the Apache License, Version 2.0 (the "License");
  4. // you may not use this file except in compliance with the License.
  5. // You may obtain a copy of the License at
  6. //
  7. // http://www.apache.org/licenses/LICENSE-2.0
  8. //
  9. // Unless required by applicable law or agreed to in writing, software
  10. // distributed under the License is distributed on an "AS IS" BASIS,
  11. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. // See the License for the specific language governing permissions and
  13. // limitations under the License.
  14. // Package mergeplan provides a segment merge planning approach that's
  15. // inspired by Lucene's TieredMergePolicy.java and descriptions like
  16. // http://blog.mikemccandless.com/2011/02/visualizing-lucenes-segment-merges.html
  17. package mergeplan
  18. import (
  19. "errors"
  20. "fmt"
  21. "math"
  22. "sort"
  23. "strings"
  24. )
  25. // A Segment represents the information that the planner needs to
  26. // calculate segment merging.
  27. type Segment interface {
  28. // Unique id of the segment -- used for sorting.
  29. Id() uint64
  30. // Full segment size (the size before any logical deletions).
  31. FullSize() int64
  32. // Size of the live data of the segment; i.e., FullSize() minus
  33. // any logical deletions.
  34. LiveSize() int64
  35. }
  36. // Plan() will functionally compute a merge plan. A segment will be
  37. // assigned to at most a single MergeTask in the output MergePlan. A
  38. // segment not assigned to any MergeTask means the segment should
  39. // remain unmerged.
  40. func Plan(segments []Segment, o *MergePlanOptions) (*MergePlan, error) {
  41. return plan(segments, o)
  42. }
  43. // A MergePlan is the result of the Plan() API.
  44. //
  45. // The planner doesn’t know how or whether these tasks are executed --
  46. // that’s up to a separate merge execution system, which might execute
  47. // these tasks concurrently or not, and which might execute all the
  48. // tasks or not.
  49. type MergePlan struct {
  50. Tasks []*MergeTask
  51. }
  52. // A MergeTask represents several segments that should be merged
  53. // together into a single segment.
  54. type MergeTask struct {
  55. Segments []Segment
  56. }
  57. // The MergePlanOptions is designed to be reusable between planning calls.
  58. type MergePlanOptions struct {
  59. // Max # segments per logarithmic tier, or max width of any
  60. // logarithmic “step”. Smaller values mean more merging but fewer
  61. // segments. Should be >= SegmentsPerMergeTask, else you'll have
  62. // too much merging.
  63. MaxSegmentsPerTier int
  64. // Max size of any segment produced after merging. Actual
  65. // merging, however, may produce segment sizes different than the
  66. // planner’s predicted sizes.
  67. MaxSegmentSize int64
  68. // The growth factor for each tier in a staircase of idealized
  69. // segments computed by CalcBudget().
  70. TierGrowth float64
  71. // The number of segments in any resulting MergeTask. e.g.,
  72. // len(result.Tasks[ * ].Segments) == SegmentsPerMergeTask.
  73. SegmentsPerMergeTask int
  74. // Small segments are rounded up to this size, i.e., treated as
  75. // equal (floor) size for consideration. This is to prevent lots
  76. // of tiny segments from resulting in a long tail in the index.
  77. FloorSegmentSize int64
  78. // Controls how aggressively merges that reclaim more deletions
  79. // are favored. Higher values will more aggressively target
  80. // merges that reclaim deletions, but be careful not to go so high
  81. // that way too much merging takes place; a value of 3.0 is
  82. // probably nearly too high. A value of 0.0 means deletions don't
  83. // impact merge selection.
  84. ReclaimDeletesWeight float64
  85. // Optional, defaults to mergeplan.CalcBudget().
  86. CalcBudget func(totalSize int64, firstTierSize int64,
  87. o *MergePlanOptions) (budgetNumSegments int)
  88. // Optional, defaults to mergeplan.ScoreSegments().
  89. ScoreSegments func(segments []Segment, o *MergePlanOptions) float64
  90. // Optional.
  91. Logger func(string)
  92. }
  93. // Returns the higher of the input or FloorSegmentSize.
  94. func (o *MergePlanOptions) RaiseToFloorSegmentSize(s int64) int64 {
  95. if s > o.FloorSegmentSize {
  96. return s
  97. }
  98. return o.FloorSegmentSize
  99. }
  100. // MaxSegmentSizeLimit represents the maximum size of a segment,
  101. // this limit comes with hit-1 optimisation/max encoding limit uint31.
  102. const MaxSegmentSizeLimit = 1<<31 - 1
  103. // ErrMaxSegmentSizeTooLarge is returned when the size of the segment
  104. // exceeds the MaxSegmentSizeLimit
  105. var ErrMaxSegmentSizeTooLarge = errors.New("MaxSegmentSize exceeds the size limit")
  106. // DefaultMergePlanOptions suggests the default options.
  107. var DefaultMergePlanOptions = MergePlanOptions{
  108. MaxSegmentsPerTier: 10,
  109. MaxSegmentSize: 5000000,
  110. TierGrowth: 10.0,
  111. SegmentsPerMergeTask: 10,
  112. FloorSegmentSize: 2000,
  113. ReclaimDeletesWeight: 2.0,
  114. }
  115. // SingleSegmentMergePlanOptions helps in creating a
  116. // single segment index.
  117. var SingleSegmentMergePlanOptions = MergePlanOptions{
  118. MaxSegmentsPerTier: 1,
  119. MaxSegmentSize: 1 << 30,
  120. TierGrowth: 1.0,
  121. SegmentsPerMergeTask: 10,
  122. FloorSegmentSize: 1 << 30,
  123. ReclaimDeletesWeight: 2.0,
  124. }
  125. // -------------------------------------------
  126. func plan(segmentsIn []Segment, o *MergePlanOptions) (*MergePlan, error) {
  127. if len(segmentsIn) <= 1 {
  128. return nil, nil
  129. }
  130. if o == nil {
  131. o = &DefaultMergePlanOptions
  132. }
  133. segments := append([]Segment(nil), segmentsIn...) // Copy.
  134. sort.Sort(byLiveSizeDescending(segments))
  135. var minLiveSize int64 = math.MaxInt64
  136. var eligibles []Segment
  137. var eligiblesLiveSize int64
  138. for _, segment := range segments {
  139. if minLiveSize > segment.LiveSize() {
  140. minLiveSize = segment.LiveSize()
  141. }
  142. // Only small-enough segments are eligible.
  143. if segment.LiveSize() < o.MaxSegmentSize/2 {
  144. eligibles = append(eligibles, segment)
  145. eligiblesLiveSize += segment.LiveSize()
  146. }
  147. }
  148. minLiveSize = o.RaiseToFloorSegmentSize(minLiveSize)
  149. calcBudget := o.CalcBudget
  150. if calcBudget == nil {
  151. calcBudget = CalcBudget
  152. }
  153. budgetNumSegments := calcBudget(eligiblesLiveSize, minLiveSize, o)
  154. scoreSegments := o.ScoreSegments
  155. if scoreSegments == nil {
  156. scoreSegments = ScoreSegments
  157. }
  158. rv := &MergePlan{}
  159. var empties []Segment
  160. for _, eligible := range eligibles {
  161. if eligible.LiveSize() <= 0 {
  162. empties = append(empties, eligible)
  163. }
  164. }
  165. if len(empties) > 0 {
  166. rv.Tasks = append(rv.Tasks, &MergeTask{Segments: empties})
  167. eligibles = removeSegments(eligibles, empties)
  168. }
  169. // While we’re over budget, keep looping, which might produce
  170. // another MergeTask.
  171. for len(eligibles) > 0 && (len(eligibles)+len(rv.Tasks)) > budgetNumSegments {
  172. // Track a current best roster as we examine and score
  173. // potential rosters of merges.
  174. var bestRoster []Segment
  175. var bestRosterScore float64 // Lower score is better.
  176. for startIdx := 0; startIdx < len(eligibles); startIdx++ {
  177. var roster []Segment
  178. var rosterLiveSize int64
  179. for idx := startIdx; idx < len(eligibles) && len(roster) < o.SegmentsPerMergeTask; idx++ {
  180. eligible := eligibles[idx]
  181. if rosterLiveSize+eligible.LiveSize() < o.MaxSegmentSize {
  182. roster = append(roster, eligible)
  183. rosterLiveSize += eligible.LiveSize()
  184. }
  185. }
  186. if len(roster) > 0 {
  187. rosterScore := scoreSegments(roster, o)
  188. if len(bestRoster) == 0 || rosterScore < bestRosterScore {
  189. bestRoster = roster
  190. bestRosterScore = rosterScore
  191. }
  192. }
  193. }
  194. if len(bestRoster) == 0 {
  195. return rv, nil
  196. }
  197. rv.Tasks = append(rv.Tasks, &MergeTask{Segments: bestRoster})
  198. eligibles = removeSegments(eligibles, bestRoster)
  199. }
  200. return rv, nil
  201. }
  202. // Compute the number of segments that would be needed to cover the
  203. // totalSize, by climbing up a logarithmically growing staircase of
  204. // segment tiers.
  205. func CalcBudget(totalSize int64, firstTierSize int64, o *MergePlanOptions) (
  206. budgetNumSegments int) {
  207. tierSize := firstTierSize
  208. if tierSize < 1 {
  209. tierSize = 1
  210. }
  211. maxSegmentsPerTier := o.MaxSegmentsPerTier
  212. if maxSegmentsPerTier < 1 {
  213. maxSegmentsPerTier = 1
  214. }
  215. tierGrowth := o.TierGrowth
  216. if tierGrowth < 1.0 {
  217. tierGrowth = 1.0
  218. }
  219. for totalSize > 0 {
  220. segmentsInTier := float64(totalSize) / float64(tierSize)
  221. if segmentsInTier < float64(maxSegmentsPerTier) {
  222. budgetNumSegments += int(math.Ceil(segmentsInTier))
  223. break
  224. }
  225. budgetNumSegments += maxSegmentsPerTier
  226. totalSize -= int64(maxSegmentsPerTier) * tierSize
  227. tierSize = int64(float64(tierSize) * tierGrowth)
  228. }
  229. return budgetNumSegments
  230. }
  231. // Of note, removeSegments() keeps the ordering of the results stable.
  232. func removeSegments(segments []Segment, toRemove []Segment) []Segment {
  233. rv := make([]Segment, 0, len(segments)-len(toRemove))
  234. OUTER:
  235. for _, segment := range segments {
  236. for _, r := range toRemove {
  237. if segment == r {
  238. continue OUTER
  239. }
  240. }
  241. rv = append(rv, segment)
  242. }
  243. return rv
  244. }
  245. // Smaller result score is better.
  246. func ScoreSegments(segments []Segment, o *MergePlanOptions) float64 {
  247. var totBeforeSize int64
  248. var totAfterSize int64
  249. var totAfterSizeFloored int64
  250. for _, segment := range segments {
  251. totBeforeSize += segment.FullSize()
  252. totAfterSize += segment.LiveSize()
  253. totAfterSizeFloored += o.RaiseToFloorSegmentSize(segment.LiveSize())
  254. }
  255. if totBeforeSize <= 0 || totAfterSize <= 0 || totAfterSizeFloored <= 0 {
  256. return 0
  257. }
  258. // Roughly guess the "balance" of the segments -- whether the
  259. // segments are about the same size.
  260. balance :=
  261. float64(o.RaiseToFloorSegmentSize(segments[0].LiveSize())) /
  262. float64(totAfterSizeFloored)
  263. // Gently favor smaller merges over bigger ones. We don't want to
  264. // make the exponent too large else we end up with poor merges of
  265. // small segments in order to avoid the large merges.
  266. score := balance * math.Pow(float64(totAfterSize), 0.05)
  267. // Strongly favor merges that reclaim deletes.
  268. nonDelRatio := float64(totAfterSize) / float64(totBeforeSize)
  269. score *= math.Pow(nonDelRatio, o.ReclaimDeletesWeight)
  270. return score
  271. }
  272. // ------------------------------------------
  273. // ToBarChart returns an ASCII rendering of the segments and the plan.
  274. // The barMax is the max width of the bars in the bar chart.
  275. func ToBarChart(prefix string, barMax int, segments []Segment, plan *MergePlan) string {
  276. rv := make([]string, 0, len(segments))
  277. var maxFullSize int64
  278. for _, segment := range segments {
  279. if maxFullSize < segment.FullSize() {
  280. maxFullSize = segment.FullSize()
  281. }
  282. }
  283. if maxFullSize < 0 {
  284. maxFullSize = 1
  285. }
  286. for _, segment := range segments {
  287. barFull := int(segment.FullSize())
  288. barLive := int(segment.LiveSize())
  289. if maxFullSize > int64(barMax) {
  290. barFull = int(float64(barMax) * float64(barFull) / float64(maxFullSize))
  291. barLive = int(float64(barMax) * float64(barLive) / float64(maxFullSize))
  292. }
  293. barKind := " "
  294. barChar := "."
  295. if plan != nil {
  296. TASK_LOOP:
  297. for taski, task := range plan.Tasks {
  298. for _, taskSegment := range task.Segments {
  299. if taskSegment == segment {
  300. barKind = "*"
  301. barChar = fmt.Sprintf("%d", taski)
  302. break TASK_LOOP
  303. }
  304. }
  305. }
  306. }
  307. bar :=
  308. strings.Repeat(barChar, barLive)[0:barLive] +
  309. strings.Repeat("x", barFull-barLive)[0:barFull-barLive]
  310. rv = append(rv, fmt.Sprintf("%s %5d: %5d /%5d - %s %s", prefix,
  311. segment.Id(),
  312. segment.LiveSize(),
  313. segment.FullSize(),
  314. barKind, bar))
  315. }
  316. return strings.Join(rv, "\n")
  317. }
  318. // ValidateMergePlannerOptions validates the merge planner options
  319. func ValidateMergePlannerOptions(options *MergePlanOptions) error {
  320. if options.MaxSegmentSize > MaxSegmentSizeLimit {
  321. return ErrMaxSegmentSizeTooLarge
  322. }
  323. return nil
  324. }