【问题标题】:Handling Race Conditions / Concurrency in Network Protocol Design在网络协议设计中处理竞争条件/并发
【发布时间】:2016-07-14 17:55:37
【问题描述】:

我正在寻找可能的技术来优雅地处理网络协议设计中的竞争条件。我发现在某些情况下,要同步两个节点以进入特定的协议状态是特别困难的。这是一个存在此类问题的示例协议。

假设 A 和 B 处于 ESTABLISHED 状态并交换数据。 A 或 B 发送的所有消息都使用单调递增的序列号,这样 A 可以知道 B 发送消息的顺序,A 可以知道 B 发送消息的顺序。或者 B 可以向另一个发送 ACTION_1 消息,以进入需要严格按顺序交换消息的不同状态:

发送 ACTION_1 接收 ACTION_2 发送 ACTION_3

但是,A 和 B 可能同时发送 ACTION_1 消息,导致他们都收到 ACTION_1 消息,而他们希望收到 ACTION_2 消息作为发送 ACTION_1 的结果。

这里有几种可能的处理方式:

1) 将 ACTION_1 发送到 ACTION_1_SENT 后更改状态。如果我们在这种状态下收到 ACTION_1,我们会检测到竞态条件,并继续仲裁谁可以开始序列。但是,我不知道如何公平地仲裁这一点。由于两端很可能会同时检测到竞态条件,因此随后的任何操作都容易出现其他类似的竞态条件,例如再次发送 ACTION_1。

2) 复制整个消息序列。如果我们在 ACTION_1_SENT 状态下收到 ACTION_1,我们会将其他 ACTION_1 消息的数据包含在 ACTION_2 消息中,等等。这只有在不需要决定谁是动作的“所有者”时才有效,因为两端都会最终对彼此执行相同的操作。

3) 使用绝对时间戳,但是准确的时间同步根本不是一件容易的事。

4)使用灯时钟,但据我了解,这些仅对因果相关的事件有用。由于在这种情况下 ACTION_1 消息没有因果关系,我看不出它如何帮助解决找出哪个最先发生以丢弃第二个的问题。

5) 使用某种预定义的方式来丢弃两端收到的两条消息之一。但是,我找不到没有缺陷的方法来做到这一点。一个天真的想法是在两边都包含一个随机数,并选择具有最高编号的消息作为“获胜者”,丢弃具有最低编号的消息。但是,如果两个数字相等,我们就会打成平手,然后我们需要另一种方法来从中恢复。一个可能的改进是在连接时处理一次仲裁并重复类似的顺序,直到两个“获胜”之一,将其标记为最喜欢的。每次出现平局时,最爱的一方获胜。

有人对如何处理这个问题有进一步的想法吗?

编辑:

这是我想出的当前解决方案。由于我找不到 100% 安全的方法来防止连接,我决定让我的协议在连接序列期间选择一个“最喜欢的”。选择这个最爱需要打破可能的联系,但在这种情况下,协议将允许多次尝试选择最爱,直到达成共识。 After the favorite is elected, all further ties are resolved by favoring the elected favorite.这将可能与协议的单个部分联系起来的问题隔离开来。

关于选举过程中的公平性,我根据每个客户端/服务器数据包中发送的两个值编写了一些相当简单的东西。在这种情况下,这个数字是一个以随机值开头的序列号,但它们可以是任何数字,只要这些数字是相当随机的。

当客户端和服务器必须解决冲突时,它们都使用 send(它们的值)和 recv(另一个值)值调用此函数。收藏夹调用此函数,并将收藏夹参数设置为 TRUE。这个函数保证在两端给出相反的结果,这样就可以在不重新发送新消息的情况下打破平局。

BOOL ResolveConflict(BOOL favorite, UINT32 sendVal, UINT32 recvVal)
{
    BOOL winner;
    int sendDiff;
    int recvDiff;
    UINT32 xorVal;

    xorVal = sendVal ^ recvVal;

    sendDiff = (xorVal < sendVal) ? sendVal - xorVal : xorVal - sendVal;
    recvDiff = (xorVal < recvVal) ? recvVal - xorVal : xorVal - recvVal;

    if (sendDiff != recvDiff)
        winner = (sendDiff < recvDiff) ? TRUE : FALSE; /* closest value to xorVal wins */
    else
        winner = favorite; /* break tie, make favorite win */

    return winner;
}

