上一篇文章中,我们提到了针对应用封装“资源容器”。本篇文章中,我们继续聊聊完成容器优化,以及封装最终的应用镜像。
优化容器实现
想要实现前文中相对简单好用的镜像,我们接下来需要依次解决:“镜像融合”
为服务端容器镜像瘦身
在之前的镜像构建中,我们还有两个比较明显的优化点:基础镜像、Python 文件体积依赖比较大。
python:3.9-slim-buster
pymilvusprotobuf
FROM soulteary/milvus:embed-2.1.0
RUN apt-get update && \
apt-get install -y ffmpeg libsm6 libxext6 && \
apt-get remove --purge -y && rm -rf /var/lib/apt/lists/*
RUN pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple && \
pip install diskcache==5.2.1 fastapi==0.65.2 towhee==0.8.0 uvicorn==0.13.4 opencv-python==4.6.0.66 torch==1.12.1 torchvision==0.13.1 Pillow==9.2.0 timm==0.6.7 && \
pip cache purge
WORKDIR /app/src
COPY . /app
RUN python3 encode.py
同时,考虑到我们是在本地运行,其实不一定非常支持 GPU 推理,可以将 GPU 版本的 PyTorch 替换为 CPU Only 版本:
RUN pip install https://download.pytorch.org/whl/cpu/torch-1.12.1%2Bcpu-cp39-cp39-linux_x86_64.whl && \
pip install https://download.pytorch.org/whl/cpu/torchvision-0.13.1%2Bcpu-cp39-cp39-linux_x86_64.whl
在构建过程中,我们也能够看到这两个版本 PyTorch 体积的差距:
=> => # Downloading https://pypi.tuna.tsinghua.edu.cn/packages/1e/2f/06d30fbc76707f14641fe737f0715f601243e039d676be487d0340559c86/torch-1.12.1-cp39-cp39-manylinux1_x86_64.whl (776.4 MB)
=> => # Downloading https://download.pytorch.org/whl/cpu/torch-1.12.1%2Bcpu-cp39-cp39-linux_x86_64.whl (189.2 MB)
soulteary/image-search-app:server-2.1.0
docker run --rm -it --name=milvus soulteary/image-search-app:server-2.1.0
---Milvus Proxy successfully initialized and ready to serve!---
docker exec -it milvus python main.py
2022-09-24 04:58:06,649 | INFO | helpers.py | load_pretrained | 244 | Loading pretrained weights from url (https://github.com/rwightman/pytorch-image-models/releases/download/v0.1-rsb-weights/resnet50_a1_0-14fe96d1.pth)
2022-09-24 04:58:06,758 | ERROR | utils.py | check_file_field | 105 | Form data requires "python-multipart" to be installed.
python-multipartpython-multipart==0.0.5python main.py
2022-09-24 05:00:03,411 | INFO | helpers.py | load_pretrained | 244 | Loading pretrained weights from url (https://github.com/rwightman/pytorch-image-models/releases/download/v0.1-rsb-weights/resnet50_a1_0-14fe96d1.pth)
INFO: Started server process [142]
2022-09-24 05:00:03,539 | INFO | server.py | serve | 64 | Started server process [142]
INFO: Waiting for application startup.
2022-09-24 05:00:03,540 | INFO | on.py | startup | 26 | Waiting for application startup.
INFO: Application startup complete.
2022-09-24 05:00:03,540 | INFO | on.py | startup | 38 | Application startup complete.
INFO: Uvicorn running on http://0.0.0.0:5000 (Press CTRL+C to quit)
2022-09-24 05:00:03,540 | INFO | server.py | _log_started_message | 199 | Uvicorn running on http://0.0.0.0:5000 (Press CTRL+C to quit)
可以看到 Python 推理服务正确的运行起来了,到这里我们的服务端素材镜像的构建和验证就都完成了。
生成前端资源镜像
在前文中,我们已经修正了应用的构建问题。
前文中提到我们要合并前后端实现,所以先暂时将它封装为资源容器镜像,以备后用:
FROM node:18-alpine as Builder
ENV NODE_ENV=production
ENV NODE_OPTIONS="--openssl-legacy-provider"
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
FROM scratch
COPY --from=Builder /app/build /app/assets
将经过调整的代码保存在项目目录[2]后,我们来实现“合并前后端实现”。
借助 ttyd 实现能够进行在线调试的 Web Console
docker cli
能够提供在线 Web Console 的应用有很多,我这里选择的是使用 Golang 实现的 ttyd,项目作者本身有提供镜像,为了得到更小的产物,我们选择基于官方镜像制作更小的产物镜像:
FROM tsl0922/ttyd:latest AS Console
FROM soulteary/milvus:embed-2.1.0 As Builder
RUN apt-get update && \
apt-get install -y upx && \
apt-get remove --purge -y && rm -rf /var/lib/apt/lists/*
WORKDIR /app/console/
COPY --from=Console /usr/bin/ttyd ./
RUN upx -9 -o ttyd.minify ttyd
FROM scratch
COPY --from=Builder /app/console/ttyd.minify /ttyd
在完成镜像构建之后,我们可以通过执行下面的命令,来验证程序被正常压缩处理:
docker run --rm -it soulteary/image-search-app:console-2.1.0 /ttyd -v
ttyd version 1.7.1-942bdab
使用 Golang 实现小而强大的应用网关
docker cli
考虑到实现效率和不同功能之间的资源隔离,我将采用三个独立的程序来实现这个功能,为了能够让程序看起来像是一个,前端程序可以避免不必要的诸如跨域之类的问题。这里选择使用 Golang 实现一个简单的 Web Server,能够提供轻量、高效的访问体验,以及通过反向代理的方式,将上文中准备好的 “Web Console” 和“Python API” 聚合到一块儿。使用 Golang 还有一个额外的好处,就是可以构建出环境无关的、小巧的二进制文件,方便跨容器环境运行。
完整的实现,我上传到了 GitHub,感兴趣的同学可以自取:soulteary/portable-docker-app/reverse-image-search/gateway[3]。
这里,我简单分享三个小技巧,首先是如何实现一个性能不差、资源不会受到篡改、稳定的静态服务器:
//go:embed assets/favicon.png
var Favicon embed.FS
//go:embed assets/index.html
var HomePage []byte
//go:embed assets
var Assets embed.FS
func API(pythonAPI string, consoleAPI string, port string) {
...
r.Any("/", func(c *gin.Context) {
c.Data(http.StatusOK, "text/html; charset=utf-8", HomePage)
})
favicon, _ := fs.Sub(Favicon, "assets")
r.Any("/favicon.png", func(c *gin.Context) {
c.FileFromFS("favicon.png", http.FS(favicon))
})
static, _ := fs.Sub(Assets, "assets/static")
r.StaticFS("/static", http.FS(static))
...
}
上面是使用 Go Embed.Fs 直接将静态文件嵌入程序的例子,如果你想了解更多相关信息,阅读早些时候的两篇文章:《深入浅出 Golang 资源嵌入方案:前篇》[4]、《深入浅出 Golang 资源嵌入方案:go-bindata篇》[5]。
至于快速将其他程序提供的网络服务聚合在一起,可以通过下面的小技巧,创建针对具体地址的反向代理路由:
func createProxy(proxyTarget string) gin.HandlerFunc {
return func(c *gin.Context) {
remote, err := url.Parse(proxyTarget)
if err != nil {
panic(err)
}
proxy := httputil.NewSingleHostReverseProxy(remote)
proxy.Director = func(req *http.Request) {
req.Header = c.Request.Header
req.Host = remote.Host
req.URL.Scheme = remote.Scheme
req.URL.Host = remote.Host
req.URL.Path = c.Param("proxyPath")
}
proxy.ServeHTTP(c.Writer, c.Request)
}
}
最后,在原本的应用代码中,是通过在启动 Nginx 之前,先执行一个配置生成脚本,生成一个静态 JS 文件,作为前端程序配置使用:
#!/bin/bash
rm -rf ./env-config.js
touch ./env-config.js
echo "window._env_ = {" >> ./env-config.js
while read -r line || [[ -n "$line" ]];
do
if printf '%s\n' "$line" | grep -q -e '='; then
varname=$(printf '%s\n' "$line" | sed -e 's/=.*//')
varvalue=$(printf '%s\n' "$line" | sed -e 's/^[^=]*=//')
fi
value=$(printf '%s\n' "${!varname}")
[[ -z $value ]] && value=${varvalue}
echo " $varname: \"$value\"," >> ./env-config.js
done < .env
echo "}" >> ./env-config.js
因为我们将前后端服务合并到了一起,原本生成静态文件交给 Nginx 提供访问的模式不再需要,所以这里可以简单的声明一个路由,将这个原本由“bash shell”生成的文件交给 Go 程序直接输出:
r.GET("/env-config.js", func(c *gin.Context) {
c.Data(http.StatusOK, "application/javascript; charset=utf-8", []byte(`window._env_ = {API_URL: "/api"}`))
c.Abort()
})
在生成“应用网关”的镜像时,我们就可以将前文中提到的“前端资源”镜像利用起来了,将镜像和网关程序打包成一个“干净又卫生”的二进制文件:
FROM soulteary/image-search-app:assets-2.1.0 as Assets
FROM golang:1.19.0-buster AS GoBuilder
RUN sed -i -E "s/\w+.debian.org/mirrors.tuna.tsinghua.edu.cn/g" /etc/apt/sources.list
RUN apt-get update && apt-get install -y upx
ENV GO111MODULE=on
ENV CGO_ENABLED=0
ENV GOPROXY=https://goproxy.cn
WORKDIR /app
COPY --from=Assets /app/assets /app/internal/web/assets
COPY gateway/ ./
RUN go build -ldflags "-w -s" -o gateway main.go && \
upx -9 -o gateway.minify gateway
FROM scratch
COPY --from=GoBuilder /app/gateway.minify /gateway
在构建完毕之后,我们使用容器运行这个服务,来进行简单的页面功能验证:
docker run --rm -it -p 3000:3000 soulteary/image-search-app:gateway-2.1.0 /gateway
命令执行完毕之后,我们将得到下面的日志输出:
Proxy API Addr: http://127.0.0.1:5000
Proxy Console API Addr: http://127.0.0.1:8090
Web Server port: 3000
http://127.0.0.1:3000
在搞定了“前后端”合并之后,我们来实现最终的应用镜像,将上面的服务相对妥善的置入一个容器。
实现 All-In-One 的容器镜像
虽然胖容器不是 Docker 官方推崇的,但是并不影响在本地使用场景中,我们选择这种方案,选择类似方案进行实践的厂商包含:Bitnami、GitLab 、Atlassian 的镜像默认都是这个方案。
为了实现这个方案,我们需要引入一个进程管理工具,我这里的选择是:supervisor。
在 Docker 容器中配置 supervisor
在容器中安装 Supervisor,非常简单,尤其是我们又基于 Debian / Ubuntu 系的操作系统:
FROM soulteary/milvus:embed-2.1.0
LABEL MAINTAINER=soulteary@gmail.com
RUN apt update && apt install supervisor -y && \
apt-get remove --purge -y && rm -rf /var/lib/apt/lists/*
SHELL ["/bin/bash", "-c"]
RUN echo $' \n\
[unix_http_server] \n\
file=/var/run/supervisor.sock \n\
chmod=0700 \n\
[inet_http_server] \n\
port=0.0.0.0:8080 \n\
[supervisord] \n\
nodaemon=true \n\
logfile=/var/log/supervisor/supervisord.log \n\
pidfile=/var/run/supervisord.pid \n\
childlogdir=/var/log/supervisor \n\
[rpcinterface:supervisor] \n\
supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface \n\
[supervisorctl] \n\
serverurl=unix:///var/run/supervisor.sock \n\
[program:milvus] \n\
command=/entrypoint.sh \n\
[program:server] \n\
directory=/app/server \n\
command=python main.py \n\
[program:gateway] \n\
command=/app/gateway/gateway \n\
[program:console] \n\
command=/app/console/ttyd --port=8090 bash \n\
'> /etc/supervisor/supervisord.conf
CMD ["/usr/bin/supervisord","-c","/etc/supervisor/supervisord.conf"]
上面的 Dockerfile 中,不但能够完成程序的安装,还声明了一个配置,用于将 Supervisor 本身的管理界面、Web Console、应用网关、向量数据库 Milvus、Python 推理服务都进行进程管理,确保服务能够持续运行,遇到故障进行自动恢复。
在搞定 Supervisor 的安装和配置之后,我们来将上文中的各种“资源镜像”糅合到这个镜像中,在 Dockerfile 的头部先添加资源镜像,并分别为它们起好别名。
FROM soulteary/image-search-app:server-2.1.0 AS Server
FROM soulteary/image-search-app:gateway-2.1.0 AS Gateway
FROM soulteary/image-search-app:console-2.1.0 AS Console
FROM soulteary/milvus:embed-2.1.0
LABEL MAINTAINER=soulteary@gmail.com
...
接着,在 Dockerfile 尾部追加从资源镜像复制资源的指令:
...
CMD ["/usr/bin/supervisord","-c","/etc/supervisor/supervisord.conf"]
COPY --from=Gateway /gateway /app/gateway/
COPY --from=Console /ttyd /app/console/ttyd
COPY --from=Server /app/server /app/server
COPY --from=Server /usr/local/lib/python3.9/site-packages /usr/local/lib/python3.9/
COPY --from=Server /root/.cache/torch /root/.cache/torch
COPY --from=Server /root/.towhee /root/.towhee
当我们构建完镜像之后,分别使用两个终端执行下面两条命令:
docker run --rm -it --name=milvus -p 3000:3000 -p 8080:8080 -v `pwd`/images:/images soulteary/image-search-app:2.1.0
docker exec -it milvus python /app/server/encode.py
libGL.solibGLX.so
# extra deps for python application
COPY --from=Server /usr/lib/x86_64-linux-gnu/libGL.so.1.7.0 /usr/lib/x86_64-linux-gnu/
COPY --from=Server /usr/lib/x86_64-linux-gnu/libGLX.so.0.0.0 /usr/lib/x86_64-linux-gnu/
COPY --from=Server /usr/lib/x86_64-linux-gnu/libGLdispatch.so.0.0.0 /usr/lib/x86_64-linux-gnu/
COPY --from=Server /usr/lib/x86_64-linux-gnu/libX11.so.6.3.0 /usr/lib/x86_64-linux-gnu/
COPY --from=Server /usr/lib/x86_64-linux-gnu/libXext.so.6.4.0 /usr/lib/x86_64-linux-gnu/
...
RUN ln -s /usr/lib/x86_64-linux-gnu/libGL.so.1.7.0 /usr/lib/x86_64-linux-gnu/libGL.so.1 && \
ln -s /usr/lib/x86_64-linux-gnu/libGLX.so.0.0.0 /usr/lib/x86_64-linux-gnu/libGLX.so.0 && \
ln -s /usr/lib/x86_64-linux-gnu/libGLdispatch.so.0.0.0 /usr/lib/x86_64-linux-gnu/libGLdispatch.so.0 && \
ln -s /usr/lib/x86_64-linux-gnu/libX11.so.6.3.0 /usr/lib/x86_64-linux-gnu/libX11.so.6 && \
...
在完成最终的 Docker 镜像的编写之后,我们将镜像构建为容器,然后推送到 DockerHub 上,通过 DockerHub 平台的二次镜像压缩,就能够得到一个相对小巧又好用的、开箱即用的“以图搜图” Docker 镜像啦。
完整的程序代码,我上传到了 soulteary/portable-docker-app/reverse-image-search[6],有需要的同学可以自取。
最后
不知不觉又写了这么多,希望这篇文章能够帮助到想要快速搭建图搜系统,实现以图搜图功能的同学,也希望详尽的设计和容器应用调优,能够给你启发,让你的程序性能越来越棒!
好了,这次就先写到这里了。
--EOF
引用链接
[1][2][3][4][5][6]
如果你觉得内容还算实用,欢迎点赞分享给你的朋友,在此谢过。
如果你想更快的看到后续内容的更新,请戳 “点赞”、“分享”、“喜欢” ,这些免费的鼓励将会影响后续有关内容的更新速度。
本文使用「署名 4.0 国际 (CC BY 4.0)」许可协议,欢迎转载、或重新修改使用,但需要注明来源。 署名 4.0 国际 (CC BY 4.0)