对于许多编程语言(包括Go),有几个很好的官方和社区支持的容器,但是这些容器可能很大。 让我们比较一下为Go应用程序构建容器的方法,然后,我将向您展示一种为容器化静态构建Go应用程序的方法。
第一部分:我们的“应用程序”
我们需要一些东西来测试我们的应用程序,所以让我们做些很小的事情:我们将获取google.com,并输出我们获取的html的大小:
package main
import (
"fmt"
"io/ioutil"
"net/http"
"os"
)
func main() {
resp, err := http.Get("https://google.com")
check(err)
body, err := ioutil.ReadAll(resp.Body)
check(err)
fmt.Println(len(body))
}
func check(err error) {
if err != nil {
fmt.Println(err)
os.Exit(1)
}
}
如果我们运行此命令,它将仅打印出一些数字。 对我来说,大约是17,000。 我故意决定对SSL进行操作,原因是我保证稍后会解释。
第2部分:Dockerize
遵循Go的官方Docker镜像之后,我们将编写一个“ onbuild” Dockerfile,如下所示:
FROM golang:onbuild
“ onbuild”图像假定您的项目结构是标准的,并且将像通用Go应用程序一样构建您的应用程序。 如果需要更多控制,可以使用其标准的Go基础映像并自行编译:
FROM golang:latest
RUN mkdir /app
ADD . /app/
WORKDIR /app
RUN go build -o main .
CMD ["/app/main"]
如果您有Makefile或在构建应用程序时必须执行的其他非标准操作,那么这很好。 我们可以从CDN下载一些资产,也可以从另一个项目中添加它们,或者我们想在容器中运行测试。
如您所见,Dockerizing Go应用程序非常简单,尤其是因为我们没有任何需要访问或导出的服务或端口。 但是官方图片有一个很大的缺点:它们真的很大。 让我们来看看:
REPOSITORY SIZE TAG IMAGE ID CREATED VIRTUAL SIZE
example-onbuild latest 9dfb1bbac2b8 19 minutes ago 520.7 MB
example-golang latest 02e19291523e 19 minutes ago 520.7 MB
golang onbuild 3be7ee2ec1ae 9 days ago 514.9 MB
golang 1.4.2 121a93c90463 9 days ago 514.9 MB
golang latest 121a93c90463 9 days ago 514.9 MB
基本为514.9MB,我们的应用程序仅增加了5.8MB。 哇。 因此,对于我们的已编译应用程序,我们仍然需要514.9MB的依赖关系? 那是怎么发生的?
答案是我们的应用程序是在容器内编译的。 这意味着容器需要安装Go,这意味着它需要Go的依赖项,这意味着我们需要一个程序包管理器和整个OS。 实际上,如果您查看golang:1.4的Dockerfile,它将以Debian Jessie开头,安装GCC编译器和一些构建工具,压缩Go并安装它。 因此,我们几乎有一个完整的Debian服务器和Go工具包来运行我们的小型应用程序。 我们能做什么?
第3部分:编译!
改进的方法是做一些……偏离常规。 我们要做的是在工作目录中编译Go,然后将二进制文件添加到容器中。 这意味着简单的docker构建将无法正常工作。 我们需要一个多步骤的容器构建:
go build -o main .
docker build -t example-scratch -f Dockerfile.scratch .
Dockerfile.scratch很简单:
FROM scratch
ADD main /
CMD ["/main"]
那么,从头开始呢? Scratch是一个空的特殊docker映像。 确实是0B:
REPOSITORY TAG IMAGE ID CREATED VIRTUAL SIZE
example-scratch latest ca1ad50c9256 About a minute ago 5.60 MB
scratch latest 511136ea3c5a 22 months ago 0 B
另外,我们的容器只有5.6MB! 凉! 但是有一个问题:
$ docker run -it example-scratch
no such file or directory
?? 这意味着什么? 花了我一段时间才能弄清楚,但是我们的Go二进制文件正在运行它的操作系统上寻找库。我们编译了应用程序,但它仍动态链接到它需要运行的库(即所有C库)它绑定到)。 不幸的是,草稿是空的,因此没有库,也没有加载路径可供查看。我们要做的是修改构建脚本,以使用所有内置库静态编译我们的应用程序:
CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o main .
我们正在禁用cgo,它为我们提供了一个静态二进制文件。 我们还将操作系统设置为Linux(以防有人在Mac或Windows上构建该操作系统),并且-a标志意味着重新构建我们正在使用的所有软件包,这意味着所有导入将在禁用cgo的情况下进行重建。 这些设置在Go 1.4中进行了更改,但是我在GitHub Issue中找到了解决方法 。 现在我们有了一个静态二进制文件! 让我们尝试一下:
$ docker run -it example-scratch
Get https://google.com: x509: failed to load system roots and no roots provided
太好了,现在呢? 这就是为什么我在示例中选择使用SSL。 在这种情况下,这是一个非常常见的陷阱:要发出SSL请求,我们需要SSL根证书。 那么我们如何将它们添加到我们的容器中呢?
根据操作系统的不同,这些证书可以位于许多不同的位置。 如果您查看Go的x509库,则可以看到Go搜索的所有位置。 对于许多Linux发行版,这是/etc/ssl/certs/ca-certificates.crt 。 因此,首先,我们将ca-certificates.crt从我们的计算机(或Linux VM或在线证书提供者)复制到我们的存储库中。 然后,我们将一个ADD添加到我们的Dockerfile中,以将该文件放置在Go期望的位置:
FROM scratch
ADD ca-certificates.crt /etc/ssl/certs/
ADD main /
CMD ["/main"]
现在,只需重建我们的形象并运行它,它就会起作用! 凉! 让我们看看我们的应用程序现在有多大:
REPOSITORY TAG IMAGE ID CREATED VIRTUAL SIZE
example-scratch latest ca1ad50c9256 About a minute ago 6.12 MB
example-onbuild latest 9dfb1bbac2b8 19 minutes ago 520.7 MB
example-golang latest 02e19291523e 19 minutes ago 520.7 MB
golang onbuild 3be7ee2ec1ae 9 days ago 514.9 MB
golang 1.4.2 121a93c90463 9 days ago 514.9 MB
golang latest 121a93c90463 9 days ago 514.9 MB
scratch latest 511136ea3c5a 22 months ago 0 B
我们添加了超过一半的内存(大部分来自静态二进制文件,而不是根证书)。 这是一个非常不错的小容器-在注册管理机构之间进行推送和拉动非常容易。
结论
我们在这篇文章中的目标是减少Go应用程序的容器大小。 Go的特殊之处在于它可以创建完全包含应用程序的静态链接二进制文件。 其他语言可以做到这一点,但是当然不是全部。 如果我们将这种减小容器大小的技术应用于其他语言,则取决于它们的最低要求。 例如,可以在容器外部编译Java或JVM应用程序,然后将其注入仅具有JVM(及其依赖项)的容器中。 这至少比具有JDK的容器小。
我非常期待社区在为容器访客创建最小的OS以及积极削减各种语言的要求方面所取得的进步。 关于公共Docker集线器的好处是,可以轻松地与所有人共享这些内容。
翻译自: https://www.javacodegeeks.com/2015/04/building-minimal-docker-containers-for-go-applications.html