Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add resource usage monitoring for build steps #3860

Merged
merged 6 commits into from
Jun 28, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,7 @@ RUN --mount=target=/root/.cache,type=cache \

FROM buildkit-export AS buildkit-linux
COPY --link --from=binaries / /usr/bin/
ENV BUILDKIT_SETUP_CGROUPV2_ROOT=1
ENTRYPOINT ["buildkitd"]

FROM binaries AS buildkit-darwin
Expand Down Expand Up @@ -255,6 +256,7 @@ ENTRYPOINT ["/docker-entrypoint.sh"]
# musl is needed to directly use the registry binary that is built on alpine
ENV BUILDKIT_INTEGRATION_CONTAINERD_EXTRA="containerd-1.6=/opt/containerd-alt-16/bin"
ENV BUILDKIT_INTEGRATION_SNAPSHOTTER=stargz
ENV BUILDKIT_SETUP_CGROUPV2_ROOT=1
ENV CGO_ENABLED=0
ENV GOTESTSUM_FORMAT=standard-verbose
COPY --link --from=gotestsum /out/gotestsum /usr/bin/
Expand Down
35 changes: 18 additions & 17 deletions executor/containerdexecutor/executor.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
"github.com/docker/docker/pkg/idtools"
"github.com/moby/buildkit/executor"
"github.com/moby/buildkit/executor/oci"
resourcestypes "github.com/moby/buildkit/executor/resources/types"
gatewayapi "github.com/moby/buildkit/frontend/gateway/pb"
"github.com/moby/buildkit/identity"
"github.com/moby/buildkit/snapshot"
Expand Down Expand Up @@ -78,7 +79,7 @@ func New(client *containerd.Client, root, cgroup string, networkProviders map[pb
}
}

