【ceph】CEPH源码解析:读写流程

相同过程

Ceph的读/写操作采用Primary-Replica模型,客户端只向Object所对应OSD set的Primary OSD发起读/写请求,这保证了数据的强一致性。当Primary OSD收到Object的写请求时,它负责把数据发送给其他副本,只有这个数据被保存在所有的OSD上时,Primary OSD才应答Object的写请求,这保证了副本的一致性。

写入数据
这里以Object写入为例,假定一个PG被映射到3个OSD上。Object写入流程如图所示。

在这里插入图片描述

 
当某个客户端需要向Ceph集群写入一个File时,首先需要在本地完成前面所述的寻址流程,将File变为一个Object,然后找出存储该Object的一组共3个OSD,这3个OSD具有各自不同的序号,序号最靠前的那个OSD就是这一组中的Primary OSD,而后两个则依次Secondary OSD和Tertiary OSD。
找出3个OSD后,客户端将直接和Primary OSD进行通信,发起写入操作(步骤1)。 Primary OSD收到请求后,分别向Secondary OSD和Tertiary OSD发起写人操作(步骤2和步骤3)。当Secondary OSD和Tertiary OSD各自完成写入操作后,将分别向Primary OSD发送确认信息(步骤4和步骤5)。当Primary OSD确认其他两个OSD的写入完成后,则自己也完成数据写入,并向客户端确认Object写入操作完成(步骤6)。
之所以采用这样的写入流程,本质上是为了保证写入过程中的可靠性,尽可能避免出现数据丢失的情况。同时,由于客户端只需要向Primary OSD发送数据,因此在互联网使用场景下的外网带宽和整体访问延迟又得到了一定程度的优化。
当然,这种可靠性机制必然导致较长的延迟,特别是,如果等到所有的OSD都将数据写入磁盘后再向客户端发送确认信号,则整体延迟可能难以忍受。因此, Ceph可以分两次向客户端进行确认。当各个OSD都将数据写入内存缓冲区后,就先向客户端发送一次确认,此时客户端即可以向下执行。待各个OSD都将数据写入磁盘后,会向客户端发送一个最终确认信号,此时客户端可以根据需要删除本地数据。
分析上述流程可以看出,在正常情况下,客户端可以独立完成OSD寻址操作,而不必依赖于其他系统模块。因此,大量的客户端可以同时和大量的OSD进行并行操作。同时,如果一个File被切分成多个Object,这多个Object也可被并行发送至多个OSD上。
从OSD的角度来看,由于同一个OSD在不同的PG中的角色不同,因此,其工作压力也可以被尽可能均匀地分担,从而避免单个OSD变成性能瓶颈。

读取数据
如果需要读取数据,客户端只需完成同样的寻址过程,并直接和Primary OSD联系。在目前的Ceph设计中,被读取的数据默认由Primary OSD提供,但也可以设置允许从其他OSD中获取,以分散读取压力从而提高性能。

原文链接:https://blog.csdn.net/lhc121386/article/details/113488420

文件读写流程

libcephfs.cc  调用 Client.cc中的client 。client::_write

cephfs:用户态客户端write

摘自:https://zhuanlan.zhihu.com/p/109573019

还是通过cp命令来研究write。

cp 2M_test /mnt/ceph-fuse/test

从fuse到cephfs客户端的函数流程如下

client::_write就是核心函数,可以简单分为两个重要部分:get_caps和file_write部分。代码如下。

int Client::_write(Fh *f, int64_t offset, uint64_t size, const char *buf, const struct iovec *iov, int iovcnt)
{ // offset = 0, size = 128K, buf是要写的内容,iov = NULL,iovcnt = 0
  Inode *in = f->inode.get();             // in->size = 0
  uint64_t endoff = offset + size;        // endoff = 128K
  utime_t start = ceph_clock_now();
  // copy into fresh buffer (since our write may be resub, async)
  bufferlist bl;
  if (buf) { if (size > 0) bl.append(buf, size);
  } else if (iov){ ... }
  uint64_t totalwritten;
  int have;
  int r = get_caps(in, CEPH_CAP_FILE_WR|CEPH_CAP_AUTH_SHARED, CEPH_CAP_FILE_BUFFER, &have, endoff);
  if (r < 0)
    return r;
  ...
}

get_caps

get_caps的入参need是"AsFw", want是"Fb"。need表示需要的cap,而want表示想要的cap,在get_caps中跟revoke有关。

need和want最关键的区别是:如果mds赋予客户端的caps中不包含need,那就无法往下写。Fw就是写的能力,而As,是因为需要获取本地缓存的Inode的mode值,需要判断(S_ISUID|S_ISGID);want在caps中可有可无,不耽误写,只与写的方式有关。

int Client::get_caps(Inode *in, int need, int want, int *phave, loff_t endoff)
{ // need = "AsFw", want = "Fb", phave是要赋值的int值,endoff = 128K
  int r = check_pool_perm(in, need);
  ...
}

首先判断是否有操作pool的权限。在Client类里面pool_perms成员是用来保存客户端对池的操作属性:即读或写。

std::map<std::pair<int64_t,std::string>, int> pool_perms;

pool_perms里面的value就是属性集合,也就4种,根据字面意思,很好理解

enum {
  POOL_CHECKED = 1,
  POOL_CHECKING = 2,
  POOL_READ = 4,
  POOL_WRITE = 8,
};

Client::check_pool_perm代码如下

int Client::check_pool_perm(Inode *in, int need)
{
  int64_t pool_id = in->layout.pool_id;                          // pool_id = 2
  std::string pool_ns = in->layout.pool_ns;                      // pool_ns = ""
  std::pair<int64_t, std::string> perm_key(pool_id, pool_ns);    // 
  int have = 0;
  while (true) {
    auto it = pool_perms.find(perm_key);                 // 看pool_perms中是否有该pool的key
    if (it == pool_perms.end())                          // 如果没有直接跳出
      break;
    if (it->second == POOL_CHECKING) {                   // 如果有,且正在checking中,等待check结束
      // avoid concurrent checkings 
      wait_on_list(waiting_for_pool_perm);                 
    } else {                                             // 否则,就是已经check完了。
      have = it->second;                                 // 获取目前该池的权限
      assert(have & POOL_CHECKED);
      break;
    }
  }
  if (!have) {
    pool_perms[perm_key] = POOL_CHECKING;                 // 置上POOL_CHECKING标志
    char oid_buf[32];
    snprintf(oid_buf, sizeof(oid_buf), "%llx.00000000", (unsigned long long)in->ino);  //对象名存入oid_buf
    object_t oid = oid_buf;
    SnapContext nullsnapc;
    C_SaferCond rd_cond;
    ObjectOperation rd_op;
    rd_op.stat(NULL, (ceph::real_time*)nullptr, NULL);
    objecter->mutate(oid, OSDMap::file_to_object_locator(in->layout), rd_op,           // 发送CEPH_OSD_OP_STAT请求给osd
		     nullsnapc, ceph::real_clock::now(), 0, &rd_cond);
    C_SaferCond wr_cond;
    ObjectOperation wr_op;
    wr_op.create(true);
    objecter->mutate(oid, OSDMap::file_to_object_locator(in->layout), wr_op,           // 发送CEPH_OSD_OP_CREATE请求给osd
		     nullsnapc, ceph::real_clock::now(), 0, &wr_cond);
    client_lock.Unlock();
    int rd_ret = rd_cond.wait();              // 等待stat回复
    int wr_ret = wr_cond.wait();              // 等待create回复
    client_lock.Lock();
    bool errored = false;
    if (rd_ret == 0 || rd_ret == -ENOENT)
      have |= POOL_READ;                      // 如果返回0或-ENOENT,则表示有READ权限
    else if (rd_ret != -EPERM) {
      errored = true;                         // stat出现错误
    }
    if (wr_ret == 0 || wr_ret == -EEXIST)
      have |= POOL_WRITE;                     // 如果返回0或-EEXIST,则表示有write权限
    else if (wr_ret != -EPERM) {
      errored = true;                         // create出现错误
    }
    if (errored) {                                              
      pool_perms.erase(perm_key);
      signal_cond_list(waiting_for_pool_perm);  // 唤醒waiting_for_pool_perm
      return -EIO;
    }
    pool_perms[perm_key] = have | POOL_CHECKED;  // 置上POOL_CHECKED标志                          
    signal_cond_list(waiting_for_pool_perm);     // 唤醒waiting_for_pool_perm
  }
  if ((need & CEPH_CAP_FILE_RD) && !(have & POOL_READ)) {    // 如果没有POOL_READ,则返回-EPERM                  
    return -EPERM;
  }
  if ((need & CEPH_CAP_FILE_WR) && !(have & POOL_WRITE)) {   // 如果没有POOL_WRITE,则返回-EPERM
    return -EPERM;
  }
  return 0;
}

检查完对pool的权限后,首先要判断客户端在Inode上拥有的caps是否有"Fw",如果有"Fw",那就得校验要写的范围,如果超过了in->max_size,即endoff > in->max_size,就说明此刻已经超过了mds分给客户端能写的范围,所以需要等待mds分配新的范围即in->max_size,并且要check_caps。

如果have中也没有"As",也需要等待caps,当然还有其他的情况。如果考虑太多,反而不太理解。简而言之,在get_caps中就两件事:

1,如果已有的caps没有"AsFw",则等待caps

2,如果已有的caps有"AsFw",则校验endoff。如果endoff > in->max_size,就去check_caps,并等待;如果endoff < in->max_size,只需要记录"AsFw"的引用。

int Client::get_caps(Inode *in, int need, int want, int *phave, loff_t endoff)
{ 
  int r = check_pool_perm(in, need);
  while (1) {
    int file_wanted = in->caps_file_wanted();  // 此时open_by_mode中有{CEPH_FILE_MODE_WR=1},所以file_wanted = "pAsxXsxFxwb"
    if ((file_wanted & need) != need) { ... }  // "pAsxXsxFxwb" & "AsFw" == "AsFw"
    int implemented;
    int have = in->caps_issued(&implemented);  // have = implemented = "pAsxLsXsxFsxwrcb"
    bool waitfor_caps = false;
    bool waitfor_commit = false;
    if (have & need & CEPH_CAP_FILE_WR) {     // "pAsxLsXsxFsxwrcb" & "AsFw" & "Fw" = "Fw"
      if (endoff > 0 && (endoff >= (loff_t)in->max_size || endoff > (loff_t)(in->size << 1)) 
          && endoff > (loff_t)in->wanted_max_size) {
	    in->wanted_max_size = endoff;                 
	    check_caps(in, 0); 
      }
      // 如果endoff > in->max_size,就说明此刻已经超过了mds分给客户端能写的范围,所以需要等待
      if (endoff >= 0 && endoff > (loff_t)in->max_size) {
	    waitfor_caps = true;
      }
      ...
    }
    if (!waitfor_caps && !waitfor_commit) { // waitfor_caps = false, waitfor_commit = false
      if ((have & need) == need) {          // "pAsxLsXsxFsxwrcb" & "AsFw" == "AsFw"
	    int revoking = implemented & ~have;   // revoking = 0
	    if ((revoking & want) == 0) {         // revoking = 0
	      *phave = need | (have & want);      // *phave = "AsFw" | ("pAsxLsXsxFsxwrcb" & "Fb") = "AsFwb"
	      in->get_cap_ref(need);              // cap_refs : {4=1, 4096=1}
	      return 0;
	    }
      }
    }
    if (waitfor_caps)
      wait_on_list(in->waitfor_caps);
  }
}

check_caps只在endoff >= (loff_t)in->max_size时才执行,算是异常情况,这种情况调用check_caps,无非就是检查caps是否需要从mds那里更新。

总结了下有三种情况需要更新caps:

1,客户端想要的cap,mds没给;

2,mds需要回收的cap,客户端没给;

3,需要更新in->max_size。代码如下。

