函式計算安裝依賴庫方法小結

倚賢發表於2018-06-14

在通常的程式設計實踐中,專案,庫和系統環境需要協同安裝和配置。而函式計算的執行環境是預製的,捨棄一些靈活性以換取更好併發效率和系統安全性。當系統和程式碼在執行期變成只讀後,原本系統層面依賴庫的安裝操作,轉移到專案內部。而函式計算作為一種新興的平臺,安裝工具還沒來得及應對這些變化。本文的目的就是從已有的工具中找到一些適用的方法,以較少手工操作,解決安裝依賴庫到專案內的問題。

函式計算開發時常需要安裝的依賴包分為兩類,一類是通過 apt 包管理工具安裝的 deb 軟體包。另一類是具體語言環境包管理工具(如 maven, pip 等)安裝的包。下面我們先分析一下不同語言環境的包管理器。

包管理器的安裝目錄

目前函式計算支援的語言執行環境為:Java/Python/Nodejs。這三種語言對應的包管理工具分別對應為 maven/pip/npm。下面我們分別討論一下這些包管理器的安裝目錄。

maven

maven 是 Java 平臺的包管理工具。maven 工具會從中央庫或者私有庫將專案檔案 pom.xml 宣告的依賴下載到 $M2_HOME/repository 目錄內。M2_HOME 的預設值為 $HOME/.m2 。 一臺開發機上的所有 Java 專案都共享這個本地 repository 目錄下的 jar 包。由於 mvn package 階段, 所有依賴的 jar 包都會被打包進最後的交付物內。所以 Java 執行時並沒有依賴 $M2_HOME/repository 下的檔案。

pip

pip 是 python 平臺的包管理工具。pip 是當下最流行和推薦的 python 包管理方式。但是把安裝包安裝包本地目錄會涉及到 python 包管理的很多細節,為了更好的理解,先展開討論一下 python 包管理的發展歷程。

2004 年之前推薦的安裝方式是 setup.py, 下載一個模組以後,可以使用這個模組提供的 setup.py 檔案

python setup.py install

setup.py 是利用 distutils 的功能寫成的。distutils 是 python 標準庫的一部分,2000年釋出,用於 python 模組的構建和安裝。

所以使用 setup.py 也可以釋出一個 python 模組

python setup.py sdist

甚至可以打包成 rpm 或者 exe 安裝包

python setup.py bdist_rpm
python setup.py bdist_wininst

setup.py 類似於 Makefile,可使用者構建和安裝。但是沒有將構建和安裝分離,每個使用者在 install 的過程中都執行一次構建有些浪費。所以 Python 社群有了 setuptools。setuptools 釋出於 2004 年,它包含了 easy_install 工具。與之一起 python 也有了 egg 格式和 PyPi 線上倉庫,對標 java 社群的 jar 格式和 Maven 倉庫。

線上模組倉庫 PyPi 帶了兩個主要的優勢

  • 只需要安裝預編譯打包好的 egg 包格式,效率更好
  • 解決了包依賴的問題,依賴包可以自動從 PyPi 下載安裝

2008 年,pip 工具釋出,開始逐步替代 easy_install,目前已經是 python 包管理的事實標準。pip 希望不再使用 Eggs 格式(雖然它支援 Eggs),而更希望採用 wheel 格式。而且 pip 也支援從程式碼版本倉庫(如 github)安裝模組。

下面我們在來看一下 python 模組的目錄結構,egg 和 wheel 都將安裝檔案分為五大類 purelib、platlib、headers、scripts 和 data 目錄。

目錄 安裝位置 用途
purelib $prefix/lib/pythonX.Y/site-packages 純 python 實現庫
platlib $exec-prefix/lib/pythonX.Y/site-packages 平臺相關的動態連結庫
headers $prefix/include/pythonX.Yabiflags/distname C 標頭檔案
script $prefix/bin 可執行檔案
data $prefix 資料檔案。例如 .conf 配置檔案,初始化 SQL 檔案之類的

$prefix$exec-prefix 是 python 的編譯器引數,可以通過 sys.prefixsys.exec_prefix 獲得。在 linux 系統下預設值都是 /usr/local

npm

npm 是 nodejs 平臺的包管理工具。npm install 命令將依賴包下載到當前目錄的 node_modules 目錄內,nodejs 執行時依賴的庫可以完全依賴於當前目錄內。但是 nodejs 有些庫依賴本地環境,會在安裝的時候構建。這些本地依賴庫會存在兩個問題,其一,構建環境和執行的環境如果不一致(比如 windows 下構建,linux 下執行),那可能無法執行。其二,假如構建時安裝了一些開發庫和執行庫,這些通過作業系統包管理工具(如 apt-get)在本地安裝的動態連結庫在執行環境的 container 裡可能不存在。

遇到的問題

瞭解了不同語言包管理器的安裝到本地的目錄結構後,再來看看函式計算安裝依賴庫遇到的問題。

依賴安裝在全域性系統目錄

Maven 和 pip 會把依賴包安裝在專案目錄之外的系統目錄。Maven 的構建時會把所以外部依賴都打包進最終交付物。所以 Maven 通常沒有執行時依賴問題。即使不用 Maven 進行工程管理的 Java 專案,在當前目錄或者其子目錄存放依賴的 jar 包,並且最終一起打包也是通常的做法。所以 Java Runtime 不存在這個問題。相比之下 pip 所管理的 Python 環境,就有此問題。pip 會把依賴安裝到系統目錄,而 函式計算的生產環境不可寫(除了 /tmp 目錄),也沒有辦法提供預製環境。

原生依賴

Python 和 Nodejs 常見庫檔案依賴系統的原生環境。需要安裝編譯環境和執行時動態連結庫。這兩種情況的移植性都是很不好的。

在函式計算所使用的 Debain/Ubuntu 系統,使用 apt 包管理系統安裝軟體和庫。預設情況下這些軟體和庫都會被安裝到系統目錄如 /usr/bin/usr/lib/usr/local/bin/usr/local/lib 等。所以原生依賴也需要想辦法安裝到本地目錄。

解決辦法

通常的相應的解法也很直觀:

  1. 執行依賴安裝的開發系統和生產執行系統保持一致。使用 fcli 提供的 sbox 環境進行依賴安裝。
  2. 依賴檔案都放到本地目錄。把 pip 的 module,可執行檔案,動態連結庫 .so 檔案都放拷貝到當前目錄

但把依賴檔案放置到當前目錄在實踐過程中往往並不容易。

  1. pip 和 apt-get 安裝的庫檔案會散落到系統的很多目錄裡,需要對不同包管理系統有深入的瞭解才能找回這些檔案。
  2. 庫檔案有傳遞依賴,往往安裝某個庫,會把一堆這個庫依賴的庫都安裝進去,手工去遍歷這些依賴是非常繁瑣的。

所以我們的問題歸結到,如何方便地把依賴安裝到當前目錄,減少手工操作。下面我們會分別介紹 pip 和 apt 包管理系統的多種方法,並比較其優劣。

依賴安裝到當前目錄

Python

方法一:使用 --install-option 引數
pip install --install-option="--install-lib=$(pwd)" PyMySQL

--install-option 會將引數傳遞給 setup.py, 而我們知道無論是 .egg 還是 .whl 檔案裡都不存在 setup.py 檔案。--install-option 會觸發基於原始碼包的安裝流程,setup.py 會觸發模組的構建流程。

--install-option 有如下選項

檔案型別 可選項
Python modules –install-purelib
extension modules –install-platlib
all modules –install-lib
scripts –install-scripts
data –install-data
C headers –install-headers

--install-lib 的效果是同時覆蓋 --install-purelib--install-platlib 的值。

另外 --install-option="--prefix=$(pwd)" 也可以安裝在當前目錄,但是這個會在當前目錄建立 lib/python2.7/site-packages 子目錄結構。

優點

  • 可以有選擇地將模組裝在本地,比如 purelib

缺點

  • 不適用沒有原始碼包的模組
  • 觸發構建系統,未體現 wheel 包的優勢
  • 需要完整安裝需要設定的引數較多,比較繁瑣
方法二:使用 --target 或者 -t 引數
pip install --target=$(pwd) PyMySQL

--target 是 pip 後來提供的引數,模組會被直接安裝到當前目錄,不會產生 lib/python2.7/site-packages 子目錄解構。該個方法簡單好用,比較適合依賴較少的情況。

方法三:結合使用 PYTHONUSERBASE--user 引數
PYTHONUSERBASE=$(pwd) pip install --user PyMySQL

使用--user 引數,使得模組被安裝到 site.USER_BASE 目錄。該目錄的預設值在 Linux 系統裡是 ~/.local,MacOS 裡是 ~/Library/Python/X.Y,Windows 下是 %APPDATA%PythonPYTHONUSERBASE 環境變數可以修改掉 site.USER_BASE 的值。

--user 的安裝效果和 --prefix= 的效果類似,也會產生 lib/python2.7/site-packages 子目錄結構

方法四:使用 virtualenv
pip install virtualenv
virtualenv path/to/my/virtual-env
source path/to/my/virtual-env/bin/activate
pip install PyMySQL

virutalenv 是 python 社群推薦的玩法,使用 virutalenv 可以不汙染全域性環境。 virtualenv 不但會把需要的模組本地化(如 PyMySQL),也會把包管理相關的工具也本地化,如 setuptools 、pip、wheel。這些模組會增大包的尺寸,但執行時並不需要。

