1 一致性与共识要回答什么
在单机上,运维脚本把新配置写进文件,随后启动的进程通常会读到这个文件中的内容。把配置服务扩展到 3 台主机后,事情就不再这样直接:主机 A 已收到新配置,主机 B 的复制连接中断,主机 C 刚从旧磁盘恢复。此时执行相同的读取命令,可能得到不同结果。问题不只是“复制得够不够快”,还包括服务已经向调用者承诺了什么,以及发生故障后还能承诺什么。
一致性描述读写操作之间允许出现的关系。例如,某次写入已经返回成功,之后开始的读取能否返回旧值?某进程先修改文件访问策略,再通知另 1 个进程重新打开文件,接收通知的进程能否仍看到旧策略?这些问题都需要明确操作的先后关系、读取的范围和成功的含义。只说“数据最终会同步”无法回答进程现在能否安全继续运行。
共识关注多个参与者怎样在故障条件下对某个决定达成约定,例如由谁作为领导者,或某条配置变更是否进入已提交的操作序列。它不是让每台机器随时存着完全相同的字节,也不是把所有网络消息都送达。复制日志可以暂时落后,只要服务对外暴露的结果仍符合约定。反过来,磁盘上的文件恰好相同,也不能证明下一次并发修改有确定的处理规则。
本课采用 3 个独立投票成员 A、B、C 的配置服务。每个成员由进程、网络端点和持久化目录组成。管理员将 worker_limit 从 4 改为 8,工作进程通过配置接口读取该值。我们关注崩溃、消息延迟、网络分区和恢复,假设成员按协议执行,不讨论恶意成员伪造投票。后面所说的多数派保证,都依赖这组故障假设。
必须先区分 3 种状态:请求已经发出,服务已经提交,客户端已经收到成功。连接在返回途中断开时,服务可能已经提交,客户端却只看到了超时。因此“调用失败”有时只说明结果未知。运维记录中应保留请求标识、提交版本和客户端观察时间,避免把通信结果直接当作数据状态。

对文件系统也要作同样区分。printf 退出成功、文件被另 1 个进程读到、主机断电后内容仍在,是不同承诺;即使使用了 fsync,它解决的持久化范围也不自动扩展到远端副本。本课不展开文件系统内部实现,只用这个对比提醒:讨论一致性时,先标明“哪个接口向谁承诺了什么”,再谈复制数量和性能。
2 线性一致、因果与最终一致
线性一致要求每个已经完成的操作,看起来都在其调用与返回之间的某个瞬间生效,所有操作能够排成符合实际先后关系的顺序。假设管理员写入 worker_limit=8 并收到成功,随后工作进程才发起读取;若这 2 个请求属于同 1 个线性一致服务的相应范围,读取就不能返回写入之前的 4。这个要求不依赖管理员主机与工作主机的时钟是否同步,而依赖调用和返回构成的实际先后关系。
若写入和读取的执行区间重叠,读取可以看到旧值,也可以看到新值,只要能够找到符合整体约束的顺序。不能因为“读到了旧值”就立即认定违反线性一致。排查时需要知道读取什么时候发起、写入什么时候完成,以及中间是否还有其他写入。终端上 2 行带时间戳的输出,未必足以建立这些关系。
因果一致保留能够建立因果关系的顺序。进程 P 先发布策略版本 policy-12,然后发送“按新策略启动”的通知;进程 Q 因收到通知而读取策略,这条链路应当避免让 Q 看见通知却看不见它所依赖的策略。实现时通常需要把相关版本或依赖信息传递给服务,不能期待服务从任意 shell 输出中猜出依赖。没有因果联系的并发修改,在不同观察者那里可能暂时呈现不同顺序。
最终一致强调收敛:当更新停止、必要的通信恢复、系统按照既定冲突规则继续运行时,副本最终应到达相容的结果。这个承诺本身没有给出“最多延迟 2 秒”,也没有保证同 1 个客户端连续读取不会倒退。运行状态展示、允许延迟的统计摘要可以采用这种模型,但需要另行给出可接受的新鲜度和收敛目标,不能把“最终”解释成故障永远可以不处理。

考虑工作进程先从 A 读到版本 12,随后负载分配把请求送到落后的 B,返回版本 11。在没有额外保证的最终一致接口中,这种倒退可能合法,却会让进程重复加载旧配置。可以使用会话固定、携带最低可接受版本或选择更强的读取接口改善体验,但它们解决的问题各不相同。携带最低版本后,B 若无法满足条件,应等待、转发或报出无法满足,而不是悄悄返回旧内容。
“读己之写”表示调用者能够读到自己已完成的写入,“单调读”表示观察到的版本不会倒退。这些是可以单独约定的会话保证,不应直接等同于完整的线性一致。服务可能保证某个会话的顺序,却仍允许其他会话看到不同结果。选择时应把业务依赖翻译成具体读写关系,而不是给产品贴上笼统的“强”或“弱”标签。
一致性还有作用范围。某个配置键上的线性一致,不等于跨 access_policy 和 worker_limit 的原子更新;文件内容读取有序,也不等于目录列表和权限元数据始终来自同 1 个快照。阅读接口文档或编写数据方案时,要列出键、文件、目录、会话或整个存储空间中的哪部分属于承诺范围。范围写得越清楚,出现异常时越容易判断是系统违约还是调用者提出了额外假设。
3 法定人数与领导者边界
法定人数是某类决定所需的参与成员集合。对于固定成员集合中的简单多数派,N 个成员需要 floor(N/2)+1 票。因此 3 个成员需要 2 票,5 个成员需要 3 票。3 个成员中的任意 2 个多数派至少共享 1 个成员;这个交集让后来的决定有机会接触先前决定留下的信息。交集提供了建立协议安全性的基础,但还不是完整证明。
共同成员必须遵守投票和日志规则。例如,在同 1 个任期内不能把领导者选票同时投给 2 个候选者,重启后也不能忘记已经作出的相关承诺。选举还要考虑候选者掌握的历史是否足够新,提交操作也必须满足协议规定的条件。如果成员随意投票,或者磁盘恢复让它遗失投票记录,仅靠“这次拿到了 2 票”不能保证先前已确认的操作被保留。
领导者的作用是给变更安排顺序,并协调复制和提交。它收到写入后,通常先记录操作,再等待符合协议的复制确认,达到提交条件后才向调用者报告成功。这里的“确认”不是收到 TCP 数据包,而是协议所要求的日志状态和持久化状态。具体系统的提交条件可能更严格,不能把示意图中的步骤直接当作所有实现通用的代码。
网络分区后,旧领导者的进程可能仍活着,端口仍能连接,甚至还能读取自己的旧文件。存活不等于仍有权完成新写入。它若只能联系自己,就无法取得 3 个成员中的 2 票;另一侧 B、C 可能选出新领导者。客户端看到 2 个自称领导者的端点并不奇怪,关键是旧端点是否还能让被保护的操作产生效果。

对外部资源尤其需要防护。假设领导者负责触发共享文件目录中的维护任务,即使新领导者已产生,旧进程仍可能继续执行先前排队的命令。可以让资源端检查随请求携带的递增栅栏令牌,拒绝已经过期的持有者。令牌应来自可靠的授权顺序,资源端必须实际验证;只在日志里打印任期号没有阻止作用。租约也可能参与授权,但它对时钟和过期判断的要求需要另外说明。
读取同样有边界。向领导者地址发送读取,并不自动获得线性一致:网络隔离的旧领导者可能不知道自己已经过期。系统需要通过其协议规定的确认机制,证明读取所依据的状态仍有效。入门阶段记住“领导者身份需要依据,读接口需要声明保证”即可,暂不展开具体读屏障、租约优化和成员变更算法。
4 动手:对照一次脑裂投票失败
下面在本机 shell 中模拟投票可达性,不启动真实集群,也不修改网络规则。每个字母代表固定成员集合中的 1 个成员;同 1 个字母重复出现只能算 1 票。实验只回答“这个候选者是否拥有固定 3 成员集合的多数”,不会模拟持久化、选举历史或真正的日志复制。把模型范围标在实验开头,才能正确解释结果。
quorum3() {
local candidate="$1" member
local a=0 b=0 c=0 votes=0
shift
for member in "$@"; do
case "$member" in
A) a=1 ;;
B) b=1 ;;
C) c=1 ;;
*) printf 'unknown voter: %s\n' "$member" >&2; return 2 ;;
esac
done
votes=$((a + b + c))
if [ "$votes" -ge 2 ]; then
printf '%s: %s/3, quorum reached\n' "$candidate" "$votes"
else
printf '%s: %s/3, quorum missing\n' "$candidate" "$votes"
return 1
fi
}
quorum3 A A B C
quorum3 A A || printf 'A must stop confirming writes\n'
quorum3 B B C
quorum3 A A A || printf 'duplicate votes do not help\n'
在 Bash 中依次执行,首个调用得到 3/3;模拟分区后 A 只能拿到 1/3,B 与 C 所在侧可以形成 2/3;最后重复列出 A,仍然只有 1/3。失败调用后的提示由 || 触发,不代表系统替你完成了隔离。实际服务应通过自己的协议拒绝不满足提交条件的写入,而不是依靠操作者看见提示后及时停止脚本。
脑裂配置中的典型错误,是把各自“可见的机器”当成新的成员总数。A 认为总共只剩 1 个成员,于是 1 票就够;B、C 认为总共是 2 个成员,于是 2 票就够。双方由此都允许写入同 1 份逻辑配置。分区本来只能改变可达性,错误配置却让它改变了授权规则。固定成员集合下,多数门槛不能随着 ping 成功的主机数自动缩小。
恢复连通后,若 A 写入 worker_limit=6,B 写入 worker_limit=8,文件复制只能搬运这 2 份结果,无法追溯性地让已经执行过的进程行为符合单一顺序。把文件按修改时间覆盖也不能撤销旧配置触发的动作。正确实验结论应是:失去法定人数的一侧必须停止确认受保护的新写入;若允许它写入,就需要承认服务采用了另一套冲突与合并语义。
实际排查时,可先用下面的只读命令建立本机证据,再查询配置服务自己的状态接口。configd 是示例进程名,端口 7400 是本课约定的示例配置,使用时应替换为实际服务值。
pgrep -a -x configd
ss -lntp 'sport = :7400'
ss -ntp '( sport = :7400 or dport = :7400 )'
journalctl -u configd --since '-10 min' --no-pager
进程存在只能证明某个 PID 存活,监听端口只能证明本机打开了套接字,已建立连接也不能证明对端及时处理并持久化日志。权限不足还可能隐藏进程归属或日志内容。应把这些证据与成员标识、任期、领导者、提交位置和复制进度一起解释;不能拿 ping 成功替代投票结果,更不能把 TCP 连接数量当作法定人数。
5 强一致与可用性取舍
当 A 与 B、C 无法通信时,A 收到新的配置写入应怎样回答?若它继续报告成功,同时 B、C 也确认其他写入,系统可能无法维持原先的单一操作顺序。若它等待恢复或返回无法完成,就牺牲了这个分区内相应操作的可用性。这里的选择针对具体操作和具体故障,不是给整套系统贴上“永远可用”或“永远一致”的标签。
CAP 讨论的是发生网络分区时,线性一致性和规定意义上的可用性无法同时完全保证。这里的可用性要求非故障节点收到的请求最终得到有效响应,不能用“马上返回错误”冒充完成了原操作。它也不是监控面板上的月度成功率。真实方案还要给请求设置有界等待和失败语义,否则无限等待虽然没有返回旧数据,也无法支持实际运维。
可以按接口拆开取舍。配置发布接口在没有法定人数时拒绝新写入;状态面板继续展示缓存值,但明确标注版本和观测年龄;要求最新权限的读取等待权威结果或失败。缓存内容必须与权威内容在响应语义上区分。如果调用方把缓存读取当成权限校验依据,就把展示层的宽松保证错误地带进了控制路径。
延迟也来自正常路径。领导者需要等待远端确认时,较慢的网络往返或持久化会抬高写入延迟。固定 3 成员中,领导者通常不必等待最慢的全部成员,但仍要满足协议要求的法定人数和持久化条件。将等待门槛从 2 份改成 1 份,看起来降低了延迟,却改变了故障后保留结果的依据;这不能被包装成毫无语义代价的性能调优。

