Milvus 編譯環境演進

Zilliz發表於2022-11-22

一、手寫動態連結

Milvus 程式碼庫分為了 C++Go 兩個部分,Go 部分負責系統主體架構、分散式系統、儲存/查詢鏈路等,C++ 部分負責查詢、索引引擎專注於單機場景下的高效能,兩者之間透過 cgo 介面呼叫。

為了維護兩種語言的程式碼,就需要加入兩種語言的生態。Go 作為一個年輕、現代的語言,開箱自帶包管理、自動化測試框架和豐富的標準庫;而經典的 C++ 就走向了另一個極端,雖然有極致的效能和可控的記憶體管理,但生態過於碎片化。幸好在 build system 領域,CMake 有成為事實標準的趨勢。

Milvus 很自然的選擇 CMake 作為 C++ 構建系統,透過編寫 CMakeLists.txt 描述要生成的 library 和 headers,而 Go 則透過 cgo 介面連結到相應的 library,在早期版本里是這樣寫的:

/*
#cgo CFLAGS: -I${SRCDIR}/../core/output/include
#cgo darwin LDFLAGS: -L${SRCDIR}/../core/output/lib -lmilvus_segcore -Wl,-rpath,"${SRCDIR}/../core/output/lib"
#cgo linux LDFLAGS: -L${SRCDIR}/../core/output/lib -lmilvus_segcore -Wl,-rpath=${SRCDIR}/../core/output/lib
#include "segcore/collection_c.h"
#include "common/type_c.h"
#include "segcore/segment_c.h"
*/
import "C"
import (
        "errors"
        "fmt"
        "unsafe"
        "github.com/milvus-io/milvus/internal/util/cgoconverter"
)

不難發現這樣寫有幾個問題:

1. 不同作業系統需要指定不同的編譯引數

2. hard code 庫檔案路徑,耦合嚴重,不利於維護

以上兩個問題相對容易解決,在使用第三方 go library 時,問題會更難解決,例如 Milvus 使用了 https://github.com/tecbot/gorocksdb 作為 Go 的 rocksdb 介面。

gorocksdb 需要修改 CGO 的一系列 go env 才能編譯成功,究其原因也是因為 gorocksdb 在使用 rocksdb library 時沒有指定 library 和 header 的路徑,必須在系統路徑中才能找到 librocksdb 。

package gorocksdb

// #include "stdlib.h"
// #include "rocksdb/c.h"
import "C"
import (
        "reflect"
        "unsafe"
)

這就導致了 Milvus 的編譯指令碼中需要 hack go env 才能順利編譯:

go env -w CGO_CFLAGS="-I${OUTPUT_LIB}/include"
ldflags=""
if [ -f "${OUTPUT_LIB}/lib/librocksdb.a" ]; then
     case "${unameOut}" in
          Linux*)     ldflags="-L${OUTPUT_LIB}/lib -l:librocksdb.a -lstdc++ -lm -lz";;
          Darwin*)    ldflags="-L${OUTPUT_LIB}/lib -lrocksdb -stdlib=libc++ -lm -lz -lbz2 -ldl";;
          *)          echo "UNKNOWN:${unameOut}"; exit 0;
     esac
else
     case "${unameOut}" in
          Linux*)     ldflags="-L${OUTPUT_LIB}/lib64 -l:librocksdb.a -lstdc++ -lm -lz";;
          Darwin*)    ldflags="-L${OUTPUT_LIB}/lib64 -lrocksdb -stdlib=libc++ -lm -lz -lbz2 -ldl";;
          *)          echo "UNKNOWN:${unameOut}" ; exit 0;
      esac
fi

if [ "$MSYSTEM" == "MINGW64" ] ; then
  ldflags="-L${OUTPUT_LIB}/lib -lrocksdb -lstdc++ -lm -lz -lshlwapi -lrpcrt4"
fi

if [[ $(arch) == 'arm64' ]]; then
  go env -w GOARCH=arm64
fi

go env -w CGO_LDFLAGS="$ldflags" && GO111MODULE=on
go get github.com/tecbot/gorocksdb

二、pkg-config 連結管理

早期的做法也不是不能用 ?,頂多汙染一下環境變數,搭開發環境的時候痛苦一次,容忍度比較高的同學也可以接受。

為了讓更多的開發者順利的在本地能開發 Milvus,以上問題急需解決。於是在 https://github.com/milvus-io/milvus/pull/17502 裡引入了 pkg-config 管理 library 和 header 路徑。

在 Milvus 裡需要做三個改造:

一是在 C++ 生成動態連結庫時同時生成 pkg-config 的 .pc 檔案

function(MILVUS_ADD_PKG_CONFIG MODULE)
    configure_file(${MODULE}.pc.in "${CMAKE_CURRENT_BINARY_DIR}/${MODULE}.pc" @ONLY)
    install(FILES "${CMAKE_CURRENT_BINARY_DIR}/${MODULE}.pc"
          DESTINATION "${CMAKE_INSTALL_LIBDIR}/pkgconfig/")
