1 Docker 打包成镜像

docker commit 将容器提交成镜像

  • docker export 将容器打包成 tar 包

docker export -o .tar

根据容器的大小,打包的效率也会随之变化

  • docker import 将 tar 包提交为镜像

docker import .tar :

根据Tar包的大小,提交镜像的效率也会随之变化

  • docker save 将镜像打包成 tar 包

docker save : -o .tar

根据镜像的大小,打包的效率也会随之变化

  • docker load 将 tar 包提交为镜像

docker load -i .tar

根据Tar包的大小,提交镜像的效率也会随之变化

以及使用 Dockerfile 编写镜像脚本

docker build -t ImageName:[Tag] -f Dockerfile .

2 Docker load 过程源码浅析

在执行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. }go
  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.

然后定义两个路径变量repoTarFile和repoDir

创建文件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 )关联起来的数据结构。

参考《Docker源码分析》

3 动手用 shell 写个 Docker

本文主要介绍使用shell实现一个简易的Docker。

一、目的

在初接触Docker的时候,我们必须要了解的几个概念就是Cgroup、Namespace、RootFs,如果本身对虚拟化的发展没有深入的了解,那么很难对这几个概念有深入的理解,本文的目的就是通过在操作系统中以交互式的方式去理解,Cgroup/Namespace/Rootfs到底实现了什么,能做到哪些事情,然后通过shell这种直观的命令行方式把我们的理解组合起来,去模仿Docker实现一个缩减的版本。

🐋1 Docker 打包成镜像及Docker部分源码分析 - 图1

二、技术拆解

2.1 Namespace

2.1.1 简介

Linux Namespace是Linux提供的一种内核级别环境隔离的方法。学习过Linux的同学应该对chroot命令比较熟悉(通过修改根目录把用户限制在一个特定目录下),chroot提供了一种简单的隔离模式:chroot内部的文件系统无法访问外部的内容。Linux Namespace在此基础上,提供了对UTS、IPC、mount、PID、network、User等的隔离机制。Namespace是对全局系统资源的一种封装隔离,使得处于不同namespace的进程拥有独立的全局系统资源,改变一个namespace中的系统资源只会影响当前namespace里的进程,对其他namespace中的进程没有影响。

Linux Namespace有如下种类:

🐋1 Docker 打包成镜像及Docker部分源码分析 - 图2

2.1.2 Namespace相关系统调用

amespace相关的系统调用有3个,分别是clone(),setns(),unshare()。

  • clone:** **创建一个新的进程并把这个新进程放到新的namespace中
  • setns:** **将当前进程加入到已有的namespace中
  • unshare: 使当前进程退出指定类型的namespace,并加入到新创建的namespace中

2.1.3 查看进程所属Namespace

上面的概念都比较抽象,我们来看看在Linux系统中怎么样去get namespace。

系统中的每个进程都有/proc/[pid]/ns/这样一个目录,里面包含了这个进程所属namespace的信息,里面每个文件的描述符都可以用来作为setns函数(2.1.2)的fd参数。

  1. #查看当前bash进程关联的Namespace
  2. # ls -l /proc/$$/ns
  3. total 0
  4. lrwxrwxrwx 1 root root 0 Jan 17 21:43 ipc -> ipc:[4026531839]
  5. lrwxrwxrwx 1 root root 0 Jan 17 21:43 mnt -> mnt:[4026531840]
  6. lrwxrwxrwx 1 root root 0 Jan 17 21:43 net -> net:[4026531956]
  7. lrwxrwxrwx 1 root root 0 Jan 17 21:43 pid -> pid:[4026531836]
  8. lrwxrwxrwx 1 root root 0 Jan 17 21:43 user -> user:[4026531837]
  9. lrwxrwxrwx 1 root root 0 Jan 17 21:43 uts -> uts:[4026531838]
  10. #这些 namespace 文件都是链接文件。链接文件的内容的格式为 xxx:[inode number]。
  11. 其中的 xxx namespace 的类型,inode number 则用来标识一个 namespace,我们也可以把它理解为 namespace ID
  12. 如果两个进程的某个 namespace 文件指向同一个链接文件,说明其相关资源在同一个 namespace 中。以ipc:[4026531839]例,
  13. ipcnamespace的类型,4026531839inode number,如果两个进程的ipc namespaceinode number一样,说明他们属于同一个namespace
  14. 这条规则对其他类型的namespace也同样适用。
  15. #从上面的输出可以看出,对于每种类型的namespace,进程都会与一个namespace ID关联。
  16. #当一个namespace中的所有进程都退出时,该namespace将会被销毁。在 /proc/[pid]/ns 里放置这些链接文件的作用就是,一旦这些链接文件被打开,
  17. 只要打开的文件描述符(fd)存在,那么就算该 namespace 下的所有进程都结束了,但这个 namespace 也会一直存在,后续的进程还可以再加入进来。

2.1.4 相关命令及操作示例

本节会用UTS/IPC/NET 3个Namespace作为示例演示如何在linux系统中创建Namespace,并介绍相关命令。

2.1.4.1 IPC Namespace

