1.容器核心技术和Docker的创新:镜像技术

容器技术主要包括CgroupNamesapce这两个内核级别的特性。 Cgroup是control group的缩写即控制组,主要用来做资源控制,可以将一组进程放到一个控制组里,通过给这个控制组分配可用资源,达到控制这组进程可用资源的目的,对不同资源的具体管理是由各个子系统分工完成的,例如cpu控制cpu使用率,memory控制内存的使用上限等等。 Namespace即命名空间,主要用来做访问隔离,针对一类资源进行抽象封装给一个容器使用,对于这类资源,每个容器都有自己的抽象,它们之间彼此不可见实现了访问隔离,例如PID namespace用来隔离进程号,User namespace用来隔离用户和用户组,linux共有6种命名空间。 另外,容器还需要rootfs实现文件系统隔离。rootfs是一个容器启动时其内部进程可见的文件系统,在容器内用户视角下修改文件时,一般会采用Copy-On-Write(COW)机制。镜像的本质就是一个rootfs。

从某种程度上来说容器 = Cgroup + Namespace + rootfs + 容器引擎,但Cgroup、Namespace这些特性都是linux中早就存在的技术,因此有人说Docker是"新瓶装旧酒",在Docker之前也有其他使用这些技术实现类似容器功能的实践,例如当年号称第一个开源PaaS平台的Cloud Foundry在将其固定格式应用宝部署到虚拟机后,也使用了Linux提供的Namespace和Cgroup技术对不同应用进行隔离和资源限制。而Docker当年却是凭着Docker镜像这个创新脱颖而出,可以说镜像技术是Docker成功的最重要之一。

前面我们已经使用nerdctl+containerd实现了在容器管理方面像直接使用docker一样的体验,本节我们更近一般学习nerdctl+buildkitd这对组合,实现镜像构建和镜像管理的功能。

2.buildkit简介

buildkit项目也是Docker公司的人开源出来的一个构建工具包,支持OCI标准的镜像构建。它主要包含以下部分:

  • 服务端buildkitd,当前支持runc和containerd作为worker,默认是runc,我们这里使用containerd
  • 客户端buildctl,负责解析Dockerfile,并向服务端buildkitd发出构建请求

可以看出buildkit是典型的C/S架构,client和server可以不在一台服务器上。而nerdctl在构建镜像方面也可以作为buildkitd的客户端,我们这里使用nerdctl。

3.部署buildkitd

下面在测试服务器上部署buildkitd。下载完成后,仍然将buildctl和buildkitd安装到/usr/local/containerd目录中:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
wget https://github.com/moby/buildkit/releases/download/v0.8.3/buildkit-v0.8.3.linux-amd64.tar.gz
tar -zxvf buildkit-v0.8.3.linux-amd64.tar.gz -C /usr/local/containerd/

tree /usr/local/containerd/bin/ | grep build
├── buildctl
├── buildkitd
├── buildkit-qemu-aarch64
├── buildkit-qemu-arm
├── buildkit-qemu-i386
├── buildkit-qemu-ppc64le
├── buildkit-qemu-riscv64
├── buildkit-qemu-s390x
├── buildkit-runc

ln -s /usr/local/containerd/bin/buildkitd /usr/local/bin/buildkitd
ln -s /usr/local/containerd/bin/buildctl /usr/local/bin/buildctl

创建systemd服务相关文件/etc/systemd/system/buildkit.socket:

1
2
3
4
5
6
7
8
9
[Unit]
Description=BuildKit
Documentation=https://github.com/moby/buildkit

[Socket]
ListenStream=%t/buildkit/buildkitd.sock

[Install]
WantedBy=sockets.target

/etc/systemd/system/buildkit.service:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
[Unit]
Description=BuildKit
Requires=buildkit.socket
After=buildkit.socketDocumentation=https://github.com/moby/buildkit

[Service]
ExecStart=/usr/local/bin/buildkitd --oci-worker=false --containerd-worker=true

