ITPub博客

首页 > 自动化运维 > > cephfs:负载均衡

cephfs:负载均衡

作者:浪花小雨滴 时间:2020-05-25 14:25:41 0 删除 编辑

分布式文件系统中,多活MDS必须对整个文件系统的管理进行分区,以便进行扩展。当MDS的负载不平衡的时候,通过子树分区来平衡mds间的负载。目前有两种分区方法:静态子树分区(也称静态pin)和动态子树分区。

静态子树分区

比较简单的方法是静态分区目录层次结构,在某些情况下,当有必要修正MDS间负载不平衡时迁移子树,将子树分配给特定的MDS。之后不管负载如何变化,也不会再迁移该子树,以及下面的子目录。通常需要系统管理员的干预才能在MDS间重新分区手动重新平衡负载。适用于数据位置固定的场景,不适合动态扩展的场景等。

将指定子树pin到相应的mds上面,如下:

# 将/mnt/test1 pin到mds.1
#setfattr -n ceph.dir.pin -v <rank> <path>

setfattr -n ceph.dir.pin -v 1 /mnt/test1

取消pin,将<rank>置为-1即可

setfattr -n ceph.dir.pin -v -1 /mnt/test1

具体的代码就不分析了。

动态子树分区

通过实时监控集群节点的负载,动态调整层次结构的子树分布于不同的MDS节点:MDS间会周期性的通过MSG_MDS_HEARTBEAT消息交互,该消息中包含了各自的负载,负载较高的节点根据本地目录的热度决定迁移子树的大小,将部分子树迁移到负载较轻的节点上。在子树迁移过程中,会阻塞来自客户端的请求,直到子树迁移完成,所以大量或频繁的子树迁移肯定会导致业务抖动,影响集群性能。为了确保子树分割尽量简单,负载较高的节点会优先将之前别的节点迁给它的子树原路迁回去,同时也减少了子树迁移过程造成的额外负担(在迁移过程中,不用再发送元数据信息)。在学习动态子树分区中有两个主要疑点:负载是怎么均衡的,以及怎么根据负载来选择子树;迁移是怎么具体迁的。根据这两个疑问,将动态子树分区分为两部分:负载均衡和子树迁移。


负载均衡

每个MDS通过每个inode和目录分片相关联的计数器来监视缓存元数据的热度。每个inode热度向量都包括了一个read和一个write计数器,而目录分片还监视readdir操作热度、从对象存储中提取热度、提交到对象存储的热度。除了其自身的热度(pop_me),每个目录分片还维护三个额外的负载向量,这些向量构成对层次结构中更深层嵌套的元数据的热度总和:一个针对所有嵌套元数据(pop_nested),一个仅针对当前子树的权威元数据(pop_auth_subtree),一个针对当前节点为权威的所有嵌套元数据(pop_auth_subtree_nested)。当MDS处理客户端请求时,将在受影响的元数据及其祖先上增加适当的计数,以提供元数据热度的层次结构视图,进而将其用于通知副本和迁移决策。关于热度的计算,见如下文章

gala张:cephfs:MDS中元数据的热度​zhuanlan.zhihu.com图标

MDS集群中的节点定期共享它们的总体负载级别,以及由每个子树的直接祖先的权威MDS分组的本地管理元数据的累积热度。这样,每个超负荷的节点都可以确定要减少总负载的一部分到低负荷的节点,使用层次结构的热度统计来选择要迁移的适当大小的子树。

下面结合代码和具体场景来分析负载均衡。化繁为简,从最简单的场景来分析:

场景:2个active的mds:mds.0,mds.1,其中暂时没有子树分布在mds.1上


MDS节点负载

mds_load_t定义如下

struct mds_load_t {
  dirfrag_load_vec_t auth;      // 根目录"/"的CDir的pop_auth_subtree_nested
                                // 即auth的目录分片元数据负载
  dirfrag_load_vec_t all;       // 根目录"/"的CDir的pop_nested
                                // 即所有目录分片的元数据负载
  double req_rate = 0.0;        // tick周期内mds处理的客户端请求数
  double cache_hit_rate = 0.0;  
  double queue_len = 0.0;       // 在待分发队列中的请求数 
  double cpu_load_avg = 0.0;    // cpu的负载
  ...
}