IPC namespace用来隔离System V IPC objects和POSIX message queues。其中System V IPC objects包含消息列表Message queues、信号量Semaphore sets和共享内存Shared memory segments。为了展现区分IPC Namespace我们这里会使用到ipc相关命令:

  1. # nsenter: 加入指定进程的指定类型的namespace中,然后执行参数中指定的命令。
  2. # 命令格式:nsenter [options] [program [arguments]]
  3. # 示例:nsenter –t 27668 –u –I /bin/bash
  4. #
  5. # unshare: 离开当前指定类型的namespace,创建且加入新的namesapce,然后执行参数中执行的命令。
  6. # 命令格式:unshare [options] program [arguments]
  7. # 示例:unshare --fork --pid --mount-proc readlink /proc/self
  8. #
  9. # ipcmk:创建shared memory segments, message queues, 和semaphore arrays
  10. # 参数-Q:创建message queues
  11. # ipcs:查看shared memory segments, message queues, 和semaphore arrays的相关信息
  12. # 参数-a:显示全部可显示的信息
  13. # 参数-q:显示活动的消息队列信息

下面将以消息队列为例,演示一下隔离效果,为了使演示更直观,我们在创建新的ipc namespace的时候,同时也创建新的uts namespace,然后为新的uts namespace设置新hostname,这样就能通过shell提示符一眼看出这是属于新的namespace的bash。示例中我们用两个shell来展示:

shell A

  1. #查看当前shell的uts / ipc namespace number
  2. # readlink /proc/$$/ns/uts /proc/$$/ns/ipc
  3. uts:[4026531838]
  4. ipc:[4026531839]
  5. #查看当前主机名
  6. # hostname
  7. myCentos
  8. #查看ipc message queues,默认情况下没有message queue
  9. # ipcs -q
  10. ------ Message Queues --------
  11. key msqid owner perms used-bytes messages
  12. #创建一个message queue
  13. # ipcmk -Q
  14. Message queue id: 131072
  15. # ipcs -q
  16. ------ Message Queues --------
  17. key msqid owner perms used-bytes messages
  18. 0x82a1d963 131072 root 644 0 0
  19. -----> 切换至shell B执行
  20. ------------------------------------------------------------------
  21. #回到shell A之后我们可以看下hostname、ipc等有没有收到影响
  22. # hostname
  23. myCentos
  24. # ipcs -q
  25. ------ Message Queues --------
  26. key msqid owner perms used-bytes messages
  27. 0x82a1d963 131072 root 644 0 0
  28. #接下来我们尝试加入shell B中新的Namespace
  29. # nsenter -t 30372 -u -i /bin/bash
  30. [root@shell-B:/root]
  31. # hostname
  32. shell-B
  33. # readlink /proc/$$/ns/uts /proc/$$/ns/ipc
  34. uts:[4026532382]
  35. ipc:[4026532383]
  36. # ipcs -q
  37. ------ Message Queues --------
  38. key msqid owner perms used-bytes messages
  39. #可以看到我们已经成功的加入到了新的Namespace中

shell B

  1. #确认当前shell和shell A属于相同Namespace
  2. # readlink /proc/$$/ns/uts /proc/$$/ns/ipc
  3. uts:[4026531838]
  4. ipc:[4026531839]
  5. # ipcs -q
  6. ------ Message Queues --------
  7. key msqid owner perms used-bytes messages
  8. 0x82a1d963 131072 root 644 0 0
  9. #使用unshare创建新的uts和ipc Namespace,并在新的Namespace中启动bash
  10. # unshare -iu /bin/bash
  11. #确认新的bash uts/ipc Namespace Number
  12. # readlink /proc/$$/ns/uts /proc/$$/ns/ipc
  13. uts:[4026532382]
  14. ipc:[4026532383]
  15. #设置新的hostname与shell A做区分
  16. # hostname shell-B
  17. # hostname
  18. shell-B
  19. #查看之前的ipc message queue
  20. # ipcs -q
  21. ------ Message Queues --------
  22. key msqid owner perms used-bytes messages
  23. #查看当前bash进程的PID
  24. # echo $$
  25. 30372
  26. 切换回shell A <-----

2.1.4.2 Net Namespace

Network namespace用来隔离网络设备, IP地址, 端口等. 每个namespace将会有自己独立的网络栈,路由表,防火墙规则,socket等。每个新的network namespace默认有一个本地环回接口,除了lo接口外,所有的其他网络设备(物理/虚拟网络接口,网桥等)只能属于一个network namespace。每个socket也只能属于一个network namespace。当新的network namespace被创建时,lo接口默认是关闭的,需要自己手动启动起。标记为”local devices”的设备不能从一个namespace移动到另一个namespace,比如loopback, bridge, ppp等,我们可以通过ethtool -k命令来查看设备的netns-local属性。

我们使用以下命令来创建net namespace。

  1. # 相关命令:
  2. # ip netns: 管理网络namespace
  3. # 用法:
  4. # ip netns list
  5. # ip netns add NAME
  6. # ip netns set NAME NETNSID
  7. # ip [-all] netns delete [NAME]

下面使用ip netns来演示创建net Namespace。

