Logoot 算法

100 阅读4分钟

Logoot algorithm

一种不需要墓碑机制的,用于线性结构的分布式crdt数据同步模型。

CCI criteria

  • Casuality 所有操作被以lamport clock 顺序排序,并在所有数据副本上以相同的顺序被应用
  • Convergence 所有副本最终会达到一致状态
  • Intention preservation 对某个副本的操作产生的影响必然会被同步给所有其它的副本,intention 一般有两种:
    1. insert
    2. delete

以上三条是功能性标准,另外添加一条性能标准

  • 系统的性能不能因为节点数量增加或者数据中包含的内容增加而受到显著影响

CCI consistency implies causal consistency and eventual consistency. Intention preservation means that an operation effect observed on a copy, must be observed in all copies whatever any sequence of concurrent operations applied before

Related works

  • Wooki:基于precondition,某些case会违反Casuality,利用墓碑机制实现 convergence
  • TreeDoc:使用二叉树来表示文档,使用墓碑机制来删除元素,其主张使用“2-phase commit”来删除墓碑,但无法在开放网络环境下使用。催生了 Commutative Replicated Data Type,利用墓碑和逻辑时间戳向量来保证Casuality和Convergence
  • The Operational Transformation approach 这个框架里的所有算法都需要使用时序向量,除了MOT2,而MOT2依赖于墓碑机制。

上述的所有实现要么依赖于时序向量,要么依赖于墓碑机制,时序向量会随着编辑者数量的增加而成比例的变大,墓碑机制则会让数据不停地增加,因此都不符合性能标准。

Proposition

The main idea of this framework is to use a data type where all concurrent operations commute. Combined with the respect of the causality relationship between operations, this commutation ensures the convergence criteria.

Logoot 模型的主要思路是,使用一种数据结构,这种数据结构中所有并发的操作都是 commute 的,结合操作之间的因果关系,convergence标准得以保证。

为了在线性结构中实现 commutativity,作者提出了基于文档中元素之间 total order 的方案,具体如下,对文档的操作可以定义为:

  • insert(pid,text)insert(pid, text) that inserts the line content text at the position identifier pid.
  • delete(pid)delete(pid) that removes the line at the position identifier pid.

在以前的论文中,位置之间的顺序使用二叉树来进行维护,后来利用crdt的方案又实用了时序向量来获取位置之间的顺序。

本文提出的方法是,使用一个整数列表来保存文档中每一行之间的顺序信息,这样可以做到,删除某一行后,不影响剩余行的顺序关系。

Logoot model

一个 Logoot 文档的基本编辑单元为 行,行使用 <pid, content> 来定义,其中, pid 是 position identifier 的简写形式,content则是行的内容,由文本组成。 假设要在文档的P行和N行插入新的一行A,则需要先在P行和N行之间创建一个新的位置,然后在以这个位置为参数生成行。

定义1

  • identifier 是一组数字 <pos, site> 其中pos是一个整数而site是节点标识
  • position 由一组 identifier 来表示
  • 在副本 s 上产生的 position 为 <pos, hs> 其中 pos=i1i2...in<p, s>,而hs是逻辑时间戳。 因此系统中的每一个 position 都是唯一的。

定义2

  • 设 p = p1p2p3...pn and q = q1q2q3...qn,当满足如下条件时,可推出 p < q: jm(i<jpi=qi)(j=n+1pj<idqj)\exist j \leq m (\forall i < j | p_i = q_i) \land (j = n+1 \lor p_j < _{id} q_j) [解释]:i<jpi=qi\forall i < j | p_i = q_i 是前置条件,意思是从j往前所有的identifier都是相等的,紧跟着的 j=n+1pj<idqjj = n+1 \lor p_j < _{id} q_j意思是说对于第一个不相等的identifier的下标 j = n+1 , pj<qjp_j < q_j

  • 对于两个identifier id1=<pos1,site1>id_1 = <pos_1, site_1>id2=<pos2,site2>id_2 = <pos_2, site_2> ,定义比较规则为: pos1<pos2 or site1<site2id1<id2pos_1 < pos_2\ or\ site_1 < site_2 \Rightarrow id_1 < id_2 [解释]:为什么 site 可以用于比较呢,site用于比较并不代表任何抽象的含义,而仅仅是为了保证当仅凭 pos 无法进行比较时,在所有端上得到一致的比较结果,或者说严格来讲这其实是一种 join 规则,其目的是为了得到相同的结果,而并没有表达逻辑上的先后关系(实际上pos相等意味着不存在逻辑上的先后关系)

