精华内容
下载资源
问答
  • 一致性问题是多智能体协同控制的基础,有广泛的应用背景。之前的研究已经给出了线性一致性协议实现均方一致的充分条件,在此基础上,对线性离散均方一致性...结论将为线性离散一致系统的噪声估计和控制提供理论依据。
  • 测试分布式系统线性一致性 参考资料:  1. 测试分布式系统线性一致性:http://www.jianshu.com/p/bddfce1494d6  2. 使用 Porcupine 进行线性一致性测试:http://www.jianshu.com/p/9aedd234ef62  3...
    测试分布式系统的线性一致性

    一. 介绍
          正确实现一个分布式系统是非常有挑战的一件事情,因为需要很好的处理并发和失败这些问题。网络包可能被延迟,重复,乱序或者丢弃,机器可能在任何时候宕机。即使一些设计被论文证明是正确的,也仍然很难再实现中避免 bug。

          通常对于一个 key-value store,我们对于它在顺序操作下面的行为都能有一个直观的认识:Get 操作如果在 Put 的后面,那么一定能得到 Put 的结果。譬如,如果 Put("x", "y") ,那么后面的 Get("x") 就能得到 "y",如果得到了 "z",那么这就是不对的。

          对于一个基于顺序规范的并发操作来说,我们会用一个一致性模型,也就是线性一致性来说明它的正确性。在一个线性一致性的系统里面,任何操作都可能在调用或者返回之间原子和瞬间执行。除了线性一致性,还有一些其他一致性的模型,但多数分布式系统都提供了线性一致性的操作:线性一致性是一个强的一致性模型,并且基于线性一致性系统,很容易去构建其他的系统。

          测试:有了一个正确性的定义,我们就可以考虑如何去测试分布式系统了。通常的做法就是对于正确的操作,不停的进行随机的错误注入,类似机器宕机,网络隔离等。我们甚至能模拟整个网络,这样我们就能做长时间的网络延迟等。因为测试时随机的,我们需要跑很多次从而确定一个系统的实现是正确的。

          线性一致性:一个更好的办法就是并发的客户端完全跑随机的操作。譬如,循环的去调用 kvstore.put(rand(), rand()) 和 kvstore.get(rand()),有可能会只用很少的 key 去增大冲突的概率。但在这种情况下,我们如何定义什么是正确的操作呢?在上面的简单的测试里面,因为每个 client 都操作的是一个独立的 key,所以我们可以非常明确的知道输出结果。
           但是 clients 并发的操作同一堆 keys,事情就变得复杂了。我们并不能预知每个操作的返回值因为这并没样一个唯一的答案。但我们可以用另一个办法:我们可以记录整个操作的历史,然后去验证这个操作历史是线性一致的。

          线性一致性验证:一个线性一致性验证器会使用一个顺序规范和一个并发操作的历史,然后执行一个判定程序去检查这个历史在规范下面是否线性一致。

    二. 相关测试的系统
    2.1  TLA+的形式规范测试
         理论上,所有的生产系统都会有一个形式规范,而且一些系统也已经有了,譬如 Raft 就有一个用 TLA+ 写的形式规范。但不幸的是,大部分的系统是没有的。

    2.2  使用Porcupine进行线性一致性测试
         详见:http://www.jianshu.com/p/9aedd234ef62

    2.3  使用Chaos测试分布式系统线性一致性
         详见:http://www.jianshu.com/p/2e65e6f37c76


    三. 参考资料:
       1.  测试分布式系统的线性一致性:http://www.jianshu.com/p/bddfce1494d6
       2.  使用 Porcupine 进行线性一致性测试:http://www.jianshu.com/p/9aedd234ef62
       3.  使用 Chaos 测试分布式系统线性一致性:http://www.jianshu.com/p/2e65e6f37c76

    展开全文
  • 非线性随机PWM反馈系统的p方指数一致稳定,孙伟,张忠,不确定随机非线性系统的输出反馈控制问题目前已经成为现代控制理论的主要热点之一。本文将线性PWM控制系统的稳定性结论推广到了非
  • 具有指定性能的非线性多主体系统的有限时间一致
  • 研究了一类离散线性切换系统一致有限时间稳定性分析和反馈镇定. 基于线性矩阵不等式技术, 给出了 在任意切换信号作用下, 离散线性切换系统有限时间稳定和有限时间有界的充分条件, 并给出了离散线性切换控制...
  • 针对系统状态不可测和具有通信时延的线性多智能体系统,提出一种基于观测器的一致性控制算法.设计观测器用于解决智能体状态不可测的问题,在观测器的基础上,提出一种控制协议来实现带时变时延的线性多智能体系统一致性...
  • 研究普通线性多智能体系统在有向拓扑结构下的一致性问题,提出一种基于分布式PID控制的新的一致性协议.\.首先通过变量转换将一致性问题转变为误差系统的渐近稳定问题;然后构造Lyapunov函数,基于线性矩阵不等式(LMI)给...
  • 线性多智能体系统自适应一致性控制,刘杨,韩红改,针对二阶非线性多智能体系统,应用自适应控制策略解决一致性控制问题。首先,在系统不存在干扰的情况下,设计了一个反馈增益在线
  • 线性事件触发控制策略的多智能体系统有限时间一致
  • 线性时滞多智能体系统的最优一致性研究,杨琳,刘杨,本文将一致性问题与代价函数相结合,研究了带有时滞的线性多智能体系统最优一致性问题。首先,采用模型变换将一致性问题转换成稳
  • 分布式系统的可线性化(线性一致性) 终于理解了线性一致性,很开心 1. 线性一致性来源 线性一致性是Maurice P. Herlihy 与 Jeannette M. Wing共同提出的关于并行对象行为正确性的一个条件模型,在《多

    终于理解了线性一致性,很开心

    1. 线性一致性来源

      线性一致性是Maurice P. Herlihy 与 Jeannette M. Wing共同提出的关于并行对象行为正确性的一个条件模型,在《多处理器并发编程的艺术》这本书中提及。原文用的是Linearizability, 目前看翻译有的叫线性一致性,有的叫可线性化性。 这个模型的解释其实还是挺复杂的,下面d大部分的理解来源于stackoverflow的这篇解答

    2. 线性一致性解析

      当我们在解释并发处理是否正确的时候,我们常常是使用偏序(partial order)来拓展到全局有序(total order),查看并发操作的历史序列是否是正确的要比直接观察过程来判断并发操作是否是正确的要容易的多。这里的操可以是一个方法调用,后面我们也是拿方法调用作为一个操作来进行举例阐述。

    1. 单线程执行的正确性

      首先,我们先把并发操作放在一边,先考虑单个线程的处理程序。我们假设有一个历史的处理序列H_S(history sequence), 这个历史的处理序列由一系列的events构成,event可能是Invoke也可能是Response,这个时候这个操作序列是这样的:每个Invoke后面都紧跟着他对应的Response.这里的紧跟着就是在Invoke_i 和 Response_i 之间不会有其他的invoke或者response.也就是对方法的调用是串行的。
    H_S有可能是这样的

    H_S : I1 R1 I2 R2 I3 R3 ... In Rn
    
    (Ii 代表第i个Invoke, Ri代表了第i个Response)
    
    

    因为没有并发,这里我们很容易就可以判断出来H_S是一个正确的操作序列,这里所谓的正确就是这个序列产生的结果是和我们编写程序时候预期的结果是一样的。这样的描述可能有点抽象,我们可以举个例子,把操作对应的方法定义如下

    //int i = 0; i is a global shared variable.
    int inc_counter() {
       while(true){
        int old_i=i;
        int j = old_id++;
        if(cas(&i,old_i,j)==0){     //没有操作成功,就循环操作,操作成功了就返回
    	continue;
        }else{
        	return j;
        }
       }
    }
    

    对应的cas操作是一个原子操作

    int cas(long *addr, long old, long new)
    {
        /* Executes atomically. */
        if(*addr != old)
            return 0;
        *addr = new;
        return 1;
    }
    

    则对inc_counter()的调用,使用单线程的话,判断程序是否是正确的,就是如果我们不停的调用inc_counter(),对应的Ri(res)中的结果res(i)的值肯定是递增的,满足了是递增的,那么就是和我们编写程序想要达到的目标是一致的。

    2. 并发的运行历史分析

      实际情况下,程序的运行大多是多线程的,有可能我们的应用程序中会有A B两个线程在运行这个方法,当我们运行项目的时候我们也可以得到一个并发的运行历史,称之为H_C (History_Concurrent),像在H_S中一样,我们也使用Ii~Ri来表示方法的调用。因为两个线程是并发的,所以A B产生的调用处理在时间上可能是相互重叠的,所以从时间维度上我们可能会得到下面的一个操作历史

    thread A:         IA1----------RA1               IA2-----------RA2
    thread B:          |      IB1---|---RB1    IB2----|----RB2      |
                       |       |    |    |      |     |     |       |
                       |       |    |    |      |     |     |       |
    real-time order:  IA1     IB1  RA1  RB1    IB2   IA2   RB2     RA2
                    ------------------------------------------------------>time
    
    

    对应的是这样的

    H_C : IA1 IB1 RA1 RB1 IB2 IA2 RB2 RA2
    
    

    这个时候我们应该如何判断序列H_C是正确的呢,我们可以根据下面的规则将H_C 进行重排序得到H_RO(History_Reorder)

    如果一个方法调用m1 发生在另一个m2调用之前,则m1在重新排序的序列中必须在m2之前。
    在这种规则下,我们说H_C等效于H_RO(history_reorder)。
    这意味着如果Ri在H_C中的Ij前面,则必须确保Ri在重新排序的序列中仍在Ij的前面,i和j没有它们的顺序,我们也可以使用a,b,c …。

    H_RO有两个属性

    1. 尊重编程顺序,也就是单线程的执行顺序,所见即所得
    2. 保留真实发生的事件(respone的值)

    在不考虑上面的两条属性的情况下,我们可以将H_C重排序为以下几种类型(下面用H_S来代表H_RO)

    H_S1: IA1 RA1 IB1 RB1 IB2 RB2 IA2 RA2
    H_S2: IB1 RB1 IA1 RA1 IB2 RB2 IA2 RA2
    H_S3: IB1 RB1 IA1 RA1 IA2 RA2 IB2 RB2
    H_S4: IA1 RA1 IB1 RB1 IA2 RA2 IB2 RB2
    
    

    但是我们不能排出下面的顺序

    H_S5: IA1 RA1 IA2 RA2 IB1 RB1 IB2 RB2
    
    

    因为在H_C中,IB1~RB1是在IA2~RA2之前发生的

      那么即使有了这些序列,我们如何确定我们的H_C是正确(correct)的呢?(当下我们只讨论这个序列是correct,而不是讨论程序的correctness),这里所谓的正确和刚才单线程情况下讨论的正确性是一致的。就是这个序列产生的结果是否和我们编写程序时候预期的结果是一样的
      答案很简单,只要对应的H_S有一个是和我们预期的结果是一样的(正确性的条件),那么就可以认为H_C是可线性化的,把H_S称为H_C的线性化,同时认为H_C是一个正确的执行,也是我们期望程序表现出来的正常的结果。如果做过并发编程,可能你就会遇到过看起来正常的程序,执行的结果却和你认为的结果相去甚远。

    还拿上面的程序举例,假设变量i的初始值为0,则程序运行可能有这样的一个序列,这里我们加上了response的结果

    thread A:         IA1----------RA1(1)                  IA2------------RA2(3)
    thread B:          |      IB1---|------RB1(2)    IB2----|----RB2(4)    |
                       |       |    |        |        |     |     |        |
                       |       |    |        |        |     |     |        |
    real-time order:  IA1     IB1  RA1(1)  RB1(2)    IB2   IA2   RB2(4)   RA2(3)
                    ---------------------------------------------------------->time
    

    对应的H_C

    H_C : IA1 IB1 RA1(1) RB1(2) IB2 IA2 RB2(4) RA2(3)
    

    对应的reorder之后的H_S

    H_S1: IA1 RA1(1) IB1 RB1(2) IB2 RB2(4)  IA2 RA2(3) 
    H_S2: IB1 RB1(2) IA1 RA1(1) IB2 RB2(4)  IA2 RA2(3) 
    H_S3: IB1 RB1(2) IA1 RA1(1) IA2 RA2(3)  IB2 RB2(4) 
    H_S4: IA1 RA1(1) IB1 RB1(2) IA2 RA2(3)  IB2 RB2(4) 
    
    

    然后使用上面提到的H_RO的两个属性,上面的H_S序列中只有H_S4是符合预期的一个执行序列,那么也就是说H_C是可线性化的,把H_S4称为H_C的线性化,同时认为H_C是一个正确的执行。

    3. 应用程序的线性化判断

    1. 程序线性化的理论要求

      目前为止,我们终于搞明白了,对于一个并发的项目,什么是可线性化的运行历史(linearizable history ),那么我们又该如何评价一个程序是否是可线性化的呢,书中暗表:

    The basic idea behind linearizability is that every concurrent history is equivalent, in the following sense, to some sequential history. [The Art of Multiprocessor Programming 3.6.1 : Linearizability] (“following sense” is the reorder rule I have talked about above)
    线性化背后的基本思想是,在遵循reorder的规则下,每个并发历史都等效于某些顺序历史。

    也就是说对应一个应用程序来说,如果他执行的每一个H_C都能通过reorder rule转化为一个正确的H_S,那么就说这个应用程序是可线性化的。

    2. linearization point 检查

      但是,将所有并发历史H_C重新排序为顺序历史H_S以判断程序是否可线性化的方法仅在理论上可行。在实践中,我们面临着由几十个线程对同一个方法的大量调用。我们不能对它们的所有历史进行重新排序。我们甚至无法列出一个复杂程序的所有并发历史(H_C),所以作者又提出来另一个叫linearization point的概念:

    The usual way to show that a concurrent object implementation is linearizable is to identify for each method a linearization point where the method takes effect.
    [The Art of Multiprocessor Programming 3.5.1 : Linearization Points]
    表明并发对象的操作是可线性化的通常方法是为每个方法确定一个当前方法生效的线性化点。

      们将围绕“并发对象”来讨论上面相关的问题(如果每个线程操作的都是非并发的对象那么程序肯定是正确的)。并发对象的实现一般是有一些方法来访问并发对象的数据。而且多线程共享一个并发对象。因此,当他们通过调用对象的方法并发访问对象时,并发对象的实现者必须确保并发方法调用的正确性。
      最重要的是要理解 方法的linearization point,同样的,where the method takes effect这句描述也确实比较难以理解,下面举一些例子来进行解释。

    假如我们有下面的方法

    //int i = 0; i is a global shared variable.
    int inc_counter() {
        int j = i++;
        return j;
    }
    
    

    很容易发现这个方法存在并发问题,比如我们将i++翻译成汇编语言可以得到

    #Pseudo-asm-code
    Load   register, address of i
    Add    register, 1
    Store  register, address of i
    
    

    因此,两个同时执行i++的线程有可能产生下面的并发历史H_C

    thread A:         IA1----------RA1(1)                  IA2------------RA2(3)
    thread B:          |      IB1---|------RB1(1)    IB2----|----RB2(2)    |
                       |       |    |        |        |     |     |        |
                       |       |    |        |        |     |     |        |
    real-time order:  IA1     IB1  RA1(1)  RB1(1)    IB2   IA2   RB2(2)   RA2(3)
                    ---------------------------------------------------------->time
    
    

    对于这样的并发历史,无论你怎样reorder,都不能得到一个正确的sequential history (H_S)。

    我们需要使用下面的方式重写相关的代码

    //int i = 0; i is a global shared variable.
    int inc_counter(){
        //do some unrelated work, for example, play a popular song.
        lock(&lock);
        i++;
        int j = i;
        unlock(&lock);
        //do some unrelated work, for example, fetch a web page and print it to the screen.
        return j;
    }
    
    

      这样的话,应该能够理解inc_counter()方法的linearization point了吧,就是整个lock和unlock中间的争议区域critial section,因为在多线程调用inc_counter()的时候,只有争议区域保持原子性的执行才能保证方法的正确性。改良后的inc_counter()方法的response是全局变量i的递增值,有可能是像下面的序列

    thread A:         IA1----------RA1(2)                 IA2-----------RA2(4)
    thread B:          |      IB1---|-------RB1(1)    IB2--|----RB2(3)    |
                       |       |    |        |         |   |     |        |
                       |       |    |        |         |   |     |        |
    real-time order:  IA1     IB1  RA1(2)  RB1(1)     IB2 IA2   RB2(3)   RA2(4)
    
    
    

    明显的,上面的序列可以转换为下面的合法sequential history (H_S)

    IB1 RB1(1) IA1 RA1(2) IB2 RB2(3) IA2 RA2(4)  //a legal sequential history
    
    

    我们对IB1~RB1IA1~RA1进行了重排序,因为他们在真实的处理时间上面有重叠,所以可以使用任意一个在前的排序方式。同时依据有效的H_S我们可以判断在H_C当中是IB1~RB1先于 IA1~RA1进入争议区域critial section

    上面的例子比较简单,我们再来看一个例子

    //top is the tio
    void push(T val) {
        while (1) {
            Node * new_element = allocte(val);
            Node * next = top->next;
            new_element->next = next;
            if ( CAS(&top->next, next, new_element)) {  //Linearization point1
                //CAS success!
                //ok, we can do some other work, such as go shopping.
                return;
            }
            //CAS fail! retry!
        }
    }
    
    T pop() {
        while (1) {
            Node * next = top->next;
            Node * nextnext = next->next;
            if ( CAS(&top->next, next, nextnext)) { //Linearization point2
                //CAS succeed!
                //ok, let me take a rest.
                return next->value;
            }
            //CAS fail! retry!
        }
    }
    
    

    这是一个充满bug的lock-free stack的算法实现,请忽略算法的细节。我只是想要展示一下push()pop()linearization point,在代码的注释中已经标注了相应的linearization point。想象一下假如有许多线程重复调用push()和pop(),它们将在CAS步骤中进行排序。其他步骤似乎无关紧要,因为无论它们同时执行什么,它们对stack的最终影响(精确地说是top变量)都取决于CAS步骤(线性化点)的顺序。如果我们可以确保线性化点真正起作用,则并发堆栈是正确的。即使H_C非常的长,但是我们可以确认必须存在与H_C等效的合法序列。

    因此,如果要实现并发对象,那么如何判断程序的正确性呢?您应该确定每个方法的线性化点,并仔细考虑(甚至证明)它们将始终保持并发对象的不变性。然后,所有方法调用的顺序H_C可以扩展到至少一个合法的总顺序(事件的顺序历史记录H_S),该顺序满足并发对象的顺序规范。

    3. 简单总结

      这里简单的再来总结一下线性化的含义,之前看过一些书和一些相关文章,可能每个作者都试图用自己的方式来阐述什么是可线性化,所以很多时候只是注意到了可线性化的其中一方面的特点,这次通过这个很好的回答的学习也算是真正了解了可线性化的含义。我们可以尝试从几个层面去阐述线性化,线性一致性最开始是用在多处理器编程当中,一般是一个进程多个线程这个时候存储是共享的,为了实现程序的线性一致性,主要需要保证的是程序在争议区执行的原子性来保证线性一致性。后面逐渐发展出来分布式系统,这个时候强调分布式系统的一致性,不仅要关注系统对执行的原子化保证,还要了解分布式系统对多副本的处理方式,这个时候多个并发使用的存储并不一定是同一个存储空间。
    还有一点是非常重要的,无论在哪个层级定义线性化,线性化的含义总是:
    线性化描述的是程序在外部调用的情况下系统总体对外响应的正确性。这个系统可以是一个java并发对象,可以是一个应用处理软件(单机的扫雷游戏),也可以是一个通过网络访问的数据存储应用(Redis),或者是一个分布式的存储系统(Zookeeper)等。线性化只是对系统提出了这些要求,但是系统具体怎么实现的他是不关注的。
    而且,并非系统都是线性化的,因为线性化对系统要求比较严格,会影响系统的吞吐量已经系统的复杂度,所以很多系统可能都工作在非线性话的状态,比如MYSQL常用的事务的隔离级别中的脏读,读提交,可重复读等都不是线性一致性的。

    1. 程序中对共享对象的操作的可线性化

      简单的概括来说,如果一个对象是线性化的,可以理解为他的所有方法都是原子的(atomic),就像是java的synchronized方法一样,而且操作的方法必须立即执行,不能使用lazy模式。这里隐含的一个条件是这个对象肯定是多个线程共享操作的。

    2. 应用程序的线性一致性

      同样的,如果把线性化扩展到一个应用程序的话,那么该应用程序的线性化可以描述为,应用程序对于输入的处理都是原子性的。程序的输出的正确性并不受并发的输入的影响。这里默认的一个隐含条件也是程序是多线程或者多进程的对输入进行处理,但是数据是共享的(内存或者磁盘上)。

    3. 分布式系统的可线性化(线性一致性)

    如果把线性化扩招到一个分布式系统的话,那么可以这样描述这个系统

    1. 这个系统提供的所有操作都可以看做是原子的,操作通过重排以后能够得到一个(invoke,respone)构成的序列。而且这个序列的执行结果和预期的是一样的。
    2. 系统可以视为只有一个副本。(在上面的并行处理器系统中并没有强调这一点,因为一般认为主存或者硬盘存储只有一个,但是在分布式系统中一般是多台计算机通过网络通信,并不进行存储的共享)

    所以在分布式系统中设计一个可线性化的系统更加困难,因为不仅要考虑到操作的原子性保证,还有多个副本(在数据可能不一致的情况下)如何保证向外提供的的数据也满足线性一致性。

    主要参考
    https://stackoverflow.com/questions/9762101/what-is-linearizability

    展开全文
  • 为减少通信时延对系统一致性的影响,针对有领导者的二阶非线性多智能体系统的领导跟随一致性进行了研究,新颖地提出近似随机脉冲时延的概念并应用于新协议。相比于传统协议,新协议在脉冲输入时延较小时,各智能体...
  • 线性一致性理论

    千次阅读 2018-08-21 17:01:25
    Jepsen测试中支持验证系统线性一致性,关于线性一致性,中文的介绍非常少,目前网上能搜到的大概只有tidb(一个创业公司PingCAP研发的分布式数据库)的一篇Linearizability 一致性验证。 要理解理论,最好的方法...

    Jepsen(项目主页)是开源的分布式测试框架,基于Clojure语言,支持各种错误注入。目前广泛应用在各种分布式系统的测试中,尤其是一致性协议实现的测试中。Jepsen测试中支持验证系统的线性一致性,关于线性一致性,中文的介绍非常少,目前网上能搜到的大概只有tidb(一个创业公司PingCAP研发的分布式数据库)的一篇Linearizability 一致性验证

    要理解理论,最好的方法还是直接去看建立这个理论的原始论文,于是在业余时间对相关论文进行了阅读。其中第1)篇我做了一个完整的翻译,其他几篇由于篇幅和时间的原因仅进行了阅读。本文下面的内容主要源自阅读如下4篇论文后的理解,如果你觉得还有不好理解的地方欢迎指出,想深入了解一下的也可以再看下原文。首先简单介绍下每篇论文涉及的内容:

    1) How to Make a Multiprocessor Computer That Correctly Executes Multiprocess Programs Lamport 1979

        此文提供了关于Sequential Consistency一个简单精确的定义。不过它主要讲述的内容是关于:处理器和内存模块在什么条件下可以保证多处理器并行执行情况下的顺序一致性。这篇文章是Lamport在1979年发表的,距今已经40年,是cache-coherence领域最常被引用的一篇早期论文。它一直在我的reading list上,又非常简短(不到两页),于是趁此机会就将它翻译了一遍:译文。为了理解Jepsen测试的原理,我们只需要理解这篇文章定义的Sequential Consistency概念即可。

    2) Linearizability: A Correctness Condition for Concurrent Objects Maurice Herlihy · Jeannette M Wing 1990

        此文首次提出了Linearizability的概念。主要内容包含线性一致性模型的形式化定义,该模型的两个关键属性(locality和nonblocking)及其证明,还包含与其他一些模型比如Sequential Consistency/Serializability(可串行化)的对比。

    3) Testing for Linearizability

        此文主要提出了用于可线性化验证的WGL算法,该算法是对WG算法的一个优化,并且包含了WG算法的相关内容。

    4)Faster linearizability checking via P-compositionality*

        此文通过将线性一致性的locality属性(一个并发系统是可线性化的当且仅当它里面的所有对象都满足可线性化)背后的思想进行扩展,提出了P-compositionality*,然后基于这个概念可以实现更高效的线性化验证。核心的思想是将需要进行线性化验证的History(历史Operation序列)根据某种Partition方式划分成多个更小的子History,直接验证这些子History是否是可线性化的。

    1 .顺序一致性(Sequential Consistency) vs 线性一致性(Linearizability)

    论文1)中关于顺序一致性的原始定义如下:

    "在设计和证明运行在该计算机上的多进程算法[1]-[3]的正确性时,通常基于如下假设:执行结果与这些处理器以某一串行顺序执行的结果相同,同时每个处理器内部操作的执行看起来又与程序描述的顺序一致。满足该条件的多处理器系统我们就认为是sequential consistent的"。这里的处理器就代表了一个独立的执行进程(或线程),每个进程(线程)内部是串行执行的。如果并行执行的结果与某个合法的串行执行顺序(在这个执行顺序中每个线程内部的执行顺序要保留)的执行结果一致,我们就认为它是符合顺序一致性的。

    我们以一个具体实例来说明顺序一致性与线性一致性的区别。假设我们目前有一个内存寄存器(Register),该寄存器支持Set和Get操作。

    现在两个线程P1 P2,它们会操作这一个寄存器,横轴代表时间,线程内部是串行执行,线程之间是并行的,假设现在观察到如下的一个执行历史。

    该执行历史满足顺序一致,但违反了线性一致。

     

     具体原因解释如下:

    对于顺序一致性来说,它要找到一个合法的顺序执行过程(只要能找到一个即可),该执行过程要保留线程内部原有的顺序(对应到上图就是:[Set 1]一定要在[Get 2]之前,[Set 3]一定要在[Set 2]之前)。根据这个要求我们可以发现:[Set 1] [Set3] [Set 2] [Get 2]就是一个合法的顺序执行过程。对于一个寄存器来说,合法的顺序执行过程需要满足这个条件:Get到的值一定是最近一次Set的那个值。

    而对于线性一致性来说,它也是要找到一个合法的顺序执行过程(只要能找到一个即可)。但是这个顺序执行过程,不仅要保留线程内部的先后顺序,还要保留线程之间的操作的先后顺序。比如上图从时间线上看,[Set 1]是最先发生的,[Get 2]和[Set 3]时间上有交叉,在线性一致性模型中,我们认为这两个是并行的,先后顺序不定,最后的[Set 2]一定是最后发生。这样满足线程内部和线程间顺序约束的执行过程只有两种,如上图所示。但是这两种执行过程都不是合法的:对于[Set 1] [Set 3] [Get2] [Set 2]来说,Set 3之后却Get到了2;对于[Set 1] [Get 2] [Set3] [Set 2]来说,Set 1之后却Get到了2。所以不满足线性一致性。

    通过这个例子我们得到了一个关于线性一致性的直观认识:它比顺序一致性具有更强的约束,一个合法的顺序执行过程除了要保留线程内部的执行顺序外,还要保留线程间操作的先后顺序。一个系统满足线性一致性,那么它一定满足顺序一致性,反过来不成立。

    2 线性一致性理论模型

    现在看一下,怎么用线性一致性的理论模型来描述上面的例子,具体如下图所示。

    首先在线性一致性模型中,把某个操作抽象为一个事件(对应到实际系统中,可以是一个多线程程序中的本地函数调用,也可以是分布式系统中的一个rpc call)。每个事件起始于invoke发出,终止于收到response。同时定义事件间的如下happen before关系:

    对于事件e1和e2来说,如果事件e1的response是在事件e2的invoke之前,我们就说e1 happen before e2。

    对于同一个线程来说,前面的事件一定happen before后面的事件。但是对于不同线程上的两个事件来说,它们之间只有在在时间线上没有交叉的情况下,才会存在happen before关系。对于有交叉的那些事件,比如下图中的event2和event3,它们两个就不存在happen before关系,对于我们要寻找的合法顺序执行过程来说,它们两个的顺序可以是任意的。如下图,事件间的所有happen before关系用绿线表示。

     History:是由事件组成的一个执行历史,如上图就是一个执行历史。如果对于一个History来说,我们可以找到一个顺序执行过程,该顺序执行过程满足如下条件:

    1.保留了History中所有事件的happen before关系 2.是一个合法的顺序执行过程

    我们就说这个History(历史执行过程)是可线性化的。

    如果一个系统的所有执行过程都是可线性化的,那么我们就说该系统是线性一致的。

    3 可线性化验证算法

    给定一个History,怎么判断它是不是可线性化的呢?最简单的方法就是枚举,我们把给定History中包含的事件进行全排列,排除掉违反了happen before约束的那些排列方式,然后对剩余的进行验证,如果能找到一个合法的执行过程,我们就认为它这个History是可线性化的。但是这个算法的复杂度是n!,如果History包含了比较多的事件,基本上就没法接受了。

    下面的算法针对这一问题提出了不同的优化思路。

    3.1 WG算法

    具体的算法伪代码如下:

    这个算法是递归的,就是对于给定History,不断从里面找合法的minimal operation的过程。

    关于minimal operation的定义如下:

    minimal op:no op happen before it.

    从时间线上看,在最左侧的那个事件以及与它有交集的事件,都属于minimal operation。

    这个算法是在保证满足happen before的前提下进行搜索,排除掉那些不满足happen before关系的搜索路径。如果当前操作作用到对象后还是合法的,就继续递归往下走,看剩余的操作能否找到一个合法的顺序执行过程,如果剩余的能直接找到就返回成功。如果当前操作不合法,就把对象恢复到当前操作之前的状态,然后尝试下一个minimal op,继续搜索。

    这个算法比简单全排列要快,比如History中的事件之间很多都具有happen before关系,搜索会很快。但是如果有很多并行的事件,比如下图所示的极端情况下所有事件都是并行的,那么复杂度也会退化到n!。

     

    3.2  WGL算法

    WGL算法是对WG算法的优化。它主要基于如下观察:

    1.执行过程中的相邻的两个读操作可以交换顺序 R1 R2 = R2 R1,因为相邻改变顺序后,读取的内容不变,同时对象的状态也不会改变

    2.状态去重:WG算法中搜索的空间实际上是由当前对象的状态和到达这个状态的operation集合来确定的。比如以一个寄存器模型为例,对于如下两个搜索路径:

    path1: e1 e2 e3 -> register=10 ...
    path2: e2 e3 e1 -> register=10 ...
    
    

    假设我们在WG算法中,当前的搜索路径为:path1,执行过e1 e2 e3之后,寄存器的当前值变成了10,然后在继续往后搜索中的某一步失败了,重新回溯,现在到达了path2,执行e2 e3 e1之后,寄存器的值也变成了10。与path1相比,寄存器的当前值相同,同时达到这个状态的事件集合相同都是e1 e2 e3(只是执行顺序不同),同时后续剩余的事件集合也肯定相同。此时实际上已经没有必要继续往下搜了,因为path1已经到达过这个状态,继续搜肯定也会失败。但是对于WG算法来说,它还是会继续搜索。

    如果我们把所有曾经达到的状态(寄存器的值+到达这个值的事件集合)都记录下来,每次搜索的时候查找一下,如果发现事件集合和寄存器与之前记录的一致,那么就可以直接跳过了。WGL算法就是使用了这种动态规划的思想,通过记住之前搜索过的状态,避免重复性的搜索。

    可以看出状态空间,从排列变成了组合,虽然有很大的降低,但依然是指数级的,复杂度依然很高。另外理论上已经证明,可线性化验证本身是一个NP完全问题。

    3.并行化搜索。我们还可以把搜索时的路径选择方法变换一下,最简单的比如进行随机化。同时启动多个线程执行不同的搜索过程,只要有一个搜索到就可以结束了。Jepsen测试的线性化验证中,默认情况下就会启动两种算法去搜索来进行加速,这种模式实际上采用的竞争模式,每个线程需要搜索的总空间是相同的,只是把路径的选择方式进行变化。也可以采用合作模式,把整个搜索空间进行划分,每个线程负责一个子空间,同样只要有一个找到即可。或者采用竞争与合作混合的并行模式进行搜索,同一个子空间下再启动多个进行竞争模式的搜索。

    3.3 P-compositionality*

    线性一致性模型的locality属性:一个系统是线性一致的,当且仅当它所有的对象都是线性一致的。该属性的详细证明见论文2),根据该属性,对于由多个对象的操作组成的一个History来说,如果它的每个对象都是线性一致的,那么这个系统就是线性一致的。这样我们在验证时,就可以把History按照对象进行分组,把同一个对象的放到一个组内,然后只要验证每个对象自己的History就可以了。这样可以大大降低元素History可线性验证的时间。

    P-compositionality*中,P代表了partition,可以理解成是把History映射到多个子History的分区函数。P-compositionality*是对locality概念的推广,locality是P-compositionality*的一种特殊划分方法,它对应的划分方法就是按照对象对History(一个History中可以包含多个对象的操作)进行划分,每个对象的事件作为一个子集合。在论文2)的定义中,对象代表了一种数据类型,通常由一组可能的values和操作组成。比如一个set就是一个对象,支持insert/get/exsits操作,在论文2)里会将set作为一个对象考虑,而P-compositionality*则进一步地可以把set里面的一个key作为一个对象考虑。以set为例,如果我们关于set的某个约束在某个partition方式下,比如根据key对History进行划分,满足:set对象是可线性化的当且仅当它的每个key都是可线性化的,我们就说它是P-compositionality*的。这样就可以把关于一个set对象的所有操作组成的History,再根据key进行更细粒度的划分,这样就只需要对同一个key的操作组成的子History分别进行验证。

    也就是说对于某些模型在某些特殊的约束条件下如果是满足P-compositionality*的,就可以把一个History按照对应的Partition方式分成多个子History,然后只要验证每个子History是可线性化的即可。相当于把复杂度中的n降低了。比如一个对象的History本来有100个事件,如果它满足P-compositionality*,通过partition就可以把它变成10个子集,每个只有10个事件,验证的事件数规模大大降低。

    比如原始的History是关于set1和set2两个set对象的操作组成的:

    set1 (insert 2)
    set1 (ok)
    set2 (insert 1)
    set2 (ok)
    set1 (get 1)
    set1 (ok)
    set2 (insert 3)
    set2 (ok)

     对于这个History来说,它的事件个数是4。根据线性一致性模型本身具有的locality属性,我们可以把这个History根据set对象分成两个History分别验证,每个变成了2个事件。

    ----------------------- sub history 1
    set1 (insert 2)
    set1 (ok)
    set1 (get 1)
    set1 (ok)
    ----------------------- sub history 2
    set2 (insert 1)
    set2 (ok)
    set2 (insert 3)
    set2 (ok)

    再根据set在按照key进行划分的情况下本身具有的P-compositionality*属性。可以继续按照每个set再按照key继续进行拆分如下:

    ----------------------- sub history 1
    set1 (insert 2)
    set1 (ok)
    ----------------------- sub history 2
    set1 (get 1)
    set1 (ok)
    ----------------------- sub history 3
    set2 (insert 1)
    set2 (ok)
    ----------------------- sub history 4
    set2 (insert 3)
    set2 (ok)

    这样每个history需要验证的事件数就变成了1。

    这个优化依赖于具体的数据模型是否存在满足P-compositionality*条件的一个约束和划分函数。

    4 如何降低验证的复杂度

    就算有如上的一些算法和优化,可线性化验证的复杂度在极端情况下仍可能是指数级的。实际进行Jepsen测试的时候,我们还可以通过如下途径来尽量降低验证的复杂度。

    4.1 尽量减少timeout的Operation

    线性一致性模型的nonblocking属性,允许一个事件只有invoke,没有response。对应到实际系统中,就是操作执行结果不确定,比如rpc调用timeout,对于这种情况我们不知道这个操作是成功还是失败。对于这种情况,在线性化验证过程中,会认为这个事件是在整个History结束之后才结束。这样这个事件就与发生在它的invoke之后的所有事件都是并行的,那么进行线性化验证时需要搜索的空间也随之变大。在极端情况下,如果所有操作都是timeout,那么所有的操作就都是并行的,整个验证复杂度是指数级的。

    需要尽量减少这种操作timeout的情况。举例来说,我们可以把读请求的超时认为是失败,原因是读请求不影响系统的状态,而对于失败的操作来说,Jepsen在进行线性化验证时会直接忽略掉。

    4.2 通过采用多个对象来增加并发

    受限于线性化验证复杂度的影响,需要限制对于同一个对象的操作个数,这样意味着测试压力不能太大。而根据locality属性,对于针对多个对象的History我们可以按照对象划分,只需要分别验证每个对象的History是否是可线性化的。因此如果我们希望增加系统的压力,产生更多的操作,可以通过增加对象个数来实现。以寄存器模型来说,我们可以通过构造多个寄存器对象进行操作来实现。Jepsen测试框架中除了普通的register模型外,本身也提供了对multi-register模型的支持,用户可以直接使用。

    展开全文
  • 针对存在时滞的多智能体系统, 提出了基于一类连续非线性函数的有限时间一致性算法. 利用Lyapunov 有 限时间稳定性理论和矩阵理论, 给出了这类算法使得系统能够在有限时间内达到一致的充分条件, 进而给出了一个...
  • 如何验证线性一致

    2018-11-01 15:00:01
    线性一致性(Linearizability)是分布式系统中常见的一致性保证。那么如何验证系统是否正确地提供了线性一致性服务呢?本文希望从‘什么是线性一致性’,‘如何验证线性一致性’,问题复杂度,常见的通用算法,以及...

    线性一致性(Linearizability)是分布式系统中常见的一致性保证。那么如何验证系统是否正确地提供了线性一致性服务呢?本文希望从‘什么是线性一致性’,‘如何验证线性一致性’,问题复杂度,常见的通用算法,以及工程实现五个部分,直观、易懂地回答这个问题。

    什么是线性一致性

    MAURICE P. HERLIHY 和 JEANNETTE M. WING曾在“ Linearizability: A Correctness Condition for Concurrent Objects ” 中对线性一致性给出了形式化的定义和证明,对分布式系统来说,简单的讲就是即使发生网络分区或机器节点异常,整个集群依然能够像单点一样提供一致的服务,即依次原子地执行每一条操作。假如我们可以站在最终操作执行的视角,将整个系统看做一个整体,一个保证线性一致性的服务应该如下图所示进行服务:
    bb

    由于每条操作是依次、原子的执行,相互之间没有重叠,为了方便理解,可以把一个操作在图上简化为一个点。如下图所示:

    bb

    然而,实际情况中,分布式系统通常是很多节点作为一个整体对外提供服务,并在内部处理网络或节点异常,我们无法站在上帝视角看到其执行序列。同时,我们真正关心的也是其作为一个整体对外的表现,而不是其中的每个单独节点。我们所能做的是站在客户端的角度,通过读写事件的发起和结束来感知整个系统。正如站在地球上仰望星空,通过光来感知天体,看到的每一次闪烁,可能真正发生在上万年之前。因此,下图才是真正可以看到的情况:
    bb

    上图,展示了在每个客户端看来,其请求从发起到结束的时间点。因此,我们希望通过一系列客户端的执行和返回序列来判断系统是否正确提供了线性一致性服务。

    如何验证线性一致性

    为了判断系统是否正确提供了线性一致性,首先在运行过程中获得一系列不同的执行历史,接着验证每组历史是否满足线性一致性,只要有一个不满足,便可以说系统不满足线性一致性。但如果没有发现不满足的历史,也不证明系统一定正确。然而,在工程中通过对大量的执行历史的验证,使得我们对自己的系统更充满信心,这就足够了。那么现在的问题转变为:如何验证一组执行历史是否满足线性一致性

    通过客户端可以看到一个读写请求的发起和结束时间,而其真正在服务端的执行可能发生在开始和结束中间的任意一点。因此,验证线性一致性的关键就是找到一组依次执行的序列,如果这组执行序列存在,则可以说这组执行历史是满足线性一致的,如下图所示:
    bb
    明显的,存在这么一组序列,因此我们说这组执行历史是符合线性一致性的。再来看一个不符合线性一致性的例子,如下图,可以看出,由于Client 3已经读到1,说明在Client 3请求结束前Client 2已经写成功,而又没有其他请求再次修改x的值,因此Client 4不应该在之后读到0。
    bb
    实践中,通常会通过在频繁注入异常的情况下,随机生成请求序列,收集执行的发起和结束历史,并寻找合理的线性执行序列,如Jespen。

    问题复杂度

    直观来看,这个问题是一个排序问题,极端情况下的时间复杂度为O(N!)。事实上,Phillip B. Gibbons和Ephraim Korach在Testing Shared Memories中已经证明其是一个NP-Complete问题。虽然Gavin Lowe在Testing for Linearizability中给出了一些特殊限制下的多项式甚至是线性复杂度的算法,但在通用场景下,判定线性一致性并不是一个容易解决的问题,其搜索空间会随着执行历史的规模急速膨胀。

    通用算法

    虽然判定线性一致性的复杂度极高,但我们还是能够通过一些技巧,在大多数场景下,在工程可接受的时间内给出结果,这里介绍三个常见的,且一脉相承的通用算法。在此之前,先对算法面临的问题进行抽象,以下图执行历史为例,给出算法的输入和期待的输出:

    bb

    Input: 调用历史

    1,Client1: Invoke Put x=0
    2,Client2: Invoke Put x=1
    3,Client1: Return Put x=0
    4,Client3: Invoke Get x
    5,CLient4: Invoke Get x
    6,Client3: Return Get 1
    7,Client4: Return Get 0
    8,Client2: Return Put x=1

    Output: 执行序列

    Client1 Put x=0
    Client4 Get 0
    Client2 Put x=1
    Client3 Get 1

    1,WG算法

    请求的调用历史中,存在着一种偏序关系:Prev,如果一个请求的Return发生时间早于另一请求的Invoke,我们便称其Prev另一个请求。显而易见,这种偏序关系是一致性验证算法必须要保留的。祸兮福所倚,也正是这种对偏序关系的保留,给了算法加速的可能。WG算法的思路非常简单:从调用历史中找出没有Prev的项,将其对应的请求执行并取出,之后对剩下的调用历史重复该算法,直到没有更多的调用历史或执行结果不满足。

    如上述例子中,“Client1 Put x=0” 和 “Client2 Put x=1” 由于其Invoke前没有任何请求Return,可以首先被取出。假如选择“Client1 Put x=0”,将其对应的Invoke和Return从调用历史中取出,得到新的历史:

    2,Client2: Invoke Put x=1
    4,Client3: Invoke Get x
    5,CLient4: Invoke Get x
    6,Client3: Return Get 1
    7,Client4: Return Get 0
    8,Client2: Return Put x=1

    和一条已经序列化的请求:

    Client1 Put x=0

    此时可以看到剩余的历史中,每一个请求的Invoke前都没有其他请求的Return,因此都可以作为下一个取出的选择。假设这次选择Client3 Get 1,然而,明显这个时候执行Get得到应该是0,与该请求的实际执行结果返回1不同,此时,需要回退并尝试其他取出策略。可以看出WG算法其实是树的深度优先搜索,其搜索树如下图,其中每个节点标识的是本次尝试序列化的请求对应的调用历史中的Invoke序号:

    由于找到一个线性序列便可以停止,因此其中虚线部分是不会被实际执行的。

    2,WGL算法

    WGL算法由Gavin Lowe在WG算法的基础上进行改进,其改进的方式主要是对搜索树的剪枝:通过缓存已经见过的配置,来减少重复的搜索。缓存配置有两部分组成:

    • 当前已经序列化的请求

    • 当前x值

    由上面的搜索过程可知,如果当前序列化的请求和当前的x值完全相同,则后续的搜索过程一定一致,因此可以略过。

    3,P-compositionality算法

    P-compositionality算法利用了线性一致性的Locality原理,即如果一个调用历史的所有子历史都满足线性一致性,那么这个历史本身也满足线性一致性。因此,可以将一些不相关的历史划分开来,形成多个规模更小的子历史,转而验证这些子历史的线性一致性,例如kv数据结构中对不同key的操作。上面提到了算法的计算时间随着历史规模的增加急速膨胀,P-compositionality相当于用分治的办法来降低历史规模,这种方法在可以划分子问题的场景下会非常有用。

    为什么Solitaire

    工程实践中,不只分布式系统,还包括需要并行访问的系统,都可能需要验证系统对外暴露的线性一致性功能。当然也有不少验证线性一致性的工具,比如大名鼎鼎的Jespen使用的Knossos,是一个Clojure版本的WGL的算法实现;Porcupine是一个Go版本的P-compositionality实现;linearizability-checker是P-compositionality算法作者自己实现的一个样例。但使用中还有几个问题没有解决:

    • 计算速度慢:由于上面提到的复杂度,一致性算法验证时间通常是相关测试中的瓶颈。尽可能的加快其计算速度,可以在相同时间内验证更多的历史,对发现系统中的潜在问题至关重要。

    • 数据模型单一:大多数的验证工具面向的都是KV接口,这就要求使用者将千差万别的系统实际接口转化为KV接口使用,而这层转换会掩盖系统中的众多复杂性,比如将Device接口转化为KV后会丢失对相互覆盖操作的验证。

    • 具体问题具体分析:对一些数据模型来说,可能存在多项式甚至是线性复杂度的算法,那么针对这些数据模型使用通用的WGL算法就舍近求远了。

    Solitaire(https://github.com/CatKang/Solitaire)是一个C++实现,更快速,支持多数据模型的线性一致性检测工具,致力于解决上述问题。其命名来源于上世纪著名的Windows桌面纸牌游戏,要求玩家在保证大小先序关系的限制下,将打乱的扑克牌整理为有序。可以说与我们的线性一致性验证工作非常契合了。

    参考

    • Linearizability: A Correctness Condition for Concurrent Objects

    • Testing for Linearizability

    • Faster linearizability checking via P -compositionality

    • Testing Distributed Systems for Linearizability

    • Testing Shared Memories

    • 线性一致性理论

    • Solitaire: 一个更快的,适配更多数据模型的一致性验证工具

    • knossos: Jespen所使用的一致性验证工具,WGL算法实现

    • porcupine: go版本P-compositionality算法实现

    • linearizability-checker: P-compositionality算法实现

    • Jespen

    原文链接:https://mp.weixin.qq.com/s/calyZj0-ZfiYuDlJWQoHaA

    来自 “ ITPUB博客 ” ,链接:http://blog.itpub.net/31559357/viewspace-2218356/,如需转载,请注明出处,否则将追究法律责任。

    转载于:http://blog.itpub.net/31559357/viewspace-2218356/

    展开全文
  • 也叫做strong consistency或者atomic consistency,于 1987年提出,线性一致性强于顺序一致性,是程序能实现的最高的一致性模型,也是分布式系统用户最期望的一致性。 与顺序一致性相比,线性一致.
  • 续时间不确定性非线性系统的鲁棒自适应反馈线性化, 使系统获得要求的跟踪性能。 在很弱的假设条件 下, 应用李雅普诺夫稳定性理论证明了闭环系统内的所有信号为一致最终有界。 仿真算例验证了该方法 的正确...
  • 研究固定拓扑结构下的分数阶非线性多智能体系统协调控制的动力学模型问题。由于实际多智能体系统中,系统的状态变量难以全部测量,为了克服这一困难,利用状态观测器对系统状态进行重构并基于重构状态进行状态反馈。...
  • 研究时变时滞与切换有向通信拓扑协议下高阶连续时间线性多智能体系统一致性问题. 利用一个线性变换将该问题等价转化为一个切换时滞系统的稳定性问题. 假定出现的每一个通信拓扑都是可一致的, 借助时滞切换系统稳定...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 689
精华内容 275
关键字:

一致线性系统