shell A

  1. #创建一对网卡,分别命名为veth0_11/veth1_11
  2. # ip link add veth0_11 type veth peer name veth1_11
  3. #查看已经创建的网卡
  4. #ip a
  5. 1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN qlen 1
  6. link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
  7. inet 127.0.0.1/8 scope host lo
  8. valid_lft forever preferred_lft forever
  9. 2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP qlen 1000
  10. link/ether 5e:75:97:0d:54:17 brd ff:ff:ff:ff:ff:ff
  11. inet 192.168.1.1/24 brd 192.168.1.255 scope global eth0
  12. valid_lft forever preferred_lft forever
  13. 3: br1: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN qlen 1000
  14. link/ether 00:00:00:00:00:00 brd ff:ff:ff:ff:ff:ff
  15. inet 172.18.0.1/24 scope global br1
  16. valid_lft forever preferred_lft forever
  17. 96: veth1_11@veth0_11: <BROADCAST,MULTICAST,M-DOWN> mtu 1500 qdisc noop state DOWN qlen 1000
  18. link/ether 5e:75:97:0d:54:0e brd ff:ff:ff:ff:ff:ff
  19. 97: veth0_11@veth1_11: <BROADCAST,MULTICAST,M-DOWN> mtu 1500 qdisc noop state DOWN qlen 1000
  20. link/ether a6:c7:1f:79:a6:a6 brd ff:ff:ff:ff:ff:ff
  21. #使用ip netns创建两个net namespace
  22. # ip netns add r1
  23. # ip netns add r2
  24. # ip netns list
  25. r2
  26. r1 (id: 0)
  27. #将两个网卡分别加入到对应的netns中
  28. # ip link set veth0_11 netns r1
  29. # ip link set veth1_11 netns r2
  30. #再次查看网卡,在bash当前的namespace中已经看不到veth0_11和veth1_11了
  31. # ip a
  32. 1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN qlen 1
  33. link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
  34. inet 127.0.0.1/8 scope host lo
  35. valid_lft forever preferred_lft forever
  36. 2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP qlen 1000
  37. link/ether 5e:75:97:0d:54:17 brd ff:ff:ff:ff:ff:ff
  38. inet 192.168.1.1/24 brd 192.168.1.255 scope global eth0
  39. valid_lft forever preferred_lft forever
  40. 3: br1: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN qlen 1000
  41. link/ether 00:00:00:00:00:00 brd ff:ff:ff:ff:ff:ff
  42. inet 172.18.0.1/24 scope global br1
  43. valid_lft forever preferred_lft forever
  44. #接下来我们切换到对应的netns中对网卡进行配置
  45. #通过nsenter --net可以切换到对应的netns中,ip a展示了我们上面加入到r1中的网卡
  46. # nsenter --net=/var/run/netns/r1 /bin/bash
  47. # ip a
  48. 1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN qlen 1
  49. link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
  50. 97: veth0_11@if96: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN qlen 1000
  51. link/ether a6:c7:1f:79:a6:a6 brd ff:ff:ff:ff:ff:ff link-netnsid 1
  52. #对网卡配置ip并启动
  53. # ip addr add 172.18.0.11/24 dev veth0_11
  54. # ip link set veth0_11 up
  55. # ip a
  56. 1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN qlen 1
  57. link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
  58. 97: veth0_11@if96: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state LOWERLAYERDOWN qlen 1000
  59. link/ether a6:c7:1f:79:a6:a6 brd ff:ff:ff:ff:ff:ff link-netnsid 1
  60. inet 172.18.0.11/24 scope global veth0_11
  61. valid_lft forever preferred_lft forever
  62. -----> 切换至shell B执行
  63. ------------------------------------------------------------------
  64. #在r1中ping veth1_11
  65. # ping 172.18.0.12
  66. PING 172.18.0.12 (172.18.0.12) 56(84) bytes of data.
  67. 64 bytes from 172.18.0.12: icmp_seq=1 ttl=64 time=0.033 ms
  68. 64 bytes from 172.18.0.12: icmp_seq=2 ttl=64 time=0.049 ms
  69. ...
  70. #至此我们通过netns完成了创建net Namespace的小实验

shell B

  1. #在shell B中我们同样切换到netns r2中进行配置
  2. #通过nsenter --net可以切换到r2,ip a展示了我们上面加入到r2中的网卡
  3. # nsenter --net=/var/run/netns/r2 /bin/bash
  4. # ip a
  5. 1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN qlen 1
  6. link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
  7. 96: veth1_11@if97: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN qlen 1000
  8. link/ether 5e:75:97:0d:54:0e brd ff:ff:ff:ff:ff:ff link-netnsid 0
  9. #对网卡配置ip并启动
  10. # ip addr add 172.18.0.12/24 dev veth1_11
  11. # ip link set veth1_11 up
  12. # ip a
  13. 1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN qlen 1
  14. link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
  15. 96: veth1_11@if97: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP qlen 1000
  16. link/ether 5e:75:97:0d:54:0e brd ff:ff:ff:ff:ff:ff link-netnsid 0
  17. inet 172.18.0.12/24 scope global veth1_11
  18. valid_lft forever preferred_lft forever
  19. inet6 fe80::5c75:97ff:fe0d:540e/64 scope link
  20. valid_lft forever preferred_lft forever
  21. #尝试ping r1中的网卡
  22. # ping 172.18.0.11
  23. PING 172.18.0.11 (172.18.0.11) 56(84) bytes of data.
  24. 64 bytes from 172.18.0.11: icmp_seq=1 ttl=64 time=0.046 ms
  25. 64 bytes from 172.18.0.11: icmp_seq=2 ttl=64 time=0.040 ms
  26. ...
  27. #可以完成通信
  28. 切换至shell A执行 <-----

示意图

🐋1 Docker 打包成镜像及Docker部分源码分析 - 图3

2.2 Cgroup

2.2.1 简介

Cgroup和namespace类似,也是将进程进行分组,但它的目的和namespace不一样,namespace是为了隔离进程组之间的资源,而cgroup是为了对一组进程进行统一的资源监控和限制。

