优化部署go的docker镜像大小
因为属实不想看论文不想做实验了(似乎我并没有认真搞多久呐)
决定摸鱼玩一玩 :)
方糖通知近期经常丢消息,延迟到达,他已经不是一个可靠的通知平台了,并且也因为腾讯限制了模板消息导致开发者准备下线业务。
除此之外,我还用了邮件通知作为辅助,不是很优雅,每次写代码的时候需要把一个写好的硬编码了 smtp 登录凭据的 python 脚本拿着到处跑,并且腾讯强制一个月修改一次密码,每次修改麻烦。此外从发送了通知到手机QQ 邮箱弹出消息可能延迟半分钟,保证尽量小的时间差这点,QQ 邮箱做得不够好。仔细想想似乎也不能把锅都给QQ,如果轮询时间间隔太小或者长连接可能消耗手机电量比较严重,半分钟已经是及时性和耗电二者的妥协了吧 :)
于是搞一个通知的小工具提上日程 :)
提出需求希望可以实现多种方式推送的平台,类似于 server 酱,发一个 http 请求,手机收到通知。
通知的平台希望支持
- 微信
- 邮箱
- 短信
那么这个通知平台需要实现一个 api,根据请求里面的 token判定用户,给用户发送通知消息。html页面可选。
寻找解决方案首先,因为网络限制,FCM 推送是不可能的,ifttt 的 webhook,pushbullet, pushover 这些就暂不考虑了。
兜兜转转找了一圈,发现点过 star 的项目两个
-
https://github.com/gotify/server
-
https://github.com/nikoksr/notify
gotify 支持安卓、电脑的浏览器等终端,但是他需要一个常驻后台的安卓程序,而我的 ColorOS7 杀后台比较激进,除了微信有免死金牌,其他的基本都活不下来。
notify 支持 mail,一定程度上符合我的需求,但是时效性有不少问题。
手机短信通知大可不必,要钱钱的,于是乎用微信的通知成了唯一解。
那么就找一个支持企业微信的通知的轮子。
找了一圈有一个比较类似的
- https://github.com/cloverzrg/wechat-work-message-push-go
但是存在一个问题,他还添加了 grafana,似乎是想用 grafana 的报警功能推送到微信
去除 grafana 基本就符合我用企业微信发送通知的功能了,但是除此之外我还需要短信推送、邮件推送的功能。
那么决定在他的基础上,根据我的需求写写。
写业务代码思考了下用 python 还是 go 写这个东西。python的话就 flask/fastapi 一把梭,但是写起来少了不少乐趣,用 python 写更多的是为了实现这个需求,而不是开心的摸鱼 :)
非常巧,申请了开源之夏的项目过了,这个项目是要根据需求写 go 代码和改代码 bug,由于我写过的 go 代码不及 python 的百分之一,要顺利地完成开源之夏的项目我似乎还得再学习学习。
看完了 wechat-work-message-push-go 的执行逻辑,结合企业微信开发文档,发现企业微信发通知的逻辑比较简单,分为 2 步:
- 根据 corpID, corpSecret 请求微信的 api 获取 access_token,access_token 两个小时内有效,获取到之后保存起来,过期了及时更新即可
- 携带 access_token 请求消息通知的 api,请求体中放 AgentId ,接收人的 id和通知内容
用 python 实现这个基础的需求并且不考虑异常处理,我觉得用不到 30 行代码 :)
但是我还是决定用 go 写 :)
三下五除二写好了,目前只写了企业微信的通知,邮件通知就下次摸鱼的时候再写吧,剩下的就是部署了。
编译成二进制放到到服务器上,然后 caddy 套个反代加了层 https 就收工了。
docker 部署丢一个二进制到服务器就完事儿了,似乎感觉少了点什么 :)
还没有摸鱼完呢 就搞完了
那么就再套娃一个 docker 吧
能跑就行的 dockerfile v1.0首先写一个 dockerfile,因为 1.13 后默认开启了 go mod,因此 go build 的时候会自动下载 go mod的依赖,这里我们只需要设定 goproxy 环境变量就行。此外我之前一直用的七牛的 goproxy.cn,但是最近在实验室连接他会有网络问题,于是换成了 goproxy.io,速度伯仲之间吧。 这里专门为设定环境变量加了一层RUN大可不必,可以直接加 environment或者和 go build放到一起,不过和这巨大的尺寸比起来,这点优化 duck 不必
1 2 3 4 5 6 7 FROM golang:1.16 AS builder WORKDIR /app COPY . . RUN go env -w GOPROXY=https://goproxy.io,direct RUN go build -o app . CMD ["./app"]好的,构建完了,972MB,中规中矩,但这对于一个简单的 web 应用的镜像来说,可以用巨大来形容了
1 2 3 docker images wxworkmsgbot_bot REPOSITORY TAG IMAGE ID CREATED SIZE wxworkmsgbot_bot latest 5c7364d4e983 2 minutes ago 972MB 多步构建,减小尺寸既然用 go 写的,运行时的依赖不是问题,可以多步构建把 artifact 复制到第二部分的镜像里面。 于是,花了一分钟时间,把 docker官方文档示例 里面的多步构建的示例复制过来。
1 2 3 4 5 6 7 8 9 10 11 FROM golang:1.16 WORKDIR /go/src/github.com/alexellis/href-counter/ RUN go get -d -v golang.org/x/net/html COPY app.go . RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app . FROM alpine:latest RUN apk --no-cache add ca-certificates WORKDIR /root/ COPY --from=0 /go/src/github.com/alexellis/href-counter/app . CMD ["./app"]示例里面加了不少参数,我刚复制的时候还不知道为什么他要做这些操作,于是减法减法减法,只留下我想要的部分,得到了 v2.0 版本的 dockerfile
1 2 3 4 5 6 7 8 9 10 11 12 13 14 FROM golang:1.16 AS builder WORKDIR /app COPY . . RUN go env -w GOPROXY=https://goproxy.io,direct RUN go build -o app . CMD ["./app"] FROM alpine:latest WORKDIR /app COPY --from=builder /app/app . CMD ["./app"]好的,非常完美, docker-compose up --build 一气呵成
一看大小只有 16.1MB,比刚刚好多了,缩小到了原来的 16.1/972=1.66% 心情愉悦 ^_^
1 2 3 4 docker images wxworkmsgbot_bot REPOSITORY TAG IMAGE ID CREATED SIZE wxworkmsgbot_bot latest 5e8c0cb18517 6 seconds ago 16.1MB再去看看启动情况,留下了一行日志就退出了
1 2 3 Recreating wxworkmsgbot_bot_1 ... done Attaching to wxworkmsgbot_bot_1 bot_1 | standard_init_linux.go:219: exec user process caused: no such file or directory问题不大,复制这行报错,面向 stackoverflow 编程
很快发现这是因为默认启用了 CGO 导致的,把他禁用就行了
此时此刻,恰如彼时彼刻,想起来, docker 文档里面就是做了这个操作的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 FROM golang:1.16 AS builder WORKDIR /app COPY . . RUN go env -w GOPROXY=https://goproxy.io,direct RUN CGO_ENABLED=0 go build -o app . CMD ["./app"] FROM alpine:latest WORKDIR /app COPY --from=builder /app/app . CMD ["./app"]加上这一行,没有问题
1 2 3 $ docker images wxworkmsgbot_bot REPOSITORY TAG IMAGE ID CREATED SIZE wxworkmsgbot_bot latest 1dfff8d58731 18 hours ago 16.1MB那么 docker文档剩下的参数是干什么的,尤其是 installsuffix,以前没看到过。
1 2 3 4 5 $ go tool link ... -installsuffix suffix set package directory suffix可以看到意思是安装的路径前缀,不过还是不太懂,继续搜了下,发现 go 开发者 ianlancetaylor 在一个 issue 里面说在新版本的 go 里面不需要了,那么我就暂且不管他。
正常运行了,于是请求一下 api
1 Get "https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid=ffffff&corpsecret=xfdasfasf": x509: certificate signed by unknown authority看起来是缺了证书
此时此刻,恰如彼时彼刻
docker 的文档里面给 alpine 装了个证书,原来是搞这个用的
那么再加进去
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 FROM golang:1.16 AS builder WORKDIR /app COPY . . RUN go env -w GOPROXY=https://goproxy.io,direct RUN CGO_ENABLED=0 go build -o app . CMD ["./app"] FROM alpine:latest RUN apk --no-cache add ca-certificates WORKDIR /app COPY --from=builder /app/app . CMD ["./app"]好的,这下真的一整个流程都 work 了
镜像大小为 16.4MB
1 2 3 docker images wxworkmsgbot_bot REPOSITORY TAG IMAGE ID CREATED SIZE wxworkmsgbot_bot latest 59af2bbb0b1c 34 seconds ago 16.4MB 继续优化大小真正的缩减大小,从现在开始,思路有两个,第一,减小二进制的大小;第二,使用 scratch 而不是 alpine。
首先减小二进制的大小。
很久以前摸鱼的时候,发现go build的结果有不少优化的空间,比如 -s去除符号表 -w 去除调试信息、-trimpath 去除路径信息(反溯源的目的)等,于是乎再修改一下 dockerfile
1 2 3 4 5 6 7 8 9 10 11 12 FROM golang:1.16 AS builder WORKDIR /app COPY . . RUN go env -w GOPROXY=https://goproxy.io,direct RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-w -s" --trimpath -o app . FROM alpine:latest RUN apk --no-cache add ca-certificates WORKDIR /app COPY --from=builder /app/app . CMD ["./app"]经过测试,正常运行,不过缩减大小并不明显,事后想想似乎是 alpine占据太大的空间,二进制文件可能已经减小了 1/3了。
1 2 3 docker images wxworkmsgbot_bot REPOSITORY TAG IMAGE ID CREATED SIZE wxworkmsgbot_bot latest ad00fe8a6c01 24 seconds ago 13.5MB下一步就是并不常见的压缩二进制的步骤:upx加壳,虽然他更多的是用来混淆对抗杀软的,但是他的压缩性能是真的可以,让我想在这里试试
放一个 upx 进去,再加一个最大压缩比例的参数,于是有了新的 dockerfile
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 FROM golang:1.16 AS builder COPY upx /app/upx WORKDIR /app COPY . . RUN go env -w GOPROXY=https://goproxy.io,direct RUN CGO_ENABLED=0 go build -ldflags="-w -s" --trimpath -o app . RUN /app/upx --best app FROM alpine #FROM scratch RUN apk --no-cache add ca-certificates WORKDIR /app COPY --from=builder /app/app . CMD ["./app"]其中构建过程中压缩的输出如下
1 2 3 4 5 6 7 8 9 10 11 12 ---> Running in d9e31badd0b6 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 -------------------- ------ ----------- ----------- 7462912 -> 2428916 32.55% linux/amd64 app Packed 1 file. Removing intermediate container d9e31badd0b6经过测试,工作正常,现在的容器只有8.51MB了,其中,二进制文件压缩前有 7462912/1024/1024=7.1MB,压缩后只有 2.3 MB,剩下的空间是 alpine 占据的
1 2 3 4 docker images wxworkmsgbot_bot REPOSITORY TAG IMAGE ID CREATED SIZE wxworkmsgbot_bot latest 72ec286b8d99 35 seconds ago 8.51MB那么是时候干掉 alpine 了
使用 scratchalpine 相比于 go 的二进制文件,还是过大了,那么可以试试把他去掉,用 scratch ,整个容器有且仅有这一个二进制文件,岂不是很酷 :)
只在多年以前刚开始看 docker 教程的时候用过静态编译的丢进去运行,从此再也没玩过了 :(
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 FROM golang:1.16 AS builder COPY upx /app/upx WORKDIR /app COPY . . RUN go env -w GOPROXY=https://goproxy.io,direct RUN CGO_ENABLED=0 go build -ldflags="-w -s" --trimpath -o app . RUN /app/upx --best app FROM scratch #FROM alpine #RUN apk --no-cache add ca-certificates WORKDIR /app COPY --from=builder /app/app . CMD ["./app"]构建后大小为
1 2 3 4 docker images wxworkmsgbot_bot REPOSITORY TAG IMAGE ID CREATED SIZE wxworkmsgbot_bot latest aa2d5689d063 2 minutes ago 2.43MB很明显,这里也会有缺证书的问题,搜索一圈可以发现有个全干工程师 在一年前发的一篇文章,从前到后和我的思路一样,连 upx 的参数都一样 太离谱了:)
那么就参考他的,直接拷贝证书过去
1 COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/于是乎有了最终能用的版本的 dockerfile
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 FROM golang:1.16 AS builder COPY upx /app/upx WORKDIR /app COPY . . RUN go env -w GOPROXY=https://goproxy.io,direct RUN CGO_ENABLED=0 go build -ldflags="-w -s" --trimpath -o app . RUN /app/upx --best app FROM scratch WORKDIR /app COPY --from=builder /app/app . COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ CMD ["./app"]最终容器的大小
1 2 3 docker images wxworkmsgbot_bot REPOSITORY TAG IMAGE ID CREATED SIZE wxworkmsgbot_bot latest 706e3549f0ad 2 minutes ago 2.64MB到此告一段落,一个有且仅有一个二进制,还有证书的容器搞完了。
到头来想想,似乎没必要这样追求极致。
毕竟这距离极致还有很远。
还可以把二进制里面再继续分析,继续减小。
就像看到了终极笔记的终极一样,终极真的就是想要追求的终极么?
alpine 因为用的 musl 而不是 glibc而导致的问题,过于精简导致的依赖缺失的问题,让我感觉还是 debian 比较靠谱 :)
所以我还是回归我最爱的 debian,把二进制放进去,完事儿 :)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 FROM golang:1.16 AS builder WORKDIR /app COPY . . RUN go env -w GOPROXY=https://goproxy.io,direct RUN CGO_ENABLED=0 go build -ldflags="-w -s" --trimpath -o app . FROM debian WORKDIR /app COPY --from=builder /app/app . COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ CMD ["./app"]主流的镜像都是基于 debian 的,因此我这里再开一次不会多占用空间的 :)
1 2 3 docker images wxworkmsgbot_bot REPOSITORY TAG IMAGE ID CREATED SIZE wxworkmsgbot_bot latest d83396e4d977 About a minute ago 122MB所以,这就是真正的最后的镜像了 :)
refer-
https://docs.docker.com/develop/develop-images/multistage-build/
- https://colobu.com/2018/08/13/create-minimal-docker-image-for-go-applications/
- https://juejin.cn/post/6844904174396637197
- https://github.com/golang/go/issues/9344#issuecomment-69944514
适度摸鱼,有益健康
过度摸鱼,不能毕业 :)
看文档写代码花了半天时间
摸鱼写这篇博客,半天时间又没了
笑看下周一组会我如何交代我给导师说的我这周准备复现的论文
我现在还在看这篇论文,还没写代码 :)
事情越多越不想搞 :(
6月30号的时候,王爷说我们现在三年级了 :(
那么一瞬间
我突然就慌了55555
不慌
晚上再来一把 csgo
看看我白银一的真正实力
Les1ie
2021.7.3 15:19