Skip to content

Commit

Permalink
nydusify: introduce optimize subcommand of nydusify
Browse files Browse the repository at this point in the history
We can statically analyze the image entrypoint dependency, or use runtime dynamic
analysis technologies such as ebpf, fanotify, metric, etc. to obtain the container
file access pattern, and then build this part of data into an independent image layer:

* preferentially fetch blob during the image startup phase to reduce network and disk IO.
* avoid frequent image builds, allows for better local cache utilization.

Implement optimize subcommand of nydusify to generate a new image, which references a new
blob included prefetch file chunks.
```
nydusify optimize --policy separated-prefetch-blob \
	--source $existed-nydus-image \
	--target $new-nydus-image \
	--prefetch-files /path/to/prefetch-files
```

More detailed process is as follows:
1. nydusify first downloads the source image and bootstrap, utilize nydus-image to output a
new bootstrap along with an independent prefetchblob;
2. nydusify generate&push new meta layer including new bootstrap and the prefetch-files ,
also generates&push new manifest/config/prefetchblob, completing the incremental image build.

Signed-off-by: Xing Ma <[email protected]>
  • Loading branch information
Xing Ma committed Dec 9, 2024
1 parent eadd808 commit 6b13369
Show file tree
Hide file tree
Showing 4 changed files with 719 additions and 1 deletion.
93 changes: 93 additions & 0 deletions contrib/nydusify/cmd/nydusify.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ import (
"runtime"
"strings"

"github.com/dragonflyoss/nydus/contrib/nydusify/pkg/optimizer"

"github.com/containerd/containerd/reference/docker"
"github.com/distribution/reference"
"github.com/dustin/go-humanize"
Expand Down Expand Up @@ -1160,6 +1162,97 @@ func main() {
return copier.Copy(context.Background(), opt)
},
},
{
Name: "optimize",
Usage: "Optimize a source nydus image and push to the target",
Flags: []cli.Flag{
&cli.StringFlag{
Name: "source",
Required: true,
Usage: "Source (Nydus) image reference",
EnvVars: []string{"SOURCE"},
},
&cli.StringFlag{
Name: "target",
Required: true,
Usage: "Target (Nydus) image reference",
EnvVars: []string{"TARGET"},
},
&cli.BoolFlag{
Name: "source-insecure",
Required: false,
Usage: "Skip verifying server certs for HTTPS source registry",
EnvVars: []string{"SOURCE_INSECURE"},
},
&cli.BoolFlag{
Name: "target-insecure",
Required: false,
Usage: "Skip verifying server certs for HTTPS target registry",
EnvVars: []string{"TARGET_INSECURE"},
},

&cli.StringFlag{
Name: "policy",
Value: "separated-blob-with-prefetch-files",
Usage: "Specify the optimizing way",
EnvVars: []string{"OPTIMIZE_POLICY"},
},
&cli.StringFlag{
Name: "prefetch-files",
Required: false,
Usage: "File path to include prefetch files for optimization",
EnvVars: []string{"PREFETCH_FILES"},
},

&cli.StringFlag{
Name: "work-dir",
Value: "./tmp",
Usage: "Working directory for image optimization",
EnvVars: []string{"WORK_DIR"},
},

&cli.StringFlag{
Name: "nydus-image",
Value: "nydus-image",
Usage: "Path to the nydus-image binary, default to search in PATH",
EnvVars: []string{"NYDUS_IMAGE"},
},

&cli.StringFlag{
Name: "push-chunk-size",
Value: "0MB",
Usage: "Chunk size for pushing a blob layer in chunked",
},
},
Action: func(c *cli.Context) error {
setupLogLevel(c)

pushChunkSize, err := humanize.ParseBytes(c.String("push-chunk-size"))
if err != nil {
return errors.Wrap(err, "invalid --push-chunk-size option")
}
if pushChunkSize > 0 {
logrus.Infof("will push layer with chunk size %s", c.String("push-chunk-size"))
}
opt := optimizer.Opt{
WorkDir: c.String("work-dir"),
NydusImagePath: c.String("nydus-image"),

Source: c.String("source"),
Target: c.String("target"),
SourceInsecure: c.Bool("source-insecure"),
TargetInsecure: c.Bool("target-insecure"),

AllPlatforms: c.Bool("all-platforms"),
Platforms: c.String("platform"),

PushChunkSize: int64(pushChunkSize),
PrefetchFilesPath: c.String("prefetch-files"),
}

return optimizer.Optimize(context.Background(), opt)
},
},
{
Name: "commit",
Usage: "Create and push a new nydus image from a container's changes that use a nydus image",
Expand Down
87 changes: 87 additions & 0 deletions contrib/nydusify/pkg/optimizer/builder.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package optimizer

import (
"context"
"encoding/json"
"os"
"os/exec"
"strings"
"time"

"github.com/pkg/errors"
"github.com/sirupsen/logrus"
)

var logger = logrus.WithField("module", "optimizer")

func isSignalKilled(err error) bool {
return strings.Contains(err.Error(), "signal: killed")
}

type BuildOption struct {
BuilderPath string
PrefetchFilesPath string
BootstrapPath string
BlobDir string
OutputBootstrapPath string
OutputJSONPath string
Timeout *time.Duration
}

type outputJSON struct {
Blobs []string `json:"blobs"`
}

func Build(option BuildOption) (string, error) {
outputJSONPath := option.OutputJSONPath
args := []string{
"optimize",
"--log-level",
"warn",
"--prefetch-files",
option.PrefetchFilesPath,
"--bootstrap",
option.BootstrapPath,
"--blob-dir",
option.BlobDir,
"--output-bootstrap",
option.OutputBootstrapPath,
"--output-json",
outputJSONPath,
}

ctx := context.Background()
var cancel context.CancelFunc
if option.Timeout != nil {
ctx, cancel = context.WithTimeout(ctx, *option.Timeout)
defer cancel()
}
logrus.Debugf("\tCommand: %s %s", option.BuilderPath, strings.Join(args, " "))

cmd := exec.CommandContext(ctx, option.BuilderPath, args...)
cmd.Stdout = logger.Writer()
cmd.Stderr = logger.Writer()

if err := cmd.Run(); err != nil {
if isSignalKilled(err) && option.Timeout != nil {
logrus.WithError(err).Errorf("fail to run %v %+v, possibly due to timeout %v", option.BuilderPath, args, *option.Timeout)
} else {
logrus.WithError(err).Errorf("fail to run %v %+v", option.BuilderPath, args)
}
return "", errors.Wrap(err, "run merge command")
}

outputBytes, err := os.ReadFile(outputJSONPath)
if err != nil {
return "", errors.Wrapf(err, "read file %s", outputJSONPath)
}
var output outputJSON
err = json.Unmarshal(outputBytes, &output)
if err != nil {
return "", errors.Wrapf(err, "unmarshal output json file %s", outputJSONPath)
}
blobID := output.Blobs[len(output.Blobs)-1]

logrus.Infof("build success for prefetch blob : %s", blobID)
return blobID, nil
}
Loading

0 comments on commit 6b13369

Please sign in to comment.