「我的磁碟怎麼又滿了?」——資料庫儲存空間全景探測與精細化瘦身實戰

前面幾篇我們聊了跨平台聯調網路的事,也像探長一樣,從好幾千個併發裡面,把搞卡系統的殭屍程序還有行鎖衝突給找出來了。那麼,平時搞資料庫維運的時候,比系統變慢更讓人頭痛的生產級災難,其實就是這四個英文單詞:No space left on device(磁碟空間不足)。
磁碟使用率跑到 100% 會怎樣呢?資料庫核心就沒法往磁碟寫交易日誌(WAL)了。這是一個大問題。整個實例馬上就只讀了,有時候甚至直接就核心級當機(Crash)了。那麼業務那邊想寫資料的話,肯定就全癱了。
很多開發同學收到磁碟告警,第一反應就是去資料庫裡敲一條 DELETE。想刪個幾千萬條半年前的日誌資料來救個急。但是詭異的事情就來了:SQL 提示成功了呀,資料也沒了,去伺服器一看,磁碟空間竟然連 1MB 都沒放出來!
那這到底是為什麼呢?
搞資料庫的人,其實不能只看著表面頭痛醫頭。今天我們就跳出單一的 SQL 視角,搞一個從「OS 物理層 -> DB 邏輯層 -> MVCC 核心層 -> 企業架構層」的排查框架出來。也就是說,我們一步步來,怎麼找準空間黑洞,搞明白「刪了資料卻不釋放空間」的底層原因。接著的話,再通過企業層級裡的架構調優,把這個磁碟空間的問題給徹底解決掉。
@[toc]
收到磁碟告警的時候,其實千萬別急著登入資料庫去亂查。正確的排障標準作業程序(SOP),通常來說是要先看作業系統(OS)的物理層。接著再到資料庫(DB)的邏輯層裡面去核對一下。
那麼首先,SSH 登入到你的 CentOS 資料庫伺服器上。我們要先搞明白,到底是不是資料庫自己占的空間。因為很多時候的話,磁碟滿了是因為應用打出來的日誌檔太大了。或者也有可能是系統核心的 Core Dump 檔案搞出來的情況。
執行以下系統級探測命令:
bash 代碼解讀複製代碼# 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)失效了。也有可能是有一個極長的交易掛在那邊了,歷史日誌堆著沒法循環覆蓋。這時候你去清理業務資料是沒用的,必須去查系統的歸檔設定。
確認了是業務資料,也就是 base 目錄膨脹了以後,我們回到 Win11 本地。接著打開 KStudio 或者 ksql 命令列,進入資料庫的管理視角裡面。
一個資料庫實例下面,通常來說會有好幾個 Database。那麼到底是誰在吃資源呢?金倉資料庫這邊給了幾個很直觀的容量測算函式。執行以下 SQL:
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。
找到這個大庫之後,還得繼續往下看。因為一個庫裡面可能有成百上千張表。我們必須像拿顯微鏡一樣,把那個真正吃空間的表給找出來。
很多新手只會用簡單的語句去查表的資料量,也就是跑個 count(*)。但是資料列數多,其實不代表占的物理磁碟空間就大。一張表可能只有 10 萬列,但是裡面存了長文字,它的體積往往比一張 1000 萬列但只存純數字的表還要大。
所以的話,我給你準備了下面這段企業層級裡的「空間掃描器 SQL」。跑一下,基本上所有表的底層物理資訊就全出來了:
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;

