我學習C語言的時候是在大學課程上,老實說,能理解那些語言概念就很不容易了,對於軟件包管理這件事聽都沒聽說過。但真實情況下,大部分的軟件項目都不可能是從零開始的,我們總要依賴某些開源的或者團隊自己開發的工具和框架庫來幫助工作,我是學習Java的時候才慢慢聽說了Maven
。
maven
的核心配置是pom.xml
文件,開發者可以根據需要在其中列出項目的依賴包,像這樣:
<dependency><groupId>org.springframework</groupId><artifactId>spring-core</artifactId><version>4.1.5.RELEASE</version></dependency>
maven
命令在工作時會找到spring-core
所依賴的其它庫
$ mvn dependency:tree ......[INFO] sample_java:sample_java:war:1.0-SNAPSHOT [INFO] +- org.springframework:spring-core:jar:4.1.5.RELEASE:compile [INFO] | \- commons-logging:commons-logging:jar:1.2:compile ......
但是這樣有個問題,某些間接依賴會導致在不同時候打出不同的包,比如上述這樣個例子,兩次打包期間如果commons-logging
發布了新版本,那麼兩次打包的內容就不一樣了,如果遇到新版本的差異,開發人員可能會莫名其妙。
ruby社區針對這個問題發明了一個叫做bundle
的工具,它也有個Gemfile
文件用來記錄直接的依賴庫,類似mvn
,但是bundle
多了一個功能,工程師可以在當前項目下執行bundle install
命令,bundle系統將根據當前的軟件倉庫狀態計算出間接依賴,並將這些間接依賴鎖定到某個版本,內容寫在 Gemfile.lock 文件中。比如這個簡單的例子:
source 'https://ruby.taobao.org' gem 'activesupport'
生成的Gemfile.lock是這樣:
GEM remote: https://ruby.taobao.org/ specs: activesupport (4.2.3) i18n (~> 0.7) json (~> 1.7, >= 1.7.7) minitest (~> 5.1) thread_safe (~> 0.3, >= 0.3.4) tzinfo (~> 1.1) i18n (0.7.0) json (1.8.3) minitest (5.7.0) thread_safe (0.3.5) tzinfo (1.2.2) thread_safe (~> 0.1) PLATFORMS ruby DEPENDENCIES activesupport
當再次執行bundle
命令時,bundle系統會根據Gemfile.lock
文件來決定間接依賴,所以開發者通常把這個文件放入版本控制系統,確保所有人和線上都用同一份Gemfile.lock
,就能避免上述的問題。
我一直覺得bundle
的做法是最先進的,不過和做nodejs
開發的同學聊天時,了解到了npm
的做法頗為特別,雖然不見得比bundle
更好,卻是各有優劣。
npm
的做法是直接把被依賴的庫放入當前庫的node_modules
目錄,依賴庫也以此類推,它的核心文件是package.json
,比如這個例子:
$ cat package.json {"name": "sample_js","version": "1.0.0","dependencies": {"browserify": "10.2.4"}} $ npm install $ npm dedupe
查看一下依賴庫
$ ls node_modules/browserify/node_modules JSONStream concat-stream glob labeled-stream-splicer readable-stream syntax-error acorn console-browserify has module-deps readable-wrap through2 assert constants-browserify htmlescape os-browserify resolve timers-browserify browser-pack crypto-browserify http-browserify parents sha.js tty-browserify browser-resolve defined https-browserify path-browserify shasum url browserify-zlib deps-sort indexof process shell-quote util buffer domain-browser inherits punycode stream-browserify vm-browserify builtins duplexer2 insert-module-globals querystring-es3 string_decoder xtend commondir events isarray read-only-stream subarg
查看一下依賴庫的依賴庫
$ ls node_modules/browserify/node_modules/crypto-browserify/node_modules bn.js browserify-aes browserify-sign create-hash diffie-hellman parse-asn1 public-encrypt brorand browserify-rsa create-ecdh create-hmac elliptic pbkdf2 randombytes
這種做法實際上是在開發環節就確定並下載了間接依賴的庫,可以看做在開發者手裡就完成了打包,這麼做有什麼好處?
相比maven
,npm
和bundle
更具備一致性,無論到哪裡,bundle
系統都保證使用lock版本,不會有“失控”的依賴庫;而相對於bundle
,npm
可以允許在一個項目中依賴同一個庫的不同版本,這比較靈活。
但是這麼做也有缺點,有些js開發者就吐槽這一點,認為浪費了內存——不同庫依賴同一個庫時,都會在自己的node_modules目錄下存放一份被依賴庫的代碼。從這個角度看,bundle
又顯得有些優勢,因為require
在同一個ruby進程中是有緩存的,不會額外浪費內存。
打個比方吧,bundle
就好像大規模的軍事單位,除了作戰部隊外還有專門的,好處是可以統一劃撥管理,降低了維護成本,缺點是有些細微的差別不好滿足。
而npm
就好像一個精干的小分隊,每個人帶自己適合的食物,雖然可能並不豐富,但是每個單兵都是一個可以獨立生存的單元。
我把npm
這種方式稱為“自帶干糧”。軟件技術中,“自帶干糧”的設計思想有很多應用場景,比如動態編譯和靜態編譯。
如果你喜歡自己從源碼編譯軟件,那麼多半熟悉LD_LIBRARY_PATH這個環境變量,這是用來指明動態鏈接庫查找路徑的,我們常常把一些模塊代碼編譯為後綴為 so
的動態鏈接庫文件,然後再運行時動態載入。
與之相比,還有一種做法叫靜態編譯,比如這樣的命令:
/opt/apache_src $ ./configure --prefix=/usr/ --enable-file-cache
其中的enable-file-cache
是靜態編譯的選項,使用這類選項,編譯工具會將模塊直接編譯進入最終的執行文件(比如apache
的執行文件就是httpd
)。
使用動態鏈接庫還是選擇靜態編譯?應該說兩種方式各有優劣,前者可以減少內存消耗,避免裝入暫時用不到的代碼,而後者則是某種角度的“自帶干糧”,這樣編譯的可執行程序,遷移起來比較容易,不會由於目標系統上沒有相應的動態鏈接庫而運行失敗。
Go語言是 google 創建的一門語言,它有很多獨特的設計,其中之一就是——它是靜態編譯的,這一點曾經讓很多人诟病,認為它寫一個hello world
都要輸出很大的可執行文件。
但是從另外一個角度看這個做法很有價值,Go語言的定位是系統級編程,這類程序和應用軟件不同,它往往比較底層,本身就是其它軟件的基礎,因此對穩定性很重視,除了硬件這種不得不考慮的因素,其它方面干擾越少越好,使用靜態鏈接方式產生兩個好處:
Go語言具備這些好處並不是偶然的,作為大規模集群計算起家的互聯網公司,Google對於系統的橫向擴展能力、可靠性、故障恢復能力都有很高的要求,自帶干糧的語言可以很好的幫助實現這些目標:
這樣看來,“自帶干糧”的做法其實非常適合互聯網應用的場景,這裡充斥著“集群”、“彈性擴展”、“服務冪等化”的做法,如果我們從事互聯網應用領域,那麼理解“自帶干糧”的做法很有價值。
這個做法其實並不神秘,“自帶干糧”的理念的一個重要體現,就是我們很熟悉的“打包”環節。
Linux服務端開發的項目,通常都會有一個“打包”環節,很多人並不完全理解這個環節的作用,實際上,這只是“自帶干糧”原則在項目管理中的落實而已。
舉兩個例子,ruby bundle的打包是這樣的:
$ bundle package... $ ls -l vendor/cache total 1744-rw-r--r-- 1 john staff 322K 7 9 03:48 activesupport-4.2.3.gem-rw-r--r-- 1 john staff 57K 7 9 03:48 i18n-0.7.0.gem-rw-r--r-- 1 john staff 149K 7 9 03:48 json-1.8.3.gem-rw-r--r-- 1 john staff 70K 7 9 03:48 minitest-5.7.0.gem-rw-r--r-- 1 john staff 118K 7 9 03:48 thread_safe-0.3.5.gem-rw-r--r-- 1 john staff 144K 7 9 03:48 tzinfo-1.2.2.gem
bundle packge命令把需要使用的gem包統統放入vendor/cache目錄,那麼當前目錄就是一個“自帶干糧”的體系。
再看Java:
$ mvn pacakge ... $ ls -l target/*.war -rw-r--r-- 1 john staff 1.0M 7 9 03:51 target/sample_java.war
mvn命令最後會輸出一個war文件,按照javaEE的相關規范,它自身包含了所有依賴的第三方jar,所以這也是一個“自帶干糧”的產出。
結合上面的例子,我們可以得出結論:打包這個行為本質上就是通過“自帶干糧”的方式把相關依賴完全納入控制,這使得我們的交付物能夠很容易的在線上水平擴展並減少環境影響。
那麼,考慮到大多數語言或者框架都有打包機制,是不是Go語言相對與ruby或者java其實沒有什麼優勢呢?答案是未必,這裡需要分開討論:
顯然,由於考慮到操作系統層面的動態鏈接庫問題,ruby和java這種虛擬機語言必須面對打包不完整的問題(與之相比,Go語言除了Glibc基本算是沒有依賴),ruby的bundle沒有處理這個問題,java則付出了“自己重新搞一遍”的代價。
說到這裡自然會產生一個問題——即使是Go,也需要glibc,因此切換OS時還是需要交叉編譯技術的,那有沒有更“完備”的方案呢?比如把所有依賴都隔離開?
答案是Docker,它把基礎庫也放入了鏡像,因此,從打包這件事來看是Docker image是真正的“自帶干糧”並且沒有遺漏的做法,未來的Build系統,以Docker image進行交付是大勢所趨。
包管理的做法各不相同,然而基本思路差別不大,理解“打包”的目的,是學生和軟件工程師的一個重要區別。
Docker安裝應用(CentOS 6.5_x64) http://www.linuxidc.com/Linux/2014-07/104595.htm
在 Docker 中使用 MySQL http://www.linuxidc.com/Linux/2014-01/95354.htm
在Ubuntu Trusty 14.04 (LTS) (64-bit)安裝Docker http://www.linuxidc.com/Linux/2014-10/108184.htm
Docker安裝應用(CentOS 6.5_x64) http://www.linuxidc.com/Linux/2014-07/104595.htm
Ubuntu 14.04安裝Docker http://www.linuxidc.com/linux/2014-08/105656.htm
阿裡雲CentOS 6.5 模板上安裝 Docker http://www.linuxidc.com/Linux/2014-11/109107.htm
Docker 的詳細介紹:請點這裡
Docker 的下載地址:請點這裡