Cgroup作用:

  1. 资源限制(Resource limiting):** **Cgroups可以对进程组使用的资源总额进行限制。如对特定的进程进行内存使用上限限制,当超出上限时,会触发OOM。
  2. 优先级分配(Prioritization): 通过分配的CPU时间片数量及硬盘IO带宽大小,实际上就相当于控制了进程运行的优先级。

  3. 资源统计(Accounting):** **Cgroups可以统计系统的资源使用量,如CPU使用时长、内存用量等等,这个功能非常适用于计费。

  4. 进程控制(Control):Cgroups可以对进程组执行挂起、恢复等操作。

Cgroups的组成:

  1. task: 在Cgroups中,task就是系统的一个进程。
  2. cgroup:** **Cgroups中的资源控制都以cgroup为单位实现的。cgroup表示按照某种资源控制标准划分而成的任务组,包含一个或多个子系统。一个任务可以加入某个cgroup,也可以从某个cgroup迁移到另外一个cgroup。
  3. subsystem: 一个subsystem就是一个内核模块,被关联到一颗cgroup树之后,就会在树的每个节点(进程组)上做具体的操作。subsystem经常被称作”resource controller”,因为它主要被用来调度或者限制每个进程组的资源,但是这个说法不完全准确,因为有时我们将进程分组只是为了做一些监控,观察一下他们的状态,比如perf_event subsystem。到目前为止,Linux支持13种subsystem(Cgroup v1),比如限制CPU的使用时间,限制使用的内存,统计CPU的使用情况,冻结和恢复一组进程等。

  4. hierarchy: 一个hierarchy可以理解为一棵cgroup树,树的每个节点就是一个进程组,每棵树都会与零到多个subsystem关联。在一颗树里面,会包含Linux系统中的所有进程,但每个进程只能属于一个节点(进程组)。系统中可以有很多颗cgroup树,每棵树都和不同的subsystem关联,一个进程可以属于多颗树,即一个进程可以属于多个进程组,只是这些进程组和不同的subsystem关联。如果不考虑不与任何subsystem关联的情况(systemd就属于这种情况),Linux里面最多可以建13颗cgroup树,每棵树关联一个subsystem,当然也可以只建一棵树,然后让这棵树关联所有的subsystem。当一颗cgroup树不和任何subsystem关联的时候,意味着这棵树只是将进程进行分组,至于要在分组的基础上做些什么,将由应用程序自己决定,systemd就是一个这样的例子。

2.2.2 查看Cgroup信息

查看当前系统支持的subsystem

  1. #通过/proc/cgroups查看当前系统支持哪些subsystem
  2. # cat /proc/cgroups
  3. #subsys_name hierarchy num_cgroups enabled
  4. cpuset 11 1 1
  5. cpu 4 67 1
  6. cpuacct 4 67 1
  7. memory 5 69 1
  8. devices 7 62 1
  9. freezer 8 1 1
  10. net_cls 6 1 1
  11. blkio 9 62 1
  12. perf_event 3 1 1
  13. hugetlb 2 1 1
  14. pids 10 62 1
  15. net_prio 6 1 1
  16. #字段含义
  17. #subsys_name: subsystem的名称
  18. #hierarchy:subsystem所关联到的cgroup树的ID,如果多个subsystem关联到同一颗cgroup树,那么他们的这个字段将一样,比如这里的cpu和cpuacct就一样,表示他们绑定到了同一颗树。如果出现下面的情况,这个字段将为0:
  19. 当前subsystem没有和任何cgroup树绑定
  20. 当前subsystem已经和cgroup v2的树绑定
  21. 当前subsystem没有被内核开启
  22. #num_cgroups: subsystem所关联的cgroup树中进程组的个数,也即树上节点的个数
  23. #enabled: 1表示开启,0表示没有被开启(可以通过设置内核的启动参数“cgroup_disable”来控制subsystem的开启).

查看进程所属cgroup

  1. #查看当前shell进程所属的cgroup
  2. # cat /proc/$$/cgroup
  3. 11:cpuset:/
  4. 10:pids:/system.slice/sshd.service
  5. 9:blkio:/system.slice/sshd.service
  6. 8:freezer:/
  7. 7:devices:/system.slice/sshd.service
  8. 6:net_prio,net_cls:/
  9. 5:memory:/system.slice/sshd.service
  10. 4:cpuacct,cpu:/system.slice/sshd.service
  11. 3:perf_event:/
  12. 2:hugetlb:/
  13. 1:name=systemd:/system.slice/sshd.service
  14. #字段含义(以冒号分为3列):
  15. # 1. cgroup树ID,对应/proc/cgroups中的hierachy
  16. # 2. cgroup所绑定的subsystem,多个subsystem使用逗号分隔。name=systemd表示没有和任何subsystem绑定,只是给他起了个名字叫systemd。
  17. # 3. 进程在cgroup树中的路径,即进程所属的cgroup,这个路径是相对于挂载点的相对路径。

2.2.3 相关命令

使用cgroup

cgroup相关的所有操作都是基于内核中的cgroup virtual filesystem,使用cgroup很简单,挂载这个文件系统就可以了。一般情况下都是挂载到/sys/fs/cgroup目录下,当然挂载到其它任何目录都没关系。

