有趣的版本号

  计算机的世界,版本号(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 路由策略

posted @ 2018-02-06 10:46  xybaby  阅读(2802)  评论(2编辑  收藏  举报