MongoDB C# Driver 投影时为何反复编译属性 setter,导致 CPU 升高?
排查高 CPU 时,要确认每次查询是否都会创建新的投影映射和 serializer,并因此重复编译属性 setter。注册 DTO 的 BsonClassMap,不能单凭此认定 LINQ 投影会复用该映射。现有材料也不足以认定这是 MongoDB C# Driver 3.7.1 的已确认缺陷。
一种可以先验证的绕行方式是,在服务器端只选择需要的字段,用原实体类型接收结果,再在内存中构造 DTO。
为什么 DTO 相同,仍可能重复编译?
属性 setter 委托的缓存取决于具体的 BsonMemberMap 实例。公开的驱动 main 分支源码中,BsonMemberMap.Setter 会检查实例字段 _setter;字段为空时,它会调用 GetPropertySetter(),后者通过 LambdaExpression.Compile() 创建委托。这表明缓存依附于具体实例,并非仅凭 DTO 类型名称全局共享。这里引用的是 main 分支的实现,不能直接作为 3.7.1 行为已核实的证据。BsonMemberMap 源码
如果实际运行的投影翻译路径每次都创建新的映射,即使成员完全相同,新实例仍可能需要生成 setter。这个原因假设需要通过调用栈和对象实例来验证。
还要区分表达式编译与 JIT:反复生成动态方法后进行 JIT,与同一个已有方法被反复编译,是两种不同的情况。单看 JIT CPU 热点,无法确定是哪一种。
先区分首次初始化与持续重复编译
建议按以下顺序检查:
- 在同一进程中先串行执行查询预热,再记录后续查询的 CPU 和
GetPropertySetter()调用次数。 - 在断点或 profiler 中检查对应的
MemberInfo、所属BsonClassMap和实例身份,确认热点成员属于 DTO 还是原实体。 - 向上追踪映射的创建位置,判断是否每次执行查询都会创建新的投影 serializer。
- 预热后进行并发测试,比较编译次数是否随请求数持续增长。
如果开销只出现在首次访问,预热可能有效;如果编译次数在预热后仍随请求增加,应继续定位重复构建路径。
示例中的 for 循环每次都等待 ToListAsync() 完成,因此是串行执行。它可以用来检查重复查询的开销,但不能单凭它证明高并发时的行为。
用字段投影加本地 DTO 映射做对照
当前需求只复制两个字段,可以先避开查询表达式中的 new GetDbCodesRespDto:
using System.Linq;
using MongoDB.Driver;
// 复用已经初始化的 collection。
var filter = Builders<DynamicApplication>.Filter.Eq(
p => p.Id,
"003566cd-3a51-4a24-9842-c32b01db0600");
var projection = Builders<DynamicApplication>.Projection
.Include(p => p.InternalId)
.Include(p => p.DbCode);
var rows = await collection
.Find(filter)
.Project<DynamicApplication>(projection)
.ToListAsync();
var result = rows.Select(p => new GetDbCodesRespDto
{
AppId = p.InternalId,
DbCode = p.DbCode
}).ToList();
Include 用于限制服务器返回的字段,Project 用于应用投影;这是驱动支持的字段投影方式。MongoDB 字段投影文档
查询完成后,再使用 LINQ to Objects 构造 DTO,其属性由普通 C# 代码赋值。可以用这个改法做对照实验,但在没有实测数据时,不能保证 CPU 会降低多少。
使用前需要检查:
DynamicApplication是否允许只反序列化部分字段;必填成员、自定义 serializer 或构造函数约束可能使该方案不适用。- 未投影的属性可能保持默认值,不应将这些对象当作完整实体继续处理或写回数据库。
- 普通字段投影默认可能包含
_id;如果不需要,可以根据实际映射显式排除。
已注册映射后,还需要做什么?
保留启动阶段的一次性 BsonClassMap 注册,并复用 MongoClient。原示例已经把注册和客户端创建放在循环外,无须把它们描述为已发现的错误。
只把表达式保存到静态字段、把查询对象移出循环,或改成 Find().Project(p => new GetDbCodesRespDto { ... }),都不能单独保证消除重复编译。复用表达式对象,并不等于复用投影翻译结果。
如果确认新的映射实例持续产生,应整理最小复现,补充两个类型的定义、BSON 特性、约定配置、.NET 运行时版本和完整热点调用栈,再向驱动项目报告。比较其他驱动版本时,应使用相同数据、预热方式和负载进行验证;现有材料没有证明某个版本已经修复该问题。
备注:内容仅供参考。