读取超时后,调用者应分辨“无法证明足够新”与“键不存在”;写入超时后,应分辨“明确拒绝”与“结果未知”。若接口支持请求标识查询或幂等重试,客户端可用相同标识确认原请求的状态。若接口没有这类能力,就需要业务侧处理重复执行风险。单纯把超时时间从 2 秒延长到 20 秒,只改变等待预算,没有解决状态不明的问题。
最终,选择来自操作后果。展示工作进程数量通常可以容忍短暂陈旧;撤销某进程访问受保护目录的授权可能要求更严格的观察顺序。但更强的配置读取也不会自动关闭已经打开的文件描述符,或终止正在运行的进程。数据层承诺需要和操作系统实际执行边界对齐,才能形成完整的安全与运维效果。
6 常见坑:把多数派当绝对正确
多数派保证的不是内容符合人的意图。管理员误把 worker_limit 写成 8000,3 个成员可以完全一致地保存这个错误。若容量限制要求最多 80 个工作进程,就应在写入入口实施值域检查,并在工作进程侧保留必要的资源保护。一致性维护观察顺序和协议约定,输入校验维护数据是否合法,职责不能互相替代。
多数派也不等于独立故障。3 个进程放在同 1 台主机,主机断电就会全部消失;3 个成员共用同 1 个存储设备,设备故障可能使所有投票记录不可用。即使进程数足够,实际故障域仍可能只有 1 个。评审时应把成员分布画到主机、供电、存储和网络路径上,再解释预期可以承受的故障,不能只数容器或 PID。
另 1 个常见错误是把“已发送到多数成员”当成“已经提交”。发送缓冲区接收了数据,远端进程读取了消息,远端完成必要持久化,以及领导者按规则推进提交位置,是不同事件。日志里出现 replicated 之类字段时,应查明它的定义。没有定义的“同步完成”提示,无法支撑故障恢复后的数据保证。

备份恢复还可能打破身份与历史假设。把旧磁盘快照直接启动成原成员,可能让它携带过时的任期、投票和日志;将同 1 份成员目录克隆成 2 个进程,又可能制造重复身份。恢复应遵循系统提供的成员恢复流程,区分原成员恢复、替换成员和建立新集群。不要仅凭端口监听成功就把新进程计入可用票数。
遇到“写成功后读到旧值”,先保留写响应中的请求标识与版本,再确认读取走的是哪种接口、是否经过客户端缓存或本机文件缓存层,最后对照服务端提交与应用位置。若客户端实际读的是工作进程加载到内存的配置,它观察的是配置传播和应用链路,而非配置存储接口本身。删除缓存可能暂时改变现象,却不能替代对承诺边界的判断。
还有些故障来自强制恢复。网络分区期间,操作者在 A 上把成员集合改成只有 A,又在 B、C 上继续运行原集群,就会产生 2 套授权来源。此类操作不能视为普通重启。应明确选定权威历史,隔离旧写入者,再按恢复流程重建服务;已经产生的冲突需要单独核对。自动重试不应偷偷执行缩小成员集合、清空状态目录等改变安全前提的动作。
7 案例:时钟回拨打乱版本顺序
某配置发布脚本使用墙上时钟的毫秒值作为版本号,读取端始终保留数值较大的版本。正常运行时,这看起来很直观:晚发布的配置通常拥有较大的数字。随后主机 A 的时间从 10:15:30 回拨到 10:15:10,脚本生成了更小的版本;主机 B 的时钟仍停在较晚位置,发布的旧内容反而拥有更大的时间戳。读取端于是拒绝接收实际较新的修改。
这里有 2 个独立问题。单台主机的墙上时钟可能回拨,多台主机的时钟也可能存在偏差。即使从未回拨,在 B 上较早发生的事件,也可能带有比 A 上较晚事件更大的时间戳。date 输出的是主机对当前时间的估计,不是跨主机操作顺序证明。提高时间精度并不能解决这个问题;从秒改成纳秒,只增加数字位数,没有增加可信的因果关系。
可以在相关主机上采集只读证据,重点是时区、同步状态和服务日志中是否出现校时或时间跳变记录。下列命令适用于常见 Linux 环境,具体同步服务名称取决于系统配置;没有某个服务单元不等于没有时间同步。
date --iso-8601=ns
timedatectl status
journalctl -u systemd-timesyncd --since '-1 hour' --no-pager
journalctl -u configd --since '-1 hour' --no-pager
不要为了复现这个案例直接调整生产主机时间。可以在设计评审中列出模拟事件:发布内容 X,时间戳为 1030;时钟回拨后发布 Y,时间戳为 1010;读取端按最大值选择 X。仅这组输入就足以揭示“时间戳越大内容越新”的规则不成立。若服务进程本身也使用墙上时钟计算超时,回拨还可能使等待异常延长,需要另行检查超时计时方式。