查看下当前系统cgroup挂载情况。

  1. #过滤系统挂载可以查看cgroup
  2. # mount |grep cgroup
  3. tmpfs on /sys/fs/cgroup type tmpfs (ro,nosuid,nodev,noexec,mode=755)
  4. cgroup on /sys/fs/cgroup/systemd type cgroup (rw,nosuid,nodev,noexec,relatime,xattr,release_agent=/usr/lib/systemd/systemd-cgroups-agent,name=systemd)
  5. cgroup on /sys/fs/cgroup/hugetlb type cgroup (rw,nosuid,nodev,noexec,relatime,hugetlb)
  6. cgroup on /sys/fs/cgroup/perf_event type cgroup (rw,nosuid,nodev,noexec,relatime,perf_event)
  7. cgroup on /sys/fs/cgroup/cpu,cpuacct type cgroup (rw,nosuid,nodev,noexec,relatime,cpuacct,cpu)
  8. cgroup on /sys/fs/cgroup/memory type cgroup (rw,nosuid,nodev,noexec,relatime,memory)
  9. cgroup on /sys/fs/cgroup/net_cls,net_prio type cgroup (rw,nosuid,nodev,noexec,relatime,net_prio,net_cls)
  10. cgroup on /sys/fs/cgroup/devices type cgroup (rw,nosuid,nodev,noexec,relatime,devices)
  11. cgroup on /sys/fs/cgroup/freezer type cgroup (rw,nosuid,nodev,noexec,relatime,freezer)
  12. cgroup on /sys/fs/cgroup/blkio type cgroup (rw,nosuid,nodev,noexec,relatime,blkio)
  13. cgroup on /sys/fs/cgroup/pids type cgroup (rw,nosuid,nodev,noexec,relatime,pids)
  14. cgroup on /sys/fs/cgroup/cpuset type cgroup (rw,nosuid,nodev,noexec,relatime,cpuset)
  15. #如果系统中没有挂载cgroup,可以使用mount命令创建cgroup
  16. #挂载根cgroup
  17. # mkdir /sys/fs/cgroup
  18. # mount -t tmpfs cgroup_root /sys/fs/cgroup
  19. #将cpuset subsystem关联到/sys/fs/cgroup/cpu_memory
  20. # mkdir /sys/fs/cgroup/cpuset
  21. # sudo mount -t cgroup cpuset -o cgroup /sys/fs/cgroup/cpuset/
  22. #将cpu和memory subsystem关联到/sys/fs/cgroup/cpu_memory
  23. # mkdir /sys/fs/cgroup/cpu_memory
  24. # sudo mount -n -t cgroup -o cpu,memory cgroup /sys/fs/cgroup/cpu_memory

除了mount命令之外我们还可以使用以下命令对cgroup进行创建、属性设置等操作,这也是我们后面脚本中用于创建和管理cgroup的命令。

  1. # Centos操作系统可以通过yum install cgroup-tools 来安装以下命令
  2. cgcreate: 在层级中创建新cgroup
  3. 用法: cgcreate [-h] [-t <tuid>:<tgid>] [-a <agid>:<auid>] [-f mode] [-d mode] [-s mode]
  4. -g <controllers>:<path> [-g ...]
  5. 示例: cgcreate -g *:student -g devices:teacher //在所有的挂载hierarchy中创建student cgroup,在devices
  6. hierarchy挂载点创建teacher cgroup
  7. cgset: 设置指定cgroup(s)的参数
  8. 用法: cgset [-r <name=value>] <cgroup_path> ...
  9. 示例: cgset -r cpuset.cpus=0-1 student //将student cgroupcpuset控制器中的cpus限制为0-1
  10. cgexec: 在指定的cgroup中运行任务
  11. 用法: cgexec [-h] [-g <controllers>:<path>] [--sticky] command [arguments]
  12. 示例: cgexec -g cpu,memory:test1 ls -l //在cpumemory控制器下的test1 cgroup中运行ls -l命令

2.3 Rootfs

2.3.1 简介

Rootfs 是 Docker 容器在启动时内部进程可见的文件系统,即 Docker容器的根目录。rootfs 通常包含一个操作系统运行所需的文件系统,例如可能包含典型的类 Unix 操作系统中的目录系统,如 /dev、/proc、/bin、/etc、/lib、/usr、/tmp 及运行 Docker 容器所需的配置文件、工具等。

就像Linux启动会先用只读模式挂载rootfs,运行完完整性检查之后,再切换成读写模式一样。Docker deamon为container挂载rootfs时,也会先挂载为只读模式,但是与Linux做法不同的是,在挂载完只读的rootfs之后,Docker deamon会利用联合挂载技术(Union Mount)在已有的rootfs上再挂一个读写层。container在运行过程中文件系统发生的变化只会写到读写层,并通过whiteout技术隐藏只读层中的旧版本文件。

Docker支持不同的存储驱动,包括 aufs、devicemapper、overlay2、zfs 和 vfs 等,目前在 Docker 中,overlay2 取代了 aufs 成为了推荐的存储驱动。

2.3.2 overlayfs

overlayFS是联合挂载技术的一种实现。除了overlayFS以外还有aufs,VFS,Brtfs,device mapper等技术。虽然实现细节不同,但是他们做的事情都是相同的。Linux内核为Docker提供的overalyFS驱动有2种:overlay2和overlay,overlay2是相对于overlay的一种改进,在inode利用率方面比overlay更有效。

overlayfs通过三个目录来实现:lower目录、upper目录、以及work目录。三种目录合并出来的目录称为merged目录。

  • lower:可以是多个,是处于最底层的目录,作为只读层。
  • upper:只有一个,作为读写层。
  • work:为工作基础目录,挂载后内容会被清空,且在使用过程中其内容用户不可见。
  • merged:为最后联合挂载完成给用户呈现的统一视图,也就是说merged目录里面本身并没有任何实体文件,给我们展示的只是参与联合挂载的目录里面文件而已,真正的文件还是在lower和upper中。所以,在merged目录下编辑文件,或者直接编辑lower或upper目录里面的文件都会影响到merged里面的视图展示。

