-

17 原理实践:自己动手使用 Golang 开发 Docker(上)

第一模块,我们从 Docker 基础概念讲到 Docker 的基本操作。第二模块,我们详细剖析了 Docker 的三大关键技术( Namespace、cgroups 和联合文件系统)的实现原理,并且讲解了 Docker 的网络模型等关键性技术。相信此时的你已经对 Docker 有了一个新的认识。

接下来的两课时,我就趁热打铁,带你动手使用 Golang 编写一个 Docker。学习这两节的内容需要你能够熟练使用 Golang 语言,如果你没有 Golang 编程基础,建议先学习一下 Golang 的基本语法。那么 Golang 究竟是什么呢? Golang 应该如何安装使用?下面我带你一一学习。

Golang 是什么?

Golang 又称为 Go,是 Google 开源的一种静态编译型语言,Golang 自带内存管理机制,相比于 C 和 C++ 语言,我们不需要关心内存的分配和回收。

Golang 是新一代的互联网编程语言,在 Golang 诞生前,C 或 C++ 作为服务端高性能编程语言,使用 C 或 C++ 开发的业务具有非常高的执行效率,但是编译和开发效率却不尽人意,Java、.NET 等语言的诞生大大提高了软件开发速度,但是运行效率和资源占用却不如 C 和 C++。

这时 Golang 横空出世,由于 Golang 较高的开发效率和执行效率,很快便从众多编程语言中脱颖而出,成为众多互联网公司的新宠儿。滴滴、知乎、阿里等众多大型互联网公司都在大量使用 Golang。 同时,Docker 和 Kubernetes 等众多明星项目也都是使用 Golang 开发的。因此,熟练掌握 Golang 将会为你加分很多。

这么好的编程语言,你是不是已经迫不及待地想要安装体验一下了?别着急,下面我带你来安装一个 Golang 环境。

Golang 安装

安装信息如下:

  • CentOS 7系统
  • Golang 版本 1.15.2

首先我们到Golang 官网(由于国内无法访问 Golang 官网,推荐到Golang 中文网下载安装包)下载一个对应操作系统的安装包。

$ cd /tmp && wget https://studygolang.com/dl/golang/go1.15.2.linux-amd64.tar.gz

解压缩安装包:

$ sudo tar -C /usr/local -xzf go1.15.2.linux-amd64.tar.gz

在 $HOME/.bashrc 文件末尾添加以下内容,将 Golang 可执行文件目录添加到系统 PATH 中:

export PATH=$PATH:/usr/local/go/bin

将 go 的安装路径添加到系统 PATH 中后,就可以在命令行直接使用 go 命令了。配置好 go 命令后,我们还需要配置 GOPATH 才能正确存放和编译我们的 go 代码。

配置 GOPATH

GOPATH 是 Golang 的源码和相关编译文件的存放路径,GOPATH 路径下有三个文件夹 src、pkg 和 bin,它们的用途分别是:

目录 用途
src 源代码存放路径或者引用的外部库
pkg 编译时生成的对象文件
bin 编译后的可执行二进制

这里我们开始配置 GOPATH 路径为 /go。首先准备相关的目录:

$ sudo mkdir /go

$ sudo mkdir /go/src

$ sudo mkdir /go/pkg

$ sudo mkdir /go/bin

然后将 GOPATH 添加到 $HOME/.bashrc 文件末尾,并且把 GOPATH 下的 bin 目录也添加到系统的 PATH 中,这样方便程序编译后直接使用。添加的内容如下:

export GOPATH=/go

export PATH=$PATH:$GOPATH/bin

# 设置 Golang 的代理,方便我们顺利下载依赖包

export GOPROXY="https://goproxy.io,direct"

接下来,使用 source $HOME/.bashrc 命令生效一下我们的配置,然后我们再使用 go env 命令查看一下我们的配置结果:

$ go env

GO111MODULE=""

GOARCH="amd64"

GOBIN=""

GOCACHE="/root/.cache/go-build"

GOENV="/root/.config/go/env"

GOEXE=""

GOFLAGS=""

GOHOSTARCH="amd64"

GOHOSTOS="linux"

GOINSECURE=""

GOMODCACHE="/go/pkg/mod"

GONOPROXY=""

GONOSUMDB=""

GOOS="linux"

GOPATH="/go"

GOPRIVATE=""

GOPROXY="https://goproxy.io,direct"

GOROOT="/usr/local/go"

