360案例講解:如何使用virtualenv?
virtualenv 是一個創建隔離的Python環境的工具。 virtualenv要解決的根本問題是庫的版本和依賴,以及權限問題。假設你有一個程序,需要LibFoo的版本1,而另一個程序需要版本2,如何同時使用兩個應用程序呢?如果將所有的庫都安裝在 /usr/lib/python2.7/site-packages(或者你的系統的標準包安裝路徑),非常容易出現將不該升級的庫升級的問題。另外,在一臺共享的機器上,如果沒有全局的 site-packages 目錄的權限(例如一個共享的主機),如何安裝Python庫呢?在這些情況下,就是該用到virtualenv的地方。它能夠創建一個自己的安裝目錄,形成一個獨立的環境,不會影響其他的virtualenv環境,甚至可以不受全局的site-packages當中安裝的包的影響。
virtualenv默認只能生成一個“干凈”的Python環境,其中只有python及其標準庫。而360需要有一個腳本,可以一鍵做到:
- 在開發環境當中,生成基本環境之后,自動安裝基本的開發庫,如 PyLint, nose, coverage等,并將源代碼目錄當中的所有項目注冊到 site-packages(執行 python setup.py develop)
- 在線上環境當中,環境生成完畢之后,自動安裝應用程序及其依賴庫。
幸好virtualenv支持生成一個定制化的腳本,可以用來生成自己的虛擬環境,關鍵是 virtualenv.create_bootstrap_script 方法,這個方法支持在生成的定制化腳本的末尾添加一段自己的代碼,而且支持在默認的環境創建完成之后調用自定義的 after_install 方法。360寫了自己的一個 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 的文檔。 360使用 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 當中的邏輯。一切看起來很完美,但是需要的可能更多:需要一個獨立的可執行的腳本,以便大家方便的創建起來一個新環境,而不是每次復制一堆文件。
為此360使用了類似 eggsecutable 的方式。這種方式依賴于Linux unzip程序的一個特性。unzip程序在試圖解壓一個zip文件的時候,并不要求一定要從第一個字節開始就是zip文件,它會在一定的區域內搜索 zip 文件的頭,并從對應的位置開始解壓。這樣一來,我們可以在一個zip文件的之前,附加上一個簡短的 shell 腳本,在其中將自身解壓,并將后續的控制權交給解壓出來的文件。比如360所做出的一個附加了zip文件的shell腳本:
#! /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(為此360還寫了一個 distribute擴展),并將其發布到更新服務器。在部署階段,運維人員獲取這個bootstrap.sh并在服務器上建立起來一個隔離環境用來部署整個應用。
至此,就有了一套基于virtualenv的環境建立腳本及其生成方案,基本的環境隔離和低權限化已經完成。但真正的開發工作,以及發布和部署,都還需要另一個python大殺器 – distribute 才行。