本篇文章,将聊聊如何在容器中完成支持多 CPU 架构(x86、ARM)、多种 CPU 类型(Intel、AMD)的 OpenBLAS 的 Docker 镜像,来让包括 Milvus 在内的,使用 OpenBLAS 的软件的 Docker 镜像构建可以“又稳又快”。

我会试着在接下来的内容中,持续分享如何让一个拥有着一万多颗“星星”的大型数据库项目的容器构建过程不断提速。

写在前面

有阅读过以往内容的同学,应该会记得,我在之前的介绍“Milvus 源码编译安装”的文章中,有提到过“要聊聊如何构建一个可靠、可信赖的 Milvus 容器镜像”。

之前提到过,因为 Milvus 使用的核心搜索引擎 knowhere 有使用到 OpenBLAS。以及,相信有过本地构建经验、或者参考前面文章“走进向量计算:从源码编译 OpenBLAS”进行过构建的同学,对于 OpenBLAS 项目的构建时间漫长,应该是深有体会的。并且,在不同 CPU 架构(x86、ARM),不同类型 CPU(Intel、AMD)的环境下,OpenBLAS 编译产物也是不同的。

所以,如果我们想搞定支持多种 CPU 架构的 Milvus 容器镜像,自然要先解决多种架构和类型硬件的 OpenBLAS 在容器内的构建。

如果我们使用 GitHub Action (免费版),你会发现 CI 跑一天都构建不出来 OpenBLAS 这个基础依赖,即使我们采用 8 Cores 这类常见规格的云服务器,也需要“哼哧哼哧”的跑个一个钟头,这里如果我们使用 docker buildx 来模拟不同硬件的话,很有可能跑 4~5 个钟头不见得有结果(可以参考 CI 中大量跑了一天以上被自动取消的任务)。

而如果我们使用预构建的方式,临时采用“按需付费”的方式,找一台配置较高的机器,或者利用本地高配置的机器,花十几分钟到半个小时,提前做好预构建产物的编译。那么,之后的构建时间,通常就能够缩短到只需要“几秒钟”了,因为文件复制的计算量非常少。

所以,想要减少开发和构建 Milvus 所需要的时间,在确定的容器环境中,制作预构建产物来减少重复构建花费的大量时间,就变得十分必要的啦。

准备工作

既然是容器环境的产物预构建,那么,我们自然需要先完成 Docker 运行环境的安装,如果你的构建环境中已经安装过了 Docker,那么可以跳过这个小节,阅读后面的内容。

如果你是桌面运行环境,可以访问官网下载安装文件,如果你使用的是服务端环境,可以参考这篇文章中的“更简单的 Docker 安装”,来完成 Docker 环境的准备。

当然,如果你没有运行 Linux 系统的设备,使用 macOS 或者在 Windows 中使用虚拟机,也是一样的。

当然,不同硬件架构能够使用的编译参数是有不同的,所以这里,我们需要根据未来计划运行 Milvus 或其他软件所需要的硬件环境,准备对应的硬件,来完成基础依赖的编译。即使Docker Buildx 只能模拟 CPU 架构来进行 Multi-ARCH 构建,但是却无法模拟 CPU 类型,无法满足不同指令集的硬件产物构建。

前文中,我们有提到需要构建不止一种产物镜像,简单来说:

  • 我们需要构建出最新和次新 Ubuntu LTS 环境下的软件,包含目前 Milvus 长期使用的 0.3.9 版本,以及未来考虑升级的最新的稳定的版本:0.3.20 。
  • 以及考虑到目前 Milvus 官方镜像还是基于 Ubuntu 18.04,所以构建这个环境下的产物,对于能够平滑替换,进行验证也是必要的。
  • 考虑到目前不仅是云环境,还是本地环境, ARM 芯片和 AMD Zen 架构的 CPU 都越来越多,所以,我们也需要考虑这两个环境下的产物构建。

当然,因为 OpenBLAS 不同版本,在ARM 芯片、AMD ZEN 架构 CPU 下存在一些兼容问题,即使我们能够得到构建产物,产物其实也并不能够通过软件的单元测试。所以,我们在构建的过程中,会忽略掉构建结果不能 100% 通过测试的“组合”。

好了,我们先来聊聊最常见的 Intel CPU 的产物构建吧。

基于 Intel x86 架构 CPU 的容器预构建