假设两端发送ACTION_1消息后进入ACTION_1_SENT状态。两者都会收到 ACTION_1_SENT 状态的 ACTION_1 消息,但只有一个会获胜。失败者接受 ACTION_1 消息并进入 ACTION_1_RCVD 状态,而获胜者丢弃传入的 ACTION_1 消息。序列的其余部分继续进行,就好像失败者从未在与获胜者的竞争条件下发送 ACTION_1。

让我知道您的想法,以及如何进一步改进。

【问题讨论】:

  • ACTION_1 和 ACTION_2 实际上是干什么用的?我知道你想让这个问题变得通用,但它可以澄清一些事情,以了解它们在你的系统中的实际用途。
  • 在这种情况下,它是用于同步剪贴板的协议。每次应用程序在剪贴板上粘贴内容时,它都会成为剪贴板的所有者并通知另一端。但是,一次只能有一个剪贴板所有者,并且两端可能同时尝试成为所有者。在这种情况下,只有一个人应该获胜以避免系统中的不一致。

标签: networking concurrency protocols race-condition


【解决方案1】:

对我来说,这个 ACTION_1 - ACTION_2 - ACTION_3 握手必须按顺序发生而没有其他消息干预的整个想法是非常繁重的,并且根本不符合网络(或一般分布式系统)的现实。您提出的一些解决方案的复杂性让我们有理由退后一步重新考虑。

在处理分布在网络上的系统时,存在各种复杂因素:数据包未到达、延迟到达、乱序到达、重复到达、时钟不同步、时钟有时会倒退,节点崩溃/重新启动等。您希望您的协议在任何这些不利条件下保持稳健,并且您希望知道确定它是稳健的。这意味着要让它变得足够简单,以便您可以考虑所有可能发生的情况。

这也意味着放弃所有节点将始终共享“一个真实状态”的想法,以及您可以让事情以非常可控、精确的“发条”顺序发生的想法。您希望针对节点就其共享状态达成一致的情况进行设计,并使系统在这种情况下进行自我修复。您还必须假设任何可能的消息可能以任何顺序出现。

在这种情况下,问题在于声明共享剪贴板的“所有权”。这是您需要首先考虑的一个基本问题:

  1. 如果所有涉及的节点无法在某个时间点进行通信,那么试图声明所有权的节点是否应该继续进行并表现得好像它是所有者一样? (这意味着当网络中断时系统不会冻结,但这意味着您有时会有多个“所有者”,并且对以后必须合并或以其他方式“修复”的剪贴板。)
  2. 或者,除非收到来自所有个其他节点的确认,否则任何节点都不应假定它是所有者? (这意味着系统有时会死机,或者只是响应非常缓慢,但您永远不会遇到变化不同的奇怪情况。)

如果您的答案是 #1:不要过多关注声明所有权的协议。想出一些简单的方法来减少两个节点同时成为“所有者”的可能性,但要非常明确可以有多个所有者。当它确实发生时,请在解决分歧的过程中付出更多的努力。仔细考虑这部分,并确保多个所有者始终会聚。不应该出现他们陷入无限循环试图收敛但失败的情况。

如果您的答案是 #2:这里是龙!您正在尝试做一些违反一些基本限制的事情。

非常明确地表明存在节点正在“寻求所有权”但尚未获得它的状态。

当一个节点寻求所有权时,我会说它应该每隔一段时间向所有其他节点发送一个请求(以防另一个节点错过第一个请求)。为每个此类请求添加一个唯一标识符,该标识符在回复中重复(因此延迟的回复不会被误解为适用于稍后发送的请求)。

要成为所有者,节点应在一定时间内收到所有其他节点的肯定回复。在该等待期间,它应该拒绝将所有权授予任何其他节点。另一方面,如果一个节点已经同意将所有权授予另一个节点,它不应该在另一个时间段内请求所有权(必须更长一些)。

如果一个节点认为它是所有者,它应该通知其他节点,并定期重复通知。

您需要处理两个节点都试图同时寻求所有权,并且都相互 NAK(拒绝所有权)的情况。您必须避免出现这样的情况:他们不断超时、重试,然后再次互相 NAK(这意味着没有人会获得所有权)。