[Install]
WantedBy=multi-user.target

启动buildkitd:

1
2
3
systemctl daemon-reload
systemctl enable buildkit
systemctl start buildkit

4.构建alpine和busybox基础镜像

alpine linux的核心特点是体积小、简单和安全,因此很多人选择alpine镜像作为基础镜像。 我们编写一个Dockerfile在docker官方alpine镜像的基础上,安装glibc,同时配置中国时区:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
FROM alpine:3.13.5

# Here we install GNU libc (aka glibc) and set C.UTF-8 locale as default.
ENV LANG=C.UTF-8
RUN echo "**** install packages ****" && \
    sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories &&\
    ALPINE_GLIBC_BASE_URL="https://github.com/sgerrand/alpine-pkg-glibc/releases/download" && \
    ALPINE_GLIBC_PACKAGE_VERSION="2.33-r0" && \
    ALPINE_GLIBC_BASE_PACKAGE_FILENAME="glibc-$ALPINE_GLIBC_PACKAGE_VERSION.apk" && \
    ALPINE_GLIBC_BIN_PACKAGE_FILENAME="glibc-bin-$ALPINE_GLIBC_PACKAGE_VERSION.apk" && \
    ALPINE_GLIBC_I18N_PACKAGE_FILENAME="glibc-i18n-$ALPINE_GLIBC_PACKAGE_VERSION.apk" && \
    apk add --no-cache --virtual=.build-dependencies wget ca-certificates && \
    echo \
        "-----BEGIN PUBLIC KEY-----\
        MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEApZ2u1KJKUu/fW4A25y9m\
        y70AGEa/J3Wi5ibNVGNn1gT1r0VfgeWd0pUybS4UmcHdiNzxJPgoWQhV2SSW1JYu\
        tOqKZF5QSN6X937PTUpNBjUvLtTQ1ve1fp39uf/lEXPpFpOPL88LKnDBgbh7wkCp\
        m2KzLVGChf83MS0ShL6G9EQIAUxLm99VpgRjwqTQ/KfzGtpke1wqws4au0Ab4qPY\
        KXvMLSPLUp7cfulWvhmZSegr5AdhNw5KNizPqCJT8ZrGvgHypXyiFvvAH5YRtSsc\
        Zvo9GI2e2MaZyo9/lvb+LbLEJZKEQckqRj4P26gmASrZEPStwc+yqy1ShHLA0j6m\
        1QIDAQAB\
        -----END PUBLIC KEY-----" | sed 's/   */\n/g' > "/etc/apk/keys/sgerrand.rsa.pub" && \
    wget \
        "$ALPINE_GLIBC_BASE_URL/$ALPINE_GLIBC_PACKAGE_VERSION/$ALPINE_GLIBC_BASE_PACKAGE_FILENAME" \
        "$ALPINE_GLIBC_BASE_URL/$ALPINE_GLIBC_PACKAGE_VERSION/$ALPINE_GLIBC_BIN_PACKAGE_FILENAME" \
        "$ALPINE_GLIBC_BASE_URL/$ALPINE_GLIBC_PACKAGE_VERSION/$ALPINE_GLIBC_I18N_PACKAGE_FILENAME" && \
    apk add --no-cache \
        "$ALPINE_GLIBC_BASE_PACKAGE_FILENAME" \
        "$ALPINE_GLIBC_BIN_PACKAGE_FILENAME" \
        "$ALPINE_GLIBC_I18N_PACKAGE_FILENAME" && \
    \
    rm "/etc/apk/keys/sgerrand.rsa.pub" && \
    /usr/glibc-compat/bin/localedef --force --inputfile POSIX --charmap UTF-8 "$LANG" || true && \
    echo "export LANG=$LANG" > /etc/profile.d/locale.sh && \
    \
    apk del glibc-i18n && \
    \
    rm "/root/.wget-hsts" && \
    apk del .build-dependencies && \
    rm \
        "$ALPINE_GLIBC_BASE_PACKAGE_FILENAME" \
        "$ALPINE_GLIBC_BIN_PACKAGE_FILENAME" \
        "$ALPINE_GLIBC_I18N_PACKAGE_FILENAME"


