容器启动流程(containerd 和 runc)
启动流程
containerd 作为一个 api 服务,提供了一系列的接口供外部调用,比如创建容器、删除容器、创建镜像、删除镜像等等。使用 docker 和 ctr 等工具,都是通过调用 containerd 的 api 来实现的。
kubelet 通过 cri 调用 containerd 和这些不一样,后续我会介绍到。
containerd 创建容器流程如下:
- 接收到 api 请求,通过调用 containerd-shim-runc-v2 调用 runc 创建容器,主要是做解压文件和准备环境的工作。
- 接收到 api 请求,创建一个 task,task 是一个容器的抽象,包含了容器的所有信息,比如容器的 id、容器的状态、容器的配置等等。
- containerd 启动一个 containerd-shim-runc-v2 进程。
- containerd-shim-runc-v2 进程 在启动一个 containerd-shim-runc-v2 进程,然后第一个 containerd-shim-runc-v2 进程退出。
- containerd 通过 IPC 通信,让第二个 containerd-shim-runc-v2 启动容器。
- containerd-shim-runc-v2 进程通过调用 runc start 启动容器。
- runc 会调用 runc init 启动容器的 init 进程。
- runc init 进程会调用
unix.Exec
的方式,替换自己的进程,启动容器的第一个进程。这个进程既是容器的启动命令,也是容器的 pid 1 进程。完成之后,runc create 进程退出。
这样 containerd-shim-runc-v2 的父进程就是 init 进程(1),而 init 进程的父进程是 containerd-shim-runc-v2 进程,这样就形成了一个进程树。
我通过 docker 启动一个容器,示例一下:
❯ docker run -d --rm -it docker.m.daocloud.io/ubuntu:22.10 sleep 3000
❯ ps -ef|grep "sleep 3000"
root 15042 15021 0 22:02 pts/0 00:00:00 sleep 3000
❯ ps -ef|grep "15021"
root 15021 1 0 22:02 ? 00:00:00 /usr/bin/containerd-shim-runc-v2 -namespace moby -id 4346ca602cd85d35b0a4a81762be6142bc6a2222f859f4af47563992efc3c59c -address /run/containerd/containerd.sock
root 15042 15021 0 22:02 pts/0 00:00:00 sleep 3000
可以看到我们的结论是正确的。
疑问解答
1.为什么要创建两个 containerd-shim 不嫌麻烦吗?
因为 第一个 containerd-shim 会在创建完第二个 containerd-shim 后退出,而作为第一个进程子进程的第二个 containerd-shim 会成为孤儿进程,这样就会被 init 进程接管,而和 containerd 本身脱离了关系。
2.为什么要想法设法把 containerd-shim 挂在 init 进程下面,而不是 containerd?
为了保证稳定性和独立性。这样做可以确保即使 containerd 崩溃或重启,由 containerd-shim 管理的容器进程仍然可以继续运行,不受影响。此外,这种设计还有助于更好地管理资源和防止资源泄露。
3.为什么 runc start 进程退出了 runc init 进程(用户进程)没有变成 init 的子进程 而是containerd-shim的子进程?
因为 containerd-shim 做了 unix 的 PR_SET_CHILD_SUBREAPER
调用, 这个系统调用大概作用为 当这个进程的子子孙孙进程变成孤儿进程的时候,这个进程会接管这个孤儿进程,而不是 init 进程接管。
架构图
代码分析
containerd api 注册 代码分析
var register = struct {
sync.RWMutex
r plugin.Registry
}{}
type Registry []*Registration
type Registration struct {
// Type of the plugin
Type Type
// ID of the plugin
ID string
// Config specific to the plugin
Config interface{}
// Requires is a list of plugins that the registered plugin requires to be available
Requires []Type
// InitFn is called when initializing a plugin. The registration and
// context are passed in. The init function may modify the registration to
// add exports, capabilities and platform support declarations.
InitFn func(*InitContext) (interface{}, error)
// ConfigMigration allows a plugin to migrate configurations from an older
// version to handle plugin renames or moving of features from one plugin
// to another in a later version.
// The configuration map is keyed off the plugin name and the value
// is the configuration for that objects, with the structure defined
// for the plugin. No validation is done on the value before performing
// the migration.
ConfigMigration func(context.Context, int, map[string]interface{}) error
}
通过 init 把接口注册进去 比如 task api 注册
代码位置 : services/tasks/local.go
func init() {
registry.Register(&plugin.Registration{
Type: plugins.ServicePlugin,
ID: services.TasksService,
Requires: tasksServiceRequires,
Config: &Config{},
InitFn: initFunc,
})
timeout.Set(stateTimeout, 2*time.Second)
}
func initFunc(ic *plugin.InitContext) (interface{}, error) {
config := ic.Config.(*Config)
v2r, err := ic.GetByID(plugins.RuntimePluginV2, "task")
if err != nil {
return nil, err
}
m, err := ic.GetSingle(plugins.MetadataPlugin)
if err != nil {
return nil, err
}
ep, err := ic.GetSingle(plugins.EventPlugin)
if err != nil {
return nil, err
}
monitor, err := ic.GetSingle(plugins.TaskMonitorPlugin)
if err != nil {
if !errors.Is(err, plugin.ErrPluginNotFound) {
return nil, err
}
monitor = runtime.NewNoopMonitor()
}
db := m.(*metadata.DB)
l := &local{
containers: metadata.NewContainerStore(db),
store: db.ContentStore(),
publisher: ep.(events.Publisher),
monitor: monitor.(runtime.TaskMonitor),
v2Runtime: v2r.(runtime.PlatformRuntime),
}
v2Tasks, err := l.v2Runtime.Tasks(ic.Context, true)
if err != nil {
return nil, err
}
for _, t := range v2Tasks {
l.monitor.Monitor(t, nil)
}
if err := blockio.SetConfig(config.BlockIOConfigFile); err != nil {
log.G(ic.Context).WithError(err).Errorf("blockio initialization failed")
}
if err := rdt.SetConfig(config.RdtConfigFile); err != nil {
log.G(ic.Context).WithError(err).Errorf("RDT initialization failed")
}
return l, nil
}
然后在 containerd 启动的时候 注册api
loaded := registry.Graph(filter(config.DisabledPlugins))
for _, p := range loaded {
result := p.Init(initContext)
if err := initialized.Add(result); err != nil {
return nil, fmt.Errorf("could not add plugin result to plugin set: %w", err)
}
instance, err := result.Instance()
delete(required, id)
// check for grpc services that should be registered with the server
if src, ok := instance.(grpcService); ok {
grpcServices = append(grpcServices, src)
}
if src, ok := instance.(ttrpcService); ok {
ttrpcServices = append(ttrpcServices, src)
}
if service, ok := instance.(tcpService); ok {
tcpServices = append(tcpServices, service)
}
s.plugins = append(s.plugins, result)
}
// register services after all plugins have been initialized
for _, service := range grpcServices {
if err := service.Register(grpcServer); err != nil {
return nil, err
}
}
for _, service := range ttrpcServices {
if err := service.RegisterTTRPC(ttrpcServer); err != nil {
return nil, err
}
}
for _, service := range tcpServices {
if err := service.RegisterTCP(tcpServer); err != nil {
return nil, err
}
}
create task
func (l *local) Create(ctx context.Context, r *api.CreateTaskRequest, _ ...grpc.CallOption) (*api.CreateTaskResponse, error) {
rtime := l.v2Runtime
_, err = rtime.Get(ctx, r.ContainerID)
if err != nil && !errdefs.IsNotFound(err) {
return nil, errdefs.ToGRPC(err)
}
if err == nil {
return nil, errdefs.ToGRPC(fmt.Errorf("task %s: %w", r.ContainerID, errdefs.ErrAlreadyExists))
}
c, err := rtime.Create(ctx, r.ContainerID, opts)
}
func (m *TaskManager) Create(ctx context.Context, taskID string, opts runtime.CreateOpts) (runtime.Task, error) {
// 启动第一个 containerd-shim-runc-v2 进程
shimTask, err := newShimTask(shim)
if err != nil {
return nil, err
}
// 给第二个 containerd-shim-runc-v2 进程传递参数
t, err := shimTask.Create(ctx, opts)
return t, nil
}
cri 代码
cri 和 task 和上述的是一样的, 通过 register 注册 api.
func init() {
registry.Register(&plugin.Registration{
Type: plugins.GRPCPlugin,
ID: "cri",
Requires: []plugin.Type{
plugins.CRIImagePlugin,
plugins.InternalPlugin,
plugins.SandboxControllerPlugin,
plugins.NRIApiPlugin,
plugins.EventPlugin,
plugins.ServicePlugin,
plugins.LeasePlugin,
plugins.SandboxStorePlugin,
},
InitFn: initCRIService,
})
}
startContainer 接口
func (in *instrumentedService) StartContainer(ctx context.Context, r *runtime.StartContainerRequest) (_ *runtime.StartContainerResponse, err error) {
if err := in.checkInitialized(); err != nil {
return nil, err
}
log.G(ctx).Infof("StartContainer for %q", r.GetContainerId())
defer func() {
if err != nil {
log.G(ctx).WithError(err).Errorf("StartContainer for %q failed", r.GetContainerId())
} else {
log.G(ctx).Infof("StartContainer for %q returns successfully", r.GetContainerId())
}
}()
res, err := in.c.StartContainer(ctrdutil.WithNamespace(ctx), r)
return res, errdefs.ToGRPC(err)
}
func (c *criService) StartContainer(ctx context.Context, r *runtime.StartContainerRequest) (retRes *runtime.StartContainerResponse, retErr error) {
task, err := container.NewTask(ctx, ioCreation, taskOpts...)
}
func (c *container) NewTask(ctx context.Context, ioCreate cio.Creator, opts ...NewTaskOpts) (_ Task, err error) {
// 通过 unix socket 的方式调用上述的 create task 接口
response, err := c.client.TaskService().Create(ctx, request)
}
containerd-shim
func run(ctx context.Context, manager Manager, config Config) error {
// Handle explicit actions
switch action {
case "delete":
case "start":
// 如果是 start 参数的话启动一个 containerd-shim-runc-v2 进程
opts := StartOpts{
Address: addressFlag,
TTRPCAddress: ttrpcAddress,
Debug: debugFlag,
}
params, err := manager.Start(ctx, id, opts)
if err != nil {
return err
}
data, err := json.Marshal(¶ms)
if err != nil {
return fmt.Errorf("failed to marshal bootstrap params to json: %w", err)
}
if _, err := os.Stdout.Write(data); err != nil {
return err
}
return nil
}
}
// manager.Start 中创建的 command 指定三个参数 Namespace 容器 id 和 containerd socket 文件的地址
func newCommand(ctx context.Context, id, containerdAddress, containerdTTRPCAddress string, debug bool) (*exec.Cmd, error) {
ns, err := namespaces.NamespaceRequired(ctx)
if err != nil {
return nil, err
}
self, err := os.Executable()
if err != nil {
return nil, err
}
cwd, err := os.Getwd()
if err != nil {
return nil, err
}
args := []string{
"-namespace", ns,
"-id", id,
"-address", containerdAddress,
}
if debug {
args = append(args, "-debug")
}
cmd := exec.Command(self, args...)
cmd.Dir = cwd
cmd.Env = append(os.Environ(), "GOMAXPROCS=4")
cmd.SysProcAttr = &syscall.SysProcAttr{
Setpgid: true,
}
return cmd, nil
}
第二个 containerd-shim 也会开启一些api服务 ,比如启动容器
func (s *service) Start(ctx context.Context, r *taskAPI.StartRequest) (*taskAPI.StartResponse, error) {
p, err := container.Start(ctx, r)
}
func (c *Container) Start(ctx context.Context, r *task.StartRequest) (process.Process, error) {
p, err := c.Start(r.ExecID)
}
// command 及调用 runc 启动 runc create 的进程
func (r *Runc) Start(context context.Context, id string) error {
return r.runOrError(r.command(context, "start", id))
}
runc
func (r *runner) run(config *specs.Process) (int, error) {
switch r.action {
case CT_ACT_CREATE:
err = r.container.Start(process)
case CT_ACT_RESTORE:
err = r.container.Restore(process, r.criuOpts)
case CT_ACT_RUN:
err = r.container.Run(process)
default:
panic("Unknown action")
}
}
func (c *Container) Start(process *Process) error {
c.m.Lock()
defer c.m.Unlock()
if c.config.Cgroups.Resources.SkipDevices {
return errors.New("can't start container with SkipDevices set")
}
if process.Init {
if err := c.createExecFifo(); err != nil {
return err
}
}
if err := c.start(process); err != nil {
if process.Init {
c.deleteExecFifo()
}
return err
}
return nil
}
// 调用 runc init 进程 /proc/self/exe 是自己的二进制文件
func (c *Container) newParentProcess(p *Process) (parentProcess, error) {
comm, err := newProcessComm()
if err != nil {
return nil, err
}
// Make sure we use a new safe copy of /proc/self/exe or the runc-dmz
// binary each time this is called, to make sure that if a container
// manages to overwrite the file it cannot affect other containers on the
// system. For runc, this code will only ever be called once, but
// libcontainer users might call this more than once.
p.closeClonedExes()
var (
exePath string
// only one of dmzExe or safeExe are used at a time
dmzExe, safeExe *os.File
)
if dmz.IsSelfExeCloned() {
// /proc/self/exe is already a cloned binary -- no need to do anything
logrus.Debug("skipping binary cloning -- /proc/self/exe is already cloned!")
exePath = "/proc/self/exe"
}
cmd := exec.Command(exePath, "init")
cmd.Args[0] = os.Args[0]
cmd.Stdin = p.Stdin
cmd.Stdout = p.Stdout
cmd.Stderr = p.Stderr
cmd.Dir = c.config.Rootfs
if cmd.SysProcAttr == nil {
cmd.SysProcAttr = &unix.SysProcAttr{}
}
}
runc init
func init() {
if len(os.Args) > 1 && os.Args[1] == "init" {
// This is the golang entry point for runc init, executed
// before main() but after libcontainer/nsenter's nsexec().
libcontainer.Init()
}
}
// libcontainer.Init() 中调用的
func startInitialization() (retErr error) {
return containerInit(it, &config, syncPipe, consoleSocket, pidfdSocket, fifofd, logFD, dmzExe, mountFds{sourceFds: mountSrcFds, idmapFds: idmapFds})
}
// linuxSetnsInit 是 exec 的时候调用的 在启动的容器执行命令
// initStandard 是启动容器
func containerInit(t initType, config *initConfig, pipe *syncSocket, consoleSocket, pidfdSocket *os.File, fifoFd, logFd int, dmzExe *os.File, mountFds mountFds) error {
if err := populateProcessEnvironment(config.Env); err != nil {
return err
}
switch t {
case initSetns:
// mount and idmap fds must be nil in this case. We don't mount while doing runc exec.
if mountFds.sourceFds != nil || mountFds.idmapFds != nil {
return errors.New("mount and idmap fds must be nil; can't mount from exec")
}
i := &linuxSetnsInit{
pipe: pipe,
consoleSocket: consoleSocket,
pidfdSocket: pidfdSocket,
config: config,
logFd: logFd,
dmzExe: dmzExe,
}
return i.Init()
case initStandard:
i := &linuxStandardInit{
pipe: pipe,
consoleSocket: consoleSocket,
pidfdSocket: pidfdSocket,
parentPid: unix.Getppid(),
config: config,
fifoFd: fifoFd,
logFd: logFd,
dmzExe: dmzExe,
mountFds: mountFds,
}
return i.Init()
}
return fmt.Errorf("unknown init type %q", t)
}
func (l *linuxStandardInit) Init() error {
return system.Exec(name, l.config.Args, os.Environ())
}
// 替换进程
func Exec(cmd string, args []string, env []string) error {
for {
err := unix.Exec(cmd, args, env)
if err != unix.EINTR {
return &os.PathError{Op: "exec", Path: cmd, Err: err}
}
}
}
容器启动流程(containerd 和 runc)的更多相关文章
- Spring基础系列-容器启动流程(1)
原创作品,可以转载,但是请标注出处地址:https://www.cnblogs.com/V1haoge/p/9870339.html 概述 我说的容器启动流程涉及两种情况,SSM开发模式和Spri ...
- Spring基础系列-容器启动流程(2)
原创作品,可以转载,但是请标注出处地址:https://www.cnblogs.com/V1haoge/p/9503210.html 一.概述 这里是Springboot项目启动大概流程,区别于SSM ...
- SpringBean容器启动流程+Bean的生命周期【附源码】
如果对SpringIoc与Aop的源码感兴趣,可以访问参考:https://javadoop.com/,十分详细. 目录 Spring容器的启动全流程 Spring容器关闭流程 Bean 的生命周期 ...
- dubbo源码学习(一)dubbo容器启动流程简略分析
最近在学习dubbo,dubbo的使用感觉非常的简单,方便,基于Spring的容器加载配置文件就能直接搭建起dubbo,之前学习中没有养成记笔记的习惯,时间一久就容易忘记,后期的复习又需要话费较长的时 ...
- Spring IOC源码(一):IOC容器启动流程核心方法概览
Spring有两种方式加载配置,分别为xml文件.注解的方式,对于xml配置的方式相信大家都不陌生,往往通过new ClassPathXmlApplicationContext("*.xml ...
- dubbo源码学习(二)dubbo容器启动流程简略分析
dubbo版本2.6.3 继续之前的dubbo源码阅读,从com.alibaba.dubbo.container.Main.main(String[] args)作为入口 简单的数据一下启动的流程 1 ...
- spring boot容器启动详解
目录 一.前言 二.容器启动 三.总结 =======正文分割线====== 一.前言 spring cloud大行其道的当下,如果不了解基本原理那么是很纠结的(看见的都是约定大于配置,但是原理呢?为 ...
- Spring IOC容器启动流程源码解析(四)——初始化单实例bean阶段
目录 1. 引言 2. 初始化bean的入口 3 尝试从当前容器及其父容器的缓存中获取bean 3.1 获取真正的beanName 3.2 尝试从当前容器的缓存中获取bean 3.3 从父容器中查找b ...
- Spring IOC容器启动流程源码解析(一)——容器概念详解及源码初探
目录 1. 前言 1.1 IOC容器到底是什么 1.2 BeanFactory和ApplicationContext的联系以及区别 1.3 解读IOC容器启动流程的意义 1.4 如何有效的阅读源码 2 ...
- Spring源码解析02:Spring IOC容器之XmlBeanFactory启动流程分析和源码解析
一. 前言 Spring容器主要分为两类BeanFactory和ApplicationContext,后者是基于前者的功能扩展,也就是一个基础容器和一个高级容器的区别.本篇就以BeanFactory基 ...
随机推荐
- 摆脱鼠标系列 - 用git命令提交代码
需求 最近开始改变用鼠标的习惯,之前一直是用鼠标点击vscode,点击提交 现在不用鼠标,改用命令行,命令很简单,主要是习惯的改变 实现 vscode环境 ctrl + ` 快捷键打开命令行 git ...
- vscode 提取扩展时出错 XHR failed
vscode 提取扩展时出错 XHR failed 起因 vscode 安装 Bracket Pair Color DLW 插件,商店打不开了 解决方案 打开 hosts 添加 13.107.42.1 ...
- C#串口开发之SerialPort类封装
目录 SerialPort类 参数封装 控件操作封装 SerialPortClient类实现 SerialPortClient类使用 测试Demo 参考文章 SerialPort类 微软在.NET中对 ...
- 超高并发下,Redis热点数据风险破解
★ Redis24篇集合 1 介绍 作者是互联网一线研发负责人,所在业务也是业内核心流量来源,经常参与 业务预定.积分竞拍.商品秒杀等工作. 近期参与多场新员工的面试工作,经常就 『超高并发场景下热点 ...
- 记录--求你了,别再说不会JSONP了
这里给大家分享我在网上总结出来的一些知识,希望对大家有所帮助 JSONP是一种很远古用来解决跨域问题的技术,当然现在实际工作当中很少用到该技术了,但是很多同学在找工作面试过程中还是经常被问到,本文将带 ...
- TP6框架--EasyAdmin学习笔记:实现数据库增删查改
这是我写的学习EasyAdmin的第三章,这一章我给大家分享下如何进行数据库的增删查改 上一章链接:点击这里前往 上一章我们说到,我仿照官方案例,定义了一条路由goodsone和创建了对应数据库,我们 ...
- C++正则表达式 <regex>
一 简介 概括而言,使用正则表达式处理字符串的流程包括: 用正则表达式定义要匹配的字符串的规则, 然后对目标字符串进行匹配, 最后对匹配到的结果进行操作. C++ 的 regex 库提供了用于表示正则 ...
- Ficow 陪你看 WWDC 2022
本文首发于 Ficow Shen's Blog,原文地址: WWDC22 - Xcode 14 新特性. 内容概览 前言 用好过滤器 Recap,节约你的宝贵时间 Essential,取其精华 必看内 ...
- 开发必会系列:《深入理解JVM(第二版)》读书笔记
一 开始前 HotSpot:http://xiaomogui.iteye.com/blog/857821 http://blog.csdn.net/u011521890/article/detail ...
- 立创EDA的使用
立创EDA的使用 1.实验原理 最近在使用立创EDA来做电路作业,这里记录一下立创EDA的基本操作,以后小型的电路设计可以在其主页完成.立创EDA是一个可以线上完成电路设计仿真以及布线的免费设计工具, ...