修改文档

对 logoot 文档的修改有两种方式:增加行和删除行。其中增加行的操作就是在两个 position 之间找到位置并生成新的 position。generateLinePositoin(p,q,N,s) 表示在第 p 行和第 q 行之间插入 N 个新的行,s 表示 site_id。

插入的新行ln需要满足 lp<ln<lql_p < l_n < l_q ,结合对于 position 的比较规则,容易得出生成新的 position 所需要满足的条件。本文中以计算机能表示的最大整数作为基数,从而使模型能够生成尽可能多的行,实际中为了满足更加变态的需求,可以自定义算法来扩大此限制。如果 lpl_plql_q之间本来就存在着差值大于或者等于N的identifier,那么就使用这个找到的差值作为基数,否则使用max_int作为基数。

确定了基数interval,根据咱们要创建的新行的数量,可以得出,每一行对应的新创建的 identifier 的增量 step 为 interval // N ,于是我们只需要每隔一行为相应的 identifier 中的 pos 参数加上 step ,就可以得到新的 identifier,从而得到新的行位置。

另外,为了尽量避免在不同的端进行并发操作时,生成了相同的 identifier,可以加入随机数来影响位置生成算法的结果。虽然identifier中第二个元素 site_id 能够保证 position 的唯一性,但避免 identifier.pos 一致可以更加稳定地进行顺序的比较。

使用 python 代码实现上述过程:

import random

def prefix(pos, length):
    """
    Returns the prefix of a position up to the given length, padded with (0, '0') if necessary.
    
    Args:
    pos: List of tuples, where each tuple contains (position, site_id)
    length: The length of the prefix to return
    
    Returns:
    A list of the first 'length' elements of 'pos', padded with (0, '0') if 'pos' is shorter than 'length'
    """
    return pos[:length] + [(0, '0')] * (length - len(pos))

def generate_line_positions(p, q, N, s):
    """
    Generate N positions between p and q in the Logoot model.
    
    Args:
    p: Position tuple list representing the starting position
    q: Position tuple list representing the ending position
    N: Number of positions to generate
    s: Site identifier
    
    Returns:
    A list of N new positions between p and q
    """
    positions = []
    
    for i in range(1, N + 1):
        new_pos = generate_position_between(p, q, s, i, N)
        positions.append(new_pos)
    
    return positions

