本篇的主要内容是关于如何删除镜像的.听起来是挺简单的一件事,但是docker本身的删除 策略定义并不清除,而且在实际的使用过程中,似乎总是与我们预计的结果不符,比如磁盘空间并没有被释放.所以在此单列一篇,详细解析镜 像删除的过程.

简介

上篇已经介绍过 很多关于镜像相关的源码,本篇就不在重复,着重分析与删除相关的部分

子命令执行

首先仍然是subcommand的执行部分,镜像删除的子命令式rmi,与之对应的函数便是 CmdRmi.

docker rmi接受两个参数:

github.com/docker/docker/api/client/commands.go#CmdRmi:

var (
    cmd     = cli.Subcmd("rmi", "IMAGE [IMAGE...]", "Remove one or more images")
    force   = cmd.Bool([]string{"f", "-force"}, false, "Force removal of the image")
    noprune = cmd.Bool([]string{"-no-prune"}, false, "Do not delete untagged parents")
)

force用来决定是否在有容器使用了此镜像时(非运行状态)强制删除,noprune指定不删 除没有tagparent layers.

for _, name := range cmd.Args() {
    body, _, err := readBody(cli.call("DELETE", "/images/"+name+"?"+v.Encode(), nil, false))
    if err != nil {
        fmt.Fprintf(cli.err, "%s\n", err)
        encounteredError = fmt.Errorf("Error: failed to remove one or more images")
    } 

docker rmi可以接受多个参数,可以从上面代码看到,实际执行时是对每一个image都执 行一个DELETE请求并处理返回结果.

服务端的handler为:

github.com/docker/docker/api/server/server.go#createRouter:

"DELETE": {
    "/containers/{name:.*}": deleteContainers,
    "/images/{name:.*}":     deleteImages,
},

请求处理

deleteImages的主要工作仍是job环境的设置以及参数的传递:

func deleteImages(eng *engine.Engine, version version.Version, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
    if err := parseForm(r); err != nil {
        return err
    }
    if vars == nil {
        return fmt.Errorf("Missing parameter")
    }
    var job = eng.Job("image_delete", vars["name"])
    streamJSON(job, w, false)
    job.Setenv("force", r.Form.Get("force"))
    job.Setenv("noprune", r.Form.Get("noprune"))

    return job.Run()
}

image_delete对应的handler暂时定义在daemon包中,后续可能也会移到graph包中:

镜像删除

func (daemon *Daemon) ImageDelete(job *engine.Job) engine.Status {
    if n := len(job.Args); n != 1 {
        return job.Errorf("Usage: %s IMAGE", job.Name)
    }
    imgs := engine.NewTable("", 0)
    if err := daemon.DeleteImage(job.Eng, job.Args[0], imgs, true, job.GetenvBool("force"), job.GetenvBool("noprune")); err != nil {
        return job.Error(err)
    }
    if len(imgs.Data) == 0 {
        return job.Errorf("Conflict, %s wasn't deleted", job.Args[0])
    }
    if _, err := imgs.WriteListTo(job.Stdout); err != nil {
        return job.Error(err)
    }
    return engine.StatusOK
}

可以看到,主要的删除是通过daemon.DeleteImage函数进行,而Table结构体则是用来 记录删除的结果:

type Table struct {
    Data    []*Env
    sortKey string
    Chan    chan *Env
}

func NewTable(sortKey string, sizeHint int) *Table {
    return &Table{
        make([]*Env, 0, sizeHint),
        sortKey,
        make(chan *Env),
    }
}

镜像查找

要删除镜像,就要先获取到关于这个镜像的一些详细信息:名称,tag,父子关系等。而输 入既可能是镜像ID,也可能是名字:

github.com/docker/docker/daemon/image_delete.go#DeleteImage:

// 解析名字和tag.repoName有可能是镜像ID
repoName, tag = parsers.ParseRepositoryTag(name)
if tag == "" {
    tag = graph.DEFAULTTAG
}

// 查找镜像,先从TagStore里面找,找不到就当成镜像ID从Graph中找.`Get(repoName)`似乎
// 显得有点多余,因为LookupImage里面已经包含了所有步骤。
img, err := daemon.Repositories().LookupImage(name)
if err != nil {
    if r, _ := daemon.Repositories().Get(repoName); r != nil {
        return fmt.Errorf("No such image: %s:%s", repoName, tag)
    }
    return fmt.Errorf("No such image: %s", name)
}

// 如果输入的是镜像ID,repoName和tag置为空.
if strings.Contains(img.ID, name) {
    repoName = ""
    tag = ""
}

父子关系查找

docker的镜像存储是一个树形结构,每个镜像只有一个父节点(镜像),但可以有多个子节 点(镜像)。Graph并没有提供多少相应的数据结构来进行节点查找,所以只能是遍历查 询:

byParents, err := daemon.Graph().ByParent()
if err != nil {
    return err
}

ByParent返回了所有镜像与其子镜像(可为多个)之间的映射关系.

github.com/docker/docker/graph/graph.go:

func (graph *Graph) ByParent() (map[string][]*image.Image, error) {
    byParent := make(map[string][]*image.Image)
    err := graph.walkAll(func(img *image.Image) {
        parent, err := graph.Get(img.Parent)
        if err != nil {
            return
        }
        if children, exists := byParent[parent.ID]; exists {
            byParent[parent.ID] = append(children, img)
        } else {
            byParent[parent.ID] = []*image.Image{img}
        }
    })
    return byParent, err
}

镜像名称与ID关系

镜像ID与名称并不是一一映射的关系。比如说我们用docker tag命令给一个镜像赋予了一 个新的名字,旧的名字是不会被取代的.所以经常会发现不同的镜像名称对应的ID都是同一 个:

当我们删除镜像时,不管是指定名字还是ID,这样的映射关系也需要提前找出来:

repos := daemon.Repositories().ByID()[img.ID]

github.com/docker/docker/graph/tags.go:

func (store *TagStore) LookupImage(name string) (*image.Image, error) {
    // FIXME: standardize on returning nil when the image doesn't exist, and err for everything else
    // (so we can pass all errors here)
    repos, tag := parsers.ParseRepositoryTag(name)
    if tag == "" {
        tag = DEFAULTTAG
    }
    img, err := store.GetImage(repos, tag)
    store.Lock()
    defer store.Unlock()
    if err != nil {
        return nil, err
    } else if img == nil {
        if img, err = store.graph.Get(name); err != nil {
            return nil, err
        }
    }
    return img, nil
}

最终repos的值就是一个 名称:tag 列表.

如果repos是多个,那么删除的情况就比较复杂.综述如下:

首先有两个基本原则:

  • 要删除的镜像必须没有子节点(没有被别的镜像使用) ,不然最多只会去掉tag信息,不 会删除实际数据
  • 删除是沿着镜像的树形结构向上删除,会删除所有没有被使用的所有父节点,直到找到一 个正在被别的镜像使用的镜像为止.

有点类似于Linux上的各种包管理软件对依赖关系的处理.具体的情况如下:

  1. docker rmi <image id>
    • 对应单个name:tag 最简单情况,直接删除,
    • 对应同一个name,多个tag 直接删除
    • 对应多个name,不加force参数会报错,加上之后会将第一个name对应的tag 信息都删掉,但镜像不会删除
  2. docker rmi name:[tag]
    • image id一一对应 直接删除
    • 与其他的tag(name相同)对应同一个image id 只会去掉这个name:[tag]信息, 不删除数据
    • 与不同的name:[tag]共享一个image id 只会去掉这个name:[tag]信息,不删除 实际数据

情况复杂,但是分析起来,其实都是很容易理解的。

github.com/docker/docker/daemon/image_delete.go#DeleteImage:

if repoName == "" {
    for _, repoAndTag := range repos {
        parsedRepo, parsedTag := parsers.ParseRepositoryTag(repoAndTag)
        if repoName == "" || repoName == parsedRepo {
            repoName = parsedRepo
            if parsedTag != "" {
                tags = append(tags, parsedTag)
            }
        } else if repoName != parsedRepo && !force {
            // the id belongs to multiple repos, like base:latest and user:test,
            // in that case return conflict
            return fmt.Errorf("Conflict, cannot delete image %s because it is tagged in multiple repositories, use -f to force", name)
        }
    }
} else {
    tags = append(tags, tag)
}

从上面代码可以看到,当输入为image id时,如果name:[tag]有多个,只会将第一个的信息清除掉, 其他的都跳过了,未做处理。

删除

真正在删除镜像之前,还需要考虑容器。如果要删除的镜像还有容器在使用(不论是否正在 运行)它,则不能删除.从上面总结的种种情况来看,真正会删除实际数据的情况不多,所以 只用考虑这几种情况即可:

github.com/docker/docker/daemon/image_delete.go#DeleteImage:

if len(repos) <= 1 {
    if err := daemon.canDeleteImage(img.ID, force); err != nil {
        return err
    }
}
func (daemon *Daemon) canDeleteImage(imgID string, force bool) error {
    // 返回容器列表
    for _, container := range daemon.List() {
        parent, err := daemon.Repositories().LookupImage(container.ImageID)
        if err != nil {
            if daemon.Graph().IsNotExist(err, container.ImageID) {
                return nil
            }
            return err
        }
        
        if err := parent.WalkHistory(func(p *image.Image) error {
            if imgID == p.ID {
                if container.IsRunning() {
                    if force {
                        return fmt.Errorf("Conflict, cannot force delete %s because the running container %s is using it, stop it and retry", stringid.TruncateID(imgID), stringid.TruncateID(container.ID))
                    }
                    return fmt.Errorf("Conflict, cannot delete %s because the running container %s is using it, stop it and use -f to force", stringid.TruncateID(imgID), stringid.TruncateID(container.ID))
                } else if !force {
                    return fmt.Errorf("Conflict, cannot delete %s because the container %s is using it, use -f to force", stringid.TruncateID(imgID), stringid.TruncateID(container.ID))
                }
            }
            return nil
        }); err != nil {
            return err
        }
    }
    return nil
}

从上面也可以看到force参数的作用:当有容器在使用这个镜像但并未运行时,force可 以强制删除镜像。

需要注意的上,上面对repos长度的判断并未覆盖全部的情况:当一个镜像ID对应对个一 个name的多个tag时,此时用docker rmi <iamge id>就绕过了与容器相关的检查, 详见issue 12135.

真正的删除分两步。第一步:untag,删除相关tag信息:

for _, tag := range tags {
    tagDeleted, err := daemon.Repositories().Delete(repoName, tag)
    if err != nil {
        return err
    }
    if tagDeleted {
        out := &engine.Env{}
        out.Set("Untagged", repoName+":"+tag)
        imgs.Add(out)
        eng.Job("log", "untag", img.ID, "").Run()
    }
}

daemon.Repositories().Delete即是从TagStore中删除相关信息.

第二步: 删除实际镜像数据,这一步并不一定会执行。

tags = daemon.Repositories().ByID()[img.ID]
if (len(tags) <= 1 && repoName == "") || len(tags) == 0 {
    if len(byParents[img.ID]) == 0 {
        if err := daemon.Repositories().DeleteAll(img.ID); err != nil {
            return err
        }
        if err := daemon.Graph().Delete(img.ID); err != nil {
            return err
        }
        out := &engine.Env{}
        out.Set("Deleted", img.ID)
        imgs.Add(out)
        eng.Job("log", "delete", img.ID, "").Run()
        if img.Parent != "" && !noprune {
            err := daemon.DeleteImage(eng, img.Parent, imgs, false, force, noprune)
            if first {
                return err
            }

        }

    }
}

这时候的tags已经是清楚过一个repo之后的了,有可能为空,也有可能有其他的值(这 时候就不能删除镜像数据)。len(tags) <= 1 && repoName == ""是应对原本就没有 tag的镜像,比如我们在用docker build时生成的很多中间镜像。

真正的数据删除是在daemon.Graph().Delete():

github.com/docker/docker/graph/graph.go:

func (graph *Graph) Delete(name string) error {
    id, err := graph.idIndex.Get(name)
    if err != nil {
        return err
    }
    tmp, err := graph.Mktemp("")
    graph.idIndex.Delete(id)
    if err == nil {
        err = os.Rename(graph.ImageRoot(id), tmp)
        // On err make tmp point to old dir and cleanup unused tmp dir
        if err != nil {
            os.RemoveAll(tmp)
            tmp = graph.ImageRoot(id)
        }
    } else {
        // On err make tmp point to old dir for cleanup
        tmp = graph.ImageRoot(id)
    }
    // Remove rootfs data from the driver
    graph.driver.Remove(id)
    // Remove the trashed image directory
    return os.RemoveAll(tmp)
}

/var/lib/docker/graph/var/lib/docker/aufs/下的相关目录都会被清除.

总结

在明晰了镜像删除的原理之后,自己如果确实想要释放磁盘空间,可以人工对相应目录里的 数据进行删除。