容器给我们的生活带来了极大便利,人人都喜欢容器,然而容器也很耗空间,动辄几百兆,上G的镜像是普遍现象。本文我们就学习容器精简的案例,通过一系列的骚操作,最终将镜像的大小从943MB减小到了6.32k。
概述
容器是实践中用来解决与操作软件版本和包依赖相关的所有问题的有效途径。 人人都喜欢容器,但是用容器就得面对各式各样庞大和杂乱的镜像,如果空间有限,则很快就会被充满,实际上可以通过一些有效的策略来减小镜像大小。
基本步骤
一个Http应用容器,可以通过指定端口提供web服务。
不进行卷挂载。
原始方案
为了获得基准镜像大小,我们用node.js创建一个简单只提供index.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/`);
- });
然后,将该文件内置到一个镜像中,镜像基于Node官方基本镜像。
- FROM node:14
- COPY . .
- CMD ["node", "index.js"]
编译
- docker build -t cchttp:01 ./
镜像大小为943MB
精简基础镜像
镜像精简最常用,最简单,最明显的策略之一就是使用较小的基础图像。Node镜像中slim 变体(基于debian,但预安装的依赖项较少)和基于Alpine Linux的alpine变体 。
这两个基础镜像分别为node:14-slim 和 node:14-alpine ,其镜像大小分别减少到167MB 和 116MB 分别。
Docker由于镜像是分层叠加的,node.js需要依赖很多层的镜像,除了精简解决方案目前还没有其他变小的方法。
更换语言
为了进一步优化,需要使用运行时依赖项更少的编译语言。而这时候肯定会首先想到的是一个静态编译语言Golang,这是个常见而且不错的选择。在Golang中一个基本的Web服务代码如下:
web.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"]
基于golang的解决方案,镜像大小818MB,还是很大。
通过分析发现是由于golang基本镜像中安装了很多依赖包,这些依赖包在构建go软件时很有用,但不是每个运行时都需要的,所以可以从这儿着手优化。
多阶段构建
Docker支持多阶段构建的机制,可以很轻松在具有所有必要依赖项的环境中构建代码,然后将生成的可执行包直接打包到其他镜像中使用。这样就可以解决我们上一步遇到需要编译时工具和包,但是运行时不需要包,这样可以极大地减少镜像大小。
注意:Docker多阶段构建的机制是Docker 17.05引入的新特性,如果要使用该功能你需要将Docker版本升级到Docker 17.05及更高版本。
到多阶段构建dockerfile:
- ###编译###
- FROM golang:1.14-alpine AS builder
- COPY . .
- RUN go build -o server .
- ###运行###
- FROM alpine:3.12
- COPY --from=builder /go/server ./server
- COPY index.html index.html
- CMD ["./server"]
- Docker images
(⊙o⊙)哇,策略生效,这样生成的镜像只有13.2MB。
静态编译结合scratch基础镜像
13M的镜像已经很不错了,但是还有其他优化的技巧。在docker世界中还有几个基础镜像scratch ,那就是一个From 0 开始的基础镜像,使用该镜像没有任何依赖,完全从0开始,所以大小也就从0开始。Linux 有个发行版LFS,其全称是Linux From Scratch ,就是从零开始自己动手编译出一个完整的OS。这个scratch基础镜像也是这个意思。
为了让scratch基础镜像支持我们的web.go运行,我们需要在编译镜像中添加静态编译的标志,确保所有依赖都可以打包到运行镜像中:
- ### 编译###
- FROM golang:1.14 as builder
- COPY . .
- RUN go build -o server \
- -ldflags "-linkmode external -extldflags -static" \
- -a web.go
- ###运行###
- FROM scratch
- COPY --from=builder /go/server ./server
- COPY index.html index.html
- CMD ["./server"]
上面构建过程中,在代码链接过程中模式设置为external,-static链接外部链接器。
优化后,镜像大小为8.65MB。
最终大杀器——汇编语言
用Golang语言编写的程序,起码也有大概M级别的大小,10MB镜像应该已经到了可以精简的极限。但是还可以用其他技巧来大幅度精简大小,但是需要使用要给终极大杀器,那就是汇编语言,最终解决方案是使用一个汇编编写的全功能http服务器assmttpd,其源码托管在GitHub(github/nemasu/asmttpd)。
我们还使用多阶段编译方法,在ubuntu基础镜像中先编译其依赖项,然后在Scratch基础镜像中打包并运行。
- ###编译###
- FROM ubuntu:18.04 as builder
- RUN apt update
- RUN apt install -y make yasm as31 nasm binutils
- COPY . .
- RUN make release
- ###运行###
- FROM scratch
- COPY --from=builder /asmttpd /asmttpd
- COPY /web_root/index.html /web_root/index.html
- CMD ["/asmttpd", "/web_root", "8080"]
产生的图像大小仅为6.34kB:
然后用该镜像运行一个容器:
- docker run -it -p 10080:8080 cchttp:07
用curl访问一下:
- curl -vv 127.0.0.1:10080
总结
本文我们探索了容器精简的各种方法和尝试。当然由于容器的功能简单,这些策略可能不发直接在实践中使用,但是可以作为容器调优的思路参考。