函数计算安装依赖库方法小结

  1. 云栖社区>
  2. 阿里云 Serverless Computing>
  3. 博客>
  4. 正文

函数计算安装依赖库方法小结

倚贤 2018-06-14 11:32:01 浏览10108

在通常的编程实践中,项目,库和系统环境需要协同安装和配置。而函数计算的运行环境是预制的,舍弃一些灵活性以换取更好并发效率和系统安全性。当系统和代码在运行期变成只读后,原本系统层面依赖库的安装操作,转移到项目内部。而函数计算作为一种新兴的平台,安装工具还没来得及应对这些变化。本文的目的就是从已有的工具中找到一些适用的方法,以较少手工操作,解决安装依赖库到项目内的问题。

函数计算开发时常需要安装的依赖包分为两类,一类是通过 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 '\n'.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?