github.com/google/cadvisor@v0.49.1/container/common/helpers.go (about) 1 // Copyright 2016 Google Inc. All Rights Reserved. 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 common 16 17 import ( 18 "fmt" 19 "math" 20 "os" 21 "path" 22 "strconv" 23 "strings" 24 "time" 25 26 "github.com/karrick/godirwalk" 27 "github.com/opencontainers/runc/libcontainer/cgroups" 28 "github.com/pkg/errors" 29 "golang.org/x/sys/unix" 30 31 "github.com/google/cadvisor/container" 32 info "github.com/google/cadvisor/info/v1" 33 "github.com/google/cadvisor/utils" 34 35 "k8s.io/klog/v2" 36 ) 37 38 func DebugInfo(watches map[string][]string) map[string][]string { 39 out := make(map[string][]string) 40 41 lines := make([]string, 0, len(watches)) 42 for containerName, cgroupWatches := range watches { 43 lines = append(lines, fmt.Sprintf("%s:", containerName)) 44 for _, cg := range cgroupWatches { 45 lines = append(lines, fmt.Sprintf("\t%s", cg)) 46 } 47 } 48 out["Inotify watches"] = lines 49 50 return out 51 } 52 53 var bootTime = func() time.Time { 54 now := time.Now() 55 var sysinfo unix.Sysinfo_t 56 if err := unix.Sysinfo(&sysinfo); err != nil { 57 return now 58 } 59 sinceBoot := time.Duration(sysinfo.Uptime) * time.Second 60 return now.Add(-1 * sinceBoot).Truncate(time.Minute) 61 }() 62 63 func GetSpec(cgroupPaths map[string]string, machineInfoFactory info.MachineInfoFactory, hasNetwork, hasFilesystem bool) (info.ContainerSpec, error) { 64 return getSpecInternal(cgroupPaths, machineInfoFactory, hasNetwork, hasFilesystem, cgroups.IsCgroup2UnifiedMode()) 65 } 66 67 func getSpecInternal(cgroupPaths map[string]string, machineInfoFactory info.MachineInfoFactory, hasNetwork, hasFilesystem, cgroup2UnifiedMode bool) (info.ContainerSpec, error) { 68 var spec info.ContainerSpec 69 70 // Assume unified hierarchy containers. 71 // Get the lowest creation time from all hierarchies as the container creation time. 72 now := time.Now() 73 lowestTime := now 74 for _, cgroupPathDir := range cgroupPaths { 75 dir, err := os.Stat(cgroupPathDir) 76 if err == nil && dir.ModTime().Before(lowestTime) { 77 lowestTime = dir.ModTime() 78 } else if os.IsNotExist(err) { 79 // Directory does not exist, skip checking for files within. 80 continue 81 } 82 83 // The modified time of the cgroup directory sometimes changes whenever a subcontainer is created. 84 // eg. /docker will have creation time matching the creation of latest docker container. 85 // Use clone_children/events as a workaround as it isn't usually modified. It is only likely changed 86 // immediately after creating a container. If the directory modified time is lower, we use that. 87 cgroupPathFile := path.Join(cgroupPathDir, "cgroup.clone_children") 88 if cgroup2UnifiedMode { 89 cgroupPathFile = path.Join(cgroupPathDir, "cgroup.events") 90 } 91 fi, err := os.Stat(cgroupPathFile) 92 if err == nil && fi.ModTime().Before(lowestTime) { 93 lowestTime = fi.ModTime() 94 } 95 } 96 if lowestTime.Before(bootTime) { 97 lowestTime = bootTime 98 } 99 100 if lowestTime != now { 101 spec.CreationTime = lowestTime 102 } 103 104 // Get machine info. 105 mi, err := machineInfoFactory.GetMachineInfo() 106 if err != nil { 107 return spec, err 108 } 109 110 // CPU. 111 cpuRoot, ok := GetControllerPath(cgroupPaths, "cpu", cgroup2UnifiedMode) 112 if ok { 113 if utils.FileExists(cpuRoot) { 114 if cgroup2UnifiedMode { 115 spec.HasCpu = true 116 117 weight := readUInt64(cpuRoot, "cpu.weight") 118 if weight > 0 { 119 limit, err := convertCPUWeightToCPULimit(weight) 120 if err != nil { 121 klog.Errorf("GetSpec: Failed to read CPULimit from %q: %s", path.Join(cpuRoot, "cpu.weight"), err) 122 } else { 123 spec.Cpu.Limit = limit 124 } 125 } 126 max := readString(cpuRoot, "cpu.max") 127 if max != "" { 128 splits := strings.SplitN(max, " ", 2) 129 if len(splits) != 2 { 130 klog.Errorf("GetSpec: Failed to parse CPUmax from %q", path.Join(cpuRoot, "cpu.max")) 131 } else { 132 if splits[0] != "max" { 133 spec.Cpu.Quota = parseUint64String(splits[0]) 134 } 135 spec.Cpu.Period = parseUint64String(splits[1]) 136 } 137 } 138 } else { 139 spec.HasCpu = true 140 spec.Cpu.Limit = readUInt64(cpuRoot, "cpu.shares") 141 spec.Cpu.Period = readUInt64(cpuRoot, "cpu.cfs_period_us") 142 quota := readString(cpuRoot, "cpu.cfs_quota_us") 143 144 if quota != "" && quota != "-1" { 145 val, err := strconv.ParseUint(quota, 10, 64) 146 if err != nil { 147 klog.Errorf("GetSpec: Failed to parse CPUQuota from %q: %s", path.Join(cpuRoot, "cpu.cfs_quota_us"), err) 148 } else { 149 spec.Cpu.Quota = val 150 } 151 } 152 } 153 } 154 } 155 156 // Cpu Mask. 157 // This will fail for non-unified hierarchies. We'll return the whole machine mask in that case. 158 cpusetRoot, ok := GetControllerPath(cgroupPaths, "cpuset", cgroup2UnifiedMode) 159 if ok { 160 if utils.FileExists(cpusetRoot) { 161 spec.HasCpu = true 162 mask := "" 163 if cgroup2UnifiedMode { 164 mask = readString(cpusetRoot, "cpuset.cpus.effective") 165 } else { 166 mask = readString(cpusetRoot, "cpuset.cpus") 167 } 168 spec.Cpu.Mask = utils.FixCpuMask(mask, mi.NumCores) 169 } 170 } 171 172 // Memory 173 memoryRoot, ok := GetControllerPath(cgroupPaths, "memory", cgroup2UnifiedMode) 174 if ok { 175 if cgroup2UnifiedMode { 176 if utils.FileExists(path.Join(memoryRoot, "memory.max")) { 177 spec.HasMemory = true 178 spec.Memory.Reservation = readUInt64(memoryRoot, "memory.min") 179 spec.Memory.Limit = readUInt64(memoryRoot, "memory.max") 180 spec.Memory.SwapLimit = readUInt64(memoryRoot, "memory.swap.max") 181 } 182 } else { 183 if utils.FileExists(memoryRoot) { 184 spec.HasMemory = true 185 spec.Memory.Limit = readUInt64(memoryRoot, "memory.limit_in_bytes") 186 spec.Memory.SwapLimit = readUInt64(memoryRoot, "memory.memsw.limit_in_bytes") 187 spec.Memory.Reservation = readUInt64(memoryRoot, "memory.soft_limit_in_bytes") 188 } 189 } 190 } 191 192 // Hugepage 193 hugepageRoot, ok := cgroupPaths["hugetlb"] 194 if ok { 195 if utils.FileExists(hugepageRoot) { 196 spec.HasHugetlb = true 197 } 198 } 199 200 // Processes, read it's value from pids path directly 201 pidsRoot, ok := GetControllerPath(cgroupPaths, "pids", cgroup2UnifiedMode) 202 if ok { 203 if utils.FileExists(pidsRoot) { 204 spec.HasProcesses = true 205 spec.Processes.Limit = readUInt64(pidsRoot, "pids.max") 206 } 207 } 208 209 spec.HasNetwork = hasNetwork 210 spec.HasFilesystem = hasFilesystem 211 212 ioControllerName := "blkio" 213 if cgroup2UnifiedMode { 214 ioControllerName = "io" 215 } 216 217 if blkioRoot, ok := GetControllerPath(cgroupPaths, ioControllerName, cgroup2UnifiedMode); ok && utils.FileExists(blkioRoot) { 218 spec.HasDiskIo = true 219 } 220 221 return spec, nil 222 } 223 224 func GetControllerPath(cgroupPaths map[string]string, controllerName string, cgroup2UnifiedMode bool) (string, bool) { 225 226 ok := false 227 path := "" 228 229 if cgroup2UnifiedMode { 230 path, ok = cgroupPaths[""] 231 } else { 232 path, ok = cgroupPaths[controllerName] 233 } 234 return path, ok 235 } 236 237 func readString(dirpath string, file string) string { 238 cgroupFile := path.Join(dirpath, file) 239 240 // Read 241 out, err := os.ReadFile(cgroupFile) 242 if err != nil { 243 // Ignore non-existent files 244 if !os.IsNotExist(err) { 245 klog.Warningf("readString: Failed to read %q: %s", cgroupFile, err) 246 } 247 return "" 248 } 249 return strings.TrimSpace(string(out)) 250 } 251 252 // Convert from [1-10000] to [2-262144] 253 func convertCPUWeightToCPULimit(weight uint64) (uint64, error) { 254 const ( 255 // minWeight is the lowest value possible for cpu.weight 256 minWeight = 1 257 // maxWeight is the highest value possible for cpu.weight 258 maxWeight = 10000 259 ) 260 if weight < minWeight || weight > maxWeight { 261 return 0, fmt.Errorf("convertCPUWeightToCPULimit: invalid cpu weight: %v", weight) 262 } 263 return 2 + ((weight-1)*262142)/9999, nil 264 } 265 266 func parseUint64String(strValue string) uint64 { 267 if strValue == "max" { 268 return math.MaxUint64 269 } 270 if strValue == "" { 271 return 0 272 } 273 274 val, err := strconv.ParseUint(strValue, 10, 64) 275 if err != nil { 276 klog.Errorf("parseUint64String: Failed to parse int %q: %s", strValue, err) 277 return 0 278 } 279 280 return val 281 } 282 283 func readUInt64(dirpath string, file string) uint64 { 284 out := readString(dirpath, file) 285 if out == "max" { 286 return math.MaxUint64 287 } 288 if out == "" { 289 return 0 290 } 291 292 val, err := strconv.ParseUint(out, 10, 64) 293 if err != nil { 294 klog.Errorf("readUInt64: Failed to parse int %q from file %q: %s", out, path.Join(dirpath, file), err) 295 return 0 296 } 297 298 return val 299 } 300 301 // Lists all directories under "path" and outputs the results as children of "parent". 302 func ListDirectories(dirpath string, parent string, recursive bool, output map[string]struct{}) error { 303 buf := make([]byte, godirwalk.MinimumScratchBufferSize) 304 return listDirectories(dirpath, parent, recursive, output, buf) 305 } 306 307 func listDirectories(dirpath string, parent string, recursive bool, output map[string]struct{}, buf []byte) error { 308 dirents, err := godirwalk.ReadDirents(dirpath, buf) 309 if err != nil { 310 // Ignore if this hierarchy does not exist. 311 if os.IsNotExist(errors.Cause(err)) { 312 err = nil 313 } 314 return err 315 } 316 for _, dirent := range dirents { 317 // We only grab directories. 318 if !dirent.IsDir() { 319 continue 320 } 321 dirname := dirent.Name() 322 323 name := path.Join(parent, dirname) 324 output[name] = struct{}{} 325 326 // List subcontainers if asked to. 327 if recursive { 328 err := listDirectories(path.Join(dirpath, dirname), name, true, output, buf) 329 if err != nil { 330 return err 331 } 332 } 333 } 334 return nil 335 } 336 337 func MakeCgroupPaths(mountPoints map[string]string, name string) map[string]string { 338 cgroupPaths := make(map[string]string, len(mountPoints)) 339 for key, val := range mountPoints { 340 cgroupPaths[key] = path.Join(val, name) 341 } 342 343 return cgroupPaths 344 } 345 346 func CgroupExists(cgroupPaths map[string]string) bool { 347 // If any cgroup exists, the container is still alive. 348 for _, cgroupPath := range cgroupPaths { 349 if utils.FileExists(cgroupPath) { 350 return true 351 } 352 } 353 return false 354 } 355 356 func ListContainers(name string, cgroupPaths map[string]string, listType container.ListType) ([]info.ContainerReference, error) { 357 containers := make(map[string]struct{}) 358 for _, cgroupPath := range cgroupPaths { 359 err := ListDirectories(cgroupPath, name, listType == container.ListRecursive, containers) 360 if err != nil { 361 return nil, err 362 } 363 } 364 365 // Make into container references. 366 ret := make([]info.ContainerReference, 0, len(containers)) 367 for cont := range containers { 368 ret = append(ret, info.ContainerReference{ 369 Name: cont, 370 }) 371 } 372 373 return ret, nil 374 } 375 376 // AssignDeviceNamesToDiskStats assigns the Device field on the provided DiskIoStats by looking up 377 // the device major and minor identifiers in the provided device namer. 378 func AssignDeviceNamesToDiskStats(namer DeviceNamer, stats *info.DiskIoStats) { 379 assignDeviceNamesToPerDiskStats( 380 namer, 381 stats.IoMerged, 382 stats.IoQueued, 383 stats.IoServiceBytes, 384 stats.IoServiceTime, 385 stats.IoServiced, 386 stats.IoTime, 387 stats.IoWaitTime, 388 stats.Sectors, 389 ) 390 } 391 392 // assignDeviceNamesToPerDiskStats looks up device names for the provided stats, caching names 393 // if necessary. 394 func assignDeviceNamesToPerDiskStats(namer DeviceNamer, diskStats ...[]info.PerDiskStats) { 395 devices := make(deviceIdentifierMap) 396 for _, stats := range diskStats { 397 for i, stat := range stats { 398 stats[i].Device = devices.Find(stat.Major, stat.Minor, namer) 399 } 400 } 401 } 402 403 // DeviceNamer returns string names for devices by their major and minor id. 404 type DeviceNamer interface { 405 // DeviceName returns the name of the device by its major and minor ids, or false if no 406 // such device is recognized. 407 DeviceName(major, minor uint64) (string, bool) 408 } 409 410 type MachineInfoNamer info.MachineInfo 411 412 func (n *MachineInfoNamer) DeviceName(major, minor uint64) (string, bool) { 413 for _, info := range n.DiskMap { 414 if info.Major == major && info.Minor == minor { 415 return "/dev/" + info.Name, true 416 } 417 } 418 for _, info := range n.Filesystems { 419 if info.DeviceMajor == major && info.DeviceMinor == minor { 420 return info.Device, true 421 } 422 } 423 return "", false 424 } 425 426 type deviceIdentifier struct { 427 major uint64 428 minor uint64 429 } 430 431 type deviceIdentifierMap map[deviceIdentifier]string 432 433 // Find locates the device name by device identifier out of from, caching the result as necessary. 434 func (m deviceIdentifierMap) Find(major, minor uint64, namer DeviceNamer) string { 435 d := deviceIdentifier{major, minor} 436 if s, ok := m[d]; ok { 437 return s 438 } 439 s, _ := namer.DeviceName(major, minor) 440 m[d] = s 441 return s 442 } 443 444 // RemoveNetMetrics is used to remove any network metrics from the given MetricSet. 445 // It returns the original set as is if remove is false, or if there are no metrics 446 // to remove. 447 func RemoveNetMetrics(metrics container.MetricSet, remove bool) container.MetricSet { 448 if !remove { 449 return metrics 450 } 451 452 // Check if there is anything we can remove, to avoid useless copying. 453 if !metrics.HasAny(container.AllNetworkMetrics) { 454 return metrics 455 } 456 457 // A copy of all metrics except for network ones. 458 return metrics.Difference(container.AllNetworkMetrics) 459 }