在Docker上创建Go镜像文件并不困难,但建立的文件很大,接近1G,使用起来不太方便。Docker镜像的一个主要难题就是如何优化,创建小的镜像。我们可以用多级构建的方法来创建Docker镜像文件,它也不复杂。但由于使用这种方法时,需要用简版的Linux(Alpine),它带来了一系列的问题。本文讲述如何解决这些问题并成功创建优化的Go镜像文件,优化之后只有14M。

单级构建:

我们用一个Go程序作为例子来展示如何创建Go镜像。下面就是这个程序的目录结构。

Go程序的具体内容并不重要,只要能运行就行了。我们重点关注“docker”子目录(“kubernetes”子目录里的文件有别的用途,会在另外的文章中讲解)。它里面有三个文件。“docker-backend.sh”是创建镜像的命令文件,“Dockerfile-k8sdemo-backend”是多级构建文件,“Dockerfile-k8sdemo-backend-full”是单级构建文件,

FROM golang:latest # 从Docker库中获取标准golang镜像
WORKDIR /app # 设置镜像内的当前工作目录
COPY go.mod go.sum ./ # 拷贝Go的包管理文件
RUN go mod download # 下载依赖包中的依赖库
COPY . . #从宿主机拷贝文件到镜像
WORKDIR /app/cmd # 设置新的镜像内的当前工作目录
RUN GOOS=linux go build -o main.exe #编译Go程序,并在生成可执行文件
CMD exec /bin/bash -c "trap : TERM INT; sleep infinity & wait" # 保持镜像一直运行,容器不被停掉

上面就是“Dockerfile-k8sdemo-backend-full”镜像文件。请阅读文件中的注释以获得解释。

生成镜像容器

cd /home/vagrant/jfeng45/k8sdemo/
docker build -f ./script/kubernetes/backend/docker/Dockerfile-k8sdemo-backend-full -t k8sdemo-backend-full .

运行镜像容器,“--name k8sdemo-backend-full”是给这个容器一个名字(k8sdemo-backend-full),最后的“k8sdemo-backend-full”是镜像的名字

docker run -td --name k8sdemo-backend-full k8sdemo-backend-full

登录镜像容器, 其中“a95c”是容器ID的前四位。

docker exec -it a95c /bin/bash

文件里有一条语句需要特别解释一下“COPY . .”,它把文件从宿主机拷贝到镜像里,在镜像里已经用“WORKDIR”设置了当前工作目录,那么宿主机的“.”(当前目录)是哪个目录呢?它不是Dockerfile文件所在的目录,而是你运行“Docker build”命令时所在的目录。

我们要把整个程序都拷贝到镜像里,那么在运行docker命令时一定是在程序的根目录,也就是“k8sdemo”目录。但是与容器有关的文件都在“script”目录的子目录下,那么当你运行“Docker build”命令时,它是怎么找到Docekrfile的呢?这里有一个重要的概念就是“build cotext”(构建上下文),由它来决定Dockerfile的缺省目录。当你运行“docker build -t k8sdemo-backend .”创建镜像时,它会从“build cotext”的根目录去找Dockerfile文件,缺省值是你运行docker命令的目录。但由于我们的Dockerfile在另外的目录里,因此需要在命令里加一个“-f”选项来指定Dockerfile的位置,命令如下。 其中“-t k8sdemo-backend-full” 是指明镜像名,格式是“name:tag”, 我们这里没有tag,就只有镜像名。

docker build -f ./script/kubernetes/backend/docker/Dockerfile-k8sdemo-backend-full -t k8sdemo-backend-full .

详情请参见Dockerfile reference

这样创建的镜像用的是全版的Linux系统,因此比较大,大概接近1G。如果要想优化,就要用多级构建。

Multi-stage builds(多级构建):

单级构建只有一个“From”语句,而在多级构建中,有多个“From”,每个“From”构成一级。例如,下面的文件有两个“From”,是一个二级构建。每一级都可以根据需要选择适合自己的基础(base)镜像来构造本级镜像。每级镜像完成之后,下一级镜像可选择只保留上一级构建中对自己有用的最终文件,而删除所有的中间产物,这样就大大节省了空间。详情请参见Use multi-stage builds

下面就是多级构建的dockerfile(“Dockerfile-k8sdemo-backend”).

FROM golang:latest as builder # 本级镜像用“builder”标识
# Set the Current Working Directory inside the container
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
WORKDIR /app/cmd
# Build the Go app
#RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o main.exe
RUN go build -o main.exe ######## Start a new stage from scratch #######
FROM alpine:latest
RUN apk --no-cache add ca-certificates
WORKDIR /root/
RUN mkdir /lib64 && ln -s /lib/libc.musl-x86_64.so.1 /lib64/ld-linux-x86-64.so.2
# Copy the Pre-built binary file from the previous stage
COPY --from=builder /app/cmd/main.exe . #把“/app/cmd/main.exe”文件从“builder”中拷贝到本级的当前目录
# Command to run the executable
CMD exec /bin/sh -c "trap : TERM INT; (while true; do sleep 1000; done) & wait"

创建镜像:

cd /home/vagrant/jfeng45/k8sdemo/
docker build -f ./script/kubernetes/backend/docker/Dockerfile-k8sdemo-backend -t k8sdemo-backend .

登录镜像:

docker run -it --name k8sdemo-backend k8sdemo-backend /bin/sh

上面的文件把构造过程分成两部分,第一部分编译并生成Go可执行文件,用的是是全版Linux. 第二部分是拷贝可执行文件到合适的目录并保持容器运行,用的是简化版Linux。第一部分的命令与单级构建指令基本相同,第二部分的命令会在后面解释。

使用这种方法大大减少了空间占用,创建的Docker镜像只有14M,但由于它使用的简化版的Linux(Alpine),导致我踩了很多坑,下面看看这些坑是如何被填上的。

踩过的坑:

1. 找不到文件

创建镜像成功后,登录镜像:

docker run -it --name k8sdemo-backend k8sdemo-backend /bin/sh

运行编译后的Go可执行文件“main.exe”,错误信息如下:

~ # ./main.exe
./main.exe not found

Go是一个静态编译的语言,也就是说在编译时就把需要的库存放在编译好的程序里了,这样在执行时就不需要再动态链接其它库,使得运行起来非常方便。但并不是所有情况下都是这样,例如但当你使用了cgo(让Go程序可以调用C程序)时,通常需要动态链接libc库(在Linux里是glibc)。Go里的net和os/user库都用了cgo。但由于Apline的Linux版本没有libc库,这样在运行时就找不到动态链接,因此报错。它有两种办法来解决:

  • CGO_ENABLED=0:当你在编译Go时加了这个参数,编译时就不会使用cgo,当然也就意味着使用cgo的库都不能用了。这是最简单的办法,但它对你的程序有所限制。
  • 使用musl:musl是一个轻量级的libc库。Apline的Linux版本里自带musl库,你只要加入如下命令就行了。
RUN mkdir /lib64 && ln -s /lib/libc.musl-x86_64.so.1 /lib64/ld-linux-x86-64.so.2

关于musl的详情请参见Statically compiled Go programs, always, even with cgo, using musl

关于这个错误的讨论请参见Installed Go binary not found in path on Alpine Linux Docker

2. Zap报错

Zap是一个很流行的Go日志库,我在程序里用它来输出日志。当加上上面的语句后,原来的错误消失了,但又有一个新的错。它是由Zap产生的。

~ # ./main.exe
panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x1 addr=0x0 pc=0x6a37ab] goroutine 1 [running]:
github.com/jfeng45/k8sdemo/config.initLog(0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, ...)
/app/config/zap.go:94 +0x1fb
github.com/jfeng45/k8sdemo/config.RegisterLog(0x0, 0x0)
/app/config/zap.go:42 +0x42
github.com/jfeng45/k8sdemo/config.BuildRegistrationInterface(0x751137, 0x5, 0x43ab77, 0x984940, 0xc00002c750, 0xc000074f50)
/app/config/appConfig.go:23 +0x26
main.testRegistration()
/app/cmd/main.go:18 +0x3a
main.main()
/app/cmd/main.go:11 +0x20

我现在也不十分清楚出错的原因,应该是跟Musl库有关。估计是Zap用到的某个库与Musl不兼容。我把日志换成另一个库Logrus问题就不存在了。这确实有点小遗憾,Zap是迄今为止我发现的最好的Go日志库。如果你坚持用Zap的话就只能用全版Linux,忍受大的镜像文件;或者改用Logrus日志库,这样就可以享受小的镜像文件。

3. k8s部署不成功

换成Logrus之后,就没再报错,Docker里的程序运行正常。但如果你用这个镜像创建k8s部署时又出了问题。

下面是k8s创建部署的命令:

vagrant@ubuntu-xenial:~/jfeng45/k8sdemo/script/kubernetes/backend$ kubectl get pod k8sdemo-backend-deployment-6b99dc6b8c-2fwnm
NAME READY STATUS RESTARTS AGE
k8sdemo-backend-deployment-6b99dc6b8c-2fwnm 0/1 CrashLoopBackOff 42 3h10m

错误信息是“CrashLoopBackOff”。它产生的原因是容器要求里面的程序一直运行,一旦运行结束,容器就会停掉。k8s发现容器停掉之后会重新部署容器,然后又被停掉,这样就陷入了死循环。

解决的办法是在镜像文件里加入如下命令:

CMD exec /bin/bash -c "trap : TERM INT; sleep infinity & wait"

详情请参见How can I keep a container running on Kubernetes?My kubernetes pods keep crashing with “CrashLoopBackOff” but I can't find any log

4. Pod出错

加入命令,重新生成镜像之后,果然解决了死循环的问题,k8s部署没有报错,但Pod又有了新的错误如下,“k8sdemo-backend-deployment-6b99dc6b8c-n6bnt”的“STATUS”是“Error”。

vagrant@ubuntu-xenial:~/jfeng45/k8sdemo/script/kubernetes/backend$ kubectl get pod
NAME READY STATUS RESTARTS AGE
envar-demo 1/1 Running 8 16d
k8sdemo-backend-deployment-6b99dc6b8c-n6bnt 0/1 Error 1 6s
k8sdemo-database-deployment-578fc88c88-mm6x8 1/1 Running 2 4d21h
nginx-deployment-77fff558d7-84z9z 1/1 Running 3 10d
nginx-deployment-77fff558d7-dh2ms 1/1 Running 3 10d

原因是在Docker文件里运行了如下命令:

CMD exec /bin/bash -c "trap : TERM INT; sleep infinity & wait"

但Alpine里没有“/bin/bash”.需要改成“/bin/sh”,需要修改成如下命令:

CMD exec /bin/sh -c "trap : TERM INT; (while true; do sleep 1000; done) & wait"

修改之后,k8s部署成功,程序运行正常。

源码:

完整源码的github链接

索引

  1. Dockerfile reference
  2. Use multi-stage builds
  3. Statically compiled Go programs, always, even with cgo, using musl
  4. Installed Go binary not found in path on Alpine Linux Docker
  5. How can I keep a container running on Kubernetes?
  6. My kubernetes pods keep crashing with “CrashLoopBackOff” but I can't find any log
  7. Building Docker Containers for Go Applications

本文由博客一文多发平台 OpenWrite 发布!

创建优化的Go镜像文件以及踩过的坑的更多相关文章

  1. Docker创建镜像文件并在容器中运行

    1.如何创建镜像文件 首先找到Docker ToolBox安装的路径,在路径下直接新建Dockerfile文件 在Dockerfile文件里写入的内容为: FROM docker/whalesay:l ...

  2. Linux mkisofs 创建光盘镜像文件(Linux指令学习笔记)

    mkisofs命令 创建光盘文件的系统的命令是mkisofs.光盘系统有多种格式,利用Linux系统提供的光盘文件系统创建 命令mkisofs,可以创建多种iso9660文件系统. 我们一般不用mki ...

  3. 创建Spark镜像文件

    创建Spark镜像文件 1.将spark容器提交到新的镜像中 $>docker commit 01a25bdf1499 myrepos:centos-spark 2.将centos-spark镜 ...

  4. Flask15 远程开发环境搭建、安装虚拟机、导入镜像文件、创建开发环境、pycharm和远程开发环境协同工作

    1 安装VM虚拟机 待更新... 2 导入镜像文件 待更新... 3 启动虚拟机 4 远程连接虚拟机 4.1 安装xShell软件 待更新... 4.2 创建一个新的连接 4.2.1 在虚拟机中获取虚 ...

  5. Win10微软官方最终正式版ISO镜像文件

    Win10微软官方最终正式版ISO镜像文件 据说Windows 10是微软发布的最后一个Windows版本,下一代Windows将作为Update形式出现.Windows 10将发布7个发行版本,分别 ...

  6. Win8.1微软官方最终正式版ISO镜像文件

    Win8.1微软官方最终正式版ISO镜像文件 经过预览版,测试版.开发版本等几个乱七八糟的版本后,2013年10月17日,微软终于如约的发布了Win8.1最终正式版. Win8.1和win8的区别 1 ...

  7. [WinAPI] API 10 [创建、打开、读写文件,获取文件大小]

    在Windows系统中,创建和打开文件都是使用API函数CreateFile,CreateFile通过指定不同的参数来表示是新建一个文件,打开已经存在的文件,还是重新建立文件等.读写文件最为直接的方式 ...

  8. QEMU 使用的镜像文件:qcow2 与 raw

    qcow2 的基本原理 qcow2 镜像格式是 QEMU 模拟器支持的一种磁盘镜像.它也是可以用一个文件的形式来表示一块固定大小的块设备磁盘.与普通的 raw 格式的镜像相比,有以下特性: 更小的空间 ...

  9. [转]如何制作tizen镜像文件(图文教程)?

    http://blog.csdn.net/flydream0/article/details/9163119 之前已讲解了如何下载及编译tizen源码(http://blog.csdn.net/fly ...

随机推荐

  1. Redis压缩包win10快速启动之记录一

    转载请标明出处: http://dujinyang.blog.csdn.net/ 本文出自:[奥特曼超人的博客] Redis压缩包 配置环境变量,直接CMD中启动,默认是打开redis.conf,当然 ...

  2. 关于CPU核心,线程,进程,并发,并行,及java线程之间的关系

    前言:作为一个转行java的小白,一直搞不清楚java中的多线程.于是来梳理一下关于CPU核心,线程,进程,并发,并行,及java线程之间的关系, 1.CPU角度来看: 我们以Intel的Core i ...

  3. Session和Cookie的用法及区别

    1. Session.Cookie是什么 1.1 概念理解 要了解session和cookie是什么,先要了解以下几个概念. 1.1.1 无状态的HTTP协议 协议:是指计算机通信网络中两台计算机之间 ...

  4. Hadoop 之 HDFS基本概念

    1.HDFS的基本概念 答:块(Block).NameNode.DataNode.HDFS的文件被分成块进行存储,默认块的大小为64M,所以说块是文件存储和处理的逻辑单元.NameNode是管理节点, ...

  5. Kilani and the Game-吉拉尼的游戏 CodeForce#1105d 模拟 搜索

    题目链接:Kilani and the Game 题目原文 Kilani is playing a game with his friends. This game can be represente ...

  6. 编程范式 --- 函数式编程(Funtional Programming,简称FP)

    函数式编程(Funtional Programming,简称FP)是一种编程范式,也就是如何编写程序的方法论 主要思想:把计算过程尽量分解成一系列可复用函数的调用 主要特征:函数是"第一等公 ...

  7. mysql 事物四大特性和事物的四个隔离

    1.事物四大特性(ACID) 原子性(atomicity):一个事务必须视为一个不可分割的最小工作单元,整个事务中的所有操作要么全部提交成功,要么全部失败回滚,对于一个事务来说,不可能只执行其中的一部 ...

  8. 夯实Java基础系列13:深入理解Java中的泛型

    目录 泛型概述 一个栗子 特性 泛型的使用方式 泛型类 泛型接口 泛型通配符 泛型方法 泛型方法的基本用法 类中的泛型方法 泛型方法与可变参数 静态方法与泛型 泛型方法总结 泛型上下边界 泛型常见面试 ...

  9. __pycache__

    最近在使用python写一个串口模块的时候,偶然发现运行脚本之后,在工程文件夹下面出现了这样一个文件夹__pycache__,所以就特意到网上查了一下这个文件夹是怎么回事. 我们先在源文件中添加一些内 ...

  10. Spring Boot 2.x基础教程:JSR-303实现请求参数校验

    请求参数的校验是很多新手开发非常容易犯错,或存在较多改进点的常见场景.比较常见的问题主要表现在以下几个方面: 仅依靠前端框架解决参数校验,缺失服务端的校验.这种情况常见于需要同时开发前后端的时候,虽然 ...