告别手工分表:金仓时序数据库超表架构落地的一次实战复盘

一、先交代下背景:三千个监测点,和一个渐渐兜不住的土办法

我在一家做智慧热力的公司写代码,管数据平台这块儿。具体干啥呢?给全市的换热站和管网做在线监测。二期改造完,监测点位涨到三千多个,压力、温度、流量,5 秒一报。你们可以自己算算这笔账,一天下来五千万行左右的写入,就这么个量级。

头一年其实挺太平的。单表存,普通关系库,没出过什么大事。真正的麻烦是从老板一句话开始的--"历史数据留半年,月报要随时能出。"

就这一句,把我们那套土办法逼上了绝路。什么叫土办法?按月手工分表呗。monitor_data_202501monitor_data_202502,一路排下去,每月 1 号凌晨跑个定时任务建下个月的表。写入靠应用层路由,查询就看你要多长时间的数据,然后决定 union 几张表。运维老周维护这套东西一年多了,表从 12 张攒到 30 多张。表越多,他眉头皱得越紧。现在想想,他当时的直觉是对的。

@TOC

二、出事那天:一张季度报表,把连接池干爆了

爆雷是上个月初,周四,我记得清楚,因为那天晚上我十一点半才到家。

财务和运行部门要季度分析,得跨 6 个月的表。应用拼出来的 SQL 大家闭着眼都能想到--6 张表 UNION ALL 再聚合,跑了 40 多秒,连接池直接被打满。更要命的是监控大屏也跟着卡,因为读写在同一个库上,报表把资源吃光,实时数据就进不来。那天群里 @ 我的消息,比这一年加起来都多。

第二天的复盘会开得倒不激烈,问题大家都心知肚明:

新同事改查询忘了带月份路由,直接扫当月大表,这种事已经出过两回了;UNION ALL 之后做聚合,索引基本等于白建;半年的明细全躺在同一块存储上,磁盘告警三天两头响。至于扩展?只剩换更贵的机器这一条路,报价单递上去就被财务打了回来。

散会前领导就说了一句:"别手工分表了,找个正经的时序方案。"

三、选型那两周,我们纠结了什么

说实话,一开始我们偏向往专用时序库那边看。后来为什么拐到金仓数据库这边来了?说出来不怕大家笑话--我们团队的 SQL 是吃饭家伙,报表工具、BI 看板,全走标准 SQL 接口。换专用时序库意味着查询语言重学、工具链重搭、老周还得再学一套运维。人力就这几个人,折腾不起。

而金仓这边 POC 跑下来,应用层几乎不用动,该写的 SQL 接着写。当场拍板。

不过让我真正下决心的,是**超表(Hypertable)**这个东西。它解决的是一个我们被折磨了很久的问题:分区这件事,到底归谁管?

手工分表时代,分区是应用的责任。建表、路由、跨表查询,全是人肉。超表呢,把分区这活儿整个下沉到了数据库内部--数据进来按时间自动切成一个个数据块(Chunk),每块只管一段时间范围的数据,再叠一个空间维度,按点位 ID 哈希,把高并发写入摊开。应用眼里从头到尾就一张 monitor_point_dataINSERT 该咋写咋写。

当时我拿这张表说服领导,现在原样贴出来:

对比项 我们原来的手工分表 金仓超表
分区谁管 应用建表+定时任务,规则散在代码里 数据库自己按时间(+空间)切 Chunk
跨时段查询 多表 UNION ALL,索引废掉 一条普通 SQL,没关的块直接不碰
写入路由 应用按时间拼表名 不存在这个环节,直接插
老数据治理 手工 DROP,忘了就堆着 到期自动按块删
以后扩容 堆硬件,烧钱 可以往分布式超表走
SQL 兼容 - 标准 SQL,BI 工具直连

领导看懂了前三行就拍板了。剩下的行,是给我自己吃的定心丸。

四、动手了:从建表到跑通,外加三个坑

1. 建表这步,简单到我不太敢信

先建一张普通表,然后用一个函数把它"升级"成超表,完事:

sql 复制代码
CREATE TABLE monitor_point_data (
    time       TIMESTAMPTZ      NOT NULL,
    point_id   INTEGER          NOT NULL,
    metric     TEXT             NOT NULL,
    value      DOUBLE PRECISION,
    quality    SMALLINT
);

-- 转为超表:时间列做主分区,point_id 哈希做空间分区
SELECT create_hypertable('monitor_point_data', 'time',
    partitioning_column => 'point_id',
    number_partitions   => 8,
    chunk_time_interval => INTERVAL '1 day'
);

就这一下,时间轴每天自动滚出新块,空间轴按点位 ID 哈希成 8 个分区。应用侧的改造是什么?是把拼表名那段代码删掉。删代码的活儿,谁不爱干呢。

2. 第一个坑:块间隔,我拍脑袋拍疼了

上线前在测试环境,我寻思"块小查得快",把间隔设成了 1 小时。结果监控里后台建新块建得飞起,写入高峰还偶尔出现锁等待。翻手册才发现自己想当然了--建新块要拿的锁,比往已有块里插数据拿的锁时间长,一堆事务同时开新块,就互相顶住了。

手册里给参考值是按日写入量算的:每天 2GB、内存 64GB 的机器,7 天一块合适;一天写 10GB 就缩到 1 天。我们日增量 1GB 上下,最后定了 1 天一块,之后没再动过。

改起来也简单,就是有个坑要知道:

sql 复制代码
-- 注意:只对之后新建的块生效,已经建好的块不动
SELECT set_chunk_time_interval('monitor_point_data', INTERVAL '24 hours');

-- 看看当前分区配置长啥样
SELECT column_name, num_partitions, time_interval
FROM timescaledb_information.dimensions
WHERE hypertable_name = 'monitor_point_data';

划重点:块间隔设大了想改小,旧块救不回来,只能等它自己滚走或者迁数据。所以别学我,先抄文档的作业。

3. 压缩和保留:800GB 的心病没了

老库半年明细占了小 800GB,这是我那阵子最大的焦虑来源。但时序数据的访问模式其实特别有规律--最近一周的数据天天查,一个月前的?出了报表根本没人碰。压缩策略照这个规律配就行:7 天内原样放着,更老的块自动压成列存。

sql 复制代码
ALTER TABLE monitor_point_data SET (
    timescaledb.compress,
    timescaledb.compress_segmentby = 'point_id',
    timescaledb.compress_orderby   = 'time DESC'
);

-- 超过 7 天的块自动压缩
SELECT add_compression_policy('monitor_point_data',
    compress_after => INTERVAL '7 days');

-- 原始明细保留 180 天,到期自动删块
SELECT add_retention_policy('monitor_point_data',
    drop_after => INTERVAL '180 days');

提醒一个我们真踩过的细节:压缩块上没法直接加带默认值的列,要加得先解压。上线第二周小陈想加个点位状态列,就被这个限制顶了回来,最后是加可空列再 UPDATE 绕过去的。压缩比实测 4:1 左右,跟官方口径基本对得上,800GB 掉到 200GB 出头。

4. 连续聚合,以及一个让我后背发凉的坑

报表慢的问题,靠连续聚合解决。道理不复杂:小时级的聚合结果提前物化成一张特殊的超表,后台按策略增量刷新,查询直接拿现成结果,不扫明细。

sql 复制代码
CREATE MATERIALIZED VIEW point_data_hourly
WITH (timescaledb.continuous) AS
SELECT point_id,
       time_bucket(INTERVAL '1 hour', time) AS bucket,
       avg(value) AS avg_val,
       max(value) AS max_val,
       min(value) AS min_val
FROM monitor_point_data
GROUP BY point_id, bucket
WITH NO DATA;

SELECT add_continuous_aggregate_policy('point_data_hourly',
    start_offset      => INTERVAL '3 days',
    end_offset        => INTERVAL '1 hour',
    schedule_interval => INTERVAL '30 minutes');

