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目录中:

 1wget https://github.com/moby/buildkit/releases/download/v0.8.3/buildkit-v0.8.3.linux-amd64.tar.gz
 2tar -zxvf buildkit-v0.8.3.linux-amd64.tar.gz -C /usr/local/containerd/
 3
 4tree /usr/local/containerd/bin/ | grep build
 5├── buildctl
 6├── buildkitd
 7├── buildkit-qemu-aarch64
 8├── buildkit-qemu-arm
 9├── buildkit-qemu-i386
10├── buildkit-qemu-ppc64le
11├── buildkit-qemu-riscv64
12├── buildkit-qemu-s390x
13├── buildkit-runc
14
15ln -s /usr/local/containerd/bin/buildkitd /usr/local/bin/buildkitd
16ln -s /usr/local/containerd/bin/buildctl /usr/local/bin/buildctl

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

1[Unit]
2Description=BuildKit
3Documentation=https://github.com/moby/buildkit
4
5[Socket]
6ListenStream=%t/buildkit/buildkitd.sock
7
8[Install]
9WantedBy=sockets.target

/etc/systemd/system/buildkit.service:

 1[Unit]
 2Description=BuildKit
 3Requires=buildkit.socket
 4After=buildkit.socketDocumentation=https://github.com/moby/buildkit
 5
 6[Service]
 7ExecStart=/usr/local/bin/buildkitd --oci-worker=false --containerd-worker=true
 8
 9[Install]
10WantedBy=multi-user.target

启动buildkitd:

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

4.构建alpine和busybox基础镜像

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

 1FROM alpine:3.13.5
 2
 3# Here we install GNU libc (aka glibc) and set C.UTF-8 locale as default.
 4ENV LANG=C.UTF-8
 5RUN echo "**** install packages ****" && \
 6    sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories &&\
 7    ALPINE_GLIBC_BASE_URL="https://github.com/sgerrand/alpine-pkg-glibc/releases/download" && \
 8    ALPINE_GLIBC_PACKAGE_VERSION="2.33-r0" && \
 9    ALPINE_GLIBC_BASE_PACKAGE_FILENAME="glibc-$ALPINE_GLIBC_PACKAGE_VERSION.apk" && \
10    ALPINE_GLIBC_BIN_PACKAGE_FILENAME="glibc-bin-$ALPINE_GLIBC_PACKAGE_VERSION.apk" && \
11    ALPINE_GLIBC_I18N_PACKAGE_FILENAME="glibc-i18n-$ALPINE_GLIBC_PACKAGE_VERSION.apk" && \
12    apk add --no-cache --virtual=.build-dependencies wget ca-certificates && \
13    echo \
14        "-----BEGIN PUBLIC KEY-----\
15        MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEApZ2u1KJKUu/fW4A25y9m\
16        y70AGEa/J3Wi5ibNVGNn1gT1r0VfgeWd0pUybS4UmcHdiNzxJPgoWQhV2SSW1JYu\
17        tOqKZF5QSN6X937PTUpNBjUvLtTQ1ve1fp39uf/lEXPpFpOPL88LKnDBgbh7wkCp\
18        m2KzLVGChf83MS0ShL6G9EQIAUxLm99VpgRjwqTQ/KfzGtpke1wqws4au0Ab4qPY\
19        KXvMLSPLUp7cfulWvhmZSegr5AdhNw5KNizPqCJT8ZrGvgHypXyiFvvAH5YRtSsc\
20        Zvo9GI2e2MaZyo9/lvb+LbLEJZKEQckqRj4P26gmASrZEPStwc+yqy1ShHLA0j6m\
21        1QIDAQAB\
22        -----END PUBLIC KEY-----" | sed 's/   */\n/g' > "/etc/apk/keys/sgerrand.rsa.pub" && \
23    wget \
24        "$ALPINE_GLIBC_BASE_URL/$ALPINE_GLIBC_PACKAGE_VERSION/$ALPINE_GLIBC_BASE_PACKAGE_FILENAME" \
25        "$ALPINE_GLIBC_BASE_URL/$ALPINE_GLIBC_PACKAGE_VERSION/$ALPINE_GLIBC_BIN_PACKAGE_FILENAME" \
26        "$ALPINE_GLIBC_BASE_URL/$ALPINE_GLIBC_PACKAGE_VERSION/$ALPINE_GLIBC_I18N_PACKAGE_FILENAME" && \
27    apk add --no-cache \
28        "$ALPINE_GLIBC_BASE_PACKAGE_FILENAME" \
29        "$ALPINE_GLIBC_BIN_PACKAGE_FILENAME" \
30        "$ALPINE_GLIBC_I18N_PACKAGE_FILENAME" && \
31    \
32    rm "/etc/apk/keys/sgerrand.rsa.pub" && \
33    /usr/glibc-compat/bin/localedef --force --inputfile POSIX --charmap UTF-8 "$LANG" || true && \
34    echo "export LANG=$LANG" > /etc/profile.d/locale.sh && \
35    \
36    apk del glibc-i18n && \
37    \
38    rm "/root/.wget-hsts" && \
39    apk del .build-dependencies && \
40    rm \
41        "$ALPINE_GLIBC_BASE_PACKAGE_FILENAME" \
42        "$ALPINE_GLIBC_BIN_PACKAGE_FILENAME" \
43        "$ALPINE_GLIBC_I18N_PACKAGE_FILENAME"
44
45
46RUN apk update --no-cache && apk add ca-certificates --no-cache && \
47    apk add tzdata --no-cache && \
48    ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && \
49    echo "Asia/Shanghai" > /etc/timezone

