背景:
Ceph QA 中使用了rados model 工具做端到端的检验,写数据下去,在读数据上来。对比osd 的数据跟客户端的数据是否一致。定位qa 数据问题绕不开rados model 流程。
使用方法:
ceph_test_rados --max-ops 4000 --objects 1024 --max-in-flight 64 --size 4000000 --min-stride-size 400000 --max-stride-size 800000 --max-seconds 3600 --op snap_remove 50 --op snap_create 50 --op rollback 50 --op setattr 25 --op read 100 --op clone_create 50 --op write 50 --op write_excl 50 --op flatten 50 --op rmattr 25 --op delete 50 --pool rbd
模块初始化:
初始化 RadosTestContext context;初始化rados 链接;
初始化WeightedTestGenerator;继承于TestOpGenerator,可以根据传入的各种op的权重生成op(gen_op)。
Loop context.loop(&gen);主要逻辑;
根据op权重,循环产生op,首先写op初始化对象。随机生成一个数,如果比op 累计权重小,gen_op。如果超过总的op个数结束。如果超过最大inflight的op个数,等待op 完成。
gen_op,会从RadosTestContext的oid_not_in_use中选出一个oid,生成op,然后从oid_not_in_use中删除该oid。等op完成后oid_not_in_use中再插入该oid。
单线程异步读写。
test op 类型:
TEST_OP_READ
TEST_OP_WRITE
TEST_OP_WRITE_EXCL
TEST_OP_WRITESAME
TEST_OP_DELETE
TEST_OP_SNAP_CREATE
TEST_OP_SNAP_REMOVE
TEST_OP_ROLLBACK
TEST_OP_CLONE_CREATE
TEST_OP_SETATTR
TEST_OP_RMATTR
TEST_OP_WATCH
TEST_OP_XCOPY
TEST_OP_APPEND
TEST_OP_APPEND_EXCL
TEST_OP_FLATTEN
重要的数据结构:
class RadosTestContext
1. map<uint64_t, CloneContext> clones;
CloneContext:
seq //最大快照
snaps //记录clone的快照ids
current_snap
2. map<uint64_t, Objects> pool_obj_cont; //clone : 对象,各个clone对象的各个snaps 对象
class Objects {
public:
map<int, map<string, ObjectDesc>> snaps; // 该map snapid : <oid_name, 对象>
bool is_cross_pool;
int cross_layers;
Objects(): is_cross_pool(false), cross_layers(0)
{}
};
3. cloned_snaps 创建clone的时候插入,保证snap remove的时候不选中,删除该snap;
clone 对象存储分数据和元数据,元数据在clones 中保存。
数据在pool_obj_cont中保存。
class ContDesc //内容的描述信息
int objnum; //对象id,跟seq num 一样
int cursnap; //当前快照
unsigned seqnum; //seq num,用于生成数据。
std::string prefix; //对象前缀
std::string oid; //对象名
class ObjectDesc
对象描述,保存ContDesc和数据生成器。 有生成数据和check 数据一致性的接口。
std::list<std::pair<ceph::shared_ptr<ContentsGenerator>, ContDesc>> layers; //数据层
std::map<std::string, ContDesc> attrs; //attrs 信息
bufferlist header; //header 数据信息 也是一种元数据,以"_"开头的。
class CloneContext 在建clone的时候会建一个对象。继承父的parents再加上本次的父快照和自己。
uint64_t seq;
map<int, uint64_t> snaps; //记录了本地快照和快照的映射关系
int current_snap; //本地当前快照id,相当于head 对象
class ContState //layer 遍历数据的时候,用的
interval_set<uint64_t> ranges; //数据有效范围,通过seqnum 产生
const uint64_t size;
public:
ContDesc cont; //内容描述
ceph::shared_ptr<ContentsGenerator> gen; //数据生成器
ContentsGenerator::iterator iter; //数据生成器 迭代器
std::list<std::pair<std::list<ContState>::iterator, StackState>> stack;
struct StackState
{
const uint64_t next; //下一段 有效数据的 start
const uint64_t size;
};
//主要用于读数据时,遇到空洞,需要向下层layer寻找有效数据时存放stack 使用。
核心数据操作:
写操作,如何生成数据?
读操作,怎么验证数据一致性?
数据生成器:
AppendGenerator append 数据生成器
AlignmentGenerator 对齐io的数据生成器
VarLenGenerator 非对齐io的数据生成器
VarLenZeroGenerator 产生0 的数据生成器
append op 调用AppendGenerator生成数据,如果需要对齐则随机调用AlignmentGenerator 和 VarLenZeroGenerator生成数据。如果不需要对齐写则调用VarLenGenerator 和 VarLenZeroGenerator 生成数据。
生成数据过程:
cont_gen->get_ranges_map 生成数据
1.根据传入的ContDesc的seqnum,生成数据范围map <offset, len>。每次非读op, seqnum都会自增。
2.根据传入的max_stride_size 和 min_stride_size,生成多段位于max_stride_size 和 min_stride_size之间的数据范围。间隔加入有效数据范围(奇数次加入,偶数次不加入)。
ContentsGenerator::iterator gen_pos = cont_gen->get_iterator;
数据生成器的核心是伪随机数生成器,minstd_rand0 (minstd_rand0 线性同余法 : xi+1=(axi+c) mod m)。是std提供的配置好a,c,m的随机数生成器。只要提供第一个种子,后续数字都是确定的。这样也就提供了可以重复产生相同数据的基础。只需要保存种子,就可以每次产生相同的数据。
gen_pos.seek 等游标操作,基本都是调用rand() 向后移动。
context->update_object 更新数据,保存ContDesc 和 gen (数据生成器)到ObjectDesc的layers中。
写数据只会用到最新一层数据。写数据的时候会把父的数据直接插入自己的layers中。
写数据也是append only 模式,新插入一层数据layer,覆盖旧的部分数据。
生成器每次生成是有一个最大长度,该长度也是根据seqnum 生成的。
数据生成器,怎么实现append 数据操作呢?
AppendGenertor 与其他数据生成器不同的是,会记录一个offset。每个get_ranges_map 是从offset 开始算起的。
数据读取过程:
读数据则可能用到老数据(比如最新一层数据,某些offset len是空洞)。
可以参考:get_data() 接口;
ObjectDesc::gen_data:现使用的offset/len 都是0,就是获取该对象的 所有数据。
1. 初始化ObjectDesc::iterator
1.1 把 obj.layers 组成ContState 添加 到iterator 的layers。
1.2 初始化current指针为第一层layer。
1.3 调整stack adjust_stack
2. 获取数据 gen_bl_advance
读取数据时,以上层layer 数据为准,上层数据覆盖底层数据。
具体怎么实现呢?
每层数据都有一个有效区以intervals的形式存储。读的时候首先读最上层layer的数据,当数据不在有效区的时候,会尝试从下层数据读取数据。一种极端的情况是所有层在该位置都是空洞,则该位置填充0。
这里介绍几个基本概念,pos 表示当前读取数据的位置;next 表示本层layer 下一段有效数据的起始位置。cur_valid_till 表示本层数据如果pos 超过该位置,数据就不准确了,需要调整stack(比如pop stack 元素等);
下面介绍下正常情况,以上图为例:
当toplayer 在pos的位置是空洞时,把该层数据压入栈stack。如果下层数据也是空洞,满足一定条件时,也需要把该层数据压入stack(为什么是满足一定条件呢?并不是所有的layer 都需要压入stack,只有该层数据不包含pos,且next < 下一层数据的next的时候可以入stack)。比如找到了layer5 ,该layer 刚好有pos的数据,读取这一段数据。
什么时候出栈呢?
当pos 大于cur_valid_till的时候,表述该层数据不准确了,需要读取stack中下层数据。这时候就会把stack 顶端的layer 数据出栈。调整current 指针,然后读取本层数据的内容。
以此类推,每次遇到空洞都会设计到stack 操作,以保证读取正确layer的数据。