計算機的世界,版本號(version)無處不在,不管是發布的軟件、產品,還是協議、框架。那什么是版本號呢
在這里是這樣定義的:
Software versioning is a way to categorize the unique states of computer software as it is developed and released.
軟件版本號是對開發、發布中的軟件的狀態的唯一(unique)概括。簡單來說,協議就是對一組狀態的手工簽名。作為程序員,我們經常用md5來簽名,保證數據完整性、可靠性。但是我們很難說,對軟件或者協議計算MD5,那么版本號就是手工維護的簽名。
為什么需要版本號,是因為軟件(如linux內核)、協議(如http)都是在不斷的發展完善中,也許是修復上一個版本的bug,也許是引入新的特性。當然,不能說有了新的版本就立馬拋棄舊的版本,用戶(廣義的,程序員也是用戶)是不會答應的,新版本也許有更高級的功能,但我用不到;新版本也許性能更好,但是不一定穩定。而且,版本升級是一個復雜的事情,維護老系統的程序員早都離職了,誰敢去升級。還有,開源的、免費的產品一旦放出,就不再屬於開發者了。因此,多個版本的軟件、協議並存是必然的事情,比如在對於Python語言,不管是官方還是一些開源組織,都呼吁放棄Python2,轉向python3,但python2還是活得好好的。只要有多個版本 -- 本質是多組不同狀態的軟件 -- 存在,我們就需要用版本號予以區分。
軟件、協議中的版本號,其最大的作用在於避免雞同鴨講。當我們討論問題的時候,首先得明確大家是在相同的語義環境下,其中,版本號就是一個很重要的context,因為同一個術語在不同的版本可能代表的意思完全不一樣,比如Python中的range函數。
本文地址:http://www.cnblogs.com/xybaby/p/8403461.html
版本號的形式
版本號的形式並沒有固定的或者約定俗成的格式,完全取決於軟件、協議的發布者。
數字形式(numerically)的版本號是最為常見的,比如http1.1,iPhone6, python2.7.3,其中 x.y.z 這種格式又是最為常見的。a代表大版本(major version),不同的a也許是不兼容的;b代表小版本(minor version),同一個大版本中的小版本一般是兼容的,小版本一般新增功能;c一般是修bug(revision)。
在服務化體系之-兼容性與版本號一文中,作者介紹到,在微服務結構中,服務的升級是高頻度的事情,但服務升級的時候,一些接口是兼容的,而另外一些接口而是不兼容的。客戶端不可能與服務端同步升級,因此多個版本的服務並存也是常態。那么在存在多個版本的服務時,客戶端請求如何路由,就依賴於版本號:
服務的版本號,和軟件的版本號一樣,一般整成三位:
第一位:不兼容的大版本, 如1.0 vs 2.0
第二位:兼容的新功能版本,如1.1 vs 1.2
第三位:兼容的BugFix版本,如1.1.0 vs 1.1.1
果拿着低版本的SDK(如1.0.0) 發起請求,會被服務化框架路由到所有的兼容版本上(如1.1.1,1.2.0),但不會到不兼容的版本上的(如2.0.1)。
當我們使用一個軟件、協議的時候,了解其版本號規則也是有好處的,比如Linux內核,也是x.y.z的形式,如2.6.8,但是第二位y卻有特殊的意義:偶數表示穩定版本;奇數表示測試版本.
通信協議中的版本號
上面提到了兼容性,兼容性也是一個很廣泛的詞匯,在本文中,專指不同版本的軟件、協議能協同工作,這個在通信協議、網絡接口中非常廣泛。在《通信協議序列化》一文中,作者循序漸進,從最簡單的緊湊模式過渡到類似protobuf這種高級模式,在這個過程中,就提到了兼容性。本節內容都是對原文的引用。
在最簡單的版本中,協議架構是這樣的:
1 struct userbase 2 { 3 unsigned short cmd;//1-get, 2-set, 定義一個short,為了擴展更多命令(理想那么豐滿) 4 unsigned char gender; //1 – man , 2-woman, 3 - ?? 5 char name[8]; //當然這里可以定義為 string name;或len + value 組合,為了敘述方便,就使用簡單定長數據 6 }
種編碼方式,稱之為緊湊模式,意思是除了數據本身外,沒有一點額外冗余信息,可以看成是Raw Data。雖然可讀性差,但是節省內存和帶寬。
但是當需要擴展協議內容的時候,問題就來了。比如,A在基本資料里面加一個生日字段,然后告訴B:
1 struct userbase 2 { 3 unsigned short cmd; 4 unsigned char gender; 5 unsigned int birthday; 6 char name[8]; 7 }
這是B就犯愁了,收到A的數據包,不知道第3個字段到底是舊協議中的name字段,還是新協議中birthday。
這是一個兼容性與可擴展性的問題,而引入版本號,加一個version字段就能解決這個問題
1 struct userbase 2 { 3 unsigned short version; 4 unsigned short cmd; 5 unsigned char gender; 6 unsigned int birthday; 7 char name[8]; 8 }
不管以后協議如何演變,只要version字段不同,接收方就能夠正確解析協議。
MVCC
Multi-Version Concurrency Control 多版本並發控制
MVCC是一種並發控制( concurrency control )機制,在RDBMS中有廣泛應用。並發控制解決的是數據庫事務acid中的I(Isolation,隔離性),比如一個讀操作與一個寫操作並發執行,如何保證讀操作不讀取到寫操作未提交的數據,即避免臟讀(dirty read)。
要實現隔離性,最簡單的方法是加鎖(Lock-Based Concurrency Control),即一條數據記錄同時只允許一個事務操作,比如並發讀寫的話可以使用讀寫鎖。加鎖雖然能解決並發控制的問題,但是在長事務中也會出現鎖的爭用甚至是死鎖的情況。而MVCC通過為每一個數據項保存多分拷貝,每一個事務操作的其實是數據在某一時間點的一份快照,除非事務被最終提交,那么其他事務是無法讀取到中間狀態的,這就達到了隔離性的要求。
加鎖與MVCC經常配合使用,二者在理念上有明確的區別,加鎖是悲觀的,認為很大概率會沖突,所以使用這一行數據之前先加鎖,在解鎖之前其他人都不能使用這條記錄;而MVCC是樂觀的,認為沖突的概率較小,所以使用時先不加鎖,如果提交的后面發現沖突了,再自行回滾。
對於一個實現了MVCC的數據存儲引擎,以更新一個記錄為例,並不是在原來的記錄上直接更新,而是拷貝、創建一個更高版本的數據記錄,然后在新的版本上更新。這樣即使同時有其他事務進行讀操作,也是在一個稍微舊一點的版本上讀取,互不影響。只有當更新記錄的事務提交之后,修改數據庫元數據,其他事務才會讀取到最新版本的數據記錄。
但MVCC對於並發寫操作就沒有那么好使了,多個並發寫在提交的時候很可能會沖突,如果發生沖突,就需要回滾,也可以通過加鎖的方式來避免並發寫。
網上有很多MVCC在工業界上的實現,比如《輕松理解MYSQL MVCC 實現機制》這篇文章中對innodb mvcc使用詳細介紹。
MVCC這種思想在分布式事務中也可以借鑒,在劉傑的《分布式原理介紹》中有相應介紹
緩存中的版本號
咋眼一看,似乎緩存中的版本號與軟件、協議的版本號不是一回事,不過一細想,其實都是對一組狀態的唯一簽名。版本號在緩存中使用非常廣泛,其根本作用在於解決緩存過期、不一致的問題。下面給出幾個例子
web中的版本號
對於這個,前端開發人員應該都很熟悉,我只是班門弄斧,做個簡單介紹。詳細的可以參見《前端資源版本控制的那些事兒》
為了優化網頁的加載、響應速度,一般會開啟瀏覽器的緩存功能,即瀏覽器會緩存資源文件(js、css)。比如下面的index.html引用了兩個資源文件:
<link rel="stylesheet" href="a.css"></link> <script src="a.js"></script>
在緩存時間內訪問頁面時,瀏覽器不會真正發出請求,而是使用緩存的資源文件。
但這樣也會引入新的問題,那就是當服務端修改html文件與資源文件,發布之后,客戶端會拉取到最新的index.html,但是讀取到的資源文件有可能還是舊的 -- 讀取到的是瀏覽器緩存的資源文件。這就暴露了任何緩存最重要的問題,緩存過期的問題,當緩存系統的數據與原數據不一致的時候,就不應當再使用緩存中的數據,而是拉取最新的原數據,同時緩存最新的元數據。
但是在瀏覽器緩存這個實例中,瀏覽器是無法及時感知到緩存的數據已過期。雖然設置了過期時間(expire),但這個過期時間只是單方面的,只能約束客戶端(瀏覽器)的行為,服務端並不保證在這個過期時間內不更新內容。這個不禁讓我想到lease機制,lease機制保證了在過期時間內不會修改原數據,因此通過緩存讀到的數據一定是最新的。
那么如何避免瀏覽器讀取到過時的緩存資源文件呢,最常用,且一般情況下也夠用的方法就是加上版本號。
<link rel="stylesheet" href="a.css?v=0.01"></link>
<script src="a.js?v=0.01"></script>
這樣當資源文件變化時,只需修改版本號(上面的v),瀏覽器就會去服務器拉取最新的資源文件。當然,如果每次修改資源文件的時候都手動修改這個版本號,也是一個費力切容易出錯的工作,所以一般都會引入自動化腳本,發布時自動修改版本號。
MongoDB元數據緩存
關於MongoDB,在我之前的文章也有一些介紹。在這里討論的是MongoDB中元數據(metadata)的緩存,MongoDB中,元數據主要是每一個chunk包含的數據范圍(range),以及chunk與shard的映射關系。元數據是整個系統的核心,需要保證高可用性與強一致性。
如上圖所示,是MongoDB最常見的Sharded Cluster結構。其中,config server存儲系統的元數據;shards真正存儲用戶數據;而mongos緩存元數據,利用元數據指定最佳的執行計划,也就是路由功能。可以看到,應用(Client app)直接與mongos交互,實際的線上應用,一般也是mongos與應用程序部署在一起,config server 與 shards對用戶是透明的。
既然應用程序利用mongos上緩存的元數據進行路由,那么緩存的元數據就必須是准確的,與config server強一致的,否則用戶請求就可能被路由到錯誤的shard上。那么MongoDB是如何解決的呢,答案就在MongoDB Sharded Cluster 路由策略
簡而言之,就是增加版本號,元數據的每一次變更(chunk的分裂與遷移)都會增加版本號。這個版本號,在shard本地和元數據中都會維護,自然mongos緩存的元數據中也是有版本號的。當請求被mongos路由到某一個shard時,會攜帶mongos上的版本號,如果該版本號低於shard上的版本號,那么說明mongos上緩存的數據已經過期,需要重新從config server上拉取。
Python method cache
在《python屬性查找》中,介紹了屬性查找的順序,而method屬於類屬性,如果一個method在類中沒有找到,那么會按照mro的順序在基類查找。那么,對於在一個多層繼承的類體系中,屬性訪問是不是會很慢呢,理論上確實如此,但是實踐測試的話並不會很明顯。原因就在於在python2.6中,引入了method cache:
Type objects now have a cache of methods that can reduce the work required to find the correct method implementation for a particular class; once cached, the interpreter doesn’t need to traverse base classes to figure out the right method to call.
可見,python解釋器會緩存訪問過的method,這樣就避免了每次訪問的時候遍歷基類。
但是,Python是動態語言,可以運行時改變代碼的行為,也就包括增刪method,這個時候緩存就與原始數據不一致了,Python是這么解決的
The cache is cleared if a base class or the class itself is modified, so the cache should remain correct even in the face of Python’s dynamic nature.
在源碼(2.7.3)中,每一個緩存的entry都是如下的struct
1 struct method_cache_entry { 2 unsigned int version; 3 PyObject *name; /* reference to exactly a str or None */ 4 PyObject *value; /* borrowed */ 5 };
核心的函數_PyType_Lookup如下:

1 PyObject * 2 _PyType_Lookup(PyTypeObject *type, PyObject *name) 3 { 4 Py_ssize_t i, n; 5 PyObject *mro, *res, *base, *dict; 6 unsigned int h; 7 8 if (MCACHE_CACHEABLE_NAME(name) && 9 PyType_HasFeature(type, Py_TPFLAGS_VALID_VERSION_TAG)) { 10 /* fast path */ 11 h = MCACHE_HASH_METHOD(type, name); 12 if (method_cache[h].version == type->tp_version_tag && 13 method_cache[h].name == name) 14 return method_cache[h].value; 15 } 16 17 /* Look in tp_dict of types in MRO */ 18 mro = type->tp_mro; 19 20 /* If mro is NULL, the type is either not yet initialized 21 by PyType_Ready(), or already cleared by type_clear(). 22 Either way the safest thing to do is to return NULL. */ 23 if (mro == NULL) 24 return NULL; 25 26 res = NULL; 27 assert(PyTuple_Check(mro)); 28 n = PyTuple_GET_SIZE(mro); 29 for (i = 0; i < n; i++) { 30 base = PyTuple_GET_ITEM(mro, i); 31 if (PyClass_Check(base)) 32 dict = ((PyClassObject *)base)->cl_dict; 33 else { 34 assert(PyType_Check(base)); 35 dict = ((PyTypeObject *)base)->tp_dict; 36 } 37 assert(dict && PyDict_Check(dict)); 38 res = PyDict_GetItem(dict, name); 39 if (res != NULL) 40 break; 41 } 42 43 if (MCACHE_CACHEABLE_NAME(name) && assign_version_tag(type)) { 44 h = MCACHE_HASH_METHOD(type, name); 45 method_cache[h].version = type->tp_version_tag; 46 method_cache[h].value = res; /* borrowed */ 47 Py_INCREF(name); 48 Py_DECREF(method_cache[h].name); 49 method_cache[h].name = name; 50 } 51 return res; 52 }
代碼邏輯並不復雜,分成三步
step1: 如果該函數有緩存,且緩存版本號與類型當前版本號一致(method_cache[h].version == type->tp_version_tag),那么直接返回緩存的結果;否則
step2:通過mro,找出method name對用的method實例
step3:緩存該method實例,版本號設置為類型當前版本號(method_cache[h].version = type->tp_version_tag)
從上面的幾個例子可以看出,緩存中的版本號有時也是dirty flag或者lazy 思想的運用:當緩存內容過期的時候,並不是立即清空或者重新加載新的數據,而是等到重新訪問緩存的時候再比較版本號,如果不一致再拉取最新數據。
總結
本文並沒有一個明確的主題,都是我平時發現的版本號(version)在各種場景下的使用,比較有趣的是MVCC與緩存中的使用。當然,我相信還有更多更有趣的使用場景,而本人所接觸的領域比較狹窄,權當拋磚引玉,歡迎各位園友指正與補充。