endfunction()
libdir=@CMAKE_INSTALL_FULL_LIBDIR@
includedir=@CMAKE_INSTALL_FULL_INCLUDEDIR@

Name: Milvus Segcore
Description: Segcore modules for Milvus
Version: @MILVUS_VERSION@

Libs: -L${libdir} -lmilvus_segcore
Cflags: -I${includedir}

二是在 go 檔案中透過 pkg-config 指定 pc 檔案

package querynode

/*
#cgo pkg-config: milvus_segcore

#include "segcore/collection_c.h"
#include "common/type_c.h"
#include "segcore/segment_c.h"
*/
import "C"
import (
        "errors"
        "fmt"
        "unsafe"
        "github.com/milvus-io/milvus/internal/util/cgoconverter"
)

三是在編譯時修改將 pc 檔案路徑加入到環境變數中


unameOut="$(uname -s)"
case "${unameOut}" in
    Linux*)     
      export PKG_CONFIG_PATH="${PKG_CONFIG_PATH}:$ROOT_DIR/internal/core/output/lib/pkgconfig:$ROOT_DIR/internal/core/output/lib64/pkgconfig"
      export LD_LIBRARY_PATH="${LD_LIBRARY_PATH}:$ROOT_DIR/internal/core/output/lib:$ROOT_DIR/internal/core/output/lib64"
      export RPATH=$LD_LIBRARY_PATH;;
    Darwin*)    
      export PKG_CONFIG_PATH="${PKG_CONFIG_PATH}:$ROOT_DIR/internal/core/output/lib/pkgconfig"
      export DYLD_LIBRARY_PATH=$ROOT_DIR/internal/core/output/lib
      export RPATH=$DYLD_LIBRARY_PATH;;
    MINGW*)          
      extra_path=$(cygpath -w "$ROOT_DIR/internal/core/output/lib")
      export PKG_CONFIG_PATH="${PKG_CONFIG_PATH};${extra_path}\pkgconfig"
      export LD_LIBRARY_PATH=$extra_path
      export RPATH=$LD_LIBRARY_PATH;;
    *)
      echo "does not supported"
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 中加入幾行程式碼即可。

[requires]
rocksdb/6.29.5
boost/1.80.0
onetbb/2021.3.0
zstd/1.5.2
arrow/8.0.1
openssl/1.1.1q
aws-sdk-cpp/1.9.234
benchmark/1.7.0
gtest/1.8.1
protobuf/3.9.1
rapidxml/1.13
yaml-cpp/0.7.0
marisa/0.2.6
zlib/1.2.13

[generators]
cmake

[options]
rocksdb:shared=True
arrow:parquet=True
arrow:compute=True
arrow:with_zstd=True
aws-sdk-cpp:text-to-speech=False
aws-sdk-cpp:transfer=False

[imports]
lib, *.dylib -> ./lib
lib, *.dll -> ./lib
lib, *.so* -> ../lib

2. 不同的作業系統,需要選擇不同的 libstdcxx 版本。

unameOut="$(uname -s)"
case "${unameOut}" in
  Darwin*)
    conan install ${CPP_SRC_DIR} --install-folder conan --build=missing -s compiler.libcxx=libc++
    ;;
  Linux*)
    # gcc4.8及以下不支援 c++11,而以上的版本如果引數中包含 --with-default-libstdcxx-abi=gcc4-compatitable 也會使用不同的symbol,同樣編譯報錯
    if [[ `gcc -v 2>&1 | sed -n 's/.*\(--with-default-libstdcxx-abi\)=\(\w*\).*/\2/p'` == "gcc4" ]]; then
      conan install ${CPP_SRC_DIR} --install-folder conan --build=missing
    else 
      conan install ${CPP_SRC_DIR} --install-folder conan --build=missing -s compiler.libcxx=libstdc++11
    fi 
    ;;
  *)
    echo "Do not support"   
    ;;
    
  esac

3. 對於 Centos7 等較老版本 Linux,libstdc++ 版本太低,會導致 link 失敗,簡單的解決辦法是使用anaconda 帶的 libstdc++.so 。

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

可以透過命令 strings /lib64/libstdc++.so.6 | grep GLIBC 檢視其版本。

4. conanfile.txt 中的引數解釋。

[options]
rocksdb:shared=True
# Milvus需要parquet支援
arrow:parquet=True
arrow:compute=True
arrow:with_zstd=True
# aws-sdk-cpp 會把aws所有的功能都包含在內,text-to-speech需要太多的依賴項,需要精簡
aws-sdk-cpp:text-to-speech=False 
aws-sdk-cpp:transfer=False

5. windows 有諸多 subsystem(cygwin、mingw、wsl),以及不同的編譯器(gcc、clang、visual studio),暫時未找到合適的選項。

以上就是關於 Milvus 編譯環境的演進過程。我們希望透過不斷地最佳化和改進,幫助使用者更方便地使用 Milvus,更放心、更簡單地享受到向量檢索、召回的價值和樂趣!

相關文章