Runc 与 Cgroups

news/2024/12/12 6:01:12/

Runc 可以算是启动创建容器的最后一步,其中设置 Cgroups,隔离 namespaces,配置网络,挂载相应的卷 等一系列操作
在这里插入图片描述本文将主要讲 runc 是如何去操作系统中的 Cgroups,实现对资源的限制和管理的

Runc 支持三种方式来限制管理资源,分别是使用 Cgroups V1, Cgroups V2, Systemd
本文将主要讲解 Cgroups V1, 关于 Cgroups V1 相关的基本概念可以参考
Linux Cgroups V1 介绍与使用

Cgroup Manager

Cgroup Manager 是 runc 实现对系统的 cgroup 操作抽象
实现了设置资源的配置,PID 加入到指定控制组控制组的销毁,控制组进程的暂停恢复,获取配置,获取统计信息等操作

type Manager interface{Apply(pid int) error			// 将 pid 加入到控制组中Set(container *config.Config) error	// 设置控制组的配置GetCgroups()(*configs.Cgroup, error)	// 获取控制组的配置GetPids()([]int, error)			// 返回控制组(cgroup) 中的 PID, 不包括子控制组GetAllPids()([]int, error)		// 返回控制组和它的子控制组的所有 PIDGetStats() (*Stats, error)		// 获取控制组统计信息Destroy() error					// 删除控制组GetPaths()map[string]string		// 获取保存 cgroup 状态文件的路径GetUnifiedPath()(string, error)	// 如果容器组没有挂载任何控制器(子系统), 则返回值同 GetPaths,否则,返回 errorFreeze(state configs.FreezerState) error	// 任务的暂停和恢复
}

GetStats 方法会返回控制组中的统计信息,记录了 CPU, Memroy, Blkio 之类的一些状态

type Stats struct {CpuStats    CpuStats    `json:"cpu_stats,omitempty"`MemoryStats MemoryStats `json:"memory_stats,omitempty"`PidsStats   PidsStats   `json:"pids_stats,omitempty"`BlkioStats  BlkioStats  `json:"blkio_stats,omitempty"`// the map is in the format "size of hugepage: stats of the hugepage"HugetlbStats map[string]HugetlbStats `json:"hugetlb_stats,omitempty"`
}

Set 方法在设置控制组 资源的时候需要传递 config.Config实际上该方法只会使用 Config.Cgroup.Resources 中的数据

// Config 定义容器的配置
type Config struct {// ....Cgroups *Cgroup `json: "cgroup"`// ....
}type Cgroup struct {Path string `json:"path"`	// cgroup 路径,相对于`层级`的地址ScopePrefix string `json:"scope_prefix"`	// ScopePrefix describes prefix for the scope namePaths map[string]string		// 所属各个控制器的 cgroup 路径,需要注意该路径是绝对路径// 包含了各个子系统资源的设置*Resources
}
type Resources struct {AllowAllDevices *bool `json:"allow_all_devices,omitempty"`AllowedDevices []*Device `json:"allowed_devices,omitempty"`DeniedDevices []*Device `json:"denied_devices,omitempty"`Devices []*Device `json:"devices"`Memory int64 `json:"memory"`MemoryReservation int64 `json:"memory_reservation"`MemorySwap int64 `json:"memory_swap"`KernelMemory int64 `json:"kernel_memory"`KernelMemoryTCP int64 `json:"kernel_memory_tcp"`CpuShares uint64 `json:"cpu_shares"`CpuQuota int64 `json:"cpu_quota"`CpuPeriod uint64 `json:"cpu_period"`CpuRtRuntime int64 `json:"cpu_rt_quota"`CpuRtPeriod uint64 `json:"cpu_rt_period"`CpusetCpus string `json:"cpuset_cpus"`CpusetMems string `json:"cpuset_mems"`PidsLimit int64 `json:"pids_limit"`BlkioWeight uint16 `json:"blkio_weight"`BlkioLeafWeight uint16 `json:"blkio_leaf_weight"`BlkioWeightDevice []*WeightDevice `json:"blkio_weight_device"`BlkioThrottleReadBpsDevice []*ThrottleDevice `json:"blkio_throttle_read_bps_device"`BlkioThrottleWriteBpsDevice []*ThrottleDevice `json:"blkio_throttle_write_bps_device"`BlkioThrottleReadIOPSDevice []*ThrottleDevice `json:"blkio_throttle_read_iops_device"`BlkioThrottleWriteIOPSDevice []*ThrottleDevice `json:"blkio_throttle_write_iops_device"`Freezer FreezerState `json:"freezer"`HugetlbLimit []*HugepageLimit `json:"hugetlb_limit"`OomKillDisable bool `json:"oom_kill_disable"`MemorySwappiness *uint64 `json:"memory_swappiness"`NetPrioIfpriomap []*IfPrioMap `json:"net_prio_ifpriomap"`NetClsClassid uint32 `json:"net_cls_classid_u"`CpuWeight uint64 `json:"cpu_weight"`CpuMax string `json:"cpu_max"`
}

