github.com/coreos/rocket@v1.30.1-0.20200224141603-171c416fac02/tests/rkt-monitor/main.go (about)

     1  // Copyright 2016 The rkt Authors
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package main
    16  
    17  import (
    18  	"bufio"
    19  	"encoding/csv"
    20  	"encoding/json"
    21  	"flag"
    22  	"fmt"
    23  	"os"
    24  	"os/exec"
    25  	"path/filepath"
    26  	"strconv"
    27  	"strings"
    28  	"time"
    29  
    30  	"github.com/appc/spec/schema"
    31  	"github.com/shirou/gopsutil/load"
    32  	"github.com/shirou/gopsutil/process"
    33  	"github.com/spf13/cobra"
    34  )
    35  
    36  type ProcessStatus struct {
    37  	Pid  int32
    38  	Name string  // Name of process
    39  	CPU  float64 // Percent of CPU used since last check
    40  	VMS  uint64  // Virtual memory size
    41  	RSS  uint64  // Resident set size
    42  	Swap uint64  // Swap size
    43  	ID   string  // Container ID
    44  }
    45  
    46  var (
    47  	pidMap map[int32]*process.Process
    48  
    49  	flagVerbose          bool
    50  	flagDuration         string
    51  	flagShowOutput       bool
    52  	flagSaveToCsv        bool
    53  	flagCsvDir           string
    54  	flagRepetitionNumber int
    55  	flagRktDir           string
    56  	flagStage1Path       string
    57  
    58  	cmdRktMonitor = &cobra.Command{
    59  		Use:     "rkt-monitor IMAGE",
    60  		Short:   "Runs the specified ACI or pod manifest with rkt, and monitors rkt's usage",
    61  		Example: "rkt-monitor mem-stresser.aci -v -d 30s",
    62  		Run:     runRktMonitor,
    63  	}
    64  )
    65  
    66  func init() {
    67  	pidMap = make(map[int32]*process.Process)
    68  
    69  	cmdRktMonitor.Flags().BoolVarP(&flagVerbose, "verbose", "v", false, "Print current usage every second")
    70  	cmdRktMonitor.Flags().IntVarP(&flagRepetitionNumber, "repetitions", "r", 1, "Numbers of benchmark repetitions")
    71  	cmdRktMonitor.Flags().StringVarP(&flagDuration, "duration", "d", "10s", "How long to run the ACI")
    72  	cmdRktMonitor.Flags().BoolVarP(&flagShowOutput, "show-output", "o", false, "Display rkt's stdout and stderr")
    73  	cmdRktMonitor.Flags().BoolVarP(&flagSaveToCsv, "to-file", "f", false, "Save benchmark results to files in a temp dir")
    74  	cmdRktMonitor.Flags().StringVarP(&flagCsvDir, "output-dir", "w", "/tmp", "Specify directory to write results")
    75  	cmdRktMonitor.Flags().StringVarP(&flagRktDir, "rkt-dir", "p", "", "Directory with rkt binary")
    76  	cmdRktMonitor.Flags().StringVarP(&flagStage1Path, "stage1-path", "s", "", "Path to Stage1 image to use")
    77  
    78  	flag.Parse()
    79  }
    80  
    81  func main() {
    82  	cmdRktMonitor.Execute()
    83  }
    84  
    85  func runRktMonitor(cmd *cobra.Command, args []string) {
    86  	if len(args) != 1 {
    87  		cmd.Usage()
    88  		os.Exit(254)
    89  	}
    90  
    91  	d, err := time.ParseDuration(flagDuration)
    92  	if err != nil {
    93  		fmt.Printf("%v\n", err)
    94  		os.Exit(254)
    95  	}
    96  
    97  	if os.Getuid() != 0 {
    98  		fmt.Printf("need to be root to run rkt images\n")
    99  		os.Exit(254)
   100  	}
   101  
   102  	f, err := os.Open(args[0])
   103  	if err != nil {
   104  		fmt.Printf("%v\n", err)
   105  		os.Exit(254)
   106  	}
   107  	decoder := json.NewDecoder(f)
   108  
   109  	podManifest := false
   110  	man := schema.PodManifest{}
   111  	err = decoder.Decode(&man)
   112  	if err == nil {
   113  		podManifest = true
   114  	}
   115  
   116  	var flavorType, testImage string
   117  	if flagStage1Path == "" {
   118  		flavorType = "stage1-coreos.aci"
   119  	} else {
   120  		if !fileExist(flagStage1Path) {
   121  			fmt.Fprintln(os.Stderr, "Given stage1 file path doesn't exist")
   122  			os.Exit(254)
   123  		}
   124  		_, flavorType = filepath.Split(flagStage1Path)
   125  	}
   126  
   127  	var containerId string
   128  	var stopCmd, execCmd, gcCmd *exec.Cmd
   129  	var loadAvg *load.AvgStat
   130  	var containerStarting, containerStarted, containerStopping, containerStopped time.Time
   131  
   132  	records := [][]string{{"Time", "PID name", "PID number", "RSS", "CPU"}}             // csv headers
   133  	summaryRecords := [][]string{{"Load1", "Load5", "Load15", "StartTime", "StopTime"}} // csv summary headers
   134  
   135  	var rktBinary string
   136  	if flagRktDir != "" {
   137  		rktBinary = filepath.Join(flagRktDir, "rkt")
   138  		if !fileExist(rktBinary) {
   139  			fmt.Fprintln(os.Stderr, "rkt binary not found!")
   140  			os.Exit(1)
   141  		}
   142  	} else {
   143  		rktBinary = "rkt"
   144  	}
   145  
   146  	for i := 0; i < flagRepetitionNumber; i++ {
   147  		// build argument list for execCmd
   148  		argv := []string{"run", "--debug"}
   149  
   150  		if flagStage1Path != "" {
   151  			argv = append(argv, fmt.Sprintf("--stage1-path=%v", flagStage1Path))
   152  		}
   153  
   154  		if podManifest {
   155  			argv = append(argv, "--pod-manifest", args[0])
   156  		} else {
   157  			argv = append(argv, args[0], "--insecure-options=image")
   158  		}
   159  		argv = append(argv, "--net=default-restricted")
   160  
   161  		execCmd = exec.Command(rktBinary, argv...)
   162  
   163  		if flagShowOutput {
   164  			execCmd.Stderr = os.Stderr
   165  		}
   166  
   167  		cmdReader, err := execCmd.StdoutPipe()
   168  		if err != nil {
   169  			fmt.Fprintln(os.Stderr, "Error creating StdoutPipe for execCmd", err)
   170  			os.Exit(254)
   171  		}
   172  
   173  		execCmdScanner := bufio.NewScanner(cmdReader)
   174  
   175  		startConfirmation := make(chan string, 1)
   176  		go func() {
   177  			var containerId string
   178  			for execCmdScanner.Scan() {
   179  				if flagShowOutput {
   180  					fmt.Println(execCmdScanner.Text())
   181  				}
   182  				if strings.Contains(execCmdScanner.Text(), "APP-STARTED!") {
   183  					startConfirmation <- containerId
   184  				} else if strings.Contains(execCmdScanner.Text(), "Set hostname to") {
   185  					sl := strings.SplitAfter(execCmdScanner.Text(), "<rkt-")
   186  					containerId = sl[len(sl)-1]
   187  					containerId = containerId[:len(containerId)-2]
   188  				}
   189  			}
   190  		}()
   191  		containerStarting = time.Now()
   192  		err = execCmd.Start()
   193  		containerId = <-startConfirmation
   194  		containerStarted = time.Now() //here we are sure - container is running (issue: #3019)
   195  		close(startConfirmation)
   196  
   197  		if flagShowOutput {
   198  			execCmd.Stdout = os.Stdout
   199  		}
   200  
   201  		if err != nil {
   202  			fmt.Printf("%v\n", err)
   203  			os.Exit(254)
   204  		}
   205  
   206  		usages := make(map[int32][]*ProcessStatus)
   207  
   208  		timeToStop := time.Now().Add(d)
   209  
   210  		for time.Now().Before(timeToStop) {
   211  			usage, err := getUsage(int32(execCmd.Process.Pid))
   212  			if err != nil {
   213  				panic(err)
   214  			}
   215  			if flagVerbose {
   216  				printUsage(usage)
   217  			}
   218  
   219  			if flagSaveToCsv {
   220  				records = addRecords(usage, records)
   221  			}
   222  
   223  			for _, ps := range usage {
   224  				usages[ps.Pid] = append(usages[ps.Pid], ps)
   225  			}
   226  
   227  			_, err = process.NewProcess(int32(execCmd.Process.Pid))
   228  			if err != nil {
   229  				// process.Process.IsRunning is not implemented yet
   230  				fmt.Fprintf(os.Stderr, "rkt exited prematurely\n")
   231  				break
   232  			}
   233  
   234  			time.Sleep(time.Second)
   235  		}
   236  
   237  		loadAvg, err = load.Avg()
   238  		if err != nil {
   239  			fmt.Fprintf(os.Stderr, "measure load avg failed: %v\n", err)
   240  		}
   241  
   242  		stopCmd = exec.Command(rktBinary, "stop", containerId)
   243  
   244  		cmdStopReader, err := stopCmd.StdoutPipe()
   245  		if err != nil {
   246  			fmt.Fprintln(os.Stderr, "Error creating StdoutPipe for stopCmd", err)
   247  			os.Exit(254)
   248  		}
   249  		cmdStopScanner := bufio.NewScanner(cmdStopReader)
   250  
   251  		containerStopping = time.Now()
   252  		stopConfirmation := make(chan bool, 1)
   253  		go func() {
   254  			for cmdStopScanner.Scan() {
   255  				if strings.Contains(cmdStopScanner.Text(), containerId) {
   256  					stopConfirmation <- true
   257  					return
   258  				}
   259  			}
   260  			stopConfirmation <- false
   261  		}()
   262  		err = stopCmd.Start()
   263  		if !<-stopConfirmation {
   264  			fmt.Println("WARNING: There was a problem stopping the container! (Container already stopped?)")
   265  		}
   266  		containerStopped = time.Now()
   267  		close(stopConfirmation)
   268  
   269  		if err != nil {
   270  			fmt.Printf("%v\n", err)
   271  			os.Exit(254)
   272  		}
   273  
   274  		gcCmd = exec.Command(rktBinary, "gc", "--grace-period=0")
   275  		gcCmd.Start()
   276  
   277  		for _, processHistory := range usages {
   278  			var avgCPU float64
   279  			var avgMem uint64
   280  			var peakMem uint64
   281  
   282  			for _, p := range processHistory {
   283  				avgCPU += p.CPU
   284  				avgMem += p.RSS
   285  				if peakMem < p.RSS {
   286  					peakMem = p.RSS
   287  				}
   288  			}
   289  
   290  			avgCPU = avgCPU / float64(len(processHistory))
   291  			avgMem = avgMem / uint64(len(processHistory))
   292  
   293  			if !flagSaveToCsv {
   294  				fmt.Printf("%s(%d): seconds alive: %d  avg CPU: %f%%  avg Mem: %s  peak Mem: %s\n", processHistory[0].Name, processHistory[0].Pid, len(processHistory), avgCPU, formatSize(avgMem), formatSize(peakMem))
   295  			}
   296  		}
   297  
   298  		if flagSaveToCsv {
   299  			summaryRecords = append(summaryRecords, []string{
   300  				strconv.FormatFloat(loadAvg.Load1, 'g', 3, 64),
   301  				strconv.FormatFloat(loadAvg.Load5, 'g', 3, 64),
   302  				strconv.FormatFloat(loadAvg.Load15, 'g', 3, 64),
   303  				strconv.FormatFloat(float64(containerStarted.Sub(containerStarting).Nanoseconds())/float64(time.Millisecond), 'g', -1, 64),
   304  				strconv.FormatFloat(float64(containerStopped.Sub(containerStopping).Nanoseconds())/float64(time.Millisecond), 'g', -1, 64)})
   305  		}
   306  
   307  		fmt.Printf("load average: Load1: %f Load5: %f Load15: %f\n", loadAvg.Load1, loadAvg.Load5, loadAvg.Load15)
   308  		fmt.Printf("container start time: %sms\n", strconv.FormatFloat(float64(containerStarted.Sub(containerStarting).Nanoseconds())/float64(time.Millisecond), 'g', -1, 64))
   309  		fmt.Printf("container stop time: %sms\n", strconv.FormatFloat(float64(containerStopped.Sub(containerStopping).Nanoseconds())/float64(time.Millisecond), 'g', -1, 64))
   310  	}
   311  
   312  	t := time.Now()
   313  	_, testImage = filepath.Split(args[0])
   314  	prefix := fmt.Sprintf("%d-%02d-%02d_%02d-%02d_%s_%s", t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute(), flavorType, testImage)
   315  	if flagSaveToCsv {
   316  		err = saveRecords(records, flagCsvDir, prefix+"_rkt_benchmark_interval.csv")
   317  		if err != nil {
   318  			fmt.Fprintf(os.Stderr, "Can't write to a file: %v\n", err)
   319  		}
   320  		err = saveRecords(summaryRecords, flagCsvDir, prefix+"_rkt_benchmark_summary.csv")
   321  		if err != nil {
   322  			fmt.Fprintf(os.Stderr, "Can't write to a summary file: %v\n", err)
   323  		}
   324  	}
   325  }
   326  
   327  func fileExist(filename string) bool {
   328  	if _, err := os.Stat(filename); err == nil {
   329  		return true
   330  	}
   331  	return false
   332  }
   333  
   334  func getUsage(pid int32) ([]*ProcessStatus, error) {
   335  	var statuses []*ProcessStatus
   336  	pids := []int32{pid}
   337  	for i := 0; i < len(pids); i++ {
   338  		proc, ok := pidMap[pids[i]]
   339  		if !ok {
   340  			var err error
   341  			proc, err = process.NewProcess(pids[i])
   342  			if err != nil {
   343  				return nil, err
   344  			}
   345  			pidMap[pids[i]] = proc
   346  		}
   347  		s, err := getProcStatus(proc)
   348  		if err != nil {
   349  			return nil, err
   350  		}
   351  		statuses = append(statuses, s)
   352  
   353  		children, err := proc.Children()
   354  		if err != nil && err != process.ErrorNoChildren {
   355  			return nil, err
   356  		}
   357  
   358  	childloop:
   359  		for _, child := range children {
   360  			for _, p := range pids {
   361  				if p == child.Pid {
   362  					fmt.Printf("%d is in %#v\n", p, pids)
   363  					continue childloop
   364  				}
   365  			}
   366  			pids = append(pids, child.Pid)
   367  		}
   368  	}
   369  	return statuses, nil
   370  }
   371  
   372  func getProcStatus(p *process.Process) (*ProcessStatus, error) {
   373  	n, err := p.Name()
   374  	if err != nil {
   375  		return nil, err
   376  	}
   377  	c, err := p.Percent(0)
   378  	if err != nil {
   379  		return nil, err
   380  	}
   381  	m, err := p.MemoryInfo()
   382  	if err != nil {
   383  		return nil, err
   384  	}
   385  	return &ProcessStatus{
   386  		Pid:  p.Pid,
   387  		Name: n,
   388  		CPU:  c,
   389  		VMS:  m.VMS,
   390  		RSS:  m.RSS,
   391  		Swap: m.Swap,
   392  	}, nil
   393  }
   394  
   395  func formatSize(size uint64) string {
   396  	if size > 1024*1024*1024 {
   397  		return strconv.FormatUint(size/(1024*1024*1024), 10) + " gB"
   398  	}
   399  	if size > 1024*1024 {
   400  		return strconv.FormatUint(size/(1024*1024), 10) + " mB"
   401  	}
   402  	if size > 1024 {
   403  		return strconv.FormatUint(size/1024, 10) + " kB"
   404  	}
   405  	return strconv.FormatUint(size, 10) + " B"
   406  }
   407  
   408  func printUsage(statuses []*ProcessStatus) {
   409  	for _, s := range statuses {
   410  		fmt.Printf("%s(%d): Mem: %s CPU: %f\n", s.Name, s.Pid, formatSize(s.RSS), s.CPU)
   411  	}
   412  	fmt.Printf("\n")
   413  }
   414  
   415  func addRecords(statuses []*ProcessStatus, records [][]string) [][]string {
   416  	for _, s := range statuses {
   417  		records = append(records, []string{time.Now().String(), s.Name, strconv.Itoa(int(s.Pid)), formatSize(s.RSS), strconv.FormatFloat(s.CPU, 'g', -1, 64)})
   418  	}
   419  	return records
   420  }
   421  
   422  func saveRecords(records [][]string, dir, filename string) error {
   423  	csvFile, err := os.Create(filepath.Join(dir, filename))
   424  	defer csvFile.Close()
   425  	if err != nil {
   426  		return err
   427  	}
   428  
   429  	w := csv.NewWriter(csvFile)
   430  	w.WriteAll(records)
   431  	if err := w.Error(); err != nil {
   432  		return err
   433  	}
   434  
   435  	return nil
   436  }