1.容器核心技术和Docker的创新:镜像技术
容器技术主要包括Cgroup和Namesapce这两个内核级别的特性。
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目录中:
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:
[Unit]
Description=BuildKit
Documentation=https://github.com/moby/buildkit
[Socket]
ListenStream=%t/buildkit/buildkitd.sock
[Install]
WantedBy=sockets.target/etc/systemd/system/buildkit.service:
[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:
systemctl daemon-reload
systemctl enable buildkit
systemctl start buildkit4.构建alpine和busybox基础镜像
alpine linux的核心特点是体积小、简单和安全,因此很多人选择alpine镜像作为基础镜像。 我们编写一个Dockerfile在docker官方alpine镜像的基础上,安装glibc,同时配置中国时区:
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基础镜像:
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 name,nerdctl images除了列出我们构建的base-alpine:latest镜像外,还有一个tag为空的重复镜像,
nerdctl的github上有人提了issues 177,官方还没有回应是buildkit的问题还是nerdctl的问题,但这并不影响我们对base-alpine这个基础镜像的使用。
接下来构建busybox基础镜像,busybox是一个集成了一百多个最常用的Linux命令和工具的软件工具箱,它在单一的可执行文件中提供了精简的工具集,有人把busybox作为linux系统的瑞士军刀, 这里编写一个Dockerfile在docker官方busybox镜像的基础上:
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基础镜像:
cp -R /usr/share/zoneinfo .
nerdctl build -t base-busybox .将前面构建的两个基础镜像推送到镜像仓库:
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:latest5.编写Web应用并完成镜像构建
下面我们用go写一个简单hello world的web应用main.go:
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:
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中。
使用多阶段构建可以确保构建出的应用服务镜像体积尽可能的小。
构建这个程序的镜像:
nerdctl build -t go-web-hello .启动容器并测试:
nerdctl run -p 8080:8080 -d go-web-hello
curl localhost:8080/hello
Hello, world!6.总结
通过本节的学习,使用nerdctl + buildkitd也可以轻松的完成容器镜像的构建。 使用containerd及nerdctl,buildkit等工具已经可以完全替代docker在镜像构建、单机启动容器和管理容器的功能了。