数据库删除数据慢,这事儿在 InfluxDB 上特别常见。很多团队用着用着发现,一条 delete 语句下去,整个查询变得卡顿,磁盘 I/O 飙升,明明只是删点旧数据,却感觉像在跟数据库打架。这篇文章就带你一步步理清为什么批量删除这么慢,以及有什么实用的优化手段。

一、先看懂 InfluxDB 的存储和删除

InfluxDB 是专门为时序数据设计的,它把数据按照时间排好序,然后放进一堆叫 TSM 的文件里。你可以把 TSM 文件理解成一本本厚厚的笔记本,每一本里都按时间顺序写着数据。写入的时候,新数据可以直接追加到新的笔记本里,不需要改动旧的本子,所以写入非常快。

但删除就不一样了。InfluxDB 不会撕掉笔记本里的某一页,而是在旁边又放一本“死亡笔记”,也就是 tombstone 文件,上面写着“哪些本子、哪些时间范围的数据已经废了”。等以后有人来查询,InfluxDB 会瞄一眼死亡笔记,跳过那些废数据。这个过程在技术上叫“删除标记”。

这种设计有个好处:删除操作本身不碰原始文件,所以不会像传统数据库那样导致文件碎片。但坏处也很明显,如果删除范围很大,死亡笔记会变得很厚,查询时需要额外检查这些标记,速度自然就下来了。

InfluxDB 2.x 里,用户通常只接触到 bucket,而 bucket 下面才是 measurement。实际上,数据仍然存储在 TSM 文件里,只是逻辑上被分到了不同的 bucket。删除操作发生在 bucket 内,所以 delete API 必须传入 bucket 参数。

1.1 Tombstone 与 Compaction 的分工

在 InfluxDB 里,tombstone 只是临时的“声明”。真正把这些废数据从文件里抹掉,靠的是后台的 Compaction(压缩)任务。Compaction 会把多个 TSM 文件合并成一个,同时把带删除标记的数据块过滤掉,生成新的干净文件,再删掉旧文件。

所以一次删除请求发出后,数据并不会马上从磁盘消失。如果你在删除后立刻查看磁盘占用,可能一点变化都没有。只有等 Compaction 跑完,空间才会被释放。

二、批量删除慢的连锁反应

为什么批量删除会引发连锁反应?我们先分解一下过程。

第一,InfluxDB 收到删除请求后,必须找到所有可能包含目标数据的 TSM 文件。如果删除的时间范围跨几个星期,或者 predicate 条件很宽,需要检查的文件数量就非常大。第二,找到文件后,还要逐个数据块比对时间范围和标签条件,这个过程需要大量 CPU 和磁盘读取。第三,为这些文件生成 tombstone 后,后续每一次查询都要带着这些标记去读文件,造成查询性能下降。第四,Compaction 在清理这些 tombstone 时,要把整个文件重写一遍,虽然删掉了数据,但写入了大量新数据,造成“写放大”。

举个例子:你删了 10GB 数据,Compaction 可能会重写 30GB 的文件内容。如果磁盘本身已经压力很大,删除操作就会让整个系统雪上加霜。

再举一个实际场景:比如你有一个传感器表,每天写入几千万条记录,运行一个月后表里就有几十亿条点。这时你只想保留最近一周的数据,如果不做任何优化,直接 delete 掉其余数据,InfluxDB 可能要把过去一个月的 TSM 文件全部翻一遍,并且为它们全部打上删除标记。这一个操作可能持续几十分钟,期间查询响应时间直线上升。

应用场景很广,比如清理过期监控数据、删除测试数据、调整数据保留周期。在这些场景里,如果批量删除没处理好,就会直接影响线上服务的稳定性。

三、用 Python 亲手试一次批量删除

以 InfluxDB 2.x 为例,下面代码全部使用 Python 3 和官方 influxdb-client 库。我们来看看代码层面到底发生了什么。这段代码会删除一个月内的所有 sensor 测量值。

# 技术栈:Python 3 + influxdb-client
from influxdb_client import InfluxDBClient
from datetime import datetime

# 连接 InfluxDB 服务
client = InfluxDBClient(url="http://localhost:8086", token="my-token", org="my-org")

# 删除操作走 delete_api
delete_api = client.delete_api()

# 定义删除范围:2024 年 1 月 1 日 0 点 到 2024 年 2 月 1 日 0 点
start = datetime(2024, 1, 1)
stop = datetime(2024, 2, 1)

# 只限定 measurement=sensor,标签不限制
delete_api.delete(
    start=str(start),
    stop=str(stop),
    predicate='_measurement="sensor"',
    bucket="my-bucket",
    org="my-org"
)

print("删除请求已发出")

这段代码运行起来,你可能以为只是发了一个 HTTP 请求。其实 InfluxDB 在服务端要做的工作非常多。特别是当你的测量表横跨几十个 TSM 文件时,任何一个文件里只要包含这个时间范围内的数据,都要被扫描一遍。而且如果这个月的 TSM 文件没有按天切分,删除过程可能要扫描多个 shard group,那时间会成倍增加。

四、优化策略实战

下面这些策略并不互相排斥,你可以像搭积木一样组合使用。我根据自己的实践按推荐程度排了个序,你可以从里面挑一两个落地。

4.1 化整为零:小批量分时删除

最直接的办法就是把大范围拆成小范围。比如一次删一个月,可以改成一次删一天,甚至一次删一小时。小范围的 tombstone 文件更小,查询受到的影响更小,Compaction 也能更快消化。

# 技术栈:Python 3 + influxdb-client
from influxdb_client import InfluxDBClient
from datetime import datetime, timedelta
import time

client = InfluxDBClient(url="http://localhost:8086", token="my-token", org="my-org")
delete_api = client.delete_api()

# 要删除 2024 年 1 月整月的数据
current = datetime(2024, 1, 1)
end = datetime(2024, 2, 1)

while current < end:
    # 每次只处理一天
    next_day = current + timedelta(days=1)

    # 删除这一天的数据,同时带上标签过滤,让范围更精确
    delete_api.delete(
        start=str(current),
        stop=str(next_day),
        predicate='_measurement="sensor" AND location="room1"',
        bucket="my-bucket",
        org="my-org"
    )

    print(f"已删除 {current} 到 {next_day} 的数据")
    time.sleep(2)  # 暂停两秒,避免频繁请求压垮数据库
    current = next_day

适用场景:删除范围很长、数据量很大的情况。优点是可以随时中断、失败后只需要重跑某一段。缺点是总耗时会拉长,所以最好在业务低峰期执行。

4.2 该扔就扔:用 DROP MEASUREMENT 代替 DELETE

如果你要删除整个 measurement,那完全不用走 delete 的流程。DROP MEASUREMENT 会直接把相关的数据文件和索引都清理掉,没有 tombstone,也不需要 Compaction 再打扫一遍。

# 技术栈:Python 3 + influxdb-client
from influxdb_client import InfluxDBClient

client = InfluxDBClient(url="http://localhost:8086", token="my-token", org="my-org")
query_api = client.query_api()

# 删除整个 measurement 的全部数据
query_api.query('DROP MEASUREMENT sensor')

print("整个 measurement 已删除")

这里要强调一下:这个操作是毁灭性的,执行前一定要确认测量表里的数据已经没有保留价值。它没有数据恢复的可能。优点是极快,缺点是不能按条件删,只能整个表一起干掉。

4.3 一劳永逸:用保留策略自动清理

很多时候我们都是要删除过期数据,那不如直接给数据库上发条,让它自己按时间自动清理。InfluxDB 的 Bucket 可以设置保留周期,数据超过一定天数后自动过期。

# 技术栈:Python 3 + influxdb-client
from influxdb_client import InfluxDBClient, BucketRetentionRules

client = InfluxDBClient(url="http://localhost:8086", token="my-token", org="my-org")
bucket_api = client.buckets_api()

# 遍历所有 bucket,找到名为 my-bucket 的一个
for bucket in bucket_api.find_buckets().buckets:
    if bucket.name == "my-bucket":
        # 设置保留 30 天,30 天前的数据会自动删除
        bucket.retention_rules = [
            BucketRetentionRules(type="expire", every_seconds=30 * 24 * 3600)
        ]
        bucket_api.update_bucket(bucket)
        print("保留策略已更新")
        break

这种方式的优点是省心、对系统影响小,因为 InfluxDB 是按分片来清理过期数据的。缺点是你没法按标签删,只能按时间整体淘汰。如果你的数据需要区分不同来源分别保留,就得用其他方式。

4.4 错峰执行:定时任务在深夜删

如果手动删除不可避免,那至少别选在白天的业务高峰期跑。写一个定时任务,每天凌晨三点清理指定时间段的数据,既能保证数据不过期,又能把对业务的影响降到最低。

