github.com/lalkh/containerd@v1.4.3/gc/scheduler/scheduler.go (about) 1 /* 2 Copyright The containerd Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package scheduler 18 19 import ( 20 "context" 21 "fmt" 22 "sync" 23 "time" 24 25 "github.com/containerd/containerd/gc" 26 "github.com/containerd/containerd/log" 27 "github.com/containerd/containerd/plugin" 28 "github.com/pkg/errors" 29 ) 30 31 // config configures the garbage collection policies. 32 type config struct { 33 // PauseThreshold represents the maximum amount of time garbage 34 // collection should be scheduled based on the average pause time. 35 // For example, a value of 0.02 means that scheduled garbage collection 36 // pauses should present at most 2% of real time, 37 // or 20ms of every second. 38 // 39 // A maximum value of .5 is enforced to prevent over scheduling of the 40 // garbage collector, trigger options are available to run in a more 41 // predictable time frame after mutation. 42 // 43 // Default is 0.02 44 PauseThreshold float64 `toml:"pause_threshold"` 45 46 // DeletionThreshold is used to guarantee that a garbage collection is 47 // scheduled after configured number of deletions have occurred 48 // since the previous garbage collection. A value of 0 indicates that 49 // garbage collection will not be triggered by deletion count. 50 // 51 // Default 0 52 DeletionThreshold int `toml:"deletion_threshold"` 53 54 // MutationThreshold is used to guarantee that a garbage collection is 55 // run after a configured number of database mutations have occurred 56 // since the previous garbage collection. A value of 0 indicates that 57 // garbage collection will only be run after a manual trigger or 58 // deletion. Unlike the deletion threshold, the mutation threshold does 59 // not cause scheduling of a garbage collection, but ensures GC is run 60 // at the next scheduled GC. 61 // 62 // Default 100 63 MutationThreshold int `toml:"mutation_threshold"` 64 65 // ScheduleDelay is the duration in the future to schedule a garbage 66 // collection triggered manually or by exceeding the configured 67 // threshold for deletion or mutation. A zero value will immediately 68 // schedule. Use suffix "ms" for millisecond and "s" for second. 69 // 70 // Default is "0ms" 71 ScheduleDelay duration `toml:"schedule_delay"` 72 73 // StartupDelay is the delay duration to do an initial garbage 74 // collection after startup. The initial garbage collection is used to 75 // set the base for pause threshold and should be scheduled in the 76 // future to avoid slowing down other startup processes. Use suffix 77 // "ms" for millisecond and "s" for second. 78 // 79 // Default is "100ms" 80 StartupDelay duration `toml:"startup_delay"` 81 } 82 83 type duration time.Duration 84 85 func (d *duration) UnmarshalText(text []byte) error { 86 ed, err := time.ParseDuration(string(text)) 87 if err != nil { 88 return err 89 } 90 *d = duration(ed) 91 return nil 92 } 93 94 func (d duration) MarshalText() (text []byte, err error) { 95 return []byte(time.Duration(d).String()), nil 96 } 97 98 func init() { 99 plugin.Register(&plugin.Registration{ 100 Type: plugin.GCPlugin, 101 ID: "scheduler", 102 Requires: []plugin.Type{ 103 plugin.MetadataPlugin, 104 }, 105 Config: &config{ 106 PauseThreshold: 0.02, 107 DeletionThreshold: 0, 108 MutationThreshold: 100, 109 ScheduleDelay: duration(0), 110 StartupDelay: duration(100 * time.Millisecond), 111 }, 112 InitFn: func(ic *plugin.InitContext) (interface{}, error) { 113 md, err := ic.Get(plugin.MetadataPlugin) 114 if err != nil { 115 return nil, err 116 } 117 118 mdCollector, ok := md.(collector) 119 if !ok { 120 return nil, errors.Errorf("%s %T must implement collector", plugin.MetadataPlugin, md) 121 } 122 123 m := newScheduler(mdCollector, ic.Config.(*config)) 124 125 ic.Meta.Exports = map[string]string{ 126 "PauseThreshold": fmt.Sprint(m.pauseThreshold), 127 "DeletionThreshold": fmt.Sprint(m.deletionThreshold), 128 "MutationThreshold": fmt.Sprint(m.mutationThreshold), 129 "ScheduleDelay": fmt.Sprint(m.scheduleDelay), 130 } 131 132 go m.run(ic.Context) 133 134 return m, nil 135 }, 136 }) 137 } 138 139 type mutationEvent struct { 140 ts time.Time 141 mutation bool 142 dirty bool 143 } 144 145 type collector interface { 146 RegisterMutationCallback(func(bool)) 147 GarbageCollect(context.Context) (gc.Stats, error) 148 } 149 150 type gcScheduler struct { 151 c collector 152 153 eventC chan mutationEvent 154 155 waiterL sync.Mutex 156 waiters []chan gc.Stats 157 158 pauseThreshold float64 159 deletionThreshold int 160 mutationThreshold int 161 scheduleDelay time.Duration 162 startupDelay time.Duration 163 } 164 165 func newScheduler(c collector, cfg *config) *gcScheduler { 166 eventC := make(chan mutationEvent) 167 168 s := &gcScheduler{ 169 c: c, 170 eventC: eventC, 171 pauseThreshold: cfg.PauseThreshold, 172 deletionThreshold: cfg.DeletionThreshold, 173 mutationThreshold: cfg.MutationThreshold, 174 scheduleDelay: time.Duration(cfg.ScheduleDelay), 175 startupDelay: time.Duration(cfg.StartupDelay), 176 } 177 178 if s.pauseThreshold < 0.0 { 179 s.pauseThreshold = 0.0 180 } 181 if s.pauseThreshold > 0.5 { 182 s.pauseThreshold = 0.5 183 } 184 if s.mutationThreshold < 0 { 185 s.mutationThreshold = 0 186 } 187 if s.scheduleDelay < 0 { 188 s.scheduleDelay = 0 189 } 190 if s.startupDelay < 0 { 191 s.startupDelay = 0 192 } 193 194 c.RegisterMutationCallback(s.mutationCallback) 195 196 return s 197 } 198 199 func (s *gcScheduler) ScheduleAndWait(ctx context.Context) (gc.Stats, error) { 200 return s.wait(ctx, true) 201 } 202 203 func (s *gcScheduler) wait(ctx context.Context, trigger bool) (gc.Stats, error) { 204 wc := make(chan gc.Stats, 1) 205 s.waiterL.Lock() 206 s.waiters = append(s.waiters, wc) 207 s.waiterL.Unlock() 208 209 if trigger { 210 e := mutationEvent{ 211 ts: time.Now(), 212 } 213 go func() { 214 s.eventC <- e 215 }() 216 } 217 218 var gcStats gc.Stats 219 select { 220 case stats, ok := <-wc: 221 if !ok { 222 return gcStats, errors.New("gc failed") 223 } 224 gcStats = stats 225 case <-ctx.Done(): 226 return gcStats, ctx.Err() 227 } 228 229 return gcStats, nil 230 } 231 232 func (s *gcScheduler) mutationCallback(dirty bool) { 233 e := mutationEvent{ 234 ts: time.Now(), 235 mutation: true, 236 dirty: dirty, 237 } 238 go func() { 239 s.eventC <- e 240 }() 241 } 242 243 func schedule(d time.Duration) (<-chan time.Time, *time.Time) { 244 next := time.Now().Add(d) 245 return time.After(d), &next 246 } 247 248 func (s *gcScheduler) run(ctx context.Context) { 249 var ( 250 schedC <-chan time.Time 251 252 lastCollection *time.Time 253 nextCollection *time.Time 254 255 interval = time.Second 256 gcTime time.Duration 257 collections int 258 // TODO(dmcg): expose collection stats as metrics 259 260 triggered bool 261 deletions int 262 mutations int 263 ) 264 if s.startupDelay > 0 { 265 schedC, nextCollection = schedule(s.startupDelay) 266 } 267 for { 268 select { 269 case <-schedC: 270 // Check if garbage collection can be skipped because 271 // it is not needed or was not requested and reschedule 272 // it to attempt again after another time interval. 273 if !triggered && lastCollection != nil && deletions == 0 && 274 (s.mutationThreshold == 0 || mutations < s.mutationThreshold) { 275 schedC, nextCollection = schedule(interval) 276 continue 277 } 278 case e := <-s.eventC: 279 if lastCollection != nil && lastCollection.After(e.ts) { 280 continue 281 } 282 if e.dirty { 283 deletions++ 284 } 285 if e.mutation { 286 mutations++ 287 } else { 288 triggered = true 289 } 290 291 // Check if condition should cause immediate collection. 292 if triggered || 293 (s.deletionThreshold > 0 && deletions >= s.deletionThreshold) || 294 (nextCollection == nil && ((s.deletionThreshold == 0 && deletions > 0) || 295 (s.mutationThreshold > 0 && mutations >= s.mutationThreshold))) { 296 // Check if not already scheduled before delay threshold 297 if nextCollection == nil || nextCollection.After(time.Now().Add(s.scheduleDelay)) { 298 // TODO(dmcg): track re-schedules for tuning schedule config 299 schedC, nextCollection = schedule(s.scheduleDelay) 300 } 301 } 302 303 continue 304 case <-ctx.Done(): 305 return 306 } 307 308 s.waiterL.Lock() 309 310 stats, err := s.c.GarbageCollect(ctx) 311 last := time.Now() 312 if err != nil { 313 log.G(ctx).WithError(err).Error("garbage collection failed") 314 315 // Reschedule garbage collection for same duration + 1 second 316 schedC, nextCollection = schedule(nextCollection.Sub(*lastCollection) + time.Second) 317 318 // Update last collection time even though failure occurred 319 lastCollection = &last 320 321 for _, w := range s.waiters { 322 close(w) 323 } 324 s.waiters = nil 325 s.waiterL.Unlock() 326 continue 327 } 328 329 log.G(ctx).WithField("d", stats.Elapsed()).Debug("garbage collected") 330 331 gcTime += stats.Elapsed() 332 collections++ 333 triggered = false 334 deletions = 0 335 mutations = 0 336 337 // Calculate new interval with updated times 338 if s.pauseThreshold > 0.0 { 339 // Set interval to average gc time divided by the pause threshold 340 // This algorithm ensures that a gc is scheduled to allow enough 341 // runtime in between gc to reach the pause threshold. 342 // Pause threshold is always 0.0 < threshold <= 0.5 343 avg := float64(gcTime) / float64(collections) 344 interval = time.Duration(avg/s.pauseThreshold - avg) 345 } 346 347 lastCollection = &last 348 schedC, nextCollection = schedule(interval) 349 350 for _, w := range s.waiters { 351 w <- stats 352 } 353 s.waiters = nil 354 s.waiterL.Unlock() 355 } 356 }