前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Ceph快照详解

Ceph快照详解

作者头像
thierryzhou
修改2022-12-02 12:42:02
3.6K0
修改2022-12-02 12:42:02
举报

简介

Ceph快照功能基于RADOS实现,但是从使用方法上分成三种情况:

  1. Pool Snapshot 对整个Pool打快照,该Pool中所有的对象都会受影响。
  2. Self Managed Snapshot 用户管理的快照,Pool受影响的对象是受用户控制的,这里的用户往往是应用,如librbd。常见的形式就是针对某一个rbd卷进行快照。
  3. 用于CephFS的快照,在Ceph 16.x以后的版本中,CephFS默认情况下已经开启了快照功能的,但是由于CephFS的快照也是基于Pool Snapshot开发的因此在多文件系统的情况时,MDS的集群之间snapID相互独立,这个快照管理带来了极大的不便,此种情况下官方不推荐开启快照功能。

快照的原理

Ceph的快照与其他系统的快照一样,是基于COW(copy-on-write)实现的。其实现由RADOS支持,基于OSD服务端——每次做完快照后再对卷进行写入时就会触发COW操作,即先拷贝出原数据对象的数据出来生成快照对象,然后对原数据对象进行写入。于此同时,每次快照的操作会更新卷的元数据,以及包括快照ID,快照链,parent信息等在内的快照信息。

此外image快照和pool快照的区别是由不同的使用方式导致的,底层的实现没有本质上的区别。从OSD的角度看,池快照和自管理的快照之间的区别在于SnapContext是通过客户端的MOSDOp还是通过最新的OSDMap到达osd。

快照的使用

image快照与pool快照

image快照与pool快照是互斥的,创建了image的存储池无法创建存储池的快照,因为存储池当前已经为unmanaged snaps mode了,而没有创建image的就可以做存储池快照。而如果创建了pool快照则无法创建image快照。

// image快照的创建命令:
rbd snap create {pool-name}/{image-name}@{snap-name}

// 回滚命令:
rbd snap rollback {pool-name}/{image-name}@{snap-name}

CephFS快照

CephFS的无法通过命令行直接操作,需要通过操作文件夹的方式来操作快照

// 为某个文件夹创建快照
mkdir .snap/snapname // snapname为快照的名称

//恢复数据的命令
p -ra .snap/snap1/* ./ 
//删除快照的命令

rmdir .snap/snap1

快照的实现

快照的相关概念

pool :每个池都是逻辑上的隔离单位,不同的 pool 可以有不同的数据处理方式,包括: ReplicasSet,Placement Groups,CRUSH。 (Rules/Snapshot/ownership 都是通过池隔离。)

head 对象:卷原始对象,包含了 SnapSet(详见关键数据结构部分)。

snap 对象:卷打快照后通过 cow 拷贝出来的对象,该对象为只读。

snap_seq: 快照的序列号(详见关键数据结构部分)。

snapdir 对象: head 对象被删除后,仍然有 snap 和 clone 对象,系统自动创建一个 snapdir 对象来保存 snapset 的信息。(详见关键数据结构部分)。

rbd_header 对象: 在 rados 中,对象里没有数据,卷的元数据都是作为这个对象的属性以 omap 方式记录到 leveldb 里。

快照的代码实现

使用 librados api 创建快照,其代码如下:

#include <iostream>
#include <string>
#include <rados/librados.hpp>

int main(int argc, const char **argv)
{
        int ret = 0;

        librados::Rados rados;
        librados::IoCtx io_ctx
        char cluster_name[] = "ceph";
        char user_name[] = "client.admin";
        uint64_t flags = 0;

        /* 初始化一个ceph集群对象 */
        {
                ret = rados.init2(user_name, cluster_name, flags);
                if (ret < 0) {
                        std::cerr << "Couldn't initialize the cluster handle! error " << ret << std::endl;
                        return EXIT_FAILURE;
                } else {
                        std::cout << "Created a cluster handle." << std::endl;
                }

                ret = rados.conf_read_file("/etc/ceph/ceph.conf");
                if (ret < 0) {
                        std::cerr << "Couldn't read the Ceph configuration file! error " << ret << std::endl;
                        return EXIT_FAILURE;
                } else {
                        std::cout << "Read the Ceph configuration file." << std::endl;
                }

                ret = rados.conf_parse_argv(argc, argv);
                if (ret < 0) {
                        std::cerr << "Couldn't parse command line options! error " << ret << std::endl;
                        return EXIT_FAILURE;
                } else {
                        std::cout << "Parsed command line options." << std::endl;
                }

                ret = rados.connect();
                if (ret < 0) {
                        std::cerr << "Couldn't connect to cluster! error " << ret << std::endl;
                        return EXIT_FAILURE;
                } else {
                        std::cout << "Connected to the cluster." << std::endl;
                }
        }

        /* 创建存储池,并创建快照 */
        {
            rados.ioctx_create("my_test_pool", io_ctx);
            io_ctx.stat(&stats);

            // 创建快照
            int oid = io_ctx.snap_create("my_test_snapshot");

            // 使用快照,恢复快照
            io_ctx.snap_rollback(oid, "my_test_snapshot");
        }

        return 0;
}

IoCtx 对象是 C++ API 接口中关于 IO Content 的抽象层,对应的底层实现在 IoCtxImpl 中,声明如下:

接下来看是 snap_create 和 snap_rollback 的代码:

// src/include/rados/librados.hpp
claszs CEPH_RADOS_API IoCtx
{
// ...
  private:
    // 通过友元只允许 Rados 对象可以创建 IoCtx 对象
    IoCtx(IoCtxImpl *io_ctx_impl_);

    friend class Rados;
    friend class libradosstriper::RadosStriper;
    friend class ObjectWriteOperation;
    friend class ObjectReadOperation;

    IoCtxImpl *io_ctx_impl;
};

// 代码来源:src/librados/IoCtxImpl.h
struct librados::IoCtxImpl {
  std::atomic<uint64_t> ref_cnt = { 0 };
  RadosClient *client = nullptr;
  int64_t poolid = 0;
  snapid_t snap_seq;        //根据是否有快照值为snap的快照序号或者CEPH_NOSNAP
  ::SnapContext snapc;      // 快照上下文
// ...
};

在IoCtxImpl里的snap_seq也被称为快照的id,当打开一个image时,如果打开的是一个卷的快照,则该值为快照对应的序号,否则该值为CEPH_NOSNAP表示操作的不是卷的快照,是卷自身。

// src/librados/IoCtx.cc
// 创建快照
int librados::IoCtx::snap_create(const char *snapname)
{
  return io_ctx_impl->snap_create(snapname);
}

// 快照回滚
int librados::IoCtx::snap_rollback(const std::string& oid, const char *snapname)
{
  return io_ctx_impl->rollback(oid, snapname);
}

/////////////////////////////////////////////////////////
// src/librados/librados_cxx.cc
// 创建快照
int librados::IoCtxImpl::snap_create(const char *snapName)
{
  int reply;
  string sName(snapName);

  ceph::mutex mylock = ceph::make_mutex("IoCtxImpl::snap_create::mylock");
  ceph::condition_variable cond;
  bool done;
  Context *onfinish = new C_SafeCond(mylock, cond, &done, &reply);
  objecter->create_pool_snap(poolid, sName, onfinish);

  std::unique_lock l{mylock};
  cond.wait(l, [&done] { return done; });
  return reply;
}

// 快照回滚
int librados::IoCtxImpl::selfmanaged_snap_rollback_object(const object_t& oid,
							  ::SnapContext& snapc,
							  uint64_t snapid)
{
  int reply;

  ceph::mutex mylock = ceph::make_mutex("IoCtxImpl::snap_rollback::mylock");
  ceph::condition_variable cond;
  bool done;
  Context *onack = new C_SafeCond(mylock, cond, &done, &reply);

  ::ObjectOperation op;
  prepare_assert_ops(&op);
  op.rollback(snapid);
  objecter->mutate(oid, oloc,
		   op, snapc, ceph::real_clock::now(),
		   extra_op_flags,
		   onack, NULL);

  std::unique_lock l{mylock};
  cond.wait(l, [&done] { return done; });
  return reply;
}

int librados::IoCtxImpl::rollback(const object_t& oid, const char *snapName)
{
  snapid_t snap;

  int r = objecter->pool_snap_by_name(poolid, snapName, &snap);
  if (r < 0) {
    return r;
  }

  return selfmanaged_snap_rollback_object(oid, snapc, snap);
}
快照的创建

接下来经过 objecter 的处理以后,snapshot 请求被发送给 mon 服务端,经过一些的消息转发,snapshot的请求会被转发到 OSDMonitor 中处理。

在OSD端,对象的Snap相关的信息保存在SnapSet数据结构中,每次IO操作都会带上snap_seq发送给OSD,OSD会查询该IO操作涉及的object的snap_seq情况。当创建一个快照以后,对镜像中的对象进行写操作时会带上新的snap_seq,Ceph接到请求后会先检查对象的Head Version,如果发现该写操作所带有的snap_seq大于原本对象的snap_seq,那么就会对原来的对象克隆一个新的Object Head Version,原来的对象会作为Snapshot,新的Object Head会带上新的snap_seq,也就是librbd之前申请到的。

ceph也有一套Watcher回调通知机制,当别的的客户端做了快照,产生了以新的快照序号,当该客户端访问,osd端知道最新快照需要变化后,通知相应的连接客户端更新最新的快照序号。如果没有及时更新,也没有太大的问题,客户端会主动更新快照序号,然后重新发起写操作。

