欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

有趣的版本号

程序员文章站 2022-05-19 15:09:19
...

  计算机的世界,版本号(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函数。

版本号的形式

  版本号的形式并没有固定的或者约定俗成的格式,完全取决于软件、协议的发布者。

  数字形式(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与缓存中的使用。当然,我相信还有更多更有趣的使用场景,而本人所接触的领域比较狭窄,权当抛砖引玉,欢迎各位朋友们指正与补充。