在通常的编程实践中,项目,库和系统环境需要协同安装和配置。而函数计算的运行环境是预制的,舍弃一些灵活性以换取更好并发效率和系统安全性。当系统和代码在运行期变成只读后,原本系统层面依赖库的安装操作,转移到项目内部。而函数计算作为一种新兴的平台,安装工具还没来得及应对这些变化。本文的目的就是从已有的工具中找到一些适用的方法,以较少手工操作,解决安装依赖库到项目内的问题。
函数计算开发时常需要安装的依赖包分为两类,一类是通过 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.prefix
和 sys.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
等。所以原生依赖也需要想办法安装到本地目录。
解决办法
通常的相应的解法也很直观:
- 执行依赖安装的开发系统和生产执行系统保持一致。使用 fcli 提供的 sbox 环境进行依赖安装。
- 依赖文件都放到本地目录。把 pip 的 module,可执行文件,动态链接库 .so 文件都放拷贝到当前目录
但把依赖文件放置到当前目录在实践过程中往往并不容易。
- pip 和 apt-get 安装的库文件会散落到系统的很多目录里,需要对不同包管理系统有深入的了解才能找回这些文件。
- 库文件有传递依赖,往往安装某个库,会把一堆这个库依赖的库都安装进去,手工去遍历这些依赖是非常繁琐的。
所以我们的问题归结到,如何方便地把依赖安装到当前目录,减少手工操作。下面我们会分别介绍 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%\Python
。PYTHONUSERBASE
环境变量可以修改掉 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 安装的链接库和可执行文件也需要安装到本地目录。网上推荐 chroot
和 apt-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。
本文是定制更好工具的一个技术积累的过程,基于此,我们会进一步推出更好用的工具,来简化开发过程。