def generate_position_between(p, q, s, k, N):
    """
    Generate a single position between p and q.
    
    Args:
    p: Position tuple list representing the starting position
    q: Position tuple list representing the ending position
    s: Site identifier
    k: Current position index (1 to N)
    N: Total number of positions to generate
    
    Returns:
    A new position between p and q
    """
    max_int = 2**32 - 1
    i = 0
    max_attempts = 1000  # Set a maximum number of attempts to prevent infinite loop

    while True:
        # Determine the bounds for the new position
        if i < len(p):
            start = p[i][0]
        else:
            start = 0
        
        if i < len(q):
            end = q[i][0]
        else:
            end = max_int
        
        # Calculate the interval
        interval = end - start
        
        if interval >= N or i >= max_attempts:
            break
        
        i += 1
    
    # Calculate the new position's integer part
    new_int = start + (end - start) * k // (N + 1)
    random_offset = random.randint(0, (end - start) // (N + 1))
    new_int += random_offset
    
    # Ensure new_int does not exceed the end bound
    if new_int >= end:
        new_int = end - 1
    
    # Create the new position by copying the common prefix and adding the new integer and site ID
    new_pos = prefix(p, i) + [(new_int, s)]
    
    return new_pos

# Example usage
p = [(1, 'A')]
q = [(10, 'B')]
N = 5
s = 'S'

new_positions = generate_line_positions(p, q, N, s)
for pos in new_positions:
    print(pos)

合并同步到的操作

对行的插入和删除操作都可以对数时间复杂度完成,因为每一个行的position都是唯一的,因此我们需要做的就是使用二分查找算法从文档中找到行所在的位置。

同时,删除操作可以直接执行而无不会对剩下数据的顺序产生影响。甚至删除操作还可以让被删除行的identifier被再次使用。

正确性

为了保证 crdt 框架对数据收敛的要求,所有并发的操作必须是 可交换 的,也就是说操作的结果与操作应用的顺序无关。如果每一行的 position 都是唯一的,并且所有的行都可以被比较,那么不同的副本就能够在应用不同顺序的多个插入操作后,仍然得到相同的结果。

以下定理证明文档中不会出现两个position一样的行。

定理一

如果逻辑时间戳参数被保留,那么在每个文档中,行的position都是唯一的

证明:因为position的最后一个参数为 site_id,因此position在不同副本之间是唯一的。 在同一个副本中,可以在已有行l1l_1的位置上执行对行l2l_2的插入操作oio_i,但是只有在对l1l_1的删除操作odo_d之后才有可能。因此,我们有这样的逻辑关系:OdOiO_d \rightarrow O_i,也就是说在逻辑时间戳存在的情况下,l2l_2只能在l1l_1被删除后才能被插入当前的副本。

定理二

如果因果性被保留,Logoot模型能够确保一致性

证明:因为由 site_id 和逻辑时间戳组成的组合是唯一的,因此每一个position都是唯一的。因此 position 的集合符合 total order 的特征,total order 中任意两个元素都可以进行比较。因此,对于多个并发的插入/删除操作,无论以什么样的顺序执行,都能够最终得到一致的效果,这是因为,position和hs组合的全局唯一性确保了不会出现两个操作同时操作同一行的情况,也就是最终每个副本都会按照特定的,且一致的顺序执行并发操作。

【注】对于删除操作,如果由于网络原因,在收到OiO_i之前收到了OdO_d,仍然有足够的信息,将操作涉及到的行标记为删除,然后在收到相应的插入操作后,再进行删除操作。

定理三

如果因果性被保留,Logoot能够保留用户意图

证明:因为position是全局唯一的,并且不可修改,因此插入操作和删除操作也是唯一的。所有副本中的行的顺序也会由于total order 的特性而保持一致。

P2P 网络下的空间复杂度

对行进行操作的时间复杂度为 logN,而在端对端网络中,空间复杂度为N,也就是线形复杂度。

这是因为,文档的大小与参与编辑的用户,或者持有副本的节点数无关,而仅仅与文档中行的数量相关。

注意到,随着产生的操作越来越多,行的 position 中 identifier 列表也会越来越长,这会导致,文档越大,文档占用空间增加的速度也会越快。

效率

由于position中,identifier列表的大小可以无限扩大,因此如果某个文档中插入了大量的行,并且从来没有行被删除过,那么文档的开销在最坏的情况下会达到 O(n2)O(n^2),其中n是行的数量。然而,由于此算法的随机特性,这种最坏的情况出现的概率非常小,实践中,行会经常被删除,从而释放出可以被重复使用的position identifier,因此实际的开销一般会较低。

为了更加有效地衡量logoot的开销,我们使用logoot模型重现了某个wiki页面的编辑历史,在最坏的情况下,总共有43352个identifier,而被插入的行有623863个(多出来的都是被删除了的)。

另一方面,相较之下,使用墓碑机制实现的文档,其大小也是没有边界的,而且一旦创建元素,就会相应地生成一个墓碑数据,且这份墓碑数据不会被删除。反观logoot的实现,position identifier的大小仍然是较低的。

方法

使用 8 bytes 整数表示 site_id和positions,4 bytes 整数记录逻辑时间戳,因此一个position identifier至少占用 20 bytes。

同时与 Wooto 和 TreeDoc 进行对比,这两种实现都使用了墓碑机制,因此无法适应出现了大量删除操作的文档。TreeDoc没有配备垃圾回收程序,因为这种功能需要知道系统中存在多少个副本,我们的实验设定的场景下是无法知道的。

Wooto和TreeDoc的开销与操作的数量和类型直接相关,其主要是受到了墓碑机制的影响。

总共测试了三种页面的编辑

  1. 频繁编辑的大型页面
  2. 频繁编辑的页面
  3. 最大的页面

结果

Logoot 对比采用了墓碑机制的 crdt 实现,能够更加高效地存储文档信息,特别是在文档编辑过程中存在破坏性更新,以及频繁地进行插入删除等情况。

限制

我们用来模拟的数据源系统,也就是 wiki 的编辑功能,在设计的时候并没有考虑协同编辑,因此我们使用这些文档的历史记录来模拟协同编辑的过程,可能并不能完全代表真实情况。

与 Woot 相比,CRDT实现需要保存因果性,主要有两种方式:逻辑时间向量和casual barriers,逻辑时间向量会导致文档尺寸迅速增大,而casual barries虽然更小一些,但也与参与协同编辑的活跃节点数正相关。并且,crdt的实现都需要在同步消息中添加额外的数据来记录因果性。

结论

Logoot不需要墓碑机制就能实现 crdt 的三大特点 casuality、conergence、intention preversation。并因此实现了线形空间复杂度。相比于Woot 和 TreeDoc,具备更好的性能。