上一篇談了怎樣給版本取個好名字,也就是版本號。說明了“語義化版本”的命名規範,也說明了這一種命名規範在依賴管理中發揮的重要作用。今天繼續談語義化版本號,說明一下這種命名方式的重要性,以及對研發和運維過程的影響。
開發人員每一次向下遊工序交付一個版本,都必須為這個版本編一個號碼,這就是版本號(也稱作版本 ID)。如果每次交付都是同樣的版本號,隨著時間的推進就會產生很多版本號相同、但是功能不一樣的二進位制包,在這種情況下部署,你可以想象會遇到多少驚奇。對於每一次生成二進位制包,都應該分配一個唯一標識,對於審計來說,這是非常重要的。最顯而易見的方法,是使用二進位制包的雜湊值作為唯一標識,以便可以驗證生成二進位制包的原始碼是否正確。當你不確定某個環境到底部署了哪個版本的時候,可以使用檔案的 MD5 碼找出版本庫中對應的版本。一些二進位制產物管理平臺可以自動提取雜湊碼。但是以雜湊碼作為標識有一些缺點,很明顯的一個缺點是雜湊碼太長,與他人溝通的時候很難記得住(你很難告訴同事“把 customer 元件從 ea3304ea2fff21dd1e501795c43c48ff 版本替換成 81b134b598b16d1605e6f76189f1c018 版本就可以解決你遇到的問題”,他也很難記住)。更重要的是雜湊碼只能標識兩個版本是否相同,卻無法體現版本之間的時間關係(哪一個版本是新的)和相容性(新版本是否完全包含老版本的功能)。所以我們應該使用規範化的版本號作為二進位制包的識別符號。
所以我們必須採用語義化版本規範,使用三段式版本號,如下:
主版本號.次版本號.修訂號
版本號遞增規則如下:
- 主版本號:當做了不相容的 API 修改,
- 次版本號:當做了向下相容的功能性新增,
- 修訂號:當做了向下相容的問題修正。
使用這種方式,版本號就不再是一個隨意的命名。任何一個以 API 方式對外提供服務的程式(無論是 Web API、訊息處理、函式 API),都應該遵循語義化版本規範。從 API 規範的意義上說,語義化版本實際上是對軟體介面規格的描述。例如一個軟體模組對外提供 Web API 服務,模組版本是 2.3.1,可以理解成這樣的規格描述:
為了準確描述程式的介面規範,我們在設計和開發的時候要儘量把介面規範和實現程式碼分離,這樣就可以更準確的控制 API 規格。以 Web API 為例,我們可以把與服務介面相關的程式碼放在單獨的目錄裡,比如把控制器程式碼全部放在 controller 目錄,輸入輸出資料結構全部在 vo 目錄。這樣就可以在釋出版本的時候根據變更的範圍準確確定版本號。
當以下變更發生時,介面的呼叫方法發生了變化,需要升級主版本號:
- controller 刪除了原有介面;
- controller 在原有介面上新增了引數,並且引數是必須的;
- vo 刪除了輸出資料的屬性;
- vo 新增了輸出資料的屬性,並且屬性是必須的。
當以下變更發生時,原有的介面仍然可以工作,需要升級次版本號:
- controller 新增了新介面;
- controller 在原有加快上新增了引數,但是引數可以不輸入;
- vo 新增了輸出資料的屬性;
- vo 新增了輸入資料的屬性,但是屬性可以不輸入。
當以下變更發生時,只需要升級修訂號:
- controller 和 vo 的程式碼都沒有改動,只改動了程式其他的部分。
用這樣的方法,版本號就可以描述程式內部的變更範圍。
下面說一下版本號對依賴管理的作用。在構建和執行軟體時,軟體的一部分要依賴於另一部分,就產生了依賴關係。在任何應用程式(甚至是最小的應用程式)中也會有一些依賴關係。至少,大多數軟體應用都對其執行的作業系統環境有依賴,Java 應用程式依賴於 JVM,它提供了 JavaSE API 的一個實現。網路服務之間也存在依賴關係。在大型軟體中,從元件中選擇好用的版本,組成一個完整的系統是一個極具難度的事。為了做好依賴管理,我們必須做下面幾件事:
- 為每一個二進位制包制定唯一的版本號,禁止一物多碼,更要禁止一碼多物。必須標識版本,才能管理依賴;
- 釋出版本時描述依賴關係;
- 使用語義化版本號,只要確定了一個版本是可用的,就可以確定一個區間的版本都可用。
我們以 Linux 作業系統為例看一下依賴管理的過程。Linux 是一個非常複雜的體系,它本身由很多二進位制包組成,使用者也需要在作業系統上安裝自己需要的程式。如果沒有一個依賴管理機制,要在 Linux 上安裝一個軟體,將會是一件困難的任務。幸運的是各種 Linux 發行版都提供了完善的包管理機制,還附帶了包管理工具。比如 Debian 作業系統,提供了 dpkg 工具,以下是使用 dpkg 檢視 wget 資訊:
$ dpkg -s wget Package: wget Section: web Maintainer: Noël Köthe <noel@debian.org> Architecture: amd64 Version: 1.18-5+deb9u3 Depends: libc6 (>= 2.17), libgnutls30 (>= 3.5.6), libidn11 (>= 1.13), libnettle6, libpcre3, libpsl5 (>= 0.13.0), libuuid1 (>= 2.16), zlib1g (>= 1:1.1.4)
這裡列出了主要資訊,有兩個資訊非常重要:
- wget 本身的版本號:1.18-5+deb9u3
- wget 依賴的其他元件版本(Depends 行)
有了這些資訊,就可以在安裝 wget 的時候檢查 Debian 上已經安裝的庫,判斷是否滿足依賴條件,包管理工具可以級聯安裝所有的依賴項。也可以檢查 wget 與已經安裝的程式是否存在依賴衝突,提示使用者進行處理。如果沒有這一套包管理機制,在 Linux 上安裝一個包是非常冒險的事情。
最後我們再來看看語義化版本是怎樣幫助我們做好老版本維護的。有時候正在生產環境執行的老版本忽然發現一個缺陷,或者需要新增一個功能,都需要對老版本進行維護。這種事情在 To B 業務非常多見,To B 業務部署在很多現場,每個現場專案實施的時期不一樣,所以版本都有一些差異,對老版本進行維護是一件不可避免的事情。
如果不使用語義化版本號,比如用一個不斷增長的序號來標識版本號,連續釋出多個版本就會形成這樣的版本路徑:
隨著時間的發展,有一些老版本會在部署在不同的現場。現在 1002 版本上發現一個缺陷,需要緊急修復。這時候該怎麼辦呢?1002 版本已經經過了 2 次升級,直接替換成 1004 版本行不行,很難判斷,所以只能基於 1002 版本升級替換。這個好辦,使用 Git 做一個分支,修改後重新釋出一個版本就可以了。缺陷修改後,形成下面這樣的版本路徑:
如果以後需要維護 1001、1003 版本,繼續發展下去,版本路徑就會越來越複雜:
維護版本分支越來越多,基本上要為每一個老版本建立一個維護分支,工作量隨著專案發展越來越大。開發團隊要把大量的精力放在老專案維護上,產品開發的工作受到越來越多的牽制。語義化版本號能怎樣改變這種局面呢?如果每一次釋出都按照語義化版本編號,那麼最初的版本路徑就是下面這樣:
用這種方式,我們就能準確判斷版本之間的相容關係,根據版本之間的替換關係確定最佳維護位置。當我們在 1.0.6 版本上發現一個缺陷,需要緊急修復,1.2.1 版本可以完全相容 1.0.6 版本的功能。如果這個缺陷已經在 1.2.1 版本得到修復,那麼升級現場的版本即可。如果必須修改程式碼,也只需要在 1.2.1 的基礎上修改,再釋出一個版本即可,不增加維護分支:
如果已經發生了主版本升級的情況,我們也只需要為每一個主版本建立一個維護分支,就能同時滿足多個專案的維護工作,降低維護工作量。
如上圖,對於所有 1.x 主版本,只需要基於 1.2.2 版本建立一個維護分支即可。