从单机瓶颈到PB级实时分析:TiDB实战手记

去年我们团队接手了一个物联网项目,每天产生超过5TB的时序数据,还要支持在线交易的实时查询。传统分库分表方案让我们头疼不已——跨节点Join慢如蜗牛,扩容要停服,更别提那令人绝望的分布式事务。直到我们遇见了TiDB,这个原生分布式数据库真正实现了“OLTP+OLAP”的HTAP能力。今天我就把实战中摸索出的经验分享出来,包括那些让你少掉头发的坑。
一、环境搭建:别被官方文档骗了
官方推荐用TiUP部署,但如果你像我一样在测试环境踩过坑,就会知道必须提前检查SSH免密登录和NTP时间同步。我第一次部署时,三台机器时间差了2秒,结果TiKV节点反复报“region epoch mismatch”。
用TiUP部署6节点集群(3个TiDB+3个TiKV,PD复用)的典型命令:
# 安装TiUP
curl --proto '=https' --tlsv1.2 -sSf https://tiup-mirrors.pingcap.com/install.sh | sh
# 部署集群(假设已配置好拓扑文件topo.yaml)
tiup cluster deploy my-tidb v7.5.0 ./topo.yaml --user root -p
# 启动集群
tiup cluster start my-tidb
踩坑提示:拓扑文件里host千万别写127.0.0.1,要用真实内网IP。我第一次测试时所有节点都在同一台物理机,结果region调度全乱套了。
二、数据建模:放弃MySQL思维
TiDB虽然兼容MySQL协议,但分布式场景下建表有讲究。我们接手的项目需要同时处理订单事务和实时报表,我总结出三条黄金法则:
- 强制指定主键:如果不显式定义主键,TiDB会用隐藏的_tidb_rowid,这会导致热点问题
- 使用AUTO_RANDOM:自增主键在分布式下会变成性能杀手,改用随机ID分散写入压力
- 分区表+列存引擎:分析类查询用TiFlash列存,行存留给事务
我们最终的表结构长这样:
CREATE TABLE `sensor_data` (
`id` bigint(20) NOT NULL AUTO_RANDOM(5),
`device_id` varchar(32) NOT NULL,
`ts` timestamp NOT NULL,
`temperature` double DEFAULT NULL,
`humidity` double DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `idx_device_ts` (`device_id`, `ts`)
) ENGINE=InnoDB
PARTITION BY RANGE (UNIX_TIMESTAMP(ts)) (
PARTITION p202401 VALUES LESS THAN (UNIX_TIMESTAMP('2024-02-01')),
PARTITION p202402 VALUES LESS THAN (UNIX_TIMESTAMP('2024-03-01'))
)
TIFLASH REPLICA 2;
注意:TiFlash副本数建议设为2,既保证高可用又不会太占内存。我们曾设成3,结果64GB内存的机器直接OOM了。
三、写入优化:批量操作的艺术
IoT场景每秒要写入数万条数据。初期我们用单条INSERT,TiDB的Raft协议导致写入延迟飙升到200ms。后来改用批量写入,性能提升了10倍:
# 使用MySQL JDBC的rewriteBatchedStatements参数
jdbc:mysql://tidb-cluster:4000/test?rewriteBatchedStatements=true&useServerPrepStmts=false
代码层面的批量插入(Go语言示例):
txn, _ := db.Begin()
stmt, _ := txn.Prepare("INSERT INTO sensor_data (device_id, ts, temperature, humidity) VALUES (?, ?, ?, ?)")
for _, data := range batchData {
stmt.Exec(data.DeviceID, data.TS, data.Temp, data.Humidity)
}
txn.Commit() // 每500条提交一次
性能数据:在3台8核32GB的机器上,单条写入约8000 TPS,批量后达到12万 TPS。但注意事务大小不要超过100MB,否则TiKV会报“transaction too large”。
四、实时分析:TiFlash的魔法
最让我惊艳的是TiFlash——完全不用改代码,只需建表时指定副本,分析查询自动走列存。比如这个统计每小时平均温度的查询:
SELECT
DATE_FORMAT(ts, '%Y-%m-%d %H:00:00') as hour,
AVG(temperature) as avg_temp
FROM sensor_data
WHERE ts >= NOW() - INTERVAL 1 HOUR
GROUP BY hour;
在TiFlash引擎下,10亿行数据的聚合查询从原来的23秒降到了1.2秒。但要注意TiFlash的数据同步有秒级延迟,如果业务要求强一致性读,需要加hint:
SELECT /*+ read_from_storage(tikv[sensor_data]) */ COUNT(*) FROM sensor_data;
五、运维实战:那些年我们踩过的坑
1. Region分裂导致性能抖动
某天下午写入突然变慢,排查发现单个Region数据量超过1GB。解决方案是调整Region分裂阈值:
tiup ctl:v7.5.0 pd -u http://pd-ip:2379 config set max-merge-region-size 20
tiup ctl:v7.5.0 pd -u http://pd-ip:2379 config set split-merge-interval 1h
2. 热点调度失效
当某个设备ID写入量暴增时,TiDB的自动调度可能反应迟钝。我们写了个脚本手动拆分热点Region:
# 找到热点Region
tiup ctl:v7.5.0 pd -u http://pd-ip:2379 store 1 | grep hot
# 手动拆分
tiup ctl:v7.5.0 pd -u http://pd-ip:2379 operator add split-region 123456
3. GC引发的查询超时
TiDB的垃圾回收默认10分钟一次,如果大事务运行时间超过GC life time,会报“GC life time is shorter than transaction duration”。我们调大了参数:
tiup ctl:v7.5.0 pd -u http://pd-ip:2379 config set gc-tuner-threshold 0.8
tiup ctl:v7.5.0 pd -u http://pd-ip:2379 config set max-gc-tuner-threshold 0.9
六、性能调优:从玄学到科学
最后分享几个压箱底的调优参数:
# TiKV配置优化(写在tikv.toml中)
[storage.block-cache]
capacity = "20GB" # 总内存的60%
[rocksdb.defaultcf]
block-cache-size = "10GB"
write-buffer-size = "256MB"
max-write-buffer-number = 5
[raftstore]
raft-log-gc-count-limit = 100000
raft-log-gc-size-limit = "512MB"
重要提示:不要盲目照搬网上的配置!先用TiDB的Dashboard看监控——如果写入延迟高就调大write-buffer,读延迟高就加大block-cache。我们曾把block-cache设成30GB,结果内存不够直接OOM了三次。
经过三个月的调优,我们的集群现在稳定支撑日均50亿条写入,复杂分析查询P99延迟控制在3秒内。TiDB让我相信,分布式数据库不再是神话,而是每个团队都能掌握的实战工具。希望这篇笔记能帮你少走些弯路,如果遇到奇怪的问题,欢迎在评论区交流。


官方文档真坑,SSH免密这种基础问题都不强调。