🐋1 Docker 打包成镜像及Docker部分源码分析 - 图4

2.3.3 文件规则

merged层目录会显示离它最近层的文件。层级关系中upperdir比lowerdir更靠近merged层,而多个lowerdir的情况下,写的越靠前的目录离merged层目录越近。相同文件名的文件会依照层级规则进行“覆盖”。

2.3.4 overlayFS如何工作

  • 读:
    如果文件在容器层(upperdir),直接读取文件;
    如果文件不在容器层(upperdir),则从镜像层(lowerdir)读取;
  • 写:
    ①首次写入: 如果在upperdir中不存在,overlay执行cow操作,把文件从lowdir拷贝到upperdir,由于overlayfs是文件级别的(即使文件只有很少的一点修改,也会产生的cow的行为),后续对同一文件的在此写入操作将对已经复制到容器的文件的副本进行操作。值得注意的是,cow操作只发生在文件首次写入,以后都是只修改副本。
    ②删除文件和目录: 当文件在容器被删除时,在容器层(upperdir)创建whiteout文件,镜像层(lowerdir)的文件是不会被删除的,因为他们是只读的,但whiteout文件会阻止他们显示。

2.3.5 在系统里创建overlayfs

  1. # 创建所需的目录
  2. # mkdir upper lower merged work
  3. # echo "lower" > lower/in_lower.txt
  4. # echo "upper" > upper/in_upper.txt
  5. # 在lower和upper中都创建 in_both文件
  6. # echo "lower" > lower/in_both.txt
  7. # echo "upper" > upper/in_both.txt
  8. #查看下我们当前的目录及文件结构
  9. # tree .
  10. .
  11. |-- lower
  12. | |-- in_both.txt
  13. | `-- in_lower.txt
  14. |-- merged
  15. |-- upper
  16. | |-- in_both.txt
  17. | `-- in_upper.txt
  18. `-- work
  19. #使用mount命令将创建的目录联合挂载起来
  20. # mount -t overlay overlay -o lowerdir=lower,upperdir=upper,workdir=work merged
  21. #查看mount结果可以看到已经成功挂载了
  22. # mount |grep overlay
  23. overlay on /data/overlay_demo/merged type overlay (rw,relatime,lowerdir=lower,upperdir=upper,workdir=work)
  24. #此时再查看文件目录结构
  25. # tree .
  26. .
  27. |-- lower
  28. | |-- in_both.txt
  29. | `-- in_lower.txt
  30. |-- merged
  31. | |-- in_both.txt
  32. | |-- in_lower.txt
  33. | `-- in_upper.txt
  34. |-- upper
  35. | |-- in_both.txt
  36. | `-- in_upper.txt
  37. `-- work
  38. `-- work
  39. #可以看到merged中包含了lower和upper中的文件
  40. #然后我查看merge中的in_both文件,验证了上层目录覆盖下层的结论
  41. # cat merged/in_both.txt
  42. upper
  43. #上面我们验证了挂载后overlayfs的读,接下来我们去验证下写
  44. #我们在merged中创建一个新文件,并查看
  45. # touch merged/new_file
  46. # tree .
  47. .
  48. |-- lower
  49. | |-- in_both.txt
  50. | `-- in_lower.txt
  51. |-- merged
  52. | |-- in_both.txt
  53. | |-- in_lower.txt
  54. | |-- in_upper.txt
  55. | `-- new_file
  56. |-- upper
  57. | |-- in_both.txt
  58. | |-- in_upper.txt
  59. | `-- new_file
  60. `-- work
  61. `-- work
  62. #可以看到新文件实际是放在了upper目录中
  63. #下面我们看下如果删除了lower和upper中都有的文件会怎样
  64. # rm -f merged/in_both.txt
  65. # tree .
  66. .
  67. |-- lower
  68. | |-- in_both.txt
  69. | `-- in_lower.txt
  70. |-- merged
  71. | |-- in_lower.txt
  72. | |-- in_upper.txt
  73. | `-- new_file
  74. |-- upper
  75. | |-- in_both.txt
  76. | |-- in_upper.txt
  77. | `-- new_file
  78. `-- work
  79. `-- work
  80. #从文件目录上看只有merge中没有了in_both文件,但是upper中的文件已经发生了变化
  81. # ll upper/in_both.txt
  82. c--------- 1 root root 0, 0 Jan 21 19:33 upper/in_both.txt
  83. #upper/in_both.txt已经变成了一个空的字符文件,且覆盖了lower层的内容

三 、Bocker

3.1 功能演示

第二部分中我们对Namespace,cgroup,overlayfs有了一定的了解,接下来我们通过一个脚本来实现个建议的Docker。脚本源自于https://github.com/p8952/bocker,我做了image/pull/存储驱动的部分修改,下面先看下脚本完成后的示例:

🐋1 Docker 打包成镜像及Docker部分源码分析 - 图5

3.2 完整脚本