GOSUMDB="sum.golang.org"

GOTMPDIR=""

GOTOOLDIR="/usr/local/go/pkg/tool/linux_amd64"

GCCGO="gccgo"

AR="ar"

CC="gcc"

CXX="g++"

CGO_ENABLED="1"

GOMOD=""

CGO_CFLAGS="-g -O2"

CGO_CPPFLAGS=""

CGO_CXXFLAGS="-g -O2"

CGO_FFLAGS="-g -O2"

CGO_LDFLAGS="-g -O2"

PKG_CONFIG="pkg-config"

GOGCCFLAGS="-fPIC -m64 -pthread -fmessage-length=0 -fdebug-prefix-map=/tmp/go-build352828668=/tmp/go-build -gno-record-gcc-switches"

从 GOPATH 和 GOPROXY 两个变量的结果,可以看到 GOPATH 和 GOPROXY 均已经生效。到此,我们的 Golang 已经安装完毕。下面,我们就开始真正的 Docker 编写之旅吧。

编写 Docker

在开始编写 Docker 之前,我先介绍几个基础知识,如果你对这些基础知识已经很熟悉了,可以直接跳过这块的基础知识。

Linux Proc 文件系统

Linux 系统中,/proc 目录是一种“文件系统”,这里我用了引号,其实 /proc 目录并不是一个真正的文件系统。/proc 目录存放于内存中,是一个虚拟的文件系统,该目录存放了当前内核运行状态的一系列特殊的文件,你可以通过这些文件查看当前的进程信息。

下面,我们通过 ls 命令查看一下 /proc 目录下的内容:

$ sudo ls -l /proc

total 0

dr-xr-xr-x  9 root    root                  0 Sep 19 21:34 1

dr-xr-xr-x  9 root    root                  0 Sep 19 21:34 30097

...省略部分输出

dr-xr-xr-x  9 root    root                  0 Sep 19 21:34 8

dr-xr-xr-x  9 root    root                  0 Sep 19 21:34 9

dr-xr-xr-x  9 root    root                  0 Sep 19 21:34 97

dr-xr-xr-x  2 root    root                  0 Sep 19 22:27 acpi

-r--r--r--  1 root    root                  0 Sep 19 22:27 buddyinfo

dr-xr-xr-x  4 root    root                  0 Sep 19 22:27 bus

-r--r--r--  1 root    root                  0 Sep 19 22:27 cgroups

-r--r--r--  1 root    root                  0 Sep 19 22:27 cmdline

-r--r--r--  1 root    root                  0 Sep 19 22:27 consoles

-r--r--r--  1 root    root                  0 Sep 19 22:27 cpuinfo

-r--r--r--  1 root    root                  0 Sep 19 22:27 crypto

-r--r--r--  1 root    root                  0 Sep 19 22:27 devices

-r--r--r--  1 root    root                  0 Sep 19 21:34 diskstats

-r--r--r--  1 root    root                  0 Sep 19 22:27 dma

dr-xr-xr-x  2 root    root                  0 Sep 19 22:27 driver

-r--r--r--  1 root    root                  0 Sep 19 22:27 execdomains

-r--r--r--  1 root    root                  0 Sep 19 22:27 fb

-r--r--r--  1 root    root                  0 Sep 19 22:27 filesystems

dr-xr-xr-x  5 root    root                  0 Sep 19 22:27 fs

-r--r--r--  1 root    root                  0 Sep 19 22:27 interrupts

-r--r--r--  1 root    root                  0 Sep 19 22:27 iomem

-r--r--r--  1 root    root                  0 Sep 19 22:27 ioports

dr-xr-xr-x 27 root    root                  0 Sep 19 22:27 irq

-r--r--r--  1 root    root                  0 Sep 19 22:27 kallsyms

-r--------  1 root    root    140737486266368 Sep 19 22:27 kcore

-r--r--r--  1 root    root                  0 Sep 19 22:27 key-users

-r--r--r--  1 root    root                  0 Sep 19 22:27 keys

-r--------  1 root    root                  0 Sep 19 22:27 kmsg

-r--------  1 root    root                  0 Sep 19 22:27 kpagecount

-r--------  1 root    root                  0 Sep 19 22:27 kpageflags

-r--r--r--  1 root    root                  0 Sep 19 22:27 loadavg

-r--r--r--  1 root    root                  0 Sep 19 22:27 locks

-r--r--r--  1 root    root                  0 Sep 19 22:27 mdstat

-r--r--r--  1 root    root                  0 Sep 19 22:27 meminfo

-r--r--r--  1 root    root                  0 Sep 19 22:27 misc

-r--r--r--  1 root    root                  0 Sep 19 22:27 modules

lrwxrwxrwx  1 root    root                 11 Sep 19 22:27 mounts -> self/mounts

-rw-r--r--  1 root    root                  0 Sep 19 22:27 mtrr

lrwxrwxrwx  1 root    root                  8 Sep 19 22:27 net -> self/net

-r--r--r--  1 root    root                  0 Sep 19 22:27 pagetypeinfo

-r--r--r--  1 root    root                  0 Sep 19 22:27 partitions

-r--r--r--  1 root    root                  0 Sep 19 22:27 sched_debug

-r--r--r--  1 root    root                  0 Sep 19 22:27 schedstat

dr-xr-xr-x  2 root    root                  0 Sep 19 22:27 scsi

lrwxrwxrwx  1 root    root                  0 Sep 19 21:34 self -> 30097

-r--------  1 root    root                  0 Sep 19 22:27 slabinfo

-r--r--r--  1 root    root                  0 Sep 19 22:27 softirqs

-r--r--r--  1 root    root                  0 Sep 19 21:34 stat

-r--r--r--  1 root    root                  0 Sep 19 21:34 swaps

dr-xr-xr-x  1 root    root                  0 Sep 19 21:34 sys

--w-------  1 root    root                  0 Sep 19 22:27 sysrq-trigger

dr-xr-xr-x  2 root    root                  0 Sep 19 22:27 sysvipc

-r--r--r--  1 root    root                  0 Sep 19 22:27 timer_list

-rw-r--r--  1 root    root                  0 Sep 19 22:27 timer_stats

dr-xr-xr-x  4 root    root                  0 Sep 19 22:27 tty

-r--r--r--  1 root    root                  0 Sep 19 22:27 uptime

-r--r--r--  1 root    root                  0 Sep 19 22:27 version

-r--------  1 root    root                  0 Sep 19 22:27 vmallocinfo

-r--r--r--  1 root    root                  0 Sep 19 22:27 vmstat

-r--r--r--  1 root    root                  0 Sep 19 22:27 zoneinfo

可以看到,这个目录下有很多数字,这些数字目录实际上是以进程 ID 命名的。除了这些以进程 ID 命名的目录,还有一些特殊的目录,这里我讲解一下与我们编写 Docker 有关的文件和目录。

  • self 目录:它是连接到当前正在运行的进程目录,比如我当前的进程 ID 为 30097,则 self 目录实际连接到 /proc/30097 这个目录。
  • /proc/{PID}/exe 文件:exe 连接到进程执行的命令文件,例如 30097 这个进程的运行命令为 docker,则执行 /proc/30097/exe ps 等同于执行 docker ps。

好了,了解完这些基础知识后,我们就开始行动吧!因为我们的精简版 Docker 是使用 Golang 编写,这里就给我们编写的 Docker 命名为 gocker 吧。

实现 gocker 的 run 命令

通过前面的章节,我们学习了要运行一个容器,必须先有镜像。这里我们首先准备一个 busybox 镜像,以便我们运行 gocker 容器。

$ mkdir /tmp/busybox && cd /tmp/busybox

$ docker export $(docker create busybox) -o busybox.tar

$ tar -xf busybox.tar

以上是我们在 /tmp/busybox 目录,使用 docker export 命令导出的一个 busybox 镜像文件,然后对镜像文件包进行解压,解压后 /tmp/busybox 目录内容如下:

$ ls -l /tmp/busybox/

total 1472

drwxr-xr-x 2 root      root        12288 Sep  9 02:09 bin

-rw------- 1 root      root      1455104 Sep 19 22:47 busybox.tar

drwxr-xr-x 4 root      root         4096 Sep 19 16:41 dev

drwxr-xr-x 3 root      root         4096 Sep 19 16:41 etc

drwxr-xr-x 2 nfsnobody nfsnobody    4096 Sep  9 02:09 home

drwxr-xr-x 2 root      root         4096 Sep 19 16:41 proc

drwx------ 2 root      root         4096 Sep 19 21:07 root

drwxr-xr-x 2 root      root         4096 Sep 19 16:41 sys

drwxrwxrwt 2 root      root         4096 Sep  9 02:09 tmp