修正配置版本时,可以让权威存储按已提交更新分配递增修订号,并要求写入携带“预期当前版本”。例如读取版本 41 后,请求“仅当当前版本仍为 41 时写入”,成功后返回新版本。若另 1 个写入者已经修改,服务应报告冲突,让调用者重新读取并决定,不能静默覆盖。这个比较与更新必须由服务作为原子操作提供,不能在 shell 中先读文件、比较数字、再写文件来冒充。
持续时间测量可使用操作系统提供的单调时钟,展示和审计仍可保留墙上时间。但单调时钟通常只在本机的相应运行范围内有意义,不能把不同主机或不同启动周期的读数直接拿来排序。采用递增修订号,也要说明其范围:单个键、某个集群,还是某条日志。恢复成新集群后若编号重新开始,客户端需要额外的集群身份来区分历史。
修复后应追踪完整链路:变更提交到修订号 42,工作进程随后报告已加载 42,实际行为符合该配置。审计日志可以同时记录修订号、请求标识、成员身份和墙上时间。时钟适合帮助人定位大致时间,修订号适合表达服务内的顺序;各自承担清楚的职责,才能避免时间异常再次改变配置优先级。
8 把一致性级别写进数据方案
数据方案应从读写对象和操作后果开始。对于配置服务,可以分别列出配置发布、权威配置读取、运行状态展示和审计查询。为每类接口写出调用方依赖的关系:发布成功后开始的权威读取必须看到该变更或更新版本;工作进程应用配置可以延迟,但要报告已加载修订号;状态展示允许陈旧,但必须能识别陈旧程度。这样比“核心数据强一致、其他最终一致”更容易执行。
成功响应需要写清楚证据。是本地接收、日志已经提交,还是所有目标进程均已应用?这 3 种接口可以同时存在,但应使用清楚的状态名称。若配置存储已经提交而工作进程尚未加载,控制台应显示传播中,不能把存储提交时间当成全体进程完成时间。若要求某些关键进程完成应用,方案还要列出目标集合和等待失败后的处理规则。
| 操作 | 所需保证 | 故障时的行为 |
|---|---|---|
| 发布受控配置 | 按约定提交后才成功,返回修订号 | 无法形成法定人数则拒绝或超时,并区分结果未知 |
| 读取当前权限策略 | 线性一致读取,注明作用范围 | 无法证明新鲜度时失败,不静默降级 |
| 展示进程运行数量 | 可用缓存,附观测时间与来源 | 标注数据陈旧,避免伪装成实时值 |
| 等待进程加载配置 | 指定目标报告已应用修订号 | 返回未完成目标,保留排查信息 |
部署部分要写出投票成员数、固定身份、故障域和持久化目录的责任。客户端策略要写出连接入口、读取模式、超时、重试以及请求标识的有效范围。恢复部分要规定如何确认权威历史、如何阻止过期写入者、如何重新加入成员。对于 3 成员多数派,只能在相应假设成立时说明可容忍 1 个成员不可用,不能把这个结论扩大成任意存储损坏都可恢复。
观测指标应覆盖端到端关系。至少保留提交修订号、各成员复制或应用位置、工作进程已加载修订号、读取模式、请求失败原因及延迟。指标名称应对应实际实现,避免把不同含义的数字直接相减。某个修订号跨过了多个无关键的更新时,版本差距也不能直接换算成秒;要度量陈旧时间,就需要明确定义采样与计算方式。
验收应使用可判断的事件,而不是“断网后大体正常”。例如,在写成功之后发起权威读取,检查是否返回该版本或合法的更新版本;隔离少数派后,检查其是否停止确认受保护写入;恢复后,检查已提交历史是否保留;模拟客户端响应丢失,检查相同请求标识的重试行为。每项都要同时记录请求、返回和服务端证据,避免只看某台主机上的文件内容。
最后,区分协议保证与服务目标。协议可以约束不返回违背顺序的值,但通常不能无条件保证任意故障都在 3 秒内恢复。恢复时间、允许的陈旧时间和正常延迟可以作为可测目标,注明适用故障和环境。若外部脚本、缓存或文件目录绕过配置服务直接写入,方案应把这条入口纳入控制,否则纸面上的一致性只覆盖了实际写入路径的一部分。
9 从一致性与共识走向分布式事务(S52)
现在我们已经能够讨论某条操作序列如何由多个副本共同维护,也知道“发布成功”和“所有进程完成应用”不是同 1 件事。下一步的问题是:当需要共同改变多个对象时,怎样避免只完成部分修改?这就是进入分布式事务之前应识别的边界。共识可以帮助某个复制组决定操作顺序,却不会自动把所有外部动作合并成不可分割的整体。
例如维护脚本需要先更新配置服务中的工作目录路径,再让远端进程切换到新目录。配置变更可能已提交,而远端主机在切换前崩溃;也可能进程已切换,回报却丢失。重新读取配置只能知道期望状态,不能证明远端动作已经执行。即使配置键的读取完全线性一致,这个跨服务流程仍可能停在中间状态。
把场景缩小到单机,也能看见相同边界:将临时文件通过 rename 替换成正式配置,通常可以避免读取者看到半份文件,但它不会同时保证远端日志写入成功,更不会确保另 1 个进程已经重新加载。某个接口的原子性必须按接口规定的范围理解,不能从局部原子操作推导出整套运维流程的全局原子性。
因此,在学习 S52 前,先把流程拆成可观察的状态:期望配置已经提交、目标进程已经接收、目标动作已经完成、结果已经确认。为动作分配稳定标识,并明确超时后查询什么、重试什么、由谁处理未完成状态。这里先建立问题清单,不提前展开事务提交协议、隔离级别或复杂恢复算法;这些机制要根据跨对象承诺来选择。
本课也给事务讨论划出了基础:底层读取若可能陈旧,协调者就不能随意把读到的值当成最新事实;底层写入若返回结果未知,重试就需要考虑重复效果;领导者失效后,外部动作还需要阻止旧执行者继续产生影响。S52 将在这些边界之上讨论多个参与者的完成与失败,而不是把“使用了共识”当成流程已经全部可靠的结论。
放进方案里
在本项目中,先将配置发布、权威读取、运行展示与进程应用确认列成独立接口。配置发布返回成功表示更新已按协议提交,并返回集群身份、请求标识和修订号;权威读取采用线性一致语义,适用范围落实到指定配置对象。运行展示允许使用缓存,同时返回观测时间和数据来源。进程是否执行新配置,以目标进程报告的已加载修订号判断,不从存储提交成功推定。
配置服务采用 3 个固定投票成员,多数门槛为 2,成员分散到独立主机并记录存储与网络故障域。网络分区时,失去法定人数的一侧停止确认受保护写入;无法满足权威读取的请求返回明确失败。缓存展示可以继续,但调用方不得将其用于需要最新策略的控制操作。外部资源若接受领导者触发的动作,应校验有效的栅栏令牌,拒绝过期执行者。
客户端区分明确拒绝、确认成功与结果未知。超时后的重试使用接口支持的稳定请求标识,并查询原请求状态;版本竞争使用服务端原子比较与更新。墙上时间仅用于展示和审计,配置优先级采用有明确范围的修订号。恢复时保留原始日志,确认权威历史,按成员恢复流程操作,不因可达节点减少而自动降低投票门槛。
验收覆盖少数派隔离、领导者退出、响应丢失、落后成员恢复和模拟时间戳回退。记录请求起止、返回语义、提交位置、成员应用位置及进程加载结果,检查已确认更新是否保留、旧执行者是否被拒绝。正常延迟、允许陈旧时间和恢复时间另列为可测目标,并注明故障条件。跨配置服务与远端进程的联合修改纳入 S52 事务设计,明确中间状态、重试边界和人工处理责任。
10 小结:下一步分布式事务(S52)
一致性规定允许观察到的读写关系,共识帮助多个成员形成受约束的共同决定。多数派需要固定成员、可靠记录和正确协议支撑;领导者存活、端口可连或时间戳更大,都不能单独证明某个结果有权成为最新结果。
检查方案时,能够回答这 3 个问题即可继续:写入成功究竟承诺到哪一步?网络分区时哪些操作等待、失败或返回缓存?客户端超时后怎样确认原请求的结果?带着明确的接口范围与失败语义进入 S52,才能进一步讨论多个参与者怎样共同完成修改。