具体到代码流程如下:

  1. 判断服务端的快照序号,如果大于客户端的序号,则用服务端的快照信息更新客户端的信息,需要注意的是客户端的序号是不允许小于服务端的序号的,如发生服务端的序号大于客户端的序号则参见上述的watcher回调通知机制。// 源码实现:src/mon/OSDMonitor.cc bool OSDMonitor::prepare_pool_op(MonOpRequestRef op) { // pg_pool_t *pp; // ... switch (m->op) { case POOL_OP_CREATE_SNAP: if (!pp.snap_exists(m->name.c_str())) { pp.add_snap(m->name.c_str(), ceph_clock_now()); dout(10) << "create snap in pool " << m->pool << " " << m->name << " seq " << pp.get_snap_epoch() << dendl; changed = true; } break; case POOL_OP_DELETE_SNAP: { snapid_t s = pp.snap_exists(m->name.c_str()); if (s) { pp.remove_snap(s); pending_inc.new_removed_snaps[m->pool].insert(s); changed = true; } } break; case POOL_OP_CREATE_UNMANAGED_SNAP: { uint64_t snapid = pp.add_unmanaged_snap( osdmap.require_osd_release < ceph_release_t::octopus); encode(snapid, reply_data); changed = true; } break; case POOL_OP_DELETE_UNMANAGED_SNAP: if (!_is_removed_snap(m->pool, m->snapid) && !_is_pending_removed_snap(m->pool, m->snapid)) { if (m->snapid > pp.get_snap_seq()) { _pool_op_reply(op, -ENOENT, osdmap.get_epoch()); return false; } pp.remove_unmanaged_snap( m->snapid, osdmap.require_osd_release < ceph_release_t::octopus); pending_inc.new_removed_snaps[m->pool].insert(m->snapid); // also record the new seq as purged: this avoids a discontinuity // after all of the snaps have been purged, since the seq assigned // during removal lives in the same namespace as the actual snaps. pending_pseudo_purged_snaps[m->pool].insert(pp.get_snap_seq()); changed = true; } break; case POOL_OP_AUID_CHANGE: _pool_op_reply(op, -EOPNOTSUPP, osdmap.get_epoch()); return false; default: ceph_abort(); break; } if (changed) { pp.set_snap_epoch(pending_inc.epoch); pending_inc.new_pools[m->pool] = pp; } out: wait_for_finished_proposal(op, new OSDMonitor::C_PoolOp(this, op, ret, pending_inc.epoch, &reply_data)); return true; } // 只列举 pg_pool_t 对象中 add_snap 的操作如何实现: void pg_pool_t::add_snap(const char *n, utime_t stamp) { ceph_assert(!is_unmanaged_snaps_mode()); flags |= FLAG_POOL_SNAPS; snapid_t s = get_snap_seq() + 1; snap_seq = s; snaps[s].snapid = s; snaps[s].name = n; snaps[s].stamp = stamp; }
  2. 把已经删除的快照过滤掉。
  3. 如果head对象存在切snaps的size不为空,并且客户端的最新快照序号大于服务端的最新快照序号,则需要克隆对象。
  4. 克隆完成后修改clone_overlap和clone_size的记录。
  5. 更新服务端快照信息。

前文提到过快照通过 COW 方式实现,在没有发生变化是通过集群我们只能观测到 snapid 的变化,快照和原数据是同一份数据,只有当发生了数据写时,Ceph 才会为快照生成相应的数据。写请求从客户端到 Ceph 服务的流程如下所示:

先看一下关键数据结构的定义:

// // 源码实现:src/osd/PrimaryLogPG.h
class PrimaryLogPG : public PG, public PGBackend::Listener {
  friend class OSD;
  friend class Watch;
  friend class PrimaryLogScrub;

  // ...
  struct OpContext {
    // ...
    const SnapSet *snapset; //旧的SnapSet,也就是OSD服务端保存的快照信息
    SnapSet new_snapset;  // 新的SnapSet,也就是写操作过后生成的结果
    SnapContext snapc;   //写操作带的客户端的SnapContext信息
    // ...
  }
  // ...
};

// 代码路径:src/osd/osd_types.h
// SnapSet 是在ceph的服务端(也就是osd端)保存快照集合的对象
struct SnapSet {
  snapid_t seq;              //最新的快照序列号
  