因为不同类型、架构的 CPU,能够构建不同 OpenBLAS 的可靠产物是不同的,所以我们先来明确要构建哪些版本。在 Intel x86 芯片环境下,我们需要满足下面的需求:

  • 最新 Ubuntu LTS 版本 22.04 环境下的两个版本的 OpenBLAS:0.3.9 和 0.3.20,满足未来 Milvus 升级到最新的 Ubuntu LTS 时使用。
  • 上一个 Ubuntu 稳定 LTS 版本 20.04 环境下的 OpenBLAS:0.3.9 和 0.3.20,满足未来 Milvus 能够升级到次新 LTS 版本时使用。
  • 目前 Milvus 官方镜像使用的 Ubuntu LTS 版本 18.04 环境下的 OpenBLAS:0.3.9 和 0.3.20,满足当前版本的 Milvus ,能够平滑切换 OpenBLAS 依赖安装,以及验证最新版本的 OpenBLAS 使用。

简单来说,就是我们需要分别构建 Ubuntu 18.04~22.04 环境下,0.3.9 和 0.3.20 两个版本的 OpenBLAS 镜像,来满足当前状况的 Milvus、过渡期的 Milvus,以及适合长远的 Milvus 使用的 OpenBLAS 预构建产物。

设计 Intel CPU 使用的通用 Dockerfile 镜像文件

即使我们需要根据排列组合做镜像,但可维护性依旧是我们最需要考虑的地方,因此,我们最好是能够使用一个 Dockerfile 文件来管理同 CPU 架构的镜像(按照 CPU 架构拆分镜像):

# 允许使用参数来动态改变我们所使用的基础镜像
ARG LTS=22.04
FROM ubuntu:${LTS} AS Base
# (示意) 安装必要的依赖
RUN apt-get update && \
    apt-get install ... && \
    apt-get remove --purge -y
# 设置工作目录
WORKDIR /src
# 允许使用参数来指定 OpenBLAS 的版本,从官方发布页面获取软件源码
ARG OPENBLAS_VERSION=0.3.9
ENV OPENBLAS_VERSION=${OPENBLAS_VERSION}
RUN wget "https://github.com/xianyi/OpenBLAS/archive/v${OPENBLAS_VERSION}.tar.gz" && \
    tar zxvf v${OPENBLAS_VERSION}.tar.gz && rm v${OPENBLAS_VERSION}.tar.gz
# 改变工作目录
WORKDIR /src/OpenBLAS-${OPENBLAS_VERSION}
# (示意) 使用适合 Intel 芯片的参数,进行编译和安装
RUN make && make install

# 将构建后的产物保存到一个干净的空镜像里,为后续使用做准备
FROM scratch
ARG OPENBLAS_VERSION=0.3.9
ENV OPENBLAS_VERSION=${OPENBLAS_VERSION}
COPY --from=Base /usr/lib/libopenblas-r${OPENBLAS_VERSION}.so /usr/lib/

上面是隐藏了一些细节(依赖安装、编译命令)的基础镜像设计,首先根据用户传递的构建参数,来确定要使用的基础 Linux 环境,和要构建的 OpenBLAS 产物版本。然后再将构建完毕的内容,复制到一个崭新的空白容器里,来简化容器复杂度,以及方便后续 Milvus 或其他软件的构建过程使用。

或许有小伙伴好奇,为什么一定要使用多阶段构建呢。如果我们没有进行多阶段构建,剥离环境和构建产物,那么我们得到的预构建镜像,大概会是下面这样的“壮观”体积。

soulteary/milvus-openblas     0.3.20-intel-x86-ubuntu-22.04     3.95GB
soulteary/milvus-openblas     0.3.20-intel-x86-ubuntu-20.04     9.03GB
soulteary/milvus-openblas     0.3.20-intel-x86-ubuntu-18.04     7.54GB
soulteary/milvus-openblas     0.3.9-intel-x86-ubuntu-22.04      3.35GB
soulteary/milvus-openblas     0.3.9-intel-x86-ubuntu-20.04      6.68GB
soulteary/milvus-openblas     0.3.9-intel-x86-ubuntu-18.04      5.55GB

当采用了多阶段构建之后,即使在不进行额外压缩的前提下,产物镜像尺寸也能够得到非常明显的变化:

soulteary/milvus-openblas     0.3.20-intel-x86-ubuntu-22.04     143MB
soulteary/milvus-openblas     0.3.20-intel-x86-ubuntu-20.04     318MB
soulteary/milvus-openblas     0.3.20-intel-x86-ubuntu-18.04     266MB
soulteary/milvus-openblas     0.3.9-intel-x86-ubuntu-22.04      122MB
soulteary/milvus-openblas     0.3.9-intel-x86-ubuntu-20.04      241MB
soulteary/milvus-openblas     0.3.9-intel-x86-ubuntu-18.04      201MB

