为你的Go应用创建轻量级Docker镜像?

缩小Go二进制文件大小

环境

youmen@youmendeMacBook-Pro % gcc -dumpversion
12.0.5

youmen@youmendeMacBook-Pro % go version
go version go1.16.5 darwin/amd64

go build使用的是静态编译,会将程序的依赖一起打包,这样一来编译得到的可执行文件可以直接在目标平台运行,无需运行环境(例如 JRE)或动态链接库(例如 DLL)的支持。
虽然 Go 的静态编译很方便,但也存在一个问题:打包生成的可执行文件体积较大,毕竟相关的依赖都被打包进来了;

默认二进制打包

package main

import "github.com/gin-gonic/gin"

func main() {
	r := gin.Default()
	r.GET("/ping", func(c *gin.Context) {
		c.JSON(200, gin.H{
			"message": "pong",
		})
	})
	r.Run("0.0.0.0:8080")
}

# go build -o test1 main.go
# du -sh test1 
14M    test1

-ldflags

# go build -ldflags "-s -w" -o test2 main.go
# du -sh test2 
 11M    test2

下面假设我们将本地编译好的 bluebell 二进制文件、配置文件和静态文件等上传到服务器的/data/app/bluebell目录下。
补充一点,如果嫌弃编译后的二进制文件太大,可以在编译的时候加上-ldflags "-s -w"参数去掉符号表和调试信息,一般能减小20%的大小;
?

CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags "-s -w" -o ./bin/bluebell
  • 在程序编译的时候可以加上-ldflags "-s -w"参数来优化编译,原理是通过去除部分链接和调试等信息来减小编译生成的可执行程序体积,具体参数如下:
  • -a:强制编译所有依赖包
  • -s:去掉符号表信息,不过panic的时候stace trace就没有任何文件名/行号信息
  • -w:去掉DWARF调试信息,不过得到的程序就不能使用gdb进行调试
  • 若对符号表无需求,-ldflags直接添加"-s"即可

:不建议-w和-s同时使用
?

UPX

brew/yum install upx
# upx test2 
                       Ultimate Packer for eXecutables
                          Copyright (C) 1996 - 2020
UPX 3.96        Markus Oberhumer, Laszlo Molnar & John Reiser   Jan 23rd 2020

        File size         Ratio      Format      Name
   --------------------   ------   -----------   -----------
  11490768 ->   4063248   35.36%   macho/amd64   test2                         

Packed 1 file.
  
# upx --brute test2
  
# du -sh test2 
4.6M    test2

upx的压缩选项

  • -o:指定输出的文件名
  • -k:保留备份原文件
  • -1:最快压缩,共1-9九个级别
  • -9:最优压缩,与上面对应
  • -d:解压缩decompress,恢复原体积
  • -l:显示压缩文件的详情,例如upx -l main.exe
  • -t:测试压缩文件,例如upx -t main.exe
  • -q:静默压缩be quiet
  • -v:显示压缩细节be verbose
  • -f:强制压缩
  • -V:显示版本号
  • -h:显示帮助信息
  • --brute:尝试所有可用的压缩方法,slow
  • --ultra-brute:比楼上更极端,very slow

UPX的原理

upx 压缩后的程序和压缩前的程序一样,无需解压仍然能够正常地运行,这种压缩方法称之为带壳压缩,压缩包含两个部分:

  • 在程序开头或其他合适的地方插入解压代码;
  • 将程序的其他部分压缩;

执行时,也包含两个部分:

  • 首先执行的是程序开头的插入的解压代码,将原来的程序在内存中解压出来;
  • 再执行解压后的程序;

也就是说,upx 在程序执行时,会有额外的解压动作,不过这个耗时几乎可以忽略。
如果对编译后的体积没什么要求的情况下,可以不使用 upx 来压缩。一般在服务器端独立运行的后台服务,无需压缩体积。
?

构建轻量级docker镜像

这个Dockerfile中使用了两次FROM指令,第二条FROM scratch行,它告诉Docker从一个全新的,完全空的容器镜像重新开始,然后将上个阶段编译好的程序复制到其中。这个才是我们随后将用于运行的Go应用程序的容器镜像。
scratch镜像是Docker项目预定义的最小的镜像。 Docker用于Go程序的多阶段构建很常见,使用scratch镜像可以节省大量空间,因为我们实际上不需要Go工具或其他任何东西来运行我们的编译好的程序,这可能也是Go在容器时代的一个优势吧。
使用scratch镜像制作的Go应用镜像在运行时会有一个不识别时区的问题,这个也是我们最近项目往Kubernetes上迁移时遇到的第一个问题,不过还好经过Google和查看Go加载时区的源码找到了解决方法
?

介绍

多阶段允许在创建Dockerfile时使用多个from,它非常有用,因为它使我们能够使用所有必需的工具构建应用程序。举个例子,首先我们使用Golang的基础镜像,然后在第二阶段的时候使用构建好的镜像的二进制文件,最后阶段构建出来的镜像用于发布到我们自己的仓库或者是用于上线发布。
?

在上述的案例中,我们总共有三个阶段:
1 . build编译阶段
2 . certs(可选,可有可无)证书认证阶段
3 . prod生产阶段
?

在build阶段主要是编译我们的应用程序,证书认证阶段将会安装我们所需要的CA证书,最后的生产发布阶段会将我们构建好的镜像推到镜像仓库中。而且发布阶段将会使用build阶段编译完毕的二进制文件和certs阶段安装的证书;

为你的Go应用创建轻量级Docker镜像?
?

项目发布的多个build阶段


?

示例工程

main.go

[root@rabbitmq-2 gin_app]# cat /root/go/gin_app/main.go 
package main

import (
	"fmt"
	"net/http"
)

func main() {
	http.HandleFunc("/", hello)
	server := &http.Server{
		Addr: ":8888",
	}
  fmt.Println("server startup...")
	if err := server.ListenAndServe(); err != nil {
		fmt.Printf("server startup failed, err:%v\n", err)
	}
}

func hello(w http.ResponseWriter, _ *http.Request) {
	w.Write([]byte("hello youmen.com!"))
}

编译阶段

Dockerfile
?

[root@rabbitmq-2 gin_app]# cat Dockerfile 
FROM golang:alpine AS build

# 为我们的镜像设置必要的环境变量
ENV GO111MODULE=on     CGO_ENABLED=0     GOOS=linux     GOARCH=amd64     GOPROXY="https://goproxy.io"

# 移动到工作目录:/build
WORKDIR $GOPATH/src/gin_docker 

# 将代码复制到容器中
ADD . ./

# 将我们的代码编译成二进制可执行文件 app
RUN go build -o app 

# 需要运行的命令
CMD ["./app"]
[root@rabbitmq-2 gin_app]# docker build -t gin_app -t gin_app . --target=build
[root@rabbitmq-2 gin_app]# docker images |grep gin_app
gin_app             latest              c35bb6310fce        10 minutes ago      321MB

[root@rabbitmq-2 gin_app]# docker run --rm -it -p 8888:8888 goweb_app
server startup...

[root@rabbitmq-2 ~]# curl localhost:8888
hello youmen.com!

生产阶段

[root@rabbitmq-2 gin_app]# cat Dockerfile 
FROM golang:alpine AS build

# 为我们的镜像设置必要的环境变量
ENV GO111MODULE=on     CGO_ENABLED=0     GOOS=linux     GOARCH=amd64     GOPROXY="https://goproxy.io"

# 移动到工作目录:/build
WORKDIR $GOPATH/src/gin_docker 

# 将代码复制到容器中
ADD . ./

# 将我们的代码编译成二进制可执行文件 app
RUN go build -ldflags "-s -w"  -o app .

###################
# 接下来创建一个小镜像
###################
FROM scratch As prod

# 从builder镜像中把/go/src/gin_docker 拷贝到当前目录
# 设置应用程序以非 root 用户身份运
# User ID 65534 通常是 ‘nobody‘ 用户.
# 映像的执行者仍应在安装过程中指定一个用户。

COPY --chown=65534:0  --from=build  /go/src/gin_docker . 
USER 65534
# 需要运行的命令
CMD ["./app"]

[root@rabbitmq-2 gin_app]# docker build -t gin_app -t gin_app . --target=pro
[root@rabbitmq-2 gin_app]# docker images |grep gin_app
gin_app             latest              592cd0dca666        32 seconds ago      4.42MB

为你的Go应用创建轻量级Docker镜像?

上一篇:02-pytest 常用的运行参数


下一篇:函数重载细说