  vector<snapid_t> snaps;    // 所有快照序号的降序列表
  vector<snapid_t> clones;   // 所有clone对象的序号升序列表。保存在做完快照后,对原对象进行写入时触发cow进行clone的快照序号,注意并不是每个快照都需要clone对象,只有做完快照后,对相应的对象进行写入操作时才会clone去拷贝数据;
  map<snapid_t, interval_set<uint64_t> > clone_overlap;  // 与上次clone对象的overlap的部分,记录在其clone数据对象后,也就是原数据对象上未写过的数据部分,是采用offset~len的方式进行记录的,比如{2=[0~1646592,1650688~12288,1667072~577536]};
  map<snapid_t, uint64_t> clone_size;   //clone对象的size(有的对象一开始并不是默认的对象大小
  
  SnapSet() : seq(0) {}
  explicit SnapSet(ceph::buffer::list& bl) {
    auto p = std::cbegin(bl);
    decode(p);
  }

  /// librados::snap_set_t 发布快照
  void from_snap_set(const librados::snap_set_t& ss, bool legacy);

  // 数据处理
  uint64_t get_clone_bytes(snapid_t clone) const;

  void encode(ceph::buffer::list& bl) const;
  void decode(ceph::buffer::list::const_iterator& bl);
  void dump(ceph::Formatter *f) const;
  static void generate_test_instances(std::list<SnapSet*>& o);  

  SnapContext get_ssc_as_of(snapid_t as_of) const {
    SnapContext out;
    out.seq = as_of;
    for (auto p = clone_snaps.rbegin();
	 p != clone_snaps.rend();
	 ++p) {
      for (auto snap : p->second) {
	if (snap <= as_of) {
	  out.snaps.push_back(snap);
	}
      }
    }
    return out;
  }

  SnapSet get_filtered(const pg_pool_t &pinfo) const;
  void filter(const pg_pool_t &pinfo);
};

SnapContext 在客户端中保存 snap 相关的信息,是当前为对象定义的快照合集。 PrimaryLogPG 透过 SnapContext 跟踪对象的全部的快照集合,当前存在的全部克隆,克隆的大小。 PrimaryLogPG 通过 new_snap 和 snapc 的比较感知到快照已经过期;另外 SnapContext 的 clone_overlap 保存本次 clone 对象和上次 clone 对象的重叠(overlap)部分,clone 操作之后,每次的写操作都要维护这个信息。这个信息用于在数据恢复阶段对象恢复的优化。

make_writeable 中封装了一部分逻辑处理的功能,更详细的功能感兴趣的同学可以自己的看一下把整个流程梳理出来,篇幅有限这里就不再展开更多的源码内容。

void PrimaryLogPG::make_writeable(OpContext *ctx)
{
  // ...

  // 存在快照并且快照已经不是最新的
  if ((ctx->obs->exists && !ctx->obs->oi.is_whiteout()) &&
      snapc.snaps.size() &&
      !ctx->cache_operation &&
      snapc.snaps[0] > ctx->new_snapset.seq) {

    hobject_t coid = soid;
    coid.snap = snapc.seq;

    const auto snaps = [&] {
      auto last = find_if_not(
        begin(snapc.snaps), end(snapc.snaps),
        [&](snapid_t snap_id) { return snap_id > ctx->new_snapset.seq; });
      return vector<snapid_t>{begin(snapc.snaps), last};
    }();

    // 准备 clone
    object_info_t static_snap_oi(coid);
    object_info_t *snap_oi;
    if (is_primary()) {
      ctx->clone_obc = object_contexts.lookup_or_create(static_snap_oi.soid);
      ctx->clone_obc->destructor_callback =
	new C_PG_ObjectContext(this, ctx->clone_obc.get());
      ctx->clone_obc->obs.oi = static_snap_oi;
      ctx->clone_obc->obs.exists = true;
      ctx->clone_obc->ssc = ctx->obc->ssc;
      ctx->clone_obc->ssc->ref++;
      if (pool.info.is_erasure())
	ctx->clone_obc->attr_cache = ctx->obc->attr_cache;
      snap_oi = &ctx->clone_obc->obs.oi;
      if (ctx->obc->obs.oi.has_manifest()) {
	if ((ctx->obc->obs.oi.flags & object_info_t::FLAG_REDIRECT_HAS_REFERENCE) &&
	    ctx->obc->obs.oi.manifest.is_redirect()) {
	  snap_oi->set_flag(object_info_t::FLAG_MANIFEST);
	  snap_oi->manifest.type = object_manifest_t::TYPE_REDIRECT;
	  snap_oi->manifest.redirect_target = ctx->obc->obs.oi.manifest.redirect_target;
	} else if (ctx->obc->obs.oi.manifest.is_chunked()) {
	  snap_oi->set_flag(object_info_t::FLAG_MANIFEST);
	  snap_oi->manifest.type = object_manifest_t::TYPE_CHUNKED;
	  snap_oi->manifest.chunk_map = ctx->obc->obs.oi.manifest.chunk_map;
	} else {
	  ceph_abort_msg("unrecognized manifest type");
	}
      }
      bool got = ctx->lock_manager.get_write_greedy(
	coid,
	ctx->clone_obc,
	ctx->op);
      ceph_assert(got);
      dout(20) << " got greedy write on clone_obc " << *ctx->clone_obc << dendl;
    } else {
      snap_oi = &static_snap_oi;
    }
    snap_oi->version = ctx->at_version;
    snap_oi->prior_version = ctx->obs->oi.version;
    snap_oi->copy_user_bits(ctx->obs->oi);

    _make_clone(ctx, ctx->op_t.get(), ctx->clone_obc, soid, coid, snap_oi);

    ctx->delta_stats.num_objects++;
    if (snap_oi->is_dirty()) {
      ctx->delta_stats.num_objects_dirty++;
      osd->logger->inc(l_osd_tier_dirty);
    }
    if (snap_oi->is_omap())
      ctx->delta_stats.num_objects_omap++;
    if (snap_oi->is_cache_pinned())
      ctx->delta_stats.num_objects_pinned++;
    if (snap_oi->has_manifest())
      ctx->delta_stats.num_objects_manifest++;
    ctx->delta_stats.num_object_clones++;
    ctx->new_snapset.clones.push_back(coid.snap);
    ctx->new_snapset.clone_size[coid.snap] = ctx->obs->oi.size;
    ctx->new_snapset.clone_snaps[coid.snap] = snaps;

    // 将快照重叠部分保存到 clone_overlap 中
    ctx->new_snapset.clone_overlap[coid.snap];
    if (ctx->obs->oi.size) {
      ctx->new_snapset.clone_overlap[coid.snap].insert(0, ctx->obs->oi.size);
    }

    // log clone
    dout(10) << " cloning v " << ctx->obs->oi.version
	     << " to " << coid << " v " << ctx->at_version
	     << " snaps=" << snaps
	     << " snapset=" << ctx->new_snapset << dendl;
    ctx->log.push_back(pg_log_entry_t(
			 pg_log_entry_t::CLONE, coid, ctx->at_version,
			 ctx->obs->oi.version,
			 ctx->obs->oi.user_version,
			 osd_reqid_t(), ctx->new_obs.oi.mtime, 0));
    encode(snaps, ctx->log.back().snaps);

    ctx->at_version.version++;
  }

  // 更新 clone_overlap 的数据和状态。
  if (ctx->new_snapset.clones.size() > 0) {
    hobject_t last_clone_oid = soid;
    last_clone_oid.snap = ctx->new_snapset.clone_overlap.rbegin()->first;
    interval_set<uint64_t> &newest_overlap =
      ctx->new_snapset.clone_overlap.rbegin()->second;
    ctx->modified_ranges.intersection_of(newest_overlap);
    if (is_present_clone(last_clone_oid)) {
      // modified_ranges is still in use by the clone
      ctx->delta_stats.num_bytes += ctx->modified_ranges.size();
    }
    newest_overlap.subtract(ctx->modified_ranges);
  }

  // 更新快照信息
  if (snapc.seq > ctx->new_snapset.seq) {
    ctx->new_snapset.seq = snapc.seq;
    if (get_osdmap()->require_osd_release < ceph_release_t::octopus) {
      ctx->new_snapset.snaps = snapc.snaps;
    } else {
      ctx->new_snapset.snaps.clear();
    }
  }
  dout(20) << "make_writeable " << soid
	   << " done, snapset=" << ctx->new_snapset << dendl;
}
快照的回滚

快照的回滚,就是把当前的head对象,回滚到某个快照对象。 具体操作如下:

  1. 删除当前 head 对象的数据
  2. 拷贝相应的 snap 对象到 head 对象

假如有 foo 对象的快照结构如下所示,它想要回退到 snap1 的版本。