您可以使用指数退避,或者您可以制定一个简单的平局规则(不必公平,因为这种情况应该很少发生)。给每个节点一个优先级(你必须弄清楚如何得出优先级),并说如果一个正在寻求所有权的节点收到来自更高优先级节点的所有权请求,它将立即停止寻求所有权并授予它改为高优先级节点。

这不会导致多个节点成为所有者,因为如果高优先级节点先前已确认低优先级节点发送的请求,它不会发送自己的请求,直到经过足够的时间它确定它之前的 ACK 不再有效。

您还必须考虑如果节点成为所有者,然后“变暗”——停止响应,会发生什么情况。在什么时候允许其他节点再次假设所有权再次“待命”?这是一个非常棘手的问题,我怀疑您找不到任何解决方案来消除同时拥有多个所有者的可能性。

可能所有节点都需要不时地相互“ping”。 (不是指 ICMP 回显,而是您自己的协议中内置的东西。)如果剪贴板所有者在一段时间内无法联系到其他人,它必须假定它不再是所有者。如果其他人在较长一段时间内无法联系到所有者,他们可以假设所有权可用并且可以请求。

【讨论】:

  • 感谢您的详细解答。我认为主要问题是有适当的机制来检测不一致并快速从中恢复。很容易检测到我们处于不一致的状态(两个客户端都收到了他们刚刚发送的请求的请求,而他们正在等待响应),但我正在寻找一种干净的方法来让系统重新站起来当它发生时。只要从协议的角度就所有者是谁达成一致,就不能合理地期望始终存在一个剪贴板所有者。
【解决方案2】:

这是此处感兴趣协议的简化答案。

在这种情况下,只有一个客户端和一个服务器,通过 TCP 进行通信。该协议的目标是两个系统剪贴板。特定序列之外的常规状态只是“CLIPBOARD_ESTABLISHED”。

每当两个系统之一将某些内容粘贴到其剪贴板上时,它都会发送 ClipboardFormatListReq 消息,并转换到 CLIPBOARD_FORMAT_LIST_REQ_SENT 状态。此消息包含在发送 ClipboardFormatListReq 消息时递增的序列号。在正常情况下,不会发生竞争条件,并且会发回 ClipboardFormatListRsp 消息以确认新的序列号和所有者。请求中包含的列表用于公开所有者提供的剪贴板数据格式,远程系统上的应用程序可以请求这些格式中的任何一种。

当应用程序向剪贴板所有者请求其中一种数据格式时,将发送带有序列号和列表中格式 id 的 ClipboardFormatDataReq 消息,状态更改为 CLIPBOARD_FORMAT_DATA_REQ_SENT。正常情况下,这段时间内剪贴板所有权没有变化,数据在 ClipboardFormatDataRsp 消息中返回。如果其他系统没有足够快地发送响应,则应使用计时器来超时,如果时间过长则中止序列。

现在,对于特殊情况:

如果我们收到处于 CLIPBOARD_FORMAT_LIST_REQ_SENT 状态的 ClipboardFormatListReq,这意味着两个系统都试图同时获得所有权。应该只选择一个所有者,在这种情况下,我们可以保持简单,选择客户作为默认获胜者。以客户端为默认所有者,服务器应以 ClipboardFormatListRsp 响应客户端,将客户端视为新所有者。

如果我们收到处于 CLIPBOARD_FORMAT_LIST_REQ_SENT 状态的 ClipboardFormatDataReq,这意味着我们刚刚收到了对先前数据格式列表中的数据的请求,因为我们刚刚发送了一个请求以成为具有新数据格式列表的新所有者。我们可以立即以失败响应,并且序列号将不匹配。

等等。我在这里试图解决的主要问题是从这些状态中快速恢复,进入一个重试循环直到它工作。立即重试的主要问题是,它的时机可能会导致新的比赛条件。我们可以通过期待这种不一致的状态来解决这个问题,只要我们在检测到它们时可以回到正确的协议状态。问题的另一部分是选择一个“获胜者”,该“获胜者”将在不重新发送新消息的情况下接受其请求。可以默认选出一个默认的获胜者,例如客户端或服务器,或者可以使用默认的最爱来实现某种随机投票系统来打破平局。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-04-01
    • 2014-02-23
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-03-13
    相关资源
    最近更新 更多