脚本一共用130行代码,完成了上面的功能,也算符合我们此次的标题了。为了大家可以更深入的理解脚本内容,这里就不再对脚本进行拆分讲解,以下是完整脚本。

  1. #!/usr/bin/env bash
  2. set -o errexit -o nounset -o pipefail; shopt -s nullglob
  3. overlay_path='/var/lib/bocker/overlay' && container_path='/var/lib/bocker/containers' && cgroups='cpu,cpuacct,memory';
  4. [[ $# -gt 0 ]] && while [ "${1:0:2}" == '--' ]; do OPTION=${1:2}; [[ $OPTION =~ = ]] && declare "BOCKER_${OPTION/=*/}=${OPTION/*=/}" || declare "BOCKER_${OPTION}=x"; shift; done
  5. function bocker_check() {
  6. case ${1:0:3} in
  7. img) ls "$overlay_path" | grep -qw "$1" && echo 0 || echo 1;;
  8. ps_) ls "$container_path" | grep -qw "$1" && echo 2 || echo 3;;
  9. esac
  10. }
  11. function bocker_init() { #HELP Create an image from a directory:\nBOCKER init <directory>
  12. uuid="img_$(shuf -i 42002-42254 -n 1)"
  13. if [[ -d "$1" ]]; then
  14. [[ "$(bocker_check "$uuid")" == 0 ]] && bocker_run "$@"
  15. mkdir "$overlay_path/$uuid" > /dev/null
  16. cp -rf --reflink=auto "$1"/* "$overlay_path/$uuid" > /dev/null
  17. [[ ! -f "$overlay_path/$uuid"/img.source ]] && echo "$1" > "$overlay_path/$uuid"/img.source
  18. [[ ! -d "$overlay_path/$uuid"/proc ]] && mkdir "$overlay_path/$uuid"/proc
  19. echo "Created: $uuid"
  20. else
  21. echo "No directory named '$1' exists"
  22. fi
  23. }
  24. function bocker_pull() { #HELP Pull an image from Docker Hub:\nBOCKER pull <name> <tag>
  25. tmp_uuid="$(uuidgen)" && mkdir /tmp/"$tmp_uuid"
  26. download-frozen-image-v2 /tmp/"$tmp_uuid" "$1:$2" > /dev/null
  27. rm -rf /tmp/"$tmp_uuid"/repositories
  28. for tar in $(jq '.[].Layers[]' --raw-output < /tmp/$tmp_uuid/manifest.json); do
  29. tar xf /tmp/$tmp_uuid/$tar -C /tmp/$tmp_uuid && rm -rf /tmp/$tmp_uuid/$tar
  30. done
  31. for config in $(jq '.[].Config' --raw-output < /tmp/$tmp_uuid/manifest.json); do
  32. rm -f /tmp/$tmp_uuid/$config
  33. done
  34. echo "$1:$2" > /tmp/$tmp_uuid/img.source
  35. bocker_init /tmp/$tmp_uuid && rm -rf /tmp/$tmp_uuid
  36. }
  37. function bocker_rm() { #HELP Delete an image or container:\nBOCKER rm <image_id or container_id>
  38. [[ "$(bocker_check "$1")" == 3 ]] && echo "No container named '$1' exists" && exit 1
  39. [[ "$(bocker_check "$1")" == 1 ]] && echo "No image named '$1' exists" && exit 1
  40. if [[ -d "$overlay_path/$1" ]];then
  41. rm -rf "$overlay_path/$1" && echo "Removed: $1"
  42. else
  43. umount "$container_path/$1"/merged && rm -rf "$container_path/$1" && ip netns del netns_"$1" && ip link del dev veth0_"$1" && echo "Removed: $1"
  44. cgdelete -g "$cgroups:/$1" &> /dev/null
  45. fi
  46. }
  47. function bocker_images() { #HELP List images:\nBOCKER images
  48. echo -e "IMAGE_ID\t\tSOURCE"
  49. for img in "$overlay_path"/img_*; do
  50. img=$(basename "$img")
  51. echo -e "$img\t\t$(cat "$overlay_path/$img/img.source")"
  52. done
  53. }
  54. function bocker_ps() { #HELP List containers:\nBOCKER ps
  55. echo -e "CONTAINER_ID\t\tCOMMAND"
  56. for ps in "$container_path"/ps_*; do
  57. ps=$(basename "$ps")
  58. echo -e "$ps\t\t$(cat "$container_path/$ps/$ps.cmd")"
  59. done
  60. }
  61. function bocker_run() { #HELP Create a container:\nBOCKER run <image_id> <command>
  62. uuid="ps_$(shuf -i 42002-42254 -n 1)"
  63. [[ "$(bocker_check "$1")" == 1 ]] && echo "No image named '$1' exists" && exit 1
  64. [[ "$(bocker_check "$uuid")" == 2 ]] && echo "UUID conflict, retrying..." && bocker_run "$@" && return
  65. cmd="${@:2}" && ip="$(echo "${uuid: -3}" | sed 's/0//g')" && mac="${uuid: -3:1}:${uuid: -2}"
  66. ip link add dev veth0_"$uuid" type veth peer name veth1_"$uuid"
  67. ip link set dev veth0_"$uuid" up
  68. ip link set veth0_"$uuid" master br1
  69. ip netns add netns_"$uuid"
  70. ip link set veth1_"$uuid" netns netns_"$uuid"
  71. ip netns exec netns_"$uuid" ip link set dev lo up
  72. ip netns exec netns_"$uuid" ip link set veth1_"$uuid" address 02:42:ac:11:00"$mac"
  73. ip netns exec netns_"$uuid" ip addr add 172.18.0."$ip"/24 dev veth1_"$uuid"
  74. ip netns exec netns_"$uuid" ip link set dev veth1_"$uuid" up
  75. ip netns exec netns_"$uuid" ip route add default via 172.18.0.1
  76. mkdir -p "$container_path/$uuid"/{lower,upper,work,merged} && cp -rf --reflink=auto "$overlay_path/$1"/* "$container_path/$uuid"/lower > /dev/null && \
  77. mount -t overlay overlay \
  78. -o lowerdir="$container_path/$uuid"/lower,upperdir="$container_path/$uuid"/upper,workdir="$container_path/$uuid"/work \
  79. "$container_path/$uuid"/merged
  80. echo 'nameserver 114.114.114.114' > "$container_path/$uuid"/merged/etc/resolv.conf
  81. echo "$cmd" > "$container_path/$uuid/$uuid.cmd"
  82. cgcreate -g "$cgroups:/$uuid"
  83. : "${BOCKER_CPU_SHARE:=512}" && cgset -r cpu.shares="$BOCKER_CPU_SHARE" "$uuid"
  84. : "${BOCKER_MEM_LIMIT:=512}" && cgset -r memory.limit_in_bytes="$((BOCKER_MEM_LIMIT * 1000000))" "$uuid"
  85. cgexec -g "$cgroups:$uuid" \
  86. ip netns exec netns_"$uuid" \
  87. unshare -fmuip --mount-proc \
  88. chroot "$container_path/$uuid"/merged \
  89. /bin/sh -c "/bin/mount -t proc proc /proc && $cmd" \
  90. 2>&1 | tee "$container_path/$uuid/$uuid.log" || true
  91. ip link del dev veth0_"$uuid"
  92. ip netns del netns_"$uuid"
  93. }
  94. function bocker_exec() { #HELP Execute a command in a running container:\nBOCKER exec <container_id> <command>
  95. [[ "$(bocker_check "$1")" == 3 ]] && echo "No container named '$1' exists" && exit 1
  96. cid="$(ps o ppid,pid | grep "^$(ps o pid,cmd | grep -E "^\ *[0-9]+ unshare.*$1" | awk '{print $1}')" | awk '{print $2}')"
  97. [[ ! "$cid" =~ ^\ *[0-9]+$ ]] && echo "Container '$1' exists but is not running" && exit 1
  98. nsenter -t "$cid" -m -u -i -n -p chroot "$container_path/$1"/merged "${@:2}"
  99. }
  100. function bocker_logs() { #HELP View logs from a container:\nBOCKER logs <container_id>
  101. [[ "$(bocker_check "$1")" == 3 ]] && echo "No container named '$1' exists" && exit 1
  102. cat "$container_path/$1/$1.log"
  103. }
  104. function bocker_commit() { #HELP Commit a container to an image:\nBOCKER commit <container_id> <image_id>
  105. [[ "$(bocker_check "$1")" == 3 ]] && echo "No container named '$1' exists" && exit 1
  106. [[ "$(bocker_check "$2")" == 0 ]] && echo "Image named '$2' exists" && exit 1
  107. mkdir "$overlay_path/$2" && cp -rf --reflink=auto "$container_path/$1"/merged/* "$overlay_path/$2" && sed -i "s/:.*$/:$(date +%Y%m%d-%H%M%S)/g" "$overlay_path/$2"/img.source
  108. echo "Created: $2"
  109. }
  110. function bocker_help() { #HELP Display this message:\nBOCKER help
  111. sed -n "s/^.*#HELP\\s//p;" < "$1" | sed "s/\\\\n/\n\t/g;s/$/\n/;s!BOCKER!${1/!/\\!}!g"
  112. }
  113. [[ -z "${1-}" ]] && bocker_help "$0" && exit 1
  114. case $1 in
  115. pull|init|rm|images|ps|run|exec|logs|commit) bocker_"$1" "${@:2}" ;;
  116. *) bocker_help "$0" ;;
  117. esac

README

:::color2 Bocker

使用100行bash实现一个docker,本脚本是依据bocker实现,更换了存储驱动,完善了pull等功能。

前置条件

为了脚本能够正常运行,机器上需要具备以下组件:

  • overlayfs
  • iproute2
  • iptables
  • libcgroup-tools
  • util-linux >= 2.25.2
  • coreutils >= 7.5

大部分功能在centos7上都是满足的,overlayfs可以通过modprobe overlay挂载。

另外你可能还要做以下设置:

  • 创建bocker运行目录 /var/lib/bocker/overlay,/var/lib/bocker/containers
  • 创建一个IP地址为 172.18.0.1/24 的桥接网卡 br1
  • 确认开启IP转发 /proc/sys/net/ipv4/ip_forward = 1
  • 创建iptables规则将桥接网络流量转发至物理网卡,示例:iptables -t nat -A POSTROUTING -s 172.18.0.0/24 -o eth0 -j MASQUERADE

实现的功能

  • docker build +
  • docker pull
  • docker images
  • docker ps
  • docker run
  • docker exec
  • docker logs
  • docker commit
  • docker rm / docker rmi
  • Networking
  • Quota Support / CGroups

+bocker init 提供了有限的 bocker build 能力

:::

四、总结

到此本文要介绍的内容就结束了,正如开篇我们提到的,写出最终的脚本实现这样一个小玩意并没有什么实用价值,真正的价值是我们通过100行左右的脚本,以交互式的方式去理解Docker的核心技术点。在工作中与容器打交道时能有更多的思路去排查、解决问题。