回滚前:
foo snap[1]:          [chunk4]          [chunk5]
foo snap[0]: [                  chunk2                   ]
foo head   :          [chunk1]                    [chunk3]
回滚后:
foo snap[1]:          [chunk4]          [chunk5]
foo snap[0]: [                  chunk2                   ]
foo head   :          [chunk4]          [chunk5] 
// 源码实现:src/osd/PrimaryLogPG.cc
int PrimaryLogPG::_rollback_to(OpContext *ctx, OSDOp& op)
{
  ObjectState& obs = ctx->new_obs;
  object_info_t& oi = obs.oi;
  const hobject_t& soid = oi.soid;
  snapid_t snapid = (uint64_t)op.op.snap.snapid;
  hobject_t missing_oid;

  dout(10) << "_rollback_to " << soid << " snapid " << snapid << dendl;

  ObjectContextRef rollback_to;

  int ret = find_object_context(
    hobject_t(soid.oid, soid.get_key(), snapid, soid.get_hash(), info.pgid.pool(),
	      soid.get_namespace()),
    &rollback_to, false, false, &missing_oid);
  if (ret == -EAGAIN) {
    /* clone must be missing */
    ceph_assert(is_degraded_or_backfilling_object(missing_oid) || is_degraded_on_async_recovery_target(missing_oid));
    dout(20) << "_rollback_to attempted to roll back to a missing or backfilling clone "
	     << missing_oid << " (requested snapid: ) " << snapid << dendl;
    block_write_on_degraded_snap(missing_oid, ctx->op);
    return ret;
  }
  {
    ObjectContextRef promote_obc;
    cache_result_t tier_mode_result;
    if (obs.exists && obs.oi.has_manifest()) {
      /* 
       * In the case of manifest object, the object_info exists on the base tier at all time,
       * so promote_obc should be equal to rollback_to 
       * */
      promote_obc = rollback_to;
      tier_mode_result =
	maybe_handle_manifest_detail(
	  ctx->op,
	  true,
	  rollback_to);
    } else {
      tier_mode_result =
	maybe_handle_cache_detail(
	  ctx->op,
	  true,
	  rollback_to,
	  ret,
	  missing_oid,
	  true,
	  false,
	  &promote_obc);
    }
    switch (tier_mode_result) {
    case cache_result_t::NOOP:
      break;
    case cache_result_t::BLOCKED_PROMOTE:
      ceph_assert(promote_obc);
      block_write_on_snap_rollback(soid, promote_obc, ctx->op);
      return -EAGAIN;
    case cache_result_t::BLOCKED_FULL:
      block_write_on_full_cache(soid, ctx->op);
      return -EAGAIN;
    case cache_result_t::REPLIED_WITH_EAGAIN:
      ceph_abort_msg("this can't happen, no rollback on replica");
    default:
      ceph_abort_msg("must promote was set, other values are not valid");
      return -EAGAIN;
    }
  }

  if (ret == -ENOENT || (rollback_to && rollback_to->obs.oi.is_whiteout())) {
    // there's no snapshot here, or there's no object.
    // if there's no snapshot, we delete the object; otherwise, do nothing.
    dout(20) << "_rollback_to deleting head on " << soid.oid
	     << " because got ENOENT|whiteout on find_object_context" << dendl;
    if (ctx->obc->obs.oi.watchers.size()) {
      // Cannot delete an object with watchers
      ret = -EBUSY;
    } else {
      _delete_oid(ctx, false, false);
      ret = 0;
    }
  } else if (ret) {
    // ummm....huh? It *can't* return anything else at time of writing.
    ceph_abort_msg("unexpected error code in _rollback_to");
  } else { //we got our context, let's use it to do the rollback!
    hobject_t& rollback_to_sobject = rollback_to->obs.oi.soid;
    if (is_degraded_or_backfilling_object(rollback_to_sobject) ||
	is_degraded_on_async_recovery_target(rollback_to_sobject)) {
      dout(20) << "_rollback_to attempted to roll back to a degraded object "
	       << rollback_to_sobject << " (requested snapid: ) " << snapid << dendl;
      block_write_on_degraded_snap(rollback_to_sobject, ctx->op);
      ret = -EAGAIN;
    } else if (rollback_to->obs.oi.soid.snap == CEPH_NOSNAP) {
      // rolling back to the head; we just need to clone it.
      ctx->modify = true;
    } else {
      if (rollback_to->obs.oi.has_manifest() && rollback_to->obs.oi.manifest.is_chunked()) {

	OpFinisher* op_finisher = nullptr;
	auto op_finisher_it = ctx->op_finishers.find(ctx->current_osd_subop_num);
	if (op_finisher_it != ctx->op_finishers.end()) {
	  op_finisher = op_finisher_it->second.get();
	}
	if (!op_finisher) {
	  bool need_inc_ref = inc_refcount_by_set(ctx, rollback_to->obs.oi.manifest, op);
	  if (need_inc_ref) {
	    ceph_assert(op_finisher_it == ctx->op_finishers.end());
	    ctx->op_finishers[ctx->current_osd_subop_num].reset(
		new SetManifestFinisher(op));
	    return -EINPROGRESS;
	  }
	} else {
	  op_finisher->execute();
	  ctx->op_finishers.erase(ctx->current_osd_subop_num);
	}
      }
      _do_rollback_to(ctx, rollback_to, op);
    }
  }
  return ret;
}
快照的删除

向 monitor 集群发出请求,将快照的 id 添加到已清除的快照的列表中。删除快照时,直接删除SnapSet相关的信息,并删除相应的快照对象。其调用流程可参看快照的创建流程。

ceph的删除是延迟删除,并不直接删除。当 pg 是 clean 状态并且没进行 scrubbing 时由由 snap_trimq 异步执行。

