K8S Building Small Container
背景
Pod 作为 Kubernetes 最小的运行单元,实际背后运行我们应用的都是一个个的容器。容器作为目前云原生及微服务不可获取的伙伴充当着重要的角色。
在我们使用 Docker 构建容器时,Docker 官方为我们提供了很多基础镜像 Images,当然我们也可以自己进行镜像 Images 的封装。在生产使用容器时,基础镜像的大小是一个我们不得不考虑的问题。
基础镜像过大代表内部自带的功能越多,同时也包含过多与我们应用无关的组件,这不仅对应用没有任何意义同时也会隐藏许多安全漏洞和程序 Bug。大多数镜像以 Ubuntu 或 Debian 为基础镜像,虽然这对兼容性和适应性有很多好处,但是这些基础镜像会为我们的容器多增加上百兆字节的额外开销。
小镜像
基于以上大镜像的问题,在生产环境使用合适的且足够小的镜像,变得尤为重要。对于减小镜像的大小一般有两种方式:
使用小型基础镜像(Small Base Images)
使用生成器模式(Builder Pattern)
Small Base Images
减少容器大小最容易的方法就是选择一个小型的基础镜像。通常官方会为我们提供大部分开发语言的多个版本的基础镜像,我们这里以 Java 镜像为例,通常不同的 tag 代表不同的镜像大小。如果官方没有提供合适的基础镜像,我们可以通过使用原始的 Alpine Linux 为基础自行封装。
以下三个版本的 jdk 基础镜像的大小最高相差 6 倍。在应用服务不多时可能者对我们来说可以接受,但随着企业逐渐发展,内部管理的应用也会越来越多,渐渐的我们发现每个镜像如果都能减少 6 倍左右,那么多企业来讲会节约一笔不小的开支,同时也会降低应用被攻击的几率。
openjdk:8u171 -> ~620MB
openjdk:8u171-slim -> ~240MB
Openjdk:8u171-alpine -> ~103MB
Builder Pattern
使用小型的基础镜像可能足够满足我们的需求了,但是我们可以通过使用生成器模式将基础镜像更小化。对于解释性语言,源代码会被送到解释程序那二并直接运行。但对于编译性语言,源代码会首先被编译成机器码,然后在被执行。
对于编译性语言,通常编译时期会依赖一些运行时不需要的工具,所以我们将这部分运行时不需要的工具从最终的容器中完全去除。我们可以使用生成器模式来完成这个功能。
我们以 golang 为例,通常容器会从一个 golang:alpine 镜像开始,接着为代码创建目录并拷贝源代码到目录,最后构建源代码并运行。
FROM golang:alpine
WORKDIR /app
ADD . /app
RUN cd /app && go build -o goapp
EXPOSE 8080
ENTRYPOINT ./goapp
虽然使用了标签为 alpine 的镜像已经足够小了,但是我们还可以将编译和运行阶段进行拆分。
FROM golang:alpine AS build
WORKDIR /app
ADD . /app
RUN cd /app && go build -o goapp
# 原始 alpine 镜像并没有安全证书,这回导致 HTTPS 请失败,所以需要进行手动安装 CA 证书
FROM alpine
RUN apk update && apk add ca-cretificates && rm -rf /var/cache/apk/*
WORKDIR /app
COPY --from=build /app/goapp /app
EXPOSE 8080
ENTRYPOINT ./goapp
通过两种构建镜像方式的对比,我们发现使用 Builder Pattern 方式比原来更加精简。
Image | Size |
---|---|
go:alpine | ~370MB |
go:multistate | ~7M |
小容器优势
小容器的优势有两点。一是更高的性能,包括构建、推送、拉取等阶段。二是更加安全。
Performance
st=>start: start build=>operation: build push=>operation: push pull=>operation: pull e=>end: end st->build->push->pull->e
Build state
Large Machine Small Machine go:onbuild 35 Seconds 54 Seconds go:multistate 23 Seconds 28 Seconds Push state
Large Machine Small Machine go:onbuild 15 Seconds 48 Seconds go:multistate 14 Seconds 16 Seconds Pull state
Large Machine Small Machine go:onbuild 26 Seconds 52 Seconds go:multistate 6 Seconds 6 Seconds 注意
:Large 代表高性能计算机。Small 代表普通计算机。
Security
将不必要的组件从镜像中除,有助于减少被攻击的范围,降低安全风险。
小的镜像因为可攻击的点过少,会增加攻击者的成本,所以一般不会选择这种镜像进行攻击。
注意
:可以通过一些容器漏洞扫描工具来证实。
注意事项
在镜像拉取阶段是最关注镜像大小的阶段,在 Kubernetes 中如果机器中某一节点挂掉,Kubernetes 会启动一个新的节点作为代替,但是这个节点是全新的,在它运行服务之前需要拉取相关容器,如果容器拉取耗时太久,代表影响用户的时间越长,所以将拉取时间减少到最低限度成为了关键。在新增节点、更新节点、迁移节点时都是如此。
在生产阶段建议选择较小的基础镜像作为底层镜像,然后在上次逐个安装应用以来的工具来确保最小化镜像。
构建较小的镜像,性能与安全的好处是真实的。
本博客所有文章除特别声明外,均采用 CC BY-SA 3.0协议 。转载请注明出处!