Skip to content

Support filtering oplog by op type with filter.cmds#949

Merged
zhongli-james merged 9 commits into
alibaba:developfrom
SisyphusSQ:feat-add-cmd-filter
Mar 31, 2026
Merged

Support filtering oplog by op type with filter.cmds#949
zhongli-james merged 9 commits into
alibaba:developfrom
SisyphusSQ:feat-add-cmd-filter

Conversation

@SisyphusSQ

Copy link
Copy Markdown
Contributor

背景

在源端 MongoDB 开启 TTL、目标端作为冷库且不希望删除同步下发的场景下,源端因 TTL 触发的过期删除不应影响目标端数据。

为支持这一需求,本次改动新增了按 oplog 顶层 op 类型进行过滤的能力,用户可通过如下配置过滤 delete oplog:

filter.cmds = d

变更内容

  • collector 配置中新增 filter.cmds
  • collector/filter 中新增 CmdFilter
  • CmdFilter 接入增量同步的 oplog filter chain
  • filter.cmds 增加配置校验,非法值启动时直接报错,避免静默失效
  • 更新 conf/collector.conf 中的配置说明与示例
  • 补充相关测试,覆盖过滤逻辑与配置校验

行为说明

  • 未配置 filter.cmds 时,行为与当前版本保持一致
  • 配置 filter.cmds=d 时,顶层 op=d 的 oplog 会在下游处理前被过滤
  • 当前实现仅支持按顶层 oplog op 类型过滤,不扩展到更复杂的表达式或嵌套语义

验证情况

  • go test ./cmd/collector -count=1
  • go build ./cmd/collector
  • go build ./collector/filter

适用场景

  • TTL delete 不下发冷库
  • 需要按 oplog 操作类型做定向过滤的增量同步场景

兼容性

该改动默认兼容现有行为:

  • 默认空配置不改变现有同步逻辑
  • 仅在显式配置 filter.cmds 时启用对应过滤能力

@CLAassistant

CLAassistant commented Mar 20, 2026

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

@zhongli-james zhongli-james self-requested a review March 23, 2026 07:00
@zhongli-james zhongli-james self-assigned this Mar 23, 2026
@zhongli-james zhongli-james added the enhancement New feature or request label Mar 23, 2026
@zhongli-james

Copy link
Copy Markdown
Collaborator
  1. 是否只适用于源端仅有TTL删除而无业务自行删除的情况,希望能补充相应的使用场景;避免其他开发者在使用相应功能后引起同步/迁移目标端的数据不一致问题。
  2. MongoDB 7.0以后的版本中,TTL删除已经不再是{op:d}类型的oplog,目前代码应该还处理不了这样o.applyOps类型的格式。也是后续可以优化的点。详情参考https://jira.mongodb.org/browse/SERVER-63040
  3. 这个CmdFilter从i/u/d/c/n的取值范围来看,除了d来适配TTL删除的场景之外,还有其他的场景么?

@SisyphusSQ

Copy link
Copy Markdown
Contributor Author
  1. 该功能适合业务场景,例如大量写入或更新的订单表(极少存在主动删除或删除以软删除为主),在某个时间点之前是冷数据,极少用户查询,或只存在业务查询的情况,可以通过同步迁移至冷存MongoDB。后通过TTL索引或者主动删除的情况,将冷数据从源端库中删除,那么就需要过滤delete操作。还有种情况,业务逻辑中存在硬删除,但是希望能够追溯删除后的数据,冷库中的数据也是有价值的。
  2. 后续研究下,我看看怎么能兼容高版本的TTL索引逻辑
  3. 暂时没有,但是对常用op类型做了兼容

@SisyphusSQ

Copy link
Copy Markdown
Contributor Author

在这个配置中,我们是否可以把语义定义为所有的delete操作都做过滤,

filter.cmds = d

这样的话,就可以对新版TTL的处理逻辑做过滤了。
op=="c" && 展开 applyOps,识别内层 op=="d" 全部过滤掉

@zhongli-james

Copy link
Copy Markdown
Collaborator
  1. 该功能适合业务场景,例如大量写入或更新的订单表(极少存在主动删除或删除以软删除为主),在某个时间点之前是冷数据,极少用户查询,或只存在业务查询的情况,可以通过同步迁移至冷存MongoDB。后通过TTL索引或者主动删除的情况,将冷数据从源端库中删除,那么就需要过滤delete操作。还有种情况,业务逻辑中存在硬删除,但是希望能够追溯删除后的数据,冷库中的数据也是有价值的。
  2. 后续研究下,我看看怎么能兼容高版本的TTL索引逻辑
  3. 暂时没有,但是对常用op类型做了兼容

感谢回复。嗯嗯,的确在【正常库->降冷库】的场景下是合理的。此时的源和目标端数据不一致也是合理且ByDesign的结果。

@zhongli-james

Copy link
Copy Markdown
Collaborator

在这个配置中,我们是否可以把语义定义为所有的delete操作都做过滤,

filter.cmds = d

这样的话,就可以对新版TTL的处理逻辑做过滤了。 op=="c" && 展开 applyOps,识别内层 op=="d" 全部过滤掉

我也是这样考虑的,或者增加对自定义c.d的支持来识别o.applyOps中的所有业务删除操作。以及考虑是否跟filter.namespace.white的白名单机制结合,只处理部分特定业务集合(需要保留冷数据)的删除操作。

@SisyphusSQ

SisyphusSQ commented Mar 23, 2026

Copy link
Copy Markdown
Contributor Author

在这个配置中,我们是否可以把语义定义为所有的delete操作都做过滤,

filter.cmds = d

这样的话,就可以对新版TTL的处理逻辑做过滤了。 op=="c" && 展开 applyOps,识别内层 op=="d" 全部过滤掉

我也是这样考虑的,或者增加对自定义c.d的支持来识别o.applyOps中的所有业务删除操作。以及考虑是否跟filter.namespace.white的白名单机制结合,只处理部分特定业务集合(需要保留冷数据)的删除操作。

在MongoDB 6.1 开始,用户主动发起的“多文档删除”本身就可能走 batched delete 路径,也就是op=="c",内层是 op=="d"。所以如果按这个语义来说的话,我们应该只走 filter.cmds = d这个配置会更好一点;不然用户的batched delete可能会被漏过滤

如果这个思路可以,我就按这个思路开始修改代码

@zhongli-james

Copy link
Copy Markdown
Collaborator

在这个配置中,我们是否可以把语义定义为所有的delete操作都做过滤,

filter.cmds = d

这样的话,就可以对新版TTL的处理逻辑做过滤了。 op=="c" && 展开 applyOps,识别内层 op=="d" 全部过滤掉

我也是这样考虑的,或者增加对自定义c.d的支持来识别o.applyOps中的所有业务删除操作。以及考虑是否跟filter.namespace.white的白名单机制结合,只处理部分特定业务集合(需要保留冷数据)的删除操作。

在MongoDB 6.1 开始,用户主动发起的“多文档删除”本身就可能走 batched delete 路径,也就是op=="c",内层是 op=="d"。所以如果按这个语义来说的话,我们应该只走 filter.cmds = d这个配置会更好一点;不然用户的batched delete可能会被漏过滤

如果这个思路可以,我就按这个思路开始修改代码

嗯,我觉得没问题。

@SisyphusSQ

Copy link
Copy Markdown
Contributor Author

我们之前虽然对"d"这个语义做了澄清,但是其他的"i"/"u"没在之前的讨论范围内。查了下MongoDB 官方文档和官方测试,会和"d"存在同样的情况。

把普通 CRUD oplog 包装成外层 op: "c"、内层 o.applyOps: [op] 的形式,有 optype = "i" 和 optype = "u" 的 nested 场景。

目前的PR中,我对于i/u/d都做了兼容。


引用:

Docs: applyOps (database command)

Source: apply_ops1.js#L265-L279