mds的节点负载值通过mds_load_t::mds_load来计算

double mds_load_t::mds_load()
{
  switch(g_conf->mds_bal_mode) {   // mds_bal_mode = 0
  case :
    return
      .8 * auth.meta_load() + .2 * all.meta_load() + req_rate + 10.0 * queue_len;
  case 1:
    return req_rate + 10.0*queue_len;
  case 2:
    return cpu_load_avg;
  }
  ceph_abort();
  return ;
}

即计算公式如下:

mds_load = 0.8 * auth.meta_load + 0.2 * all.meta_load + req_rate + 10.0 * queue_len


目录分片负载

dirfrag_load_vec_t的定义如下

class dirfrag_load_vec_t {
public:
  static const int NUM = 5;
  std::vector < DecayCounter > vec;
  ...
  double meta_load() {
    return 
      1*vec[META_POP_IRD].get_last() + 
      2*vec[META_POP_IWR].get_last() +
      1*vec[META_POP_READDIR].get_last() +
      2*vec[META_POP_FETCH].get_last() +
      4*vec[META_POP_STORE].get_last();
  }
  ...
}

目录分片的负载包括读热度、写热度、读目录热度、元数据从对象存储读入和写入的热度。目录分片的负载值通过meta_load()函数来计算,计算公式如下

meta_load = 1 * read + 2 * write + 1 * readdir + 2 * fetch + 4 * store


心跳交互

MDS0每10秒发起心跳,其他的mds节点收到mds0的心跳后,分别向除自己外的所有mds节点发送自己的心跳。流程图如下。

MDBalancer::send_heartbeat函数如下

void MDBalancer::send_heartbeat()
{
  // my load
  mds_load_t load = get_load(now);   // 得到自己的负载
  map<mds_rank_t, mds_load_t>::value_type val(mds->get_nodeid(), load);
  mds_load.insert(val);
  // import_map -- how much do i import from whom
  map<mds_rank_t, float> import_map;
  set<CDir*> authsubs;
  mds->mdcache->get_auth_subtrees(authsubs);  // 此时mds0就两个子树:根目录"/"和"mds0"系统目录
  for (set<CDir*>::iterator it = authsubs.begin(); it != authsubs.end(); ++it) {
    CDir *im = *it;
    mds_rank_t from = im->inode->authority().first;
    if (from == mds->get_nodeid()) continue;
    if (im->get_inode()->is_stray()) continue;
    // 此时import_map为空
    import_map[from] += im->pop_auth_subtree.meta_load(now, mds->mdcache->decayrate);
  }
  set<mds_rank_t> up;
  mds->get_mds_map()->get_up_mds_set(up);   // 获取up的mds
  for (set<mds_rank_t>::iterator p = up.begin(); p != up.end(); ++p) {
    if (*p == mds->get_nodeid())
      continue;
    MHeartbeat *hb = new MHeartbeat(load, beat_epoch, last_epoch_under);
    hb->get_import_map() = import_map;
    messenger->send_message(hb, mds->mdsmap->get_inst(*p)); // 将消息发送出去
  }
}

每个mds节点收齐所有mds的心跳信息后,就开始相应的计算,来判断是否要迁移。

处理心跳,MDBalancer::handle_heartbeat函数如下

void MDBalancer::handle_heartbeat(MHeartbeat *m)
{
  ...
  if (who == ) {  // 心跳来自mds0时,发送心跳给其他mds
    if (beat_epoch != m->get_beat())  mds_load.clear();
    beat_epoch = m->get_beat();
    send_heartbeat();
  }
  {
    // set mds_load[who]
    mds_load_map_t::value_type val(who, m->get_load());
    pair < mds_load_map_t::iterator, bool > rval (mds_load.insert(val));
    if (!rval.second) {
      rval.first->second = val.second;
    }
  }
  mds_import_map[ who ] = m->get_import_map();
  mds_last_epoch_under_info[who] = m->get_last_epoch_under();
  {
    unsigned cluster_size = mds->get_mds_map()->get_num_in_mds();
    if (mds_load.size() == cluster_size) {  // 所有节点的心跳信息收集齐,开始准备均衡
      ...
      prep_rebalance(m->get_beat());
    }
  }
  ...
}


迁移发动条件

从mds.0的角度来分析。首先计算当前节点中auth的目录元数据负载(auth.meta_load)在所有负载(mds_laod)中锁占比例,计算公式:load_fac = auth.meta_load / mds_load。代码如下

void MDBalancer::prep_rebalance(int beat)
{
  ...
  if (g_conf->mds_thrash_exports) { ...
  } else {
    int cluster_size = mds->get_mds_map()->get_num_in_mds();   // cluster_size = 2
    mds_rank_t whoami = mds->get_nodeid();                     // whoami = 0
    rebalance_time = ceph_clock_now();
    // rescale!  turn my mds_load back into meta_load units
    double load_fac = 1.0;
    map<mds_rank_t, mds_load_t>::iterator m = mds_load.find(whoami);
    if ((m != mds_load.end()) && (m->second.mds_load() > )) {
      double metald = m->second.auth.meta_load(rebalance_time, mds->mdcache->decayrate);
      double mdsld = m->second.mds_load();
      load_fac = metald / mdsld;
    }
  ...
}

本场景中mds.1上没有子树,mds.0上的子树都是权威的,所以load_fac ≈ 1.0。接着计算整个集群中总的元数据负载(total_load),计算公式如下:

total_load = \sum_{i = 0}^{n}(mdsload(i)) * loadfac

总负载除以mds个数就得到平均负载target_load,也即各个mds需要平衡到元数据负载的平均值。代码如下。

void MDBalancer::prep_rebalance(int beat)
{
    ...
    double total_load = 0.0;
    multimap<double,mds_rank_t> load_map;
    // 计算total_load
    for (mds_rank_t i=mds_rank_t(); i < mds_rank_t(cluster_size); i++) {
      map<mds_rank_t, mds_load_t>::value_type val(i, mds_load_t(ceph_clock_now()));
      std::pair < map<mds_rank_t, mds_load_t>::iterator, bool > r(mds_load.insert(val));
      mds_load_t &load(r.first->second);
      double l = load.mds_load() * load_fac;
      mds_meta_load[i] = l;
      if (whoami == i) my_load = l;
      total_load += l;
      load_map.insert(pair<double,mds_rank_t>( l, i ));
    }
    // target load
    target_load = total_load / (double)cluster_size;
    ...
}

接下来通过target_load与自己节点的负载比较,如果两次满足:my_load > 1.1 * target_load,就可以发动迁移。代码如下。

void MDBalancer::prep_rebalance(int beat)
{
    ...
    // under or over?
    if (my_load < target_load * (1.0 + g_conf->mds_bal_min_rebalance)) {
      dout(5) << "  i am underloaded or barely overloaded, doing nothing." << dendl;
      last_epoch_under = beat_epoch;
      mds->mdcache->show_subtrees();
      return;
    }
    // am i over long enough?
    if (last_epoch_under && beat_epoch - last_epoch_under < 2) {
      dout(5) << "  i am overloaded, but only for " << (beat_epoch - last_epoch_under) << " epochs" << dendl;
      return;
    }
   ...
}


负载重匹配

在本场景中mds.0的负载为80,mds.1的负载为0,mds.0中关键的变量如下

total_load = 80,target_load = 40,my_load = 80

所以mds.0就是exporter,mds.1是importer,迁移的负载大小为80 - 40 = 40。代码如下

void MDBalancer::prep_rebalance(int beat)
{
  ...
   // 填充exporter和importer
    multimap<double,mds_rank_t> importers;
    multimap<double,mds_rank_t> exporters;
    set<mds_rank_t>             importer_set;
    set<mds_rank_t>             exporter_set;
    for (multimap<double,mds_rank_t>::iterator it = load_map.begin();it != load_map.end(); ++it) {
      if (it->first < target_load) {
	importers.insert(pair<double,mds_rank_t>(it->first,it->second));
	importer_set.insert(it->second);
      } else if (it->first > target_load * (1.0 + g_conf->mds_bal_min_rebalance)) {
        int mds_last_epoch_under = (it->second == whoami) ?  : mds_last_epoch_under_info[it->second];
        if (!mds_last_epoch_under || beat_epoch - mds_last_epoch_under >= 2) {
	  exporters.insert(pair<double,mds_rank_t>(it->first,it->second));
	  exporter_set.insert(it->second);
        }
      }
    }

    if (true) {
      // big -> small exporters
      for (multimap<double,mds_rank_t>::reverse_iterator ex = exporters.rbegin(); ex != exporters.rend(); ++ex) {
	double maxex = get_maxex(state, ex->second);    // maxex = 40
	// check importers. for now, just in arbitrary order (no intelligent matching).
	for (map<mds_rank_t, float>::iterator im = mds_import_map[ex->second].begin();
	     im != mds_import_map[ex->second].end();
	     ++im) {
	  double maxim = get_maxim(state, im->first);       // maxim = 40
	  try_match(state, ex->second, maxex, im->first, maxim);
	}
      }
    }
    // 匹配exporter和importer,由于本场景很简单,mds.0和吗,mds.1匹配。
    if (beat % 2 == 1) {
      // big exporters to big importers
      multimap<double,mds_rank_t>::reverse_iterator ex = exporters.rbegin();
      multimap<double,mds_rank_t>::iterator im = importers.begin();
      while (ex != exporters.rend() && im != importers.end()) {
        double maxex = get_maxex(state, ex->second);
	double maxim = get_maxim(state, im->second);
	if (maxex < .001 || maxim < .001) break;
	try_match(state, ex->second, maxex, im->second, maxim);
	if (maxex <= .001) ++ex;
	if (maxim <= .001) ++im;
      }
    } else { ...
    }
  ...
}


子树选择

负载重匹配后,还得先判断要迁移的大小,即要迁移的大小amount必须大于MIN_OFFLOAD(这里是10),也必须大于0.2 * target_load,很显然这里都满足,代码如下

void MDBalancer::try_rebalance(balance_state_t& state)
{
  ...
  // make a sorted list of my imports
  map<double,CDir*>    import_pop_map;
  multimap<mds_rank_t,CDir*>  import_from_map;
  set<CDir*> fullauthsubs;
  mds->mdcache->get_fullauth_subtrees(fullauthsubs);
  ...
  // do my exports!
  set<CDir*> already_exporting;
  for (auto &it : state.targets) {
    mds_rank_t target = it.first;
    double amount = it.second;           // amount = 40
    if (amount < MIN_OFFLOAD) continue;  // MIN_OFFLOAD = 10,即如果要迁移的负载必须大于10
    if (amount / target_load < .2) continue;
    double have = 0.0;
    // search imports from target
    if (amount-have < MIN_OFFLOAD) {
      continue;
    }
    ...
    if (amount-have < MIN_OFFLOAD) {
      //fudge = amount-have;
      continue;
    }

    // okay, search for fragments of my workload
    set<CDir*> candidates;
    mds->mdcache->get_fullauth_subtrees(candidates);
    list<CDir*> exports;
    for (set<CDir*>::iterator pot = candidates.begin(); pot != candidates.end(); ++pot) {
      if ((*pot)->get_inode()->is_stray()) continue;
      find_exports(*pot, amount, exports, have, already_exporting);
      if (have > amount-MIN_OFFLOAD)
	break;
    }
    //fudge = amount - have;
    ...
}

接着就是选择子树,这里mds.0只有一个根目录子树,所以从根目录开始递归遍历选择合适的目录。这部分的代码在MDBalancer::find_exports,算法很明了,就简单讲下这部分的流程:

1,从子树根开始(本场景是根目录"/")遍历子目录,

2,如果有子目录的热度小于要迁移的热度(40)且要大于midchunk ,则将此目录作为迁移目录的候选之一,如果候选目录的热度加起来大于needmin(32),就停止选择。

3,如果有子目录的热度大于40,则往下递归,找热度小于40的子树。

本场景的目录树如下

所以最终选择的要迁移的目录是/test/test1和/test/test4,这两者的热度和为45 。


子树迁移

待续...

来自 “ ITPUB博客 ” ,链接:https://blog.itpub.net/31537575/viewspace-3125067/ ,如需转载,请注明出处,否则将追究法律责任。

请登录后发表评论 登录
全部评论

注册时间:2018-05-16

  • 博文量
    4259
  • 访问量
    40024