blkio.go (8348B)
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 cgroup1 18 19 import ( 20 "bufio" 21 "fmt" 22 "io" 23 "os" 24 "path/filepath" 25 "strconv" 26 "strings" 27 28 v1 "github.com/containerd/cgroups/v3/cgroup1/stats" 29 30 specs "github.com/opencontainers/runtime-spec/specs-go" 31 ) 32 33 // NewBlkio returns a Blkio controller given the root folder of cgroups. 34 // It may optionally accept other configuration options, such as ProcRoot(path) 35 func NewBlkio(root string, options ...func(controller *blkioController)) *blkioController { 36 ctrl := &blkioController{ 37 root: filepath.Join(root, string(Blkio)), 38 procRoot: "/proc", 39 } 40 for _, opt := range options { 41 opt(ctrl) 42 } 43 return ctrl 44 } 45 46 // ProcRoot overrides the default location of the "/proc" filesystem 47 func ProcRoot(path string) func(controller *blkioController) { 48 return func(c *blkioController) { 49 c.procRoot = path 50 } 51 } 52 53 type blkioController struct { 54 root string 55 procRoot string 56 } 57 58 func (b *blkioController) Name() Name { 59 return Blkio 60 } 61 62 func (b *blkioController) Path(path string) string { 63 return filepath.Join(b.root, path) 64 } 65 66 func (b *blkioController) Create(path string, resources *specs.LinuxResources) error { 67 if err := os.MkdirAll(b.Path(path), defaultDirPerm); err != nil { 68 return err 69 } 70 if resources.BlockIO == nil { 71 return nil 72 } 73 for _, t := range createBlkioSettings(resources.BlockIO) { 74 if t.value != nil { 75 if err := os.WriteFile( 76 filepath.Join(b.Path(path), "blkio."+t.name), 77 t.format(t.value), 78 defaultFilePerm, 79 ); err != nil { 80 return err 81 } 82 } 83 } 84 return nil 85 } 86 87 func (b *blkioController) Update(path string, resources *specs.LinuxResources) error { 88 return b.Create(path, resources) 89 } 90 91 func (b *blkioController) Stat(path string, stats *v1.Metrics) error { 92 stats.Blkio = &v1.BlkIOStat{} 93 94 var settings []blkioStatSettings 95 96 // Try to read CFQ stats available on all CFQ enabled kernels first 97 if _, err := os.Lstat(filepath.Join(b.Path(path), "blkio.io_serviced_recursive")); err == nil { 98 settings = []blkioStatSettings{ 99 { 100 name: "sectors_recursive", 101 entry: &stats.Blkio.SectorsRecursive, 102 }, 103 { 104 name: "io_service_bytes_recursive", 105 entry: &stats.Blkio.IoServiceBytesRecursive, 106 }, 107 { 108 name: "io_serviced_recursive", 109 entry: &stats.Blkio.IoServicedRecursive, 110 }, 111 { 112 name: "io_queued_recursive", 113 entry: &stats.Blkio.IoQueuedRecursive, 114 }, 115 { 116 name: "io_service_time_recursive", 117 entry: &stats.Blkio.IoServiceTimeRecursive, 118 }, 119 { 120 name: "io_wait_time_recursive", 121 entry: &stats.Blkio.IoWaitTimeRecursive, 122 }, 123 { 124 name: "io_merged_recursive", 125 entry: &stats.Blkio.IoMergedRecursive, 126 }, 127 { 128 name: "time_recursive", 129 entry: &stats.Blkio.IoTimeRecursive, 130 }, 131 } 132 } 133 134 f, err := os.Open(filepath.Join(b.procRoot, "partitions")) 135 if err != nil { 136 return err 137 } 138 defer f.Close() 139 140 devices, err := getDevices(f) 141 if err != nil { 142 return err 143 } 144 145 var size int 146 for _, t := range settings { 147 if err := b.readEntry(devices, path, t.name, t.entry); err != nil { 148 return err 149 } 150 size += len(*t.entry) 151 } 152 if size > 0 { 153 return nil 154 } 155 156 // Even the kernel is compiled with the CFQ scheduler, the cgroup may not use 157 // block devices with the CFQ scheduler. If so, we should fallback to throttle.* files. 158 settings = []blkioStatSettings{ 159 { 160 name: "throttle.io_serviced", 161 entry: &stats.Blkio.IoServicedRecursive, 162 }, 163 { 164 name: "throttle.io_service_bytes", 165 entry: &stats.Blkio.IoServiceBytesRecursive, 166 }, 167 } 168 for _, t := range settings { 169 if err := b.readEntry(devices, path, t.name, t.entry); err != nil { 170 return err 171 } 172 } 173 return nil 174 } 175 176 func (b *blkioController) readEntry(devices map[deviceKey]string, path, name string, entry *[]*v1.BlkIOEntry) error { 177 f, err := os.Open(filepath.Join(b.Path(path), "blkio."+name)) 178 if err != nil { 179 return err 180 } 181 defer f.Close() 182 sc := bufio.NewScanner(f) 183 for sc.Scan() { 184 // format: dev type amount 185 fields := strings.FieldsFunc(sc.Text(), splitBlkIOStatLine) 186 if len(fields) < 3 { 187 if len(fields) == 2 && fields[0] == "Total" { 188 // skip total line 189 continue 190 } else { 191 return fmt.Errorf("invalid line found while parsing %s: %s", path, sc.Text()) 192 } 193 } 194 major, err := strconv.ParseUint(fields[0], 10, 64) 195 if err != nil { 196 return err 197 } 198 minor, err := strconv.ParseUint(fields[1], 10, 64) 199 if err != nil { 200 return err 201 } 202 op := "" 203 valueField := 2 204 if len(fields) == 4 { 205 op = fields[2] 206 valueField = 3 207 } 208 v, err := strconv.ParseUint(fields[valueField], 10, 64) 209 if err != nil { 210 return err 211 } 212 *entry = append(*entry, &v1.BlkIOEntry{ 213 Device: devices[deviceKey{major, minor}], 214 Major: major, 215 Minor: minor, 216 Op: op, 217 Value: v, 218 }) 219 } 220 return sc.Err() 221 } 222 223 func createBlkioSettings(blkio *specs.LinuxBlockIO) []blkioSettings { 224 settings := []blkioSettings{} 225 226 if blkio.Weight != nil { 227 settings = append(settings, 228 blkioSettings{ 229 name: "weight", 230 value: blkio.Weight, 231 format: uintf, 232 }) 233 } 234 if blkio.LeafWeight != nil { 235 settings = append(settings, 236 blkioSettings{ 237 name: "leaf_weight", 238 value: blkio.LeafWeight, 239 format: uintf, 240 }) 241 } 242 for _, wd := range blkio.WeightDevice { 243 if wd.Weight != nil { 244 settings = append(settings, 245 blkioSettings{ 246 name: "weight_device", 247 value: wd, 248 format: weightdev, 249 }) 250 } 251 if wd.LeafWeight != nil { 252 settings = append(settings, 253 blkioSettings{ 254 name: "leaf_weight_device", 255 value: wd, 256 format: weightleafdev, 257 }) 258 } 259 } 260 for _, t := range []struct { 261 name string 262 list []specs.LinuxThrottleDevice 263 }{ 264 { 265 name: "throttle.read_bps_device", 266 list: blkio.ThrottleReadBpsDevice, 267 }, 268 { 269 name: "throttle.read_iops_device", 270 list: blkio.ThrottleReadIOPSDevice, 271 }, 272 { 273 name: "throttle.write_bps_device", 274 list: blkio.ThrottleWriteBpsDevice, 275 }, 276 { 277 name: "throttle.write_iops_device", 278 list: blkio.ThrottleWriteIOPSDevice, 279 }, 280 } { 281 for _, td := range t.list { 282 settings = append(settings, blkioSettings{ 283 name: t.name, 284 value: td, 285 format: throttleddev, 286 }) 287 } 288 } 289 return settings 290 } 291 292 type blkioSettings struct { 293 name string 294 value interface{} 295 format func(v interface{}) []byte 296 } 297 298 type blkioStatSettings struct { 299 name string 300 entry *[]*v1.BlkIOEntry 301 } 302 303 func uintf(v interface{}) []byte { 304 return []byte(strconv.FormatUint(uint64(*v.(*uint16)), 10)) 305 } 306 307 func weightdev(v interface{}) []byte { 308 wd := v.(specs.LinuxWeightDevice) 309 return []byte(fmt.Sprintf("%d:%d %d", wd.Major, wd.Minor, *wd.Weight)) 310 } 311 312 func weightleafdev(v interface{}) []byte { 313 wd := v.(specs.LinuxWeightDevice) 314 return []byte(fmt.Sprintf("%d:%d %d", wd.Major, wd.Minor, *wd.LeafWeight)) 315 } 316 317 func throttleddev(v interface{}) []byte { 318 td := v.(specs.LinuxThrottleDevice) 319 return []byte(fmt.Sprintf("%d:%d %d", td.Major, td.Minor, td.Rate)) 320 } 321 322 func splitBlkIOStatLine(r rune) bool { 323 return r == ' ' || r == ':' 324 } 325 326 type deviceKey struct { 327 major, minor uint64 328 } 329 330 // getDevices makes a best effort attempt to read all the devices into a map 331 // keyed by major and minor number. Since devices may be mapped multiple times, 332 // we err on taking the first occurrence. 333 func getDevices(r io.Reader) (map[deviceKey]string, error) { 334 335 var ( 336 s = bufio.NewScanner(r) 337 devices = make(map[deviceKey]string) 338 ) 339 for i := 0; s.Scan(); i++ { 340 if i < 2 { 341 continue 342 } 343 fields := strings.Fields(s.Text()) 344 major, err := strconv.Atoi(fields[0]) 345 if err != nil { 346 return nil, err 347 } 348 minor, err := strconv.Atoi(fields[1]) 349 if err != nil { 350 return nil, err 351 } 352 key := deviceKey{ 353 major: uint64(major), 354 minor: uint64(minor), 355 } 356 if _, ok := devices[key]; ok { 357 continue 358 } 359 devices[key] = filepath.Join("/dev", fields[3]) 360 } 361 return devices, s.Err() 362 }