Source: apply_ops1.js#L283-L286

Source: apply_ops1.js#L430-L470

Source: batched_multi_deletes_oplog.js#L41-L51

Source: batched_multi_deletes_oplog.js#L88-L103

Source: batched_multi_deletes_single_oplog.js#L28-L45

@zhongli-james

Copy link
Copy Markdown
Collaborator

好的感谢,我check下PR内容,没问题就处理掉。再次感谢贡献~

Comment thread collector/filter/oplog_filter.go Outdated
}
default:
_ = LOG.Error("unknown applyOps type, filter can't handle. log:%+v", log.Object)
LOG.Error("unknown applyOps type, filter can't handle. log:%+v", log.Object)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

维持原有的写法好了,不然IDE会有错误未处理的提示。

Comment thread executor/executor.go Outdated
return logs
}

// extractApplyOpsForTransform normalizes applyOps into []bson.D so namespace

@zhongli-james zhongli-james Mar 26, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oplog_filter.go中也有针对o.applyOps的case处理,新增函数的话,可以考虑统一下


collector/filter/oplog_filter.go 中新增了 extractApplyOps,executor/executor.go 中新增了 extractApplyOpsForTransform,两者几乎完全一样,但存在微妙差异——filter 版本遇到非法元素时会打 LOG.Error 日志,executor 版本则静默返回 false。这违反了 DRY 原则,更危险的是未来修一处忘了另一处会导致行为分裂。

建议:将 extractApplyOps 提取到 oplog 公共包中作为统一工具函数,两处共用。如果 executor 侧确实不需要日志,可以让公共函数返回 error,调用方自行决定是否打日志:

Comment thread executor/executor.go
}
ops[i] = transSubLog.Dump(keys, false)
}
oplog.SetFiled(partialLog.Object, "applyOps", ops)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

竟然是一个transform功能一直遗留的bug。。

Comment thread collector/filter/oplog_filter.go Outdated

// extractApplyOps extracts applyOps inner operations from the supported BSON
// representations already used in the project.
func extractApplyOps(logObject bson.D) ([]bson.D, bool) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这个函数transform侧也有一个,有办法合并么。

Comment thread cmd/collector/sanitize.go Outdated
return nil
}

func normalizeFilterCmds(cmds []string) ([]string, error) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

字符串规范化做了三遍
normalizeFilterCmds(sanitize.go)在启动时做了 TrimSpace + ToLower。NewCmdFilter(oplog_filter.go)构造时又做了一遍。Filter 方法在运行时对每条 oplog 的 log.Operation 又做了 TrimSpace(ToLower(...))。

对于配置值,校验后应该保证已经是规范形式,构造函数不需要再处理。对于 log.Operation,MongoDB 的 oplog op 字段在正常情况下不会有空格或大写,这里的防御性处理反而带来了不必要的性能开销——每条 oplog 都要走一次字符串分配。

建议:

NewCmdFilter 中去掉 TrimSpace/ToLower,信任上游 normalizeFilterCmds 的校验结果
Filter 中对 log.Operation 的处理改为直接使用,或用一个简单的 == 判断(oplog 的 op 字段是 MongoDB 内部保证的单字符小写)
同理 filterApplyOpsDML 中对 inner op 也在做 TrimSpace(ToLower(...)),可以去掉

Comment thread collector/filter/oplog_filter.go Outdated

innerOperation = strings.TrimSpace(strings.ToLower(innerOperation))
if filter.shouldFilterApplyOpsInnerOp(innerOperation) {
LOG.Info("CmdFilter filter inner %s op in applyOps: %v", innerOperation, ele)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

日志级别太高了。


在高吞吐增量同步场景下,如果 applyOps 中有大量匹配的内层操作(比如批量 TTL 删除),这行 Info 级别日志会产生海量输出,严重影响性能和日志可读性。

建议:将逐条过滤的日志降为 Debug 级别,只保留最终汇总的 Info 日志(即 "CmdFilter applyOps DML filter?..." 那行)。或者改为计数方式,只在结束时打印 "filtered N inner ops"。

Comment thread cmd/collector/sanitize.go Outdated
continue
}
if _, ok := validFilterCmds[cmd]; !ok {
return nil, fmt.Errorf("filter.cmds[%s] should in {i, u, d, c, n}", cmd)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

建议:改为 "filter.cmds: unknown op type %q, must be one of {i, u, d, c, n}"。

单元测试同理

Comment thread collector/filter/oplog_filter.go Outdated
return false
}

type CmdFilter struct {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"Cmd" 在 MongoDB 语境中通常指 command(即 op=c 的 oplog),但这个 filter 实际过滤的是所有 op 类型(i/u/d/c/n)。叫 CmdFilter 容易让人误以为只过滤 command 类型。配置项 filter.cmds 也有同样的问题。

建议:考虑改名为 OpTypeFilter,配置项改为 filter.op_types 或 filter.oplog_ops。如果改名成本太高,至少在结构体和配置项旁加上详细注释说明 "cmds" 此处的含义。

Comment thread cmd/collector/sanitize.go Outdated
"u": {},
"d": {},
"c": {},
"n": {},

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

当前 chain 的顺序是 AutologousFilter → NoopFilter → CmdFilter → GidFilter,CmdFilter 排在 NoopFilter 之后。这意味着 op=n 的 oplog 已经被 NoopFilter 过滤了,CmdFilter 中配置 n 实际上永远不会生效。虽然从功能上无害,但配置 filter.cmds=n 不报错也不生效,会让用户困惑。

建议: 要么在 normalizeFilterCmds 中遇到 n 时打一个 warn 日志提示 "noop is already filtered by NoopFilter",要么从 validFilterCmds 中移除 n。

@SisyphusSQ

Copy link
Copy Markdown
Contributor Author

好的,这些意见我综合下,看下应该怎么修正

@SisyphusSQ

Copy link
Copy Markdown
Contributor Author

后续我想把LOG这个sdk换掉,现在日志的sdk是原来的作者自己开发的,如果能替换成更流行的zap日志sdk。会尽量将当前的日志行为保持一致

@zhongli-james

Copy link
Copy Markdown
Collaborator

好的,这些意见我综合下,看下应该怎么修正

好的,感激不尽。有一些评论是大模型给出的,我看了下也觉得合理就copy过来了。或许还是可以多借助AI的能力来辅助开发。

@zhongli-james

Copy link
Copy Markdown
Collaborator

后续我想把LOG这个sdk换掉,现在日志的sdk是原来的作者自己开发的,如果能替换成更流行的zap日志sdk。会尽量将当前的日志行为保持一致

可以的,其实我之前也有过类似的想法。一些组件的确太古早了(有的都archived了),可以演进到更好更现代化的库。

@SisyphusSQ

Copy link
Copy Markdown
Contributor Author

OK 再review下呢

@zhongli-james zhongli-james merged commit 25fd5a4 into alibaba:develop Mar 31, 2026
1 check passed
@zhongli-james

Copy link
Copy Markdown
Collaborator

再次感谢你的贡献!下次版本release的时候我会更新到 贡献者名单 中~

utils.ReadWriteConcernDefault, utils.ReadWriteConcernDefault, "")
assert.Equal(t, nil, err, "should be equal")

err = conn.Client.Database("zz").Drop(nil)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这里的tset会有点小问题,我处理下哈~

"codeName" : "NamespaceNotFound",
	"errmsg" : "cannot apply insert or update operation on a non-existent namespace zz.y: { ns: \"zz.y\", op: \"i\", o: { _id: \"bson-d-1\", hello: \"worlod\" } }",

zhongli-james added a commit to zhongli-james/MongoShake that referenced this pull request Apr 7, 2026
zhongli-james added a commit to zhongli-james/MongoShake that referenced this pull request Apr 17, 2026
zhongli-james added a commit to zhongli-james/MongoShake that referenced this pull request Apr 17, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants