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