从pull镜像到启动容器

完成测试环境初始化后,重新查看/var/lib/containerd目录:

  1. tree /var/lib/containerd/ -L 2
  2. /var/lib/containerd/
  3. ├── io.containerd.content.v1.content
  4. └── ingest
  5. ├── io.containerd.metadata.v1.bolt
  6. └── meta.db
  7. ├── io.containerd.runtime.v1.linux
  8. ├── io.containerd.runtime.v2.task
  9. ├── io.containerd.snapshotter.v1.btrfs
  10. ├── io.containerd.snapshotter.v1.native
  11. └── snapshots
  12. ├── io.containerd.snapshotter.v1.overlayfs
  13. └── snapshots
  14. └── tmpmounts

/var/lib/containerd的各个子目录很清晰的对应到了content, snapshot, metadata和runtime,和containerd的架构示意图中的子系统和组件上:
Containerd是如何存储容器镜像和数据的 - 图1
/var/lib/containerd下各个子目录的名称也可以对应到使用ctr plugin ls查看打印的部分插件名称,实际上这些目录是Containerd的插件用于保存数据的目录,每个插件都可以有自己单独的数据目录,Containerd本身不存储数据,它的所有功能都是通过插件实现的。
下面按照下面containerd的数据流图,依次执行从pull镜像、启动容器、在容器中创建一个文件步骤,并观察containerd的数据目录的变化。
Containerd是如何存储容器镜像和数据的 - 图2
先pull一个镜像:

  1. nerdctl pull redis:alpine3.13
  2. docker.io/library/redis:alpine3.13: resolved |++++++++++++++++++++++++++++++++++++++|
  3. index-sha256:eaaa58f8757d6f04b2e34ace57a71d79f8468053c198f5758fd2068ac235f303: done |++++++++++++++++++++++++++++++++++++++|
  4. manifest-sha256:b7cb70118c9729f8dc019187a4411980418a87e6a837f4846e87130df379e2c8: done |++++++++++++++++++++++++++++++++++++++|
  5. config-sha256:1690b63e207f6651429bebd716ace700be29d0110a0cfefff5038bb2a7fb6fc7: done |++++++++++++++++++++++++++++++++++++++|
  6. layer-sha256:6ab1d05b49730290d3c287ccd34640610423d198e84552a4c2a4e98a46680cfd: done |++++++++++++++++++++++++++++++++++++++|
  7. layer-sha256:8cc52074f78e0a2fd174bdd470029cf287b7366bf1b8d3c1f92e2aa8789b92ae: done |++++++++++++++++++++++++++++++++++++++|
  8. layer-sha256:aa7854465cce07929842cb49fc92f659de8a559cf521fc7ea8e1b781606b85cd: done |++++++++++++++++++++++++++++++++++++++|
  9. layer-sha256:8173c12df40f1578a7b2dfbbc0034a4fbc8ec7c870fd32b9236c2e5e1936616a: done |++++++++++++++++++++++++++++++++++++++|
  10. layer-sha256:540db60ca9383eac9e418f78490994d0af424aab7bf6d0e47ac8ed4e2e9bcbba: done |++++++++++++++++++++++++++++++++++++++|
  11. layer-sha256:29712d301e8c43bcd4a36da8a8297d5ff7f68c3d4c3f7113244ff03675fa5e9c: done |++++++++++++++++++++++++++++++++++++++|
  12. elapsed: 16.4s total: 7.7 Mi (481.5 KiB/s)