看著上面掃描器出來的這個表格,你要去讀懂裡面藏著的那些技術門道。重點看下面這三個東西:
sys_relation_size): 這個的話,就是業務資料在磁碟上實實在在的占位,也就是 Heap Tuple。就像截圖裡紅箭頭指的那個,那個叫 test_bloat 的模擬業務日誌表,純資料就有 558 MB。這就是整個庫體積暴增的元兇了。那麼現在,我們就要來看看開頭說的那個讓人崩潰的現象了:明明執行了 DELETE,為什麼空間就是不釋放呢?
金倉資料庫為了實現企業層級裡的高併發讀寫,也就是讀不阻塞寫,寫不阻塞讀。它在核心裡面用了 MVCC(多版本並發控制) 這個機制。
這個機制是怎麼運作的呢?當你敲 UPDATE 去改一條資料的時候,底層並不是在原來的地方把舊資料抹掉。它是插入了一條全新的資料版本,然後給舊資料打上一個「已過期(Dead Tuple)」的標記。同樣的道理,你敲 DELETE 刪資料的時候,底層也就是給這些資料打了個「被刪除」的隱形標記。物理磁碟上並沒有馬上把它們擦掉。
那些被打上標記的,物理上還占著磁碟空間的資料,在資料庫工程界叫作「表膨脹(Table Bloat)」。這也就解釋了,為什麼你刪了百萬級的資料以後,去 CentOS 上用 du -sh 看底層的物理檔案,大小還是一點都不變的情況。
動手實驗:一鍵復現表膨脹想親自驗證一下的話,你可以在測試庫跑一下這個腳本:先插 50 萬條長文字資料進去,接著
DELETE刪掉裡面的 30 萬條。這個時候你再去查這個表的物理大小,你會發現空間一點都沒變,這就是經典的表膨脹現場。
那麼我們需要通過系統統計視圖,來看看一張表到底虛胖到什麼程度了。執行以下 SQL:
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% 的情況了。這就說明這張表裡的死資料實在太多了。也就是說,它在那白白占著空間,而且的話,你跑全表掃描的時候,效能往往也會掉得很厲害。)
那麼既然 DELETE 沒法把空間放出來,我們怎麼才能把這些死掉的資料占的物理空間,真正還給作業系統呢?其實,我們需要用到資料庫裡的一個清理功能,也就是清理命令(VACUUM)。
VACUUM 表名;這個操作的話,通常來說是比較輕量的。它會去掃一遍全表,然後把你那些死亡資料,也就是 Dead Tuple 占的空間,標記成可以用的狀態。注意啊:它其實還是不會把空間還給作業系統的,物理檔案的大小也不會變小! 那它有啥用呢?它的作用就是,以後要是再有新的 INSERT 資料進來的話,資料庫會優先去用這些空出來的位置。也就是說,它能擋住物理檔案繼續變大。這個操作是不鎖表的,平時經常跑一跑沒問題。VACUUM FULL 表名;這個才是真正能把空間摳出來,還給 CentOS 作業系統的辦法。那它是怎麼做的呢?底層邏輯其實很簡單的。也就是說,它會去建一個新的物理檔案,接著把活著的那些資料都拷過去,排得緊實一點,最後把原來那個撐大的舊檔案給刪掉。 【高危警告】: 敲 VACUUM FULL 的時候,它會給這張表加上最高等級的排他鎖(AccessExclusiveLock)。啥意思呢?就是拷貝完之前,業務那邊想做增刪改查的話,全都會被堵住,根本執行不了!所以說,除非是半夜那種停機維護的時間段,千萬別在業務跑得最猛的時候去敲這個命令!一直靠 VACUUM FULL 去處理問題,這其實也就是維運層面沒辦法的辦法。那麼,如果你是有架構思維的 DBA 的話,我們往往就需要去規劃一下底層的東西了。也就是說,要從根本上把一個地方容量不夠,還有清理的時候鎖表這些痛點給解決掉。
比如說你的 /opt 所在的系統碟,容量已經到 99% 了。甚至於你想跑個 VACUUM FULL,連要用的暫存交換空間都擠不出來了。那遇到這種情況怎麼辦呢?
這個時候的話,我們可以在 CentOS 上掛一塊新的 2TB 資料碟上去,比如掛到 /data_new。接著,我們用金倉資料庫裡面的表空間(Tablespace)功能,在業務不用停的情況下,把那些很大的歷史表,挪到新的磁碟裡面去:
sql 代碼解讀複製代碼-- 1. 在新磁碟路徑下建立一個名為 ts_archive 的物理表空間
CREATE TABLESPACE ts_archive LOCATION '/data_new/kingbase_ts';
-- 2. 將龐大的歷史表,從預設的滿載磁碟,動態挪移到新的表空間(新磁碟)中
ALTER TABLE trade_history_log SET TABLESPACE ts_archive;
敲了這條指令以後,核心就會直接把這張表的物理檔案,全都搬到 /data_new 底下去了。那麼這樣的話,原來那個快滿的系統碟,也就不用那麼吃緊了。
對於系統日誌表、訂單流水表這種帶著明顯時間屬性,每天瘋狂寫入的資料。真的不要再寫定時任務去 DELETE 一個月前的資料了!因為 MVCC 機制的存在,高頻的大批量 DELETE 簡直就是自己給自己找麻煩。
企業層級裡的終極解決方案是:時間範圍分區表(Range Partition)。
也就是說,在建表初期,我們就按照月份,把一張龐大的邏輯表,在物理底層切成 12 張獨立的子表,就像 log_2025_01, log_2025_02 那樣。
當 1 月份的資料過期了需要清理的時候,我們不再去執行 DELETE 了。而是直接敲一條很輕量的 DDL 命令:
sql 代碼解讀複製代碼DROP TABLE log_2025_01;
這種做法的好處在於: DROP TABLE 根本不走 MVCC 機制,也就不會產生什麼死亡標記。它在 Linux 底層直接就把那個物理檔案給抹掉了,也就是 unlink。100GB 的空間,0.1 秒內瞬間就釋放還給作業系統了。一點表膨脹都不產生,而且也不會影響目前 2 月份子表的寫入!
結語
這篇文章,其實走完了一個挺完整的過程。我們先是在 CentOS 上跑 du -sh 去看物理磁碟的情況。接著用 sys_total_relation_size 去摸清邏輯層面的占用。然後還扒開了 MVCC 底下那個讓人頭痛的「表膨脹」機制。最後呢,又用上了表空間騰挪、分區表這些手段,把整體架構往上提了一下。磁碟空間這事兒,算是實打實地處理了一回。
搞技術的,我們不光要知道怎麼把空間清出來,這只是第一步。更關鍵的是,得搞明白「空間它到底為什麼沒了」,把這個根兒給挖出來。然後,再往前走一步的話,就是通過事先的架構設計,把這些要命的容量問題,在它剛冒出點苗頭的時候就直接按下去。
你想想看,到了這會兒,像連線數爆炸、鎖衝突卡頓、磁碟空間告警這些情況,你都能應付得來。其實這已經算是過了那個只能被動去救火的階段了。但是,在系統更底層的地方,有時候還會藏著一些那種情況。就是平時跑得好好的,偶然給你慢一下,慢得讓人摸不著頭腦的詭異 SQL。
所以,在下一篇裡,咱們這個系列專欄就要進入到最後的部分了,也就是——「獵捕慢查詢:執行計畫的微觀解析與索引調優實戰」。我會跟你一塊兒,去琢磨那些看起來很複雜的「執行樹」到底是怎麼回事。然後把資料庫的效能瓶頸給它控制住。行,那咱們下一篇見吧!