config.Cgroup 需要注意一下,其中 Path 字段是相对于层级挂载路径的控制器路径,层级的概念在Cgroups V1 中已经解释

比如,我的控制组系统中路径是 /sys/fs/cgroup/cpu/iceber/cgroup1
其中/sys/fs/cgroup/cpu 是 Cgroups 的一个层级,他绑定了 cpu 这个子系统/controller
config.Cgroup.Path 就应该是 /iceber/cgroup1

confi.Cgroup.Paths 这个是直接提供每个子系统控制组的系统路径,他会屏蔽掉 Path 字段的作用

Cgroups V1 Manager

// libcontainer/cgroups/fs/apply_raw.gotype Manager struct {mu       sync.MutexCgroups  *configs.CgroupRootless bool // ignore permission-related errorsPaths    map[string]string // 记录各个子系统下控制组的路径
}

我们使用 Manager 直接初始化相应字段就可以了

manger := &Manager{Cgroups: configCgroup,
}

manager.Cgroups 实际会使用到的字段是 configs.Cgroup.Path,configs.Cgroups.Paths,通过这两个字段来找到子系统控制组路径

manager.Cgroups.Resources 字段可能也会使用到
比如 Freeze 方法就会利用到 manager.Cgroups.Resources.Freezer 字段

apply 方法 将 pid 加入到控制组

apply 的作用是根据 manager.Cgroups.Path/Paths 将 pid 参数加入子系统控制组

getCgroupData 函数会返回 cgroupData,而 subsystem 接口的 Apply 方法便是通过该结构来执行具体的逻辑
cgroupDatapath 方法可以获取子系统的系统路径

func (m *Manager) Apply(pid int) (err error) {if m.Cgroups == nil {return nil}m.mu.Lock()defer m.mu.Unlock()var c = m.Cgroupsd, err := getCgroupData(m.Cgroups, pid)if err != nil {return err}// m.Paths 记录了各个 subsystem 下控制组的路径m.Paths = make(map[string]string)// 判断 cgroups 配置中是否指定子系统下控制组的路径if c.Paths != nil {for name, path := range c.Paths {// 检查子系统是否挂载正常_, err := d.path(name)if err != nil {if cgroups.IsNotFound(err) {continue}return err}m.Paths[name] = path}// 将 pid 加入到指定控制组中return cgroups.EnterPid(m.Paths, pid)}// 根据cgroups.Path 来获取相应子系统下控制组路径for _, sys := range m.getSubsystems() {// 获取子系统下控制组路径p, err := d.path(sys.Name())if err != nil {// 由于安全原因,devices 必须存在if cgroups.IsNotFound(err) && sys.Name() != "devices" {continue}return err}m.Paths[sys.Name()] = p// 执行具体子系统的 Apply 逻辑if err := sys.Apply(d); err != nil {// handle error}}return nil
}

我们现在来看一下 getCgroupData 具体做了什么
注意:cgroupData 其实只针对 manager.Cgroups.Path 有效,因为manager.Cgroups.Paths 已经指定了完整系统路径

/*
eg: cgroupData 中的数据是这样的
{root: "sys/fs/cgroup"innerPath: "/parentCgroup/cgroup1"pid: 10000config: *config.Cgroup
}
*/
type cgroupData struct {root      string	// Cgroups 的挂载目录innerPath string	// 子系统下控制组的相对目录config    *configs.Cgroup	// cgroups 的配置,包含了各个子系统的资源配置,会在子系统设置相关资源时使用pid       int		// 加入到控制组的 PID
}func getCgroupData(c *configs.Cgroup, pid int) (*cgroupData, error) {// getCgroupRoot 会通过从 /proc/self/mountinfo 查询 Cgroups 挂载点的目录root, err := getCgroupRoot()if err != nil {return nil, err}// 要求配置中提供配置组的路径,Path 是相对于 rootif (c.Name != "" || c.Parent != "") && c.Path != "" {return nil, fmt.Errorf("cgroup: either Path or Name and Parent should be used")}// 对路径进行安全处理cgPath := libcontainerUtils.CleanPath(c.Path)cgParent := libcontainerUtils.CleanPath(c.Parent)cgName := libcontainerUtils.CleanPath(c.Name)// 默认使用 Path, 而 Parent 和 Name 其实已经废除innerPath := cgPathif innerPath == "" {innerPath = filepath.Join(cgParent, cgName)}return &cgroupData{root:      root,innerPath: innerPath,config:    c,pid:       pid,}, nil
}

Apply 方法最终会调用各个子系统Apply 方法,那我们现在来看一下子系统的接口定义,以及以 cpu 子系统 为例的 Apply 逻辑

subsystem(子系统)/controller(控制器)

Cgroups 中使用 子系统/控制器 来管理和限制具体的资源

runc 提供了 subsystem 接口,定义了控制器需要提供给 Manager 使用的方法

type subsystem interface {     Name() string	// 返回子系统的名字  // 创建 cgroupData 指定的控制组,并将 cgroupData.pid 加入到该控制组Apply(*cgroupData) error	// 设置控制组路径的资源配置Set(path string, cgroup *configs.Cgroup) error	GetStats(path string, stats *cgroups.Stats) error	 // 获取指定控制组路径下的资源统计信息Remove(*cgroupData) error	// 移除 cgroupData 中指定的控制组  
}// 声明 Manger 会使用的所有子系统var subsystemsLegacy = subsystemSet{&CpusetGroup{},&DevicesGroup{},&MemoryGroup{},&CpuGroup{},&CpuacctGroup{},&PidsGroup{},&BlkioGroup{},&HugetlbGroup{},&NetClsGroup{},&NetPrioGroup{},&PerfEventGroup{},&FreezerGroup{},&NameGroup{GroupName: "name=systemd", Join: true},
}

我们现在来看一看 cpu 子系统的 Apply 方法做了哪些事情

type CpuGroup struct {}
func (s *CpuGroup) Apply(d *cgroupData) error {// 获取 cpu 子系统的控制组地址path, err := d.path("cpu")if err != nil && !cgroups.IsNotFound(err) {return err}return s.ApplyDir(path, d.config, d.pid)
}func (s *CpuGroup) ApplyDir(path string, cgroup *configs.Cgroup, pid int) error {if path == "" {return nil}// 确保路径存在if err := os.MkdirAll(path, 0755); err != nil {return err}// 在添加进程加入控制组之前,设置实时资源配置// 因为如果进程已经进入 SCHED_RR 模式并且没有设置 RT 带宽,再次添加会失败if err := s.SetRtSched(path, cgroup); err != nil {return err}// 将 pid 加入到控制组中return cgroups.WriteCgroupProc(path, pid)
}

可以看到,实际 Apply 做了两件事,第一件是创建控制组的目录,第二件事就是把 PID 加入到控制组

我们顺势看一下资源配置是如何设置的,其实就是简单地对控制组下相应配置文件进行修改,并没有什么黑魔法

func (s *CpuGroup) SetRtSched(path string, cgroup *configs.Cgroup) error {if cgroup.Resources.CpuRtPeriod != 0 {if err := fscommon.WriteFile(path, "cpu.rt_period_us", strconv.FormatUint(cgroup.Resources.CpuRtPeriod, 10)); err != nil {return err}}if cgroup.Resources.CpuRtRuntime != 0 {if err := fscommon.WriteFile(path, "cpu.rt_runtime_us", strconv.FormatInt(cgroup.Resources.CpuRtRuntime, 10)); err != nil {return err}}return nil
}func (s *CpuGroup) Set(path string, cgroup *configs.Cgroup) error {if cgroup.Resources.CpuShares != 0 {if err := fscommon.WriteFile(path, "cpu.shares", strconv.FormatUint(cgroup.Resources.CpuShares, 10)); err != nil {return err}}if cgroup.Resources.CpuPeriod != 0 {if err := fscommon.WriteFile(path, "cpu.cfs_period_us", strconv.FormatUint(cgroup.Resources.CpuPeriod, 10)); err != nil {return err}}if cgroup.Resources.CpuQuota != 0 {if err := fscommon.WriteFile(path, "cpu.cfs_quota_us", strconv.FormatInt(cgroup.Resources.CpuQuota, 10)); err != nil {return err}}return s.SetRtSched(path, cgroup)
}

Manager 再分析

Freeze 暂停/恢复 控制组 中任务

实际是操作 freezer 子系统,修改控制组中 freezer.state 文件

func (m *Manager) Freeze(state configs.FreezerState) error {if m.Cgroups == nil {return errors.New("cannot toggle freezer: cgroups not configured for container")}// 获取 Freeze subsystem 的控制组地址paths := m.GetPaths()dir := paths["freezer"]prevState := m.Cgroups.Resources.Freezerm.Cgroups.Resources.Freezer = state// 获取 subsystem,修改 freezer.state 文件freezer, err := m.getSubsystems().Get("freezer")if err != nil {return err}err = freezer.Set(dir, m.Cgroups)if err != nil {m.Cgroups.Resources.Freezer = prevStatereturn err}return nil
}

总结

实际上 runc 相当于使用了一个方便操作系统 Cgroups 的包,这个包并没有什么黑魔法,只是对子系统下的配置文件进行查询和修改


http://www.ppmy.cn/news/626497.html

相关文章

一个简易版的T4代码生成框架

对于企业开发来说,代码生成在某种意义上可以极大地提高开发效率和质量。在众多代码生成方案来说,T4是一个不错的选择,今天花了点时间写了一个简易版本的T4代码生成的“框架”,该框架仅仅是定义了一些基本的基类以及其他与VS集成相…

单片机esp32s2实现win10之USB副屏

视频演示: 骚气双副屏,单片机实现win10 USB副屏演示esp32 s2_哔哩哔哩_bilibili-https://www.bilibili.com/video/BV1tU4y1F7B6?spm_id_from333.999.0.0 开源计划 2021年最后一天,庆祝新年。 github开源地址如下,欢迎复刻魔改…

Linux服务器Jenkins部署打包Flutter

程序猿日常 记Jenkins部署打包Flutter参考Linux服务器Jenkins部署打包Flutter 安装Flutter环境 Flutter SDK 下载地址 配置服务器Flutter环境变量 创建任务 #!/bin/bash -ilex source /etc/profileflutter clean flutter pub get flutter build apk

王爽《汇编语言》期末考试题库(附答案)

单选题 第一章 PC机的最小信息单位是( )。 A. bit B. 字节 C. 字长 D. 字 A PC机的最小信息单位是比特(bit),常用来表示一位二进制数字(0或1)。字节(byte)是计算机中常用的数据单位,一个字…

Java 微服务框架选型

微服务(Microservices)是一种架构风格,一个大型复杂软件应用由一个或多个微服务组成。系统中的各个微服务可被独立部署,各个微服务之间是松耦合的。每个微服务仅关注于完成一件任务并很好地完成该任务。在所有情况下,每…

互联网快讯:喜茶回应上市;掌门教育、猿辅导转型素质教育

零售电商 1、淘宝逛逛推出双11种草期:10月1日起为达人设立亿级资源奖励 2、达达集团、京东与微软三方达成战略合作,共创数码3C产品1小时达零售新模式 3、喜茶、乐乐茶回应赴港上市:暂无上市计划 4、全家计划至2024财年开设1000家无人结…

2C领域最后一个资本宠儿,快看成“超新Z世代”娱乐平台

文 | 曾响铃 来源 | 科技向令说(xiangling0815) 未来,终究是属于年轻人的。 尤其是在数字娱乐行业,赢下年轻用户,相当于手握通往未来的船票,这也是当下B站被看好的原因。据了解,80%的B站用户…

凯兑换系统服务器角色,王者荣耀新英雄凯怎么兑换

王者荣耀新英雄司马懿克制技巧及手机模拟大师运行攻略 8月23日正式上线的王者荣耀新英雄司马懿,定位是法师刺客。他可以获取敌方英雄视野,远距离跳跃,还有几秒钟的大镰刀普攻形态。 王者荣耀新英雄奕星怎么玩?国服第一全能王蓝烟最…