Ceph中的容量計算與管理


轉自:https://www.ustack.com/blog/ceph%ef%bc%8drongliang/

t01c0d4e027bd6c3c14

在部署完Ceph集群之后,一般地我們可以通過Ceph df這個命令來查看集群的容量狀態,但是Ceph是如何計算和管理的呢?相信大家都比較好奇。因為用過 ceph df這個命令的人都會有這個疑問,它的輸出到底是怎么計算的呢?為什么所有pool的可用空間有時候等於GLOBAL中的可用空間,有時候不等呢? 帶着這些疑問我們可以通過分析ceph df的實現,來看看Ceph是如何計算容量和管理容量的。

一般情況下ceph df的輸出如下所示:

ceph-df
 
1
2
3
4
5
6
7
8
[root@study-1 ~] # ceph df
GLOBAL:
     SIZE     AVAIL      RAW USED     %RAW USED 
     196G     99350M       91706M         45.55 
POOLS:
     NAME           ID     USED       %USED     MAX AVAIL     OBJECTS 
     rbd            1      20480k      0.02        49675M          11 
     x              2         522         0        49675M          11

從上面的輸出可以看到,ceph對容量的計算其實是分為兩個維度的。一個是GLOBAL維度,一個是POOLS的維度。

GLOBAL 維度中有SIZE,AVAIL,RAW USED,%RAW USED。

POOLS 維度中有 USED,%USED,MAX AVAIL,OBJECTS。

我們這里先把注意力放在RAW USED,和AVAIL上。這個兩個分析清楚之后,其它的也就迎刃而解了。

這里我們粗略算一下GLOBAL中的RAW USED 為91706M,明顯大於下面pool 中USED 20480k*3 + 522bytes*3啊。而且各個pool的MAX AVAIL 相加並不等於GLOBAL中的AVAIL。我們需要深入代碼分析一下為什么。

分析

Ceph 命令基本上都是首先到Montior這里,如何Monitor能處理請求,就直接處理,不能就轉發。

我們看看Monitor是如何處理ceph df這個命令的。Monitor處理命令主要是在Monitor::hanlde_command函數里。

handle_command
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
else  if  (prefix ==  "df" ) {
       bool  verbose = (detail ==  "detail" );
       if  (f)
         f->open_object_section( "stats" );
       pgmon()->dump_fs_stats(ds, f.get(), verbose);
       if  (!f)
         ds <<  '\n' ;
       pgmon()->dump_pool_stats(ds, f.get(), verbose);
       if  (f) {
         f->close_section();
         f->flush(ds);
         ds <<  '\n' ;
       }
     }

從上面的代碼可以知道,主要是兩個函數完成了df命令的輸出。一個是pgmon()->dump_fs_stats,另一個是pgmon()->dump_pool_stats。

dump_fs_stats 對應GLOBAL這個維度。dump_pool_stats對應POOLS這個維度。

  • GLOBAL維度

從PGMonitor::dump_fs_stats開始:

dump_fs_stats
 
