Ceph OSD日志分析

时间:2022-03-08 12:46:21

Thomas是本人在Ceph中国社区翻译小组所用的笔名,该文首次发布在Ceph中国社区,现转载到本人博客,以供大家传阅

Ceph OSD日志分析

本文由 Ceph中国社区-Thomas翻译,陈晓熹校稿 。

英文出处:CEPH OSD JOURNAL 欢迎加入 翻译小组

简介

与ext4这类日志文件系统类似,Ceph OSD日志也是一种事务日志。它是基于文件系统的OSD的关键组成部分,提供存储系统所需的一致性保证。

让我们从Ceph documentation中有关日志的描述开始:

Ceph OSD使用日志有两个原因:速度及一致性。

速度: 日志使得Ceph OSD Daemon进程能够快速的提交小IO。Ceph将小的随机IO顺序的写入日志,让后端文件系统有更多时间来合并IO,提升系统的突发负载能力。然而,这可能带来剧烈的性能抖动,表现为一段时间内的高速写入,而之后一段时间内没有任何写入直至文件系统追上日志的进度。

一致性:Ceph OSD Daemon进程需要一个文件系统接口来保证多个操作的原子性。Ceph OSD Daemon进程提交操作描述到日志并将操作应用到文件系统。这使得能够原子的更新一个对象(如:pg metadata)。在filestore配置的[min_sync_interval max_sync_interval]间隔范围内,Ceph OSD Daemon进程会停止写操作,并同步文件系统与日志,建立同步点[译者注:此时会记录已同步的最大Seq号],以便Ceph OSD Daemon清除日志项、重用日志空间。在故障发生后,Ceph OSD Daemon从最后一个同步操作点开始重放日志。

为什么?

一个事务完成后就会得到ObjectStore的通知。但是,何时完成才算真的完成呢?

当一个write系统调用返回时只能保证后续的read能读到之前写入的数据。系统内核将数据放在缓存中,而不会立刻写入到磁盘。这主要是性能原因。你可以通过设置特殊选项来打开磁盘[译者注:O_DSYNC],等数据写入到磁盘才让write返回。但是,这会导致性能急剧下降,特别是有很多小IO写的时候。Ceph通过完全日志来解决这个问题。即日志中同时包含数据和元数据。因此,每个操作都要写两次:首先是写到日志,之后要应用到磁盘。日志按上面描述的方式打开[译者注:Ceph默认以O_DSYNC + O_DIRECT 方式打开日志]从而保证日志直到实际写入磁盘后才返回。日志的顺序写一定程度上弥补了direct IO的低效。周期性地,或者由事务触发,或者日志满,都会引起磁盘同步(fssync)及日志清理。

术语

在下面的叙述中,我称服务于OSD存储的块设备为磁盘,承载日志的设备、不管是何种设备类型都称为日志。

相关工作

版本信息

本文基于Ceph master commit-ish 0a76aa5 from 2015-03-16而作。

概述

本文聚焦于日志的实现,通过代码分析达成该目的。

作为该文的兴趣引入点,我将先回顾源代码。然后,在深入事务的执行原理前,描述下各种日志模式的不同点。之后,我将描述数据是如何被写入日志的。接着,介绍日志和文件系统同步以及日志重做事务是怎么回事。最后,介绍日志文件结构。

代码回顾

源码文件

主要功能

一系列的ObjectStore::Transaction事务经由queue_transactions接口提交到FileStore。在这里,基于不同的日志模式,这些事务被按照不同的方式处理,很可能,通过JournalingObjectStore::_op_journal_transactions接口提交到日志,然后调用FileJournal::submit_entry应用日志。

周期性地,通过空间事务或者日志满引起的FileStore::sync\_entry()调用同步磁盘文件系统以及调用FileJournal::committed_thru刷新日志。

函数 调用者 功能
FileStore::queue_transactions OSD 基于日志模式处理事务
FileStore::_do_op OpWQ 调用FileStore::_do_transaction
FileStore::_finish_op OpWQ 调用onreadable, onreadable_sync
FileStore::_do_transaction _do_transactions 应用事务、调用FileStore::_$OPERATION
FileStore::sync_entry() Thread; sync_cond 同步OSD文件系统
JournalingObjectStore:: _op_journal_transactions queue_transactions Op转换成Buffer、提交到日志
FileJournal::submit_entry _op_journal_transactions 添加日志到FileJournal
FileJournal::committed_thru sync_entry() 设置日志项为提交状态(之后丢失它们)

重要的工具类

功能
FileStore::Op 多个事务及回调的包装,与事务中包含的操作不同
JournalingObjectStore::SubmitManager 管理Op序列号
JournalingObjectStore::ApplyManager 将Op应用到日志上
FileStore::OpWQ 工作队列
ObjectStore::Transaction 多个操作的事务封装

日志模式

FileStore可以采用不同的日志模式。各种日志模式的不同之处在于:何时记录日志?何时通知?何时写入到磁盘?常用的日志模式是:writeback(ext4、xfs)以及parallel(btrfs)。其他的日志模式还有:No journal(不鼓励使用),Trailing(文档引述:废弃,不要使用)。

No Journal

Ceph OSD日志分析

没有日志的情况下,IO事务会被立刻调度。在sync调用后,会触发oncommit回调。因此,当FileStore执行sync的时候,会触发大量的回调通知。

Writeahead

Ceph OSD日志分析

先将事务记录到日志,日志提交后,才会将事务提交到应用队列,并触发回调函数通知客户端写入完成。这种模式是为像XFS和ext4之类写就绪文件系统准备的。在一个磁盘文件中存储了日志重做序列号commit_op_seq。该序列号在每次同步后都会递增,重放日志将从这个序列号标识的操作往后进行。

Parallel

Ceph OSD日志分析

日志与事务调度同时执行。这种模式是为Btrfs这类写时复制文件系统而设计。它们提供了稳定的快照回滚机制。执行日志重做时,当前的脏文件系统会被回滚到前一个快照。快照加上日志会将文件系统恢复到一个一致性状态。

Trailing

Ceph OSD日志分析

这种模式已经废弃,它先执行事务再提交日志。

默认模式

下面的代码段摘自os/FileStore.cc:

// select journal mode?
if (journal) {
    if (!m_filestore_journal_writeahead &&
        !m_filestore_journal_parallel &&
        !m_filestore_journal_trailing) {
        if (!backend->can_checkpoint()) {
            m_filestore_journal_writeahead = true;
            dout(0) << "mount: enabling WRITEAHEAD journal mode: checkpoint is not enabled" << dendl;
        } else {
           m_filestore_journal_parallel = true;
           dout(0) << "mount: enabling PARALLEL journal mode: fs, checkpoint is enabled" << dendl;
        }
   } else {
       if (m_filestore_journal_writeahead)
           dout(0) << "mount: WRITEAHEAD journal mode explicitly enabled in conf" << dendl;
       if (m_filestore_journal_parallel)
           dout(0) << "mount: PARALLEL journal mode explicitly enabled in conf" << dendl;
       if (m_filestore_journal_trailing)
           dout(0) << "mount: TRAILING journal mode explicitly enabled in conf" << dendl;
   }
    if (m_filestore_journal_writeahead)
       journal->set_wait_on_full(true);
} else {
    dout(0) << "mount: no journal" << dendl;
}

如果后端文件系统(backend)支持检查点,就采用Parallel模式。否则就是Writeahead模式。Trailing模式和No Journal模式需要在配置文件中直接设置。

事务:应用/日志

事务通过os/FileStore.cc中实现的FileStore::queue_transactions接口进入到FileStore。该方法基于不同的日志模式处理事务。事务包含三种类型的回调指针。在事务被处理后,FileStore及日志会调用它们。在整个代码库中,它们的名称不总是一样。但它们的常用名称及含义如下表所示:

ObjectStore::Transaction FileStore::queue_transactions 描述
oncommit ondisk 日志已提交,可恢复,但还不可读
onapplied onreadable 事务可读,即:已写入磁盘。异步回调
onapplied_sync onreadable_sync 与onreadable含义一样。不过是同步回调

通常oncommit/ondisk在事务提交到日志后被调用,在No Journal模式中是一个例外,它在同步磁盘数据后被调用。

两组onapplied/onreadable回调的区别在于FileStore调用它们的方式。事务处理线程执行事务后立即调用同步onapplied_sync/onreadable_sync回调,并将异步onapplied/onreadable投递到finisher服务线程。

事务在Trailing模式下是另一个例外,它不在线程池中执行,而是首先调用_do_op,然后调用_finish_op,onreadable回调在_finish_op中被调用。

Parallel及Writeahead

if (journal && journal->is_writeable() && !m_filestore_journal_trailing) {
    Op *o = build_op(tls, onreadable, onreadable_sync, osd_op);
    op_queue_reserve_throttle(o, handle);
    journal->throttle();
    uint64_t op_num = submit_manager.op_submit_start();
    o->op = op_num;

    if (m_filestore_do_dump)
        dump_transactions(o->tls, o->op, osr);

    if (m_filestore_journal_parallel) {
        dout(5) << "queue_transactions (parallel) " << o->op << " " << o->tls << dendl;

        _op_journal_transactions(o->tls, o->op, ondisk, osd_op);

        // queue inside submit_manager op submission lock
        queue_op(osr, o);
    } else if (m_filestore_journal_writeahead) {
        dout(5) << "queue_transactions (writeahead) " << o->op << " " << o->tls << dendl;

        osr->queue_journal(o->op);

        _op_journal_transactions(o->tls, o->op,
                           new C_JournaledAhead(this, osr, o, ondisk),
                           osd_op);
    } else {
        assert(0);
    }
    submit_manager.op_submit_finish(op_num);
    return 0;
}

Parallel

先将事务放入日志队列,然后将磁盘IO操作放入另一个队列。

Writeahead

用C_JournaledAhead封装ondisk回调。新的ondisk通过queue_op加入队列,原先的ondisk回调在之后被处理。

No Journal

if (!journal) {
    Op *o = build_op(tls, onreadable, onreadable_sync, osd_op);
    dout(5) << __func__ << " (no journal) " << o << " " << tls << dendl;

    op_queue_reserve_throttle(o, handle);

    uint64_t op_num = submit_manager.op_submit_start();
    o->op = op_num;

    if (m_filestore_do_dump)
        dump_transactions(o->tls, o->op, osr);

    queue_op(osr, o);

    if (ondisk)
        apply_manager.add_waiter(op_num, ondisk);
    submit_manager.op_submit_finish(op_num);
    return 0;
}

No Journal模式与上面两种模式相似,不同之处在于ondisk回调的处理方式。由于没有使用日志,要等到磁盘同步完成后事务才被看成是提交的。apply_manager.add_waiter(op_num, ondisk)就是用来干这个事。磁盘同步完成后ApplyManager会调用队列中的waiters。

Trailing

uint64_t op = submit_manager.op_submit_start();
dout(5) << "queue_transactions (trailing journal) " << op << " " << tls << dendl;

if (m_filestore_do_dump)
    dump_transactions(tls, op, osr);

apply_manager.op_apply_start(op);
int r = do_transactions(tls, op);

if (r >= 0) {
    _op_journal_transactions(tls, op, ondisk, osd_op);
} else {
    delete ondisk;
}

// start on_readable finisher after we queue journal item, as on_readable callback
// is allowed to delete the Transaction
if (onreadable_sync) {
    onreadable_sync->complete(r);
}
op_finisher.queue(onreadable, r);

submit_manager.op_submit_finish(op);
apply_manager.op_apply_finish(op);

return r;

Trailing模式与其他模式有很大的不同,事务没有在线程池中执行,而是在当前线程中执行。事务完成后才提交到日志。最后会触发onreadable回调,在其他模式中该操作由sync_entry完成。

比与其他模式的明显区别更有意思的是,这种模式下事务的执行代码十分的简洁。例如调用submit_manager和apply_manager以及由finisher完成回调。

写日志

通过FileStore::submit_entry方法将日志项添加到日志。日志项首先被添加到FileJournal的writeq队列。除了IO数据外,oncommit回调被添加到另外一个队列并在日志完成时调用。日志的磁盘数据结构请查看后文的磁盘数据结构一节。

日志操作由一个独立的线程完成。线程函数是FileJournal::write_thread_entry,它是一个循环。基于libaio的支持情况,最终的写操作由do_write或者do_aio_write完成。

尽管如此,在日志完成后还需要更新日志文件超级快中的journaled_seq并由finisher线程负责完成oncommit回调。

日志文件以O_DIRECTO_DSYNC选项打开(Linux Man Page:open(2))。

FileStore 同步

同步在FileStore::sync_entry()中实现。它运行在一个独立的线程中并等待在条件变量sync_cond上。同步完成后,磁盘或者快照中的committed_op_seq与日志中的committed_up_to就一致。同步时,首先从ApplyManager获得需要同步的序列号。具体的同步与文件系统相关。如果文件系统支持检查点:(可以回头看看并行模式)

  1. 创建检查点
  2. 同步检查点
  3. 写序列号到快照

否则调用fssync同步整个文件系统,如果文件系统支持sync就调用它同步。之后记录序列号。最后通过ApplyManager通知日志清除已经同步的日志项[译者注:事实上由于日志是循环使用的,类似于循环链表,只需移动头指针即可]。

同步间隔

同步是周期性的,由事务或者日志满事件触发[译者注:事实上是日志半满,参见FileJournal::check_for_full]。同步间隔通过filestore max sync intervalfilestore min sync interval配置。默认值分别是5s及0.01s。

Ceph’s documentation中对同步间隔的描述如下:

周期性地,FileStore需要停止写入并同步文件系统,以创建一致的提交点。之后才能释放提交点之前的日志项。同步得越频繁,同步所需要的时间就越短同时留在日志文件中的数据也就越少。同步得越不频繁,后端的文件系统就能够更好的聚合小IO写及优化元数据更新,可能带来更好的同步效率。

日志刷新

日志刷新与FileStore同步是等效的。通过FileJournal::committed_thru(uint64_t seq)方法通知日志刷新。seq参数需要大于上次的提交序列号。基于日志文件头中的start指针及序列号来丢弃老的日志项。如果支持TRIM,磁盘块数据也会被丢弃。

日志重做

非常直观。日志重做在JournalingObjectStore::journal_replay中实现。

总之:

  • 打开日志文件
  • 读出日志项
  • 解析出事务
  • 将事务传递到do_transactions中执行

Mount调用中发起的日志重做是OSD初始化的一部分。如果文件系统支持检查点,它会将OSD回滚到最后一个一致性检查点。

磁盘数据结构

在这一节,我将描述日志的数据结构。

日志

Ceph OSD日志分析

flags 只定义了一个标志:FLAG_CRC。每个新OSD的默认值。

fsid Ceph FSID

block_size 通常与页大小一致

alignment 通常与block_size一致

max_size 按block_size对齐的日志文件最大大小

start 第一个日志项的起始偏移

committed_up_to 已提交的最大日志序列号(该序列号之前的日志项都已经提交了)

start_seq 第一个日志项的序列号

日志项

Ceph OSD日志分析

seq 日志序列号

crc32c 数据部分的CRC32哈希值

pre_pad 数据前的填充区

post_pad 数据后的填充区

magic1 日志项存放位置

magic2 fsid与seq及len的异或值(fsid XOR seq XOR len

每个日志项都有一个日志头和日志尾,实际上日志尾是日志头的一个拷贝。日志数据按照日志文件头中指定的方式对齐。

日志数据

一系列的事务被传递到日志。在日志处理过程中,首先用encoding.h中定义的编码函数编码这些事务。每种类型的事务都在ObjectStore.h中定义了自己的解码器。需要注意的是日志项中不仅包括元数据,还包含IO数据。也就是说,一个写事务包括了它的IO数据。事务编码后,传递到日志也就被当成一个不透明的数据块。