歡迎來到Linux教程網
Linux教程網
Linux教程網
Linux教程網
Linux教程網 >> Linux基礎 >> Linux教程 >> 談談軟件包

談談軟件包

日期:2017/2/28 14:00:22   编辑:Linux教程

我學習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

這種做法實際上是在開發環節就確定並下載了間接依賴的庫,可以看做在開發者手裡就完成了打包,這麼做有什麼好處?

相比mavennpmbundle更具備一致性,無論到哪裡,bundle系統都保證使用lock版本,不會有“失控”的依賴庫;而相對於bundlenpm可以允許在一個項目中依賴同一個庫的不同版本,這比較靈活。

但是這麼做也有缺點,有些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語言的定位是系統級編程,這類程序和應用軟件不同,它往往比較底層,本身就是其它軟件的基礎,因此對穩定性很重視,除了硬件這種不得不考慮的因素,其它方面干擾越少越好,使用靜態鏈接方式產生兩個好處:

  • 降低遷移成本:在一個linux裡編譯好的Go程序,通常可以直接copy到其它linux上使用,而如果依賴動態鏈接庫,那麼可能由於目標系統上缺乏動態鏈接庫而失敗,這樣,Go程序的安裝復雜性會很高,這對基礎的系統軟件是不利的。
  • 外部錯誤干擾bug定位:即使目標系統上有所需的動態鏈接庫,也不一定就沒有問題,由於版本依賴等問題,目標系統的動態鏈接庫未必和本來設想的相同,如果由於外部錯誤導致bug,而開發人員並不知道這一情況,對bug定位無疑是一個災難。

Go語言具備這些好處並不是偶然的,作為大規模集群計算起家的互聯網公司,Google對於系統的橫向擴展能力、可靠性、故障恢復能力都有很高的要求,自帶干糧的語言可以很好的幫助實現這些目標:

  • 橫向擴展能力:這個和遷移成本相關,由於不再需要動態鏈接庫的配合,應用程序可以很方便完整的在新系統上部署,因此,一旦需要橫向擴展,至少在軟件安裝方面,Go語言就有了很大的便利性。
  • 可靠性:Google的哲學是,通過軟件而不是硬件提供可靠性。那麼,如果硬件的可靠性不可依賴,軟件系統的可靠性和容錯就要通過類似備份之類的冗余計算來得到,這時,一個“自帶干糧”的應用程序顯然很容易部署為冪等的集群,因此可以在很大程度上幫助實現冗余計算。
  • 故障恢復能力:這將受益於對外部干擾的排除,由於“自帶干糧”,Go程序已經包含了bug分析的幾乎全部信息,開發人員不需要依靠線上環境,在線下就能分析界定問題。

這樣看來,“自帶干糧”的做法其實非常適合互聯網應用的場景,這裡充斥著“集群”、“彈性擴展”、“服務冪等化”的做法,如果我們從事互聯網應用領域,那麼理解“自帶干糧”的做法很有價值。

這個做法其實並不神秘,“自帶干糧”的理念的一個重要體現,就是我們很熟悉的“打包”環節。

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(使用bundle作為包管理工具):因為是虛擬機語言,通常情況下,bundle的“打包”僅限於ruby語言的各種gem,有些對動態鏈接庫的依賴並沒有納入管理,因此,雖然“自帶干糧”卻會有“忘記帶飯碗”的尴尬情況
  • java(使用maven作為包管理工具):同上,也是虛擬機語言,不過,和ruby不同的是,java由於社區風格的差異,通常習慣於使用pure 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 的下載地址:請點這裡

Copyright © Linux教程網 All Rights Reserved