最新消息:

我们在360如何使用Python – virtualenv 篇

python admin 3011浏览 0评论

我们简单介绍了我们的项目的基本结构和开发流程。整个开发流程都是基于 distribute 的。关于我们如何使用distribute,会在后面一部分当中介绍。基于 distribute,我们具有了从代码构建到发布,测试和部署的基本框架。但是在实际的开发当中,我们还有一些问题需要解决,最重要的一点就是开发和运行环境的隔离和低权限。

所谓环境的隔离,是指在同一台机器上,能够并行(side-by-side)地部署多个python环境,每个环境之间互相独立,拥有自己的python程序,库和可执行程序。这样,我们既可以在一台机器上为多名开发者提供互不影响的开发环境,也能在同一台服务器上为多个应用提供互不影响的执行环境。同时,为了能够让多名开发者或多个应用实现真正意义上的隔离,还需要让每个环境能够在各自的非root且不能sudo的用户手里进行管理,包括升级包,安装新的包等等,否则需要为每个人都提供sudo权限,会破坏隔离性。

一个直觉的解决方案是在机器上安装为每个人安装一个python,彼此在不同的目录当中,每个目录只授权给对应的人员权限。但这种方案会导致大量的冗余的文件,比如python的标准库,就需要每个安装的目录里面一份。而且如果我们需要做小版本升级(比如将 python 2.7.2 升级到 python 2.7.3)的时候,需要对每个目录重新执行一次install操作。