从上面命令的执行过程来看总共pull了1个index, 1个config, 1个manifest和6个layer。
io.containerd.metadata.v1.bolt/meta.db是boltdb文件,存储了对images和bundles的持久化引用。 boltdb是一个嵌入式的key/value数据库,containerd的源码文件https://github.com/containerd/containerd/blob/master/metadata/buckets.go头部的注释描述了db schema数据结构。

  1. // keys.
  2. // ├──version : <varint> - Latest version, see migrations
  3. // └──v1 - Schema version bucket
  4. // ╘══*namespace*
  5. // ├──labels
  6. // │ ╘══*key* : <string> - Label value
  7. // ├──image
  8. // │ ╘══*image name*
  9. // │ ├──createdat : <binary time> - Created at
  10. // │ ├──updatedat : <binary time> - Updated at
  11. // │ ├──target
  12. // │ │ ├──digest : <digest> - Descriptor digest
  13. // │ │ ├──mediatype : <string> - Descriptor media type
  14. // │ │ └──size : <varint> - Descriptor size
  15. // │ └──labels
  16. // │ ╘══*key* : <string> - Label value
  17. // ├──containers
  18. // │ ╘══*container id*
  19. // │ ├──createdat : <binary time> - Created at
  20. // │ ├──updatedat : <binary time> - Updated at
  21. // │ ├──spec : <binary> - Proto marshaled spec
  22. // │ ├──image : <string> - Image name
  23. // │ ├──snapshotter : <string> - Snapshotter name
  24. // │ ├──snapshotKey : <string> - Snapshot key
  25. // │ ├──runtime
  26. // │ │ ├──name : <string> - Runtime name
  27. // │ │ ├──extensions
  28. // │ │ │ ╘══*name* : <binary> - Proto marshaled extension
  29. // │ │ └──options : <binary> - Proto marshaled options
  30. // │ └──labels
  31. // │ ╘══*key* : <string> - Label value
  32. // ├──snapshots
  33. // │ ╘══*snapshotter*
  34. // │ ╘══*snapshot key*
  35. // │ ├──name : <string> - Snapshot name in backend
  36. // │ ├──createdat : <binary time> - Created at
  37. // │ ├──updatedat : <binary time> - Updated at
  38. // │ ├──parent : <string> - Parent snapshot name
  39. // │ ├──children
  40. // │ │ ╘══*snapshot key* : <nil> - Child snapshot reference
  41. // │ └──labels
  42. // │ ╘══*key* : <string> - Label value
  43. // ├──content
  44. // │ ├──blob
  45. // │ │ ╘══*blob digest*
  46. // │ │ ├──createdat : <binary time> - Created at
  47. // │ │ ├──updatedat : <binary time> - Updated at
  48. // │ │ ├──size : <varint> - Blob size
  49. // │ │ └──labels
  50. // │ │ ╘══*key* : <string> - Label value
  51. // │ └──ingests
  52. // │ ╘══*ingest reference*
  53. // │ ├──ref : <string> - Ingest reference in backend
  54. // │ ├──expireat : <binary time> - Time to expire ingest
  55. // │ └──expected : <digest> - Expected commit digest
  56. // └──leases
  57. // ╘══*lease id*
  58. // ├──createdat : <binary time> - Created at
  59. // ├──labels
  60. // │ ╘══*key* : <string> - Label value
  61. // ├──snapshots
  62. // │ ╘══*snapshotter*
  63. // │ ╘══*snapshot key* : <nil> - Snapshot reference
  64. // ├──content
  65. // │ ╘══*blob digest* : <nil> - Content blob reference
  66. // └──ingests
  67. // ╘══*ingest reference* : <nil> - Content ingest reference

可以看出主要记录了关于image, content, snapshots, containers的元数据。这里编写了一个简单的go程序读取打印一下当前boltdb中的内容,注意上面结构描述中的一些binary类型会打印成乱码。

  1. package main
  2. import (
  3. "fmt"
  4. "log"
  5. bolt "go.etcd.io/bbolt"
  6. )
  7. func main() {
  8. {
  9. db, err := bolt.Open("/var/lib/continaerd/io.containerd.metadata.v1.bolt/meta.db", 0666, nil)
  10. if err != nil {
  11. log.Fatal(err)
  12. }
  13. defer db.Close()
  14. db.View(func(tx *bolt.Tx) error {
  15. b := tx.Bucket([]byte("v1")).Bucket([]byte("default"))
  16. space := ""
  17. travelBucket(b, space)
  18. return nil
  19. })
  20. }
  21. }
  22. func travelBucket(b *bolt.Bucket, space string) {
  23. space = space + "\t"
  24. b.ForEach(func(k, v []byte) error {
  25. if v == nil {
  26. fmt.Printf("%sbucket=%s: \n", space, k)
  27. travelBucket(b.Bucket([]byte(k)), space)
  28. } else {
  29. fmt.Printf("%skey=%s, value=%s\n", space, k, v)
  30. }
  31. return nil
  32. })
  33. }