func (w *containerdExecutor) Run(ctx context.Context, id string, root executor.Mount, mounts []executor.Mount, process executor.ProcessInfo, started chan<- struct{}) (err error) {
func (w *containerdExecutor) Run(ctx context.Context, id string, root executor.Mount, mounts []executor.Mount, process executor.ProcessInfo, started chan<- struct{}) (rec resourcestypes.Recorder, err error) {
if id == "" {
id = identity.NewID()
}
Expand All @@ -105,25 +106,25 @@ func (w *containerdExecutor) Run(ctx context.Context, id string, root executor.M

resolvConf, err := oci.GetResolvConf(ctx, w.root, nil, w.dnsConfig)
if err != nil {
return err
return nil, err
}

hostsFile, clean, err := oci.GetHostsFile(ctx, w.root, meta.ExtraHosts, nil, meta.Hostname)
if err != nil {
return err
return nil, err
}
if clean != nil {
defer clean()
}

mountable, err := root.Src.Mount(ctx, false)
if err != nil {
return err
return nil, err
}

rootMounts, release, err := mountable.Mount()
if err != nil {
return err
return nil, err
}
if release != nil {
defer release()
Expand All @@ -132,14 +133,14 @@ func (w *containerdExecutor) Run(ctx context.Context, id string, root executor.M
lm := snapshot.LocalMounterWithMounts(rootMounts)
rootfsPath, err := lm.Mount()
if err != nil {
return err
return nil, err
}
defer lm.Unmount()
defer executor.MountStubsCleaner(ctx, rootfsPath, mounts, meta.RemoveMountStubsRecursive)()

uid, gid, sgids, err := oci.GetUser(rootfsPath, meta.User)
if err != nil {
return err
return nil, err
}

identity := idtools.Identity{
Expand All @@ -149,21 +150,21 @@ func (w *containerdExecutor) Run(ctx context.Context, id string, root executor.M

newp, err := fs.RootPath(rootfsPath, meta.Cwd)
if err != nil {
return errors.Wrapf(err, "working dir %s points to invalid target", newp)
return nil, errors.Wrapf(err, "working dir %s points to invalid target", newp)
}
if _, err := os.Stat(newp); err != nil {
if err := idtools.MkdirAllAndChown(newp, 0755, identity); err != nil {
return errors.Wrapf(err, "failed to create working directory %s", newp)
return nil, errors.Wrapf(err, "failed to create working directory %s", newp)
}
}

provider, ok := w.networkProviders[meta.NetMode]
if !ok {
return errors.Errorf("unknown network mode %s", meta.NetMode)
return nil, errors.Errorf("unknown network mode %s", meta.NetMode)
}
namespace, err := provider.New(ctx, meta.Hostname)
if err != nil {
return err
return nil, err
}
defer namespace.Close()

Expand All @@ -179,21 +180,21 @@ func (w *containerdExecutor) Run(ctx context.Context, id string, root executor.M
processMode := oci.ProcessSandbox // FIXME(AkihiroSuda)
spec, cleanup, err := oci.GenerateSpec(ctx, meta, mounts, id, resolvConf, hostsFile, namespace, w.cgroupParent, processMode, nil, w.apparmorProfile, w.selinux, w.traceSocket, opts...)
if err != nil {
return err
return nil, err
}
defer cleanup()
spec.Process.Terminal = meta.Tty
if w.rootless {
if err := rootlessspecconv.ToRootless(spec); err != nil {
return err
return nil, err
}
}

container, err := w.client.NewContainer(ctx, id,
containerd.WithSpec(spec),
)
if err != nil {
return err
return nil, err
}

defer func() {
Expand All @@ -214,7 +215,7 @@ func (w *containerdExecutor) Run(ctx context.Context, id string, root executor.M
Options: []string{"rbind"},
}}))
if err != nil {
return err
return nil, err
}

defer func() {
Expand All @@ -225,7 +226,7 @@ func (w *containerdExecutor) Run(ctx context.Context, id string, root executor.M

if nn, ok := namespace.(OnCreateRuntimer); ok {
if err := nn.OnCreateRuntime(task.Pid()); err != nil {
return err
return nil, err
}
}

Expand All @@ -238,7 +239,7 @@ func (w *containerdExecutor) Run(ctx context.Context, id string, root executor.M
}
})
})
return err
return nil, err
}

func (w *containerdExecutor) Exec(ctx context.Context, id string, process executor.ProcessInfo) (err error) {
Expand Down
3 changes: 2 additions & 1 deletion executor/executor.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"net"
"syscall"

resourcestypes "github.com/moby/buildkit/executor/resources/types"
"github.com/moby/buildkit/snapshot"
"github.com/moby/buildkit/solver/pb"
)
Expand Down Expand Up @@ -55,7 +56,7 @@ type Executor interface {
// Run will start a container for the given process with rootfs, mounts.
// `id` is an optional name for the container so it can be referenced later via Exec.
// `started` is an optional channel that will be closed when the container setup completes and has started running.
Run(ctx context.Context, id string, rootfs Mount, mounts []Mount, process ProcessInfo, started chan<- struct{}) error
Run(ctx context.Context, id string, rootfs Mount, mounts []Mount, process ProcessInfo, started chan<- struct{}) (resourcestypes.Recorder, error)
// Exec will start a process in container matching `id`. An error will be returned
// if the container failed to start (via Run) or has exited before Exec is called.
Exec(ctx context.Context, id string, process ProcessInfo) error
Expand Down
140 changes: 140 additions & 0 deletions executor/resources/cpu.go
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we add comments linking to the specifications for the underlying file formats in https://www.kernel.org/doc/html/latest/admin-guide/cgroup-v2.html.

Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
package resources

import (
"bufio"
"os"
"path/filepath"
"strconv"
"strings"

"github.com/moby/buildkit/executor/resources/types"
"github.com/pkg/errors"
)

const (
cpuUsageUsec = "usage_usec"
cpuUserUsec = "user_usec"
cpuSystemUsec = "system_usec"
cpuNrPeriods = "nr_periods"
cpuNrThrottled = "nr_throttled"
cpuThrottledUsec = "throttled_usec"
)

func getCgroupCPUStat(cgroupPath string) (*types.CPUStat, error) {
cpuStat := &types.CPUStat{}

// Read cpu.stat file
cpuStatFile, err := os.Open(filepath.Join(cgroupPath, "cpu.stat"))
if err != nil {
if errors.Is(err, os.ErrNotExist) {
return nil, nil
}
return nil, err
}
defer cpuStatFile.Close()

scanner := bufio.NewScanner(cpuStatFile)
for scanner.Scan() {
line := scanner.Text()
fields := strings.Fields(line)

if len(fields) < 2 {
continue
}

key := fields[0]
value, err := strconv.ParseUint(fields[1], 10, 64)
if err != nil {
continue
}

switch key {
case cpuUsageUsec:
cpuStat.UsageNanos = uint64Ptr(value * 1000)
case cpuUserUsec:
cpuStat.UserNanos = uint64Ptr(value * 1000)
case cpuSystemUsec:
cpuStat.SystemNanos = uint64Ptr(value * 1000)
case cpuNrPeriods:
cpuStat.NrPeriods = new(uint32)
*cpuStat.NrPeriods = uint32(value)
case cpuNrThrottled:
cpuStat.NrThrottled = new(uint32)
*cpuStat.NrThrottled = uint32(value)
case cpuThrottledUsec:
cpuStat.ThrottledNanos = uint64Ptr(value * 1000)
}
}

if err := scanner.Err(); err != nil {
return nil, err
}

// Read cpu.pressure file
pressure, err := parsePressureFile(filepath.Join(cgroupPath, "cpu.pressure"))
if err == nil {
cpuStat.Pressure = pressure
}

return cpuStat, nil
}
func parsePressureFile(filename string) (*types.Pressure, error) {
content, err := os.ReadFile(filename)
if err != nil {
if errors.Is(err, os.ErrNotExist) { // pressure file requires CONFIG_PSI
return nil, nil
}
return nil, err
}

lines := strings.Split(string(content), "\n")

pressure := &types.Pressure{}
for _, line := range lines {
// Skip empty lines
if len(strings.TrimSpace(line)) == 0 {
continue
}

fields := strings.Fields(line)
prefix := fields[0]
pressureValues := &types.PressureValues{}

for i := 1; i < len(fields); i++ {
keyValue := strings.Split(fields[i], "=")
key := keyValue[0]
valueStr := keyValue[1]
jedevc marked this conversation as resolved.
Show resolved Hide resolved

if key == "total" {
totalValue, err := strconv.ParseUint(valueStr, 10, 64)
if err != nil {
return nil, err
}
pressureValues.Total = &totalValue
} else {
value, err := strconv.ParseFloat(valueStr, 64)
if err != nil {
return nil, err
}

switch key {
case "avg10":
pressureValues.Avg10 = &value
case "avg60":
pressureValues.Avg60 = &value
case "avg300":
pressureValues.Avg300 = &value
}
}
}

switch prefix {
case "some":
pressure.Some = pressureValues
case "full":
pressure.Full = pressureValues
}
}

return pressure, nil
}
Loading