mirror of
https://github.com/hanzoai/visor.git
synced 2026-09-24 17:04:14 +00:00
- `runsc fscheckpoint` saves a filesystem checkpoint, analogous to `runsc
checkpoint`. `-direct` and `-leave-running` flags are supported for
compatibility with `runsc checkpoint`. `-save-restore-exec*` is currently
not supported due to lack of a use case. `-compression` support is not
planned (specific files in filesystem checkpoint images may use compression
as appropriate, but this is an implementation detail).
`-exclude-committed-zero-pages` is inapplicable.
- `runsc create -fs-restore-image-path` restores a filesystem checkpoint. The
`-fs-restore-direct` flag has the same effect as `runsc restore -direct`.
The behavior of `runsc restore -background` is always in effect for
filesystem checkpoints.
PiperOrigin-RevId: 885914969
385 lines
12 KiB
Go
385 lines
12 KiB
Go
// Copyright 2024 The gVisor Authors.
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package kernel
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"math"
|
|
|
|
"gvisor.dev/gvisor/pkg/cleanup"
|
|
"gvisor.dev/gvisor/pkg/context"
|
|
"gvisor.dev/gvisor/pkg/log"
|
|
"gvisor.dev/gvisor/pkg/sentry/checkpoint"
|
|
"gvisor.dev/gvisor/pkg/sentry/pgalloc"
|
|
"gvisor.dev/gvisor/pkg/sentry/state/stateio"
|
|
"gvisor.dev/gvisor/pkg/state"
|
|
"gvisor.dev/gvisor/pkg/sync"
|
|
"gvisor.dev/gvisor/pkg/timing"
|
|
)
|
|
|
|
// Saver is an interface for saving the kernel.
|
|
type Saver interface {
|
|
SaveAsync() error
|
|
SpecEnviron(containerName string) []string
|
|
FSSave() error
|
|
}
|
|
|
|
// CheckpointGeneration stores information about the last checkpoint taken.
|
|
//
|
|
// +stateify savable
|
|
type CheckpointGeneration struct {
|
|
// Count is incremented every time a checkpoint is triggered, even if the
|
|
// checkpoint failed.
|
|
Count uint32
|
|
// Restore indicates if the current instance resumed after the checkpoint or
|
|
// it was restored from a checkpoint.
|
|
Restore bool
|
|
}
|
|
|
|
// AddStateToCheckpoint adds a key-value pair to be additionally checkpointed.
|
|
func (k *Kernel) AddStateToCheckpoint(key, v any) {
|
|
k.checkpointMu.Lock()
|
|
defer k.checkpointMu.Unlock()
|
|
if k.additionalCheckpointState == nil {
|
|
k.additionalCheckpointState = make(map[any]any)
|
|
}
|
|
k.additionalCheckpointState[key] = v
|
|
}
|
|
|
|
// PopCheckpointState pops a key-value pair from the additional checkpoint
|
|
// state. If the key doesn't exist, nil is returned.
|
|
func (k *Kernel) PopCheckpointState(key any) any {
|
|
k.checkpointMu.Lock()
|
|
defer k.checkpointMu.Unlock()
|
|
if v, ok := k.additionalCheckpointState[key]; ok {
|
|
delete(k.additionalCheckpointState, key)
|
|
return v
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// SetSaver sets the kernel's Saver.
|
|
// Thread-compatible.
|
|
func (k *Kernel) SetSaver(s Saver) {
|
|
k.checkpointMu.Lock()
|
|
defer k.checkpointMu.Unlock()
|
|
k.saver = s
|
|
}
|
|
|
|
// Saver returns the kernel's Saver.
|
|
// Thread-compatible.
|
|
func (k *Kernel) Saver() Saver {
|
|
k.checkpointMu.Lock()
|
|
defer k.checkpointMu.Unlock()
|
|
return k.saver
|
|
}
|
|
|
|
// CheckpointGen returns the current checkpoint generation.
|
|
func (k *Kernel) CheckpointGen() CheckpointGeneration {
|
|
k.checkpointMu.Lock()
|
|
defer k.checkpointMu.Unlock()
|
|
|
|
return k.checkpointGen
|
|
}
|
|
|
|
// IncCheckpointGenOnRestore increments the checkpoint generation upon restore.
|
|
func (k *Kernel) IncCheckpointGenOnRestore() {
|
|
k.checkpointMu.Lock()
|
|
defer k.checkpointMu.Unlock()
|
|
|
|
k.checkpointGen.Count++
|
|
k.checkpointGen.Restore = true
|
|
|
|
k.CheckpointWait.signal(k.checkpointGen, nil)
|
|
}
|
|
|
|
// OnCheckpointAttempt is called when a checkpoint attempt is completed. err is
|
|
// any checkpoint errors that may have occurred.
|
|
func (k *Kernel) OnCheckpointAttempt(err error) {
|
|
if err == nil {
|
|
log.Infof("Checkpoint completed successfully.")
|
|
} else {
|
|
log.Warningf("Checkpoint attempt failed with error: %v", err)
|
|
}
|
|
|
|
k.checkpointMu.Lock()
|
|
defer k.checkpointMu.Unlock()
|
|
|
|
k.checkpointGen.Count++
|
|
k.checkpointGen.Restore = false
|
|
|
|
k.CheckpointWait.signal(k.checkpointGen, err)
|
|
}
|
|
|
|
// WaitForCheckpoint waits for the Kernel to have been successfully checkpointed.
|
|
func (k *Kernel) WaitForCheckpoint() error {
|
|
// Send checkpoint result to a channel and wait on it.
|
|
ch := make(chan error, 1)
|
|
callback := func(_ CheckpointGeneration, err error) { ch <- err }
|
|
key := k.CheckpointWait.Register(callback, k.CheckpointGen().Count+1)
|
|
defer k.CheckpointWait.Unregister(key)
|
|
|
|
return <-ch
|
|
}
|
|
|
|
// SignalAllCheckpointWaiters signals all checkpoint waiters with err.
|
|
func (k *Kernel) SignalAllCheckpointWaiters(err error) {
|
|
k.CheckpointWait.signal(CheckpointGeneration{Count: math.MaxUint32}, err)
|
|
}
|
|
|
|
type checkpointWaiter struct {
|
|
// count indicates the checkpoint generation that this waiter is interested in.
|
|
count uint32
|
|
// callback is the function that will be called when the checkpoint generation
|
|
// reaches the desired count. It is set to nil after the callback is called.
|
|
callback func(CheckpointGeneration, error)
|
|
}
|
|
|
|
// CheckpointWaitable is a waitable object that waits for a
|
|
// checkpoint to complete.
|
|
//
|
|
// +stateify savable
|
|
type CheckpointWaitable struct {
|
|
k *Kernel
|
|
|
|
mu sync.Mutex `state:"nosave"`
|
|
|
|
// Don't save the waiters, because they are repopulated after restore. It also
|
|
// allows for external entities to wait for the checkpoint.
|
|
waiters map[*checkpointWaiter]struct{} `state:"nosave"`
|
|
}
|
|
|
|
// Register registers a callback that is notified when the checkpoint generation count is higher
|
|
// than the desired count.
|
|
func (w *CheckpointWaitable) Register(cb func(CheckpointGeneration, error), count uint32) any {
|
|
w.mu.Lock()
|
|
defer w.mu.Unlock()
|
|
|
|
waiter := &checkpointWaiter{
|
|
count: count,
|
|
callback: cb,
|
|
}
|
|
if w.waiters == nil {
|
|
w.waiters = make(map[*checkpointWaiter]struct{})
|
|
}
|
|
w.waiters[waiter] = struct{}{}
|
|
|
|
if gen := w.k.CheckpointGen(); count <= gen.Count {
|
|
// The checkpoint has already occurred. Signal immediately.
|
|
waiter.callback(gen, nil)
|
|
waiter.callback = nil
|
|
}
|
|
return waiter
|
|
}
|
|
|
|
// Unregister unregisters a waiter. It must be called even if the channel
|
|
// was signalled.
|
|
func (w *CheckpointWaitable) Unregister(key any) {
|
|
w.mu.Lock()
|
|
defer w.mu.Unlock()
|
|
|
|
delete(w.waiters, key.(*checkpointWaiter))
|
|
if len(w.waiters) == 0 {
|
|
w.waiters = nil
|
|
}
|
|
}
|
|
|
|
func (w *CheckpointWaitable) signal(gen CheckpointGeneration, err error) {
|
|
w.mu.Lock()
|
|
defer w.mu.Unlock()
|
|
|
|
for waiter := range w.waiters {
|
|
if waiter.callback != nil && waiter.count <= gen.Count {
|
|
waiter.callback(gen, err)
|
|
waiter.callback = nil
|
|
}
|
|
}
|
|
}
|
|
|
|
// loadPrivateMemoryFiles loads the private MemoryFiles from mfmap and it reads
|
|
// private MemoryFile metadata from `r`. This consumes bytes from `r`, so this
|
|
// must be called only once.
|
|
func loadPrivateMemoryFiles(ctx context.Context, r io.Reader, mfmap map[checkpoint.ResourceID]*pgalloc.MemoryFile, opts *pgalloc.LoadOpts) error {
|
|
// Load the metadata.
|
|
var meta privateMemoryFileMetadata
|
|
if _, err := state.Load(ctx, r, &meta); err != nil {
|
|
return err
|
|
}
|
|
// Ensure that it is consistent with mfmap.
|
|
if len(mfmap) != len(meta.owners) {
|
|
return fmt.Errorf("inconsistent private memory files on restore: savedMFOwners = %v, mfmap = %v", meta.owners, mfmap)
|
|
}
|
|
// Load all private memory files.
|
|
for _, fsID := range meta.owners {
|
|
mf, ok := mfmap[fsID]
|
|
if !ok {
|
|
return fmt.Errorf("saved private memory file for %q was not configured on restore", fsID)
|
|
}
|
|
err := mf.LoadFrom(ctx, r, opts)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to load MemoryFile %p fsID %q: %w", mf, fsID, err)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (k *Kernel) loadMemoryFiles(ctx context.Context, r io.Reader) error {
|
|
var opts pgalloc.LoadOpts
|
|
if err := k.mf.LoadFrom(ctx, r, &opts); err != nil {
|
|
return fmt.Errorf("failed to load main MemoryFile %p: %w", k.mf, err)
|
|
}
|
|
if err := loadPrivateMemoryFiles(ctx, r, pgalloc.MemoryFileMapFromContext(ctx), &opts); err != nil {
|
|
return fmt.Errorf("failed to load private MemoryFiles: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// AsyncMFLoader loads all MemoryFiles asynchronously; thanks to having
|
|
// separate pages and page metadata files, as opposed to a single state file
|
|
// containing everything.
|
|
//
|
|
// The AsyncMFLoader helps achieve the following goals:
|
|
// - Loading the main MemoryFile as early as possible.
|
|
// - Loading the private MemoryFiles, also as early as possible but after
|
|
// the main MemoryFile.
|
|
// - Wait for various events to occur before proceeding.
|
|
// - Report errors as they occur.
|
|
type AsyncMFLoader struct {
|
|
// privateMFsChan is used to tell the background goroutine about private
|
|
// MemoryFiles, once they are known. This channel is written to exactly once.
|
|
privateMFsChan chan map[checkpoint.ResourceID]*pgalloc.MemoryFile
|
|
|
|
mainMFStartWg sync.WaitGroup
|
|
mainMetadataErr error
|
|
|
|
metadataWg sync.WaitGroup
|
|
metadataErr error
|
|
|
|
loadWg sync.WaitGroup
|
|
loadErr error
|
|
}
|
|
|
|
// NewAsyncMFLoader creates a new AsyncMFLoader. It takes ownership of
|
|
// pagesMetadata and pagesFile. It creates a background goroutine that will
|
|
// load all the MemoryFiles. The background goroutine immediately starts
|
|
// loading the main MemoryFile.
|
|
// If timeline is provided, it will be used to track async page loading.
|
|
// It takes ownership of the timeline, and will end it when done loading all
|
|
// pages.
|
|
func NewAsyncMFLoader(pagesMetadata io.ReadCloser, pagesFile stateio.AsyncReader, mainMF *pgalloc.MemoryFile, timeline *timing.Timeline) *AsyncMFLoader {
|
|
mfl := &AsyncMFLoader{
|
|
privateMFsChan: make(chan map[checkpoint.ResourceID]*pgalloc.MemoryFile, 1),
|
|
}
|
|
mfl.mainMFStartWg.Add(1)
|
|
mfl.metadataWg.Add(1)
|
|
mfl.loadWg.Add(1)
|
|
go mfl.backgroundGoroutine(pagesMetadata, pagesFile, mainMF, timeline)
|
|
return mfl
|
|
}
|
|
|
|
func (mfl *AsyncMFLoader) backgroundGoroutine(pagesMetadata io.ReadCloser, pagesFile stateio.AsyncReader, mainMF *pgalloc.MemoryFile, timeline *timing.Timeline) {
|
|
defer timeline.End()
|
|
defer pagesMetadata.Close()
|
|
cu := cleanup.Make(func() {
|
|
mfl.metadataWg.Done()
|
|
mfl.loadWg.Done()
|
|
})
|
|
defer cu.Clean()
|
|
|
|
mfl.loadWg.Add(1)
|
|
apfl, err := pgalloc.StartAsyncPagesFileLoad(pagesFile, func(err error) {
|
|
defer mfl.loadWg.Done()
|
|
mfl.loadErr = err
|
|
}, timeline) // transfers ownership of pagesFile
|
|
if err != nil {
|
|
mfl.loadWg.Done()
|
|
log.Warningf("Failed to start async page loading: %v", err)
|
|
return
|
|
}
|
|
cu.Add(apfl.MemoryFilesDone)
|
|
|
|
opts := pgalloc.LoadOpts{
|
|
PagesFile: apfl,
|
|
Timeline: timeline,
|
|
}
|
|
// Note that we depend on opts.PagesFileOffset being carried between
|
|
// LoadFrom calls, so the same opts must be used for all calls.
|
|
|
|
timeline.Reached("loading mainMF")
|
|
log.Infof("Loading metadata for main MemoryFile: %p", mainMF)
|
|
ctx := context.Background()
|
|
err = mainMF.LoadFrom(ctx, pagesMetadata, &opts)
|
|
mfl.metadataErr = err
|
|
mfl.mainMetadataErr = err
|
|
mfl.mainMFStartWg.Done()
|
|
if err != nil {
|
|
log.Warningf("Failed to load main MemoryFile %p: %v", mainMF, err)
|
|
return
|
|
}
|
|
timeline.Reached("waiting for privateMF info")
|
|
privateMFs := <-mfl.privateMFsChan
|
|
timeline.Reached("received privateMFs info")
|
|
log.Infof("Loading metadata for %d private MemoryFiles", len(privateMFs))
|
|
if err := loadPrivateMemoryFiles(ctx, pagesMetadata, privateMFs, &opts); err != nil {
|
|
log.Warningf("Failed to load private MemoryFiles: %v", err)
|
|
mfl.metadataErr = err
|
|
return
|
|
}
|
|
|
|
// Report metadata load completion.
|
|
timeline.Reached("metadata load done")
|
|
log.Infof("All MemoryFile metadata has been loaded")
|
|
cu.Release()()
|
|
|
|
// Wait for page loads to complete and report errors.
|
|
mfl.loadWg.Wait()
|
|
if mfl.loadErr != nil {
|
|
timeline.Invalidate("page load failed")
|
|
log.Warningf("Failed to load MemoryFile pages: %v", mfl.loadErr)
|
|
return
|
|
}
|
|
log.Infof("All MemoryFile pages have been loaded.")
|
|
}
|
|
|
|
// KickoffPrivate notifies the background goroutine of the private MemoryFiles.
|
|
func (mfl *AsyncMFLoader) KickoffPrivate(mfmap map[checkpoint.ResourceID]*pgalloc.MemoryFile) {
|
|
mfl.privateMFsChan <- mfmap
|
|
}
|
|
|
|
// WaitMainMFStart waits for the background goroutine to successfully start
|
|
// asynchronously loading the main MemoryFile.
|
|
func (mfl *AsyncMFLoader) WaitMainMFStart() error {
|
|
mfl.mainMFStartWg.Wait()
|
|
return mfl.mainMetadataErr
|
|
}
|
|
|
|
// WaitMetadata waits for the background goroutine to successfully complete
|
|
// reading all MemoryFile metadata and report any errors.
|
|
func (mfl *AsyncMFLoader) WaitMetadata() error {
|
|
mfl.metadataWg.Wait()
|
|
return mfl.metadataErr
|
|
}
|
|
|
|
// Wait waits for the background goroutine to successfully complete fully
|
|
// loading all the MemoryFiles and report any errors.
|
|
func (mfl *AsyncMFLoader) Wait() error {
|
|
if err := mfl.WaitMetadata(); err != nil {
|
|
return err
|
|
}
|
|
mfl.loadWg.Wait()
|
|
return mfl.loadErr
|
|
}
|