构建alpine基础镜像:

1nerdctl build -t base-alpine .
2
3nerdctl images
4WARN[0000] unparsable image name "overlayfs@sha256:8628f76ed129c628eccb2259cf61a3138adac73247565f04fb00ea79fab6ae28"
5REPOSITORY     TAG                  IMAGE ID        CREATED          SIZE
6base-alpine    latest               8628f76ed129    3 minutes ago    8.8 MiB
7                                    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镜像的基础上:

1FROM busybox:1.33.1-glibc
2
3COPY zoneinfo /usr/share/zoneinfo/
4RUN ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && echo 'Asia/Shanghai' >/etc/timezone

构建busybox基础镜像:

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

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

1nerdctl image tag base-alpine:latest harbor.mycompany.com/base/base-alpine:latest
2nerdctl image tag base-busybox:latest harbor.mycompany.com/base/base-busybox:latest
3
4nerdctl login -u username -p password harbor.mycompany.com
5
6nerdctl push harbor.mycompany.com/base/base-alpine:latest
7nerdctl push harbor.mycompany.com/base/base-busybox:latest

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

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

 1package main
 2
 3import (
 4	"io"
 5	"log"
 6	"net/http"
 7)
 8
 9func main() {
10	handler := func(w http.ResponseWriter, req *http.Request) {
11		io.WriteString(w, "Hello, world!\n")
12	}
13	http.HandleFunc("/hello", handler)
14	err := http.ListenAndServe(":8080", nil)
15	if err != nil {
16		log.Fatal(err)
17	}
18}

编写构建这个程序的Dockerfile:

 1FROM golang:1.16.4-alpine3.13 as builder
 2WORKDIR /go/src/hello
 3COPY main.go .
 4ENV GO111MODULE=off
 5RUN go build .
 6
 7FROM harbor.mycompany.com/base/base-alpine:latest
 8WORKDIR /app/
 9COPY --from=0 /go/src/hello/hello .
10USER nobody
11CMD ["./hello"]
12EXPOSE 8080

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

构建这个程序的镜像:

1nerdctl build -t go-web-hello .

启动容器并测试:

1nerdctl run -p 8080:8080 -d go-web-hello
2
3curl localhost:8080/hello
4Hello, world!

6.总结

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

参考