This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

core / spindle / engines / microvm / cgroup.go
9.5 kB 326 lines
1//go:build linux 2 3package microvm 4 5import ( 6 "errors" 7 "fmt" 8 "log/slog" 9 "os" 10 "path/filepath" 11 "regexp" 12 "strconv" 13 "strings" 14 "syscall" 15 16 cgroups "github.com/containerd/cgroups/v3" 17 "github.com/containerd/cgroups/v3/cgroup2" 18 "github.com/prometheus/procfs" 19) 20 21var ( 22 cgroupInvalidChar = regexp.MustCompile(`[^a-zA-Z0-9\-_.]`) 23 cgroupConsecutiveSep = regexp.MustCompile(`[-_.]{2,}`) 24) 25 26const ( 27 cgroupParentSelf = "self" 28 supervisorCgroupName = "supervisor" 29) 30 31type CgroupLimits struct { 32 Enabled bool 33 Parent *CgroupParent 34 Name string 35 MemoryMaxMiB int64 36 SwapMaxMiB *int64 37 PidsMax int64 38 // cpu.max quota as a percentage of one core (100 = one core). <= 0 39 // leaves cpu unlimited 40 CPUQuotaPercent int64 41 // io.weight (1-10000). 0 leaves io unlimited, written directly since 42 // this cgroup2 lib only models io.bfq.weight/io.max 43 IOWeight uint64 44} 45 46type CgroupParent struct { 47 root *cgroup2.Manager 48 mountpoint string 49 group string 50} 51 52type CgroupHandle struct { 53 manager *cgroup2.Manager 54} 55 56func initCgroupParent(parent string, supervisorMemoryMinMiB int64, logger *slog.Logger) (*CgroupParent, error) { 57 if parent == "" { 58 parent = cgroupParentSelf 59 } 60 if cgroups.Mode() != cgroups.Unified { 61 return nil, fmt.Errorf("microVM cgroups require cgroup v2 unified mode") 62 } 63 64 mountpoint, group, err := resolveCgroupParent(parent) 65 if err != nil { 66 return nil, err 67 } 68 if _, err := os.Stat(filepath.Join(mountpoint, strings.TrimPrefix(group, "/"))); err != nil { 69 return nil, fmt.Errorf("stat cgroup parent %q:%q: %w", mountpoint, group, err) 70 } 71 72 root, err := cgroup2.Load(group, cgroup2.WithMountpoint(mountpoint)) 73 if err != nil { 74 return nil, fmt.Errorf("load cgroup parent %q:%q: %w", mountpoint, group, err) 75 } 76 77 if group != "/" { 78 if err := moveParentProcesses(root, supervisorMemoryMinMiB, logger); err != nil { 79 return nil, err 80 } 81 } else if err := probeRootSubtreeControl(mountpoint); err != nil { 82 if !errors.Is(err, syscall.EBUSY) { 83 return nil, fmt.Errorf("enable controllers in subtree_control of cgroup root %q: %w", mountpoint, err) 84 } 85 // populated namespace root, not the real root: vacate it too 86 if err := moveParentProcesses(root, supervisorMemoryMinMiB, logger); err != nil { 87 return nil, err 88 } 89 } 90 91 if logger != nil { 92 logger.Info("initialized microVM cgroup parent", "mountpoint", mountpoint, "group", group) 93 } 94 return &CgroupParent{root: root, mountpoint: mountpoint, group: group}, nil 95} 96 97func prepareCgroup(limits CgroupLimits, logger *slog.Logger) (*CgroupHandle, error) { 98 if !limits.Enabled { 99 return nil, nil 100 } 101 if limits.Parent == nil || limits.Parent.root == nil { 102 return nil, fmt.Errorf("cgroup parent is not initialized") 103 } 104 name := sanitizeCgroupName(limits.Name) 105 if name == "" { 106 return nil, fmt.Errorf("cgroup name is empty") 107 } 108 109 manager, err := limits.Parent.root.NewChild(name, cgroupResources(limits)) 110 if err != nil { 111 return nil, fmt.Errorf("create cgroup %q: %w", name, err) 112 } 113 114 if limits.IOWeight > 0 { 115 if err := manager.ToggleControllers([]string{"io"}, cgroup2.Enable); err != nil { 116 _ = manager.Delete() 117 return nil, fmt.Errorf("enable io controller for cgroup %q: %w", name, err) 118 } 119 ioWeightPath := filepath.Join(limits.Parent.mountpoint, strings.TrimPrefix(limits.Parent.group, "/"), name, "io.weight") 120 if err := os.WriteFile(ioWeightPath, []byte(strconv.FormatUint(limits.IOWeight, 10)), 0); err != nil { 121 _ = manager.Delete() 122 return nil, fmt.Errorf("write io.weight for cgroup %q: %w", name, err) 123 } 124 } 125 126 if logger != nil { 127 logger.Info("created microVM cgroup", "name", name, "parentGroup", limits.Parent.group) 128 } 129 return &CgroupHandle{manager: manager}, nil 130} 131 132func cgroupResources(limits CgroupLimits) *cgroup2.Resources { 133 resources := &cgroup2.Resources{} 134 if limits.MemoryMaxMiB > 0 || limits.SwapMaxMiB != nil { 135 memory := &cgroup2.Memory{} 136 if limits.MemoryMaxMiB > 0 { 137 maxBytes := limits.MemoryMaxMiB * 1024 * 1024 138 memory.Max = &maxBytes 139 } 140 if limits.SwapMaxMiB != nil { 141 swapBytes := *limits.SwapMaxMiB * 1024 * 1024 142 memory.Swap = &swapBytes 143 } 144 oomGroup := true 145 memory.OOMGroup = &oomGroup 146 resources.Memory = memory 147 } 148 if limits.PidsMax > 0 { 149 resources.Pids = &cgroup2.Pids{Max: limits.PidsMax} 150 } 151 if limits.CPUQuotaPercent > 0 { 152 quota := limits.CPUQuotaPercent * 1000 // 100% of one 100000us period 153 resources.CPU = &cgroup2.CPU{Max: cgroup2.NewCPUMax(&quota, nil)} 154 } 155 return resources 156} 157 158func supervisorResources(memoryMinMiB int64) *cgroup2.Resources { 159 if memoryMinMiB <= 0 { 160 return nil 161 } 162 minBytes := memoryMinMiB * 1024 * 1024 163 return &cgroup2.Resources{ 164 Memory: &cgroup2.Memory{Min: &minBytes}, 165 } 166} 167 168func (h *CgroupHandle) AddProcess(pid int, logger *slog.Logger) error { 169 if h == nil || h.manager == nil { 170 return nil 171 } 172 if pid <= 0 { 173 return fmt.Errorf("invalid pid %d", pid) 174 } 175 if err := h.manager.AddProc(uint64(pid)); err != nil { 176 return fmt.Errorf("add pid %d to cgroup: %w", pid, err) 177 } 178 if logger != nil { 179 logger.Info("added process to microVM cgroup", "pid", pid) 180 } 181 return nil 182} 183 184func (h *CgroupHandle) Close() error { 185 if h == nil || h.manager == nil { 186 return nil 187 } 188 return h.manager.Delete() 189} 190 191func (h *CgroupHandle) OOMKilled() bool { 192 if h == nil || h.manager == nil { 193 return false 194 } 195 metrics, err := h.manager.Stat() 196 if err != nil || metrics == nil || metrics.MemoryEvents == nil { 197 return false 198 } 199 return metrics.MemoryEvents.OomKill > 0 200} 201 202// probeRootSubtreeControl enables the domain controllers the engine needs 203// in the "/" parent's subtree. this fails EBUSY at a populated cgroup 204// namespace root (no-internal-process constraint); the real root is exempt. 205func probeRootSubtreeControl(mountpoint string) error { 206 // cpu/io may be unavailable (eg. no CONFIG_BLK_CGROUP), memory/pids 207 // are the hard requirement 208 if err := os.WriteFile(filepath.Join(mountpoint, "cgroup.subtree_control"), []byte("+memory +pids +cpu +io"), 0); err == nil { 209 return nil 210 } 211 return os.WriteFile(filepath.Join(mountpoint, "cgroup.subtree_control"), []byte("+memory +pids"), 0) 212} 213 214func resolveCgroupParent(parent string) (string, string, error) { 215 mountpoint, err := cgroup2Mountpoint() 216 if err != nil { 217 return "", "", err 218 } 219 220 if parent == "" || parent == cgroupParentSelf { 221 group, err := selfCgroupV2Path() 222 if err != nil { 223 return "", "", err 224 } 225 return mountpoint, group, nil 226 } 227 if !filepath.IsAbs(parent) { 228 return "", "", fmt.Errorf("cgroup parent must be %q or an absolute delegated cgroupfs path: %q", cgroupParentSelf, parent) 229 } 230 231 cleanParent := filepath.Clean(parent) 232 rel, err := filepath.Rel(mountpoint, cleanParent) 233 if err != nil { 234 return "", "", fmt.Errorf("resolve cgroup parent %q relative to cgroup2 mount %q: %w", cleanParent, mountpoint, err) 235 } 236 if rel == ".." || strings.HasPrefix(rel, "../") { 237 return "", "", fmt.Errorf("cgroup parent %q is outside cgroup2 mount %q", cleanParent, mountpoint) 238 } 239 if rel == "." { 240 return mountpoint, "/", nil 241 } 242 243 group := "/" + filepath.ToSlash(rel) 244 if err := cgroup2.VerifyGroupPath(group); err != nil { 245 return "", "", fmt.Errorf("invalid cgroup parent path %q: %w", group, err) 246 } 247 return mountpoint, group, nil 248} 249 250func cgroup2Mountpoint() (string, error) { 251 mounts, err := procfs.GetMounts() 252 if err != nil { 253 return "", fmt.Errorf("read procfs mountinfo: %w", err) 254 } 255 for _, mount := range mounts { 256 if mount.FSType == "cgroup2" { 257 return mount.MountPoint, nil 258 } 259 } 260 return "", fmt.Errorf("cgroup v2 mountpoint not found") 261} 262 263func selfCgroupV2Path() (string, error) { 264 self, err := procfs.Self() 265 if err != nil { 266 return "", fmt.Errorf("open procfs self: %w", err) 267 } 268 groups, err := self.Cgroups() 269 if err != nil { 270 return "", fmt.Errorf("read procfs self cgroups: %w", err) 271 } 272 for _, group := range groups { 273 if group.HierarchyID != 0 { 274 continue 275 } 276 path := group.Path 277 if path == "" { 278 path = "/" 279 } 280 if err := cgroup2.VerifyGroupPath(path); err != nil { 281 return "", fmt.Errorf("invalid self cgroup path %q: %w", path, err) 282 } 283 return path, nil 284 } 285 return "", fmt.Errorf("current process has no cgroup v2 hierarchy entry") 286} 287 288func moveParentProcesses(parent *cgroup2.Manager, supervisorMemoryMinMiB int64, logger *slog.Logger) error { 289 procs, err := parent.Procs(false) 290 if err != nil { 291 return fmt.Errorf("list parent cgroup processes: %w", err) 292 } 293 294 // first create with empty resources 295 supervisor, err := parent.NewChild(supervisorCgroupName, &cgroup2.Resources{}) 296 if err != nil { 297 return fmt.Errorf("create supervisor cgroup: %w", err) 298 } 299 300 // move procs 301 for _, pid := range procs { 302 if err := supervisor.AddProc(pid); err != nil { 303 return fmt.Errorf("move pid %d to supervisor cgroup: %w", pid, err) 304 } 305 } 306 307 // now apply resources. we can't do this while parent has procs still 308 if res := supervisorResources(supervisorMemoryMinMiB); res != nil { 309 // we use a "new" parent here, this is so we enable subtree_control. 310 // .Update() does not work here... 311 if _, err = parent.NewChild(supervisorCgroupName, res); err != nil { 312 return fmt.Errorf("apply supervisor cgroup resources: %w", err) 313 } 314 } 315 316 if logger != nil && len(procs) > 0 { 317 logger.Info("moved spindle processes to supervisor cgroup", "processes", len(procs)) 318 } 319 return nil 320} 321 322func sanitizeCgroupName(name string) string { 323 name = cgroupInvalidChar.ReplaceAllLiteralString(name, "-") 324 name = cgroupConsecutiveSep.ReplaceAllLiteralString(name, "-") 325 return strings.Trim(name, "-_.") 326}