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 }