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  }