1
2
3
4
5
6
7
8
9
10
11
12
void  PGMonitor::dump_fs_stats(stringstream &ss, Formatter *f,  bool  verbose)  const
{
   if  (f) {
     f->open_object_section( "stats" );
     f->dump_int( "total_bytes" , pg_map.osd_sum.kb * 1024ull);
     f->dump_int( "total_used_bytes" , pg_map.osd_sum.kb_used * 1024ull);
     f->dump_int( "total_avail_bytes" , pg_map.osd_sum.kb_avail * 1024ull);
     if  (verbose) {
       f->dump_int( "total_objects" , pg_map.pg_sum.stats.sum.num_objects);
     }
     f->close_section();
   }

可以看到相關字段數值的輸出主要依賴pg_map.osd_sum的值,而osd_sum是各個osd_stat的總和。所以我們需要知道單個osd的osd_stat_t是如何計算的。

stat_pg_update
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
void  OSDService::update_osd_stat(vector< int >& hb_peers)
{
   Mutex::Locker lock(stat_lock);
   osd_stat.hb_in.swap(hb_peers);
   osd_stat.hb_out.clear();
   osd->op_tracker.get_age_ms_histogram(&osd_stat.op_queue_age_hist);
   // fill in osd stats too
   struct  statfs stbuf;
   int  r = osd->store->statfs(&stbuf);
   if  (r < 0) {
     derr <<  "statfs() failed: "  << cpp_strerror(r) << dendl;
     return ;
   }
   uint64_t bytes = stbuf.f_blocks * stbuf.f_bsize;
   uint64_t used = (stbuf.f_blocks - stbuf.f_bfree) * stbuf.f_bsize;
   uint64_t avail = stbuf.f_bavail * stbuf.f_bsize;
   osd_stat.kb = bytes >> 10;
   osd_stat.kb_used = used >> 10;
   osd_stat.kb_avail = avail >> 10;
   osd->logger->set(l_osd_stat_bytes, bytes);
   osd->logger->set(l_osd_stat_bytes_used, used);
   osd->logger->set(l_osd_stat_bytes_avail, avail);
   check_nearfull_warning(osd_stat);
   dout(20) <<  "update_osd_stat "  << osd_stat << dendl;
}

從上面我們可以看到update_osd_stat 主要是通過osd->store->statfs(&stbuf),來更新osd_stat的。因為這里使用的是Filestore,所以需要進入FileStore看其是如何statfs的。

FIleStore::statfs
1
2
3
4
5
6
7
8
9
10
int  FileStore::statfs( struct  statfs *buf)
{
   if  (::statfs(basedir.c_str(), buf) < 0) {
     int  r = - errno ;
     assert (!m_filestore_fail_eio || r != -EIO);
     assert (r != -ENOENT);
     return  r;
   }
   return  0;
}

可以看到上面FileStore主要是通過::statfs()這個系統調用來獲取信息的。這里的basedir.c_str()就是data目錄。所以osd_sum計算的就是將所有osd 數據目錄的磁盤使用量加起來。回到上面的輸出,因為我使用的是一個磁盤上的目錄,所以在statfs的時候,會把該磁盤上的其它目錄也算到Raw Used中。回到上面的輸出,因為使用兩個OSD,且每個OSD都在同一個磁盤下,所以GLOBAL是這么算的

1

同上,就知道Ceph如何算Raw Used,AVAIL的。

  • POOLS維度

從PGMonitor::dump_pool_stats()來看,該函數以pool為粒度進行循環,通過 pg_map.pg_pool_sum來獲取pool的信息。其中USED,%USED,OBJECTS是根據pg_pool_sum的信息算出來的。而MAX AVAIL 是單獨算出來的。

這里有一張圖,可以幫助同學們梳理整個的流程。中間僅取了一些關鍵節點。有一些省略,如想知道全貌,可以在PGMonitor::dump_pool_stats查閱。

2

通過分析代碼我們知道,pool的使用空間(USED)是通過osd來更新的,因為有update(write,truncate,delete等)操作的的時候,會更新ctx->delta_stats,具體請見ReplicatedPG::do_osd_ops。舉例的話,可以從處理WRITE的op為入手點,當處理CEPH_OSD_OP_WRITE類型的op的時候,會調用write_update_size_and_usage()。里面會更新ctx->delta_stats。當IO處理完,也就是applied和commited之后,會publish_stats_to_osd()。

這里會將變化的pg的stat_queue_item入隊到pg_stat_queue中。然后設置osd_stat_updated為True。入隊之后,由tick_timer在C_Tick_WithoutOSDLock這個ctx中通過send_pg_stats()將PG的狀態發送給Monitor。這樣Monitor就可以知道pg的的變化了。

可用空間,即MAX AVAIL的值,計算稍微有點復雜。Ceph是先計算Available的值,然后根據副本策略再計算MAX AVAIL的值。Available的值是在get_rule_avail()中計算的。在該函數中通過get_rule_weight_osd_map()算出來一個有weight的osd列表。

注意這里的weight一般是小於1的,因為它除以了sum。而sum就是pool中所有osd weight的總和。在拿到weight列表后,就會根據pg_map.osd_stat中kb_avail的值進行除以weight,選出其中最小的,作為Available的值。

這么描述有些抽象了,具體舉一個例子。比如這里我們的pool中有三個osd,假設kb_avail都是400G

即,

{osd_0: 0.9, osd_1, 0.8, osd_2: 0.7}。計算出來的weight值是{osd_0: 0.9/2.4,osd_1: 0.8/2.4,osd_2: 0.7/2.4}

這樣后面用osd的available 空間除以這里的weight值,這里的Available的值就是400G*0.7/2.4。這里附上一個公式,可能更直觀一些。

3 

然后根據你的POOL的副本策略不同,POOL的AVAL計算方式也不同。如果是REP模式,就是直接除以副本數。如果是EC模式,則POOL的AVAL是Available * k / (m + k)。

所以一般情況下,各個POOL的MAX AVAIL之和與GLOBAL的AVAIL是不相等的,但是可以很接近(相差在G級別可以忽略為接近)。

總結

分析到這里,我們知道CEPH中容量的計算是分維度的,如果是GLOBAL維度的話,因為使用的是osd的所在磁盤的statfs來計算所以還是比較准確的。而另一個維度POOLS

由於需要考慮到POOL的副本策略,CRUSH RULE,OSD WEIGHT,計算起來還是比較復雜的。容量的管理主要是在OSD端,且OSD會把信息傳遞給MON,讓MON來維護。

計算osd weight值比較復雜,這里附上算weight的函數,添加了一些注釋,有助於感興趣的同學一起分析。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
int  CrushWrapper::get_rule_weight_osd_map(unsigned ruleno, map< int , float > *pmap)
{
   if  (ruleno >= crush->max_rules)
     return  -ENOENT;
   if  (crush->rules[ruleno] == NULL)
     return  -ENOENT;
   crush_rule *rule = crush->rules[ruleno];
   // build a weight map for each TAKE in the rule, and then merge them
   for  (unsigned i=0; i<rule->len; ++i) {
     map< int , float > m;
     float  sum = 0;
     if  (rule->steps[i].op == CRUSH_RULE_TAKE) { //如果是take的話,則進入
       int  n = rule->steps[i].arg1;
       if  (n >= 0) {  // n如果大於等於0的話是osd,否則是buckets
     m[n] = 1.0;  // 如果是osd的話,因為這里是直接take osd,所有有沒有權重已經不重要了
     sum = 1.0;
       else  // 不是osd,是buckets的話
     list< int > q;
     q.push_back(n);  // buckets 的id 入隊
     //breadth first iterate the OSD tree
     while  (!q.empty()) {
       int  bno = q.front();  // 取出buckets的id
       q.pop_front();   // 出隊
       crush_bucket *b = crush->buckets[-1-bno];  // 根據序號拿到buckets
       assert (b);  // 這個buckets必須是存在的
       for  (unsigned j=0; j<b->size; ++j) {  // 從buckets的items數組中拿相應的bucket
         int  item_id = b->items[j];  
         if  (item_id >= 0) {  //it's an OSD
           float  w = crush_get_bucket_item_weight(b, j);   // 拿出該osd的weight
           m[item_id] = w;  // m 入隊
           sum += w;  // weight加和
         else  //not an OSD, expand the child later
           q.push_back(item_id);   // 如果不是osd,則添加其item_id,所以這里是一個樹的深度遍歷
         }
       }
     }
       }
     }
     for  (map< int , float >::iterator p = m.begin(); p != m.end(); ++p) {
       map< int , float >::iterator q = pmap->find(p->first);
       // 因為我們這里傳入的pmap是沒有數據的
       // 所以第一次必中,
       if  (q == pmap->end()) {
     (*pmap)[p->first] = p->second / sum;
       else  {
     // 這里還需要考慮osd在不同的buckets里的情況
     q->second += p->second / sum;
       }
     }
   }
   return  0;
}

關於作者:

李田清:UnitedStack有雲存儲工程師,3年OpenStack開發和架構經驗,熟悉分布式存儲系統。主要關注分布式存儲,與雲計算領域。致力於將Ceph打造為真正高效,穩定的,能滿足客戶真實需求的分布式存儲。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM