在Docker上建立Go镜像文件并不困难,但创建的文件很大,接近1G,使用起来不太方便。Docker镜像的一个主要难题就是如何优化,建立小的镜像。咱们能够用多级构建的方法来建立Docker镜像文件,它也不复杂。但因为使用这种方法时,须要用简版的Linux(Alpine),它带来了一系列的问题。本文讲述如何解决这些问题并成功建立优化的Go镜像文件,优化以后只有14M。linux
咱们用一个Go程序做为例子来展现如何建立Go镜像。下面就是这个程序的目录结构。nginx
Go程序的具体内容并不重要,只要能运行就好了。咱们重点关注“docker”子目录(“kubernetes”子目录里的文件有别的用途,会在另外的文章中讲解)。它里面有三个文件。“docker-backend.sh”是建立镜像的命令文件,“Dockerfile-k8sdemo-backend”是多级构建文件,“Dockerfile-k8sdemo-backend-full”是单级构建文件,git
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”镜像文件。请阅读文件中的注释以得到解释。github
生成镜像容器golang
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
docker run -td --name k8sdemo-backend-full k8sdemo-backend-full
登陆镜像容器, 其中“a95c”是容器ID的前四位。ubuntu
docker exec -it a95c /bin/bash
文件里有一条语句须要特别解释一下“COPY . .”,它把文件从宿主机拷贝到镜像里,在镜像里已经用“WORKDIR”设置了当前工做目录,那么宿主机的“.”(当前目录)是哪一个目录呢?它不是Dockerfile文件所在的目录,而是你运行“Docker build”命令时所在的目录。bash
咱们要把整个程序都拷贝到镜像里,那么在运行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,就只有镜像名。app
docker build -f ./script/kubernetes/backend/docker/Dockerfile-k8sdemo-backend-full -t k8sdemo-backend-full .
详情请参见Dockerfile referencedom
这样建立的镜像用的是全版的Linux系统,所以比较大,大概接近1G。若是要想优化,就要用多级构建。
单级构建只有一个“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),致使我踩了不少坑,下面看看这些坑是如何被填上的。
建立镜像成功后,登陆镜像:
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库,这样在运行时就找不到动态连接,所以报错。它有两种办法来解决:
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
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日志库,这样就能够享受小的镜像文件。
换成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
加入命令,从新生成镜像以后,果真解决了死循环的问题,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部署成功,程序运行正常。
本文由博客一文多发平台 OpenWrite 发布!