在执行docker load的时候,首先由Docker Client向Docker Server发送load请求。是由Docker Daemon完成镜像的load。Docker是C/S架构,所以先Docker Client去解析请求,源码实现在./docker/api/cient/command.go#L2289-2316

    1. func (cli *DockerCli) CmdLoad(args ...string) error {
    2. cmd := cli.Subcmd("load", "", "Load an image from a tar archive on STDIN")

    通过client包的Subcmd方法返回一个实例化的flagSet对象cmd

    1. func (cli *DockerCli) Subcmd(name, signature, description string) *flag.FlagSet {
    2. flags := flag.NewFlagSet(name, flag.ContinueOnError)
    3. flags.Usage = func() {
    4. fmt.Fprintf(cli.err, "\nUsage: docker %s %s\n\n%s\n\n", name, signature, description)
    5. flags.PrintDefaults()
    6. os.Exit(2)
    7. }
    8. return flags
    9. }

    通过cmd.String生成命令行参数对应的变量,变量为指针类型,通过cmd.Parse(args)对变量进行解析,解析后就会建立绑定关系。由于docker load -i没有不被解析的参数,所以,如果cmd.NArg()不等于零,则异常,返回帮助信息。

    1. infile := cmd.String([]string{"i", "-input"}, "", "Read from a tar archive file, instead of STDIN")
    2. if err := cmd.Parse(args); err != nil {
    3. return err
    4. }
    5. if cmd.NArg() != 0 {
    6. cmd.Usage()
    7. return nil
    8. }

    以上其实,代码其实就是对docker load后面的参数进行解析绑定,如果有异常直接返回。详细的解释,可以参考处理命令行参数的flags库。

    定义io.Reader类型和error类型的变量,然后打开输入的文件,此时*infile存储的是load的镜像。in_put存储的是镜像文件对象。

    1. var (
    2. input io.Reader = cli.in
    3. err error
    4. )
    5. if *infile != "" {
    6. input, err = os.Open(*infile)
    7. if err != nil {
    8. return err
    9. }
    10. }

    准备好参数后,就调用http请求的POST方法,向Docker Server发送请求。

    1. if err := cli.stream("POST", "/images/load", input, cli.out, nil); err != nil {
    2. return err
    3. }
    4. return nil

    Docker Server接受Client的发送的请求。Docker Server接收到镜像的load请求后,通过路由分发最后由具体的方法去处理,具体方法在./docker/api/server/server.go#622-626

    1. func postImagesLoad(eng *engine.Engine, version version.Version, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
    2. job := eng.Job("load")
    3. job.Stdin.Add(r.Body)
    4. return job.Run()
    5. }

    这个方法主要工作就是创建镜像load的Job,并且把请求的body作为Job的标准输入参数,最后触发下载的Job.

    Docker Daemon是完成Job执行的主要载体。具体的执行函数是CmdLoad,具体在./docker/graph/load.go#18-94

    1. func (s *TagStore) CmdLoad(job *engine.Job) engine.Status {
    2. // 创建临时目录
    3. tmpImageDir, err := ioutil.TempDir("", "docker-import-")
    4. var (
    5. repoTarFile = path.Join(tmpImageDir, "repo.tar")
    6. repoDir = path.Join(tmpImageDir, "repo")
    7. )
    8. tarFile, err := os.Create(repoTarFile)
    9. if err != nil {
    10. return job.Error(err)
    11. }
    12. if _, err := io.Copy(tarFile, job.Stdin); err != nil {
    13. return job.Error(err)
    14. }
    15. tarFile.Close()
    16. repoFile, err := os.Open(repoTarFile)
    17. if err != nil {
    18. return job.Error(err)
    19. }
    20. if err := os.Mkdir(repoDir, os.ModeDir); err != nil {
    21. return job.Error(err)
    22. }

    首先在临时目录tmp中创建目录docker-import-xxxx.

    然后定义两个路径变量repoTarFilerepoDir

    创建文件repoTarFile,并且返回文件对象tarFile,然后把输入的镜像文件对象复制给打开的文件对象tarFile。打开已经拷贝的镜像文件repoTarFile,创建文件repoDir

    1. images, err := s.graph.Map()
    2. if err != nil {
    3. return job.Error(err)
    4. }
    5. excludes := make([]string, len(images))
    6. i := 0
    7. for k := range images {
    8. excludes[i] = k
    9. i++
    10. }
    11. if err := archive.Untar(repoFile, repoDir, &archive.TarOptions{Excludes: excludes}); err != nil {
    12. return job.Error(err)
    13. }

    然后通过方法s.graph.Map()获取已有的镜像列表,得到镜像的列表excludes,然后通过方法archive.Untar去解压,如果load的镜像已经存在的话,则不会解压镜像的层次,也就不会出现目录,下面的s.recursiveLoad方法就不会调用。

    1. dirs, err := ioutil.ReadDir(repoDir)
    2. if err != nil {
    3. return job.Error(err)
    4. }
    5. for _, d := range dirs {
    6. if d.IsDir() {
    7. if err := s.recursiveLoad(job.Eng, d.Name(), tmpImageDir); err != nil {
    8. return job.Error(err)
    9. }
    10. }
    11. }

    通过ioutil.ReadDir方法去读取解压的文件repoDir,然后遍历,如果是文件夹则执行方法s.recursiveLoad

    1. func (s *TagStore) recursiveLoad(eng *engine.Engine, address, tmpImageDir string) error {
    2. if err := eng.Job("image_get", address).Run(); err != nil {
    3. log.Debugf("Loading %s", address)
    4. // core
    5. imageJson, err := ioutil.ReadFile(path.Join(tmpImageDir, "repo", address, "json"))
    6. if err != nil {
    7. log.Debugf("Error reading json", err)
    8. return err
    9. }
    10. // core
    11. layer, err := os.Open(path.Join(tmpImageDir, "repo", address, "layer.tar"))
    12. if err != nil {
    13. log.Debugf("Error reading embedded tar", err)
    14. return err
    15. }
    16. // core
    17. img, err := image.NewImgJSON(imageJson)
    18. if err != nil {
    19. log.Debugf("Error unmarshalling json", err)
    20. return err
    21. }
    22. if img.Parent != "" {
    23. if !s.graph.Exists(img.Parent) {
    24. if err := s.recursiveLoad(eng, img.Parent, tmpImageDir); err != nil {
    25. return err
    26. }
    27. }
    28. }
    29. // core
    30. if err := s.graph.Register(imageJson, layer, img); err != nil {
    31. return err
    32. }
    33. }
    34. log.Debugf("Completed processing %s", address)
    35. return nil
    36. }

    首先通过方法eng.Job()判断,加载的镜像的层是否已经存在,如果存在则直接退出,如果不存在,通过方法ioutil.ReadFile()读取解压的json信息。通过方法os.Open(path.Join(tmpImageDir, “repo”, address, “layer.tar”))读取打开的文件。然后实例化一个image的类。

    image的类定义和初始化在./docker/image/image.go#322-331

    1. func NewImgJSON(src []byte) (*Image, error) {
    2. ret := &Image{}
    3. log.Debugf("Json string: {%s}", src)
    4. // FIXME: Is there a cleaner way to "purify" the input json?
    5. if err := json.Unmarshal(src, ret); err != nil {
    6. return nil, err
    7. }
    8. return ret, nil
    9. }

    然后一个实例化的镜像对象,

    然后对镜像的Json文件进行解析,如果存在Parent的layer,则循环执行s.recursiveLoad,在解压后,会遍历对镜像的层进行添加,为啥函数内还需要去判断Parent的layer是否存在去执行循环呢?如果不要内部的判断,从文件中遍历image的layer也是可以的。

    需要去Register,把导入的镜像的每个层都注册到graph中。具体代码在./docker/graph/graph.go#161-218.

    在grap中注册后,还需要在store中建立容器名字和镜像ID之间的关系。

    1. repositoriesJson, err := ioutil.ReadFile(path.Join(tmpImageDir, "repo", "repositories"))
    2. if err == nil {
    3. repositories := map[string]Repository{}
    4. if err := json.Unmarshal(repositoriesJson, &repositories); err != nil {
    5. return job.Error(err)
    6. }
    7. log.Debugf("repositories: %s", repositories)
    8. for imageName, tagMap := range repositories {
    9. for tag, address := range tagMap {
    10. if err := s.Set(imageName, tag, address, true); err != nil {
    11. return job.Error(err)
    12. }
    13. }
    14. }
    15. } else if !os.IsNotExist(err) {
    16. return job.Error(err)
    17. }

    打开文件repositories,然后遍历去执行方法s.Set(),具体的实现是./docker/graph/tags.go#172-205

    1. type TagStore struct {
    2. path string
    3. graph *Graph
    4. Repositories map[string]Repository
    5. sync.Mutex
    6. // FIXME: move push/pull-related fields
    7. // to a helper type
    8. pullingPool map[string]chan struct{}
    9. pushingPool map[string]chan struct{}
    10. }
    1. type Repository map[string]string

    类TagStore的属性Repositories,是一个map嵌套的类型。

    1. func (store *TagStore) Set(repoName, tag, imageName string, force bool) error {
    2. img, err := store.LookupImage(imageName)
    3. store.Lock()
    4. defer store.Unlock()
    5. if err != nil {
    6. return err
    7. }
    8. if tag == "" {
    9. tag = DEFAULTTAG
    10. }
    11. if err := validateRepoName(repoName); err != nil {
    12. return err
    13. }
    14. if err := validateTagName(tag); err != nil {
    15. return err
    16. }
    17. if err := store.reload(); err != nil {
    18. return err
    19. }
    20. var repo Repository
    21. if r, exists := store.Repositories[repoName]; exists {
    22. repo = r
    23. } else {
    24. repo = make(map[string]string)
    25. if old, exists := store.Repositories[repoName]; exists && !force {
    26. return fmt.Errorf("Conflict: Tag %s:%s is already set to %s", repoName, tag, old)
    27. }
    28. // core
    29. store.Repositories[repoName] = repo
    30. }
    31. repo[tag] = img.ID
    32. return store.save()
    33. }

    方法Set先去graph去查找是否存在对用的镜像,通过方法store.LookupImage(imageName)。最终也是通过graph.get()方法去获取镜像是否存在graph中。

    1. func (store *TagStore) LookupImage(name string) (*image.Image, error) {
    2. // FIXME: standardize on returning nil when the image doesn't exist, and err for everything else
    3. // (so we can pass all errors here)
    4. repos, tag := parsers.ParseRepositoryTag(name)
    5. if tag == "" {
    6. tag = DEFAULTTAG
    7. }
    8. img, err := store.GetImage(repos, tag)
    9. store.Lock()
    10. defer store.Unlock()
    11. if err != nil {
    12. return nil, err
    13. } else if img == nil {
    14. if img, err = store.graph.Get(name); err != nil {
    15. return nil, err
    16. }
    17. }
    18. return img, nil
    19. }

    如果存在,那就需要存到store中,建立容器名字和容器ID之间的关系。store.Repositories[repoName] = repo,也就是存储的是{“imageName”:{“tag”:”imageID”}}

    而最后的store.save()会进行存盘,具体的文件是./var/lib/docker/repositories-devicemapper

    docker load上传过程结束,代码执行完后,需要调用

    1. defer os.RemoveAll(tmpImageDir)

    去删除刚开始创建的临时目录。

    docker相关的镜像文件存在/var/lib/docker目录下,镜像的存储结构主要分成两部分,一是镜像ID之间的联系,二是镜像ID与镜像名称之间的关联。前者的结构体是Graph,后者是TagStore

    Graph结构存储了各个镜像的元数据及其之间的关系,但是并没有建立镜像名字和镜像的ID之间的关系。而TagStore结构存储了镜像名字tag(centos:latest)与其镜像ID( d0955f21bf24 )关联起来的数据结构。