搞数据库的,磁盘满了别只敲DELETE!这篇Kingbase实战教程教你怎么从OS到内核一步步精准排查和瘦身。
Kingbase磁盘满时跑DELETE不会释放空间,原因是MVCC机制和未归档WAL。本文从OS物理层(df -Th、du -sh)到DB逻辑层(sys_database_size函数)逐步排查,用SQL扫描出test_bloat表占用558MB纯数据。还解释了TOAST机制和索引数据比过高(如纯数据1GB但索引5GB)的性能陷阱。最后给出企业级归档配置和表空间迁移等优化方案。
《Kingbase护城河》——数据库存储空间全景探测与精细化瘦身实战
“我的磁盘怎么又满了?”——数据库存储空间全景探测与精细化瘦身实战 前面几篇我们聊了跨平台联调网络的事,也像探长一样,从好几千个并发里面,把搞卡系统的僵尸进程还有行锁冲突给找出来了。那么,平时搞数据库运维的时候,比系统变慢更让人头疼的生产级灾难,其实就是这四个英文单词: No space left on device (磁盘空间不足) 。 磁盘使用率跑到 100% 会怎样呢?数据库内核就没法往磁盘写事务日志(WAL)了。这是一个大问题。整个实例马上就只读了,有时候甚至直接就内核级宕机(Crash)了。那么业务那边想写数据的话,肯定就全瘫了。 很多开发同学收到磁盘告警,第一反应就是去数据库里敲一条 DELETE 。想删个几千万条半年前的日志数据来救个急。但是诡异的事情就来了: SQL 提示成功了呀,数据也没了,去服务器一看,磁盘空间竟然连 1MB 都没放出来! 那这到底是为什么呢? 搞数据库的人,其实不能只看着表面头痛医头。今天我们就跳出单一的 SQL 视角,搞一个从“OS物理层 -> DB逻辑层 -> MVCC内核层 -> 企业架构层”的排查框架出来。也就是说,我们一步步来,怎么找准空间黑洞,搞明白“删了数据却不释放空间”的底层原因。接着的话,再通过企业层级里的架构调优,把这个磁盘空间的问题给彻底解决掉。 @[toc] 第一阶段:宏观俯瞰 —— 跨越 OS 与 DB 的双重视角审计 收到磁盘告警的时候,其实千万别急着登录数据库去乱查。正确的排障标准作业程序(SOP),通常来说是要先看操作系统(OS)的物理层。接着再到数据库(DB)的逻辑层里面去核对一下。 1. OS 物理层扫盲:是谁吃掉了磁盘? 那么首先,SSH 登录到你的 CentOS 数据库服务器上。我们要先搞明白,到底是不是数据库自己占的空间。因为很多时候的话,磁盘满了是因为应用打出来的日志文件太大了。或者也有可能是系统内核的 Core Dump 文件搞出来的情况。 执行以下系统级探测命令: # 1. 查看全系统挂载点的使用率,锁定爆满的磁盘分区 df -Th # 2. 假设数据库安装在 /opt/Kingbase/ES/V9 目录下,我们进入该目录 cd /opt/Kingbase/ES/V9/data # 3. 统计数据目录下各个核心子目录的物理大小(极其关键) du -sh * | sort -hr | head -n 10 DBA 避坑指南(健康基线法则): 看看上面那个真实环境的截图。也就是说,哪怕你这个数据库实例啥业务都没跑,非常健康的情况下,容量吃得最多的永远是 base 目录,这个是放实际业务数据的。然后就是 sys_wal 目录,它是放预写式日志的。 这个健康的基线模型要记住。在真实的生产环境里头,你看到 base 目录很大,那是业务数据在涨,这很正常。但是,如果你看到 sys_wal 目录大得离谱,比如说占了几十上百GB。那为什么会这样呢?通常来说,这说明你的 归档机制(Archive)失效了 。也有可能是有一个极长的事务挂在那边了,历史日志堆着没法循环覆盖。这时候你去清理业务数据是没用的,必须去查系统的归档配置。 2. DB 逻辑层全景:实例与库级容量核查 确认了是业务数据,也就是 base 目录膨胀了以后,我们回到 Win11 本地。接着打开 KStudio 或者 ksql 命令行,进入数据库的管理视角里面。 一个数据库实例下面,通常来说会有好几个 Database。那么到底是谁在吃资源呢?金仓数据库这边给了几个很直观的容量测算函数。执行以下 SQL: SELECT d.datname AS 数据库名称, sys_size_pretty(sys_database_size(d.datname)) AS 物理总大小, sys_database_size(d.datname) AS 原始字节数 FROM sys_database d ORDER BY 原始字节数 DESC ; (注: sys_size_pretty 这个函数的话,其实挺好用的。它会自动把那一大串字节数,给你换成 KB、MB、GB 这种好读的单位。) 结合上面的监控反馈,我们一眼就能看出来。经过海量业务写入,那个 test 库的体积已经飙到了 575 MB,占了资源消耗的榜首 ,而其他系统级的默认库才 17 MB。 找到这个大库之后,还得继续往下看。因为一个库里面可能有成百上千张表。我们必须像拿显微镜一样,把那个真正吃空间的表给找出来。 第二阶段:微观狙击 —— X 光级表级容量透视与 TOAST 探秘 很多新手只会用简单的语句去查表的数据量,也就是跑个 count(*) 。但是数据行数多,其实不代表占的物理磁盘空间就大。一张表可能只有 10 万行,但是里面存了长文本,它的体积往往比一张 1000 万行但只存纯数字的表还要大。 1. 打造“DBA 超级空间扫描仪” 所以的话,我给你准备了下面这段企业层级里的“空间扫描仪 SQL”。跑一下,基本上所有表的底层物理信息就全出来了: SELECT schemaname AS 模式名, relname AS 表名, sys_size_pretty(sys_relation_size(relid)) AS 纯数据大小, sys_size_pretty(sys_indexes_size(relid)) AS 索引总大小, sys_size_pretty(sys_total_relation_size(relid)) AS 表及附属总物理大小, CAST (sys_indexes_size(relid) AS float ) / sys_relation_size(relid) AS 索引数据比 FROM sys_stat_user_tables WHERE sys_relation_size(relid) > 0 ORDER BY sys_total_relation_size(relid) DESC LIMIT 10 ; 2. 深度剖析:纯数据、索引与 TOAST 机制 看着上面扫描仪出来的这个表格,你要去读懂里面藏着的那些技术门道。重点看下面这三个东西: 纯数据大小( sys_relation_size ): 这个的话,就是业务数据在磁盘上实实在在的占位,也就是 Heap Tuple。就像截图里红箭头指的那个,那个叫 test_bloat 的模拟业务日志表,纯数据就有 558 MB。这就是整个库体积暴增的元凶了。 索引数据比过高危机: 截图里这个表的索引是 0,因为它是一张纯日志流水表。但是如果在真实生产环境里,你看到一张表的纯数据才 1GB,索引大小居然有 5GB。也就是说索引数据比大于 5 了。那为什么会这样呢?这说明研发同学在这表上建了一堆没用的索引。每次写数据进去,数据库还得去维护那几棵庞大的索引树。这不仅浪费磁盘空间,往往更是把写入性能拉垮的致命伤。 神秘的差值与 TOAST 机制: 你仔细看的话,有时候会发现纯数据大小加上索引大小,还是比总物理大小要小。中间差的那些容量跑哪去了呢?这就得说说金仓底层的 TOAST(超大属性存储技术) 了。表里面要是有了巨大的 JSON 或者是长文本字段,内核就会自动把这些单行超限的大字段给切碎压缩。接着的话,转移到一张隐蔽的系统附属表里面去存着了。 第三阶段:内核揭秘 —— 为什么删了数据,空间还没变? 那么现在,我们就要来看看开头说的那个让人崩溃的现象了:明明执行了 DELETE ,为什么空间就是不释放呢? 1. 撕开 MVCC 的底层面纱 金仓数据库为了实现企业层级里的高并发读写,也就是读不阻塞写,写不阻塞读。它在内核里面用了 MVCC(多版本并发控制) 这个机制。 这个机制是怎么运作的呢?当你敲 UPDATE 去改一条数据的时候,底层并不是在原来的地方把旧数据抹掉。它是 插入了一条全新的数据版本,然后给旧数据打上一个“已过期(Dead Tuple)”的标记 。同样的道理,你敲 DELETE 删数据的时候,底层也就是给这些数据打了个“被删除”的隐形标记。物理磁盘上并没有马上把它们擦掉。 那些被打上标记的,物理上还占着磁盘空间的数据,在数据库工程界叫作“表膨胀(Table Bloat)”。这也就解释了,为什么你删了百万级的数据以后,去 CentOS 上用 du -sh 看底层的物理文件,大小还是一点都不变的情况。 动手实验:一键复现表膨胀 想亲自验证一下的话,你可以在测试库跑一下这个脚本:先插 50 万条长文本数据进去,接着 DELETE 删掉里面的 30 万条。这个时候你再去查这个表的物理大小,你会发现空间一点都没变,这就是经典的表膨胀现场。 2. 检测表膨胀率:揪出“虚胖”的表 那么我们需要通过系统统计视图,来看看一张表到底虚胖到什么程度了。执行以下 SQL: SELECT relname AS 表名, n_live_tup AS 存活数据行数, n_dead_tup AS 死亡标记行数, ROUND(n_dead_tup:: numeric / (n_dead_tup + n_live_tup) * 100 , 2 ) AS 虚胖膨胀率百分比 FROM sys_stat_user_tables WHERE n_dead_tup + n_live_tup > 0 ORDER BY 虚胖膨胀率百分比 DESC ; (注:要是你看到某张表的 n_dead_tup 比 n_live_tup 还要大得多,膨胀率都过了 50% 的情况了。这就说明这张表里的死数据实在太多了。也就是说,它在那白白占着空间,而且的话,你跑全表扫描的时候,性能往往也会掉得很厉害。) 3. 清理多占的空间:常规 VACUUM 与彻底的 VACUUM FULL 那么既然 DELETE 没法把空间放出来,我们怎么才能把这些死掉的数据占的物理空间,真正还给操作系统呢?其实,我们需要用到数据库里的一个清理功能,也就是 清理命令(VACUUM) 。 常规清理: VACUUM 表名; 这个操作的话,通常来说是比较轻量的。它会去扫一遍全表,然后把你那些死亡数据,也就是 Dead Tuple 占的空间,标记成可以用的状态。 注意啊:它其实还是不会把空间还给操作系统的,物理文件的大小也不会变小! 那它有啥用呢?它的作用就是,以后要是再有新的 INSERT 数据进来的话,数据库会优先去用这些空出来的位置。也就是说,它能挡住物理文件继续变大。这个操作是不锁表的,平时经常跑一跑没问题。 彻底清理: VACUUM FULL 表名; 这个才是真正能把空间抠出来,还给 CentOS 操作系统的办法。那它是怎么做的呢?底层逻辑其实很简单的。也就是说,它会去建一个新的物理文件,接着把活着的那些数据都拷过去,排得紧实一点,最后把原来那个撑大的旧文件给删掉。 【高危警告】: 敲 VACUUM FULL 的时候,它会给这张表加上最高级别的 排他锁(AccessExclusiveLock) 。啥意思呢?就是拷贝完之前,业务那边想做增删改查的话,全都会被堵住,根本执行不了!所以说,除非是半夜那种停机维护的时间段, 千万别在业务跑得最猛的时候去敲这个命令! 第四阶段:架构调整 —— 用物理隔离和分区机制解决长远问题 一直靠 VACUUM FULL 去处理问题,这其实也就是运维层面没办法的办法。那么,如果你是有架构思维的 DBA 的话,我们往往就需要去规划一下底层的东西了。也就是说,要从根本上把一个地方容量不够,还有清理的时候锁表这些痛点给解决掉。 1. 物理层空间转移:用表空间(Tablespace)来搞定 比如说你的 /opt 所在的系统盘,容量已经到 99% 了。甚至于你想跑个 VACUUM FULL ,连要用的临时交换空间都挤不出来了。那遇到这种情况怎么办呢? 这个时候的话,我们可以在 CentOS 上挂一块新的 2TB 数据盘上去,比如挂到 /data_new 。接着,我们用金仓数据库里面的表空间(Tablespace)功能,在业务不用停的情况下,把那些很大的历史表,挪到新的磁盘里面去: -- 1. 在新磁盘路径下创建一个名为 ts_archive 的物理表空间 CREATE TABLE SPACE ts_archive LOCATION '/data_new/kingbase_ts' ; -- 2. 将庞大的历史表,从默认的满载磁盘,动态腾挪到新的表空间(新磁盘)中 ALTER TABLE trade_history_log SET TABLESPACE ts_archive; 敲了这条指令以后,内核就会直接把这张表的物理文件,全都搬到 /data_new 下面去了。那么这样的话,原来那个快满的系统盘,也就不用那么吃紧了。 2. 逻辑架构调整:从 DELETE 到表分区(Partitioning) 对于系统日志表、订单流水表这种带着明显时间属性,每天疯狂写入的数据。真的不要再写定时任务去 DELETE 一个月前的数据了!因为 MVCC 机制的存在,高频的大批量 DELETE 简直就是自己给自己找麻烦。 企业层级里的终极解决方案是:时间范围分区表(Range Partition)。 也就是说,在建表初期,我们就按照月份,把一张庞大的逻辑表,在物理底层切成 12 张独立的子表,就像 log_2025_01 , log_2025_02 那样。 当 1 月份的数据过期了需要清理的时候,我们不再去执行 DELETE 了。而是直接敲一条很轻量的 DDL 命令: DROP TABLE log_2025_01; 这种做法的好处在于: DROP TABLE 根本不走 MVCC 机制,也就不会产生什么死亡标记。它在 Linux 底层直接就把那个物理文件给抹掉了,也就是 unlink 。100GB 的空间,0.1 秒内瞬间就释放还给操作系统了。一点表膨胀都不产生,而且也不会影响当前 2 月份子表的写入! 结语 这篇文章,其实走完了一个挺完整的过程。我们先是在 CentOS 上跑 du -sh 去看物理磁盘的情况。接着用 sys_total_relation_size 去摸清逻辑层面的占用。然后还扒开了 MVCC 底下那个让人头疼的“表膨胀”机制。最后呢,又用上了表空间腾挪、分区表这些手段,把整体架构往上提了一下。磁盘空间这事儿,算是实打实地处理了一回。 搞技术的,我们不光要知道怎么把空间清出来,这只是第一步。更关键的是,得搞明白“空间它到底为什么没了”,把这个根儿给挖出来。然后,再往前走一步的话,就是通过事先的架构设计,把这些要命的容量问题,在它刚冒出点苗头的时候就直接按下去。 你想想看,到了这会儿,像连接数爆炸、锁冲突卡顿、磁盘空间告警这些情况,你都能应付得来了。其实这已经算是过了那个只能被动去救火的阶段了。但是,在系统更底层的地方,有时候还会藏着一些那种情况。就是平时跑得好好的,偶然给你慢一下,慢得让人摸不着头脑的诡异 SQL。 所以,在下一篇里,咱们这个系列专栏就要进入到最后的部分了,也就是—— “猎捕慢查询:执行计划的微观解析与索引调优实战” 。我会跟你一块儿,去琢磨那些看起来很复杂的“执行树”到底是怎么回事。然后把数据库的性能瓶颈给它控制住。行,那咱们下一篇见吧!