TTL(Time-To-Live)索引是 MongoDB 提供的一种特殊单字段索引,可在文档过期后自动删除。本文介绍 TTL 索引的工作原理、创建与管理方法、使用限制、常见问题与解决方案,以及在云数据库 MongoDB 版上的最佳实践建议。
概述
TTL(Time-To-Live)索引是 MongoDB 提供的一种特殊单字段索引,能够在文档达到指定过期时间后自动将其删除。它适合管理具有明确生命周期的数据,例如日志记录、会话信息、临时缓存、验证码等场景。通过合理使用 TTL 索引,可以有效控制集合的数据量,避免存储空间无限增长。
在生产环境中,TTL 索引的不当使用可能引发多种问题,包括 CPU 周期性飙高、过期数据未被及时删除、磁盘空间持续增长等。本文将从 TTL 索引的工作原理、使用方法、常见问题及最佳实践等方面进行全面介绍,帮助您在云数据库 MongoDB 版上正确使用 TTL 索引。
TTL 索引的工作原理
基本机制
每个 mongod 进程启动时,会创建一个名为 TTLMonitor 的后台线程,该线程默认每隔 60 秒发起一轮 TTL 清理操作。每轮操作的流程如下:
搜集当前数据库中所有的 TTL 索引。
依次对每个 TTL 索引生成执行计划并执行数据清理。
删除索引字段值加上
expireAfterSeconds小于当前时间的文档。
过期阈值的计算方式为:索引字段的日期值 + expireAfterSeconds 的秒数。如果该结果小于当前时间,则文档被认为已过期。
副本集中的行为
在副本集架构中,TTL 后台线程仅在 Primary 节点上执行删除操作。Secondary 节点的 TTL 线程处于空闲状态,通过复制 Primary 的 oplog 来同步删除操作。这意味着 TTL 删除会产生额外的 oplog,可能影响副本集的同步延迟。
版本差异:批量删除优化(MongoDB 7.0+)
从 MongoDB 7.0 开始(对应开发版本 6.1 引入的特性),TTL 删除操作引入了"公平删除"机制,通过类似"时间片"的方式为每个 TTL 索引分配删除时间,避免某些集合的过期数据长期得不到清理。主要改进包括:
ttlMonitorBatchDeletes:启用批量删除模式,默认开启。开启后 TTL 删除会更加公平地分配到各个集合。
ttlIndexDeleteTargetTimeMS:每个 TTL 索引每轮删除的时间上限,默认 1000 ms。
ttlIndexDeleteTargetDocs:每个 TTL 索引每轮删除的文档数上限,默认 50000。
ttlMonitorSubPassTargetSecs:子轮次遍历 TTL 索引的时间上限,默认 60 秒。
这些参数在执行计划中体现为 BATCHED_DELETE 阶段,可通过 explain 命令查看。
创建和管理 TTL 索引
创建 TTL 索引
使用 createIndex 方法,在日期类型字段上创建 TTL 索引。
示例 1:创建 TTL 索引,文档在 lastModifiedDate 字段值的 3600 秒后过期。
db.eventlog.createIndex(
{ "lastModifiedDate": 1 },
{ expireAfterSeconds: 3600 }
)示例 2:指定时刻过期。将 expireAfterSeconds 设为 0,由字段值直接决定过期时间。
db.sessions.createIndex(
{ "expireAt": 1 },
{ expireAfterSeconds: 0 }
)修改 TTL 索引的过期时间
使用 collMod 命令修改已有 TTL 索引的 expireAfterSeconds 值,无需重建索引。
db.runCommand({
"collMod": "log_events",
"index": {
"keyPattern": { "createdAt": 1 },
"expireAfterSeconds": 600
}
})将普通索引转换为 TTL 索引(MongoDB 6.0+)
从 MongoDB 6.0 开始,可以通过 collMod 命令将已存在的普通单字段索引转换为 TTL 索引,而不需要先删除再重建。
db.runCommand({
"collMod": "tickets",
"index": {
"keyPattern": { "lastModifiedDate": 1 },
"expireAfterSeconds": 100
}
})监控 TTL 运行状态
您可以通过以下命令监控 TTL 的运行情况。
// 查看 TTL 删除的文档总数
db.serverStatus().metrics.ttl.deletedDocuments
// 查看 TTL 线程的运行轮次
db.serverStatus().metrics.ttl.passes
// 查看 TTL 线程的扫描周期(默认 60 秒)
db.runCommand({ getParameter: 1, ttlMonitorSleepSecs: 1 })
// 查看当前正在执行的 TTL 删除操作
db.currentOp()TTL 索引的使用限制
在使用 TTL 索引前,需要了解以下重要限制:
TTL 索引仅支持单字段索引。复合索引无法使用 TTL 功能,即使设置了
expireAfterSeconds也会被忽略。_id 字段不支持 TTL 索引。
索引字段必须为 BSON Date 类型。如果字段值不是 Date 类型(如 UNIX 时间戳整数、字符串等),文档将不会被自动删除。
不包含索引字段的文档不会过期。
如果字段是数组类型,使用数组中最早的日期值来计算过期时间。
不能通过
createIndex修改已有 TTL 索引,必须使用collMod命令。expireAfterSeconds的取值范围为 0 到 2,147,483,647,且不得设为NaN,否则可能导致不可预期的行为和数据丢失。
常见问题与解决方案
问题一:TTL 删除导致 CPU 周期性飙高
问题现象
实例 CPU 使用率出现明显的周期性峰值,每隔固定时间间隔出现一次。通过 db.currentOp() 可以观察到 TTL 线程正在执行大量删除操作。
原因分析
当业务在同一时刻集中插入大量数据,并且这些数据的 TTL 字段值相同或接近时,它们会在同一时间窗口内集中过期。TTL 线程在单次扫描中需要删除大量文档,会导致显著的 CPU 和 IO 压力。
解决方案
打散过期时间:在插入数据时,为 TTL 字段增加随机偏移量,将过期时间分散到一个时间窗口内,避免集中删除。例如,对于 24 小时过期的数据,可在过期时间上加减数分钟的随机偏移。
升级到 MongoDB 7.0 及以上版本:利用批量删除和公平删除机制,平滑删除压力。
选择合适的实例规格:确保实例的 CPU 和内存资源足以应对 TTL 删除带来的额外负载。
示例:为插入的数据添加随机过期偏移。
// 在 24 小时基础上增加 0~600 秒的随机偏移
var randomOffset = Math.floor(Math.random() * 600);
var expireTime = new Date(Date.now() + (86400 + randomOffset) * 1000);
db.logs.insertOne({
data: "log content",
createdAt: expireTime
})问题二:数据过期后未被删除
问题现象
文档已超过预期的过期时间,但仍然存在于集合中。
排查步骤
确认字段类型:检查 TTL 索引字段的值是否为 BSON Date 类型。一个常见错误是使用 UNIX 时间戳整数而非 Date 对象。
// 检查字段类型 db.collection.findOne({}, { createdAt: 1 }) // 正确示例:ISODate("2024-01-01T00:00:00Z") // 错误示例:1704067200 (整数时间戳)确认字段是否存在:查看文档中是否实际包含了 TTL 索引字段。不包含该字段的文档不会被自动删除。
检查索引定义:确认索引确实包含
expireAfterSeconds属性。db.collection.getIndexes()检查索引方向:确保 TTL 索引是正序(值为
1)。倒序索引可能导致 TTL 功能异常。如发现索引方向不正确,删除后重建即可。检查 TTL Monitor 是否开启:确认
ttlMonitorEnabled参数为true。db.adminCommand({ getParameter: 1, ttlMonitorEnabled: 1 })
问题三:TTL 删除速度跟不上数据插入速度
问题现象
集合数据量持续增长,磁盘空间不断膨胀,即使已配置 TTL 索引。
原因分析
TTL 线程是单线程且每 60 秒执行一次。当数据插入的速率远超过 TTL 清理速率时,过期数据会不断累积。此外,如果实例上存在多个 TTL 索引,它们之间是串行执行的,进一步降低清理效率。
解决方案
调整扫描频率:可以通过
ttlMonitorSleepSecs参数缩短扫描间隔,但需注意更高频率会增加系统负载。// 将扫描间隔调整为 10 秒 db.adminCommand({ setParameter: 1, ttlMonitorSleepSecs: 10 })业务层面主动清理:对于数据写入量特别大的场景,建议在业务层实现定时批量删除逻辑,而不是完全依赖 TTL 索引。
考虑使用时间分区集合:对于极高写入场景,可以按时间分集合存储,直接删除整个过期集合,这比 TTL 索引逐条删除文档高效得多。
问题四:删除后磁盘空间未释放
问题现象
TTL 删除了大量文档,但磁盘使用率并未显著下降。
解决方案
MongoDB 使用 WiredTiger 存储引擎,删除文档后空间会被标记为可重用,但不会立即返还给操作系统。如果需要立即回收磁盘空间,可以执行 compact 命令或通过初始化同步来重新构建数据文件。建议在业务低峰期进行 compact 操作。
最佳实践建议
一、确保数据类型正确
TTL 索引仅对 BSON Date 类型的字段生效。建议在应用层或通过 MongoDB 的 Schema Validation 功能强制确保 TTL 字段为正确的日期类型。
db.createCollection("sessions", {
validator: {
$jsonSchema: {
properties: {
expireAt: { bsonType: "date", description: "TTL 字段必须为 Date 类型" }
},
required: ["expireAt"]
}
}
})二、分散过期时间,避免集中删除
这是避免 TTL 删除导致 CPU 飙高的最有效方法。在插入数据时,为过期时间添加随机偏移量,使同一批数据的过期时间分散到一个时间范围内。这样 TTL 线程每次扫描时只需要删除少量文档,显著降低对系统性能的影响。
对于保留时间较长的数据(如 24 小时以上),建议在过期时间上添加 0 到 10 分钟的随机偏移。
三、谨慎调整 expireAfterSeconds
降低已有 TTL 索引的 expireAfterSeconds 值会导致大量已有文档立即过期,可能触发大规模删除操作,严重影响实例性能。建议的做法是:
先估算调整后会有多少文档立即过期。
在业务低峰期操作。
如果过期文档数量巨大,建议先通过业务脚本分批删除部分数据,再修改
expireAfterSeconds。
四、新建 TTL 索引时的注意事项
在一个已经包含大量符合过期条件的文档的集合上创建 TTL 索引,可能导致索引建立完成后立即触发大规模删除。建议:
在业务低峰期创建 TTL 索引。
先清理历史过期数据,再创建 TTL 索引。
使用后台方式建索引,避免影响正常业务。
五、不要长期关闭 TTL Monitor
虽然可以通过 ttlMonitorEnabled 参数临时关闭 TTL 功能,但不建议在生产环境长期使用。关闭期间累积的大量过期文档会在重新开启时集中删除,可能导致严重的性能问题。
临时关闭 TTL Monitor 后,务必及时重新开启,避免过期数据大量堆积。
六、合理评估 TTL 对 oplog 的影响
TTL 删除的每一条文档都会生成一条 oplog。当删除量较大时,会显著增加 oplog 的写入量,可能影响副本集的同步延迟。建议通过以下方式评估影响:
监控
metrics.ttl.deletedDocuments,了解 TTL 删除量。监控副本集同步延迟,确保 Secondary 能处理 TTL 产生的额外 oplog。
必要时调大 oplog 大小,以避免因 TTL 删除导致 oplog 溢出。
七、高写入场景的替代方案
TTL 索引适用于中等写入量的场景。对于极高写入量的场景,建议考虑以下替代方案:
时间分区集合:按时间维度拆分集合(如按天、按周),直接 drop 过期集合,这是最高效的数据过期方式。
时间序列集合:MongoDB 5.0+ 支持时间序列集合,内置数据过期功能,按桶批量删除,效率远高于普通集合的逐条删除。
业务层定时清理:通过定时任务在业务低峰期执行批量删除,可以更精细地控制删除速率和时机。
各版本 TTL 特性参考
版本 | 特性说明 |
6.0+ | 支持通过 |
7.0+ | 引入批量删除机制(BATCHED_DELETE),提升删除效率;实现公平删除,避免某些集合被"饿死";支持时间序列集合的部分 TTL 索引,支持更灵活的 |
TTL 相关参数速查
参数名 | 说明 | 默认值 |
| TTL 线程扫描间隔。 | 60 秒 |
| TTL 功能开关。 | true |
| 批量删除模式(7.0+)。 | true |
| 每轮删除时间上限(7.0+)。 | 1000 ms |
| 每轮删除文档数上限(7.0+)。 | 50000 |
| 子轮遍历时间上限(7.0+)。 | 60 秒 |
总结
TTL 索引是 MongoDB 中一个强大且实用的自动数据过期机制,能够有效地管理具有明确生命周期的数据。在生产环境中,需要结合业务场景合理配置,以避免性能问题。以下是核心要点:
确保 TTL 字段类型为 BSON Date,这是 TTL 索引正常工作的前提。
打散过期时间是避免 CPU 毛刺的最有效手段。
持续监控 TTL 的运行状态(删除量、扫描轮次、副本集同步延迟)。
对于高写入场景,考虑使用时间分区集合或时间序列集合等替代方案。
升级到较新版本(如 7.0+)可获得更好的 TTL 删除性能和公平性。