apt-get

apt-get 安裝的連結庫和可執行檔案也需要安裝到本地目錄。網上推薦 chrootapt-get -o RootDir=$(pwd) 的方法,經過一番嘗試都碰到一些問題走不下去。在這個基礎上做了些改進,使用 apt-get 下載 deb 包, dpkg 安裝 deb 包。

apt-get install -d -o=dir::cache=$(pwd) libx11-6 libx11-xcb1 libxcb1
for f in $(ls ./archives/*.deb)
do 
    dpkg -x $pwd/archives/$f $pwd
done

如何執行

Java 通過設定 classpath 來轉載 jar 和 class 檔案。nodejs 會自動裝載當前目錄下 node_modules 下面的 package 。這些都是常見用法,此處不再贅述。

python

python 會從 sys.path 說指向的目錄列表裡裝載 module 檔案。

> import sys
> print `
`.join(sys.path)

/usr/lib/python2.7
/usr/lib/python2.7/plat-x86_64-linux-gnu
/usr/lib/python2.7/lib-tk
/usr/lib/python2.7/lib-old
/usr/lib/python2.7/lib-dynload
/usr/local/lib/python2.7/dist-packages
/usr/lib/python2.7/dist-packages

由於 sys.path 預設會包含當前目錄,因為使用 --target 或者 -t 引數的方法會將 module 安裝在當前目錄,所以上面提到的方法二無需設定 sys.path。

sys.path 是可以編輯的陣列,所以在程式開始處使用 sys.path.append(dir) 即可。為了讓程式更具備可移植新也可以使用環境變數 PYTHONPATH。

export PYTHONPATH=$PYTHONPATH:$(pwd)/lib/python2.7/site-packages

apt-get

apt-get 安裝的可執行檔案和動態連結庫,需要保證在到 PATH 和 LD_LIBRARY_PATH 環境變數裡設定的目錄列表裡能找到。

PATH

PATH 變數是系統用來查詢可執行程式的路徑列表,比較簡單,把 bin 、usr/bin 和 usr/local/bin 等 bin 或者 sbin 目錄都通通加到 PATH 裡去。

export PATH=$(pwd)/bin:$(pwd)/usr/bin:$(pwd)/usr/local/bin:$PATH

注意上面是 bash 的寫法,在 java,python,nodejs 裡如何修改當前程式的 PATH 環境變數請做響應的調整。

LD_LIBRARY_PATH

LD_LIBRARY_PATH 類似於 PATH,是用來查詢動態連結庫的路徑列表。通常系統會把動態連結放到 /lib/usr/lib/usr/local/lib 目錄下。但是有些模組也會放在這些目錄的子目錄裡,比如 /usr/lib/x86_64-linux-gnu。這些子目錄通常都會記錄在 /etc/ld.so.conf.d/ 下的檔案裡。

cat /etc/ld.so.conf.d/x86_64-linux-gnu.conf
# Multiarch support
/lib/x86_64-linux-gnu
/usr/lib/x86_64-linux-gnu

所以 $(pwd)/etc/ld.so.conf.d/ 下所有檔案裡宣告的目錄裡的 so 檔案也需要能從 LD_LIBRARY_PATH 環境變數裡的目錄列表裡找到。

注意,執行時修改環境變數 LD_LIBRARY_PATH 可能不生效,至少對於 python 這個問題是已知的。LD_LIBRARY_PATH 變數裡已經預設了 /code/lib 目錄。所以一個可行的辦法是用軟連結把依賴的 so 都軟鏈到 /code/lib 目錄下

小結

本文重點解決的是 pip 和 apt-get 命令如何將庫安裝到本地目錄,而後執行時如何設定環境變數讓本地安裝的庫檔案被程式找到。

python 提供的 4 種方法,對於常見的場景都是適用的。細微的差別也在上文中有提到,使用的繁簡程式也有略有差別的,可能根據自己的偏好選擇使用。

apt-get 也提供了一種可行的辦法,該方法不是唯一的選擇,相比其他可行的方法,該方法考慮到已經安裝在系統裡的 deb 包,就不再安裝了,以節省程式包的尺寸。為了進一步節省尺寸也可以把安裝進去的執行時無關的檔案刪除掉,如使用者手冊 man。

本文是定製更好工具的一個技術積累的過程,基於此,我們會進一步推出更好用的工具,來簡化開發過程。

參考閱讀

  1. How does python find packages?
  2. Pip User Guide
  3. python-lambda-local
  4. python-lambda
  5. Python 包管理工具解惑
  6. Running apt-get for another partition/directory?


相關文章