写在 25.06.11
这篇问答原文整理的范围更大更广,从 24 年八月刚 reorg 完开始接触 AD,到后面一直work on sync engine。大部分问题都通过问 ChatGPT 得到了答案,一些更细节的则是请教同事和 TL。有想过要不要整理为更系统的文章,而不是这样七零八碎的问答集。但是想到 replication 的原理虽然是那么个原理,但是确实理解过程中也有很多小问题,就算系统地写,也是不断加 note 打补丁 😃,所以就这样吧。问答集的形式也挺好的,自己在看的时候也很能回顾当时的心境。
Q: What's the difference between High watermark vector and UTD vector? They both store the last sync usn.
High watermark vector is used to mark the highest watermark among the servers, while UTD vector is used for replication. They are not always the same.
UTD 是“接收方”告诉“来源方”它已经知道哪些 DC 的哪些 USN ✅ 来源方根据这张清单跳过不该发的变更 ✅ 所以 if invocationId in UTD → 是拿接收方的 UTD 来对比
特性 High Watermark (HWM) Up-To-Date Vector (UTD) 表示什么 本地从某个特定来源 DC 拉到的最大 USN 我知道所有 DC 的哪些 USN 已复制 粒度 每个来源 DC 一个值 所有 DC 的全局快照 作用 控制“从哪开始拉” 控制“哪些不需要再拉” 是否能避免重复复制 ❌ 不能 ✅ 可以 多路径拓扑下有效吗 ❌ 不可靠 ✅ 可以去重
用途 UTD Vector HWM 确定 哪些变更可以跳过(已知) ✅ ❌ 确定 从哪开始拉增量(source 端 USN) ❌ ✅ 避免重复复制 ✅ ❌ 减少查找压力、提高效率 ✅ ✅ 是否和具体 DC 的复制关系有关 ❌(全局) ✅(pair 级别)
Q: How replication works with UTD and HWM?
如果某条变更 originatingInvocationId 是 DC-D, 但它 replicate 到 DC-A 的时候被赋予 USN=1120, 然后 DC-B 来向 DC-A 拉变更 ——
A 应该把这个变更发出去吗?还是跳过?
🧠 正确答案:A 会发它,但是否真正“发出去”,要看 B 的 UTD 是否已包含 D 的变更
也就是说:
✅ 变更被“放进 changelog”是按 A 的本地 USN 排的 ❗ 但是否发出去,要看变更 originatingInvocationId = D 是否在 B 的 UTD[D] 之内
📦 再来一步步看:
情况设定:
- 变更来自 DC-D,originatingInvocationId = D_ID, originatingUSN = 5000
- 它在 DC-A 被 apply 后,A 分配了
localUSN = 1120(这个是 A 的 “change log sequence number”)- DC-B 准备从 A 拉数据
- B 的状态:
HWM[A] = 1000 UTD = { D_ID = 4900, // B 从 D 拿到的最大变更是 USN 4900 A_ID = 1150, // B 拿过 A 的本地变更(或间接拿过) }
那么 A 的复制逻辑是:
FOR usn in (1001..currentUSN at A): change = A.changeLog[usn] oid = change.originatingInvocationId // D_ID ousn = change.originatingUSN // 5000 IF oid in B'UTD AND ousn <= B'UTD[oid] THEN SKIP ELSE SEND对比 B 的
UTD[D_ID] = 4900→ 5000 > 4900 → ✅ NOT SKIPPED✅ 所以:A 会把这条变更传给 B!
🎯 核心理解点:
所有变更都按 A 的 USN 排队(用于扫描) 但变更的
originatingInvocationId和originatingUSN决定它是否应该被 replicate 出去
🧠 图解思路:
A 的 changelog: USN | Originating DC | Orig.USN | Object ------+----------------+-----------+-------- 1001 | A | 1001 | O1 1002 | A | 1002 | O2 ... 1120 | D | 5000 | O3 ← 这个来自 D! ... HWM[A] = 1000 UTD = { A_ID = 1150 D_ID = 4900 }你是 A,要从 USN=1001 开始扫 扫到 1120 时:
- 看
originatingInvocationId = D_ID- 看
originatingUSN = 5000 > UTD[D_ID]=4900→ ✅ 需要 replicate!
✅ 小结:关键点口诀
📌 扫描顺序:按来源 DC 的 HWM → 遍历 local USN 📌 判断是否 skip:按变更的 originatingInvocationId + originatingUSN 与接收方的 UTD 比 📌 所以 foreign-originated 变更即使 USN 在 A 很早,也可能需要 replicate!
Q: 假如 DC A 上有如下 changes,格式 (local usn, originiating DC, originating usn),数据 (1000, A, 1000) (1001, B, 20) (1002, C, 100),DC A 和 DC D sync complete 完之后,D 上 UTD[A] 是 1000 还是 1002?
D 上的
UTD[A] = 1000,不是 1002。因为 UTD Vector 的索引和值如下,重点在 originating 而不是 local。微软文档也明确说明,UTD Vector 保存的是“从每个 DC 看到的最高 originating write USN”。
originating DC / Invocation ID → 最高 originating USN因此这三条 change 同步到 D 后:
A 上 local USN Originating DC Originating USN 1000 A 1000 1001 B 20 1002 C 100D 的 UTD Vector 至少会变成:
UTD[A] = 1000 UTD[B] = 20 UTD[C] = 100而 D 针对直接复制伙伴 A 的 High-Watermark 才会是:HWM[A] = 1002。
因为 A 的 local USN 1001、1002 表示:
这些 change 是第 1001、1002 次被写入 A 的本地数据库。
但它们携带的 replication stamp 仍分别是:
(B invocation ID, 20) (C invocation ID, 100)A 不会因为转发了 B、C 的 change,就把它们改写成:
(A, 1001) (A, 1002)否则 originating 信息就丢失了,也无法进行 propagation dampening。
在复制周期的最终响应中,A 返回的
pUpToDateVecSrc描述的是 A 已经拥有的 originating updates;协议要求它包含 A 自己的 cursor,其中的值是“D 可以确认已应用的、由 A 发起的最高 originating USN”,而不是 A 扫描到的最高 local USN。所以这个例子非常清楚地展示了两个数字的区别:
1002 = D 已经扫描完 A 本地数据库到哪里 → HWM 1000 = D 已经知道多少由 A 真正发起的 change → UTD[A]当然,前提是题目列出的就是相关 NC 中的完整状态;如果 A 以前还发起过更高的 originating USN,那么
UTD[A]会保留那个更高值。
Q: 能将 UTD vector 和 HWM 合并只用一个 vector 吗?UTD 会被更新为 local usn 而不是 per originating usn,每次拉 change 就可以从 UTD 开始,这样会有什么问题吗?还能进行 propagation dampening 吗?
可以把字段“合并”,但不能把两种语义真正合并。
因为一条 change 同时存在两个不同坐标:
它在当前复制源 A 的日志位置: (A, local USN = 1002) 它在整个复制系统中的身份: (C, originating USN = 100)前者用于“从 A 的哪里继续扫描”,后者用于“这条 change 我是否已经通过任何路径见过”。一个数无法同时回答这两个问题。
你的设计实际就是把 UTD 变成 HWM
假设 D 同步完 A:
A local USN Origin 1000 A:1000 1001 B:20 1002 C:100你的合并后状态是:
cursor[A] = 1002下次从 A 拉取:
只扫描 A.localUSN > 1002这完全可行,但它本质上就是现在的:
HWM[D ← A] = 1002只要 D 永远从同一个 A 拉,并且 A 的 invocation ID 不变,仅靠这个游标确实可以增量复制。
问题出现在多主、多路径和复制源切换。
1. 换一个复制源后,A 的 local USN 没有意义
假设 D 原来从 A 拉,保存:
cursor[A] = 1002之后拓扑变化,D 改为从 C 拉。C 上可能是:
C local USN Origin 80 C:99 81 A:1000 82 C:100 83 C:101D 实际上已经通过 A 收到:
A:1000 C:100现在只缺:
C:101但是 A 的
1002与 C 的83属于不同 DC 的本地计数器,完全不可比较。如果错误地对 C 使用:
C.localUSN > 1002那么 C 不会返回任何 change,D 会漏掉
C:101。所以你必须改成:
cursor[A] = 1002 cursor[C] = ?也就是为每个直接复制源维护独立游标。这仍然是 HWM,而不是全局 UTD。
2. 新复制源只能从头扫描,大量重复传输
如果 D 从没直接向 C 复制过,那么没有
cursor[C],只能从较低位置开始扫描,例如:C.localUSN > 0C 会扫描并可能返回:
C:99 A:1000 C:100 C:101但前三项 D 已经通过 A 获得了。
仅靠
cursor[A]=1002,C 无法知道 D 已经拥有这些 change,因为:A 的 local USN 1002只在 A 的数据库里有意义。C 不知道:
A.localUSN 1002 对应的是 C:100A 的 local USN 不会作为 change 的全局身份沿复制链传播。
UTD 则允许 D 告诉 C:
UTD[A] = 1000 UTD[C] = 100于是 C 可以在发送前判断:
A:1000 已有 C:99 已有 C:100 已有 C:101 未有最终只发送
C:101。MS-DRSR 的过滤逻辑确实是根据 change 的stamp.uuidOriginating找到对应 UTD cursor,再比较stamp.usnOriginating,而不是比较该 change 在当前复制源上的 local USN。
3. 无法抑制环路中的重复传播
考虑一个环:
A → B → C → AA 产生:
(A, 100)传播过程可能是:
A local 100 → B local 500 → C local 900如果只维护直接复制源的 local cursor:
C 知道它从 B 扫描到了 500但当 C 向 A 复制时,C 无法仅通过这个 cursor 表达:
我这里的 change C.localUSN=900, 实际上就是你 A 自己产生的 A:100。于是它可能把 A 的 change 绕一圈再次发回 A。
保留 originating stamp 后,A 的 UTD 中有:
UTD[A] >= 100所以复制源可以在网络发送前过滤掉这条 change。这就是 UTD 的 propagation dampening 作用。UTD cursor 的正式语义也是:“已经包含 DC x 在 originating USN ≤ y 发起的所有 change”。
仅靠 HWM 不一定会让最终数据错误——版本元数据仍可能让接收方识别旧 change——但会产生大量重复扫描、序列化、网络传输和冲突比较。
4. local USN 不能表达“间接获得的知识”
HWM 表达的是:
D 从直接伙伴 A 扫描到了 A 的哪个日志位置UTD 表达的是:
D 不管通过哪条路径,已经见过每个 originating DC 的哪些 change例如:
D ← A ← BD 从没直接连接过 B,但通过 A 已获得
B:20:HWM[D ← A] = 1002 UTD[B] = 20仅保存
A:1002,其他 DC 无法推导出 D 已经知道B:20。而 UTD 是路径无关的知识描述;微软也将其定义为每个 originating DC 的最高 originating write USN。
5. 要让合并方案重新具备这些能力,只能把 UTD 再造出来
你可能尝试让每条 change 保存完整路径:
Origin: C:100 在 A 上是 local 1002 在 B 上是 local 5000 在 E 上是 local 300 ...这样其他 DC 才能解释各个 local cursor。
但这会造成:
- 元数据随传播路径增长;
- 同一 change 经过不同路径会有不同历史;
- 拓扑改变后映射复杂;
- 本质上仍必须保留稳定的
(origin invocation ID, originating USN)。因此最简单的设计就是 AD 当前的设计:每条 change 只保留一个稳定的 origin stamp,同时为直接复制关系维护一个扫描位置。
两者的准确分工
HWM: (source invocation ID, source local USN) 回答:“下次从这个复制源的哪里继续扫描?” UTD: (origin invocation ID, originating USN) 回答:“无论从哪条路径收到,我已经知道哪些逻辑 change?”在实际请求中,AD 同时携带直接源游标
usnvecFrom和目标端的pUpToDateVecDest,说明它们是两个独立过滤维度,而不是重复信息。所以结论是:
只从固定复制源增量读取时,local-USN cursor 足够;但在 AD 这种多主、可换源、多路径拓扑中,它只能替代 HWM,不能替代 UTD。
若每个源分别保存 local cursor,系统仍可正确运行,但拓扑切换时会重复扫描和重复传输;若把一个源的 local cursor 用于另一个源,则会直接漏 change。
Q: UTD vector 什么时候会被更新?
UTD vector 只会在 cycle 结束 catch up 上之后才会更新,注意不是每次 sync 拉了一次 change 就会 cycle 结束,得 source DC 上所有 change 都被拉到 target => which means
objectUsn == propertyUsn。Details see blog below.
Once a destination DC updates itself from an originating DC – i.e. the replication cycle completes – the source DC sends its UTDV table to the destination DC. The destination DC then updates its UTDV table with the info from the received UTDV table. Each entry in the received table is compared with the one it has and one of the following happens:
- If the received table has an entry that the destination DC’s UTDV table does not have – meaning there’s another DC for this replica that it isn’t aware of, this DC has replicated successfully with the originating DC and so all the info it has is now also present with the destination DC, and so it is as good as saying this new DC has replicated with the destination DC and we are aware of it the same way the originating DC is aware – so a new entry is added to the destination DC’s UTDV table with the name of this unknown DC and the corresponding info from the received UTDV table.
- If the received table has an entry that the destination DC’s UTDV table already has, and its USN value is higher than what the destination DC’s table notes – meaning whatever changes this known DC had for this partition has already replicated with the originating DC and thus the destination DC – and so its entry in the UTDV can actually be updated, the UTDV table for that server is updated with the value from the received UTDV table.
The UTDV table also records timestamps along with the USN value. This way DCs can quickly identify other DCs that are not replicating. These timestamps record the time the DC last replicated with the other DC – either directly or indirectly.
Q: HWM 也只在 cycle 结束后更新吗?还是每一次 sync 都会呢?
从 Sync Engine 的角度看,HWM 可以理解为 cookie 的一部分(虽然表面 opaque 😃),每次 sync DC 都会 return data with cookie,new cookie 中会包含新的 HWM 让你下次带上,所以结论是每次 sync 都会更新而不是只在 cycle 结束。
准确答案是:HWM 的 cookie 在每个成功处理的响应/chunk 后都会更新;UTD Vector 只在整个 replication cycle 完成时更新。
但 HWM 不是一个简单的单一整数,而是
USN_VECTOR/DsReplicaHighWaterMark结构,所以还要区分其中的“临时进度”和“完整 cycle 进度”。微软协议的明确流程
客户端收到每一个
IDL_DRSGetNCChanges响应后,会处理其中的对象和链接更新,然后执行UpdateRepsFrom(...)只有随后满足:
ulResult == 0 并且 fMoreData == false才会执行
UpdateUTDandPAS(...)微软的
ProcessGetNCChangesReply伪代码明确如此:UpdateRepsFrom(...) if success and fMoreData = false: UpdateUTDandPAS(...)因此两者的更新时机不同:
HWM / repsFrom.usnVec: 每个成功应用的 response 后更新 UTD Vector: 仅最后一个 response,也就是 cycle 完成后更新
UpdateRepsFrom本身明确把当前响应返回的usnvecTo写入该直接复制伙伴对应的repsFrom。这里没有要求fMoreData=false;只要求当前响应成功应用。rf.usnVec := msgReplyNative.usnvecTo而
repsFrom.usnVec的定义也是「保存最近一次发送给该源 DC 的IDL_DRSGetNCChanges请求所对应响应里的usnvecTo」,不是“最近一次完整 cycle 的最终响应”。一个三页 replication cycle
假设 B 从 A 拉取,初始 HWM 是:
HWM = 1000第一包返回:
usnvecTo = 1050 fMoreData = trueB 成功应用后:
repsFrom[A].usnVec = 1050第二个请求带:
usnvecFrom = 1050第二包返回:
usnvecTo = 1100 fMoreData = trueB 成功应用后:
repsFrom[A].usnVec = 1100第三包返回:
usnvecTo = 1150 fMoreData = false pUpToDateVecSrc = ...B 成功应用后:
repsFrom[A].usnVec = 1150 UTD_B = merge(pUpToDateVecSrc)所以时间线是:
第一包成功: HWM 更新,UTD 不更新 第二包成功: HWM 更新,UTD 不更新 最后一包成功: HWM 更新,UTD 更新微软的
GetResponseSubset也明确说明,每个响应都会产生一个新的msgOut.usnvecTo,供客户端在下一个请求中作为msgIn.usnvecFrom使用。Samba 代码也是每个 chunk 更新请求 HWM
Samba 客户端的复制循环是:
while True: level, ctr = get_nc_changes(...) process_chunk(...) if ctr.more_data == 0: break # update the request's HWM so we get the next chunk drs_copy_highwater_mark(req.highwatermark, ctr.new_highwatermark)也就是说,只要还有下一包,Samba 就把本包返回的
new_highwatermark复制进下一次请求。因此至少在“同一个 cycle 内下一次 RPC 使用什么 HWM”这一点上,Samba 非常明确:
每个 chunk 都推进 HWM,不等最后一个 chunk。
但 HWM 结构内部还有两个不同进度
Samba 将 HWM 定义为:
typedef struct { hyper tmp_highest_usn; /* updated after each object update */ hyper reserved_usn; hyper highest_usn; /* updated after a full replication cycle */ } drsuapi_DsReplicaHighWaterMark;这意味着不能简单说:
HWM 这个整数每一页更新。
更准确地说:
- 整个 HWM cookie 每个响应都会被返回、保存并带入下一次请求;
tmp_highest_usn可以表达 cycle 内部的中间进度;highest_usn在 Samba 的定义中表示完整 replication cycle 的稳定边界。微软的
USN_VECTOR对应包含三个字段:USN usnHighObjUpdate; USN usnReserved; USN usnHighPropUpdate;但微软明确说明,出于互操作性考虑,只有结构大小和全零表示是标准化的;具体字段语义属于服务器实现的 cookie 内部细节。
因此,跨实现最安全的说法是:
HWM cookie 每个成功 response 都更新(笔者注:maybe object usn);其中用于表示完整 cycle 边界的内部值(笔者注:maybe property usn),可以只在 cycle 完成时推进。
为什么中途就更新 HWM 是安全的
因为 B 只有在成功应用本包更新后,才调用
UpdateRepsFrom。假设:
第一包: 1001~1050已经成功写入 B,那么 B 可以安全保存当前 cookie。即使随后第二包失败,下次也可以从该 cookie 表示的状态继续,而不必重做第一包。
反过来,如果第一包应用失败:
ulResult != 0微软的
UpdateRepsFrom不会把rf.usnVec更新为响应中的usnvecTo,而只增加失败计数并记录错误。这保证了:
只有已成功应用的数据 才能推动持久化 HWM为什么 UTD 必须等 cycle 结束
UTD 是更强的知识声明,例如
UTD[A] = 1150表示目标已经拥有 A originating 到 1150 的完整前缀。如果一个 cycle 有三包,在只处理完第一包时就把 UTD 推到最终目标,那么第二包失败后,B 会错误地宣称自己已经拥有尚未收到的更新,未来这些更新可能被源端过滤掉。
所以微软明确只在
fMoreData = false时更新 UTD。最终结论
状态 每个成功 chunk 完整 cycle 结束 下一请求使用的 HWM cookie 更新 更新 repsFrom.usnVec更新 更新 HWM 内部临时进度 可推进 推进 HWM 内部稳定 cycle 边界 不一定推进 推进 UTD Vector 不更新 更新 因此最准确的一句话是:
AD 在每个成功应用的
GetNCChanges响应后更新直接伙伴的 HWM/cookie,以支持分页和断点续传;只有在最后一包fMoreData=false后,才更新 UTD Vector。
Q: Replicate 的时候是按照 Object USN 还是 Property USN?
DirSync 的时候 Cookie 中包含 object usn 和 property usn, object usn 会是当前对象所有 property usn 中最大的一个(最后一次被更改),replicate 的时候会按照 object usn 排序,所以很可能两个 object,更早被改过的 property 更晚被 replicate。比如对下面的 case,假设当前 HWM 是 400(上一次 sync 完 object/property usn = 400),可能此次 sync 的只有 object B,而 object A 的 property1 会等到下一次 sync 500 的时候才跟随 property2 被 replicate 出去。
Object A (500): property1: 420, property2: 500
Object B (450): property1: 400, property2: 450
所以 more data 的判断条件是 object usn == property usn,所有的 change 都已经被 sync,dirsync catch up with latest changes。
这轮 sync 拉完 object B 之后,cookie 更新 object usn 为 450,但 property usn 还是 400 不变,等到下一次 sync object A 之后,不再有更新的 change,object usn = property usn = 500。
另外,注要注意的是,Dirsync 在不同 DC 之间 failover 时,比如从 DC1 failover 到 DC2,DC2 上的 AD 会根据 cookie(from DC1)的 UTD vector 数据获取当前 DC2 对应的 usn(同一水位线,DC1/2 在此水位线上已经互相完全同步),并从这个 usn 开始 sync。所以如果 DirSync 没有 catch up 最新的数据(object usn != property usn,UTD vector 没有被更新),failover 之后就可能会拿到重复的已经做过的 change,但这是 by design,AD 为了保证不丢 change。完整文档可参见 如何跟踪 AD 的 change。
Q: DC failover 的时候是怎么对应水位线的?
HWM 是 per source DC 的,所以 failover 了就没用了。UTD vector 表达的是对整体的已知,所以会以 UTD[newDC] 作为新的起点拉 changes,然后一样使用 UTD 来过滤减少发送已知项。本质来说,它不是在对应水位线,它只是一种对每台 originating DC 的已知状态。
这里有一个关键纠正:
DirSync failover 到 DC2 时,DC2 不会根据 cookie 的 UTD vector,反查出某个“等价的 DC2 local USN”,然后从该 local USN 继续。
真实机制是:
- cookie 内同时包含:
- 生成 cookie 的 DC invocation ID;
- 该 DC 专用的 local-USN 游标
usnVector;- 与 DC 无关、按 originating DC 划分的 UTD vector。
- DC2 发现 cookie 的 source invocation ID 属于 DC1,不是自己:
- 丢弃 DC1 的 local-USN 游标;
- 保留 cookie 的 UTD vector。
- DC2 用 UTD vector 对自己数据库中的 attribute replication stamp 逐项过滤。
微软协议明确规定:只有 cookie 的
uuidSourceDsaInvocationId等于当前 DC 的 invocation ID 时,才把 cookie 的usnVector放入usnvecFrom;UTD vector 则无论是否换 DC 都会复制到pUpToDateVecDest。“水位线”实际上是一个向量边界
你可以把它画成:
已处理水位 I1 ─────────────── 100 I2 ─────────────── 20 I3 ─────────────── 7对 DC2 上的任意 change:
I1:99 位于水位线下 I2:20 位于水位线上 I3:8 位于水位线上方 I1:101 位于水位线上方所以 DC2 “找到对应水位线”的准确含义是:
DC2 使用每条 change 自带的 originating stamp,把它放到 cookie 的向量坐标系中判断前后。
而不是:
先找出某一个 DC2 local USN,例如 5005,再执行
uSNChanged > 5005。为什么不能可靠地找出单个 DC2 local USN?因此向量边界通常无法压缩成新 DC 上的一个标量 local USN。
local USN 在 failover 后仍有什么作用
第一次在 DC2 使用 DC1 cookie 时:
DC1 local cursor → 不可复用 UTD vector → 可以复用DC2 生成下一份 cookie 后,它会写入:
sourceInvocationID = I2 usnVector = DC2 自己的游标 UTD vector = 新的向量状态从下一次开始,只要客户端继续连接 DC2:
cookie.sourceInvocationID == DC2.invocationIDDC2 就可以同时使用:
- DC2 local-USN cookie,继续当前 DC2 上的分页/扫描状态;
- UTD vector,过滤已知的 originating updates。
协议把这两个阶段明确分开:UTD 决定哪些 replication stamps 属于未见 change,
usnvecFrom/usnvecTo则维护当前服务器上的响应子集和后续 cookie 状态。最终结论
你的描述可以修正为:
DirSync 从 DC1 failover 到 DC2 时,DC2 不会找到与 DC1 HWM 完全等价的 DC2 HWM,也无法将 DC1 cookie 的 UTD vector 换算成一个 DC2 local USN。DC1 的 local-USN cursor 因 invocation ID 不匹配而失效;DC2 保留 UTD vector,并按照每条 change 的
(originating invocation ID, originating USN)判断它位于已知水位线之下还是之上。完成这一轮后,再生成一份包含 DC2 local-USN cursor 的新 cookie。这也是为什么微软说跨 DC 使用 cookie 不会漏 change,但可能返回重复 change,极端情况下甚至接近一次全量同步。
Q: 为什么 UTD vector 的更新一定要在 cycle 结束 object usn == property usn 的时候?因为 DC apply changes 的时候为了加快速度本地乱序的吗?
假设这些更新都最初由 A 产生:
A:91 A:92 ... A:100它们后来被 S 接收,S 上可能形成:
S local USN originating stamp 5001 A:91 5002 A:100 5003 A:92 5004 A:93 ... 5010 A:99这两套顺序互相独立。
为什么 S 上 originating USN 可能乱序
不是因为 AD 为了性能刻意“并行乱序 apply”。我没有找到微软资料证明这是某种性能优化,不应该这样猜。
准确原因是:
DRS 协议不要求 response 按 originating DC 的 originating USN 顺序组成。
微软的
GetResponseSubset只要求服务器从 changed objects/links 中选择一个子集,并用 cookie 保证整个 cycle 最终覆盖全部变化;同一对象甚至可以在同一响应或不同响应中重复出现。协议没有规定必须按某个 origin 的 USN 顺序发送。例如某个中转 DC X 可能分包发送:
第 1 包: A:91 A:100 第 2 包: A:92~A:99S 按收到并成功应用的顺序分配 local USN,于是就可能形成:
5001 → A:91 5002 → A:100 5003 → A:92 ...所以 originating sequence 中有洞,不代表 S-local sequence 中有洞。
Samba 确实按 source-local USN 排序
Samba 的服务端实现不是随便取本包最大 local USN。
它会:
- 根据 source 上的
uSNChanged收集候选对象;- 按
uSNChanged排序;- 按顺序分页发送;
- 使用保存的
num_processed等状态继续下一页。Samba 的比较函数明确是:
/* sort the objects we send first by uSNChanged */ if (m1->usn < m2->usn) { return -1; }而临时 HWM 的推进逻辑是:
if (uSN <= hwm->tmp_highest_usn) { return; } hwm->tmp_highest_usn = uSN;也就是按照已处理的 source-local 位置推进。
因此在 Samba 正常路径下,如果存在:
S-local 5001 S-local 5002 ... S-local 5010第一包不能发送 5001 和 5010, 却把简单 HWM 推进到 5010, 再把 5002~5009 留到后面。
Windows 协议层更宽松:HWM 是 opaque cookie
微软并未标准化
USN_VECTOR三个字段的具体内部含义,只标准化了:
- 总大小是 24 字节;
- 全零值的表示。
微软明确把它定义为跨
IDL_DRSGetNCChanges调用传递状态的 cookie。所以 Windows 实现即使不严格按 local USN 排序,也可以让 cookie 表示:
已经发送:91、100 仍待发送:92~99而不是简单表达:
所有 local USN ≤ 100 都已发送
GetResponseSubset的规范只要求服务器通过usnvecFrom/usnvecTo中维护的状态,确定整个 changed set 已经发送、完整 cycle 最终覆盖全部changedObjs和changedLinks后,才返回fMoreData=false。但这只能说明“协议允许更复杂的实现”,不能证明 Windows 实际会乱序发送。Samba 的实现则明确选择了 source-local USN 排序。
也就是说,只有以下两种方式之一才安全:
方式一: 严格按 source-local USN 前缀发送, HWM 可以是简单的连续位置。 方式二: 允许非顺序发送, 但 HWM 必须是能记录待处理集合的 opaque cookie。不能:
乱序发送 + 直接把本包最大的 local USN 当成完整前缀(笔者注:理论是这样,但是这个 opaque cookie 就很不 opaque 啊,它规定了总大小 3*8=24 字节,其实就是把字段 + 含义都规定好了。微软内部实现其实也是按照 local usn 顺序取,只是 source DC 打包 / target DC 解包并 apply changes 的时候可能会不按顺序。)
那为什么 UTD 仍然只在 cycle 结束时更新?
正确原因不需要假设同一 origin 乱序。
因为最终返回的 UTD Vector 是一个整个 cycle 的知识目标,不是当前 page 内容的摘要。
假设 S 在 cycle 开始时的 UTD 是:
S.UTD: A → 100 C → 200S 的 local 流严格有序:
S-local 5001 → A:91 S-local 5002 → A:92 ... S-local 5010 → A:100 S-local 5011 → C:191 S-local 5012 → C:192 ... S-local 5020 → C:200分页也完全有序:
第一页:S-local 5001~5010 第二页:S-local 5011~5020B 处理完第一页后,确实已经获得
A → 100,但是尚未获得C:191~C:200,如果 B 在第一页后直接把 S 的整个 UTD Vector 合并进来:B.UTD: A → 100 C → 200那么它会错误宣称已经拥有
C:200的知识。如果第二页随后失败,B 下一次把
UTD[C] = 200发给其他源,其他源就会把C:191~C:200当成 B 已知的更新过滤掉。整个例子中:
- S-local 顺序完全连续;
- A-origin 顺序完全连续;
- C-origin 顺序也完全连续;
- 问题只是第一页尚未完成整个 vector 所代表的 cycle goal。
微软对 cycle goal 的定义正是:cycle 最终结束时,客户端应拥有服务端在 cycle 开始时已有的全部更新;最终响应里的
pUpToDateVecSrc必须至少覆盖 cycle 开始时源的整个 UTD knowledge。客户端处理规范也明确:
每个成功 response: UpdateRepsFrom() // 更新 HWM/cookie 只有 fMoreData == false: UpdateUTDandPAS() // 合并 UTD为什么不能每页只更新“本页完成的 UTD entry”
理论上可以设计一种协议,让每个响应额外携带:
这一页完成后,可以安全断言: A → 100 C → 190 D → 50也就是一个 page-safe UTD Vector。
但当前 DRS 协议没有把
pUpToDateVecSrc定义成这种逐页确认信息。它描述的是源已经拥有的更新知识,而最终 cycle goal 才保证这些知识已经被客户端完整获得。响应结构本身也把pUpToDateVecSrc描述成源已应用更新的 stamp filter,而不是“本页已经完整传给客户端的更新”。仅查看这一页出现的最大 originating USN 也不够,因为客户端不知道:
- 此 origin 是否还有内容在后续页;
- 某些编号是否是本 NC 的真实空洞;
- 后续对象或链接值是否还携带更低但尚未传输的 stamp;
- 服务端的 changed set 是否在 cycle 中扩展。
要逐页安全更新 UTD,服务端必须为每个 origin 显式给出一个“已完整交付的连续前缀”,相当于额外维护一个 per-page vector acknowledgement。AD 没有采用这个设计。
HWM 为什么可以逐页更新
HWM/cookie 作出的声明更弱:
我已经成功处理了当前源在这个复制 cycle 中交付到这个位置的内容;下次请根据这个 cookie 继续。
它没有声明:
我已经获得源所知道的所有 A、C、D updates。
它只影响当前 source 而不会影响其他 source。
最终区别
同一个协议在 A→S 和 S→B 上完全一致:
每条复制边: 按该 source 的本地枚举状态分页 每页成功后推进 HWM/cookie 整个 cycle 结束后合并 UTD两者更新时机不同,不是因为一段有序、另一段无序,而是因为声明强度不同:
HWM: 当前直接源的分页进度。 UTD: 对于所有 originating DC 的完整知识断言。所以正确结论是:
UTD 等到 cycle 结束才更新的真正原因,是源的整个 UTD Vector 代表完整 cycle goal;在任意中间页,客户端尚未获得这个 vector 所覆盖的全部来源更新,即使每条 source-local 流都严格有序。
Q: 假设 replication chain 是 A 到 B 到 C,B sync 给 C 的时候有 A 的 source invocation id 和 usn吗?如果没有的话 C 收到之后怎么同时更新对 A/B 的 UTD Vector?
change 的 metadata 里面总是有 source 相关的信息,所以 C 会收到 A 相关的信息。最后 C 更新 UTD vector 的时机是 C cycle 结束,收到 B 的 UTD vector 后做 merge。B 传给 C 的 UTD vector 里面关于 A 的部分已经在 A sync to B 的时候被更新,所以 C 能接收到 A 的最新值。
Q: UTD 被更新时只会更新 originating invocationId 那一行吗?还是说 sync source 也会一起更新?
如上所说,是根据 UTD vector 做 merge,实际被更新的 records 并不确定。
Q: UTD vector 中已有的 entry 会被删除吗?比如 DC demote/rebuild 之后,相关 invocationId 都应该不再有意义?
不会,会一直保留。目前 vector size 可能有 1k+,可是我们实际只有 24 台 DC,目前没发现什么问题,毕竟只是一个 vector。
Q: 如果 B 的 UTD[A] = 1150,HWM[A] = 1000,实际 A 在处理的时候就是 1150 以下的都会被认为已经 sync 过,返回的都是 1151-1200 吗?HWM 仍然实际没起作用呀?
因为 UTD vector 只会在 replication cycle 结束后更新,所以 UTD[A] = 1150 意味着 B 已经知道了 A 上
local usn <= 1150的所有 changes,1000 ~ 1150 的部分在复制时会被跳过、不会被 A replicate 出去。但不确定的是 A 在处理时是 1. 以 HWM 为 baseline 从 1001 开始比,然后在结果集上根据 UTD 筛除,还是 2. 先计算
max(HWM, UTD[source])做 baseline 再枚举。A 实际是否计算
max(HWM, UTD[A])对 Windows:公开资料不足以证明物理查询顺序
微软 OpenSpec 描述的是必须满足的协议语义:
- UTD 是 stamp filter;
usnvecFrom是 cycle cookie;- 最终响应必须满足 cycle goal。
它没有公开 Windows NTDS 内部究竟是怎么实现的。
对 Samba:确实明确取最大值
Samba 的 AD-compatible DRS 服务端源码明确写道:
getnc_state->min_usn = req10->highwatermark.highest_usn; for each cursor in UTD: if cursor.source == current_source_invocation_id: if cursor.highest_usn > min_usn: min_usn = cursor.highest_usn;也就是:
min_usn = max(HWM, UTD[current source])随后 Samba 会先过滤:
object.uSNChanged <= min_usn → 整个对象不用考虑对于留下来的对象,再逐属性根据:
(originating invocation ID, originating USN)与完整 UTD Vector 比较。链接值也先比较 local USN,再应用 UTD filter。
所以对于 Samba,你给出的两个选项中,正确的是 选项 2:
baseline = max(HWM, UTD[A])为什么跳过 A-local 1001~1150 不会漏掉其他 origin
对于正常、完整、同一 NC、同一 invocation ID 的复制周期,微软的 cycle goal 要求:
周期结束时,客户端必须包含服务端在周期开始时拥有的全部更新。
而最终返回的 UTD 必须覆盖源端周期开始时的整个 UTD 知识,不只是当前源自己的 entry。
假设 A 的顺序是:
A-local 1001 → C:400 A-local 1150 → A:1150B 若通过
A → X → B的完整复制链获得了UTD_B[A]=1150,那么:
- X 获得
A:1150的完整 cycle 同时必须获得或已经拥有 A 当时知道的C:400;- B 从 X 完成 cycle 时,也必须获得或已经拥有 X 的这部分知识;
- B 的整个 UTD 应当同时覆盖对应的
Ccursor。否则 X 或 B 就不能安全地在最终 UTD 中断言其已经达到相应的 cycle goal。
因此,不能只孤立地看
UTD[A]这个 scalar。它存在于一个由完整复制周期建立的、具有因果闭包性质的 UTD Vector 中。这也是为什么兼容实现能够安全地把UTD[A]当成扫描 A-local 流的下界。那 HWM 到底还有什么用
即使实现使用
max(HWM, UTD[source]),HWM 仍然有独立作用。直接伙伴专属状态
HWM 绑定:
destination source invocation ID NCUTD 则是这个 NC 的全局 originating knowledge,来源可以直接也可以间接,而 HWM 只会直接。
分页和中断恢复
一个复制周期可能有多页:
request 1 → fMoreData=true request 2 → fMoreData=true request 3 → fMoreData=falseHWM 是每页间继续的 cookie;UTD 不是这种分页 cookie。微软明确规定
GetResponseSubset使用 HWM /usnvecFrom判断已经发送了哪些 changed objects/links。当 HWM 更大时提供更好的枚举起点
如果:
HWM = 1200 UTD[A] = 1150则使用 1200,避免重新检查 1151~1200。
当 UTD[A] 更大时利用跨路径知识
如果:
HWM = 1000 UTD[A] = 1150则使用 1150,避免因为直接伙伴 HWM 落后而重复扫描。
所以它们不是二选一,而是:
HWM: 直接路径已处理到哪里 UTD[source]: 通过所有路径,至少可以安全认为源的知识前缀到了哪里 完整 UTD: 候选属性/链接是否已经通过任意路径见过HWM 和 UTD 大小比较
三种情况都合法:
HWM > UTD[A] 直接从 A 的枚举进度更靠前。 HWM < UTD[A] B 经由其他路径获得了较新的 A 知识。 HWM = UTD[A] 两者刚好收敛到同一安全边界。最终最稳妥的表述是:
微软协议规定 HWM/cookie 和 UTD stamp filter 是两个独立状态,但没有公开 Windows NTDS 的物理查询计划。公开的 Samba AD-compatible 实现会使用
max(HWM, UTD[source])作为 local-USN 候选枚举下界,然后再用完整 UTD Vector 逐属性、逐链接过滤。HWM 仍负责直接伙伴复制周期的状态、分页和续传,而 UTD 提供跨路径的知识与去重。
Q: AD 演进历史?UTD vector 是后来增加的 improvement 吗?
Windows 2000 Server 中的第一版 Active Directory 就已经同时具有:
- High-Watermark(HWM,高水位标记)
- Up-to-Dateness Vector(UTD Vector,最新度向量)
它们不是后来 Windows Server 2003 才加入的,而是 AD 最初多主复制模型的基本组成部分。两者从一开始就分工不同,HWM 针对直接复制源,UTD Vector 针对所有原始写入者。
- HWM 找出 A 上哪些记录值得检查;
- UTD Vector 判断这些记录的原始变更是否已经通过其他路径收到。
这就是 AD 的 propagation dampening:既支持任意拓扑、多跳复制,又避免同一原始更新在环形拓扑中不断重复传播。
版本 关键机制 Windows 2000 / AD v1 Multi-master、invocationId、originating USN、HWM、UTD Vector V1、属性级复制元数据与冲突解决 Windows Server 2003 请求/响应协议扩展、UTD Vector V2 (增加字段 timeLastSyncSuccess用于复制延迟报告)、Linked-Value Replication、复制与拓扑等方面的增强后续版本 RODC、AD Recycle Bin、虚拟化安全机制、额外复制元数据和协议扩展等
Q: 假如不用 UTD vector 只用 HWM,AD 能 work 吗?
能 work,但会变成“可以收敛、效率很差”的多主复制系统。
UTD Vector 不是 AD 正确性的唯一基础;它主要负责 跨路径去重(propagation dampening)。真正保证并发修改能够确定性收敛的,是每个属性携带的复制元数据,例如版本号、originating DC、originating USN 和时间戳。微软的复制模型明确把这些信息组成 attribute stamp;属性的 originating 修改会增加版本号,而单纯复制不会改变 originating version。
只有 HWM,没有 UTD 会发生什么
假设有三台 DC:
A ─── B \ / CA 产生更新:
X = { version: 1, origin: A, originatingUSN: 100 }第一条路径
A → BB 应用更新,并给它分配自己的 local USN:
B local USN = 500 origin 仍然是 A:100 version 仍然是 1第二条路径
A → CC 也应用同一个更新:
C local USN = 700 origin 仍然是 A:100然后 C 再从 B 拉取。
由于 C 对 B 的 HWM 只知道:
我从 B 读到了 local USN 499B 上的这个对象现在是 local USN 500,所以 仅靠 HWM,B 会再次把 X 发给 C。
C 收到后比较复制元数据:
本地:origin=A, originatingUSN=100, version=1 收到:origin=A, originatingUSN=100, version=1发现是同一个更新,于是忽略。
所以:
没有 UTD: B 会发送重复数据 C 收到后才发现重复并丢弃 有 UTD: C 提前告诉 B:“A:100 我已经见过了” B 根本不发送微软对两者的分工描述也是:
- HWM 跟踪从某个直接复制伙伴收到的位置;
- UTD Vector 跟踪来自所有 originating DC 的最高 originating USN;
- 目标 DC 把 UTD Vector 发给源 DC,源 DC 据此缩小需要发送的属性集合。
为什么不会无限循环
考虑环形拓扑:
A → B → C → AA 创建更新后:
A 创建 X A → B:B 应用 B → C:C 应用 C → A:A 已经有完全相同的 originating stamp,忽略只要“相同或更旧的复制元数据”不会被再次作为新修改应用,它就不会产生新的有效更新,也不会永远循环。
因此,没有 UTD 时,更新大致会:
沿拓扑中的很多边传播,直到到达一个已经拥有相同或更新版本的 DC,然后被丢弃。
UTD 的作用是把“收到后丢弃”提前成“发送前过滤”。
换句话说:
UTD 不负责判断谁赢 UTD 负责判断是否值得发送 复制元数据负责判断: 收到的属性更新是否比本地更新这也是为什么微软把 UTD 和 HWM称为互补过滤机制:HWM 排除直接伙伴游标之前的对象,UTD 再根据更新的真正来源过滤属性和对象。
初始复制本身就证明“没有 UTD 也能复制”
微软的 DRS 协议在构造复制请求时明确规定:
if (ObjExists(nc)) then // 如果目标上已经存在该 NC,发送该 NC 的 replUpToDateVector msgIn.pUpToDateVecDest := ConcreteUTDFromAbstractUTD(nc!replUpToDateVector) else msgIn.pUpToDateVecDest := null endif也就是说,在目标 DC 尚未拥有该目录分区、执行初始复制时,UTD Vector 可以为空;复制仍然通过 HWM 为零、全量枚举和复制元数据正常完成。
不过这只能证明:
单次复制协议可以在 UTD 为空的情况下工作。
它不代表长期运行时不维护 UTD 是一个合理设计。
没有 UTD 的代价
在树形拓扑中:
A → B → C每个更新基本只有一条传播路径,UTD 的价值相对较小。
但在 AD 这种具有冗余路径的拓扑里:
B / \ A─────C \ / D同一个 originating update 可能通过多条路径到达同一 DC。没有 UTD,就会产生:
- 更多网络传输;
- 更多对象和属性序列化;
- 更多数据库读取;
- 更多元数据比较;
- 更高的 CPU 和磁盘 I/O;
- 更长的复制周期;
- 大规模 forest 中可能出现严重的重复传播放大。
微软将 UTD 消除多路径冗余的能力称为 propagation dampening。
最终结论
可以把 AD 的复制正确性拆成三层:
HWM 解决:从这个直接伙伴的本地日志读到哪里 UTD Vector 解决:这条 originating update 是否已经通过其他路径见过 Attribute/Link replication metadata 解决:收到更新后,应该接受、忽略还是进行冲突裁决所以,没有 UTD Vector,AD 风格的多主复制理论上仍能完成传播和最终收敛,前提是保留 originating metadata 和确定性的冲突解决机制。
但是,它会失去跨路径的发送前去重,同一更新可能沿大量复制路径重复传输,再由接收端丢弃。
因此 UTD Vector 对“能不能实现多主复制”不是绝对必要条件,但对 AD 这种存在环路、冗余路径和大量 DC 的实用多主复制系统,几乎是不可缺少的效率机制。
Q: 那能不能只用 UTD vector 不用 HWM 呢?
AD 为什么需要 HWM
AD 更自然的增量枚举方式是根据当前源 DC 的本地变更顺序:
S local USN: 5001 → 从 A 收到的更新 5002 → S 本地产生的更新 5003 → 从 B 收到的更新 5004 → 从 C 收到的更新目标 D 对 S 保存:
HWM(S) = 5000下次只需要扫描:
S.localUSN > 5000立即得到四个候选更新。
然后再用 UTD 过滤:
候选 5001,origin=A:100 D.UTD[A] = 100 → 已见过,过滤 候选 5002,origin=S:700 D.UTD[S] = 699 → 没见过,发送 候选 5003,origin=B:300 D.UTD[B] = 300 → 已见过,过滤 候选 5004,origin=C:900 D.UTD[C] = 850 → 没见过,发送两层过滤是:
HWM: 在源 DC 的本地有序空间里,快速找候选集 UTD: 根据更新的真正来源,剔除已经通过其他路径收到的更新微软也将两者描述为互补状态:
- HWM 跟踪从某个特定源、特定分区收到的最近变化 => 快速找到候选更新;
- UTD 跟踪来自所有 originating DC 的更新,用来过滤要发送的内容 => 从候选更新中排除目标已知的更新。
如果没有 HWM,最大问题是如何找出“可能需要发送的更新”。
只用 UTD 的三种实现
1. 每次全库扫描
源 DC 扫描所有对象、属性和链接值:
for each attribute: stamp = (originDC, originatingUSN) if stamp.usn > destinationUTD[stamp.originDC]: send(attribute)这是正确、可行的,但每次复制可能都需要扫描整个 NC。
微软协议的抽象
GetChangesInScope确实是按这种逻辑描述 UTD 过滤的:检查 scope 中对象的属性 stamp 和链接 stamp,并与目标 UTD 比较。问题是效率可能接近:
复制成本 ≈ 整个目录大小 O(对象数 × 平均属性数 + 链接值数)即使一个更新都没有,也可能仍需检查大量状态。
HWM 的作用正是将比如一千万个对象缩小成上次复制后 localUSN 发生变化的 100 个对象:
复制成本 ≈ 最近发生的变化量2. 为每个 origin 建索引或日志,但结构会复杂很多
维护如下索引,然后分别按照 UTD 的每个 cursor 查询。这相当于把一个简单的源本地日志,改造成一组分布式来源日志。
(originInvocationID, originatingUSN) → object / attribute / link可以工作,但代价是:
每个 originating invocation 都需要可查询状态;
需要合并很多 origin stream;
要处理 retired invocation ID;
要保存删除、链接删除等足够长时间;
索引和垃圾回收更复杂;
每次请求可能要查询大量 origin。
分页更加复杂。使用单一源本地顺序时很容易:
本次发送 localUSN 5001..5100 下一次从 5100 继续仅 UTD 时,你可能同时在读取:
A:1001..1100 B:801..850 C:2501..2600如果本次响应只能发送其中一部分,就必须记录:
A 已枚举到哪里 B 已枚举到哪里 C 已枚举到哪里 ...或者服务器保存一个不透明的快照与归并游标。这实际上会重新产生一个“枚举进度结构”。
举例:
A:1001 → User1.description A:1002 → User2.displayName B:801 → Group1.member + User3 C:2501 → User4.isDeleted收到目标 UTD:
A → 1000 B → 800 C → 2500分别执行:
查询 A 的 originUSN > 1000 查询 B 的 originUSN > 800 查询 C 的 originUSN > 2500然后把所有结果合并。
这在理论上没问题,但你把一个查询:
S.localUSN > HWM变成了:
对每个历史 invocationId 查询 + 对所有结果归并 + 进行分页 + 保证不能漏掉任何 origin 序列如果一个 NC 中存在
D个 originating invocation ID,通常至少需要:O(D) 次范围定位 + O(K log D) 的多路归并其中
K是本轮候选更新数。而 HWM 方案大致是:
一次 localUSN 范围扫描 + O(K) 的 UTD lookupUTD 本身记录的是每个 originating source 已知的最高 USN,因此它的规模随出现过的来源 DC 或 invocation ID 增长。
3. 使用 Merkle tree 或状态摘要
也可以完全不用 USN HWM,例如 Cassandra/Dynamo 风格的 anti-entropy:
比较分区摘要 → 找到不同范围 → 比较对象版本 → 修复差异这也能工作,但已经不是 AD 当前这种:
local-USN incremental enumeration + originating-USN deduplication的设计了。
网络负载不一定更高,但也不会明显更低
理想实现
如果源 DC 有高效的
(origin, originatingUSN)索引,并且在发送前完成 UTD 过滤,那么实际发送的对象和属性可以与 AD 方案近似相同:网络 payload:基本相同删除 HWM 只省掉一个很小的 partner-specific cookie;UTD Vector 仍然需要随请求传输。AD 的请求本来就同时携带
usnvecFrom和pUpToDateVecDest。所以它几乎不会带来有意义的网络节省。
简化实现
如果源无法高效按 UTD 查询,只能:
先发送大量候选 目标接收后再过滤那么网络负载会明显增加。
全库服务器端扫描
如果源全库扫描,但在发送前过滤:
网络负载可能不增加 服务器 CPU / 磁盘 I/O 会大幅增加因此只用 UTD 的主要问题通常首先是 源端计算和存储访问成本,不一定首先表现为网络流量。
HWM 的结构优势
比较一下复制状态的规模。
只有 HWM:
每条直接复制关系、每个 NC: 一个 source-local cursor例如:
D 从 S 复制: HWM = 5000源只需执行一次连续范围查询:
localUSN > 5000只有 UTD + per-origin log:
A → 100 B → 200 C → 300 D → 400 ... 几百或几千个 invocation ID源可能需要对每个 origin 查询并合并结果。
因此 HWM 把:很多个来源有序流 映射为源 DC 上的 一个本地有序流,这是它最重要的价值,大幅简化了范围查询、批处理、限流、断点续传和错误恢复。
结论
准确来说:
只用 UTD Vector、不使用 HWM,可以设计出正确的复制系统。
但必须选择以下至少一种方式:
每次扫描所有当前状态;
优点: 实现概念简单 不需要 HWM 缺点: 每轮 CPU、磁盘 I/O 接近全库规模 空闲复制也昂贵维护按
(origin DC, originating USN)排序的索引或日志;优点: 能做到真正增量 网络 payload 可与 AD 接近 缺点: 额外索引与写放大 多路查询和归并 分页状态复杂 历史 invocation 管理复杂 连续前缀和故障恢复困难使用其他 anti-entropy 数据结构。
最简洁的区别是:
HWM 是 enumeration cursor: 从源的变化流哪里继续读? UTD 是 knowledge vector: 目标已经知道哪些原始更新?UTD 可以替代“过滤逻辑”,但不能在不改变存储和索引设计的情况下替代 HWM 的“候选枚举逻辑”。
因此 AD 同时使用两者的原因不是“缺一不可才能正确”,而是两者正好解决两个正交问题:
HWM: 用源 DC 的单一 local-USN 顺序, 低成本枚举上次以后发生的候选变化。 UTD: 用 originating DC 的知识向量, 过滤目标已经通过任何路径获得的变化。HWM 使复制成本主要与“最近变化量”相关;UTD 使网络成本主要与“真正未知的变化量”相关。 同时使用,才能既避免全库扫描,又避免多路径重复传播。
Q: 只用 UTD vector 不用 HWM 的情况下,为什么一定需要按 NC 扫描?UTD vector 里面不是有源自己吗?不能按照 UTD vector[sourceDC] 的值开始复制吗?
可以,因为 UTD Vector 只在完整 cycle 结束时才安全推进,所以在相同 invocation ID、同一 NC 和相同复制范围下,
UTD[S]可以作为扫描 S local-USN 流的一个保守下界。
S:700是 S 在本地 USN 700 时产生的 originating update。在它之前,S 已经处理了:S-local 699 → A:51因此可以把
S:700看成某种“因果栅栏”:A:51 happens-before S:700AD 不允许一个完整复制周期仅传播这个后继知识,却让目标缺少仍然必要的前驱知识。
这不是单靠
UTD[S]=700这个数学字段的定义实现的,而是因为复制周期结束时,源返回的是一个 cycle goal:该 vector 必须只断言那些源能够保证目标已经应用的更新。微软对 cycle goal 的定义明确要求,周期结束时客户端包含源在周期开始时已有的全部更新。但是,它不能高效替代真正的 HWM,原因:S 可能长期不产生自己的写入。
假设 S 自己最后一次 originating write 是:
S:700之后 S 从其他 DC 接收了大量更新:
S-local 701 → A:100 S-local 702 → B:200 ... S-local 100000 → C:90000D 已经从 S 完整复制到了 local USN 100000,但:
UTD_D[S] = 700可能仍然不变,因为 701~100000 都不是由 S originating 的更新。
于是:
实际 HWM(D ← S) = 100000 UTD_D[S] = 700下一次复制:
- 使用 HWM:只扫描
localUSN > 100000;- 仅使用
UTD[S]:又从localUSN > 700开始,重复扫描近十万条候选,然后再用完整 UTD 过滤掉。最终修正
真正的问题不是安全性,而主要是精确度和性能:
UTD[S]: 只有 S 自己产生 originating update 时才容易前进 HWM: S 接收任何来源的更新、local-USN 流向前推进时都能前进因此只靠
UTD[S]可能提供一个安全但非常陈旧的 source-local 下界,但可能反复扫描大量已经处理过的 S-local 记录;HWM 则精确记录 D 对 S 本地枚举流的实际消费位置,准确记录了这条直接复制关系真正扫描到了哪里。
Q: 什么是 LVR,为什么需要?
Linked-Value Replication (LVR)是 Active Directory 中用于提升复制效率和冲突处理能力的一项核心机制,主要用于 多值属性(multi-valued attributes) 的复制,比如member🧠 问题背景:为什么需要 LVR?
早期(Windows 2000)时,AD 对多值属性的复制采用 整属性复制,存在两个问题:
- 效率低 如果你向一个组里加一个成员,整个成员列表(可能上千个成员)都要被当作一个整体再次复制,非常浪费。
- 冲突处理差 如果两个 DC 同时对同一个组各加一个成员,整属性复制会导致其中一边的更改被丢弃(因为是整体覆盖)。
✅ LVR 是怎么做的?
👉 每一个值都独立复制、带独立元数据
- 你新增或删除组的一个成员,AD 只复制这一项,而不是整个属性值列表。
- 每个值都有自己的:
- Originating Invocation ID
- Originating USN
- Version
- Timestamp
🔄 复制时只增量同步
- 比如 A 添加 member=User1,B 添加 member=User2
- 互相复制时就能合并成 [User1, User2],而不会覆盖丢失
🔐 技术上怎么实现?
在属性的定义上会指定:
isMemberOfPartialAttributeSet: TRUElinkID:标明这个属性是 link 类型有 linkID 的属性:
- 被称为 linked attributes
- LVR 只适用于这些属性(不是所有 multi-valued attribute)
📁 存储结构支持:
msDS-ReplValueMetaData
- 用来记录每个值的变更元信息
- 这是区别于
msDS-ReplAttributeMetaData的结构(后者是整属性级别)
✨ 举个例子(组成员):
你有组
CN=Group1,成员列表是:
- 初始时:member = []
- DC-A 添加了 User-A → member = [User-A]
- DC-B 添加了 User-B → member = [User-B]
有了 LVR:
- A 和 B 互相复制后: → member = [User-A, User-B]
没 LVR(旧机制):
- B 会覆盖 A 的修改或反之 → member = [User-B] 或 [User-A]
✅ 总结
特性 LVR Support 多值属性是否可按值独立复制 ✅ 是 冲突时能否合并多方更改 ✅ 是 是否每个值有独立的复制元数据 ✅ 是(有 msDS-ReplValueMetaData)是否适用于所有 multi-valued 属性 ❌ 否(只适用于有 linkID 的)
Q: USN is typed as long, would it be possible to use up of it?
🧮 能用多久?
以人类尺度来看,几乎不会用完。我们来算一下:
假设一个 DC 每秒 1,000 次变更(这已经非常爆炸):
每年变更数 ≈ 1000 * 60 * 60 * 24 * 365 ≈ 31.5 billion(315 亿)你能用的 USN 范围是:
2^64 ≈ 1.8 x 10^19能撑多久:
1.8e19 / 3.15e10 ≈ 571 million years 😅✅ 也就是说:正常运行,USN 是几乎不可能用完的
😰 但真用完了会怎么样?(理论)
如果 USN 用完了,Active Directory 无法为新的变更分配新的 USN
可能出现的后果:
- 无法写入任何新对象 / 属性
- 无法复制数据(因为 replication 需要比较 USN)
- 整个目录系统挂掉(无法产生新的 change log)
这就类似数据库主键溢出一样,是灾难级别的事情
🧯 有没有保护机制?
虽然微软 没有官方明说“USN wrap protection”,但实际有以下防护:
✅ 1. 使用 64-bit 整数,几乎不会 wrap(long enough to last forever)
AD 从一开始就是用 unsigned 64-bit 设计的,防溢出风险考虑非常充分。
✅ 2. USN 不是跨 DC 累加的,是 per-DC 自己维护的
每个 DC 有自己的 USN counter:
USN@DC1 = 12,345,678 USN@DC2 = 432,198,321这样避免了全球唯一编号压力。
✅ 3. DC 重建时重新分配 Invocation ID 和新 USN 空间(干净 slate)
如果真出现奇怪增长(例如:
- 自动脚本疯狂写入
- replication loop 造成海量版本
- 逻辑错误导致 USN 快速增长
→ 你可以选择重建 DC,换一套 invocationId + 清零 USN
⚠️ 有没有实际案例出现 USN 超限?
查遍微软 support 和 field case:
❌ 目前没有任何一个公开案例表明有 DC 的 USN 用完了
但微软 AD 团队内部曾经在内部文档中说:
“USN wrap-around is theoretically possible but practically infeasible due to 64-bit size and per-DC isolation.”
Q: A 不发送 local usn,那 A 给 B 的数据是按照 local usn 排序的吗?B 在 apply replicated 数据的时候,也是严格按照顺序 apply 的吗?还是会一个包里面随机 apply 呢?
单条 record 包含 objectusn/propertyusn,所以 A 发给 B 数据包含 local usn。
AD replication is not ordered-by-network, but consistent-by-version.
❓1. A 给 B 的 replication 数据,是按照 A 的
localUSN排序的吗?✅ 是的!
Active Directory 在构造 outbound replication queue 的时候,是从
HWM+1开始,按localUSN升序扫描 changelog,也就是按照日志生成顺序(时间顺序)发数据。这也是为什么:
- HWM 是游标
- localUSN 是 changelog 的索引键(有序日志)
❓2. A 发送的时候是不是按顺序发的?
✅ 是的!
虽然 replication 采用 按对象 batching(按对象归组发包),但:
- 传输顺序是根据 localUSN 的
- 包内对象顺序可能会被 batching 优化打乱(为了效率),但整体流是基于 changelog 顺序的
❓3. B 接收到数据,是不是“严格按顺序 apply”?
🟡 不一定是“严格顺序”,但逻辑上是“按依赖顺序”保证一致性
这是最 subtle 的地方!我们分两种情况讲:
🧠 对象级变更(普通属性更新)
✅ B 在 apply replicate 数据时,对每个对象是 逐个属性 apply 的
➤ 变更不会乱序覆盖,AD 会比对版本号再应用
所以即使一个 batch 里对象乱序、版本顺序乱了,也不会影响最终一致性:
- 属性已经是最新 → 跳过
- 属性是旧版本 → 覆盖
- 属性等版本 → 无冲突,覆盖无影响
==> 版本号(version + timestamp + originatingUSN) 是决定最终写入的关键,不是顺序
🔗 Linked 属性(如
member/memberOf)对于这种需要对“多值集合”做增删的:
- AD 使用 Linked Value Replication (LVR)
- 每条“add/remove member” 会独立 replicate + apply
⏱ 虽然顺序对最终集合状态可能有影响,但 AD 会:
- 按照版本号 + timestamp 排序
- 确保最终集合的一致性
🚀 并发 apply:AD 内部采用线程池多线程执行 apply 操作
所以 replication apply 是 异步、并发的,但:
- 每个对象内部的变更是原子性的
- 每个属性的版本号判断确保最终一致性
- replication 引擎保证同一个对象不会被两个线程同时 apply(锁定)TODO replication engine single thread?
Q: Samba 在本地 apply changes 的时候是顺序的吗?
是的,但要精确到“哪个层级的顺序”。
结论
在 Samba 当前实现中:
同一个 DRS response chunk 里的普通对象,是按接收到的对象数组顺序,一个接一个串行 apply 的,不是并行乱序 apply。
但:
- 它保证的是对象 apply 顺序;
- 不保证 originating USN 逐号出现;
- linked-value updates 是特殊路径,会延后到 transaction 的
prepare_commit阶段处理;- 整个 chunk 在一个数据库事务中提交。
1. 普通对象确实逐个 apply
Samba 接收到 DRS 对象链表后,
dsdb_replicated_objects_convert()按first_object -> next_object的顺序遍历,并依次写入:out->objects[0] out->objects[1] out->objects[2] ...这里没有重新按 originating USN 排序。
真正 apply 时,Samba 使用:
ar->index_current指向当前对象。
replmd_replicated_apply_next()只处理:ar->objs->objects[ar->index_current]当前对象处理完成后,代码才执行:
ar->index_current++; return replmd_replicated_apply_next(ar);进入下一个对象。也就是说它是 callback 驱动的串行链,而不是一次把多个对象并发提交。
逻辑相当于:
apply object[0] 等待成功 apply object[1] 等待成功 apply object[2] 等待成功 ...2. local USN 也按实际 apply 顺序分配
当某个复制对象确实有远端属性胜出、需要修改本地数据库时,Samba 调用:
ldb_sequence_number(ldb, LDB_SEQ_NEXT, &ar->seq_num);取得新的本地 sequence number,然后把它写入:
uSNChanged = ar->seq_num metadata.local_usn = ar->seq_num因此,对实际被接受并写入的对象更新而言,接收 DC 的 local USN 按 apply 顺序单调增加。
例如收到顺序是:
object X,含 origin=A:91 object Y,含 origin=A:92 object Z,含 origin=A:100可能在 S 上得到:
S-local 5001 → object X / A:91 S-local 5002 → object Y / A:92 S-local 5003 → object Z / A:100但若某个 incoming update 比本地旧、所有属性都被冲突规则忽略,它可能不会产生新的 local USN。所以不能认为“每个收到的 DRS 对象必定消耗一个 USN”。
3. 但不能把 DRS 看成逐 USN 的 changelog replay
这里是前面讨论最容易出错的地方。
Samba 源端正常复制时,确实把候选对象按对象的
uSNChanged排序,然后通过num_processed顺序分页。但一个 DRS object 可能包含多个属性,例如:
Object X: attr1 origin=A:91 attr2 origin=A:100 attr3 origin=C:40这个对象只有一个当前的:
uSNChanged = 100源端枚举的是:
uSNChanged > baseline 的对象然后再检查该对象每个属性的 originating metadata,决定哪些属性需要发送。Samba 代码也是先检查对象的
uSNChanged,再逐属性应用 UTD filter。因此,不能假设数据库里一定有如下一一对应的十条可重放记录。
A-local 91 → A:91 A-local 92 → A:92 ... A-local 100 → A:100可能实际只有:
Object X 当前 uSNChanged = 100 其中保存: attr1 stamp=A:91 attr2 stamp=A:100中间历史对象版本已经被当前状态覆盖,并不会像 WAL 一样逐条发送。(笔者注:再次强调 AD 是 state based 模型,不是 log based,所以 replicate 的时候按照 object state,两台 DC 上的 changes / usn 就可能完全不一样。)
4. 整个 chunk 是一个事务
dsdb_replicated_objects_commit()在 apply chunk 前调用:ldb_transaction_start()然后对整个 replicated-object set 执行 extended operation,所有对象处理完成后才
prepare_commit和正式提交;发生错误则取消整个事务。所以可以理解为:
chunk 内: 对象逐个计算、逐个写入事务 chunk 结束: 整个事务统一 commit这不是并行 apply。
5. Linked attributes 是例外
链接值更新不会完全按照普通对象顺序立即落库。
Samba 会先把收到的 linked attributes 保存起来,并按 source object 和 attribute 分组;普通对象全部处理完后,链接更新在 transaction 的
prepare_commit阶段处理。代码注释明确说这样做是为了允许链接在相关对象创建之前到达。所以准确表述是:
普通 replicated objects: 按接收数组顺序串行 apply linked-value updates: 收集、分组,在事务后段处理回到 A → S → B 的问题
在 Samba 的普通复制路径中:
A → S: A 按 A-local uSNChanged 排序候选对象; S 按收到的对象顺序串行 apply, 为真正生效的本地对象修改分配 S-local USN。 S → B: S 再按 S-local uSNChanged 排序候选对象; B 同样串行 apply。因此,前面用“为了加速,S 乱序 apply A:100,然后才 apply A:92~99”来解释是不对的。
更根本的原因是:DRS 复制的是对象当前状态及属性级复制元数据,不是把 originating USN 当成逐条日志严格 replay。对象 apply 是串行的,但对象携带的各个属性 stamp 不需要按 originating USN 连续排列。
Q: Replication 的数据是怎么 batching 的?batch 大小怎么定?传输的数据里含不含 before value?
🧠 目录同步数据的分批策略(batching)
✅ 目录复制采用「对象为单位」做 batching,且带有如下策略:
维度 是否影响 batch 对象数量 ✅ 有默认最大数量(对象/属性数) 数据体积 ✅ 会考虑总大小(最大 RPC payload 限制) 属性数量 ✅ 一个对象属性太多会被单独拆成 batch 网络条件(压缩、带宽) ✅ 有影响,尤其是跨站点复制 🧩 真实实现中使用的协议是 DRS RPC(Directory Replication Service Remote Protocol),详见 MS-DRSR 协议规范。
🔸 你可以理解为:
ReplicaSyncRequest { StartUsn = HWM+1, MaxBytes = ~500KB, MaxObjects = ~1000 }AD 会在返回数据时:
- 拉够一定数量的变更对象
- 或者达到最大 allowed bytes(例如 512KB)
- 就形成一个 batch 返回
📦 所以 batch 是基于「对象数量 + 数据大小 + 属性复杂度」的混合触发规则。
📦 一个 batch 里是什么?
一个 replication batch 会包含:
数据项 说明 对象的 objectGUID✅ 你改了哪个对象 所有“变更属性”当前值 ✅ 仅包含新的值(current value) 每个变更的 originatingInvocationId,originatingUSN,timestamp,versionNumber✅ 用于去重、UTD 比较 增删信息(如 LVR) ✅ 对 linked value 属性(如 member)会有 add/remove 操作信息
❌ 不包含的内容:
不包含 理由 属性的“旧值”(before value) ❌ 没必要。AD 是 状态复制,不是事件溯源系统 差异 delta patch ❌ 不支持属性内的“增量变更”(比如修改一个字符串的一部分) Replication 是将“修改后的完整属性值”发送给目标
🧠 为什么不带 “before” 值?
因为:
- Active Directory 复制是 状态复制模型(state-based)
- 它复制的是“对象的最新状态”,不是“操作序列”
- 冲突解决依赖 version/timestamp,不依赖变更历史
所以:
❌ AD replication ≠ event sourcing ✅ 它是 a snapshot-delta propagation protocol
Q: 一个 Batch 包大小会跟物理距离有关吗?
是的,根据实际体验,一次 DirSync 的包可能与(我们 sync 的 BE 和所使用的 DC 之间的) inter/intra site 物理距离相关,一个包的数据量大小(
avg_DirSyncObjectCount)可以相差 100x。
Q: LVR 对 link 的 replicate 会带上 old value 吗,不然怎么表示是增加还是删除特定的 value 呢?
🔁 在 LVR 中,删除(Remove)操作确实需要带上被删除的旧值,否则接收端无法知道删除的是哪个 value。
🔍 为什么要带 old value?
因为 LVR 的核心是 “value-level” 的操作,而非 “attribute-level”。
举个例子:
Group
CN=Admins的member属性有多个值。如果你要从中删除一个 member(比如
CN=Alice),那么复制时,必须明确告诉目标 DC:
“我要删掉
member = CN=Alice这个具体的值。”这就需要带上被删除的 old value(CN=Alice) 以及相关的 metadata(版本号、originating USN、invocationID 等)。
✅ LVR 是如何表示增删的?
LVR 使用特殊的数据结构叫:
msDS-ReplValueMetaData每一个被 replicate 的值,都携带以下元数据字段:
字段名 说明 LastOriginatingChangeTime时间戳 Version增加/删除操作的版本号 OriginatingInvocationID来源 DC 的唯一 ID OriginatingUSN来源 DC 上的 USN LocalUSN本地记录的 USN Flags指示是“Add” 还是 “Delete” 的关键位 ✅
🏷️
Flags字段的定义:在内部结构中(比如 REPLVALINF 或 DS_REPL_ATTR_VALUE_META_DATA),
Flags是一个位掩码,可能的取值包括:
0x0→ 表示这是一个 添加的值0x1→ 表示这是一个 删除的值事实上,这个flag是:fIsPresent: FALSE if and only if the link value has been removed from the attribute.
🧪 举个实际例子(伪结构):Real code here
{ "Value": "CN=User1,OU=Users,DC=contoso,DC=com", "OriginatingInvocationID": "{GUID}", "OriginatingUSN": 12345, "Version": 4, "Flags": 0x1, // ⬅️ 表示这是一个删除操作 }
🧠 小结:LVR 增删操作识别方式
内容 普通属性 LVR 属性(如 member/memberOf) 操作粒度 整体属性 单个 value 是否记录增删操作 ❌ 不记录 ✅ 有明确的 Add/Delete标识元数据结构 msDS-ReplAttributeMetaDatamsDS-ReplValueMetaData增删标识位置 无 在 Flags字段中
Q: 可是 AD replicate 不是 state-based 吗,source DC 怎么知道呢?这条 record usn 对应的 change 就包含了这个信息吗?
🧠 TL;DR:
虽然 AD 是 state-based,每一条 LVR change 记录中仍然带有“这是 Add 还是 Delete” 的语义信息,因为:
- 每个 LVR 的值在内部被存储成“半对象”,具有自己的 replication metadata。
- 这份 metadata 就说明了:
- 是什么值;
- 来自谁(originating DC);
- 在哪里被修改(USN);
- 版本号;
- 是否是删除操作(Flags=0x1) ← ✔ 关键就在这。
🔍 更深入一点:为什么 State-Based 却能做到语义复制?
我们先对比两者:
普通属性复制(非 LVR) LVR 属性复制(如 member)整个属性作为一个 blob 看待 每个值独立被 track 和 replicate 不知道你是删了 A 还是改成 B 能精确知道 Add A、Delete B 仅根据最终值决定是否发 复制“每一条变化”
✅ 所以答案是:
是的,这条 change 对应的 originatingUSN 所指向的记录,就已经包含了 “Add/Delete” 的语义信息。
因为:
- 在 NTDS 的数据库中(Jet engine),LVR 属性对应的数据结构如:
REPLVALINFDS_REPL_ATTR_VALUE_META_DATA- 每一项 value metadata 里都带有:
- 该值
- origin 的信息
- 操作版本号
- Flag 表明是否为删除
所以哪怕是 state-based 拉增量的范围,比如:
Give me all changes from USN=12345你返回的 change 里,就已经是「我要删除哪个 value」这样的语义数据了。
🧪 举个 LVR 删除的例子:
{ "Attribute": "member", "Value": "CN=Bob,OU=Users,DC=contoso,DC=com", "Version": 5, "Flags": 0x1, // 表示删除这个值 "OriginatingUSN": 12345, "OriginatingInvocationId": "{GUID}" }
📌 小结:
项目 是否携带删除语义 原因 普通属性复制 ❌ 不携带 只知道当前属性值,不知道怎么变的 LVR 属性复制(如 member) ✅ 有明确语义 每个值都带 metadata,包含 add/delete 信息
Q: 有实际 LVR 的 case 吗?
LVR works like sequense, full sync will replicate all value data
member;range=1-1=<GUID=d6b51da7-69e1-4a4f-aba5-b8b5eeb01462>;<SID=S-1-5-21-888882438-985158672-1989895028-2018>;CN=MB12,OU=test_domain0521003912.com,OU=Microsoft Exchange Hosted Organizations,DC=SG2TDSO100009ED,DC=extest,DC=microsoft,DC=com,<GUID=4ba6974e-85b7-4d91-a5c4-0cccb15d3d0b>;<SID=S-1-5-21-888882438-985158672-1989895028-2017>;CN=MB11,OU=test_domain0521003912.com,OU=Microsoft Exchange Hosted Organizations,DC=SG2TDSO100009ED,DC=extest,DC=microsoft,DC=com,<GUID=eb9677fc-73d2-4022-89de-aa0ddbc1c24c>;<SID=S-1-5-21-888882438-985158672-1989895028-2020>;CN=U12,OU=test_domain0521003912.com,OU=Microsoft Exchange Hosted Organizations,DC=SG2TDSO100009ED,DC=extest,DC=microsoft,DC=com,<GUID=763a3dce-f2d0-437c-8fca-25e053605884>;<SID=S-1-5-21-888882438-985158672-1989895028-2023>;CN=U15,OU=test_domain0521003912.com,OU=Microsoft Exchange Hosted Organizations,DC=SG2TDSO100009ED,DC=extest,DC=microsoft,DC=com,<GUID=7399c7bf-18c7-41c1-92fb-a375fc5a14c3>;<SID=S-1-5-21-888882438-985158672-1989895028-2024>;CN=U16,OU=test_domain0521003912.com,OU=Microsoft Exchange Hosted Organizations,DC=SG2TDSO100009ED,DC=extest,DC=microsoft,DC=com,<GUID=4ae9b4b5-d518-411e-9a3b-2b01d81065ed>;<SID=S-1-5-21-888882438-985158672-1989895028-2005>;CN=U10,OU=test_domain0521003912.com,OU=Microsoft Exchange Hosted Organizations,DC=SG2TDSO100009ED,DC=extest,DC=microsoft,DC=com,<GUID=574863a4-e60e-47ed-9e27-d311e1f7c8d8>;<SID=S-1-5-21-888882438-985158672-1989895028-2019>;CN=U11,OU=test_domain0521003912.com,OU=Microsoft Exchange Hosted Organizations,DC=SG2TDSO100009ED,DC=extest,DC=microsoft,DC=com,<GUID=acf68373-2861-4329-9b71-d134f591232d>;<SID=S-1-5-21-888882438-985158672-1989895028-2022>;CN=U14,OU=test_domain0521003912.com,OU=Microsoft Exchange Hosted Organizations,DC=SG2TDSO100009ED,DC=extest,DC=microsoft,DC=com,<GUID=c1440758-da9b-45ea-abf9-8595ba4f6fe5>;<SID=S-1-5-21-888882438-985158672-1989895028-2021>;CN=U13,OU=test_domain0521003912.com,OU=Microsoft Exchange Hosted Organizations,DC=SG2TDSO100009ED,DC=extest,DC=microsoft,DC=com,<GUID=f0912ed3-ea1e-4b4d-aa54-8af2b9805a25>;<SID=S-1-5-21-888882438-985158672-1989895028-2001>;CN=U6,OU=test_domain0521003912.com,OU=Microsoft Exchange Hosted Organizations,DC=SG2TDSO100009ED,DC=extest,DC=microsoft,DC=com,<GUID=e6eb5fcb-c4fc-41d2-9244-4e7ce8953cd2>;<SID=S-1-5-21-888882438-985158672-1989895028-2003>;CN=U8,OU=test_domain0521003912.com,OU=Microsoft Exchange Hosted Organizations,DC=SG2TDSO100009ED,DC=extest,DC=microsoft,DC=com,<GUID=077ed197-e41f-440c-9049-ef613a4fba23>;<SID=S-1-5-21-888882438-985158672-1989895028-2004>;CN=U9,OU=test_domain0521003912.com,OU=Microsoft Exchange Hosted Organizations,DC=SG2TDSO100009ED,DC=extest,DC=microsoft,DC=com,<GUID=f54c7b41-0c55-4462-ae2a-72841937f061>;<SID=S-1-5-21-888882438-985158672-1989895028-2000>;CN=U5,OU=test_domain0521003912.com,OU=Microsoft Exchange Hosted Organizations,DC=SG2TDSO100009ED,DC=extest,DC=microsoft,DC=com,<GUID=8f227922-1411-44a5-aaf1-82c78a66aa4f>;<SID=S-1-5-21-888882438-985158672-1989895028-2002>;CN=U7,OU=test_domain0521003912.com,OU=Microsoft Exchange Hosted Organizations,DC=SG2TDSO100009ED,DC=extest,DC=microsoft,DC=com member;range=0-0=<GUID=159c87f7-5401-41f2-97be-aa8bddfdebd9>;<SID=S-1-5-21-888882438-985158672-1989895028-1999>;CN=U4,OU=test_domain0521003912.com,OU=Microsoft Exchange Hosted Organizations,DC=SG2TDSO100009ED,DC=extest,DC=microsoft,DC=com,<GUID=a0cde7f2-37e8-483e-a54f-792bb46fadfa>;<SID=S-1-5-21-888882438-985158672-1989895028-1998>;CN=U3,OU=test_domain0521003912.com,OU=Microsoft Exchange Hosted Organizations,DC=SG2TDSO100009ED,DC=extest,DC=microsoft,DC=com,<GUID=6fba86e8-f415-4d1b-b425-b6a68252779d>;<SID=S-1-5-21-888882438-985158672-1989895028-1996>;CN=U1,OU=test_domain0521003912.com,OU=Microsoft Exchange Hosted Organizations,DC=SG2TDSO100009ED,DC=extest,DC=microsoft,DC=com,<GUID=6c4c8ce7-cccb-4322-b859-44aff8b9b337>;<SID=S-1-5-21-888882438-985158672-1989895028-1997>;CN=U2,OU=test_domain0521003912.com,OU=Microsoft Exchange Hosted Organizations,DC=SG2TDSO100009ED,DC=extest,DC=microsoft,DC=com
Q: 对非 LVR 的复制来说,如果要删除一个property 是怎么做的呢?
Deleted properties would be placed with empty value (
NULL) and replicate out with version number increased.
Q: 如果一个对象非常大(属性很多 or 单个属性特别大),会不会单个对象就超过 batch 限制?那 AD 会怎么处理?能分成多个 batch 吗?
Yes,大对象会导致一个 batch 只能塞一个对象,甚至拆对象属性来分批发!
Active Directory 复制协议(MS-DRSR)做了专门处理这种情况的逻辑,尤其是在遇到:
- 属性特别多的对象(如
userCertificate,member多值)- 大型单值属性(如
jpegPhoto,userParameters)
🎯 怎么处理大对象?
👇 DRSR 协议(replication engine)做了以下策略:
情况 处理方式 一个对象大到接近 batch 限额 ✅ 整个 batch 只发这个对象 对象太大,单个属性块都装不下 ✅ 拆分属性块分多个 batch 属性多到超过属性上限 ✅ 对象内容会被分多个 partial records 发出 📦 实际效果:
✅ 大对象的属性会被分批 serialize + replicate
也就是说:
一个对象可能需要多个 batch 才能被完整同步完!
你可以在
repadmin /showrepl或复制 trace 里看到:Continuing large object replication Partial attribute replication in progress这就是 AD 在做 多阶段对象复制(chunked replication)
🧠 例子模拟:
假设:
- 一个用户对象有 50 个
proxyAddresses,每个都很长- 加上附带的
userCertificate,thumbnailPhoto,description等单个对象可能 >512KB。
那 DRSR 会把它切成:
Batch 1: objectGUID = X attributes: proxyAddresses[0~24] thumbnailPhoto (partial) Batch 2: objectGUID = X attributes: proxyAddresses[25~49] thumbnailPhoto (rest) description✅ 最终在目标 DC 上 merge 成完整对象。
⚠️ 注意:
- 这种 chunked replication 机制 只对 replication 层可见
- 应用层(如 LDAP、PowerShell)看到的永远是完整对象
- 分块是临时行为,只为传输用,最后 merge 再 apply
Q: DC 之间互相 replicate 的时候也会等待拿到至少有个完整的对象吗?中间 sub batch 对 DC 可见吗?
🧠 答案概要:
问题 答案 分片是否立即可见? ❌ 否,直到整个对象复制完成前,这个对象的变更 对目标 DC 是不可见的 是什么机制保证一致性? ✅ 内部有“replication queue” 和“apply gating”机制,确保完整后才 commit 如果复制失败怎么办? 复制失败的片段不会被 apply,整个对象 replication 会被重试 📦 更详细解释:
🧩 1. 大对象复制如何分片?
- AD 复制过程使用一种叫
DS_REPL_OBJ结构体的数据包。- 当一个对象非常大(可能由于:
- 属性数量太多(例如
member上千个);- 某个属性内容很大(如照片、证书、描述);
- 就会把这个对象的复制 change 拆成多个数据包(sub-batches)来传输。
举个例子:
一个对象包含的数据总量为 2MB,AD 当前复制 batch 限制为 512KB:
- 第一次复制:Header + 属性块1(500KB)
- 第二次复制:属性块2(500KB)
- ...
- 第四次复制:最后一块(200KB)
所有这些 sub-packets 会在接收方 AD 的内存缓冲中组装。
🔒 2. Sub-batch 是否“可见”?
❌ 不会被立即 apply 到目录数据库,不会被其他 API 查询看到!
- 接收端 DC 会暂存在「Replication Receive Queue」中。
- AD 复制引擎(KCC + DRS)只有在完整对象拼接完毕、结构验证通过之后,才会写入 Jet(NTDS.dit)中。
- 在此之前,其他 LDAP 或 AD API 查询不到这个对象的新状态。
🔄 3. 什么情况会失败或回滚?
- 如果网络中断、中途失败或源 DC 重启等,目标 DC 会清除尚未完成的部分。
- 下次 sync 会重新请求整个对象(除非 partial 成功记录被支持 —— 通常不会)。
Q: 属性很多的时候不是有 LVR 吗?还会分 batch 吗?
特性 是否避免分 batch? 说明 LVR 开启后 ❌ 不避免 LVR 是优化“属性层面的复制逻辑”,不是传输层面的打包机制 对象太大 ✅ 会分 batch 即使用了 LVR,只要 total data 超出 batch 限制(默认 512KB),仍需分批
Q: [Conflict Resolution] 如果在 AD v1(没有 UTD Vector)的时候,B 收到 A 发来的 change,要怎么判断这个 change 是不是自己已经 apply 过了?是不是只能遍历所有 local change 看是否匹配?这其实也就是 usn?
✅ 简明结论:
在没有 UTD Vector 的年代(如 Windows 2000 初代 AD), AD 是靠每个对象的“属性版本元数据(Replication Metadata)”来做精确比对,不是遍历所有 change log。
所以:
- ❌ 不会遍历 local changelog
- ✅ 会直接比对当前对象上的属性元数据
🧠 那时是怎么实现去重的?
AD 每个对象在本地都保存了一份 “属性级别的 replication metadata”,它包含:
attribute: mail originatingInvocationId: A_ID originatingUSN: 1200 version: 5 timestamp: 2025-04-10T08:00Z当 B 收到 A 发来的 change:
originatingInvocationId = A_ID originatingUSN = 1200 attribute = mail version = 5它会去比对自己本地 User-X 的
如果本地已有:
originatingInvocationId = A_ID originatingUSN = 1200 version = 5→ ✅ 一模一样,认为 已经 apply 过 → 跳过
如果本地没有、或者版本较低 → apply
🎯 那这些属性 metadata 存在哪?
在每个对象的隐含属性中叫做:
msDS-ReplAttributeMetaData你可以用
repadmin /showobjmeta或ldifde读出来。
🔁 所以过程简化成:
- B 收到变更(change)来自 A
- 对应对象已经存在
- B 查看该对象上变更属性的 metadata:
- 看
originatingInvocationId- 看
originatingUSN- 看
version- 如果完全一样 → skip 如果更旧 → apply 覆盖
✅ 所以最终回答你的问题:
问题 答案 没有 UTD Vector 怎么判断重复? 靠对象上每个属性的 replication metadata(不是 changelog) 会遍历所有 local USN 的 change 吗? ❌ 不会。不会回查 changelog 需要 changelog 吗? ✅ changelog 只是作为发送时数据源,apply 不用它
🔍 为什么后来还需要 UTD Vector?
因为:
属性级 metadata 能判断“是否重复”, 但 不能避免 A 发给 B 这条变更!
所以:
- 有 metadata → 可以 skip 变更
- 但没有 UTD → 变更还是会被重复发
📉 这就浪费带宽 + 计算资源
Q: Windows Server 2003 引入 UTD Vector 后,AD 还会不会存 msDS-ReplAttributeMetaData?如果会,那是不是“只是不用它了”?
✅ 精准回答:
Windows 2003 之后仍然存
msDS-ReplAttributeMetaData,而且一直用! UTD Vector 是用来“减少重复发送” 而msDS-ReplAttributeMetaData是用来“决定是否应用变更”也就是说:
✅ 两者在复制机制中并不是替代关系,而是配合关系!
🧠 各自的职责总结一下:
机制 用于 是否仍保留 从属对象 UTD Vector 判断变更是否应该从 Source DC 发出来(全局过滤) ✅ 保留(2003+引入) 会话级别,用于复制会话 ReplAttributeMetaData 判断变更是否应该被应用(属性级别判断) ✅ 一直存在 每个对象的属性 metadata
🧬 再来个现实类比理解:
你可以把这俩理解成:
机制 类比 UTD Vector 快递公司派件前判断:“你是不是已经收到这个包了?” AttributeMetaData 你签收时看快递内容:“这货是不是我已经有了版本?是不是更新?”
✅ 所以即使有 UTD Vector,B 仍然要:
- 用 UTD Vector 判断哪些变更 A 不需要发给我(源端过滤)
- 一旦收到变更,B 还会比对自己对象上的 metadata:
originatingInvocationIdoriginatingUSNversion来决定 apply or skip。
Q: 如果收到一条 change,它的 replication metadata version 比我本地的还低,该怎么处理?应该 drop 还是覆盖?
✅ 答案简明:
应该 drop,不 apply。
AD 复制引擎会做 版本号 + originating metadata 比对, 如果收到的 change 比本地版本旧,就 直接丢弃该属性变更。
🧠 原因:Active Directory 是 state-based, version-aware replication
它不是 naive apply 的复制,而是:
始终以对象每个属性的
versionNumber、originatingUSN、originatingInvocationId为依据判断是否接纳变更。
🔍 具体判断逻辑是这样的:
当收到一条变更,系统会检查:
对应本地该对象属性是否存在?
- 如果 ❌ 没有 → apply ✅
- 如果 ✅ 有 → 比较以下字段:
字段 用途 决策 versionNumber 每次变更会加一 ✅ 更高 → apply;更低 → drop timestamp 发起时间 conflict resolver fallback originatingInvocationId 区分不同 DC 冲突检测中关键身份信息(GUID 大的赢) originatingUSN 发起变更时该 DC 的 USN 更高优先(辅助判断) 如果 version 更低,那这条变更即使 UTD Vector 不小心漏掉了它,也不会生效!
✅ 所以这是一种双保险机制:
- UTD Vector → 控制“要不要发”
- 属性级 metadata(msDS-ReplAttributeMetaData) → 控制“要不要 apply”
⚠️ 为什么 version 会更低还发过来了?
虽然这种情况很少,但可能会发生:
原因 描述 UTD Vector 不准 复制链断、metadata 失效、invocationId 重置 回滚 VM / 快照还原 DC 恢复旧快照,导致 USN/metadata 倒退 DC 被 demote + promote UTD vector 没更新,但 replica 还留旧记录 人工干预 / 低层写入 admin script 或 API 绕过正常 version 流程 所以 AD 的设计非常稳健:
哪怕复制链条错发了旧数据,也不会覆盖掉最新版本
Q: 如果 version 比我本地小(5 < 6),但 originatingUSN 却比我本地大(1300 > 1200),怎么办?是不是数据坏了?会 apply 吗?还是 drop?
✅ 正确答案先说:
这种变更会被 ❌ drop,不会 apply。
因为 版本号是第一判断依据,它代表变更的“逻辑顺序”。
🧠 为什么 AD 要这样做?
Active Directory 复制冲突处理逻辑是:
先看 versionNumber → 再看 timestamp → 再看 originatingUSN(辅助)
所以即使:
originatingUSN = 1300(看上去更新)versionNumber = 5(但比我已有的 6 小)🛑 AD 认为这是一个「旧版本」→ 直接丢弃!
🔬 微软官方冲突解决顺序是这样:
按顺序判断:
- versionNumber
- timestamp
- originatingInvocationId(tie-break)
- originatingUSN(辅助排顺序,但不是冲突裁定主依据)
❗那为什么会出现 version < 本地、USN 却 > 本地的情况?
你真的挖到了一个现实中可能出 bug 的边缘场景:
场景 原因 人工写入或脚本绕过 修改属性但不自增版本(绕过 API 层) 快照还原 AD 还原到过去的状态,但 USN 被修改为未来值 DC 未正常更新 version 某些系统 bug / 内部 API 忽略 version bump 调用低层 DS API 时设置了错误字段 如 DsReplicaUpdateRefs使用了错误值写入
Q: 如果收到的变更 version = 7(比我已有的 6 高)✅,但它的 originatingUSN = 1100(比我记录在 UTD 里的 1200 小)❌,到底 apply 不 apply?
🧠 快答结论:
❌ 不会 apply。会被 replication 引擎 skip 掉,甚至连发都不发。
即使 version = 7,看起来逻辑上“更新”—— 但因为 originatingUSN ≤ UTD[A_ID](我认为你已经发过 ≤1200 了),所以我根本不会从你这儿拿这个变更!
✅ 你发现了 UTD Vector 的重大风险点之一:
UTD 是基于 originatingUSN 的“乐观推测”机制,它不看版本号、不看属性内容,只看“你声称自己发到哪了”!
所以:
- 如果 UTD[A_ID] = 1200
- 你发来的变更 USN = 1100 → 🚫直接跳过,不会 even check version
- 即使 version 是 7(高于我本地的 6)→ 😱 我也 miss 掉了!
🧯 为什么 AD 要这样做?
因为 AD 假设:
originatingUSN 是单调递增的,不会回退 版本号应该随 USN 同步递增,不会 version 更高但 USN 更小
换句话说:UTD 认为:USN 是变更顺序的可信代表
⚠️ 所以如果 USN “穿越回去”了(USN 小但版本高),就意味着:
- 出现了数据不一致
- UTD 被误导
- 当前 DC 会漏 apply 合法的更新(你说得对)
✅ 那现实中怎么办?
这就是为什么:
🔥 Active Directory 强烈依赖以下前提:
保证 描述 USN 递增 所有新变更必须用更大的 originatingUSN version 同步递增 每次属性修改都必须 version++ 不允许回滚 DC 快照 否则会产生 USN 倒退、UTD 不准 DC crash 恢复必须换 invocationId 否则其他 DC 以为你是之前的自己
💡 微软实际处理这种情况的方法:
场景 应对策略 快照恢复 / USN 倒退 强制分配新 invocationId,旧的 UTD 记录不再生效检测到 version higher, USN lower 会产生一致性告警(Event 1988),并需 admin 介入 受害对象被标记 标记为 inconsistent,对象会被“重新 full sync”(MS内部称为 reconciliation)
✅ 所以最终你的判断是对的:
💣 如果 version 比本地高,但 originatingUSN 比我 UTD 记录还小 → 这条更新被默默 skip,AD 认为“我早拿过你了”,实际却错过了
这就是:
UTD vector 的盲区:它只 trust USN,不检查 version
Q: 区分 source DC 的 UTD Vector Check 和 target DC 的 Conflict Resolution
🧠 所以必须分清两种判断机制:
机制 判断时机 用来做什么 决策基础 目标 UTD Vector Check ✅ 在 source DC 决定发不发 时 是否把这条变更发给你 originatingInvocationId + originatingUSN节省带宽 Conflict Resolution ✅ 在 target DC 接收到变更后 要不要 apply versionNumber → timestamp → originId保证一致性 ✅ 这两段逻辑发生在:
[A → B replicate] 期间: Step 1: A 扫 changelog,HWM+1 开始 Step 2: 对每条变更,判断是否 in B’s UTD ❌ 如果 USN ≤ UTD → skip → B 永远看不到这条变更 Step 3: 若通过,A 发变更给 B Step 4: B 收到 → 进入 Conflict Resolution 流程 1️⃣ version 更高 → apply 1️⃣ version 相同 → check timestamp 1️⃣ 最后 tie-breaker: originating ID
Q: 如果 DC A 在 t1 改成了 v1,DC B 在 t2 改成了 v2,两个变更彼此不知道,因为还没同步,现在 A 和 B 互相同步,会发生什么?谁赢?谁被 drop?
⚖️ AD 的 Conflict Resolution 顺序(再次强调):
当同一个属性(如
1️⃣ versionNumber → 谁的版本号高谁赢 2️⃣ timestamp → 谁的时间更新谁赢(更晚 wins) 3️⃣ originatingInvocationId → GUID 大的赢(tie-breaker)🧾 所以最终 A 的 changelog + 对象状态是:
- changelog entry(localUSN = A 本地分配的)
- metadata 更新成「来自 B 的更新」的那一条
💡 微妙但重要的点:
A apply 了来自 B 的变更,但它不会认为这是自己发的! 因为 metadata 是:origin = B
所以如果以后 C 来问 A 拉数据:
- A 会把这条来自 B 的变更再次 relay 给 C
- 但仍然会保留:originatingInvocationId = B
这就是 transitive replication 的核心!
🎯 为什么 v1 会“丢”?
你说得对,它的确是丢了,但这里的“丢”不是 bug,而是 Active Directory 明确的设计选择:
AD replication 不保留每一条变更的历史,不合并冲突,只保留最后赢下来的值。
✅ 用更正式的说法:
AD 是:
- ❌ event-sourcing(事件流)系统
- ✅ state-based (状态同步) 系统
它同步的是:每个对象、每个属性的“最终状态”,而不是「发生了哪些事」。
🚫 v1 就这样“死”了:
项目 会发生? 被记录到 changelog? ✅ 是的,在 A 本地 changelog 中 被 replicate 给其他 DC? ❌ 不会,version 比 v2 低 被应用? ❌ 未来不会了 会被找回来吗? ❌ 除非你手动恢复或查日志备份 ✅ 为什么要这样设计?
因为:
- 📈 为了 性能和可扩展性
- 💥 多主系统中「合并」太复杂,冲突难以定义
- 🤝 相比之下,简单 + deterministic 的“last writer wins”模型更稳妥
- 🧠 AD 依靠版本号、timestamp 和 origin ID 来保证“所有 DC 最终一致”
Q: CNF 对象是怎么产生的?发生命名冲突时,是谁的对象会被改名(变成 CNF)?是 Source 还是 Target?被改名的对象,会不会自动 version+1?是谁来做?
AD 实际也会依据类似 attribute level conflict 的方式(VersionNumber、Timestamp、InvocationId)处理 object level conflict,实践可以参考我这篇 AD 如何通过 CNF 对象来解决冲突?
✅ 回答 1:永远是 target(接收端)重命名它自己的对象,source 保留不动
举例:
DC-A 创建了CN=Alice(objectGUID = X)DC-B 也创建了一个CN=Alice(objectGUID = Y)现在 A → B 发起 replication
在同步过程中:
B 会发现:我本地也有一个CN=Alice,GUID 不一样A 发来的对象必须“落入”同一个 DN所以 B 必须 保留一个(通常保留 A 的)B 会主动重命名它自己的对象,加上\0ACNF:{GUID}
🧠 源端 A 的对象永远不改,只有目标端 B 的对象会被系统“保护性重命名”✅ 回答 2:会触发 version +1,而且是 AD replication 引擎主动做的
当目标 DC(比如 B)重命名它自己的冲突对象时:
- 会改动对象的
CN(名字)- 属于属性层面的变更
- 所以系统会:
- ✅ 自动 version++
- ✅ 更新 timestamp
- ✅ 写入
msDS-ReplAttributeMetaData- ✅ 新的 localUSN 被记录
即使这个变更不是你手动触发的,AD replication 引擎也会按照完整 apply 流程写入元数据
Q: CNF 的 GUID 判断和之前 conflict resolution 的 version、timestamp 等是什么关系呢?
这是Active Directory 冲突解决的两条分支逻辑 之间的接口边界——也就是:
🔍「对象层面(Object-level)冲突判断」 和 🔍「属性层面(Attribute-level)冲突解决」之间的关系!
🌱 先说两个不同层级的“冲突”
层级 触发条件 判定依据 行为 🧱 对象级冲突 DN 相同,但 objectGUID 不同 ObjectGUID 重命名其中一个 → CNF 🧩 属性级冲突 对象 GUID 相同,属性值不同 version → timestamp → originId 谁赢 apply,另一个 drop 1️⃣ 对象级冲突:先判定 ObjectGUID 是否一致
这发生在同步之初,当你要将一个对象 "落地" 到目标 DC:
sourceObject.DistinguishedName == targetObject.DistinguishedName系统就要比对:
if sourceObject.ObjectGUID != localObject.ObjectGUID → 命名冲突!
✅ 冲突解决方式:
保留 source(即复制过来的对象)改名 local 的对象,加\0ACNF:{GUID}后缀对 local 对象触发 rename(CN 属性变更),并 version+1
👉 这个阶段根本不管 version、timestamp 等 attribute-level 元数据
2️⃣ 属性级冲突:只有当对象 GUID 相同才会进入
如果:
- 同一对象(
objectGUID相同)- 属性有多个版本(A 改了、B 也改了)
才会触发我们之前讲的:
1. Compare version 2. If tie, compare timestamp 3. If tie, compare originatingInvocationId这个逻辑是 per-attribute 的,不管对象是否曾冲突。
🧠 所以“谁先谁后”的顺序是:
When replicating an object: Step 1️⃣ Check if DN is already occupied └─ if no → create object └─ if yes → check ObjectGUID Step 1a: if GUID same → proceed to attribute merge Step 1b: if GUID different → object conflict → accept the object with larger version number, timestamp, invocation id → rename another one to CNF Step 2️⃣ For each attribute: → compare version / timestamp / originId → apply or drop accordingly
