Docker source code(2) – docker rm

Table of Contents

1. Overview

在上一篇中写了 container 的创建与启动过程,这一篇写的是 docker rm

2. Code

2.1. docker rm

docker rm 的参数十分简单

Usage: docker rm [OPTIONS] CONTAINER [CONTAINER…]

Remove one or more containers

Options: -f, –force Force the removal of a running container (uses SIGKILL) -l, –link Remove the specified link -v, –volumes Remove anonymous volumes associated with the container

还是从 cli 开始

2.1.1. docker cli

cli/command/container/rm.go L:25 看到 rm 的参数

flags.BoolVarP(&opts.rmVolumes, "volumes", "v", false, "Remove anonymous volumes associated with the container")
flags.BoolVarP(&opts.rmLink, "link", "l", false, "Remove the specified link")
flags.BoolVarP(&opts.force, "force", "f", false, "Force the removal of a running container (uses SIGKILL)")

在 cli/command/container/utils.go L:132 中有一个 parallelOperation 函数用来做并行的调用,本质上就是包装了一个 channel 然后启动相应数量的

goroutine, 出现问题就将错误丢到 channel 里,在调用处从 channel 里取错误。

因为在使用 docker rm 时是有 docker rm container1 container2 这样的场景,想让这里的请求变成并行的

在 cli/command/container/rm.go L:60 看到 cli 调用了 client 的 containerRemove

dockerCli.Client().ContainerRemove(ctx, container, options)

2.1.2. dockerd

来到 client/container_remove.go L:11 可以看到将参数拼成了请求体然后发送给 dockerd, 关于 dockerd 的路由处理在上一篇中讲过,之后不再赘述

在 daemon/delete.go L:23 看到 ContainerRm 的实现

  1. 根据 name 调用 dockerd 的 GetContainer 结构确定容器是否存在
  2. 通过 SetRemovalInPorgress 改变标志位确定容器是否在删除中
  3. 再次通过 container id 检查确认容器存在, 这是避免在上一步 get 后容器被删除
  4. 检查 rmLink 参数是否设置,如果设置执行 rmLink, 然后直接返回
  5. 没有设置 rmLink 则调用真正的清理函数 cleanupContainer
  6. 将 RemovalProgress 标志位设置为 false.这个调用是使用 defer 的,因此无论在哪一步失败都会保证标志位被正确设置

link 这个东西是早期的 docker 在没有 network 概念时用来做多容器之间通信的,已经快被移除了所以这部分代码有兴趣自己去看

cleanupContainer 的定义在 daemon/delete.go L:80

cleanupContainer(container *container.Container, forceRemove, removeVolume bool) (err error)

1.首先检查容器是否是运行状态, 如果在运行且 forceRemove 为 true 就先执行 Kill, 否则就抛出错误 2.通过 containerStop 发送一个 kill 信号然后等待 3s 3.调用 container 这个结构体的锁, 更改 container.Dead 状态, 确保无法再次启动 4.通过 CheckPointTo 将容器状态保存到磁盘 5.解锁 6.检查容器的读写层是否为空, 如果部位空调用 imageService.ReleaseLayer 进行清理 7.检查容器创建的路径是否全部清理干净 8.依次删除 linkName、selinux 的 label, 内存中的 idIndex、containers、containersReplica 9.移除所有的挂载点 10.调用 releaseName 删除 link 11.将容器的状态标记为已删除 12.记录一个 destroy 的事件

  1. daemon.Kill daemon/kill.go L:135
    1. 通过 daemon.killPossiblyDeadProcess 发送 SIGKILL 信号
    2. L:82 收到 SIGKILL 信号后执行 ExitOnNext,保证不会被重启
    3. 调用 daemon.kill 向 containerd 发送 container 的 pid 与信号 kill.go L:187
    4. 如果 containerd 返回了 notfound 的错误, 就启动一个 goroutine 等待一个 "container stop time", 然后进行清理
    5. 如果没有错误则返回

2.1.3. libcontainerd

SingalProcess 的定义在 libcontainerd/remote/client.go L:336

SignalProcess(ctx context.Context, containerID, processID string, signal int) error

SignalProcess 首先通过 container id 与 pid 获取到 containerd.Process 然后再发送一个 kill 信号

  1. 先通过 getContainer 从 containerd 拿到容器信息
  2. 再使用 LoadProcess 通过 pid 获取到 containerd.Process

containerd.Process 是一个 interface

type Process interface {
	// ID returns the id for the process
	ID() string
	// Pid returns the pid for the process
	Pid() int
	// ExitStatus returns the exit status
	ExitStatus() int
	// ExitedAt is the time the process exited
	ExitedAt() time.Time
	// Stdin returns the process STDIN
	Stdin() io.Closer
	// Stdio returns io information for the container
	Stdio() stdio.Stdio
	// Status returns the process status
	Status(context.Context) (string, error)
	// Wait blocks until the process has exited
	Wait()
	// Resize resizes the process console
	Resize(ws console.WinSize) error
	// Start execution of the process
	Start(context.Context) error
	// Delete deletes the process and its resourcess
	Delete(context.Context) error
	// Kill kills the process
	Kill(context.Context, uint32, bool) error
	// SetExited sets the exit status for the process
	SetExited(status int)
}

  1. 调用 containerd.Process 的 Kill

2.1.4. containerd

LoadProcess 的实现在 task.go L:546

LoadProcess(ctx context.Context, id string, ioAttach cio.Attach) (Process, error)
  1. 使用 containerid 向 grpc 接口发送请求
  2. 返回一个 process 的结构,这个结构实现了 containerd.Process 接口
type process struct {
     id   string 
     task *task
     pid  uint32
     io   cio.IO
}

containerd 中的 server 在 services/tasks/local.go L:325

  1. 从存储中获得容器信息
  2. 通过容器中的 runtime 获得一个 runtime.Task 这里的 runtime 还是 runc v2
  3. getProcessState 通过 State 获取容器的状态 runtime/v2/runc/v2/service.go L:483

调用 process Kill 的过程就是向 grpcserver 发送一个请求 runtime/v2/process.go L:39

后端的处理流程在 runtime/v2/runc/v2/service.go L:555 1.查看 container 是否存在 2.调用 container 的 Kill,这里的 kill 其实是分成两步: 1.杀掉 exec 进程 2.杀掉所有进程 3.runtime/v2/runc/container.go L:441 这里返回的是 process 实际上是 execProcess 这个结构体

pkg/process/exec.go L:144 中可以看到实际杀死容器的逻辑 1.拿到 pid 2.确定 pid 状态 3.通过 unix.Kill 发送 kill 信号

到这里 kill 结束

实际上杀死容器就是一个 SIGKILL 的事, 但是由于 dockerd 的削减 containerd 逐渐加厚,以及各种为了兼容之前写的代码的问题 导致整个流程变得非常琐碎, 每一个组件都要维护冗余的信息,每个状态的改变又要加锁。无论是看代码还是更改都会让人头大。 所以 docker 在未来不会作为一个最流行的容器引擎,会有更小巧与精致的竞争对手取而代之。但在 k8s 的环境下,是谁都无所谓了。 (Podman 加油

Created: 2023-11-24 Fri 09:49

Validate