// 相关源码:src/osd/PrimaryLogPG.h
class PrimaryLogPG : public PG, public PGBackend::Listener {
  friend class OSD;
  friend class Watch;
  friend class PrimaryLogScrub;

  // ...
  struct SnapTrimmer : public boost::statechart::state_machine< SnapTrimmer, NotTrimming > {
    PrimaryLogPG *pg;
    explicit SnapTrimmer(PrimaryLogPG *pg) : pg(pg) {}
    void log_enter(const char *state_name);
    void log_exit(const char *state_name, utime_t duration);
    bool permit_trim();
    bool can_trim() {
      return
	permit_trim() &&
	!pg->get_osdmap()->test_flag(CEPH_OSDMAP_NOSNAPTRIM);
    }
  } snap_trimmer_machine;
};
CephFS快照

CephFS 通过在希望快照的目录下执行 mkdir 创建 .snap 目录来创建快照。这时 fs 客户端会生成一个 SnapRealm,它保留较少的数据,用于将 SnapContex 与每个打开的文件关联以进行写入。 SnapRealm 中包含 srnode(磁盘上的元数据,包含序列计数器,时间戳,相关的快照 id 列表和 past_parents) , past_parents , past_children , inodes_with_caps 等属于快照的一部分的信息。

// 代码路径: https://github.com/ceph/ceph-client/fs/ceph/dir.c
static int ceph_mkdir(struct user_namespace *mnt_userns, struct inode *dir,
		      struct dentry *dentry, umode_t mode)
{
	struct ceph_mds_client *mdsc = ceph_sb_to_mdsc(dir->i_sb);
	struct ceph_mds_request *req;
	struct ceph_acl_sec_ctx as_ctx = {};
	int err;
	int op;

	err = ceph_wait_on_conflict_unlink(dentry);
	if (err)
		return err;

	// 目录格式为 .snap/foo 则进入快照处理逻辑
	if (ceph_snap(dir) == CEPH_SNAPDIR) {
		/* mkdir .snap/foo is a MKSNAP */
		op = CEPH_MDS_OP_MKSNAP;
		dout("mksnap dir %p snap '%pd' dn %p\n", dir,
		     dentry, dentry);
	} else if (ceph_snap(dir) == CEPH_NOSNAP) {
		dout("mkdir dir %p dn %p mode 0%ho\n", dir, dentry, mode);
		op = CEPH_MDS_OP_MKDIR;
	} else {
		err = -EROFS;
		goto out;
	}

	if (op == CEPH_MDS_OP_MKDIR &&
	    ceph_quota_is_max_files_exceeded(dir)) {
		err = -EDQUOT;
		goto out;
	}

	mode |= S_IFDIR;
	err = ceph_pre_init_acls(dir, &mode, &as_ctx);
	if (err < 0)
		goto out;
	err = ceph_security_init_secctx(dentry, mode, &as_ctx);
	if (err < 0)
		goto out;

	req = ceph_mdsc_create_request(mdsc, op, USE_AUTH_MDS);
	if (IS_ERR(req)) {
		err = PTR_ERR(req);
		goto out;
	}

	req->r_dentry = dget(dentry);
	req->r_num_caps = 2;
	req->r_parent = dir;
	ihold(dir);
	set_bit(CEPH_MDS_R_PARENT_LOCKED, &req->r_req_flags);
	req->r_args.mkdir.mode = cpu_to_le32(mode);
	req->r_dentry_drop = CEPH_CAP_FILE_SHARED | CEPH_CAP_AUTH_EXCL;
	req->r_dentry_unless = CEPH_CAP_FILE_EXCL;
	if (as_ctx.pagelist) {
		req->r_pagelist = as_ctx.pagelist;
		as_ctx.pagelist = NULL;
	}
	err = ceph_mdsc_do_request(mdsc, dir, req);
	if (!err &&
	    !req->r_reply_info.head->is_target &&
	    !req->r_reply_info.head->is_dentry)
		err = ceph_handle_notrace_create(dir, dentry);
	ceph_mdsc_put_request(req);
out:
	if (!err)
		ceph_init_inode_acls(d_inode(dentry), &as_ctx);
	else
		d_drop(dentry);
	ceph_release_acl_sec_ctx(&as_ctx);
	return err;
}

当执行快照操作的时候,客户端会将请求发送到 MDS 服务器,然后在服务器的 Server::handle_client_mksnap() 中处理,它会从 SnapServer 中非配一个 snapid ,同时利用新的 SnapRealm 创建一个新的 inode ,然后将其提交到 MDlog ,提交后会触发 MDCache::do_realm_invalidate_and_update_notify() ,快照的大部分工作由这部分完成。快照的元数据会作为目录信息的一部分被存储在 OSD 端。

