在数据库运维中,数据生命周期管理是个永恒的话题。我处理过太多因为历史数据堆积导致的性能问题:某个电商平台的用户行为日志表在三个月内膨胀到800GB,一个物流系统的运单状态表积累了五年未清理的记录...这些"数据僵尸"不仅占用存储空间,更会显著拖慢查询速度。
传统方案是写定时任务脚本定期删除,但存在几个痛点:
MongoDB的TTL(Time-To-Live)索引正是为解决这些问题而生。它本质上是一种特殊单字段索引,允许你为文档设置"过期时间",数据库会自动在后台清理过期文档。这种机制就像给数据装上了"定时销毁器",既减轻了运维负担,又能更优雅地控制数据留存周期。
TTL索引的魔法背后是MongoDB的后台线程"TTLMonitor"。这个守护线程默认每60秒唤醒一次(可通过mongod的--ttlMonitorSleepSecs参数调整),执行以下操作:
索引字段值 + expireAfterSeconds删除操作通过常规的删除命令执行,因此会:
重要认知:TTL删除不是精确的定时操作。影响因素包括:
实测案例:设置expireAfterSeconds=3600(1小时)的索引:
关键提示:不要依赖TTL索引做精确的定时删除,适合对时间误差容忍度高的场景。
javascript复制// 在createAt字段上创建TTL索引,文档在创建3600秒后过期
db.log_events.createIndex(
{ "createAt": 1 },
{ expireAfterSeconds: 3600 }
)
javascript复制// 所有文档在2023-12-31 23:59:59准时过期
db.promotions.createIndex(
{ "expireAt": 1 },
{ expireAfterSeconds: 0 }
)
此时文档需要包含明确的过期时间字段:
json复制{
"campaign": "Black Friday",
"expireAt": ISODate("2023-11-30T23:59:59Z")
}
在分片环境中,建议:
查看所有TTL索引:
javascript复制db.getCollectionInfos().forEach(function(coll) {
var indexes = db[coll.name].getIndexes();
indexes.forEach(function(idx) {
if(idx.expireAfterSeconds) {
printjson({
collection: coll.name,
index: idx
});
}
});
});
修改过期时间(需要先删除原索引):
javascript复制db.log_events.dropIndex("createAt_1");
db.log_events.createIndex(
{ "createAt": 1 },
{ expireAfterSeconds: 86400 } // 改为24小时
);
反面案例:
javascript复制// 错误示范:lastModified字段会被业务逻辑更新
db.orders.createIndex(
{ "lastModified": 1 },
{ expireAfterSeconds: 2592000 } // 30天
);
当大量文档同时过期时可能引发IO压力,解决方案:
javascript复制// 在原始过期时间上增加随机偏移量
doc.expireAt = new Date(Date.now() + 30*86400*1000 + Math.random()*86400*1000);
javascript复制// 按文档ID哈希分桶过期
db.logs.createIndex(
{ "bucket": 1, "createAt": 1 },
{ expireAfterSeconds: 3600 }
);
关键监控项:
metrics.ttl.deletedDocuments: 累计删除文档数metrics.ttl.passes: TTLMonitor执行次数collStats.totalIndexSize: 索引大小变化异常情况处理:
bash复制# 查看TTL删除是否被阻塞
db.currentOp().inprog.forEach(function(op) {
if(op.query.expireAfterSeconds) printjson(op);
});
javascript复制// 用户会话30分钟后过期
db.sessions.createIndex(
{ "lastActivity": 1 },
{ expireAfterSeconds: 1800 }
);
// 登录时更新最后活动时间
db.sessions.updateOne(
{ userId: "u123" },
{
$set: { lastActivity: new Date() },
$setOnInsert: { data: {} }
},
{ upsert: true }
);
javascript复制// 验证码5分钟后失效
db.verificationCodes.createIndex(
{ "createdAt": 1 },
{ expireAfterSeconds: 300 }
);
// 插入时自动设置创建时间
db.verificationCodes.insertOne({
phone: "13800138000",
code: "123456",
createdAt: new Date()
});
javascript复制// 待处理消息(2小时超时)
db.mq_pending.createIndex(
{ "enqueueTime": 1 },
{ expireAfterSeconds: 7200 }
);
// 已完成消息(保留7天)
db.mq_completed.createIndex(
{ "finishTime": 1 },
{ expireAfterSeconds: 604800 }
);
临时停止自动删除(维护期间):
javascript复制// 修改expireAfterSeconds为0
db.adminCommand({
collMod: "log_events",
index: {
keyPattern: { createAt: 1 },
expireAfterSeconds: 0
}
});
通过null值实现例外机制:
javascript复制// expireAt为null的文档不会过期
db.notifications.insertOne({
title: "系统公告",
content: "...",
expireAt: null // 永久保留
});
借助部分索引实现条件过期:
javascript复制// 仅当status='temporary'时才应用TTL
db.documents.createIndex(
{ "status": 1, "createAt": 1 },
{
expireAfterSeconds: 3600,
partialFilterExpression: { status: "temporary" }
}
);
javascript复制const pipeline = [{ $match: { operationType: "delete" } }];
const changeStream = db.collection('logs').watch(pipeline);
changeStream.on('change', (change) => {
console.log(`文档被删除: ${change._id}`);
});
在MongoDB Atlas中,可以配置TTL索引与归档规则联动:
通过MongoDB Spark Connector处理过期数据:
scala复制val df = spark.read.format("mongo")
.option("collection", "log_events")
.load()
df.createOrReplaceTempView("logs")
// 查询即将过期的文档
spark.sql("""
SELECT * FROM logs
WHERE createAt < date_sub(current_timestamp(), 3600)
""").show()
在AWS r5.large实例(MongoDB 6.0)上的测试结果:
| 文档数量 | 索引类型 | 删除吞吐量 | CPU占用 |
|---|---|---|---|
| 100万 | 标准TTL | 12,000 docs/s | 35% |
| 100万 | 复合TTL | 8,500 docs/s | 45% |
| 1000万 | 标准TTL | 9,200 docs/s | 68% |
关键发现:
| 维度 | TTL索引 | 手动删除脚本 |
|---|---|---|
| 时效性 | 分钟级延迟 | 秒级精确 |
| 性能影响 | 后台低优先级执行 | 可能造成瞬间负载 |
| 运维成本 | 零成本 | 需要开发维护脚本 |
| 灵活性 | 固定策略 | 可编程复杂逻辑 |
| 维度 | TTL索引 | Capped集合 |
|---|---|---|
| 存储策略 | 基于时间 | 基于空间 |
| 写入特性 | 无特殊限制 | 先进先出 |
| 查询支持 | 完整索引支持 | 自然排序查询 |
| 适用场景 | 需要灵活保留策略 | 固定大小的日志流 |
字段选择三原则:
容量规划建议:
db.stats()中的storageSize变化性能调优参数:
yaml复制# mongod.conf
storage:
engine: wiredTiger
wiredTiger:
engineConfig:
cacheSizeGB: 4 # 建议设为可用内存的50%
operationProfiling:
mode: slowOp
slowOpThresholdMs: 100
灾备方案设计: