github.com/lalkh/containerd@v1.4.3/cmd/containerd-stress/density.go (about)

     1  /*
     2     Copyright The containerd Authors.
     3  
     4     Licensed under the Apache License, Version 2.0 (the "License");
     5     you may not use this file except in compliance with the License.
     6     You may obtain a copy of the License at
     7  
     8         http://www.apache.org/licenses/LICENSE-2.0
     9  
    10     Unless required by applicable law or agreed to in writing, software
    11     distributed under the License is distributed on an "AS IS" BASIS,
    12     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13     See the License for the specific language governing permissions and
    14     limitations under the License.
    15  */
    16  
    17  package main
    18  
    19  import (
    20  	"bufio"
    21  	"context"
    22  	"encoding/json"
    23  	"fmt"
    24  	"io/ioutil"
    25  	"os"
    26  	"os/signal"
    27  	"path/filepath"
    28  	"strconv"
    29  	"strings"
    30  	"syscall"
    31  
    32  	"github.com/containerd/containerd"
    33  	"github.com/containerd/containerd/cio"
    34  	"github.com/containerd/containerd/namespaces"
    35  	"github.com/containerd/containerd/oci"
    36  	"github.com/sirupsen/logrus"
    37  	"github.com/urfave/cli"
    38  )
    39  
    40  var densityCommand = cli.Command{
    41  	Name:  "density",
    42  	Usage: "stress tests density of containers running on a system",
    43  	Flags: []cli.Flag{
    44  		cli.IntFlag{
    45  			Name:  "count",
    46  			Usage: "number of containers to run",
    47  			Value: 10,
    48  		},
    49  	},
    50  	Action: func(cliContext *cli.Context) error {
    51  		config := config{
    52  			Address:     cliContext.GlobalString("address"),
    53  			Duration:    cliContext.GlobalDuration("duration"),
    54  			Concurrency: cliContext.GlobalInt("concurrent"),
    55  			Exec:        cliContext.GlobalBool("exec"),
    56  			JSON:        cliContext.GlobalBool("json"),
    57  			Metrics:     cliContext.GlobalString("metrics"),
    58  		}
    59  		client, err := config.newClient()
    60  		if err != nil {
    61  			return err
    62  		}
    63  		defer client.Close()
    64  		ctx := namespaces.WithNamespace(context.Background(), "density")
    65  		if err := cleanup(ctx, client); err != nil {
    66  			return err
    67  		}
    68  		logrus.Infof("pulling %s", imageName)
    69  		image, err := client.Pull(ctx, imageName, containerd.WithPullUnpack)
    70  		if err != nil {
    71  			return err
    72  		}
    73  		logrus.Info("generating spec from image")
    74  
    75  		s := make(chan os.Signal, 1)
    76  		signal.Notify(s, syscall.SIGTERM, syscall.SIGINT)
    77  
    78  		if err != nil {
    79  			return err
    80  		}
    81  		var (
    82  			pids  []uint32
    83  			count = cliContext.Int("count")
    84  		)
    85  	loop:
    86  		for i := 0; i < count+1; i++ {
    87  			select {
    88  			case <-s:
    89  				break loop
    90  			default:
    91  				id := fmt.Sprintf("density-%d", i)
    92  
    93  				c, err := client.NewContainer(ctx, id,
    94  					containerd.WithNewSnapshot(id, image),
    95  					containerd.WithNewSpec(
    96  						oci.WithImageConfig(image),
    97  						oci.WithProcessArgs("sleep", "120m"),
    98  						oci.WithUsername("games")),
    99  				)
   100  				if err != nil {
   101  					return err
   102  				}
   103  				defer c.Delete(ctx, containerd.WithSnapshotCleanup)
   104  
   105  				t, err := c.NewTask(ctx, cio.NullIO)
   106  				if err != nil {
   107  					return err
   108  				}
   109  				defer t.Delete(ctx, containerd.WithProcessKill)
   110  				if err := t.Start(ctx); err != nil {
   111  					return err
   112  				}
   113  				pids = append(pids, t.Pid())
   114  			}
   115  		}
   116  		var results struct {
   117  			PSS             int `json:"pss"`
   118  			RSS             int `json:"rss"`
   119  			PSSPerContainer int `json:"pssPerContainer"`
   120  			RSSPerContainer int `json:"rssPerContainer"`
   121  		}
   122  
   123  		for _, pid := range pids {
   124  			shimPid, err := getppid(int(pid))
   125  			if err != nil {
   126  				return err
   127  			}
   128  			smaps, err := getMaps(shimPid)
   129  			if err != nil {
   130  				return err
   131  			}
   132  			results.RSS += smaps["Rss:"]
   133  			results.PSS += smaps["Pss:"]
   134  		}
   135  		results.PSSPerContainer = results.PSS / count
   136  		results.RSSPerContainer = results.RSS / count
   137  
   138  		return json.NewEncoder(os.Stdout).Encode(results)
   139  	},
   140  }
   141  
   142  func getMaps(pid int) (map[string]int, error) {
   143  	f, err := os.Open(fmt.Sprintf("/proc/%d/smaps", pid))
   144  	if err != nil {
   145  		return nil, err
   146  	}
   147  	defer f.Close()
   148  	var (
   149  		smaps = make(map[string]int)
   150  		s     = bufio.NewScanner(f)
   151  	)
   152  	for s.Scan() {
   153  		var (
   154  			fields = strings.Fields(s.Text())
   155  			name   = fields[0]
   156  		)
   157  		if len(fields) < 2 {
   158  			continue
   159  		}
   160  		n, err := strconv.Atoi(fields[1])
   161  		if err != nil {
   162  			continue
   163  		}
   164  		smaps[name] += n
   165  	}
   166  	if err := s.Err(); err != nil {
   167  		return nil, err
   168  	}
   169  	return smaps, nil
   170  }
   171  
   172  func getppid(pid int) (int, error) {
   173  	bytes, err := ioutil.ReadFile(filepath.Join("/proc", strconv.Itoa(pid), "stat"))
   174  	if err != nil {
   175  		return 0, err
   176  	}
   177  	s, err := parseStat(string(bytes))
   178  	if err != nil {
   179  		return 0, err
   180  	}
   181  	return int(s.PPID), nil
   182  }
   183  
   184  // Stat represents the information from /proc/[pid]/stat, as
   185  // described in proc(5) with names based on the /proc/[pid]/status
   186  // fields.
   187  type Stat struct {
   188  	// PID is the process ID.
   189  	PID uint
   190  
   191  	// Name is the command run by the process.
   192  	Name string
   193  
   194  	// StartTime is the number of clock ticks after system boot (since
   195  	// Linux 2.6).
   196  	StartTime uint64
   197  	// Parent process ID.
   198  	PPID uint
   199  }
   200  
   201  func parseStat(data string) (stat Stat, err error) {
   202  	// From proc(5), field 2 could contain space and is inside `(` and `)`.
   203  	// The following is an example:
   204  	// 89653 (gunicorn: maste) S 89630 89653 89653 0 -1 4194560 29689 28896 0 3 146 32 76 19 20 0 1 0 2971844 52965376 3920 18446744073709551615 1 1 0 0 0 0 0 16781312 137447943 0 0 0 17 1 0 0 0 0 0 0 0 0 0 0 0 0 0
   205  	i := strings.LastIndex(data, ")")
   206  	if i <= 2 || i >= len(data)-1 {
   207  		return stat, fmt.Errorf("invalid stat data: %q", data)
   208  	}
   209  
   210  	parts := strings.SplitN(data[:i], "(", 2)
   211  	if len(parts) != 2 {
   212  		return stat, fmt.Errorf("invalid stat data: %q", data)
   213  	}
   214  
   215  	stat.Name = parts[1]
   216  	_, err = fmt.Sscanf(parts[0], "%d", &stat.PID)
   217  	if err != nil {
   218  		return stat, err
   219  	}
   220  
   221  	// parts indexes should be offset by 3 from the field number given
   222  	// proc(5), because parts is zero-indexed and we've removed fields
   223  	// one (PID) and two (Name) in the paren-split.
   224  	parts = strings.Split(data[i+2:], " ")
   225  	fmt.Sscanf(parts[22-3], "%d", &stat.StartTime)
   226  	fmt.Sscanf(parts[4-3], "%d", &stat.PPID)
   227  	return stat, nil
   228  }