此文已由作者張耕源授權網易雲社群釋出。
歡迎通路網易雲社群,了解更多網易技術産品營運經驗。
Debian 打包一直是比較冷僻的技術,大部分同學都不會接觸到它。但是我們 Debian 伺服器上安裝的各種軟體服務,都是通過各種打包工具制作出來的安裝包部署到伺服器上的。
Debian 打包雖然比較煩瑣複雜,但是它提供了比較健全的一整套軟體部署、安裝、更新、維護的流程,并有一系列與之配套的自動化工具,可以避免人工操作可能出現各種遺漏、錯誤,特别是在大規模部署時基本不可能人工操作。
我們雲計算使用的 Openstack 基礎服務,也是通過自己從頭制作安裝包、上傳到 Debian 倉庫、并最終通過 puppet 等自動化工具實作服務的部署、更新。
之前我們一直采用 Debian 官方的流程對 Openstack 的 Python 服務打包,但是在幾年的實踐中發現了各種無法解決的問題,不得不自己另外實施一套全新的打包方案。
本文主要介紹 Debian 新打包方案的起因、原理、流程。
起因
我們以前使用了很長時間的 Debian 社群官方的 Openstack 服務打包方案,中間還嘗試過一段時間的 virtualenv 打包方案,各自都有較大的問題,詳見下面。
社群打包方案
原來我們從 Debian 社群 Openstack 項目打包倉庫 fork 出來的自己做一些修改和 backport 然後打包的方案,所有服務及其依賴的 Python 子產品都通過 Debian 的 deb 格式安裝包安裝,這樣存在一個主要問題:Debian 官方倉庫中的 Python 子產品版本更新太慢了。
比如常用的 Python 資料庫第三方子產品 SQLAlchemy ,在 Python 的官方 PyPI 中已經更新到了 1.1.4 版本,但在 Debian Wheezy 的倉庫中僅有 0.7.8 版本,差了4個大版本。
我們在使用 Openstack 服務中發現的一些資料庫相關問題,本來是簡單的更新 SQLAlchemy 版本就能搞定的,由于這個問題變得很難解決。
這個問題直接導緻我們很難更新一些有問題的 Python 子產品依賴版本;有些需要的子產品甚至根本沒有 Debian 安裝包,引進新子產品、功能比較困難;更新 Openstack 服務的大版本基本不可能,後續也基本不可能從 Debian Wheezy 更新到 Jessie 了,影響非常大。
virtualenv 打包方案
在發現社群官方的打包方案的嚴重問題後,我們後面也嘗試了一段時間通過 Python virtualenv 虛拟環境打包的方式,即在一個 virtualenv 虛拟環境中通過 Python pip 工具安裝相關 Python 依賴并将整個安裝了服務與依賴的 virtualenv 環境打包成 Debian 安裝包。
這個方案在後續使用中也發現很多問題,最大的問題還是本質上 virtualenv 并不能真正隔離系統的 Python 環境和自身虛拟環境的 Python 環境,最終導緻服務各種詭異錯誤。
我們這裡可以看一個例子
$ virtualenv test$ source test/bin/activate>>> import sys>>> sys.path
['','/home/stanzgy/workspace/test/lib/python2.7','/home/stanzgy/workspace/test/lib/python2.7/plat-linux2','/home/stanzgy/workspace/test/lib/python2.7/lib-tk','/home/stanzgy/workspace/test/lib/python2.7/lib-old','/home/stanzgy/workspace/test/lib/python2.7/lib-dynload','/usr/lib/python2.7','/usr/lib/python2.7/plat-linux2','/usr/lib/python2.7/lib-tk','/home/stanzgy/workspace/test/local/lib/python2.7/site-packages','/home/stanzgy/workspace/test/lib/python2.7/site-packages']>>> import json>>> json.__file__'/usr/lib/python2.7/json/__init__.pyc'>>> import _json>>> _json.__file__'/home/stanzgy/workspace/test/lib/python2.7/lib-dynload/_json.so'
在這個例子中,我們建立了一個虛拟環境 test ,并嘗試在虛拟環境 importPython 自帶的 json 子產品,結果發現引用的子產品位址事實上是作業系統而不是虛拟環境的。
從 sys.path 的結果可以看到,虛拟環境中的 Python import 子產品時會嘗試先從虛拟環境中的 Python PATH 搜尋,然後會嘗試從系統的 Python PATH 搜尋。如果 import 的子產品二次引用其他的 Python 子產品實作,則可能導緻系統的 Python 子產品和虛拟環境中的 Python 子產品交叉使用的情況。
在上面的例子中,可以看到 json 子產品和實作其部分功能的 _json 子產品分别屬于系統和虛拟環境。如果系統和虛拟環境中的 Python 版本、子產品版本不一緻,則很容易導緻服務出現問題,并且最重導緻 Python 程序本身崩潰,并且很難調試、查找原因。
virtualenv 打包方案從原理上并不可靠。
新 Debian 打包方案
需求
我們 Openstack 服務 Debian 打包在現有基礎上的需求主要有三點:
- 能自由指定、更新 Python 依賴子產品版本
- 不同 Openstack 服務之間的 Python 環境互相隔離
- Openstack 服務的 Python 環境和系統的 Python 環境隔離
社群的方案三點都不滿足,virtualenv 方案隻滿足第一、二點。
原理
新打包的流程比較複雜,但原理用一句話就能描述清楚:每次打包獨立編譯 Python ,編譯時通過設定 RPATH 變量實作隔離效果。
RPATH 是 Python 編譯時設定的變量,效果是寫死指定并限制程式運作時動态連結庫的的搜尋路徑,類似 LD_LIBRARY_PATH。關于它的詳細資訊和讨論可以參考 Wikipedia 和 Debian Wiki
我們每個服務都使用不同的RPATH變量編譯 Python 後,相當于每個服務都安裝在一個獨立的 Python 隔離環境裡,使用各自獨立的運作時動态連結庫搜尋路徑。這樣每個服務既可以随意更新修改自己的 Python 依賴子產品版本、也避免了之前 virtualenv 方案存在的嚴重的系統環境隔離問題,解決了上面的三點需求。
下面是一個采用了新打包方案的 Openstack 服務的 Python 環境。
>>> import sys>>> sys.path
['','/srv/stack/nova/lib/python27.zip','/srv/stack/nova/lib/python2.7','/srv/stack/nova/lib/python2.7/plat-linux2','/srv/stack/nova/lib/python2.7/lib-tk','/srv/stack/nova/lib/python2.7/lib-old','/srv/stack/nova/lib/python2.7/lib-dynload','/srv/stack/nova/lib/python2.7/site-packages']>>> import json>>> json.__file__'/srv/stack/nova/lib/python2.7/json/__init__.py'>>> import _json>>> _json.__file__'/srv/stack/nova/lib/python2.7/lib-dynload/_json.so'
可以看到它有獨立的 Python sys.path 路徑、并且不存在和系統的 Python 交叉調用的問題。
流程
新 Debian 打包方案的流程,可簡單描述為:
- 指定 RPATH,編譯、安裝 Python
- 使用新編譯的 Python pip 安裝依賴
- 安裝 Python 服務到新編譯好的 Python 獨立環境
- 将上面建立的整個 Python 獨立環境打包
- 處理其他配置檔案、啟動腳本等
下面為每個步驟的詳細說明
編譯 Python
所有項目編譯 Python 使用同一個 build-python.sh 腳本
#!/bin/bash...export PROJECT_PREFIX=${PROJECT_PREFIX:-/srv/stack}export PROJECT_BASE=$PROJECT_PREFIX/$PROJECTexport PYTHON_FILE=${PYTHON_FILE:-Python-2.7.12.tar.xz}# Get python tarballCUR_DIR=$PWDTEMP_DIR=$(mktemp -d /tmp/pybuild.XXXX)cd $TEMP_DIRwget $PYTHON_URL/$PYTHON_FILEmkdir -p py27 && tar Jxf $PYTHON_FILE -C py27 --strip-components=1cd py27# Compile pythonDPKG_CPPFLAGS=$(dpkg-buildflags --get CFLAGS)
DPKG_CFLAGS=$(dpkg-buildflags --get CPPFLAGS)
DPKG_LDFLAGS=$(dpkg-buildflags --get LDFLAGS)
CFLAGS="$DPKG_CFLAGS $DPKG_CPPFLAGS" \
LDFLAGS="$DPKG_LDFLAGS,-rpath=$PROJECT_BASE/lib" \
./configure \
--prefix=$PROJECT_BASE \
--enable-shared \
--enable-unicode=ucs2 \
--with-ensurepip=install \
--enable-ipv6 \
--with-dbmliborder=bdb:gdbm \
--with-fpectl \
--with-system-expat \
--with-system-ffi
make
make installcd $CUR_DIRrm -rf $TEMP_CIR
這個腳本會自動下載下傳 Python 2.7.12 ,并指定 /srv/stack/${PROJECT} 為每個 Openstack 項目的 $BASE 目錄,/srv/stack/${PROJECT}/lib 為其 RPATH 目錄,設定一些編譯選項後編譯安裝 Python 到 $BASE 目錄。
安裝 Python 依賴
目前新打包的 Python 依賴在上面編譯 Python 後通過 pip 安裝。
export REQUIREMENT_FILE=debian/deb-requirements.txt# install pip requirements inside the bootstrap system$(PROJECT_PREFIX)/$(PROJECT)/bin/pip install \
-U -r $(CURDIR)/$(REQUIREMENT_FILE)
每個 Openstack 項目可以在其項目根目錄下 $REQUIREMENT_FILE 中指定符合 python-pip 格式的 Python 依賴,這個檔案通常是通過 pip freeze 生成的。
安裝服務
在 Python 依賴安裝成功後,安裝項目本身到獨立 Python 環境中。
# install the project inside the bootstrap system$(PROJECT_PREFIX)/$(PROJECT)/bin/python setup.py clean \
build --executable "$(PROJECT_PREFIX)/$(PROJECT)/bin/python" install
需要注意的是,在安裝項目時需要指定我們自己編譯的 Python 路徑 --executable "$(PROJECT_PREFIX)/$(PROJECT)/bin/python" 。
其他
完成上面的步驟後,剩下隻需要走正常的 Debian 打包流程,處理分包、配置檔案等即可。
結語
我們使用的這個新 Python 服務 Debian 打包方案,既能享受到 Debian 包管理系統的各種便利和自動化,又能自由使用 Python PyPI 中的各種最新子產品,兼顧了兩者的優點又避開了各自的缺點。它已經在我們的測試環境穩定運作了幾個月,趨于穩定,希望它後續能在我們服務安裝部署中更好的服務我們。