完整文件,我上传到了 GitHub,方便有需要的同学自取:soulteary/docker-openblas/blob/main/intel/Dockerfile

我们将上面的内容保存为 Dockerfile,就能够正式进行 Intel x86 CPU 环境下的镜像构建了。

Ubuntu 和 Intel 环境下的 OpenBLAS 构建

先来处理 Ubuntu 22.04 系统环境下,0.3.9 版本 OpenBLAS 的构建:

docker build --build-arg=LTS=22.04 --build-arg=OPENBLAS_VERSION=0.3.9 -t soulteary/milvus-openblas:0.3.9-intel-22.04 .

执行命令,经过漫长的等待之后,将能够看到类似下面的输出,包含了一大堆“测试通过”的日志信息和容器“构建完毕”的提示:

...
OPENBLAS_NUM_THREADS=2 ./xscblat1
 Real CBLAS Test Program Results


 Test of subprogram number  1         CBLAS_SDOT     
                                    ----- PASS -----

 Test of subprogram number  2         CBLAS_SAXPY    
                                    ----- PASS -----
...
 cblas_zsyr2k PASSED THE TESTS OF ERROR-EXITS

 cblas_zsyr2k PASSED THE COLUMN-MAJOR COMPUTATIONAL TESTS (  1764 CALLS)
 cblas_zsyr2k PASSED THE ROW-MAJOR    COMPUTATIONAL TESTS (  1764 CALLS)

 END OF TESTS
...
make[1]: Leaving directory '/src/OpenBLAS-0.3.9/exports'

 OpenBLAS build complete. (BLAS CBLAS LAPACK)

  OS               ... Linux             
  Architecture     ... x86_64               
  BINARY           ... 64bit                 
  C compiler       ... GCC  (command line : gcc)
  Fortran compiler ... GFORTRAN  (command line : gfortran)
  Library Name     ... libopenblas-r0.3.9.a (Single threaded)  

To install the library, you can run "make PREFIX=/path/to/your/installation install".

Removing intermediate container 41f18b3da43e
 ---> c5545163375d
Step 10/10 : RUN make PREFIX=/usr NO_STATIC=1 install
 ---> Running in 80583523b475
make -j 8 -f Makefile.install install
make[1]: Entering directory '/src/OpenBLAS-0.3.9'
Generating openblas_config.h in /usr/include
Generating f77blas.h in /usr/include
Generating cblas.h in /usr/include
Copying LAPACKE header files to /usr/include
Copying the shared library to /usr/lib
Generating openblas.pc in /usr/lib/pkgconfig
Generating OpenBLASConfig.cmake in /usr/lib/cmake/openblas
Generating OpenBLASConfigVersion.cmake in /usr/lib/cmake/openblas
Install OK!
make[1]: Leaving directory '/src/OpenBLAS-0.3.9'
...
...
Removing intermediate container 80583523b475
 ---> 980e4e15139b
Successfully built 980e4e15139b
Successfully tagged soulteary/milvus-openblas:0.3.9-intel-20.04

如果你的构建环境获取 GitHub Release 中的源码包存在网络问题,可以考虑在构建参数中使用 --build-arg=https_proxy=YOUR_PROXY_ADDR ,强制内容获取走你的指定网络来解决问题。

同样的,我们可以执行下面的命令,来搞定 Ubuntu 22.04 环境下,OpenBLAS 0.3.20 版本的镜像构建:

docker build --build-arg=LTS=22.04 --build-arg=OPENBLAS_VERSION=0.3.20 -t soulteary/milvus-openblas:0.3.20-intel-22.04 .

同理,我们可以根据切换参数的内容,来完成 Ubuntu 20.04 和 Ubuntu 18.04 系统版本下 OpenBLAS 0.3.9 和 0.3.20 两个版本的镜像构建:

docker build \
    --build-arg=LTS=20.04 \
    --build-arg=OPENBLAS_VERSION=0.3.9 \
    -t soulteary/milvus-openblas:0.3.9-intel-x86-ubuntu-20.04 .
docker build \
    --build-arg=LTS=20.04 \
    --build-arg=OPENBLAS_VERSION=0.3.20 \
    -t soulteary/milvus-openblas:0.3.20-intel-x86-ubuntu-20.04 .
...

为了节约篇幅,完整内容,我已经上传到了 GitHub,有需要的同学可以自取:soulteary/docker-openblas/blob/main/intel/build.sh

基于 AMD Zen 架构 CPU 的容器预构建

和 Intel x86 小节中的最大不同是,在 AMD Zen 架构的 CPU 的容器构建中,由于比较老的版本的 OpenBLAS 在该架构上的兼容性存在问题,即使能构建出来产物,看着一堆堆的测试报错、警告,以及测试安装时的错误日志,也没有人能放心的使用它们,所以我们只构建 OpenBLAS 0.3.20 版本。

此外,在构建 Intel x86 架构 CPU 的时候,我们的构建参数使用的是 TARGET=CORE2,在构建 AMD Zen 架构镜像的时候,需要替换为 TARGET=ZEN。完整的镜像文件,我上传到了 GitHub:soulteary/docker-openblas/blob/main/amd-zen/Dockerfile,有需要可以自取。

构建命令,和构建 Intel x86 时类似,也是通过改变参数,来调整构建环境,和使用软件的版本:

docker build \
    --build-arg=LTS=22.04 \
    --build-arg=OPENBLAS_VERSION=0.3.20 \
    -t soulteary/milvus-openblas:0.3.20-intel-x86-ubuntu-22.04 .

docker build \
    --build-arg=LTS=20.04 \
    --build-arg=OPENBLAS_VERSION=0.3.20 \
    -t soulteary/milvus-openblas:0.3.20-intel-x86-ubuntu-20.04 .

docker build \
    --build-arg=LTS=18.04 \
    --build-arg=OPENBLAS_VERSION=0.3.20 \
    -t soulteary/milvus-openblas:0.3.20-intel-x86-ubuntu-18.04 .

命令执行完毕,将会得到类似下面的结果:

...
make[1]: Leaving directory '/src/OpenBLAS-0.3.20/exports'

 OpenBLAS build complete. (BLAS CBLAS LAPACK)

  OS               ... Linux             
  Architecture     ... x86_64               
  BINARY           ... 64bit                 
  C compiler       ... GCC  (cmd & version : gcc (Ubuntu 11.2.0-19ubuntu1) 11.2.0)
  Fortran compiler ... GFORTRAN  (cmd & version : GNU Fortran (Ubuntu 11.2.0-19ubuntu1) 11.2.0)
  Library Name     ... libopenblas-r0.3.20.a (Single-threading)  
  Supporting multiple x86_64 cpu models with minimum requirement for the common code being ZEN

To install the library, you can run "make PREFIX=/path/to/your/installation install".

Removing intermediate container f276420c3691
 ---> a094af160f34
Step 10/10 : RUN make PREFIX=/usr NO_STATIC=1 install
 ---> Running in aa4ce3961832
make -j 16 -f Makefile.install install
make[1]: Entering directory '/src/OpenBLAS-0.3.20'
Generating openblas_config.h in /usr/include
Generating f77blas.h in /usr/include
Generating cblas.h in /usr/include
Copying LAPACKE header files to /usr/include
Copying the shared library to /usr/lib
Generating openblas.pc in /usr/lib/pkgconfig
Generating OpenBLASConfig.cmake in /usr/lib/cmake/openblas
Generating OpenBLASConfigVersion.cmake in /usr/lib/cmake/openblas
Install OK!
make[1]: Leaving directory '/src/OpenBLAS-0.3.20'
...
Removing intermediate container aa4ce3961832
 ---> 5aebe4bd2ed3
Successfully built 5aebe4bd2ed3
Successfully tagged soulteary/milvus-openblas:0.3.20-amd-zen-ubuntu-22.04

最终,将镜像都构建完毕之后,我们能够得到下面的结果:

soulteary/milvus-openblas     0.3.20-amd-zen-ubuntu-22.04     143MB
soulteary/milvus-openblas     0.3.20-amd-zen-ubuntu-18.04     266MB
soulteary/milvus-openblas     0.3.20-amd-zen-ubuntu-20.04     318MB

基于 ARMv64 架构 CPU 的容器预构建

依旧是先准备 Dockerfile,和 AMD Zen 架构 CPU 遇到的问题类似,0.3.9 版本和一些 Ubuntu 发行版中,我们会在构建过程和结果中遇到一些报错和警告,虽然能够得到构建产物,但是和上面的原因一样,我们需要的是稳定、可靠的产物,所以,可以排除掉 0.3.9 版本的 OpenBLAS 的所有“镜像组合”。

对于 ARMv64 设备的镜像构建,我们可以使用两种方式。一是使用在以往文章中提到过的 buildx 来进行构建,下面的命令将在构建完毕之后,自动将镜像推送到 DockerHub 中:

docker buildx build -t group/name:version -f ./Dockerfile.armv8 --push --platform=linux/arm64 .

此外,我们还可以选择和上文中构建 Intel / AMD x86 CPU 一样,将构建的事情,放在具有这个硬件架构的设备上完成构建。相比较前者,这样的构建效率有质的不同(快的多的多),恰好这几种 CPU 的设备我手头都有,所以我就选择第二种方案啦。

不过,和上文中 x86 CPU 的构建配置还是有一些不同,我们需要指定构建参数为 TARGET=ARMV8,完整 Dockerfile,我上传到了 GitHub soulteary/docker-openblas/blob/main/armv8/Dockerfile,有需要可以自取。

在准备好 Dockerfile 之后,我们使用下面的命令进行 ARMv8 环境下的镜像构建:

docker build \
    --build-arg=LTS=22.04 \
    --build-arg=OPENBLAS_VERSION=0.3.20 \
    -t soulteary/milvus-openblas:0.3.20-armv8-ubuntu-22.04 .

docker build \
    --build-arg=LTS=20.04 \
    --build-arg=OPENBLAS_VERSION=0.3.20 \
    -t soulteary/milvus-openblas:0.3.20-armv8-ubuntu-20.04 .

docker build \
    --build-arg=LTS=18.04 \
    --build-arg=OPENBLAS_VERSION=0.3.20 \
    -t soulteary/milvus-openblas:0.3.20-armv8-ubuntu-18.04 .

相比较 x86 环境下的构建,ARMv8 环境的产物镜像会构建的飞快,并且产物会小巧不少:

soulteary/milvus-openblas   0.3.20-armv8-ubuntu-22.04     88.8MB
soulteary/milvus-openblas   0.3.20-armv8-ubuntu-20.04     169MB
soulteary/milvus-openblas   0.3.20-armv8-ubuntu-18.04     147MB

好啦,到这里如何在容器中构建 OpenBLAS 就分享完毕啦。

如何使用

镜像的使用本为两部分,第一部分是获取镜像,你既可以使用我制作好的镜像,也可以进行自行构建。第二部分,则是在容器中使用多阶段构建,完成“软件安装”(跨镜像文件 COPY)。

如果你不想花费时间重新构建这几类不同硬件环境的镜像,可以使用我提过的镜像文件,经过 DockerHub 的压缩,这些镜像的尺寸得以进一步变得苗条,最小的镜像不过 20MB,最大的也才 47 MB:https://hub.docker.com/repository/docker/soulteary/milvus-openblas/tags

如果你希望自行构建,根据自己的需求改变构建参数,使用起来心里更踏实,也可以参考我的项目 https://github.com/soulteary/docker-openblas,并结合上文进行构建参数调整,来进行本地构建。

聊完了镜像的获取,我们来看看镜像在容器中如何使用吧。

关于预构建镜像的使用,其实非常简单,就如同我们执行 make install 一样,将文件拷贝到正确的目录中,并按照“传统”用软链做好副本的重命名即可,比如这样:

FROM soulteary/milvus-openblas:0.3.20-intel-x86-ubuntu-20.04 AS OpenBLAS

FROM ubuntu:20.04
LABEL maintainer=soulteary@gmail.com

COPY --from=OpenBLAS /usr/lib/libopenblas-r0.3.20.so /usr/lib/
RUN ln -s /usr/lib/libopenblas-r0.3.20.so /usr/lib/libopenblas.so.0 && \
    ln -s /usr/lib/libopenblas.so.0 /usr/lib/libopenblas.so

在使用过程中,我们只需要保证基础镜像(Ubuntu)版本是对号的,CPU 运行环境是对号的,那么就都能够通过上面这种“跨镜像”复制的方式,来减少 Milvus 或者其他软件构建过程中的时间浪费。

最后

既然本文主要聊 OpenBLAS 的容器构建,Milvus 相关的内容,我们还是在之后的文章中,再进行展开吧。

在接下来的向量数据库相关的内容中,我们将继续聊聊之前立过的各种 Flag。以及不断分享如何把一个原本需要非常长时间构建的软件,不断优化到一个喝杯水就能搞定的程度。

毕竟使用这类带有“魔法”的软件完成一些好玩的事情之外,让这些“魔法”能够生效的更快、能量更强,也是一件十分有趣的事情。

–EOF