系统重演是服务系统一个很重要的特性,但基本上很难。除了某些对系统可靠性要求很高的系统,比如交易系统。系统重演就像魔法时光回溯那样,将所有的过程不失真的完全执行一遍。这是个理想状态,但最大可能性的重演具有很高的意义,我希望能在单一世界中,实践这个特性。
我们首先界定下,什么是系统重演。我们认为对于系统,任何时间,任何环境下,只要相同的输入,都会得到相同的结果,那么,这个过程就是系统重演。这个定义借用了视频的概念,就像一个视频一样,任何时候播放,都是一样的画面和声音。
我们知道,最容易重演的情况发生一个线性执行的序列里面,而且这个序列完成功能所需要的资源,都是自包含的。比如一个C函数:
int max(int a,int b)
{
return a > b ? a : b ;
}
只要指定A和B,那么任何时候执行这个函数的结果都是一样的。如果MAX函数引用了全局变量C,那么情况就会发生改变,比如:
int c = 0;
int max(int a,int b)
{
int tmp = a > b ? a : b ;
return c > tmp ? c : tmp ;
}
如果C发生了变化,那么相同的输入就会面临发生改变。虽然,在服务系统中的情况远比这个要复杂很多,但是归根到底,原理还是一样。
我们将一个超大规模的系统分解为多个子系统,每个子系统又可以分析成更小的单位。当粒度达到了函数这个层次,那么重演就容易多了。可实际上,这只是理论上。因为,当粒度越小,那么要记录各个单位之间的数据流几乎是不可能的,那么重演也就成为不可能的。因此,首先确定一个最小的可重演的粒度,至关重要。
我们要决定这个粒度的时候要满足2个条件:1、重演单元之间数据量传输最小;2、划分出来的单元的数量最少。第二个条件是优先考虑的要素,因为系统重演是个庞大的工程,单元太多,基本上很难完成。
显然,一个系统可以由多个子系统构成的,如果每个子系统都是可重演的,那么整个系统就是可重演的。当然,并不是每个子系统都需要重演,不过,我们在探讨这个论题时,依然要假设每个子系统都需要重演。从重演的定义可以看出,影响一个系统不可重演的因素,主要包括几个因素:
1、系统对时间有依赖,导致在不同时间重演时,发生不同的结果。
2、系统对环境有依赖,导致在不同环境重演时,发生不同的结果。
我们知道,系统对时间依赖导致的不可重演的情况比较好理解,我们可以时钟当作一种外部资源,系统对时间的依赖实际上就是对时钟这种外部资源的依赖,那么,我们通过模拟时钟,依然可以完成系统重演。
而对环境的依赖则比较复杂,一种是外部环境,另外一种是内部环境。外部环境引起系统无法重演的因素很多,比如磁盘错误,比如网络失败,或者其他种种,往往是不可预测以及不可枚举的。对于这些因素我们可以通过分析日志,以及设置相应的触发条件来尽可能重现这种环境。
而内部环境引起不可重演的因素相对较少,但也同样不可预测。比如随机数,比如锁,等无法准备判断结果的因素。
对于一个可重演的系统,从设计角度来看,我们只需要将那些不可重演的子系统割裂出来,作为外部模块就可以了。那么串行化请求,以及记录结果日志这2个手段,就能实现系统重演。对内部环境的锁引起不可重演的情况,第一方式,就是割裂子模块的之间的依赖程度,尽量的并行化,从而避免资源竞争。将资源统一化管理,就能有序的控制和重现资源输出记录。
如果是一个高性能的系统,那么串行化请求,显然是不可取的。这又给我们提出另外一个问题,如何应对海量的请求呢?如果是无状态的请求,本身并没有系统依赖,难度相对要低很多,关键是具有上下文依赖的请求,在时序上,对结果存在影响,是考察的关键。一个上下文环境主要可以区分为3种,一个是由请求本身引起的局部变量,这个没啥关系。一个是全局变量,不算配置,配置的全局变量是静态的,没什么影响。另外一种是其他请求引起的局部变量,这种情况是最复杂的。比如A玩家攻击B玩家,那么最终的伤害,要依赖于B玩家本身的状态。这种情况,必须割裂A对B状态的依赖,因此设计上需要一个仲裁机制的存在。
原文链接:https://www.f2er.com/javaschema/287603.html