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
方法便是通过该结构来执行具体的逻辑
cgroupData 的 path
方法可以获取子系统的系统路径
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 的包,这个包并没有什么黑魔法,只是对子系统
下的配置文件进行查询和修改