4bafaa00aa
The containerd client is very chatty at the best of times. Because the libcontained API is stateless and references containers and processes by string ID for every method call, the implementation is essentially forced to use the containerd client in a way which amplifies the number of redundant RPCs invoked to perform any operation. The libcontainerd remote implementation has to reload the containerd container, task and/or process metadata for nearly every operation. This in turn amplifies the number of context switches between dockerd and containerd to perform any container operation or handle a containerd event, increasing the load on the system which could otherwise be allocated to workloads. Overhaul the libcontainerd interface to reduce the impedance mismatch with the containerd client so that the containerd client can be used more efficiently. Split the API out into container, task and process interfaces which the consumer is expected to retain so that libcontainerd can retain state---especially the analogous containerd client objects---without having to manage any state-store inside the libcontainerd client. Signed-off-by: Cory Snider <csnider@mirantis.com>
204 lines
4.9 KiB
Go
204 lines
4.9 KiB
Go
//go:build !windows
|
|
// +build !windows
|
|
|
|
package daemon // import "github.com/docker/docker/daemon"
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"fmt"
|
|
"os/exec"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/docker/docker/api/types/container"
|
|
"github.com/docker/docker/errdefs"
|
|
libcontainerdtypes "github.com/docker/docker/libcontainerd/types"
|
|
"github.com/pkg/errors"
|
|
)
|
|
|
|
func validatePSArgs(psArgs string) error {
|
|
// NOTE: \\s does not detect unicode whitespaces.
|
|
// So we use fieldsASCII instead of strings.Fields in parsePSOutput.
|
|
// See https://github.com/docker/docker/pull/24358
|
|
//nolint: gosimple
|
|
re := regexp.MustCompile("\\s+([^\\s]*)=\\s*(PID[^\\s]*)")
|
|
for _, group := range re.FindAllStringSubmatch(psArgs, -1) {
|
|
if len(group) >= 3 {
|
|
k := group[1]
|
|
v := group[2]
|
|
if k != "pid" {
|
|
return fmt.Errorf("specifying \"%s=%s\" is not allowed", k, v)
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// fieldsASCII is similar to strings.Fields but only allows ASCII whitespaces
|
|
func fieldsASCII(s string) []string {
|
|
fn := func(r rune) bool {
|
|
switch r {
|
|
case '\t', '\n', '\f', '\r', ' ':
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
return strings.FieldsFunc(s, fn)
|
|
}
|
|
|
|
func appendProcess2ProcList(procList *container.ContainerTopOKBody, fields []string) {
|
|
// Make sure number of fields equals number of header titles
|
|
// merging "overhanging" fields
|
|
process := fields[:len(procList.Titles)-1]
|
|
process = append(process, strings.Join(fields[len(procList.Titles)-1:], " "))
|
|
procList.Processes = append(procList.Processes, process)
|
|
}
|
|
|
|
func hasPid(procs []uint32, pid int) bool {
|
|
for _, p := range procs {
|
|
if int(p) == pid {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func parsePSOutput(output []byte, procs []uint32) (*container.ContainerTopOKBody, error) {
|
|
procList := &container.ContainerTopOKBody{}
|
|
|
|
lines := strings.Split(string(output), "\n")
|
|
procList.Titles = fieldsASCII(lines[0])
|
|
|
|
pidIndex := -1
|
|
for i, name := range procList.Titles {
|
|
if name == "PID" {
|
|
pidIndex = i
|
|
break
|
|
}
|
|
}
|
|
if pidIndex == -1 {
|
|
return nil, fmt.Errorf("Couldn't find PID field in ps output")
|
|
}
|
|
|
|
// loop through the output and extract the PID from each line
|
|
// fixing #30580, be able to display thread line also when "m" option used
|
|
// in "docker top" client command
|
|
preContainedPidFlag := false
|
|
for _, line := range lines[1:] {
|
|
if len(line) == 0 {
|
|
continue
|
|
}
|
|
fields := fieldsASCII(line)
|
|
|
|
var (
|
|
p int
|
|
err error
|
|
)
|
|
|
|
if fields[pidIndex] == "-" {
|
|
if preContainedPidFlag {
|
|
appendProcess2ProcList(procList, fields)
|
|
}
|
|
continue
|
|
}
|
|
p, err = strconv.Atoi(fields[pidIndex])
|
|
if err != nil {
|
|
return nil, fmt.Errorf("Unexpected pid '%s': %s", fields[pidIndex], err)
|
|
}
|
|
|
|
if hasPid(procs, p) {
|
|
preContainedPidFlag = true
|
|
appendProcess2ProcList(procList, fields)
|
|
continue
|
|
}
|
|
preContainedPidFlag = false
|
|
}
|
|
return procList, nil
|
|
}
|
|
|
|
// psPidsArg converts a slice of PIDs to a string consisting
|
|
// of comma-separated list of PIDs prepended by "-q".
|
|
// For example, psPidsArg([]uint32{1,2,3}) returns "-q1,2,3".
|
|
func psPidsArg(pids []uint32) string {
|
|
b := []byte{'-', 'q'}
|
|
for i, p := range pids {
|
|
b = strconv.AppendUint(b, uint64(p), 10)
|
|
if i < len(pids)-1 {
|
|
b = append(b, ',')
|
|
}
|
|
}
|
|
return string(b)
|
|
}
|
|
|
|
// ContainerTop lists the processes running inside of the given
|
|
// container by calling ps with the given args, or with the flags
|
|
// "-ef" if no args are given. An error is returned if the container
|
|
// is not found, or is not running, or if there are any problems
|
|
// running ps, or parsing the output.
|
|
func (daemon *Daemon) ContainerTop(name string, psArgs string) (*container.ContainerTopOKBody, error) {
|
|
if psArgs == "" {
|
|
psArgs = "-ef"
|
|
}
|
|
|
|
if err := validatePSArgs(psArgs); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
ctr, err := daemon.GetContainer(name)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
tsk, err := func() (libcontainerdtypes.Task, error) {
|
|
ctr.Lock()
|
|
defer ctr.Unlock()
|
|
|
|
tsk, err := ctr.GetRunningTask()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if ctr.Restarting {
|
|
return nil, errContainerIsRestarting(ctr.ID)
|
|
}
|
|
return tsk, nil
|
|
}()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
infos, err := tsk.Pids(context.Background())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
procs := make([]uint32, len(infos))
|
|
for i, p := range infos {
|
|
procs[i] = p.Pid
|
|
}
|
|
|
|
args := strings.Split(psArgs, " ")
|
|
pids := psPidsArg(procs)
|
|
output, err := exec.Command("ps", append(args, pids)...).Output()
|
|
if err != nil {
|
|
// some ps options (such as f) can't be used together with q,
|
|
// so retry without it
|
|
output, err = exec.Command("ps", args...).Output()
|
|
if err != nil {
|
|
if ee, ok := err.(*exec.ExitError); ok {
|
|
// first line of stderr shows why ps failed
|
|
line := bytes.SplitN(ee.Stderr, []byte{'\n'}, 2)
|
|
if len(line) > 0 && len(line[0]) > 0 {
|
|
err = errors.New(string(line[0]))
|
|
}
|
|
}
|
|
return nil, errdefs.System(errors.Wrap(err, "ps"))
|
|
}
|
|
}
|
|
procList, err := parsePSOutput(output, procs)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
daemon.LogContainerEvent(ctr, "top")
|
|
return procList, nil
|
|
}
|