一篇文章彻底搞懂TiDB集群各种容量计算方式
背景
TiDB 集群的監控面板里面有兩個非常重要、且非常常用的指標,相信用了 TiDB 的都見過:
- Storage capacity:集群的總容量
- Current storage size:集群當前已經使用的空間大小
當你準備了一堆服務器,經過各種思考設計部署了一個 TiDB 集群,有沒有想過這兩個指標和服務器磁盤之間到底是啥關系?
反正我們經常被客戶問這個問題,以前雖然能說出個大概,總體方向上沒錯,但是深究一下其實并不嚴謹,這次翻了源碼徹底把這個問題搞清楚。開始之前再賣一個關子,大家可以看看自己手上的集群監控有沒有這種情況:
TiKV 實例的已用空間(store size)+ 可用空間(available size) ≠ 總空間(capacity size)
盤越大越明顯。
再仔細點看,監控上顯示的總容量大小和 TiKV 實例所在盤大小也不匹配。
是不是有億點意外。
結論先行
- PD 監控下的
Storage capacity和Current storage size來自各個 store 的累加,這里 store 包含了 TiKV 和 TiFlash -
Current storage size包含了多個數據副本(TiKV 和 TiFlash 的所有副本數),非真實數據大小 - TiKV 實例容量統計的是 TiKV 所在磁盤的整體大小與
raftstore.capacity參數較小的值,同時監控用的 bytes(SI) 標準顯示,就是說不是用1024做的轉換而是1000,所以和df -h輸出的盤大小有差距 - TiKV 實例的已用空間只統計了
data-dir下的部分目錄,非整個data-dir或整塊盤 - 基于前兩條,可用空間也就不等于總空間減去已用空間了
看到的現象
本文描述的內容基于以下集群:
[tidb@localhost ~]$ tiup cluster display tidb-test
tiup is checking updates for component cluster ...
Starting component `cluster`: /home/tidb/.tiup/components/cluster/v1.13.0/tiup-cluster display tidb-test
Cluster type: ? ? ? tidb
Cluster name: ? ? ? tidb-test
Cluster version: ? v6.5.5
Deploy user: ? ? ? tidb
SSH type: ? ? ? ? ? builtin
Dashboard URL: ? ? http://x.236:2379/dashboard
Grafana URL: ? ? ? http://x.242:3000
ID ? ? ? ? Role ? ? ? Host ? Ports ? ? ? OS/Arch ? ? ? Status ? Data Dir ? ? Deploy Dir
-- ? ? ? ? ---- ? ? ? ---- ? ----- ? ? ? ------- ? ? ? ------ ? -------- ? ? ----------
x.242:3000 ? grafana ? ? x.242 3000 ? ? ? ? linux/x86_64 Up ? ? ? - ? ? ? ? ? /data/tidb-deploy/grafana-3000
x.235:2379 ? pd ? ? ? ? x.235 2379/2380 ? linux/x86_64 Up ? ? ? /data/tidb-data/pd-2379 ? ? ? ? ? ? ? /data/tidb-deploy/pd-2379
x.236:2379 ? pd ? ? ? ? x.236 2379/2380 ? linux/x86_64 Up|L|UI /data/tidb-data/pd-2379 ? ? ? ? ? ? ? /data/tidb-deploy/pd-2379
x.237:2379 ? pd ? ? ? ? x.237 2379/2380 ? linux/x86_64 Up ? ? ? /data/tidb-data/pd-2379 ? ? ? ? ? ? ? /data/tidb-deploy/pd-2379
x.242:9090 ? prometheus x.242 9090/12020 ? linux/x86_64 Up ? ? ? /data/tidb-data/prometheus-9090 ? ? ? /data/tidb-deploy/prometheus-9090
x.235:4000 ? tidb ? ? ? x.235 4000/10080 ? linux/x86_64 Up ? ? ? - ? ? ? ? ? ?/data/tidb-deploy/tidb-4000
x.236:4000 ? tidb ? ? ? x.236 4000/10080 ? linux/x86_64 Up ? ? ? - ? ? ? ? ? ? /data/tidb-deploy/tidb-4000
x.237:4000 ? tidb ? ? ? x.237 4000/10080 ? linux/x86_64 Up ? ? ? - ? ? ? ? ? ? /data/tidb-deploy/tidb-4000
x.241:9000 ? tiflash ? ? x.241 9000/8123/3930/20170/20292/8234 linux/x86_64 Up /data/tiflash/tidb-data/tiflash-9000 /data/tiflash/tidb-deploy/tiflash-9000
x.238:20160 tikv ? ? ? x.238 20160/20180 linux/x86_64 Up ? ? ? /data/tidb-data/tikv-20160 ? ? ? ? ? /data/tidb-deploy/tikv-20160
x.239:20160 tikv ? ? ? x.239 20160/20180 linux/x86_64 Up ? ? ? /data/tidb-data/tikv-20160 ? ? ? ? ? /data/tidb-deploy/tikv-20160
x.240:20160 tikv ? ? ? x.240 20160/20180 linux/x86_64 Up ? ? ? /data/tidb-data/tikv-20160 ? ? ? ? ? /data/tidb-deploy/tikv-20160
各節點磁盤情況(來自TiDB Dashboard統計):
在此之前,我一直以為 PD 監控面板下的集群總空間是 PD 讀取了所有 TiKV+TiFlash 實例部署盤的累計大小,所以我嘗試把上圖的4個存儲節點的磁盤容量相加發現并不等于集群總容量(文章開頭的圖片有顯示),差了100多個G:
Dashboard上4個存儲節點磁盤容量均為475.8G,累計容量475.8G * 4 = 1903.2G
Grafana顯示的單個 TiKV 實例:510.9G,總空間:2.04 T
再和操作系統顯示的磁盤容量對比,發現能和 Dashboard 顯示的對應上:
[tidb@localhost ~]$ df -h
Filesystem ? ? ? ? ? ? ? Size Used Avail Use% Mounted on
/dev/mapper/centos-root 476G 347G 110G 77% /
devtmpfs ? ? ? ? ? ? ? ? 31G ? ? 0 ? 31G ? 0% /dev
tmpfs ? ? ? ? ? ? ? ? ? ? 31G 4.0K ? 31G ? 1% /dev/shm
tmpfs ? ? ? ? ? ? ? ? ? ? 31G 3.1G ? 28G 10% /run
tmpfs ? ? ? ? ? ? ? ? ? ? 31G ? ? 0 ? 31G ? 0% /sys/fs/cgroup
/dev/sda1 ? ? ? ? ? ? ? 477M 138M 310M 31% /boot
但仔細看容量單位的區別,發現 Dashboard 顯示的是GiB,Grafana 顯示的是GB,兩者是有區別的。嘗試在系統中用GB顯示磁盤大小:
[tidb@localhost ~]$ df -H
Filesystem ? ? ? ? ? ? ? Size Used Avail Use% Mounted on
/dev/mapper/centos-root 511G 372G 118G 77% /
devtmpfs ? ? ? ? ? ? ? ? 34G ? ? 0 ? 34G ? 0% /dev
tmpfs ? ? ? ? ? ? ? ? ? ? 34G 4.1k ? 34G ? 1% /dev/shm
tmpfs ? ? ? ? ? ? ? ? ? ? 34G 3.3G ? 30G 10% /run
tmpfs ? ? ? ? ? ? ? ? ? ? 34G ? ? 0 ? 34G ? 0% /sys/fs/cgroup
/dev/sda1 ? ? ? ? ? ? ? 500M 145M 325M 31% /boot
這里輸出的511G能和 Grafana 監控對應上,同時按4個511G的存儲節點計算也能和總容量對應上。
但是用同樣的方法并不能解釋 TiKV 已用空間的偏差問題,檢查結果如下:
# 輸出內容為240節點tikv數據目錄大小,但監控顯示tikv已用空間333.7GB
[tidb@localhost ~]$ du -sh /data/tidb-data/tikv-20160
329G /data/tidb-data/tikv-20160
[tidb@localhost ~]$ du -sh --si /data/tidb-data/tikv-20160
353G /data/tidb-data/tikv-20160
總結一下看到的現象:
- Dashboard 上顯示的 TiKV 盤大小(GiB)是實際部署盤的總大小,Grafana 也是部署盤的總大小但單位是GB
- Grafana 集群總容量是所有存儲節點部署盤的累計大小(GB)
- TiKV 實例已用空間大小計算方式未知(要搞清楚只能扒源碼了)
不同進制轉換帶來的影響
這里簡單提一下GB和GiB的區別,幫助大家理解。
- GB 是按10進制來轉換,也就是說1GB=1000MB,市面上廠商宣傳的大小都是10進制,可理解為商業標準
- GiB 是按2進制來轉換,也就是說1GiB=1024MiB,計算機系統只認這個,可理解為事實標準
那么當你買了一臺128G存儲的手機,實際使用中會發現空間“縮水”了,U盤、硬盤等也類似。
與這兩個進制差異有關的還有兩個行業標準,即byte(SI)和byte(IEC),感興趣的可以去查一下歷史,這里只需要知道:
-
byte(SI)對應十進制 -
byte(IEC)對應二進制
Grafana 里面可以使用編輯監控面板調整顯示單位,例如:
如果把單位統一的話,前2個現象就很好解釋了。
但需要注意的是,在 Grafana 中并不是所有面板都采用了byte(SI),甚至同一個指標也出現不同面板顯示的單位不一樣,比如Overview下面的TiDB分組內存面板使用十進制,System Info分組內存面板使用二進制,用的時候要小心。
TiKV 的數據文件
要搞清楚 TiKV 的已用空間是怎么計算的,先提一下 TiKV 相關的數據文件。大家都知道 TiKV 底層用的 RocksDB 作為持久層,并且 raft 日志和實際數據分別對應一個RocksDB 實例,那么看看 TiKV 的數據目錄到底放了啥東西,以前面的集群為例:
[tidb@localhost ~]$ cd /data/tidb-data/tikv-20160
[tidb@localhost tikv-20160]$ ll -h
total 14G
drwxr-xr-x 2 tidb tidb 1.1M Dec 22 15:22 db
drwxr-xr-x 4 tidb tidb 132K Dec 21 18:20 import
-rw-r--r-- 1 tidb tidb 20K Nov 15 14:47 last_tikv.toml
-rw-r--r-- 1 tidb tidb ? 0 Nov 15 14:53 LOCK
-rw-r--r-- 1 tidb tidb 301M Mar 3 2023 raftdb-2023-03-03T17-32-13.506.info
...
-rw-r--r-- 1 tidb tidb 301M Nov 13 21:51 raftdb-2023-11-13T21-51-00.249.info
-rw-r--r-- 1 tidb tidb 31M Nov 15 14:47 raftdb.info
drwxr-xr-x 2 tidb tidb 4.0K Dec 22 00:08 raft-engine
-rw-r--r-- 1 tidb tidb 301M Jan 18 2023 rocksdb-2023-01-18T10-12-55.000.info
...
-rw-r--r-- 1 tidb tidb 301M Dec 7 03:01 rocksdb-2023-12-07T03-01-02.905.info
-rw-r--r-- 1 tidb tidb 197M Dec 22 17:11 rocksdb.info
drwxr-xr-x 2 tidb tidb 4.0K Dec 21 16:27 snap
-rw-r--r-- 1 tidb tidb 4.8G Nov 15 14:53 space_placeholder_file
幾類文件解讀一下:
- db 目錄,這是最終數據的存放目錄,
db在源碼中寫死無法修改 - rocksdb[-xxx-xxx].info文件,數據 RocksDB 實例的日志文件,已經按日期歸檔好的可手動刪除
- raft-engine 目錄,這是 raft 日志存放目錄,受參數
raft-engine.dir控制,沒有開啟Raft Engine特性時名稱默認為raft,受參數raftstore.raftdb-path控制 - raftdb[-xxx-xxx].info文件,raft日志 RocksDB 實例的日志文件,已經按日期歸檔好的可手動刪除
- snap 目錄,快照數據存放目錄
- import 目錄,看名字是和導入相關,具體什么作用未知
- space_placeholder_file 文件,預留空間的臨時文件(TiKV磁盤告警救急用,磁盤越大這個文件越大),相關參數
storage.reserve-space - last_tikv.toml 和 LOCK 文件,看名字猜測就行
從前面的觀察來看,被監控統計到的 TiKV 已用空間比整個數據目錄要小,那么可以推測出只統計了數據目錄下的部分文件或目錄,具體是哪些就要從源碼里尋找答案。
Show Me The Code
TiDB的監控數據分為兩類,一類是服務器環境信息(CPU、內存、磁盤、網絡等),一類是TiDB運行指標(Duration、QPS、Region數、容量等)。前者通過與Prometheus配套的標準探針采集,即node_exporter和black_exporter,后者通過在源碼中類似埋點方式采集數據然后由Prometheus來拉取。
import (
...
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/collectors"
...
)
?
var (
// PanicCounter measures the count of panics.
PanicCounter *prometheus.CounterVec
?
// MemoryUsage measures the usage gauge of memory.
MemoryUsage *prometheus.GaugeVec
)
以集群總容量這個指標入手,看看在源碼中它是如何采集的。對應的公式:
pd_cluster_status{k8s_cluster="$k8s_cluster", tidb_cluster="$tidb_cluster", instance="$instance",type="storage_capacity"}
用關鍵字storage_capacity去 PD 源碼里搜索找到如下代碼:
func (s *storeStatistics) Collect() {
placementStatusGauge.Reset()
?
metrics := make(map[string]float64)
...
metrics["storage_capacity"] = float64(s.StorageCapacity)
?
for typ, value := range metrics {
clusterStatusGauge.WithLabelValues(typ).Set(value)
}
...
}
數據來自storeStatistics的StorageCapacity字段,根據引用關系繼續往上翻:
func (s *storeStatistics) Observe(store *core.StoreInfo) {
...
// Store stats.
s.StorageSize += store.StorageSize()
s.StorageCapacity += store.GetCapacity()
? ?...
}
從這里可以看出總容量(Storage capacity)和總已用空間(Current storage size)都是從各個store累加得來,并不是pd直接從存儲節點計算。
繼續看GetCapacity()是如何實現:
func (ss *storeStats) GetCapacity() uint64 {
ss.mu.RLock()
defer ss.mu.RUnlock()
return ss.rawStats.GetCapacity()
}
func newStoreStats() *storeStats {
return &storeStats{
rawStats: ? ? &pdpb.StoreStats{},
avgAvailable: movingaverage.NewHMA(60), // take 10 minutes sample under 10s heartbeat rate
}
}
這里rawStats 是一個pdpb.StoreStats類型,引用了另一個倉庫:https://github.com/pingcap/kvproto 。最終實現為:
// https://github.com/pingcap/kvproto/blob/master/pkg/pdpb/pdpb.pb.go#L4371C1-L4376C2
func (m *StoreStats) GetCapacity() uint64 {
if m != nil {
return m.Capacity
}
return 0
}
從調用關系來看,說明 PD 采集的數據都是來自 TiKV 上報(heartbeat)。繼續追蹤 TiKV 源碼,以heartbeat為突破口:
pub fn handle_store_heartbeat(
? ? ? &mut self,
? ? ? ?mut stats: pdpb::StoreStats,
? ? ? ?is_fake_hb: bool,
? ? ? ?store_report: Option<pdpb::StoreReport>,
? ) {
? ? ? ...
? ? ? ?let (capacity, used_size, available) = self.collect_engine_size().unwrap_or_default();
? ? ? ?if available == 0 {
? ? ? ? ? ?warn!(self.logger, "no available space");
? ? ? }
?
? ? ? ?stats.set_capacity(capacity);
? ? ? ?stats.set_used_size(used_size);
? ? ? ?stats.set_available(available);
? ? ? ...
}
這里的stats正是一個pdpb::StoreStats類型,我們想要分析的3個指標都在這,繼續看他們的出處collect_engine_size():
fn collect_engine_size<EK: KvEngine, ER: RaftEngine>(
? ?coprocessor_host: &CoprocessorHost<EK>,
? ?store_info: Option<&StoreInfo<EK, ER>>,
? ?snap_mgr_size: u64,
) -> Option<(u64, u64, u64)> {
? ?if let Some(engine_size) = coprocessor_host.on_compute_engine_size() {
? ? ? ?return Some((engine_size.capacity, engine_size.used, engine_size.avail));
? }
? ?let store_info = store_info.unwrap();
? ?// 這里跟根據kv engine的目錄(${data_dir}/db)獲取了所在磁盤的信息
? ?let disk_stats = match fs2::statvfs(store_info.kv_engine.path()) {
? ? ? ?Err(e) => {
? ? ? ? ? ?error!(
? ? ? ? ? ? ? ?"get disk stat for rocksdb failed";
? ? ? ? ? ? ? ?"engine_path" => store_info.kv_engine.path(),
? ? ? ? ? ? ? ?"err" => ?e
? ? ? ? ? );
? ? ? ? ? ?return None;
? ? ? }
? ? ? ?Ok(stats) => stats,
? };
? ?// total_space得到磁盤的總容量,參考API:https://docs.rs/fs2/latest/fs2/fn.total_space.html
? ?let disk_cap = disk_stats.total_space();
? ?// 計算得出tikv實例的容量,用磁盤容量與參數設置的容量(raftstore.capacity)相比
? ?// 如果沒有設置raftstore.capacity參數,或者是磁盤容量小于設置的容量,那么取磁盤容量,否則取設置的容量,本質就是取較小的那個
? ?let capacity = if store_info.capacity == 0 || disk_cap < store_info.capacity {
? ? ? ?disk_cap
? } else {
? ? ? ?store_info.capacity
? };
? ?// 計算已用大小,快照大小(snap目錄) + kv engine大小(db目錄) + raft engine大小(raft-engine目錄)
? ?let used_size = snap_mgr_size
? ? ? ?+ store_info
? ? ? ? ? .kv_engine
? ? ? ? ? .get_engine_used_size()
? ? ? ? ? .expect("kv engine used size")
? ? ? ?+ store_info
? ? ? ? ? .raft_engine
? ? ? ? ? .get_engine_size()
? ? ? ? ? .expect("raft engine used size");
? ?// 計算邏輯可用空間,總容量-已用空間
? ?let mut available = capacity.checked_sub(used_size).unwrap_or_default();
? ?// We only care about rocksdb SST file size, so we should check disk available
? ?// here.
? ?// 最終可用空間是取邏輯可用和磁盤可用的較小值
? ?available = cmp::min(available, disk_stats.available_space());
? ?Some((capacity, used_size, available))
}
核心邏輯分析都寫在注釋里,值得認真一看!
以為扒到這里就happy ending了,但是偶然又發現了另一個方法讓我陷入沉思:
? ?fn init_storage_stats_task(&self, engines: Engines<RocksEngine, ER>) {
? ? ? ...
? ? ? ?self.background_worker
? ? ? ? ? .spawn_interval_task(DEFAULT_STORAGE_STATS_INTERVAL, move || {
? ? ? ? ? ? ? ?let disk_stats = match fs2::statvfs(&store_path) {
? ? ? ? ? ? ? ? ? ?Err(e) => {
? ? ? ? ? ? ? ? ? ? ? ?error!(
? ? ? ? ? ? ? ? ? ? ? ? ? ?"get disk stat for kv store failed";
? ? ? ? ? ? ? ? ? ? ? ? ? ?"kv path" => store_path.to_str(),
? ? ? ? ? ? ? ? ? ? ? ? ? ?"err" => ?e
? ? ? ? ? ? ? ? ? ? ? );
? ? ? ? ? ? ? ? ? ? ? ?return;
? ? ? ? ? ? ? ? ? }
? ? ? ? ? ? ? ? ? ?Ok(stats) => stats,
? ? ? ? ? ? ? };
? ? ? ? ? ? ? ?let disk_cap = disk_stats.total_space();
? ? ? ? ? ? ? ?let snap_size = snap_mgr.get_total_snap_size().unwrap();
?
? ? ? ? ? ? ? ?let kv_size = engines
? ? ? ? ? ? ? ? ? .kv
? ? ? ? ? ? ? ? ? .get_engine_used_size()
? ? ? ? ? ? ? ? ? .expect("get kv engine size");
?
? ? ? ? ? ? ? ?let raft_size = engines
? ? ? ? ? ? ? ? ? .raft
? ? ? ? ? ? ? ? ? .get_engine_size()
? ? ? ? ? ? ? ? ? .expect("get raft engine size");
? ? ? ? ? ? ? ...
? ? ? ? ? ? ? ?let placeholer_file_path = PathBuf::from_str(&data_dir)
? ? ? ? ? ? ? ? ? .unwrap()
? ? ? ? ? ? ? ? ? .join(Path::new(file_system::SPACE_PLACEHOLDER_FILE));
?
? ? ? ? ? ? ? ?let placeholder_size: u64 =
? ? ? ? ? ? ? ? ? ?file_system::get_file_size(placeholer_file_path).unwrap_or(0);
? ? ? ? ? ? ? ?// 這里的已用空間計算方式與heartbeat有區別,把space_placeholder_file文件算進去了,還加了raft engine單獨部署的邏輯
? ? ? ? ? ? ? ?let used_size = if !separated_raft_mount_path {
? ? ? ? ? ? ? ? ? ?snap_size + kv_size + raft_size + placeholder_size
? ? ? ? ? ? ? } else {
? ? ? ? ? ? ? ? ? ?snap_size + kv_size + placeholder_size
? ? ? ? ? ? ? };
? ? ? ? ? ? ? ?let capacity = if config_disk_capacity == 0 || disk_cap < config_disk_capacity {
? ? ? ? ? ? ? ? ? ?disk_cap
? ? ? ? ? ? ? } else {
? ? ? ? ? ? ? ? ? ?config_disk_capacity
? ? ? ? ? ? ? };
?
? ? ? ? ? ? ? ?let mut available = capacity.checked_sub(used_size).unwrap_or_default();
? ? ? ? ? ? ? ?available = cmp::min(available, disk_stats.available_space());
? ? ? ? ? ? ? ...
? ? ? ? ? })
? }
init_storage_stats_task在tikv啟動時被調用,就是說這是幾個指標在初始化時的計算方式,整體邏輯與heartbeat上報并無區別,但已用空間計算方式有輕微差異:
- space_placeholder_file 被算進去了
- 如果raft engine使用了單獨的部署目錄(代碼里叫path_in_diff_mount_point),那么raft日志的大小是不算在tikv已用空間內的
看起來前后計算不一致,但是由于heartbeat是持續更新的,最終是以heartbeat上報的為準。
這個差異準備提issue問問看。
PD 源碼倉庫:https://github.com/tikv/pd
TiKV 源碼倉庫:https://github.com/tikv/tikv
結尾
結論已經在文章開頭給出了,希望大家看了本文都能對TiDB集群的各種空間計算有了清晰的認識。
本文只討論的 TiKV 的容量計算細節,TiFlash 的計算方式也類似,我對比了 TFlash 的數據目錄大小和監控顯示已用大小10多G的差距,應該也是只計算了部分目錄,但是總容量還是算的整塊盤。不太熟悉 c++,留給其他大佬去探索吧??。
作者介紹:hey-hoho,來自神州數碼鈦合金戰隊,是一支致力于為企業提供分布式數據庫TiDB整體解決方案的專業技術團隊。團隊成員擁有豐富的數據庫從業背景,全部擁有TiDB高級資格證書,并活躍于TiDB開源社區,是官方認證合作伙伴。目前已為10+客戶提供了專業的TiDB交付服務,涵蓋金融、證券、物流、電力、*、零售等重點行業。
總結
以上是生活随笔為你收集整理的一篇文章彻底搞懂TiDB集群各种容量计算方式的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 大龄码农的业余作品:升讯威在线客服系统:
- 下一篇: 快速批量升级 NugetPackage