幸好我们不是第一个遇到这种需求的人,于是我们找到了 virtualenv。virtualenv 官网(http://virtualenv.org/) 是这样描述virtualenv的:

virtualenv 是一个创建隔离的Python环境的工具。

virtualenv要解决的根本问题是库的版本和依赖,以及权限问题。假设你有一个程序,需要LibFoo的版本1,而另一个程序需要版本2,如何同时使用两个应用程序呢?如果将所有的库都安装在 /usr/lib/python2.7/site-packages(或者你的系统的标准包安装路径),非常容易出现将不该升级的库升级的问题。

另外,在一台共享的机器上,如果没有全局的 site-packages 目录的权限(例如一个共享的主机),如何安装Python库呢?

在这些情况下,就是该用到virtualenv的地方。它能够创建一个自己的安装目录,形成一个独立的环境,不会影响其他的virtualenv环境,甚至可以不受全局的site-packages当中安装的包的影响。

简直就是为我们的需求量身定做的一样。但仔细想来,virtualenv 并不能完全满足我们的需求。virtualenv默认只能生成一个“干净”的Python环境,其中只有python及其标准库。而我们需要有一个脚本,可以一键做到:

  1. 在开发环境当中,生成基本环境之后,自动安装基本的开发库,如 PyLint, nose, coverage等,并将源代码目录当中的所有项目注册到 site-packages(执行 python setup.py develop)
  2. 在线上环境当中,环境生成完毕之后,自动安装应用程序及其依赖库。

幸好virtualenv支持生成一个定制化的脚本,可以用来生成自己的虚拟环境,关键是 virtualenv.create_bootstrap_script 方法,这个方法支持在生成的定制化脚本的末尾添加一段自己的代码,而且支持在默认的环境创建完成之后调用自定义的 after_install 方法。我们写了自己的一个 after_install 方法,加载同一目录下的 after_install.py 文件并执行其中的 main 函数。

在这个main函数当中,我们将after_install的需求整理成下面几种类型的动作:

  • 用 easy_install 安装指定的包,支持指定 pypi 服务器地址。
  • 将未打包的一些脚本或二进制文件复制到新的 python 环境当中,比如一些私有C库的 python 绑定。
  • 枚举当前目录下的代码目录,并对其中的 setup.py 执行 develop 操作

第一个和第二个问题都容易解决。我们建立了一个配置文件,在其中指定 pypi 服务器的地址,easy_install 需要安装的包列表,需要复制到 python 环境当中的文件的文件名称,并将这些文件放在与 after_install.py 同一目录的 data 子目录下,而after_install.py只要忠实地按照配置文件当中的配置进行 easy_install 或复制的操作即可。

比较复杂的是第三点,因为当前目录当中可能有多个项目,而这些项目之间是具有相互依赖的,同时他们可能都会依赖于一些外部的python包。如果不谨慎地考虑 develop 的顺序,可能导致 develop 一个较为高层的库的时候,将它依赖的底层库从 pypi 上面获取到并安装到环境当中,而后续 develop 这个底层库的时候,有可能造成两个库的冲突。

对于这种情况,我们需要先枚举出当前目录下的所有包含有 setup.py 文件的目录,然后获取到其中的依赖关系,并根据依赖关系进行排序,最后根据这个顺序依次进行 develop。

获取一个项目(python包)的依赖关系,只能从 setup.py 文件当中抽取。关于 setup.py 当中定义包和包的依赖关系的内容,请参见 distribute 的文档。我们使用 monkey patch 的方法,hook了 setup 函数,在枚举出所有项目的 setup.py 文件之后,动态加载每个 setup.py(使用 imp.load_source),也就获取到了相应的依赖关系。下面的代码当中,_get_project_depends 接受一个 project 名称,和一个 project 所在目录的名字,即可获取这个项目所依赖的 Python 包的列表。

import setuptools
def _get_project_depends(project, project_path):
    if not getattr(setuptools, '_is_hook_', False):
        setuptools._is_hook_ = True
        _hook_setuptools()

    setup_py = os.path.join(project_path, 'setup.py')
    if not os.path.exists(setup_py):
        return None

    setuptools._this_round_depends = []

    imp.load_source('%s_setup' % project, setup_py)

    return setuptools._this_round_depends

def _hook_setuptools():
    def _hook_find_packages(*args, **kwargs):
        return []
    setuptools.find_packages = _hook_find_packages

    def _hook_setup(name=None, install_requires=[], dependency_links=[], **kwargs):
        for depend in install_requires:
            setuptools._this_round_depends.append(depend.replace('-', '_'))
        for depend in dependency_links:
            setuptools._this_round_depends.append(depend)
    setuptools.setup = _hook_setup

随后我们根据每个项目的依赖关系,做成一个字典,key就是Python包的名字,而value是该包所依赖的包的列表。根据依赖关系进行排序这个问题,也常常被我拿来做面试题,有兴趣的同仁也可以自己实现一下:

def sort_with_depends(dep_map):
    """根据 dep_map 对其中的 key进行排序。

    :param dict dep_map: 依赖图关系,key是每个项目,value是该项目依赖的项目(一个list)
    :return: 一个排好序的列表,其中每个项目所依赖的项目都在它前面出现
    """

至此,after_install.py 扩展脚本的功能就不是问题了。

这样,我们有了一个通过 virtualenv.create_bootstrap_script 生成的bootstrap.py脚本,一个 after_install.py 作为扩展脚本,一个配置文件用来配置 after_install,以及一个目录用来保存所有需要直接复制的文件。他们的入口是 bootstrap.py,直接用 python 执行它即可创建一个 virtaulenv 环境,然后自动调用 after_install.py 当中的逻辑。一切看起来很完美,但是我们需要的可能更多:我们需要一个独立的可执行的脚本,以便大家方便的创建起来一个新环境,而不是每次复制一堆文件。

为此我们使用了类似 eggsecutable 的方式。

这种方式依赖于Linux unzip程序的一个特性。unzip程序在试图解压一个zip文件的时候,并不要求一定要从第一个字节开始就是zip文件,它会在一定的区域内搜索 zip 文件的头,并从对应的位置开始解压。这样一来,我们可以在一个zip文件的之前,附加上一个简短的 shell 脚本,在其中将自身解压,并将后续的控制权交给解压出来的文件。比如我们所做出的一个附加了zip文件的shell脚本:

bootstrap-sh

其中前面部分就是我们的加载脚本:

#! /bin/sh

PYTHON_WITH_VERSION=python2.7

if [ -z $1 ]; then
    echo "Require environment name."
    exit 1
fi

if [ -e .bootstrap ]; then
    rm -rf .bootstrap
fi

unzip -q -d.bootstrap.tmp $0 >/dev/null 2>&1
mv .bootstrap.tmp .bootstrap

$PYTHON_WITH_VERSION .bootstrap/bootstrap.py --no-site-packages $*

exit 0

这之后就是一个zip文件,通过unzip可以方便地将其解压。

明白了原理,我们就可以很容易地写出一个 bootstrap.sh 文件的生成器 make-bootstrap。借助这个生成器,我们可以方便地生成一个自包含的 bootstrap.sh文件。对应于需要的场景,我们有两种方式使用这个脚本:

方式一:预先生成好一个 bootstrap.sh,并将这个文件签入到svn当中,放在开发环境的根目录下;每个开发人员签出代码之后,立即执行这个脚本构建一个自己的开发环境:

  ./bootstrap.sh dev-sharelib

后续开始开发之前,可以运行

source dev-sharelib/bin/activate

激活这个开发环境,随后即可在其中进行开发和测试了。

方式二:在应用程序的构建过程当中,调用 make-bootstrap 脚本生成一个指定配置的 bootstrap.sh(为此我们还写了一个 distribute扩展,后续介绍 distribute 的时候会提到),并将其发布到更新服务器。在部署阶段,运维人员获取这个bootstrap.sh并在服务器上建立起来一个隔离环境用来部署整个应用。

至此,我们就有了一套基于virtualenv的环境建立脚本及其生成方案,基本的环境隔离和低权限化已经完成。但真正的开发工作,以及发布和部署,都还需要另一个python大杀器 – distribute 才行。下一篇我将介绍一下我们在这方面的实践。


 

转载请注明:爱开源 » 我们在360如何使用Python – virtualenv 篇

您必须 登录 才能发表评论!