代码路径:https://github.com/ceph/ceph

void Server::handle_client_mksnap(MDRequestRef& mdr)
{
  // ...

  // 服务端会通过 snapclient 与 snapserver 进行通信

  // 创建 snapid
  if (!mdr->more()->stid) {
    mds->snapclient->prepare_create(diri->ino(), snapname,
				    mdr->get_mds_stamp(),
				    &mdr->more()->stid, &mdr->more()->snapidbl,
				    new C_MDS_RetryRequest(mdcache, mdr));
    return;
  }

  version_t stid = mdr->more()->stid;
  snapid_t snapid;
  auto p = mdr->more()->snapidbl.cbegin();
  decode(snapid, p);
  dout(10) << " stid " << stid << " snapid " << snapid << dendl;

  ceph_assert(mds->snapclient->get_cached_version() >= stid);

  SnapPayload payload;
  if (req->get_data().length()) {
    try {
      auto iter = req->get_data().cbegin();
      decode(payload, iter);
    } catch (const ceph::buffer::error &e) {
      // backward compat -- client sends xattr bufferlist. however,
      // that is not used anywhere -- so (log and) ignore.
      dout(20) << ": no metadata in payload (old client?)" << dendl;
    }
  }

  // 日志
  SnapInfo info;
  info.ino = diri->ino();
  info.snapid = snapid;
  info.name = snapname;
  info.stamp = mdr->get_op_stamp();
  info.metadata = payload.metadata;

  auto pi = diri->project_inode(mdr, false, true);
  pi.inode->ctime = info.stamp;
  if (info.stamp > pi.inode->rstat.rctime)
    pi.inode->rstat.rctime = info.stamp;
  pi.inode->rstat.rsnaps++;
  pi.inode->version = diri->pre_dirty();

  auto &newsnap = *pi.snapnode;
  newsnap.created = snapid;
  auto em = newsnap.snaps.emplace(std::piecewise_construct, std::forward_as_tuple(snapid), std::forward_as_tuple(info));
  if (!em.second)
    em.first->second = info;
  newsnap.seq = snapid;
  newsnap.last_created = snapid;

  // 日志记录 inode 的变化
  mdr->ls = mdlog->get_current_segment();
  EUpdate *le = new EUpdate(mdlog, "mksnap");
  mdlog->start_entry(le);

  le->metablob.add_client_req(req->get_reqid(), req->get_oldest_client_tid());
  le->metablob.add_table_transaction(TABLE_SNAP, stid);
  mdcache->predirty_journal_parents(mdr, &le->metablob, diri, 0, PREDIRTY_PRIMARY, false);
  mdcache->journal_dirty_inode(mdr.get(), &le->metablob, diri);

  // 日志记录 snaprealm 的变化
  submit_mdlog_entry(le, new C_MDS_mksnap_finish(this, mdr, diri, info),
                     mdr, __func__);
  mdlog->flush();
}

需要注意的是 CephFS 的快照和多个文件系统的交互是存在问题的——每个 MDS 集群独立分配 snappid,如果多个文件系统共享一个池,快照会冲突。如果此时有客户删除一个快照,将会导致其他人丢失数据,并且这种情况不会抛出异常,这也是 CephFS 的快照不推荐使用的原因。

克隆卷

克隆卷的操作必须要在快照被保护起来(无法删除)之后才能进行。在克隆卷生成之后,在 librbd 端开始读写时会先根据快照链构造出其父子关系,而在具体的 I/O 请求的时候这个父子关系会被用到。克隆出的卷在有新数据写入之前,读取数据的需求都是引用父卷和快照的数据。

对于克隆卷的读写会先去找这个卷的对象,如果未找到,就去寻找其 parent 对象,层层往上,直到找到位置。所以一旦快照链比较长就会导致效率较低,所以 Ceph 的克隆卷提供了 flatten 功能,这个功能会将所有的数据全部拷贝一份,然后生成一个新的卷。新生成的卷会完全独立存在,不再保持原有的父子关系。但是 flatten 本身是一个耗时比较大的操作。

更多技术分享浏览我的博客:

https://thierryzhou.github.io

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 简介
    • 快照的原理
      • 快照的使用
        • 快照的实现
          • 快照的相关概念
          • 快照的代码实现
      • 更多技术分享浏览我的博客:
      相关产品与服务
      文件存储
      文件存储(Cloud File Storage,CFS)为您提供安全可靠、可扩展的共享文件存储服务。文件存储可与腾讯云服务器、容器服务、批量计算等服务搭配使用,为多个计算节点提供容量和性能可弹性扩展的高性能共享存储。腾讯云文件存储的管理界面简单、易使用,可实现对现有应用的无缝集成;按实际用量付费,为您节约成本,简化 IT 运维工作。
      领券
      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档