到这里只需明白一点,meta.db中存储的是各个存储的元数据。那么实际pull的镜像被存储到哪了呢? 镜像内容被存储到了io.containerd.content.v1.content/blobs/sha256中:

  1. ll io.containerd.content.v1.content/blobs/sha256
  2. total 10668
  3. 1690b63e207f6651429bebd716ace700be29d0110a0cfefff5038bb2a7fb6fc7
  4. 29712d301e8c43bcd4a36da8a8297d5ff7f68c3d4c3f7113244ff03675fa5e9c
  5. 540db60ca9383eac9e418f78490994d0af424aab7bf6d0e47ac8ed4e2e9bcbba
  6. 6ab1d05b49730290d3c287ccd34640610423d198e84552a4c2a4e98a46680cfd
  7. 8173c12df40f1578a7b2dfbbc0034a4fbc8ec7c870fd32b9236c2e5e1936616a
  8. 8cc52074f78e0a2fd174bdd470029cf287b7366bf1b8d3c1f92e2aa8789b92ae
  9. aa7854465cce07929842cb49fc92f659de8a559cf521fc7ea8e1b781606b85cd
  10. b7cb70118c9729f8dc019187a4411980418a87e6a837f4846e87130df379e2c8
  11. eaaa58f8757d6f04b2e34ace57a71d79f8468053c198f5758fd2068ac235f303

上面的9个文件,正好对应1个index文件, 1个config, 1个manifest文件和6个layer文件。index和manifest可以直接用cat命令查看,layer文件可以用tar解压缩。因此content中保存的是config, manifest, tar文件是OCI镜像标准的那套东西。
而实际上containerd也确实是将这些content中的tar解压缩到snapshot中。
查看/var/lib/containerd目录:

  1. tree /var/lib/containerd/ -L 3
  2. /var/lib/containerd/
  3. ├── io.containerd.content.v1.content
  4. ├── blobs
  5. └── sha256
  6. └── ingest
  7. ├── io.containerd.metadata.v1.bolt
  8. └── meta.db
  9. ├── io.containerd.runtime.v1.linux
  10. ├── io.containerd.runtime.v2.task
  11. ├── io.containerd.snapshotter.v1.btrfs
  12. ├── io.containerd.snapshotter.v1.native
  13. └── snapshots
  14. ├── io.containerd.snapshotter.v1.overlayfs
  15. ├── metadata.db
  16. └── snapshots
  17. ├── 1
  18. ├── 2
  19. ├── 3
  20. ├── 4
  21. ├── 5
  22. └── 6
  23. └── tmpmounts

可以看到io.containerd.snapshotter.v1.overlayfs/snapshots中多了名称为1~6的6个子目录,查看这6个目录:

  1. tree /var/lib/containerd/io.containerd.snapshotter.v1.overlayfs/snapshots/ -L 3
  2. /var/lib/containerd/io.containerd.snapshotter.v1.overlayfs/snapshots/
  3. ├── 1
  4. ├── fs
  5. ├── bin
  6. ├── dev
  7. ├── etc
  8. ├── home
  9. ├── lib
  10. ├── media
  11. ├── mnt
  12. ├── opt
  13. ├── proc
  14. ├── root
  15. ├── run
  16. ├── sbin
  17. ├── srv
  18. ├── sys
  19. ├── tmp
  20. ├── usr
  21. └── var
  22. └── work
  23. ├── 2
  24. ├── fs
  25. ├── etc
  26. └── home
  27. └── work
  28. ├── 3
  29. ├── fs
  30. ├── etc
  31. ├── lib
  32. ├── sbin
  33. ├── usr
  34. └── var
  35. └── work
  36. ├── 4
  37. ├── fs
  38. ├── bin
  39. ├── etc
  40. ├── lib
  41. ├── tmp
  42. ├── usr
  43. └── var
  44. └── work
  45. ├── 5
  46. ├── fs
  47. └── data
  48. └── work
  49. └── 6
  50. ├── fs
  51. └── usr
  52. └── work

containerd的snapshotter的主要作用就是通过mount各个层为容器准备rootfs。containerd默认配置的snapshotter是overlayfs,overlayfs是联合文件系统的一种实现。 overlayfs将只读的镜像层成为lowerdir,将读写的容器层成为upperdir,最后联合挂载呈现出mergedir。
下面启动一个redis容器:

nerdctl run -d —name redis redis:alpine3.13

可以看到io.containerd.snapshotter.v1.overlayfs/snapshots中多了名称为7的目录:

  1. tree /var/lib/containerd/io.containerd.snapshotter.v1.overlayfs/snapshots/ -L 2
  2. /var/lib/containerd/io.containerd.snapshotter.v1.overlayfs/snapshots/
  3. ├── 1
  4. ├── fs
  5. └── work
  6. ├── 2
  7. ├── fs
  8. └── work
  9. ├── 3
  10. ├── fs
  11. └── work
  12. ├── 4
  13. ├── fs
  14. └── work
  15. ├── 5
  16. ├── fs
  17. └── work
  18. ├── 6
  19. ├── fs
  20. └── work
  21. └── 7
  22. ├── fs
  23. └── work

可以使用mount命令查看容器挂载的overlayfs的RootFS:

  1. mount | grep /var/lib/containerd
  2. overlay on /run/containerd/io.containerd.runtime.v2.task/default/8102f7fbee26792830e54e80b3488714ac559e092c59beb2e311cf8e88f475d6/rootfs type overlay (rw,relatime,lowerdir=/var/lib/containerd/io.containerd.snapshotter.v1.overlayfs/snapshots/6/fs:/var/lib/containerd/io.containerd.snapshotter.v1.overlayfs/snapshots/5/fs:/var/lib/containerd/io.containerd.snapshotter.v1.overlayfs/snapshots/4/fs:/var/lib/containerd/io.containerd.snapshotter.v1.overlayfs/snapshots/3/fs:/var/lib/containerd/io.containerd.snapshotter.v1.overlayfs/snapshots/2/fs:/var/lib/containerd/io.containerd.snapshotter.v1.overlayfs/snapshots/1/fs,upperdir=/var/lib/containerd/io.containerd.snapshotter.v1.overlayfs/snapshots/7/fs,workdir=/var/lib/containerd/io.containerd.snapshotter.v1.overlayfs/snapshots/7/work)

可以看出: snapshots/6/fs, snapshots/5/fs…snapshots/1/fs为lowerdir,snapshots/7/fs为upperdir。 最终联合挂载合并呈现的目录为/run/containerd/io.containerd.runtime.v2.task/default/8102f7fbee26792830e54e80b3488714ac559e092c59beb2e311cf8e88f475d6/rootfs即为容器的rootfs,ls查看这个目录可以看到一个典型的linux系统目录结构:

  1. ls /run/containerd/io.containerd.runtime.v2.task/default/8102f7fbee26792830e54e80b3488714ac559e092c59beb2e311cf8e88f475d6/rootfs
  2. bin data dev etc home lib media mnt opt proc root run sbin srv sys tmp usr var

我们exec到容器中去,并在/root目录中创建一个hello文件:

nerdctl exec -it redis sh
echo hello > /root/hello

在宿主机上的upperdir中可以找到这个文件。在容器中对文件系统做的改动都会体现在upperdir中:

ls /var/lib/containerd/io.containerd.snapshotter.v1.overlayfs/snapshots/7/fs/root/ hello

containerd的Snapshotter

最后来看一下containerd的Snapshot组件。Snapshot为containerd实现了Snapshotter用于管理文件系统上容器镜像的快照和容器的rootfs挂载和卸载等操作功能。 snapshotter对标Docker中的graphdriver存储驱动的设计。contaienrd在设计上使用snapshotter新模式取代了docker中的graphdriver,其核心开发人员也在其博客中 Where are containerd’s graph drivers?中介绍了为什么要这么做。