void Client::check_caps(Inode *in, unsigned flags)
{ // flags = 0
  unsigned wanted = in->caps_wanted();       // wanted = "pAsxXsxFxwb"
  unsigned used = get_caps_used(in);         
  unsigned cap_used;
  int implemented;
  int issued = in->caps_issued(&implemented);    // issued = "pAsxLsXsxFsxwrcb" = implemented
  int revoking = implemented & ~issued;          // revoking = 0
  int retain = wanted | used | CEPH_CAP_PIN;     // retain = "pAsxXsxFxwb"
  if (!unmounting) {
    if (wanted)
      retain |= CEPH_CAP_ANY;                    // retain = "pAsxLsxXsxFsxcrwbl"
  }
  for (auto &it : in->caps) {
    mds_rank_t mds = it.first;
    Cap &cap = it.second;
    MetaSession *session = &mds_sessions.at(mds);
    cap_used = used;                                  
    if (in->auth_cap && &cap != in->auth_cap)
        cap_used &= ~in->auth_cap->issued;
    revoking = cap.implemented & ~cap.issued;          // revoking = 0
    // in->wanted_max_size = 0 < in->max_size = 4M, 如果wanted_max_size超过了max_size,发送caps消息给mds,这里没超过
    if (in->wanted_max_size > in->max_size && in->wanted_max_size > in->requested_max_size 
        && &cap == in->auth_cap)
      goto ack;

    /* approaching file_max? */
    if ((cap.issued & CEPH_CAP_FILE_WR) && &cap == in->auth_cap && is_max_size_approaching(in)) {
      goto ack;
    }

    /* completed revocation? */// 如果需要revoke caps,且revoking中的caps没有被用到,发送caps给mds
    if (revoking && (revoking & cap_used) == 0) {
      goto ack;
    }
    /* want more caps from mds? */// wanted = "pAsxXsxFxwb", cap.wanted = 0, cap.issued = "pAsxLsXsxFsxwrcb",这里为false
    if (wanted & ~(cap.wanted | cap.issued))
      goto ack;
      
    if (wanted == cap.wanted &&         // mds knows what we want.这里wanted = "pAsxXsxFxwb", cap.wanted = 0
	    ((cap.issued & ~retain) == 0) &&// and we don't have anything we wouldn't like
	    !in->dirty_caps)                 // and we have no dirty caps
        continue;

    if (!(flags & CHECK_CAPS_NODELAY)) {
      cap_delay_requeue(in);
      continue;
    }
    ...
 ack:
    ...
    send_cap(in, session, &cap, flags & CHECK_CAPS_SYNCHRONOUS, cap_used, wanted,
	     retain, flushing, flush_tid);
}

需要更新in->max_size的情况就两种

1,in->wanted_max_size > in->max_size,即客户端的wanted_max_size大于max_size。in->wanted_max_size只会在endoff >= in->max_size的情况下更新,这也可以看做是endoff > in->max_size时,需要发送CEPH_CAP_OP_UPDATE消息给mds。

2,is_max_size_approaching(in)为true时,发送CEPH_CAP_OP_UPDATE消息给mds。is_max_size_approaching代码如下:

static bool is_max_size_approaching(Inode *in)
{
  /* mds will adjust max size according to the reported size */
  if (in->flushing_caps & CEPH_CAP_FILE_WR) // 之前已发送的CEPH_CAP_OP_UPDATE消息,mds还没回
    return false;
  if (in->size >= in->max_size)             
    return true;
  /* half of previous max_size increment has been used */
  if (in->max_size > in->reported_size &&
      (in->size << 1) >= in->max_size + in->reported_size)   //  这个不太好理解
    return true;
  return false;
}

这里得单独看(in->size <<1) >= in->max_size + in->reported_size,in->reported_size就是上一次发送CEPH_CAP_OP_UPDATE消息时,客户端已写的size。in->size就可以看做上次写的数据长度 Sn - 1 和这一次写的数据长度  和,最终推导上面的不等式为

(in->size <<1) = 2 * (Sn -1  + Sn ) >= in->max_size + Sn

继续:2 * Sn >= in->max_size - ,这样就很好理解了。即已写的空间超过上一次分配max_size后的剩余空间的一半。

如果不需要发送caps,并且没有标记 CHECK_CAPS_NODELAY,则将caps放入delayed_list,等待5秒后,再拿出来执行check_caps。

caps相关的解决完后,就开始写。写的过程中注意写的模式,如果flags中有O_DIRECT,即f->flags & O_DIRECT为true,则不经过ObjectCache,直接调用filer模块去写;如果flags没有O_DIRECT,则要判断是同步,还是异步写,先写到ObjectCache层,如果是同步写,写到缓存后,flush下。

int Client::_write(Fh *f, int64_t offset, uint64_t size, const char *buf, const struct iovec *iov, int iovcnt)
{
  ...
 
  if (f->flags & O_DIRECT)            // 如果flags中有O_DIRECT,则清除caps中的"Fb"
    have &= ~CEPH_CAP_FILE_BUFFER; 
  if (cct->_conf->client_oc && (have & CEPH_CAP_FILE_BUFFER)) {
    // do buffered write
    if (!in->oset.dirty_or_tx)      
      get_cap_ref(in, CEPH_CAP_FILE_CACHE | CEPH_CAP_FILE_BUFFER);
    get_cap_ref(in, CEPH_CAP_FILE_BUFFER);
    r = objectcacher->file_write(&in->oset, &in->layout, in->snaprealm->get_snap_context(), offset, size, bl, ceph::real_clock::now(),0);
    put_cap_ref(in, CEPH_CAP_FILE_BUFFER);
    // flush cached write if O_SYNC is set on file fh
    // O_DSYNC == O_SYNC on linux < 2.6.33
    // O_SYNC = __O_SYNC | O_DSYNC on linux >= 2.6.33
    if ((f->flags & O_SYNC) || (f->flags & O_DSYNC)) {
      _flush_range(in, offset, size);
    }
    } else {
    if (f->flags & O_DIRECT)
      _flush_range(in, offset, size);
    // simple, non-atomic sync write
    C_SaferCond onfinish("Client::_write flock");
    unsafe_sync_write++;
    get_cap_ref(in, CEPH_CAP_FILE_BUFFER);  // released by onsafe callback

    filer->write_trunc(in->ino, &in->layout, in->snaprealm->get_snap_context(),
		       offset, size, bl, ceph::real_clock::now(), 0,
		       in->truncate_size, in->truncate_seq,
		       &onfinish);
    client_lock.Unlock();
    onfinish.wait();
    client_lock.Lock();
    _sync_write_commit(in);
  }
}

写到ObjectCache或者后端存储后,更新本地的in->size,并将"Fw"标脏。

in->size = totalwritten + offset;
in->mark_caps_dirty(CEPH_CAP_FILE_WR)

最后一步就是通过is_max_size_approaching函数判断是否需要更新max_size。

Client ------>mds

MDS更新max_size

假设需要更新caps,那就得发送MClientCaps给mds,发送MClientCaps内容如下

class MClientCaps : public Message {
 ...
 public:

// 成员变量
  struct ceph_mds_caps_head head;                // head.op = CEPH_CAP_OP_UPDATE
                                                 // head.ino : Inode号
                                                 // head.caps = "pAsxLsXsxFsxcrwb"
                                                 // head.wanted = wanted = "pAsxXsxFxwb"
                                                 // head.dirty = dirty = "Fw"
  uint64_t size;                                 // size = in->size = 2M
  uint64_t  max_size;                            // max_size = in->wanted_max_size
  uint64_t truncate_size;                        
  uint64_t change_attr;                          // Inode中属性的改变次数
  uint32_t truncate_seq;                         
  utime_t mtime,atime, ctime, btime;
  ...
  /* advisory CLIENT_CAPS_* flags to send to mds */
  unsigned flags;
private:
  file_layout_t layout; 
 
}

这里面MClientCaps中max_size存在的作用是计算下一次mds分配给客户端的max_size大小。在Locker::_do_cap_update中的代码如下,

if (m->get_max_size() > new_max) {
    change_max = true;
    forced_change_max = true;
    new_max = calc_new_max_size(latest, m->get_max_size());
} else {
    new_max = calc_new_max_size(latest, size);
    if (new_max > old_max)
	change_max = true;
    else
	new_max = old_max;
}

计算max_size的函数是Locker::calc_new_max_size,如果m->get_max_size()大于之前的max_size时,就以m->get_max_size()为入参;

否则就以当前客户端已写的size为入参。

uint64_t Locker::calc_new_max_size(inode_t *pi, uint64_t size)
{
  uint64_t new_max = (size + 1) << 1;
  uint64_t max_inc = g_conf->mds_client_writeable_range_max_inc_objs;   // max_inc = 1024
  if (max_inc > 0) {
    max_inc *= pi->layout.object_size;                     // max_inc = 1024 * 4M = 4G
    new_max = std::min(new_max, size + max_inc);                        
  }
  return ROUND_UP_TO(new_max, pi->get_layout_size_increment());         // 
}

2 * (size + 1)< size + 4G时,new_max就是2 * (size + 1)

#define ROUND_UP_TO(n, d) ((n)%(d) ? ((n)+(d)-(n)%(d)) : (n))

最终max_size约等于2 * size对齐4M的值,简单来看就是客户端写了多少,MDS下次再分配2倍的空间给客户端。

2 * (size + 1)> size + 4G时,new_max就是size + 4G,最终max_size就是size + 4G,MDS每次多分配4G给客户端。

file_write

这个涉及到ObjectCache,待续...

对应PPT:ceph源码io读写流程分析串讲-CSDN下载

一、OSD模块简介

1.1 消息封装:在OSD上发送和接收信息。

cluster_messenger -与其它OSDs和monitors沟通
client_messenger -与客户端沟通

1.2 消息调度

Dispatcher类,主要负责消息分类

1.3 工作队列:

1.3.1 OpWQ: 处理ops(从客户端)和sub ops(从其他的OSD)。运行在op_tp线程池。

1.3.2 PeeringWQ: 处理peering任务,运行在op_tp线程池。

1.3.3 CommandWQ:处理cmd命令,运行在command_tp。

1.3.4 RecoveryWQ: 数据修复,运行在recovery_tp。

1.3.5 SnapTrimWQ: 快照相关,运行在disk_tp。

1.3.6 ScrubWQ: scrub,运行在disk_tp。

1.3.7 ScrubFinalizeWQ: scrub,运行在disk_tp。

1.3.8 RepScrubWQ: scrub,运行在disk_tp。

1.3.9 RemoveWQ: 删除旧的pg目录。运行在disk_tp。

1.4 线程池:

有4种OSD线程池:

1.4.1 op_tp: 处理ops和sub ops

1.4.2 recovery_tp:处理修复任务

1.4.3 disk_tp: 处理磁盘密集型任务

1.4.4 command_tp: 处理命令

1.5 主要对象:

ObjectStore *store;

OSDSuperblock superblock; 主要是版本号等信息

OSDMapRef  osdmap;

1.6 主要操作流程: 参考文章

1.6.1 客户端发起请求过程

1.6.2 op_tp线程处理数据读取

1.6.3 对象操作的处理过程

1.6.4 修改操作的处理

1.6.5 日志的写入

1.6.6 写操作处理

1.6.7 事务的sync过程

1.6.8 日志恢复

1.7 整体处理过程图

Ceph OSD 使用诸如Btrfs 和XFS 的日志文件系统。在将数据提交到备用存储之前,Ceph 首先将数据写入一个称为日志( journal) 的独立存储区域,日志是相同的机械磁盘(如OSD) 或不同的SSD 磁盘或分区上一小块缓冲区大小的分区,甚至也可以是文件系统上的一个文件。在这种机制中,Ceph 的所有写都是先到日志,然后再到备用存储,如下图所示。
 



作者:启迪云
链接:https://www.zhihu.com/question/21718731/answer/561372178

二、客户端写入数据大致流程及保存形式

2.1 读写框架

              image           

image       image             

2.2 客户端写入流程

在客户端使用 rbd 时一般有两种方法:

  • 第一种 是 Kernel rbd。就是创建了rbd设备后,把rbd设备map到内核中,形成一个虚拟的块设备,这时这个块设备同其他通用块设备一样,一般的设备文件为/dev/rbd0,后续直接使用这个块设备文件就可以了,可以把 /dev/rbd0 格式化后 mount 到某个目录,也可以直接作为裸设备使用。这时对rbd设备的操作都通过kernel rbd操作方法进行的。 
  • 第二种是 librbd 方式。就是创建了rbd设备后,这时可以使用librbd、librados库进行访问管理块设备。这种方式不会map到内核,直接调用librbd提供的接口,可以实现对rbd设备的访问和管理,但是不会在客户端产生块设备文件。

应用写入rbd块设备的过程:

  1. 应用调用 librbd 接口或者对linux 内核虚拟块设备写入二进制块。下面以 librbd 为例。
  2. librbd 对二进制块进行分块,默认块大小为 4M,每一块都有名字,成为一个对象
  3. librbd 调用 librados 将对象写入 Ceph 集群
  4. librados向 主OSD 写入分好块的二进制数据块 (先建立TCP/IP连接,然后发送消息给 OSD,OSD 接收后写入其磁盘)
  5. 主OSD 负责同时向一个或者多个次 OSD 写入副本。注意这里是写到日志(Journal)就返回,因此,使用SSD作为Journal的话,可以提高响应速度,做到服务器端对客户端的快速同步返回写结果(ack)。
  6. 当主次OSD都写入完成后,主OSD 向客户端返回写入成功。
  7. 当一段时间(也许得几秒钟)后Journal 中的数据向磁盘写入成功后,Ceph通过事件通知客户端数据写入磁盘成功(commit),此时,客户端可以将写缓存中的数据彻底清除掉了。
  8. 默认地,Ceph 客户端会缓存写入的数据直到收到集群的commit通知。如果此阶段内(在写方法返回到收到commit通知之间)OSD 出故障导致数据写入文件系统失败,Ceph 将会允许客户端重做尚未提交的操作(replay)。因此,PG 有个状态叫 replay:“The placement group is waiting for clients to replay operations after an OSD crashed.”。

                                                       

也就是,文件系统负责文件处理,librbd 负责块处理,librados 负责对象处理,OSD 负责将数据写入在Journal和磁盘中。

2.3 RBD保存形式

如下图所示,Ceph 系统中不同层次的组件/用户所看到的数据的形式是不一样的:

                         

  • Ceph 客户端所见的是一个完整的连续的二进制数据块,其大小为创建 RBD image 是设置的大小或者 resize 的大小,客户端可以从头或者从某个位置开始写入二进制数据。
  • librados 负责在 RADOS 中创建对象(object),其大小为 pool 的 order 决定,默认情况下 order = 22 此时 object 大小为 4MB;以及负责将客户端传入的二进制块条带化为若干个条带(stripe)。
  • librados 控制哪个条带由哪个 OSD 写入(条带 ---写入哪个----> object ----位于哪个 ----> OSD)
  • OSD 负责创建在文件系统中创建文件,并将 librados 传入的数据写入数据。

  Ceph client 向一个 RBD image 写入二进制数据(假设 pool 的拷贝份数为 3):

(1)Ceph client 调用 librados 创建一个 RBD image,这时候不会做存储空间分配,而是创建若干元数据对象来保存元数据信息。

(2)Ceph client 调用 librados 开始写数据。librados 计算条带、object 等,然后开始写第一个 stripe 到特定的目标object。

(3)librados 根据 CRUSH 算法,计算出 object 所对应的主 OSD ID,并将二进制数据发给它。

(4)主OSD 负责调用文件系统接口将二进制数据写入磁盘上的文件(每个 object 对应一个 file,file 的内容是一个或者多个 stripe)。

(5)主ODS 完成数据写入后,它使用 CRUSH 算啊计算出第二个OSD(secondary OSD)和第三个OSD(tertiary OSD)的位置,然后向这两个 OSD 拷贝对象。都完成后,它向 ceph client 反馈该 object 保存完毕。

(6)然后写第二个条带,直到全部写入完成。全部完成后,librados 还应该会做元数据更新,比如写入新的 size 等。

完整的过程(来源):

osd(object store daemon )  守护进程(daemon)是一类在后台运行的特殊进程 。

 该过程具有强一致性的特点:

  • Ceph 的读写操作采用 Primary-Replica 模型,Client 只向 Object 所对应 OSD set 的 Primary 发起读写请求,这保证了数据的强一致性。
  • 由于每个 Object 都只有一个 Primary OSD,因此对 Object 的更新都是顺序的,不存在同步问题。
  • 当 Primary 收到 Object 的写请求时,它负责把数据发送给其他 Replicas,只要这个数据被保存在所有的OSD上时,Primary 才应答Object的写请求,这保证了副本的一致性。这也带来一些副作用。相比那些只实现了最终一致性的存储系统比如 Swift,Ceph 只有三份拷贝都写入完成后才算写入完成,这在出现磁盘损坏时会出现写延迟增加。

    在 OSD 上,在收到数据存放指令后,它会产生2~3个磁盘seek操作:

  • 把写操作记录到 OSD 的 Journal 文件上(Journal是为了保证写操作的原子性)。
  • 把写操作更新到 Object 对应的文件上。
  • 把写操作记录到 PG Log 文件上。

三、客户端请求流程(转的一只小江的博文,写的挺好的)

RADOS读对象流程

                             image              

 RADOS写对象操作流程

                                   image          

例子:

#!/usr/bin/env python
import sys,rados,rbd
def connectceph():
      cluster = rados.Rados(conffile = '/root/xuyanjiangtest/ceph-0.94.3/src/ceph.conf')
      cluster.connect()
      ioctx = cluster.open_ioctx('mypool')
      rbd_inst = rbd.RBD()
      size = 4*1024**3 #4 GiB
      rbd_inst.create(ioctx,'myimage',size)
      image = rbd.Image(ioctx,'myimage')
      data = 'foo'* 200
      image.write(data,0)
      image.close()
      ioctx.close()
        cluster.shutdown()
 
if __name__ == "__main__":
        connectceph()

1. 首先cluster = rados.Rados(conffile = 'ceph.conf'),用当前的这个ceph的配置文件去创建一个rados,这里主要是解析ceph.conf中中的集群配置参数。然后将这些参数的值保存在rados中。

2. cluster.connect() ,这里将会创建一个radosclient的结构,这里会把这个结构主要包含了几个功能模块:

消息管理模块Messager,数据处理模块Objector,finisher线程模块。

3. ioctx = cluster.open_ioctx('mypool'),为一个名字叫做mypool的存储池创建一个ioctx ,ioctx中会指明radosclient与Objector模块,同时也会记录mypool的信息,包括pool的参数等。

4. rbd_inst.create(ioctx,'myimage',size) ,创建一个名字为myimage的rbd设备,之后就是将数据写入这个设备。

5. image = rbd.Image(ioctx,'myimage'),创建image结构,这里该结构将myimage与ioctx 联系起来,后面可以通过image结构直接找到ioctx。这里会将ioctx复制两份,分为为data_ioctx和md_ctx。见明知意,一个用来处理rbd的存储数据,一个用来处理rbd的管理数据。

流程图:

                      143540_OSPk_2460844  

1. image.write(data,0),通过image开始了一个写请求的生命的开始。这里指明了request的两个基本要素 buffer=data 和 offset=0。由这里开始进入了ceph的世界,也是c++的世界。

由image.write(data,0)  转化为librbd.cc 文件中的Image::write() 函数,来看看这个函数的主要实现

ssize_t Image::write(uint64_t ofs, size_t len, bufferlist& bl)
{      
  ImageCtx *ictx = (ImageCtx *)ctx;     
  int r = librbd::write(ictx, ofs, len, bl.c_str(), 0);     
  return r;      
}

2. 该函数中直接进行分发给了librbd::wrte的函数了。跟随下来看看librbd::write中的实现。该函数的具体实现在internal.cc文件中。

ssize_t write(ImageCtx *ictx, uint64_t off, size_t len, const char *buf, int op_flags)
{     
    Context *ctx = new C_SafeCond(&mylock, &cond, &done, &ret);   //---a     
    AioCompletion *c = aio_create_completion_internal(ctx, rbd_ctx_cb);//---b     
    r = aio_write(ictx, off, mylen, buf, c, op_flags);  //---c       
    while (!done)            
      cond.Wait(mylock);  // ---d
}

---a.这句要为这个操作申请一个回调操作,所谓的回调就是一些收尾的工作,信号唤醒处理。

---b。这句是要申请一个io完成时 要进行的操作,当io完成时,会调用rbd_ctx_cb函数,该函数会继续调用ctx->complete()。

---c.该函数aio_write会继续处理这个请求。

---d.当c句将这个io下发到osd的时候,osd还没请求处理完成,则等待在d上,直到底层处理完请求,回调b申请的 AioCompletion, 继续调用a中的ctx->complete(),唤醒这里的等待信号,然后程序继续向下执行。

3.再来看看aio_write 拿到了 请求的offset和buffer会做点什么呢?

int aio_write(ImageCtx *ictx, uint64_t off, size_t len, const char *buf,            AioCompletion *c, int op_flags)
{       
    //将请求按着object进行拆分       
    vector<ObjectExtent> extents;       
    if (len > 0)        
    {          
      Striper::file_to_extents(ictx->cct, ictx->format_string, &ictx->layout, off, 
      clip_len,0, extents);   //---a       
    } 
    
    //处理每一个object上的请求数据       
    for (vector<ObjectExtent>::iterator p = extents.begin(); p != extents.end(); ++p)
    {            
      C_AioWrite *req_comp = new C_AioWrite(cct, c); //---b            
      AioWrite *req = new AioWrite(ictx, p->oid.name, p->objectno, p- >offset,bl,….., 
      req_comp);     //---c                
      r = req->send();    //---d       
    }
}

根据请求的大小需要将这个请求按着object进行划分,由函数file_to_extents进行处理,处理完成后按着object进行保存在extents中。file_to_extents()存在很多同名函数注意区分。这些函数的主要内容做了一件事儿,那就对原始请求的拆分。(file_to_extents :https://blog.csdn.net/don_chiang709/article/details/90607215

一个rbd设备是有很多的object组成,也就是将rbd设备进行切块,每一个块叫做object,每个object的大小默认为4M,也可以自己指定。file_to_extents函数将这个大的请求分别映射到object上去,拆成了很多小的请求如下图。最后映射的结果保存在ObjectExtent中。

                              

 原本的offset是指在rbd内的偏移量(写入rbd的位置),经过file_to_extents后,转化成了一个或者多个object的内部的偏移量offset0。这样转化后处理一批这个object内的请求。

4. 再回到 aio_write函数中,需要将拆分后的每一个object请求进行处理。

---b.为写请求申请一个回调处理函数。

---c.根据object内部的请求,创建一个叫做AioWrite的结构。

---d.将这个AioWrite的req进行下发send().

5. 这里AioWrite 是继承自 AbstractWrite ,AbstractWrite 继承自AioRequest类,在AbstractWrite 类中定义了send的方法,看下send的具体内容.

int AbstractWrite::send()  {      
  if (send_pre())           //---a
}

#进入send_pre()函数中
bool AbstractWrite::send_pre()
{
  m_state = LIBRBD_AIO_WRITE_PRE;   // ----a       
  FunctionContext *ctx =    //----b            
  new FunctionContext( boost::bind(&AioRequest::complete, this, _1));       
  m_ictx->object_map.aio_update(ctx); //-----c
}

---a.修改m_state 状态为LIBRBD_AIO_WRITE_PRE。

---b.申请一个回调函数,实际调用AioRequest::complete()

---c.开始下发object_map.aio_update的请求,这是一个状态更新的函数,不是很重要的环节,这里不再多说,当更新的请求完成时会自动回调到b申请的回调函数。

6. 进入到AioRequest::complete() 函数中。

void AioRequest::complete(int r)
{     
 if (should_complete(r))   //---a
}

---a.should_complete函数是一个纯虚函数,需要在继承类AbstractWrite中实现,来7. 看看AbstractWrite:: should_complete()

AbstractWrite:: should_complete()

bool AbstractWrite::should_complete(int r)
{    
  switch (m_state)     
  {         
   case LIBRBD_AIO_WRITE_PRE:  //----a        
    {           
      send_write(); //----b

----a.在send_pre中已经设置m_state的状态为LIBRBD_AIO_WRITE_PRE,所以会走这个分支。

----b. send_write()函数中,会继续进行处理,

7.1.下面来看这个send_write函数

void AbstractWrite::send_write()
{      
  m_state = LIBRBD_AIO_WRITE_FLAT;   //----a       
  add_write_ops(&m_write);    // ----b       
  int r = m_ictx->data_ctx.aio_operate(m_oid, rados_completion, &m_write);
}

---a.重新设置m_state的状态为 LIBRBD_AIO_WRITE_FLAT。

---b.填充m_write,将请求转化为m_write。

---c.下发m_write  ,使用data_ctx.aio_operate 函数处理。继续调用io_ctx_impl->aio_operate()函数,继续调用objecter->mutate().

8. objecter->mutate()

ceph_tid_t mutate(……..)  
{     
  Op *o = prepare_mutate_op(oid, oloc, op, snapc, mtime, flags, 
onack, oncommit, objver);//----d     
 return op_submit(o);
}

---d.将请求转化为Op请求,继续使用op_submit下发这个请求。在op_submit中继续调用_op_submit_with_budget处理请求。继续调用_op_submit处理。

8.1 _op_submit 的处理过程。这里值得细看

ceph_tid_t Objecter::_op_submit(Op *op, RWLock::Context& lc)
{
    check_for_latest_map = _calc_target(&op->target, &op->last_force_resend); //---a     
    int r = _get_session(op->target.osd, &s, lc);  //---b    
    _session_op_assign(s, op); //----c     _send_op(op, m); //----d
}

----a. _calc_target,通过计算当前object的保存的osd,然后将主osd保存在target中,rbd写数据都是先发送到主osd,主osd再将数据发送到其他的副本osd上。这里对于怎么来选取osd集合与主osd的关系就不再多说,在《ceph的数据存储之路(3)》中已经讲述这个过程的原理了,代码部分不难理解。

----b. _get_session,该函数是用来与主osd建立通信的,建立通信后,可以通过该通道发送给主osd。再来看看这个函数是怎么处理的

9. _get_session

int Objecter::_get_session(int osd, OSDSession **session, RWLock::Context& lc)
{     
  map<int,OSDSession*>::iterator p = osd_sessions.find(osd);   //----a  
  
 if (p != osd_sessions.end()) {
    auto s = p->second;
    s->get();
    *session = s;
    return 0;
  }   
  OSDSession *s = new OSDSession(cct, osd); //----b     
  osd_sessions[osd] = s;//--c     
  s->con = messenger->get_connection(osdmap->get_inst(osd));//-d
……
}

----a.首先在osd_sessions中查找是否已经存在一个连接可以直接使用,第一次通信是没有的。

----b.重新申请一个OSDSession,并且使用osd等信息进行初始化。

---c. 将新申请的OSDSession添加到osd_sessions中保存,以备下次使用。

----d.调用messager的get_connection方法。在该方法中继续想办法与目标osd建立连接。

10. messager 是由子类simpleMessager实现的,下面来看下SimpleMessager中get_connection的实现方法

ConnectionRef SimpleMessenger::get_connection(const entity_inst_t& dest)
{     
  Pipe *pipe = _lookup_pipe(dest.addr);     //-----a     
  if (pipe)  {     
  } else {       
   pipe = connect_rank(dest.addr, dest.name.type(), NULL, NULL); //----b     
  }
}

----a.首先要查找这个pipe,第一次通信,自然这个pipe是不存在的。

----b. connect_rank 会根据这个目标osd的addr进行创建。看下connect_rank做了什么。

11. SimpleMessenger::connect_rank

Pipe *SimpleMessenger::connect_rank(const entity_addr_t& addr,  int type, PipeConnection *con,    Message *first)
{      
    Pipe *pipe = new Pipe(this, Pipe::STATE_CONNECTING, static_cast<PipeConnection*>(con));      //----a     
   pipe->set_peer_type(type); //----b     
   pipe->set_peer_addr(addr); //----c     
   pipe->policy = get_policy(type); //----d     
   pipe->start_writer();  //----e     
   return pipe; //----f
}

----a.首先需要创建这个pipe,并且pipe同pipecon进行关联。

----b,----c,-----d。都是进行一些参数的设置。

----e.开始启动pipe的写线程,这里pipe的写线程的处理函数pipe->writer(),该函数中会尝试连接osd。并且建立socket连接通道。

目前的资源统计一下,写请求可以根据目标主osd,去查找或者建立一个OSDSession,这个OSDSession中会有一个管理数据通道的Pipe结构,然后这个结构中存在一个发送消息的处理线程writer,这个线程会保持与目标osd的socket通信。

12. 建立并且获取到了这些资源,这时再回到_op_submit 函数中

ceph_tid_t Objecter::_op_submit(Op *op, RWLock::Context& lc)
{
    check_for_latest_map = _calc_target(&op->target, &op->last_force_resend); //---a     
    int r = _get_session(op->target.osd, &s, lc);  //---b     
    _session_op_assign(s, op); //----c     
    MOSDOp *m = _prepare_osd_op(op); //-----d     
    _send_op(op, m); //----e
}

---c,将当前的op请求与这个session进行绑定,在后面发送请求的时候能知道使用哪一个session进行发送。

--d,将op转化为MOSDop,后面会以MOSDOp为对象进行处理的。

---e,_send_op 会根据之前建立的通信通道,将这个MOSDOp发送出去。_send_op 中调用op->session->con->send_message(m),这个方法会调用SimpleMessager-> send_message(m), 再调用_send_message(),再调用submit_message().在submit_message会找到之前的pipe,然后调用pipe->send方法,最后通过pipe->writer的线程发送到目标osd。

自此,客户就等待osd处理完成返回结果了。

                  

1.看左上角的rados结构,首先创建io环境,创建rados信息,将配置文件中的数据结构化到rados中。

2.根据rados创建一个radosclient的客户端结构,该结构包括了三个重要的模块,finiser 回调处理线程、Messager消息处理结构、Objector数据处理结构。

最后的数据都是要封装成消息 通过Messager发送给目标的osd。

3.根据pool的信息与radosclient进行创建一个ioctx,这里面包好了pool相关的信息,然后获得这些信息后在数据处理时会用到。

4.紧接着会复制这个ioctx到imagectx中,变成data_ioctx与md_ioctx数据处理通道,最后将imagectx封装到image结构当中。之后所有的写操作都会通过这个image进行。顺着image的结构可以找到前面创建并且可以使用的数据结构。

5.通过最右上角的image进行读写操作,当读写操作的对象为image时,这个image会开始处理请求,然后这个请求经过处理拆分成object对象的请求。拆分后会交给objector进行处理查找目标osd,当然这里使用的就是crush算法,找到目标osd的集合与主osd。

6.将请求op封装成MOSDOp消息,然后交给SimpleMessager处理,SimpleMessager会尝试在已有的osd_session中查找,如果没有找到对应的session,则会重新创建一个OSDSession,并且为这个OSDSession创建一个数据通道pipe,把数据通道保存在SimpleMessager中,可以下次使用。

7.pipe 会与目标osd建立Socket通信通道,pipe会有专门的写线程writer来负责socket通信。在线程writer中会先连接目标ip,建立通信。消息从SimpleMessager收到后会保存到pipe的outq队列中,writer线程另外的一个用途就是监视这个outq队列,当队列中存在消息等待发送时,会就将消息写入socket,发送给目标OSD。

8. 等待OSD将数据消息处理完成之后,就是进行回调,反馈执行结果,然后一步步的将结果告知调用者。

四、Ceph读流程

OSD端读消息分发流程

                                    image               

OSD端读操作处理流程

                                             image

总体流程图:

                                        

4ac886ce405a7e638b2979b693802a8e

int read(inodeno_t ino,
             file_layout_t *layout,
             snapid_t snap,
             uint64_t offset,
             uint64_t len,
             bufferlist *bl,   // ptr to data
             int flags,
             Context *onfinish,
             int op_flags = 0)    --------------------------------Filer.h

Striper::file_to_extents(cct, ino, layout, offset, len, truncate_size, extents);//将要读取数据的长度和偏移转化为要访问的对象,extents沿用了brtfs文件系统的概念
objecter->sg_read_trunc(extents, snap, bl, flags, truncate_size, truncate_seq, onfinish, op_flags);//向osd发起请求

对于读操作而言:

1.客户端直接计算出存储数据所属于的主osd,直接给主osd上发送消息。

2.主osd收到消息后,可以调用Filestore直接读取处在底层文件系统中的主pg里面的内容然后返回给客户端。具体调用函数在ReplicatedPG::do_osd_ops中实现。

CEPH_OSD_OP_MAPEXT||CEPH_OSD_OP_SPARSE_READ

r = osd->store->fiemap(coll, soid, op.extent.offset, op.extent.length, bl);

CEPH_OSD_OP_READ

r = pgbackend->objects_read_sync(soid, miter->first, miter->second, &tmpbl);

五、Ceph写流程

OSD端写操作处理流程

                                   image

而对于写操作而言,由于要保证数据写入的同步性就会复杂很多:

1.首先客户端会将数据发送给主osd,

2.主osd同样要先进行写操作预处理,完成后它要发送写消息给其他的从osd,让他们对副本pg进行更改,

3.从osd通过FileJournal完成写操作到Journal中后发送消息告诉主osd说完成,进入5

4.当主osd收到所有的从osd完成写操作的消息后,会通过FileJournal完成自身的写操作到Journal中。完成后会通知客户端,已经完成了写操作。

5.主osd,从osd的线程开始工作调用Filestore将Journal中的数据写入到底层文件系统中。

写的逻辑流程图如图:

                     

从图中我们可以看到写操作分为以下几步:
1.OSD::op_tp线程从OSD::op_wq中拿出来操作如本文开始的图上描述,具体代码流是

                     

    ReplicatePG::apply_repop中创建回调类C_OSD_OpCommit和C_OSD_OpApplied

    FileStore::queue_transactions中创建了回调类C_JournaledAhead

2.FileJournal::write_thread线程从FileJournal::writeq中拿出来操作,主要就是写数据到具体的journal中,具体代码流:

                

3.Journal::Finisher.finisher_thread线程从Journal::Finisher.finish_queue中拿出来操作,通过调用C_JournalAhead留下的回调函数FileStore:_journaled_ahead,该线程开始工作两件事:首先入底层FileStore::op_wq通知开始写,再入FileStore::ondisk_finisher.finisher_queue通知可以返回。具体代码流:

           

4.FileStore::ondisk_finisher.finisher_thread线程从FileStore::ondisk_finisher.finisher_queue中拿出来操作,通过调用C_OSD_OpCommit留下来的回调函数ReplicatePG::op_commit,通知客户端写操作成功

                                     

5.FileStore::op_tp线程池从FileStore::op_wq中拿出操作(此处的OP_WQ继承了父类ThreadPool::WorkQueue重写了_process和_process_finish等函数,所以不同于OSD::op_wq,它有自己的工作流程),首先调用FileStore::_do_op,完成后调用FileStore::_finish_op。

                

6. FileStore::op_finisher.finisher_thread线程从FileStore::op_finisher.finisher_queue中拿出来操作,通过调用C_OSD_OpApplied留下来的回调函数ReplicatePG::op_applied,通知数据可读。

                                    

具体OSD方面的源码逐句解析可以参考一只小江的博文

PPT下载地址:https://download.csdn.net/download/guzyguzyguzy/8853025?ops_request_misc=&request_id=&biz_id=103&utm_term=Ceph%E8%AF%BB%E5%86%99%E6%B5%81%E7%A8%8B&utm_medium=distribute.pc_search_result.none-task-download-2~download~sobaiduweb~default-1-8853025.pc_v2_rank_dl_default&spm=1018.2226.3001.4451.2 

  • 1
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值