这个坑必须单独写一段。我们最初保留策略设 30 天,聚合刷新窗口也伸到 30 天前--想着"一步到位"。结果聚合刷新的时候发现源数据已经被删了,它就把物化好的结果也跟着删了。报表空了半天才发现,那半天里领导看过两次大屏。后背发凉。

正确姿势:保留周期(180 天)要远大于刷新窗口(3 天),让刷新永远落在还有原始数据的时间段里。手册里对这个组合是有明确警告的,我一个字一个字读得太晚了。

日常查趋势,还是普通 SQL,配时间桶函数,要多细有多细:

sql 复制代码
SELECT time_bucket('5 minutes', time) AS five_min,
       avg(value) AS avg_val
FROM monitor_point_data
WHERE point_id = 1024
  AND time >= now() - INTERVAL '2 hours'
GROUP BY five_min
ORDER BY five_min;

五、三个月跑下来,数字长这样

光讲体感没用,放实测数据。不敢说多惊艳,但每一项都是我们自己的环境里量出来的:

指标 迁移前(手工分表) 迁移后(超表)
跨季度报表 40 秒往上,高峰直接超时 2 秒以内,走的连续聚合
写入高峰 排队,连接池耗尽 稳,没见堆积
半年数据占用 差不多 800GB 210GB 左右(压缩后)
每月运维动作 建表删表核对脚本 零,策略自己跑
加新点位 改路由配置+回归测试 加个配置,库侧啥也不用干

最让我记到现在的是上个月的一件小事。领导路过工位,临时要看某个片区供暖季以来的压力曲线。我在座位上敲了条 SQL,十几秒出图。他愣了一下,大概是没想到这么快。放在以前,这句话的潜台词是"今晚加班导数据"。

六、絮叨几句,都是掏心窝的

回头看,这次迁移技术上真不难,难的是心态--把原来攥在应用层手里的那点"分表智慧",整个交还给数据库。交出去的那几天我其实挺没安全感的,总想着"它真的能替我管好吗"。三个月跑下来,答案交给时间了。

几条经验,给要做同样事的人:

块间隔先抄作业再微调,按日写入量对照手册来,别拍脑袋,我替你们拍过了,疼。唯一索引必须带上分区列,只想拿单列做唯一约束会直接报错,这个报错信息还挺长。保留策略和聚合刷新窗口要放在一起设计,别分开拍板,这是全套方案里最容易翻车的组合,血泪。最后说个容易忽略的:超表的价值不止在快。应用代码变干净了,小陈他们不用再研究那套分表路由--这种工程上的松快,长期看比快几秒值钱。

还有句话想对运维同行说。标准 SQL 意味着你的备份脚本、监控探针、权限体系都能接着用,金仓的时序能力是长在数据库里的,不是外挂一套要单独伺候的东西。在我们这种三五个人的小团队眼里,这点可能比任何单项跑分都实在。

相关推荐
草莓熊Lotso1 小时前
【Redis 初阶】Hash 类型深度解析:结构化数据存储的最优解
linux·网络·数据库·redis·tcp/ip·缓存·哈希算法
nvd111 小时前
K3s + ArgoCD 中的密码管理
数据库·oracle·argocd
Wang's Blog2 小时前
PostgreSQL笔记62: 分区表维护最佳实践——默认分区、锁策略与性能调优
数据库·笔记·postgresql
熊出没2 小时前
解密数仓中的ODS、DWD、DWS、ADS
数据库·数据仓库
梦想不只是梦与想2 小时前
MySQL 不同操作系统的安装方式
数据库·mysql·mysql安装
熊文豪2 小时前
向量数据库单独建一套,这笔账划不划算
数据库
一只旭宝2 小时前
预约系统版本2(基于第一版改良)
服务器·数据库·c++
卓怡学长2 小时前
w192基于springboot教务管理系统
java·数据库·spring boot·spring·maven·intellij-idea
码农颜2 小时前
6.4.1 监听连接
java·数据库·mysql