yjs是CRDT的一种实现。本文翻译了 yjs 的支撑论文中的部分内容,用于理解yjs的实现原理。文中引用部分是原文表达,其中某些格式没有做调整,仅作为辅助理解文章内容使用。
摘要
Near real-time collaboration using Web browsers is becoming rapidly more and more popular for many applications such as text editing, coding, sketching and others. These applications require reliable algorithms to ensure consistency among the participating Web clients. Operational Transformation (OT) and more recently Commutative Replicated Data Types (CRDT) have become widely adopted solutions for this kind of problem. However, most existing approaches are non-trivial and require trade-offs between expressiveness, suitable infrastructure, performance and simplicity. The ever growing number of potential use cases, the new possibilities of cutting-edge messaging protocols that shaped the near real-time Web, and the use of N-way communication between clients (e.g. WebRTC), create a need for peer-to-peer algorithms that perform well and are not restricted to only a few supported data types. In this paper, we present YATA, an approach for peer-to-peer shared editing applications that ensures convergence, preserves user intentions, allows offline editing and can be utilized for arbitrary data types in the Web browser. Using Yjs, its open-source JavaScript library implementation, we have evaluated the performance and multiple usage of YATA in Web and mobile browsers, both on test and real-world data. The promising evaluation results as well as the uptake by many commercial vendors and open-source projects indicate a wide applicability of YATA.
近实时协作的应用在 Web 浏览器上变得越来越流行,如文本编辑、代码协同、草图绘制等应用。这些应用程序需要可靠的算法来确保参与的客户端之间的一致性。操作转换算法(OT)和最近流行的 交换可复制数据类型(CRDT)在近实时协作上被广泛使用。然而,大多数现有的方法都不能解决所有问题,需要在 expressiveness、基础设施、性能和简单性之间进行权衡。近实时的 Web 消息传递协议潜在用例的不断增加,以及客户端之间 N-way communication(如 WebRTC)的使用,使得对性能良好且支持多数数据类型的 P2P 算法的需求日益增加。在本文中,我们介绍了 YATA,这是一种用于 P2P 协同编辑的方法,它确保了收敛性,保留了用户意图,允许离线编辑,并可用于 Web 浏览器中的任意数据类型。YATA 算法的开源 JavaScript 库实现是 Yjs。我们评估了 YATA 在 Web 和移动浏览器中的性能和多种使用的情况,包括测试数据和真实数据。这些评估结果以及 Yjs 在许多在商业应用和开源项目中的使用,表明 YATA 可以被广泛使用。
Keywords: Near real-time collaborative editing; commutative replicateddata types; operational transformation; peer-to-peer information systems
关键词:近实时协同编辑;交换复制数据类型(CRDT);操作转化(OT)算法;P2P 信息系统
3 THE YATA APPROACH
The YATA approach is created to provide a scalable solution for P2P optimistic concurrency control on the Web. The main goals are to allow the P2P collaborative editing of Webpages (DOM elements), graphs, lists, objects and arbitrary types in the Web browser, using cutting-edge protocols for message propagation. Therefore, the algorithm proposes a basic structure using a linked list, which can be extended to achieve collaboration on new shareable data types. YATA’s linked list internal representation and a collection of predefined rules limit the number of possible conflicts and ensure intention preservation and convergence. The core idea is enforcing a total order on the shared data types. YATA also supports offline editing, being meant to cope with requirements coming from both Web and mobile clients, such as small operation updates for low band width, on and off connections, random message order at receive time, etc.
YATA 方法旨在为 Web 上的 P2P 乐观并发控制 提供可扩展的解决方案。主要目标是使用先进的消息传播协议(webRTC/websocket),让 Webpages(DOM elements), graphs, lists, objects 和 Web 浏览器中的任意类型可以 P2P 协作编辑。因此,该算法提出了一种基于链表的基本结构,通过扩展链表以实现新的可共享数据类型上的协作。YATA 的链表内部设计和一系列预先定义的规则限制了可能的冲突数量,并保证了意图保留和收敛。其核心思想是在共享数据类型上确保全序关系。YATA 还支持离线编辑,来应对 Web 和移动客户端的需求。
YATA currently supports collaboration on linear data,trees, associative arrays and graphs. Using those types, it is possible to create more complex data types.
YATA 目前支持 linear data,trees, associative arrays 和 graphs 方面的协作。使用这些类型,可以创建更复杂的数据类型。
In the following we formalize our approach and exemplify YATA’S behavior on text (linear data). After giving the assumptions, definitions and describing how convergence achieved, we extend the linear representation to more data types and explain how those are further realized us-ing YATA.
在下文中,我们将展示 YATA,并将 YATA 的行为应用于 text (linear data)。在给出如何实现收敛的假设、定义和描述后,我们将线性类型扩展到更多的数据类型,并解释如何使用 YATA 进一步实现这些数据类型。
3.1 Requirements
Unique identifiers. Each user is represented by an unique identifier (userId). Additionally, each user gets an operation counter which gets incremented every time a user creates an operation. Upon its creation, the operation thus gets as-signed a unique identifier which is composed of the userId and the current operation counter.
唯一标识符。每个用户都用一个唯一的 userId 表示,都会获得一个操作计数器 count,该计数器在用户每次创建一个操作时都会递增。创建操作后,该操作将获得一个唯一标识符(userId, count),该标识符由 userId 和当前操作计数器 count 组成。
Operations. YATA represents linear data (e.g. text) as a doubly linked list. We define only two types of changes on this representation:insert and delete. As it is shown inFigure 1, every element in the linked list is represented by an insert operation (also named insertion). When an insertion is deleted, it is just marked as such and not removed from the list (i.e. tombstone approach). Therefore, delete operations do not have an effect on our insert algorithm. In Section 3.5 we define a garbage collection mechanism that, in combination with our insert algorithm 3.4, can remove deleted insertions.
操作。YATA 将线性数据(如文本)表示为双链表。我们在线性数据中仅定义了两种类型的操作:插入和删除。如图 1 所示,链表中的每个元素都由一个插入操作产生,即元素和插入操作一一对应。删除操作采用墓碑机制,即仅做一个标记,而不真正从列表中删除。因此,删除操作对我们的插入算法没有影响。在第 3.5 节中,我们定义了一种垃圾收集机制,该机制与我们的插入算法 3.4 相结合,可以移除被标记为删除的节点。
We denote an insert operation as(,,,,,), whereis’s unique identifier,is the content (e.g. a character),is aflag that marks an insertion as deleted, and,,are references to other already existing insertions. We represent linear data as a doubly linked listof insertions. Therefore,, andreference to the previous node, respectively next node in the list. denotes the direct predecessor at creation time (i.e., the node after which it was originally integrated to).
我们将一个插入操作表示为 ,其中 是 的唯一标识符, 是操作的内容(例如字符), 是将插入标记为是否删除,而 ,, 是对其他现有插入的引用。我们将线性数据表示为双向链表 , 和 分别引用链表中的前一个节点和下一个节点, 则表示创建时的直接前驱节点。
We defineas the natural predecessor relation on .
我们将 定义为 上的自然前置关系。 指 是 的前驱节点; 指 ,并且 可能等于
Example. When a user creates a new insertion at a local site, this is integrated between two insertions , and . The newly created insertion is therefore defined as: . Note that ,and are defined when an insertion has been applied to the list and may change when new insertions are integrated into S, but , defined at insertion creation time is never modified. After the user integrates a new insertion at his local site he sends it (via broadcast), as is, to all users.
举个例子。当用户在本地站点创建一个新插入时,会将该插入集成在两个插入之间。假设两个插入是 和 ,则新创建的插入将被定义为:。其中 和 是在一个 插入操作 被置于链表时定义的,当另一个新插入操作集成到 中时,可能会导致 和 发生更改。但在插入创建时定义的 永远不会发生改变。当用户在本地上集成新的插入操作后,本地站点会将其发送给所有用户。
A special case occurs when an insertion is performed at the beginning or at the end of S, because there is no insertion to refer to as , respectively . This can be fixed by using special delimiters, which denote the beginning and the end of S, respectively. Therefore, we assume without loss of generality that an insertion always defines ,, and
在 的开头或结尾执行插入时会出现一种特殊情况,即没有左插入或者右插入。这可以通过使用特殊的分隔符来解决,这些分隔符分别表示 的开始和结束。因此,我们可以假设 插入操作 总是可以定义 、 和 。
3.2 YATA 算法
The example in Figure 1 shows how a received operation is integrated in S. Here, the red connections reference the intention of the insertion, which is defined through and - i.e. insert the letter between these two letters. When the insertion is integrated, YATA assures that it will be placed some where between these letters. Convergence is therefore ensured, unless one or more remote operations were already inserted between and , which then leads to a conflict that needs to be solved.
图 1 中的例子显示了如何将接收到的 操作 集成到 中。在这里,红色连接指 插入意图,该意图通过 和 定义,即在这两个字母(位置 上的字母 和位置 上的字母 )之间插入。当插入被集成时,YATA 确保它将被放置在这些字母之间的某个位置。因此可以确保收敛,除非在 和 之间已经插入了一个或多个远程操作,这将导致需要去解决冲突。
Intention PreservationThe intention of an insertion is preserved if and only if the insertion is integrated some where between and . This notion of intention preservation conforms to the natural perception for the intention of text insertions and it is similar to other definitions found in the literature. In [2], the intention preservation is defined when each character inserted by a user between two other characters in a document keeps its relative position between its neighbors during the editing process.
意图保留:当且仅当插入被集成在 和 之间某处时,插入 的意图才被保留。这种意图保留的概念是符合实际的文本插入。
The concurrent insertion problem: In the example in Figure 1, the intention of the insertion of “T” is that should be inserted between the characters “Y” and the “A” - e.g., at creation time, “T” sees only “YA”. However, a letter sequence “AT” has been already inserted between these two letters. In the example,, andconflictwith .
并发插入问题:在图 1 的示例中,插入 的意图是在字符 和 之间插入 ,比如在创建时, 只看到 。但是在本例中, 两个字母之间已经插入了字母序列 ,所以造成了 和 与 冲突。
Conflicting insertions: Keeping the above notations, assuming , then we say that conflicts with .
插入冲突:使用上述符号,假设 ,那么我们说 与 冲突。
In the following we define a function that specifies a position for in the set of conflicting insertions (). As such is integrated between , and when . Furthermore, we show that every site converges when integrating with (i.e., we prove that is a strict total order function).
下面我们将定义函数 。它指定了 在插入集中发生冲突的位置。当 时, 将被插入到 和 之间。此外,我们还证明了当使用 函数集成时,每个节点都收敛(即 是一个严格全序函数)。
In the following we will frequently refer to the graphical representation of insertions as it is shown in Figure 1. The insertion has three references/connections. On the left hand site of there is a connection, and a connection to . On the right hand site of there is a connection to . While , and define the usual predecessor/successor relation in a linked list. The connection will never change and is employed to find the strict total order function .
在下文中,我们将经常提到图 1 中插入的表示。插入 有三个连接。在 的左边有一条 和 到 的连接。在 右边有一条 到 的连接。而 和 定义了列表中常见的前置/后续关系。 连接永远不会更改,它用于查找严格全序函数 。
We compose the following three rules in order to find a strict total order on conflicting operations.
为了找到解决冲突的严格全序函数 ,我们提出了以下三个规则。
Rule 1 We forbid crossing of origin connections (red lines in the graphical representation) between conflicting insertions. This rule is easily explained using the graphical representation of insertions in the linked list. As we stated before, every insertion has an origin connection to an insertion to the left (to a predecessor). Only when two operations are concurrently inserted after the same insertion, they will have the same origin.
规则 1:我们禁止 发生冲突的的插入 的 origin 连接(图中红线的连接) 交叉。使用链表中插入的图形表示很容易解释这一规则。如前所述,每个插入都有一个到左侧/前置插入 的 origin 连接。只有当两个插入操作在同一个插入后面并行插入时,它们才会有相同的 origin。
Figure 2 illustrates the two cases that are allowed when line crossing is forbidden. Either, one operation is between the other operation and its origin, or the origin of the one operation is a successor of the other operation. Therefore, the following formula must hold for conflicting insertions and :
图 2 说明了禁止红线交叉线时允许的两种情况。要么一个操作位于另一个操作与其 origin 之间,要么一个操作的 origin 是另一个操作的后续操作。因此,在插入 和 发生冲突时,以下公式必然成立:
Rule 2Specifies transitivity on . Let . Then following rule ensures, that there is no that is greater than , but smaller than , with respect to
规则 2:指定 上的传递性。 确保了没有大于但小于的
Rule 3When two conflicting insertions have the same origin, the insertion with the smaller creator id is to the left. We borrow this rule from the OT approach. But in OT this rule is applied when the position parameters are equal.
规则 3:当两个冲突的插入具有相同的原点时,创建者 id 较小的插入位于左侧。我们从 OT 算法中借用了这条规则。
We get retrieve the total order function by enforcing all three rules:
通过强制执行这三条规则,我们得到了全序函数 的定义:
3.3 Correctness
only depends on the connection, and we specified above, that never changes. We can conclude that whenever two sites compare conflicting insertions, they will find the same order for insertions. Furthermore, this implies that all sites will eventually converge. Finally, we prove that is a strict total order function, i.e. is a total order on conflicting operations. Therefore, we have to show that for all conflicting insertions , , and the ordering function is antisymmetric, transitive, and total.
仅取决于 连接,我们在上面指定了 永不改变。我们可以得出结论,每当两个站点发生插入的冲突时,它们都会找到相同的插入顺序。这意味着所有站点最终都将聚合。接下来,我们将证明了 是一个严格的全序函数,即 是冲突操作的全序函数。因此,我们必须证明,对于所有冲突的插入 、和,排序函数 是具有反对称性、可传递性和整体性。
具体的证明过程过于数学,请读者移步论文原文。
3.4 插入算法 Insert Algorithm
Previously, we proved that there exists a total order relation on conflicting insertions. In this section we show how we can compute the new position for an insertion, when we already have an ordered list of insertions.
在此之前,我们证明了 发生冲突的插入操作 存在全序关系。在本节中,我们将给出一个算法,当我们已经有一个有序的插入列表时,计算新的插入的位置。
Listing 3.4 shows how the conflicting insertion can be solved algorithmically. The algorithm exploits property (3) (no line crossing) as a breaking condition. Therefore, we stop computing when origin connections definitely will cross.
下图显示了如何通过算法解决冲突插入。该算法利用不存在的交叉的 origin 链接作为中断条件。因此,当连接之间会交叉时,停止计算。
The worst case time complexity of the algorithm is O(|C|2) where |C| is the number of conflicting operations. In the case that the breaking condition is reached in the first iteration, no positions are compared. This is why the best case time complexity is O(1). A complexity analysis is presented in Section 3.7.
该算法最坏情况下的时间复杂度为,其中 是 发生冲突的插入操作 的数量。在第一次迭代中达到破坏条件的情况下,不比较任何位置。这就是为什么最佳情况下的时间复杂度为。
3.5 垃圾回收 Garbage Collection
In the literature, garbage collection has been also proposed in [21] where “cold” areas of a document are identified or in Logoot [31], which uses a graveyard for removed operations. Conceptually, an insertion marked for deletion can be garbage collected when all sites received the remove operation and have in their internal representation the operation that is to be garbage collected. However, it is hard to determine if all collaborators know simultaneously that a content was deleted. Ideally, using classic methods such as state vectors, a mechanism where YATA ensures that all sites have applied a removal can be a candidate solution. As a downside, such a mechanism would require more network resources and leads to a decrease in performance, especially in P2P settings. An optimal solution to this issue is still being considered.
从概念上讲,当所有站点都接收到了删除操作并在其内部存储中标记了该删除操作应该被垃圾回收机制回收时,可以对该操作进行垃圾回收。但是,很难确定所有站点是否能够同时知道某个内容已被删除。
In the current approach, the problem is simplified by assuming that all users retrieved a certain remove operation after a fixed time period t which can be set according to the expected protocol and network characteristics (e.g., 30 s). In practice, YATA uses two buffers for garbage collection, to ensure that list elements are not directly removed. As such, once ok can be garbage collected, it will be moved into the first buffer. If nothing changes, after t seconds it will be copied into the second array and from here will be re- moved by the garbage collector (i.e., can be safely removed from the list and the buffer). From our practical experiences and the use in production, such a delay is sufficient to ensure that content will be removed safely, without any losses that may lead to inconsistencies. This is in line with experiments performed for assessing the NRT criteria and measuring the time in which operations are being applied. These experiments (cf. Figure 9, Section 6) show that the average time for receiving and applying a remove operation using text (with a length under 103 characters) at a remote site is approx. 12 ms. Under the same conditions, receiving and applying a single remove operation with a length of 105 characters at a remote site is done within an average time of 39.3 ms (SD 2.45 ms), from a pool of ten measurements.
在目前的方法中,通过假设所有用户都可以在一个固定时间段 t 后接收到某个删除操作(参数 t 可以根据预期的协议和网络特性进行设置,如 t=30s),可以简化上述问题。实际上,YATA 使用两个缓冲区进行垃圾回收,以确保不会直接删除列表元素。因此,如果 可以被垃圾回收,它将被移动到第一个缓冲区中。如果没有发生任何变化,t 秒后它将被复制到第二个缓冲区中,并从这里被垃圾回收器删除(即可以安全地从列表和缓冲区中删除)。从我们的实际经验和生产环境的使用效果来看,这样的延迟足以确保内容被安全地删除,而不会造成各个站点的不一致。
As a consequence of YATA’s rules, in some cases it is not possible to remove insert operations. The reason is that for an operation that is inserted between two undeleted insert-type operations, this could lead to a deleted predecessor or successor (cf. Figure 4).
根据 YATA 的规则,在某些情况下无法删除插入操作。原因是,对于在两个未删除的插入操作之间插入的操作,可能导致前驱节点或后续节点被删除(参见图 4)。
In order to ensure consistency, YATA demands that a new insertion is always inserted between the most left non- deleted character and its direct successor. Only then, the garbage collector can remove all operations that are to the right of the first deleted insertion.
为了保证一致性,在 YATA 算法中,新插入操作的位置必须在最左边的未删除字符和它的直接后继字符之间。只有这样,垃圾回收机制才能删除第一个已删除插入的右侧的所有操作。
Furthermore, due to its design, the garbage collector in YATA may break late join mechanisms. This is because when a user is offline for a period longer than t seconds, it will still hold references to deleted operations, while online users who already performed certain removals do not. Therefore, YATA does not support garbage collection for a site while it is offline.
此外,YATA 中的垃圾收集器可能会破坏延迟加入机制。这是因为当用户离线时间超过 t 秒时,它仍然会保留对已删除操作的引用,而已经执行某些删除操作的在线用户则不会。因此,YATA 不支持站点离线时的垃圾回收。
3.6 离线编辑支持 offline Editing Support
YATA supports offline editing using the internal data representation which is maintained at each client. Once clients are online, YATA performs a check for diverged states of the shared data and synchronizes it.
YATA 在每个客户端都维护一个状态向量,用于支持离线编辑。客户端连接网络后,YATA 会对共享数据的不同状态进行检查和同步。
Every site holds a state vector. It saves the next expected operation id per user. As an example, consider user1 with userId 1 is in a session with user2 with userId 2. Both users created two operations. As we explained above, the operation id is defined as a tuple of userId and operation counter. Therefore, the state vector is expressed as: [(1, 2), (2, 2)] (assuming we start counting with 0).
每个站点都有一个状态向量。 它保存每个用户的下一个预期操作 ID。 例如,假设 ID 为 1 的用户 user1 与 ID 为 2 的用户 user2 处于会话中。两个用户都创建了两个操作。正如我们之前解释过的,操作 id 被定义 (userId, count)。 因此,状态向量表示为: (假设从 0 开始计数)。
For synchronization, the state vector is not sent with each operation, but it is sent only once to all clients. A user that receives a state vector compares it with the local state vector and sends all remaining operations to the synchronizing client. In order to make operations integrable on the remote instances, operations are sent in the order and the form in which they were created. Our YATA’s implementation can transform integrated operations to their original form.
进行同步时,状态向量不是随每个操作一起发送,而是向所有客户端仅发送一次。 接收状态向量的用户将其与本地状态向量进行比较,并将所有剩余的操作发送到同步客户端。 为了使操作可在远程实例上集成,要按照创建它们的顺序和形式来进行发送。YATA 可以将这些操作转化其为原本的形式。