151 lines
4.5 KiB
Go
151 lines
4.5 KiB
Go
// Copyright 2020 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 dockerutil
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"time"
|
|
|
|
"golang.org/x/sys/unix"
|
|
)
|
|
|
|
// profile represents profile-like operations on a container.
|
|
//
|
|
// It is meant to be added to containers such that the container type calls
|
|
// the profile during its lifecycle. Standard implementations are below.
|
|
|
|
// profile is for running profiles with 'runsc debug'.
|
|
type profile struct {
|
|
BasePath string
|
|
Types []string
|
|
Duration time.Duration
|
|
cmd *exec.Cmd
|
|
}
|
|
|
|
// profileInit initializes a profile object, if required.
|
|
//
|
|
// N.B. The profiling filename initialized here will use the *image*
|
|
// name, and not the unique container name. This is intentional. Most
|
|
// of the time, profiling will be used for benchmarks. Benchmarks will
|
|
// be run iteratively until a sufficiently large N is reached. It is
|
|
// useful in this context to overwrite previous runs, and generate a
|
|
// single profile result for the final test.
|
|
func (c *Container) profileInit(image string) {
|
|
if !*pprofBlock && !*pprofCPU && !*pprofMutex && !*pprofHeap {
|
|
return // Nothing to do.
|
|
}
|
|
c.profile = &profile{
|
|
BasePath: filepath.Join(*pprofBaseDir, c.runtime, c.logger.Name(), image),
|
|
Duration: *pprofDuration,
|
|
}
|
|
if *pprofCPU {
|
|
c.profile.Types = append(c.profile.Types, "cpu")
|
|
}
|
|
if *pprofHeap {
|
|
c.profile.Types = append(c.profile.Types, "heap")
|
|
}
|
|
if *pprofMutex {
|
|
c.profile.Types = append(c.profile.Types, "mutex")
|
|
}
|
|
if *pprofBlock {
|
|
c.profile.Types = append(c.profile.Types, "block")
|
|
}
|
|
}
|
|
|
|
// createProcess creates the collection process.
|
|
func (p *profile) createProcess(c *Container) error {
|
|
// Ensure our directory exists.
|
|
if err := os.MkdirAll(p.BasePath, 0755); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Find the runtime to invoke.
|
|
path, err := RuntimePath()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get runtime path: %v", err)
|
|
}
|
|
|
|
// The root directory of this container's runtime.
|
|
rootDir := fmt.Sprintf("/var/run/docker/runtime-%s/moby", c.runtime)
|
|
if _, err := os.Stat(rootDir); os.IsNotExist(err) {
|
|
// In docker v20+, due to https://github.com/moby/moby/issues/42345 the
|
|
// rootDir seems to always be the following.
|
|
rootDir = "/var/run/docker/runtime-runc/moby"
|
|
}
|
|
|
|
// Format is `runsc --root=rootDir debug --profile-*=file --duration=24h containerID`.
|
|
args := []string{fmt.Sprintf("--root=%s", rootDir), "debug"}
|
|
for _, profileArg := range p.Types {
|
|
outputPath := filepath.Join(p.BasePath, fmt.Sprintf("%s.pprof", profileArg))
|
|
args = append(args, fmt.Sprintf("--profile-%s=%s", profileArg, outputPath))
|
|
}
|
|
args = append(args, fmt.Sprintf("--duration=%s", p.Duration)) // Or until container exits.
|
|
args = append(args, fmt.Sprintf("--delay=%s", p.Duration)) // Ditto.
|
|
args = append(args, c.ID())
|
|
|
|
// Best effort wait until container is running.
|
|
for now := time.Now(); time.Since(now) < 5*time.Second; {
|
|
if status, err := c.Status(context.Background()); err != nil {
|
|
return fmt.Errorf("failed to get status with: %v", err)
|
|
} else if status.Running {
|
|
break
|
|
}
|
|
time.Sleep(100 * time.Millisecond)
|
|
}
|
|
p.cmd = exec.Command(path, args...)
|
|
p.cmd.Stderr = os.Stderr // Pass through errors.
|
|
if err := p.cmd.Start(); err != nil {
|
|
return fmt.Errorf("start process failed: %v", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// killProcess kills the process, if running.
|
|
func (p *profile) killProcess() error {
|
|
if p.cmd != nil && p.cmd.Process != nil {
|
|
return p.cmd.Process.Signal(unix.SIGTERM)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// waitProcess waits for the process, if running.
|
|
func (p *profile) waitProcess() error {
|
|
defer func() { p.cmd = nil }()
|
|
if p.cmd != nil {
|
|
return p.cmd.Wait()
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Start is called when profiling is started.
|
|
func (p *profile) Start(c *Container) error {
|
|
return p.createProcess(c)
|
|
}
|
|
|
|
// Stop is called when profiling is started.
|
|
func (p *profile) Stop(c *Container) error {
|
|
killErr := p.killProcess()
|
|
waitErr := p.waitProcess()
|
|
if waitErr != nil && killErr != nil {
|
|
return killErr
|
|
}
|
|
return waitErr // Ignore okay wait, err kill.
|
|
}
|