有趣的版本號


  計算機的世界,版本號(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 }
_PyType_Lookup

 

  代碼邏輯並不復雜,分成三步

  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與緩存中的使用。當然,我相信還有更多更有趣的使用場景,而本人所接觸的領域比較狹窄,權當拋磚引玉,歡迎各位園友指正與補充。

references

通信協議序列化

服務化體系之-兼容性與版本號

輕松理解MYSQL MVCC 實現機制

前端資源版本控制的那些事兒

MongoDB Sharded Cluster 路由策略


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM