告别关系型数据库:百万日活社区监控架构选型复盘
我们去年接手了一个日活 120 万左右的社区 APP 的后端重构任务。当时最让我头疼的不是业务代码,而是监控系统的存储层。原来的架构用的是 MySQL 5.7,把接口 QPS、用户在线数、帖子发布量这些指标按分钟粒度往一张大表里写。
一开始数据量小,大家也没在意。等到日活突破 80 万,这张表每天新增近 200 万行记录。某天早上 9 点,运营同学打开后台看昨天的活跃用户趋势,页面转了 15 秒还没出来。我登上服务器一看,慢查询日志里全是 SELECT AVG(value) FROM metrics WHERE type = 'qps' AND time > '2024-03-01',MySQL 在 2 亿行数据上做范围扫描,CPU 直接干到 90%。
那时候我才真正意识到,关系型数据库设计出来就不是干这个的。它擅长的是事务和关联查询,而监控数据有几个非常典型的特征:数据只追加、几乎不更新、按时间顺序写入、查询往往是对一段时间内的聚合。用 MySQL 存,就像把流水账记在账本里,每次想算个月度平均值,都得把整本账翻一遍。
我们当时对比了几个方案。Prometheus 确实火,但它是拉模型,而且存储层对长期历史数据支持不够,我们有些运营报表需要查过去 90 天的趋势,Prometheus 的本地 TSDB 撑不住。TimescaleDB 是基于 PostgreSQL 的扩展,功能很强,但运维复杂度偏高,我们团队当时人手不够,不想再维护一套 PostgreSQL 集群。
最终选了 InfluxDB 2.7。原因很实际:第一,它是专门为时序数据设计的,写入和存储压缩比我们预期的要好很多;第二,我们当时用的 Telegraf 采集器可以直接对接,改造成本低;第三,InfluxDB 2.x 的 UI 自带 Dashboard 和告警,能先顶一阵子,不用立刻搭 Grafana。
迁移过程里有个细节我印象很深。原来 MySQL 表里有 metric_type、value、create_time 三个核心字段。我一开始想当然地在 InfluxDB 里建了一个 measurement 叫 metrics,把 metric_type 当成 tag。结果写入一周后,tag cardinality 爆炸——因为 metric_type 里混入了类似 user_id_123456 这种高基数的数据。InfluxDB 的 tag 是高基数敏感的,一旦 tag 值太多,内存占用会急剧上升。那次线上 InfluxDB 进程被 OOM Killer 干掉了两次,我才明白:tag 必须选那些低基数、用于分组的字段,比如 host、region、env;而像具体数值或高唯一性 ID,必须放在 field 里。
后来我们重新设计 schema,把真正的指标类型(如 cpu_usage、order_qps)作为 measurement 名,环境信息作为 tag,数值作为 field,问题才彻底解决。同样的数据量,写入 QPS 稳定在 3 万左右,查询 7 天内的聚合数据基本在 200ms 内返回。
这里给一个我们当时验证 schema 的示例,用的是 InfluxDB 2.7 的 Line Protocol 写入方式,通过 Python 客户端实现:
from influxdb_client import InfluxDBClient, Point
from influxdb_client.client.write_api import SYNCHRONOUS
import time
# 实际项目中这些配置放在环境变量里
client = InfluxDBClient(
url="http://localhost:8086",
token="your-influxdb-token",
org="community-app"
)
write_api = client.write_api(write_options=SYNCHRONOUS)
# 模拟一次接口 QPS 上报
point = Point("api_qps") \
.tag("env", "prod") \
.tag("region", "cn-beijing") \
.tag("endpoint", "/api/post/list") \
.field("value", 1420.5) \
.time(time.time_ns())
write_api.write(bucket="monitor-30d", record=point)
这个结构在后来支撑了我们整个社区的实时监控。直到今年我们开始评估升级到 InfluxDB 3.0,因为社区提到它基于 Apache Arrow 和 DataFusion 的新引擎在分析查询上会有明显提升,这也是后面章节要展开的内容。
---
实战案例:电商大促下订单流量监控系统的落地实践
去年双 11,我们负责一个电商平台的订单核心链路监控。这个系统平时订单量在每秒 300 单左右,但大促开场前 10 分钟会瞬间冲到每秒 1.2 万单。我们必须在 5 秒内发现订单下跌、支付失败率飙升这些异常,否则损失就是实打实的金额。
最开始我们用的是 ELK 架构,把订单事件往 Elasticsearch 里写,再用 Kibana 做图。听起来合理,但实际跑起来问题很多。Elasticsearch 对时序数据的压缩不够友好,同样存 1 小时的订单指标,ES 占用的磁盘是 InfluxDB 的 3 倍多。更麻烦的是查询延迟,有一次大促预热,运营同学想看最近 30 分钟的订单趋势,ES 的聚合查询花了 4 秒多,Dashboard 几乎不可用。
后来我们决定把实时指标层切到 InfluxDB 2.7。设计目标很明确:写入不能丢数据、查询要在 200ms 内、系统能扛住 3 倍于平时的流量峰值。
我们当时采集了几个关键指标:
- 每秒订单创建数(order_create_qps)
- 每秒支付成功数(order_pay_qps)
- 订单从创建到支付的平均耗时(order_pay_latency_avg)
- 各渠道订单占比(channel 维度)
这里有一个非常具体的教训。最初我把 order_id 作为 tag 往 InfluxDB 里写,心想这样以后可以按订单查。结果大促压测时,InfluxDB 内存一路涨到 12GB,写入延迟从 10ms 变成 800ms。排查下来发现,order_id 是绝对的高基数,每秒几万个不同值,InfluxDB 的索引机制会为每个 tag 值维护内存索引,直接把服务拖垮。从那以后我定了个规矩:InfluxDB 的 tag 只放“用来分组的维度”,比如 channel、region、env;每个订单的唯一标识,要么不存,要么放 field 里。
最终我们用的 schema 是这样的:
measurement: order_metrics
tags: channel, env, region
fields: create_qps, pay_qps, pay_latency
timestamp: 秒级或毫秒级
写入端我们用 Telegraf 做了一层聚合。订单服务不直接写 InfluxDB,而是先往 Kafka 发原始事件,Telegraf 消费 Kafka 后做 10 秒窗口的聚合,再批量写入 InfluxDB。这样写入 QPS 从 1.2 万降到了 120 左右,InfluxDB 的压力小了很多。
下面是我们 Telegraf 配置里的一段真实片段,用来说明怎么做聚合后写入:
[[inputs.kafka_consumer]]
brokers = ["kafka-01:9092", "kafka-02:9092"]
topics = ["order_events"]
data_format = "json"
[[aggregators.basicstats]]
period = "10s"
drop_original = true
fields = ["create_qps", "pay_qps"]
[[outputs.influxdb_v2]]
urls = ["http://influxdb-prod:8086"]
token = "your-token"
organization = "ecommerce"
bucket = "order-monitor-7d"
这个架构在大促当天表现很稳。订单流量从 300 QPS 涨到 1.2 万 QPS 的过程中,InfluxDB 的写入延迟始终在 50ms 以下,查询最近 1 小时的聚合数据基本在 150ms 左右返回。
后来我们还用上了 InfluxDB 2.x 的 连续查询(CQ) 做自动降采样。原始数据保留 7 天,超过 7 天的自动聚合成 1 分钟粒度存到长期 bucket 里。这样既保住了实时精度,又控制了存储成本。这个需求如果用 MySQL 或 ES 做,要么写一堆定时任务,要么存储膨胀到无法接受。
今年我们也在关注 InfluxDB 3.0 的进展。它提到会增强实时流处理能力,如果未来能把这种降采样逻辑直接下沉到引擎层,而不是靠 CQ 任务去跑,那架构还能再简化一层。
---
性能硬核对比:InfluxDB 2.7 vs 3.0 写入与查询压测实录
今年初我们内部做了一次很实在的对比测试。背景是我们的监控平台准备升级,到底继续用 InfluxDB 2.7 还是迁移到 3.0 早期版本,需要拿数据说话。
测试环境是 3 台 8 核 16GB 的云服务器,数据盘是 SSD。InfluxDB 2.7 用的是官方最新稳定版 2.7.6,InfluxDB 3.0 用的是基于 Apache Arrow 和 DataFusion 引擎的预览构建版(当时是 2024 年 3 月的内部测试镜像)。
我们模拟的是一个典型的 IoT + 监控混合场景:
- 写入:100 个“设备”,每个设备每 100ms 上报一次数据,包含 5 个指标字段
- 数据总量:单节点约 2 亿条数据点
- 查询:分别测试 1 小时、24 小时、7 天时间窗口的聚合查询
先说写入。InfluxDB 2.7 在批量写入(batch size 5000)下,单节点稳定写入 QPS 在 28 万左右,CPU 占用约 65%。InfluxDB 3.0 的写入表现略高,稳定在 32 万 QPS 左右,CPU 占用反而低了 10 个百分点。我分析下来,3.0 的 Arrow 内存格式在批量写入时的序列化开销更小,这是架构层面的优势。
但真正让我意外的是查询性能。我们用一个典型的 SQL 查询来对比:
SELECT
date_bin(interval '10 minutes', time) as t,
avg(cpu_usage),
avg(memory_usage)
FROM device_metrics
WHERE time >= now() - interval '24 hours'
AND region = 'cn-beijing'
GROUP BY t
ORDER BY t;
在 InfluxDB 2.7 里,这个查询平均耗时 1.8 秒。同样的查询在 3.0 里只用了 620 毫秒。我后来反复测了几次,确认不是缓存原因。DataFusion 的查询优化器确实比 2.x 的 TSM 引擎在复杂聚合上更有优势,尤其是涉及多字段、多时间区间的场景。
不过 3.0 也不是没有代价。我们当时发现,3.0 对内存的占用比 2.7 更“激进”。同样的数据集,2.7 占用约 4.2GB 内存,3.0 在查询高峰期能冲到 6.8GB。如果你的机器内存紧张,这一点必须提前考虑。
还有一个细节值得提。InfluxDB 3.0 开始原生支持更多 SQL 语义,我们原来在 2.7 里用 InfluxQL 写的查询,有一部分要改写成标准 SQL。比如原来 SELECT mean(value) FROM metrics WHERE time > now() - 1h 这种,在 3.0 里要写成 SELECT avg(value) FROM metrics WHERE time >= now() - interval '1 hour'。虽然改动不大,但如果你有大量历史查询脚本,迁移成本要算进去。
下面是我们压测时用的一个 Python 写入脚本,两种版本都兼容,只是 endpoint 和 token 不同:
from influxdb_client import InfluxDBClient, Point, WritePrecision
from datetime import datetime
import random
import time
client = InfluxDBClient(
url="http://localhost:8086",
token="your-token",
org="benchmark"
)
write_api = client.write_api()
start_time = time.time()
for i in range(100000):
p = Point("device_metrics") \
.tag("device_id", f"dev-{random.randint(1,100)}") \
.tag("region", "cn-beijing") \
.field("cpu_usage", random.uniform(0, 100)) \
.field("memory_usage", random.uniform(0, 100)) \
.time(datetime.utcnow(), WritePrecision.NS)
write_api.write(bucket="bench-1d", record=p)
print(f"Write 100k points cost: {time.time() - start_time:.2f}s")
综合来看,如果你现在已经在用 2.7,而且系统稳定,不用急着迁移。但如果你是新项目,或者查询复杂度高、数据量增长快,3.0 的架构确实更有后劲。按照 InfluxDB 官方 2024-2026 年的路线图,3.0 会是未来的核心方向,SQL 兼容性、Arrow 生态集成、AI/ML 能力都会往这个版本集中。
我们现在的策略是:生产环境继续跑 2.7,但新功能验证和性能敏感的场景,先在 3.0 上做试点。等 3.0 的正式稳定版发布,再规划全面切换。
4. 踩坑实录:一次由Tag Cardinality引发的线上OOM排查与修复
去年双十一大促,我们负责的一个电商实时订单监控系统突然告警,InfluxDB 2.7.4 实例内存占用飙到 16G 上限,随后直接 OOM 重启。系统写入量其实不算夸张,峰值也就 8 万 QPS,按理说这个配置(8C16G)完全扛得住。
我第一反应是去查慢查询,但日志里全是正常的写入请求,没有复杂查询。接着看监控,发现 influxdb_tsdb_cache_memory_bytes 这个指标一直在涨,从不下降。这时候我才意识到问题可能出在 Tag Cardinality(标签基数) 上。
我们的订单数据 Schema 设计是这样的:
-- 原来的 measurement 设计(问题版本)
measurement: order_metrics
tags:
order_id -- 错误示范:把订单ID作为tag
user_id -- 错误示范:把用户ID作为tag
region
fields:
amount
item_count
timestamp
我当时为了查询方便,把 order_id 和 user_id 都塞进了 Tag,心想这样就能快速按订单或用户筛选。但我忽略了 InfluxDB 的底层原理:Tag 是用来建立索引的,每个 Tag 的唯一值组合都会在内存中维护一个索引条目。
一个订单 ID 是全局唯一的,这意味着每写入一条数据,就会在内存里新增一个索引条目。大促期间每分钟产生几十万新订单,内存不炸才怪。
我通过 InfluxDB 自带的 CLI 工具验证了我的猜测:
# 查看 tag cardinality
influx query '
import "influxdata/influxdb/schema"
schema.tagKeys(bucket: "order_monitor")
' --host http://localhost:8086
# 统计 order_id 的基数
influx query '
from(bucket: "order_monitor")
|> range(start: -1h)
|> filter(fn: (r) => r._measurement == "order_metrics")
|> group(columns: ["order_id"])
|> count()
' --host http://localhost:8086
结果出来我傻了,order_id 的基数在 1 小时内达到了 200 万,这就是内存泄漏的根源。
修复方案很简单,把高基数的字段从 Tag 移到 Field:
-- 修复后的 Schema 设计
measurement: order_metrics
tags:
region -- 低基数:只有几个地区
channel -- 低基数:app, web, mini_program
fields:
order_id -- 移到 field,不再参与索引
user_id -- 移到 field
amount
item_count
timestamp
修改后,我写了一个数据迁移脚本,把历史数据重新写入新的 bucket:
import influxdb_client
from influxdb_client.client.write_api import SYNCHRONOUS
import time
# 初始化客户端,我们用的 2.7.4 版本
client = influxdb_client.InfluxDBClient(
url="http://localhost:8086",
token="your-admin-token",
org="production"
)
write_api = client.write_api(write_options=SYNCHRONOUS)
query_api = client.query_api()
# 读取旧数据并转换
query = '''
from(bucket: "order_monitor_old")
|> range(start: -30d)
|> filter(fn: (r) => r._measurement == "order_metrics")
'''
tables = query_api.query(query)
points = []
count = 0
for table in tables:
for record in table.records:
# 重构数据点,把 order_id 和 user_id 从 tag 变成 field
new_point = influxdb_client.Point("order_metrics") \
.tag("region", record.values.get("region", "unknown")) \
.tag("channel", record.values.get("channel", "unknown")) \
.field("order_id", record.values.get("order_id", "")) \
.field("user_id", record.values.get("user_id", "")) \
.field("amount", float(record.values.get("amount", 0))) \
.field("item_count", int(record.values.get("item_count", 0))) \
.time(record.time)
points.append(new_point)
count += 1
# 批量写入,每 5000 条提交一次
if count % 5000 == 0:
write_api.write(bucket="order_monitor_new", record=points)
points = []
print(f"Migrated {count} records...")
# 写入剩余数据
if points:
write_api.write(bucket="order_monitor_new", record=points)
print(f"Total migrated: {count} records")
改完之后,内存占用直接从 14G 降到了 1.2G,写入延迟也从 800ms 优化到了 120ms。
教训总结:Tag 只放用于分组的低基数维度(如 region、host、env),绝对不要把 UUID、手机号、订单号这种高基数标识放进去。如果你不确定某个字段适不适合做 Tag,先算一下它的唯一值数量,超过 10 万就要警惕了。
---
5. 生态集成:基于Telegraf+Flink构建实时流处理分析流水线
我们现在的架构是:设备端产生数据 -> Telegraf 采集 -> Kafka 缓冲 -> Flink 实时处理 -> InfluxDB 存储 -> Grafana 展示。这套流水线在 3.0 架构下跑得很稳,特别是 InfluxDB 3.0 基于 Apache Arrow 的引擎,查询性能比 2.x 提升了不少。
先说 Telegraf 配置。我们采集的是工厂传感器的温度、振动数据,每个车间 200 个传感器,每秒上报一次。Telegraf 的配置要针对高吞吐做优化:
# telegraf.conf
[agent]
interval = "1s"
round_interval = true
metric_batch_size = 5000 # 增大批次,减少网络开销
metric_buffer_limit = 100000
collection_jitter = "0s"
flush_interval = "5s"
# 输入插件:MQTT 订阅传感器数据
[[inputs.mqtt_consumer]]
servers = ["tcp://mqtt-broker:1883"]
topics = ["factory/sensors/+/data"]
qos = 1
connection_timeout = "30s"
data_format = "json"
# 关键:把传感器ID映射为 tag
[inputs.mqtt_consumer.tags]
source = "mqtt"
# 处理器:转换数据格式
[[processors.converter]]
[processors.converter.fields]
float = ["temperature", "vibration", "humidity"]
# 输出到 Kafka,不直接写 InfluxDB,避免背压
[[outputs.kafka]]
brokers = ["kafka-1:9092", "kafka-2:9092"]
topic = "sensor_metrics"
data_format = "influx"
influx_max_line_bytes = 0
timeout = "30s"
# 关键配置:开启压缩
compression_codec = "snappy"
数据进 Kafka 后,我用 Flink 1.17 做实时降采样和异常检测。这里有个坑:Flink 默认的序列化方式跟 InfluxDB 的 Line Protocol 不兼容,所以我自定义了一个 Sink。
// Flink 主处理逻辑
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.api.datastream.DataStream;
import org.apache.flink.api.common.eventtime.WatermarkStrategy;
import org.apache.flink.streaming.connectors.kafka.FlinkKafkaConsumer;
import org.apache.flink.api.common.serialization.SimpleStringSchema;
import java.util.Properties;
public class SensorAnalyticsJob {
public static void main(String[] args) throws Exception {
final StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.enableCheckpointing(5000); // 每5秒做一次 checkpoint
Properties props = new Properties();
props.setProperty("bootstrap.servers", "kafka-1:9092");
props.setProperty("group.id", "flink-sensor-processor");
// 从 Kafka 消费原始传感器数据
FlinkKafkaConsumer<String> source = new FlinkKafkaConsumer<>(
"sensor_metrics",
new SimpleStringSchema(),
props
);
source.assignTimestampsAndWatermarks(WatermarkStrategy.noWatermarks());
DataStream<String> stream = env.addSource(source);
// 实时处理:解析 -> 异常检测 -> 降采样
DataStream<String> processed = stream
.map(new SensorDataParser()) // 解析 Line Protocol
.keyBy(r -> r.getSensorId()) // 按传感器分组
.process(new AnomalyDetector()) // 异常检测逻辑
.map(new DownsampleMapper()); // 降采样到 10 秒粒度
// 写入 InfluxDB 3.0
processed.addSink(new InfluxDBSink());
env.execute("Sensor-RealTime-Analytics");
}
}
// 自定义 InfluxDB Sink
import org.apache.flink.configuration.Configuration;
import org.apache.flink.streaming.api.functions.sink.RichSinkFunction;
import okhttp3.*;
public class InfluxDBSink extends RichSinkFunction<String> {
private OkHttpClient client;
private String influxUrl = "http://influxdb:8086/api/v2/write";
private String token = "your-token";
private String org = "factory";
private String bucket = "sensor_data";
@Override
public void open(Configuration parameters) {
client = new OkHttpClient();
}
@Override
public void invoke(String lineProtocol, Context context) {
// 批量写入,这里简化成单条,生产环境建议攒批
RequestBody body = RequestBody.create(lineProtocol, MediaType.parse("text/plain"));
Request request = new Request.Builder()
.url(influxUrl + "?org=" + org + "&bucket=" + bucket)
.addHeader("Authorization", "Token " + token)
.post(body)
.build();
try {
client.newCall(request).execute().close();
} catch (Exception e) {
System.err.println("Write failed: " + e.getMessage());
}
}
}
在 InfluxDB 端,我配置了 保留策略(RP) 和 任务(Task) 来做自动降采样。3.0 的任务引擎比 2.x 好用,直接用 SQL 就能写:
-- 创建降采样任务:每 5 分钟执行一次,把 10 秒精度的数据聚合成 1 分钟精度
CREATE TASK downsample_1m
EVERY 5m
AS
INSERT INTO sensor_data_retention.sensor_metrics_1m
SELECT
region,
device_type,
DATE_BIN(INTERVAL '1 minute', time, '2024-01-01') AS time,
AVG(temperature) AS avg_temp,
MAX(vibration) AS max_vibration,
COUNT(*) AS sample_count
FROM sensor_data.sensor_metrics
WHERE time >= NOW() - INTERVAL '10 minutes'
GROUP BY region, device_type, DATE_BIN(INTERVAL '1 minute', time, '2024-01-01')
这个流水线跑了一年多,每天处理 20 亿条数据,InfluxDB 的压缩率大概在 1:8 左右,存储成本比我们之前用 TimescaleDB 低了 40%。
---
6. 面试高频:InfluxDB核心概念与RP策略深度解析
面试时经常有人问我:"InfluxDB 和 MySQL 到底有啥本质区别?" 我一般不直接背定义,而是拿我们订单系统的例子说事。
Measurement 相当于表,但没那么简单
在 MySQL 里,一张 orders 表存所有订单。但在 InfluxDB 里,我把 order_metrics 这个 measurement 设计成只存指标,不存实体属性。
-- MySQL 的表结构(存实体)
CREATE TABLE orders (
order_id VARCHAR(64) PRIMARY KEY,
user_id BIGINT,
amount DECIMAL(10,2),
region VARCHAR(20),
created_at TIMESTAMP
);
-- InfluxDB 的 measurement(存指标变化)
-- 数据长这样:
-- order_metrics,region=sh,channel=app amount=299.0,item_count=3 1704067200000000000
Tag 和 Field 的区别是面试必问,也是最容易用错的
我之前踩过的坑就是没分清这两者的用途。Tag 是索引,Field 是值。
-- 正确的设计
measurement: server_metrics
tags:
host -- 索引:用于 WHERE 和 GROUP BY,基数低(几百台服务器)
env -- 索引:prod, staging, dev
service -- 索引:order-service, user-service
fields:
cpu_usage -- 非索引:用于计算 AVG/MAX,不需要精确匹配
mem_usage -- 非索引
qps -- 非索引
为什么这么设计?因为 Tag 会进内存索引,Field 不会。如果你把 cpu_usage 放进 Tag,那每次 CPU 值变化(比如从 45.2 变到 45.3),都会产生一个新的索引条目。
数据保留策略(RP)是我们生产环境里最实用的功能
我们系统里,监控数据分三层存储:
-- 1. 原始数据:保留 7 天,用于实时排障
CREATE RETENTION POLICY "raw_7d" ON "monitoring"
DURATION 7d
REPLICATION 1
SHARD DURATION 1d -- 分片周期,影响查询性能
-- 2. 降采样数据:保留 30 天,用于周报分析
CREATE RETENTION POLICY "downsample_30d" ON "monitoring"
DURATION 30d
REPLICATION 1
SHARD DURATION 7d
-- 3. 归档数据:保留 1 年,用于月度/年度趋势
CREATE RETENTION POLICY "archive_1y" ON "monitoring"
DURATION 52w
REPLICATION 1
SHARD DURATION 30d
这里有个细节:SHARD DURATION 设置很关键。如果设置太短(比如 1h),会产生大量小文件,查询变慢;如果太长(比如 1 年),删除过期数据时 InfluxDB 要扫描整个大文件,也会卡顿。我们测试下来,原始数据用 1d,降采样用 7d 是比较均衡的。
实际写入时的 RP 指定
from influxdb_client import InfluxDBClient, Point
from datetime import datetime
client = InfluxDBClient(url="http://localhost:8086", token="xxx", org="prod")
# 写入到指定 RP
write_api = client.write_api()
# 方式一:在 bucket 名后加 RP(2.x 兼容写法)
write_api.write(
bucket="monitoring/raw_7d", # 指定 RP
record=Point("server_metrics")
.tag("host", "server-01")
.tag("env", "prod")
.field("cpu", 85.2)
.time(datetime.utcnow())
)
# 方式二:用 Task 自动路由(推荐)
# 在 InfluxDB UI 里配置 Task,自动把数据从 raw_7d 复制到 downsample_30d
面试时我还会补充一个细节:InfluxDB 3.0 的 RP 机制和 2.x 略有不同。3.0 基于 Apache Arrow,底层存储是 Parquet 格式,数据过期是通过删除整个 Parquet 文件实现的,比 2.x 的 TSM 引擎更高效。这也是为什么我们今年开始往 3.0 迁移的原因之一。
如果你在面试中被问到 "如何优化 InfluxDB 查询",我的经验是:第一,确保 WHERE 条件里用的是 Tag 而不是 Field;第二,时间范围尽量精确,不要 WHERE time > '2024-01-01'(全量扫描);第三,GROUP BY 的字段必须是 Tag,否则会触发全表扫描。
我们之前有个慢查询,就是因为用了 GROUP BY field_name,导致查询耗时从 200ms 飙升到 8 秒。改回 GROUP BY tag_name 后立刻恢复正常。
站长实战手记
去年帮一个做智能充电桩的朋友救火,印象太深了。他们当时要监控全国两万多根桩的实时状态,每秒上报数据量大概在 5 万点左右。最初技术负责人坚持用 MySQL,结果跑了不到一周,库就撑不住了,磁盘 IO 常年 100%,历史数据稍微一多,查询直接超时。
我接手后第一件事就是换存储,上了 InfluxDB 2.7。当时为了图省事,我把充电桩的序列号直接塞进了 Tag,心想这样查单桩数据方便。结果上线第二天,整个数据库直接 OOM 挂掉。我折腾了大半夜,盯着日志看,才反应过来:两万多个序列号作为 Tag,产生了极高的基数(Cardinality),InfluxDB 的内存直接被元数据撑爆了。
后来我重构了 Schema,把序列号这种高基数数据挪到了 Field 里,Tag 只保留城市、运营商这种低基数的维度。改完重启,内存瞬间降了下来,查询也丝滑了。
关于选型,我现在的看法很直接:
* 如果你只是做后台管理系统的报表,数据量一天连一万条都不到,千万别上时序库。MySQL 或 PostgreSQL 完全够用,引入新组件只会徒增运维负担。
* 真正的场景是那种设备多、上报频率高、且你需要看趋势图的业务。比如监控服务器 CPU、IoT 设备上报,这时候 InfluxDB 的压缩率和查询速度才是真的香。
最后给个建议:学 InfluxDB 别光看概念,一定要亲手去建个库,试着往里灌点数据。特别是 Tag 和 Field 的区别,这是新手最容易翻车的地方,别等上线了才拍大腿。