RUN apk update --no-cache && apk add ca-certificates --no-cache && \
    apk add tzdata --no-cache && \
    ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && \
    echo "Asia/Shanghai" > /etc/timezone

构建alpine基础镜像:

1
2
3
4
5
6
7
nerdctl build -t base-alpine .

nerdctl images
WARN[0000] unparsable image name "overlayfs@sha256:8628f76ed129c628eccb2259cf61a3138adac73247565f04fb00ea79fab6ae28"
REPOSITORY     TAG                  IMAGE ID        CREATED          SIZE
base-alpine    latest               8628f76ed129    3 minutes ago    8.8 MiB
                                    8628f76ed129    15 hours ago     8.8 MiB

构建镜像成功后,查看镜像时,报了一个警告WARN[0000] unparsable image namenerdctl images除了列出我们构建的base-alpine:latest镜像外,还有一个tag为空的重复镜像, nerdctl的github上有人提了issues 177,官方还没有回应是buildkit的问题还是nerdctl的问题,但这并不影响我们对base-alpine这个基础镜像的使用。

接下来构建busybox基础镜像,busybox是一个集成了一百多个最常用的Linux命令和工具的软件工具箱,它在单一的可执行文件中提供了精简的工具集,有人把busybox作为linux系统的瑞士军刀, 这里编写一个Dockerfile在docker官方busybox镜像的基础上:

1
2
3
4
FROM busybox:1.33.1-glibc

COPY zoneinfo /usr/share/zoneinfo/
RUN ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && echo 'Asia/Shanghai' >/etc/timezone

构建busybox基础镜像:

1
2
cp -R /usr/share/zoneinfo .
nerdctl build -t base-busybox .

将前面构建的两个基础镜像推送到镜像仓库:

1
2
3
4
5
6
7
nerdctl image tag base-alpine:latest harbor.mycompany.com/base/base-alpine:latest
nerdctl image tag base-busybox:latest harbor.mycompany.com/base/base-busybox:latest

nerdctl login -u username -p password harbor.mycompany.com

nerdctl push harbor.mycompany.com/base/base-alpine:latest
nerdctl push harbor.mycompany.com/base/base-busybox:latest

5.编写Web应用并完成镜像构建

下面我们用go写一个简单hello world的web应用main.go:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
package main

import (
	"io"
	"log"
	"net/http"
)

func main() {
	handler := func(w http.ResponseWriter, req *http.Request) {
		io.WriteString(w, "Hello, world!\n")
	}
	http.HandleFunc("/hello", handler)
	err := http.ListenAndServe(":8080", nil)
	if err != nil {
		log.Fatal(err)
	}
}

编写构建这个程序的Dockerfile:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
FROM golang:1.16.4-alpine3.13 as builder
WORKDIR /go/src/hello
COPY main.go .
ENV GO111MODULE=off
RUN go build .

FROM harbor.mycompany.com/base/base-alpine:latest
WORKDIR /app/
COPY --from=0 /go/src/hello/hello .
USER nobody
CMD ["./hello"]
EXPOSE 8080

这里使用多阶段构建,golang:1.16.4-alpine3.13只在构建阶段会用到,go build出的二进制文件在构建的第二阶段被拷贝到base-alpine中。 使用多阶段构建可以确保构建出的应用服务镜像体积尽可能的小。

构建这个程序的镜像:

1
nerdctl build -t go-web-hello .

启动容器并测试:

1
2
3
4
nerdctl run -p 8080:8080 -d go-web-hello

curl localhost:8080/hello
Hello, world!

6.总结

通过本节的学习,使用nerdctl + buildkitd也可以轻松的完成容器镜像的构建。 使用containerd及nerdctl,buildkit等工具已经可以完全替代docker在镜像构建、单机启动容器和管理容器的功能了。

参考