drwxr-xr-x 3 root      root         4096 Sep  9 02:09 usr

drwxr-xr-x 4 root      root         4096 Sep  9 02:09 var

准备好镜像文件后,把我为你准备好的 gocker 代码下载下来吧,这里我使用手动下载源码的方式克隆代码:

$ mkdir -p /go/src/github.com/wilhelmguo

$ cd /go/src/github.com/wilhelmguo && git clone https://github.com/wilhelmguo/gocker.git

$ cd gocker

$ git checkout lesson-17

我的 GOPATH 在 /go 目录下,如果你的 GOPATH 跟我不一致,请根据 GOPATH 存放和编译源码。本课时的源码存放在这里,你也可以在线阅读。

代码下载完后,我们进入 gocker 的目录,查看下源码文件:

$ tree .

.

|-- go.mod

|-- go.sum

|-- main.go

|-- README.md

|-- runc

|   `-- run.go

`-- vendor

... 省略 vendor 目录结构

15 directories, 59 files

本项目使用 go mod 管理包依赖,go mod 是在 golang 1.11 版本加入的新的特性,是用来管理包的依赖的,也是目前官方的包依赖管理工具。如果你想学习更多个 go mod 使用方法,可以参考官网

可以看到该源码下有两个主要文件:一个是 main.go 文件,这是 gocker 的主入口函数;另外一个是 run.go ,这个文件是 gocker run 命令的具体实现。

下面我们使用 go install 命令来编译一下我们的 gocker 项目:

$ go install

执行完 go install 后, Golang 会自动帮助我们编译当前项目下的代码,编译后的二进制文件存放在 $GOPATH/bin 目录下。由于我们之前在 $HOME/.bashrc 文件下把 $GOPATH/bin 放入了系统 PATH 中,所以此时你可以直接使用 gocker 命令了。 接下来我们使用 gocker 来启动一个容器:

# gocker run -it -rootfs=/tmp/busybox /bin/sh

2020/09/19 23:46:27 Current path is  /tmp/busybox

2020/09/19 23:46:27 CmdArray is  [/bin/sh]

/ #

如果出现 pivotRoot error pivot_root invalid argument 的报错,可以先执行 unshare -m 命令,然后使用 rm -rf /tmp/busybox/.pivot_root 命令删除临时文件,再次重试即可。

这里我们使用 it 参数指定以命令行交互的模式启动容器,rootfs 指定准备好的镜像目录。执行完上面的命令后 busybox 容器就成功启动了。 这时候,我们使用 ps 命令查看一下当前进程信息:

/ # /bin/ps -ef

PID   USER     TIME  COMMAND

    1 root      0:00 /bin/sh

    5 root      0:00 /bin/ps -ef

此时,容器内的进程已经与主机完全隔离。 我们再查看一下当前目录下的内容:

/ # pwd

/ # /bin/ls -l

total 1468

drwxr-xr-x    2 root     root         12288 Sep  8 18:09 bin

-rw-------    1 root     root       1455104 Sep 19 14:47 busybox.tar

drwxr-xr-x    4 root     root          4096 Sep 19 08:41 dev

drwxr-xr-x    3 root     root          4096 Sep 19 08:41 etc

drwxr-xr-x    2 nobody   nobody        4096 Sep  8 18:09 home

dr-xr-xr-x  122 root     root             0 Sep 19 15:46 proc

drwx------    2 root     root          4096 Sep 19 13:07 root

drwxr-xr-x    2 root     root          4096 Sep 19 08:41 sys

drwxrwxrwt    2 root     root          4096 Sep  8 18:09 tmp

drwxr-xr-x    3 root     root          4096 Sep  8 18:09 usr

drwxr-xr-x    4 root     root          4096 Sep  8 18:09 var

可以看到当前目录已经为根目录,并且根目录下的文件就是我们上面准备的 busybox 镜像文件。 到此,一个完全由我们自己编写的 gocker 已经可以启动容器了。

结语

本课时我们讲解了 Golang 是什么, 并且配置好了 Golang 环境,编译了 gocker,也了解了 Linux /proc 文件系统的一些重要功能,最后使用 gocker 成功启动了一个 busybox 容器。

那么你知道,为什么 Docker 会选择使用 Golang 来开发吗?思考后,把你的想法写在留言区。

下一课时我将为你全面剖析 gocker 的源码以及它的实现原理,让你能够自己动手把它写出来,到时见。