一篇文章徹底搞懂 TiDB 集羣各種容量計算方式

作者丨hey-hoho

來自神州數碼鈦合金戰隊

神州數碼鈦合金戰隊是一支致力於爲企業提供分佈式數據庫 TiDB 整體解決方案的專業技術團隊。團隊成員擁有豐富的數據庫從業背景,全部擁有 TiDB 高級資格證書,並活躍於 TiDB 開源社區,是官方認證合作伙伴。目前已爲 10+ 客戶提供了專業的 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 上報的爲準。

○ PD 源碼倉庫: https://github.com/tikv/pd

○ TiKV 源碼倉庫: https://github.com/tikv/tikv

結尾

結論已經在文章開頭給出了,希望大家看了本文都能對 TiDB 集羣的各種空間計算有了清晰的認識。

本文只討論的 TiKV 的容量計算細節,TiFlash 的計算方式也類似,我對比了 Ti F lash 的數據目錄大小和監控顯示已用大小 10 多 G 的差距,應該也是隻計算了部分目錄,但是總容量還是算的整塊盤。不太熟悉 c++,留給其他大佬去探索吧 。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章