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

   使用小型的基础镜像可能足够满足我们的需求了,但是我们可以通过使用生成器模式将基础镜像更小化。对于解释性语言,源代码会被送到解释程序那二并直接运行。但对于编译性语言,源代码会首先被编译成机器码,然后在被执行。

   对于编译性语言,通常编译时期会依赖一些运行时不需要的工具,所以我们将这部分运行时不需要的工具从最终的容器中完全去除。我们可以使用生成器模式来完成这个功能。

解释语言、编译语言
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 会启动一个新的节点作为代替,但是这个节点是全新的,在它运行服务之前需要拉取相关容器,如果容器拉取耗时太久,代表影响用户的时间越长,所以将拉取时间减少到最低限度成为了关键。在新增节点、更新节点、迁移节点时都是如此。

  • 在生产阶段建议选择较小的基础镜像作为底层镜像,然后在上次逐个安装应用以来的工具来确保最小化镜像。

  • 构建较小的镜像,性能与安全的好处是真实的。



Kubernetes     

本博客所有文章除特别声明外,均采用 CC BY-SA 3.0协议 。转载请注明出处!