• Milvus 编译环境演进


    一、手写动态链接

    Milvus 代码库分为了 C++Go 两个部分,Go 部分负责系统主体架构、分布式系统、存储/查询链路等,C++ 部分负责查询、索引引擎专注于单机场景下的高性能,两者之间通过 cgo 接口调用。

    为了维护两种语言的代码,就需要加入两种语言的生态。Go 作为一个年轻、现代的语言,开箱自带包管理、自动化测试框架和丰富的标准库;而经典的 C++ 就走向了另一个极端,虽然有极致的性能和可控的内存管理,但生态过于碎片化。幸好在 build system 领域,CMake 有成为事实标准的趋势。

    Milvus 很自然的选择 CMake 作为 C++ 构建系统,通过编写 CMakeLists.txt 描述要生成的 library 和 headers,而 Go 则通过 cgo 接口链接到相应的 library,在早期版本里是这样写的:

    1. /*
    2. #cgo CFLAGS: -I${SRCDIR}/../core/output/include
    3. #cgo darwin LDFLAGS: -L${SRCDIR}/../core/output/lib -lmilvus_segcore -Wl,-rpath,"${SRCDIR}/../core/output/lib"
    4. #cgo linux LDFLAGS: -L${SRCDIR}/../core/output/lib -lmilvus_segcore -Wl,-rpath=${SRCDIR}/../core/output/lib
    5. #include "segcore/collection_c.h"
    6. #include "common/type_c.h"
    7. #include "segcore/segment_c.h"
    8. */
    9. import "C"
    10. import (
    11.         "errors"
    12.         "fmt"
    13.         "unsafe"
    14.         "github.com/milvus-io/milvus/internal/util/cgoconverter"
    15. )

    不难发现这样写有几个问题:

    1. 不同操作系统需要指定不同的编译参数

    2. hard code 库文件路径,耦合严重,不利于维护

    以上两个问题相对容易解决,在使用第三方 go library 时,问题会更难解决,例如 Milvus 使用了 GitHub - tecbot/gorocksdb: gorocksdb is a Go wrapper for RocksDB 作为 Go 的 rocksdb 接口。

    gorocksdb 需要修改 CGO 的一系列 go env 才能编译成功,究其原因也是因为 gorocksdb 在使用 rocksdb library 时没有指定 library 和 header 的路径,必须在系统路径中才能找到 librocksdb 。

    1. package gorocksdb
    2. // #include "stdlib.h"
    3. // #include "rocksdb/c.h"
    4. import "C"
    5. import (
    6.         "reflect"
    7.         "unsafe"
    8. )

    这就导致了 Milvus 的编译脚本中需要 hack go env 才能顺利编译:

    1. go env -w CGO_CFLAGS="-I${OUTPUT_LIB}/include"
    2. ldflags=""
    3. if [ -f "${OUTPUT_LIB}/lib/librocksdb.a" ]; then
    4.      case "${unameOut}" in
    5.           Linux*)     ldflags="-L${OUTPUT_LIB}/lib -l:librocksdb.a -lstdc++ -lm -lz";;
    6.           Darwin*)    ldflags="-L${OUTPUT_LIB}/lib -lrocksdb -stdlib=libc++ -lm -lz -lbz2 -ldl";;
    7.           *)          echo "UNKNOWN:${unameOut}"exit 0;
    8.      esac
    9. else
    10.      case "${unameOut}" in
    11.           Linux*)     ldflags="-L${OUTPUT_LIB}/lib64 -l:librocksdb.a -lstdc++ -lm -lz";;
    12.           Darwin*)    ldflags="-L${OUTPUT_LIB}/lib64 -lrocksdb -stdlib=libc++ -lm -lz -lbz2 -ldl";;
    13.           *)          echo "UNKNOWN:${unameOut}" ; exit 0;
    14.       esac
    15. fi
    16. if [ "$MSYSTEM" == "MINGW64" ] ; then
    17.   ldflags="-L${OUTPUT_LIB}/lib -lrocksdb -lstdc++ -lm -lz -lshlwapi -lrpcrt4"
    18. fi
    19. if [[ $(arch) == 'arm64' ]]; then
    20.   go env -w GOARCH=arm64
    21. fi
    22. go env -w CGO_LDFLAGS="$ldflags" && GO111MODULE=on
    23. go get github.com/tecbot/gorocksdb

    二、pkg-config 链接管理

    早期的做法也不是不能用 😏,顶多污染一下环境变量,搭开发环境的时候痛苦一次,容忍度比较高的同学也可以接受。

    为了让更多的开发者顺利的在本地能开发 Milvus,以上问题急需解决。于是在 refine complie configuration by jiaoew1991 · Pull Request #17502 · milvus-io/milvus · GitHub 里引入了 pkg-config 管理 library 和 header 路径。

    在 Milvus 里需要做三个改造:

    一是在 C++ 生成动态链接库时同时生成 pkg-config 的 .pc 文件

    1. function(MILVUS_ADD_PKG_CONFIG MODULE)
    2.     configure_file(${MODULE}.pc.in "${CMAKE_CURRENT_BINARY_DIR}/${MODULE}.pc" @ONLY)
    3.     install(FILES "${CMAKE_CURRENT_BINARY_DIR}/${MODULE}.pc"
    4.           DESTINATION "${CMAKE_INSTALL_LIBDIR}/pkgconfig/")
    5. endfunction()
    1. libdir=@CMAKE_INSTALL_FULL_LIBDIR@
    2. includedir=@CMAKE_INSTALL_FULL_INCLUDEDIR@
    3. Name: Milvus Segcore
    4. Description: Segcore modules for Milvus
    5. Version: @MILVUS_VERSION@
    6. Libs: -L${libdir} -lmilvus_segcore
    7. Cflags: -I${includedir}

    二是在 go 文件中通过 pkg-config 指定 pc 文件

    1. package querynode
    2. /*
    3. #cgo pkg-config: milvus_segcore
    4. #include "segcore/collection_c.h"
    5. #include "common/type_c.h"
    6. #include "segcore/segment_c.h"
    7. */
    8. import "C"
    9. import (
    10.         "errors"
    11.         "fmt"
    12.         "unsafe"
    13.         "github.com/milvus-io/milvus/internal/util/cgoconverter"
    14. )

    三是在编译时修改将 pc 文件路径加入到环境变量中

    1. unameOut="$(uname -s)"
    2. case "${unameOut}" in
    3.     Linux*)     
    4.       export PKG_CONFIG_PATH="${PKG_CONFIG_PATH}:$ROOT_DIR/internal/core/output/lib/pkgconfig:$ROOT_DIR/internal/core/output/lib64/pkgconfig"
    5.       export LD_LIBRARY_PATH="${LD_LIBRARY_PATH}:$ROOT_DIR/internal/core/output/lib:$ROOT_DIR/internal/core/output/lib64"
    6.       export RPATH=$LD_LIBRARY_PATH;;
    7.     Darwin*)    
    8.       export PKG_CONFIG_PATH="${PKG_CONFIG_PATH}:$ROOT_DIR/internal/core/output/lib/pkgconfig"
    9.       export DYLD_LIBRARY_PATH=$ROOT_DIR/internal/core/output/lib
    10.       export RPATH=$DYLD_LIBRARY_PATH;;
    11.     MINGW*)          
    12.       extra_path=$(cygpath -w "$ROOT_DIR/internal/core/output/lib")
    13.       export PKG_CONFIG_PATH="${PKG_CONFIG_PATH};${extra_path}\pkgconfig"
    14.       export LD_LIBRARY_PATH=$extra_path
    15.       export RPATH=$LD_LIBRARY_PATH;;
    16.     *)
    17.       echo "does not supported"
    18. esac

    通过以上的修改,Milvus 的代码无需 hard code library 路径,也无需 hack 环境变量,就可以有效的解决开发环境的搭建问题。

    三、conan 包管理

    在 2.0 之前,Milvus C++ 部分的外部依赖不多,仅有 Boost、Protobuf、Arrow、GTest 等知名的第三方库,而有些已经在 linux 发行版里自带,只要通过 apt、yum、brew 等命令直接安装即可使用。但随着 C++ 引擎支持的索引越来越多、功能愈发复杂,依赖项也急剧膨胀,在 2.2 系列里开始引入 aws-cpp-sdk、marisa、json 等 library,可见的未来中会引入更多的依赖项,C++ 包管理的需求也就提上了日程。

    conan 是目前几个 c++ 包管理产品中相对成熟的,conancenter 中 library 也相对充足,跟 CMake 集成很容易。所以在 https://github.com/milvus-io/milvus/pull/19920 中尝试将 conan 引入作为包管理器。

    C++ 编译受操作系统、编译器类型、C++ 版本、libstdc++ 版本影响很大,这些变量交织在一起会产生很多意想不到的错误,这里把遇到的一些问题整理一番。

    1. 引入 conan 很容易,只要定义 conanfile.txt,在 CMakefiles.txt 中加入几行代码即可。

    1. [requires]
    2. rocksdb/6.29.5
    3. boost/1.80.0
    4. onetbb/2021.3.0
    5. zstd/1.5.2
    6. arrow/8.0.1
    7. openssl/1.1.1q
    8. aws-sdk-cpp/1.9.234
    9. benchmark/1.7.0
    10. gtest/1.8.1
    11. protobuf/3.9.1
    12. rapidxml/1.13
    13. yaml-cpp/0.7.0
    14. marisa/0.2.6
    15. zlib/1.2.13
    16. [generators]
    17. cmake
    18. [options]
    19. rocksdb:shared=True
    20. arrow:parquet=True
    21. arrow:compute=True
    22. arrow:with_zstd=True
    23. aws-sdk-cpp:text-to-speech=False
    24. aws-sdk-cpp:transfer=False
    25. [imports]
    26. lib, *.dylib -> ./lib
    27. lib, *.dll -> ./lib
    28. lib, *.so* -> ../lib

    2. 不同的操作系统,需要选择不同的 libstdcxx 版本。

    1. unameOut="$(uname -s)"
    2. case "${unameOut}" in
    3.   Darwin*)
    4.     conan install ${CPP_SRC_DIR} --install-folder conan --build=missing -s compiler.libcxx=libc++
    5.     ;;
    6.   Linux*)
    7.     # gcc4.8及以下不支持 c++11,而以上的版本如果参数中包含 --with-default-libstdcxx-abi=gcc4-compatitable 也会使用不同的symbol,同样编译报错
    8.     if [[ `gcc -v 2>&1 | sed -n 's/.*\(--with-default-libstdcxx-abi\)=\(\w*\).*/\2/p'== "gcc4" ]]; then
    9.       conan install ${CPP_SRC_DIR} --install-folder conan --build=missing
    10.     else 
    11.       conan install ${CPP_SRC_DIR} --install-folder conan --build=missing -s compiler.libcxx=libstdc++11
    12.     fi 
    13.     ;;
    14.   *)
    15.     echo "Do not support"   
    16.     ;;
    17.     
    18.   esac

    3. 对于 Centos7 等较老版本 Linux,libstdc++ 版本太低,会导致 link 失败,简单的解决办法是使用anaconda 带的 libstdc++.so 。

    1. mkdir /tmp/stdlib && cd /tmp/stdlib && \
    2.     wget https://repo.anaconda.com/archive/Anaconda3-2019.07-Linux-x86_64.sh && \
    3.     sh Anaconda3-2019.07-Linux-x86_64.sh -b -p conda && \
    4.     cp conda/lib/libstdc++.so.6.0.26 /usr/lib64 && \
    5.     rm /usr/lib64/libstdc++.so.6 && \
    6.     ln -s /usr/lib64/libstdc++.so.6.0.26 /usr/lib64/libstdc++.so.6 && \
    7.     cp conda/lib/libatomic.so.1.2.0 /usr/lib64 && \
    8.     ln -s /usr/lib64/libatomic.so.1.2.0 /usr/lib64/libatomic.so && \
    9.     ln -s /usr/lib64/libatomic.so.1.2.0 /usr/lib64/libatomic.so.1 && \
    10.     rm -rf /tmp/stdlib

    可以通过命令 strings /lib64/libstdc++.so.6 | grep GLIBC 查看其版本。

    4. conanfile.txt 中的参数解释。

    1. [options]
    2. rocksdb:shared=True
    3. # Milvus需要parquet支持
    4. arrow:parquet=True
    5. arrow:compute=True
    6. arrow:with_zstd=True
    7. # aws-sdk-cpp 会把aws所有的功能都包含在内,text-to-speech需要太多的依赖项,需要精简
    8. aws-sdk-cpp:text-to-speech=False 
    9. aws-sdk-cpp:transfer=False

    5. windows 有诸多 subsystem(cygwin、mingw、wsl),以及不同的编译器(gcc、clang、visual studio),暂时未找到合适的选项。

    以上就是关于 Milvus 编译环境的演进过程。我们希望通过不断地优化和改进,帮助用户更方便地使用 Milvus,更放心、更简单地享受到向量检索、召回的价值和乐趣!

  • 相关阅读:
    b站黑马JavaScript的Ajax案例代码——新闻列表案例
    【Linux】第五站:Linux权限
    Django 更新数据 save()方法
    python机器学习之门之sklearn的使用(使用鸢尾花数据集)
    springboot生成PDF,并且添加水印
    Spring Cloud应用- Eureka原理、搭建
    【毕业设计】基于单片机的移动便携桌面加湿器 - 物联网 嵌入式
    pycharm新建html
    建模杂谈系列158 再探函数链的实现
    JDK1.5后增加了泛型,那么为什么要有泛型呢?我们该如何自定义泛型结构呢?
  • 原文地址:https://blog.csdn.net/weixin_44839084/article/details/127888200