# 技术栈:Python 3 + influxdb-client + schedule
import schedule
import time
from datetime import datetime, timedelta
from influxdb_client import InfluxDBClient

client = InfluxDBClient(url="http://localhost:8086", token="my-token", org="my-org")
delete_api = client.delete_api()

def clean_old_data():
    # 计算要清理的时间段:三天前到四天前的那一天
    now = datetime.now()
    start = now - timedelta(days=4)
    end = now - timedelta(days=3)
    delete_api.delete(
        start=str(start),
        stop=str(end),
        predicate='_measurement="sensor"',
        bucket="my-bucket",
        org="my-org"
    )
    print(f"清理完成:{start} 到 {end}")

# 每天凌晨 3 点执行清理
schedule.every().day.at("03:00").do(clean_old_data)

while True:
    schedule.run_pending()
    time.sleep(60)

这个方案的缺点是,如果凌晨的任务因为某些原因失败了,数据就会堆积。所以最好加一些重试或监控告警,保证清理任务能成功执行。

五、替代方案:绕开删除操作

有时候,与其纠结怎么高效删除,不如干脆不删,用“换库换桶”的方式。

5.1 直接删桶

如果你有多个 bucket 分别存储不同阶段的数据,那到了生命周期结束的时候,可以直接把整个 bucket 删掉。这个操作不需要遍历文件、不需要写 tombstone,速度非常快。

# 技术栈:Python 3 + influxdb-client
from influxdb_client import InfluxDBClient

client = InfluxDBClient(url="http://localhost:8086", token="my-token", org="my-org")
bucket_api = client.buckets_api()

# 删除名为 old-bucket 的整个存储桶,所有数据立刻消失
bucket_api.delete_bucket("old-bucket")

print("存储桶已删除")

这种方案的前提是,你已经把需要的数据迁移到了新桶,并且应用已经切换过去。如果数据不重要,那更好,直接删,省心又省力。

5.2 按分片规划生命周期

InfluxDB 默认按时间把数据切分到不同的分片(shard)里。分片过了保留期后,会被整片删除,不会留下 tombstone。如果你在设计数据存储时,让数据的生命周期和分片大小对齐,那“删除”就变成了 InfluxDB 的自动行为,你完全不需要操心。

比如你可以把保留周期设置为 7 天,InfluxDB 就会把数据切成一天一个分片,过期后逐个删除。这种做法的查询性能也不错,因为分片粒度小,索引也更紧凑。

我简单总结一下不同方案的优缺点:小批量删除最灵活,但总耗时长;DROP MEASUREMENT 最快,但不能按条件删;保留策略最省心,但只能整体过期;定时删除能错峰,但需要额外维护;删桶最彻底,适合快速抛弃旧数据。你可以在实际工作中根据数据量大小和业务容忍度来选择。

六、注意事项

这里要重点提醒几个坑。

  1. 删除操作不可恢复。所有 delete、drop、删桶操作执行前,都要确认备份是否到位。
  2. Tombstone 不会立即释放磁盘空间。删除后要等 Compaction 完成,磁盘占用才会下降。
  3. 大规模删除会带来写放大,导致磁盘 I/O 飙升,可能拖慢正常写入和查询。
  4. 删除后查询性能会暂时下降,直到 Compaction 处理完。
  5. 避免在业务高峰期做删除,尽量安排在凌晨。
  6. 不要并发执行多个删除任务,InfluxDB 内部会对删除做锁控制,并发反而更慢。
  7. 不同版本行为有差异。InfluxDB 1.x 的 DELETE 和 2.x 的 delete API 在细节上不一样,使用前先看文档。
  8. 观察数据库的日志和监控面板,重点关注 compaction 相关指标,如果发现堆积,要及时降低删除频率。

七、总结

说到底,InfluxDB 的批量删除慢,不是哪一行代码的问题,而是它的存储引擎设计决定了删除是一个“重操作”。TSM 文件加 tombstone 的机制,让写操作特别快,但让删除变成了先标记、后清理的异步过程。这个过程中,查询性能会下降,磁盘写入会放大。

优化的大方向就是三个:能自动过期就不手动删;能整表删就不带条件删;要手动删就切成小段、错峰执行。如果数据量实在太庞大,那就用换桶、换库的方式来绕开删除。希望这些来自实战的优化策略,能让你在维护 InfluxDB 时少一些焦虑,多一份从容。