阅读 52

【译】小容器挑战:构建6kB大小的HTTP Server容器

本文演示了如何构建一个最小化的容器镜像(当然该镜像还得能做点有用的事:))。通过利用多段构建(multistage builds)、scratch基础镜像和基于汇编(assembly)的http服务器程序, 我们能够将其尺寸降低到6.32kB

膨胀的容器

容器经常被奉为解决所有与软件运维相关难题的灵丹妙药。虽然我喜欢容器,但我经常会碰到有各种问题的野路子镜像。一个常见的问题是镜像尺寸,有时一个镜像可能会达到数GB!

因此,我决定挑战一下自己,构建一个尽可能小的容器镜像。

挑战

规则很简单:

  • 容器应在特定端口上通过http提供对文件内容的访问
  • 不允许卷安装

最朴素的解决方案

作为对照,我们先用node.js创建一个简单的服务器 index.js

const fs = require("fs");
const http = require('http');

const server = http.createServer((req, res) => {
  res.writeHead(200, { 'content-type': 'text/html' })
  fs.createReadStream('index.html').pipe(res)
})

server.listen(port, hostname, () => {
  console.log(`Server: http://0.0.0.0:8080/`);
});
复制代码

并把它放到一个镜像里(该容器使用nodejs官方镜像):

FROM node:14
COPY . .
CMD ["node", "index.js"]
复制代码

这个镜像构建后的尺寸为943MB

更小的基础镜像

为了减小目标镜像的尺寸,最简单的策略就是使用较小的基本镜像。nodejs的官方镜像有一个叫slim的变体(仍基于debian,但预安装的依赖项较少)和一个叫做alpine的基于Alpine Linux的变体。

使用node:14-slimnode:14-alpine作为基本镜像,会使最终的镜像尺寸分别减小到167MB116MB

由于docker镜像的分层叠加特性,新增的每一层都要构建在原有的基础镜像上,所以也就没什么更好的能减小nodejs镜像尺寸的办法了。

编译语言

为了更进一步,我们可以使用运行时依赖项更少的编译语言。当然有多种选择,但是对于构建Web服务,golang是一个比较"流行"的选择。

为此我创建了一个最基础的的文件服务器server.go,如下:

package main

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

func main() {
	fileServer := http.FileServer(http.Dir("./"))
	http.Handle("/", fileServer)
	fmt.Printf("Starting server at port 8080\n")
	if err := http.ListenAndServe(":8080", nil); err != nil {
			log.Fatal(err)
	}
}
复制代码

并使用golang官方的基础镜像做构建:

FROM golang:1.14
COPY . .
RUN go build -o server .
CMD ["./server"]
复制代码

最后的尺寸是 818MB。😡

这里的问题是,golang的基础镜像里安装了许多依赖项,这些依赖在构建golang程序时很有用,但运行时一般都不需要。

多段构建

Docker有一个被称为 多段构建 (Multi-stage Builds) 的特性,可以在安装了所有必要依赖项的环境中构建代码,然后将生成的可执行文件复制到其他镜像中去。

这个特性往往很有用,最主要原因之一是可以大幅度的减小镜像尺寸!

通过如下方式重构Dockerfile:

### build stage ###
FROM golang:1.14-alpine AS builder
COPY . .
RUN go build -o server .

### run stage ###
FROM alpine:3.12
COPY --from=builder /go/server ./server
COPY index.html index.html
CMD ["./server"]
复制代码

镜像尺寸一下子变成了13.2MB!🙂

静态编译 + scratch镜像

13MB还算不错,但是我们仍然可以通过一些方法让它变得更小。

有一个叫做 scratch 的基本镜像,该镜像不包含任何内容,大小为零。所以使用它构建的任何镜像都必须自带所有必需的依赖项。

为了让我们的go服务器做到这一点,需要在编译过程中添加一些标志,以确保必要的库能以静态链接的方式被编译到可执行文件中:

### build stage ###
FROM golang:1.14 as builder
COPY . .
RUN go build \
  -ldflags "-linkmode external -extldflags -static" \
  -a server.go

### run stage ###
FROM scratch
COPY --from=builder /go/server ./server
COPY index.html index.html
CMD ["./server"]
复制代码

具体来说,我们将链接模式设置为external,并将-static标志传递给了外部链接器。

这两个更改使镜像大小缩减到8.65MB!😀

ASM为赢!

用Go之类的语言编写的小于10MB的镜像几乎已经到达极限了... 但其实我们还可以继续缩小尺寸!

Github用户 nemasu 在github上用汇编形式编写了一个全功能http服务器 assmttpd

对其进行容器化,需要做的就是在make release之前将一些构建所需的依赖项安装到ubuntu基础镜像中去:

### build stage ###
FROM ubuntu:18.04 as builder
RUN apt update
RUN apt install -y make yasm as31 nasm binutils 
COPY . .
RUN make release

### run stage ###
FROM scratch
COPY --from=builder /asmttpd /asmttpd
COPY /web_root/index.html /web_root/index.html
CMD ["/asmttpd", "/web_root", "8080"] 
复制代码

将生成的可执行文件asmttpd复制到scratch映像中,并通过CMD来调用。

这样产生的镜像大小仅为6.34kB!🥳


下图为容器镜像列表,可供参考。 image.png


从最初的943MB的nodejs镜像一直到这个微小的6.34kB 汇编镜像,希望您喜欢这个过程。

通过对上述过程的理解,希望您也能对自己的容器镜像做优化,让它们变得更小。


原文链接: devopsdirective.com/posts/2021/…

文章分类
后端
文章标签