peering耗时优化方案:跳过wait_up_thru阶段




需求

减少日常运维操作导致的peering的耗时,如停止osd、启动osd、调整osd权重、迁移pool等,从而减少对客户端IO造成的影响。

现状

当前waitupthru阶段耗时是整个peering阶段最长的,该阶段的耗时与monitor的Paxos决议间隔时间强相关(当前配置是间隔1s),也跟monitor服务的繁忙程度有关,之前通过更换monitor所用的存储盘为ssd盘之后,已经大幅降低了waitupthru阶段的耗时,从而也很大程度上降低了peering耗时,对客户端IO的影响也大大降低。

但通过分析多次线上日常运维对打桩卷IO的影响情况,仍然发现有部分osd的peering耗时达到5s甚至8s,其中最耗时的阶段仍然是waitupthru,可达7s左右。另外观察影响打桩卷IO较小的场景,其peering阶段耗时均较低,一般为1s多(绝大部分仍然为waitupthru占据),因此仍然需要进一步优化waitupthru耗时。

方案

本方案的总体流程变动

相关名词解释

peering相关流程请参考:Ceph peering相关问题

WaitUpThru是peering的最后一个阶段,其作用是等待osd通知monitor把他的up_thru字段更新到osdmap中,up_thru字段用来表明该osd何时(哪个epoch)完成了peering,一旦更新完成,就表示该osd上的pg已经可以接受客户的IO请求,后续生成past_intervals时该interval就不能被跳过(可能有IO写入,如果跳过则可能导致数据丢失)。

如果没有这个字段,则无法区分特定场景下的interval是否有IO写入,官方举例如下:

在上述场景下,epoch 3这个阶段,B所处的状态可能有2个,1)B正常运行并且可以处理IO;2)B已经down,只是mon还没发现或者没有更新到osdmap;如果是情况1,那么在peering阶段就不能跳过2这个interval,如果是情况2,则可以安全跳过,osd的up_thru就是用来区分情况2的,即:

如果这种情况下,B在epoch 3这个interval其实是没有完成peering的,因此肯定没有IO写入,可以在后面的peering阶段跳过。

而如果B在epoch 3这个interval的up_thru成功更新成了3,则表示它正常运行并且完成了peering,有IO写入,后续peering不能跳过。

past_intervals在发生变化后(新加入或老的interval被清理),都会把pg的dirty_big_info字段设置为true,然后把更新后的past_intervals存盘(leveldb),在osd启动时会重新加载past_intervals信息。因此我们只需要考虑的是配置项修改后新生成的interval的maybe_went_rw的值是否符合预期即可。

因此如果要跳过WaitUpThru阶段,就必须要做到将每个interval都看作接收过客户端IO请求(写请求),而不能跳过。

方案设计

计划实现一个开关osd_wait_up_thru,来控制OSD在peering过程中是否需要等待up_thru字段更新到osdmap并返回给osd,并且该开关可以随时打开关闭而不影响OSD的运行和数据可靠性、一致性。false表示不等待up_thru字段更新到osdmap,true表示等待。

在peering跳转到WaitUpThru阶段的位置(通过发送NeedUpThru事件给pg状态机实现跳转),加上这个配置项条件的判断,如果为false则不进入waitupthru阶段。

在GetInfo阶段,会首先生成一系列的interval也即past_intervals,然后把这些interval中的osd列表都放入一个set中(prior_set),之后给他们发送pg info查询请求,找出哪个或者哪些osd的pg信息比较全,然后用来在GetLog阶段获取pg log,生成权威日志,供数据恢复使用。

生成interval过程中会根据up_thru字段检查该interval是否曾经接收过客户端写IO,如果没有则可以不考虑这个interval(这个interval的osd不放入pg info查询的os集合),而如果我们之前跳过了WaitUpThru阶段,则可能无法区分该interval是否有写IO,因此只能将其加入pg info查询的osd集合,带来的影响就是多查询了一些osd,并且这个osd可能已经无法启动。但更多的情况下,3副本存储池,一般都有至少2个副本运行,因此每个interval一般都是会有写IO,很少能跳过,并且3副本对应的osd一般都不会发生变化,如pg 1.0从创建后一般都是在固定的3个osd上(如osd.1,2,3),除非我们对其做过运维操作如调整权重或者踢osd。因此并不会导致pg info或pg log需要多发送给很多的无效osd造成耗时增加。

需要考虑的异常场景如下:

  • 开关临界场景:即配置项开关从开到关或者从关到开,不能造成数据丢失或其他问题
  • OSD频繁up、down场景(正常或误报):不能导致数据丢失或其他问题
  • 某个interval单副本运行,但坏盘导致其无法启动,如何把pg恢复正常,尤其是当该interval实际上可能没有接收客户端IO请求的场景,跳过WaitUpThru阶段是否会引入新的问题?

针对配置项开关临界场景的设计如下:

  • 在线修改配置项(从true到false):根据上面的整体流程图可以看出,这一过程实际上是把interval的maybe_went_rw=true的场景变得更加宽泛,也即只会把原本为false的变为true,让pg在peering时给更多的osd发送查询pg info+log请求,在我们场景下都是ok的,唯一需要考虑的是异常场景3的情况下(单副本运行期间坏盘),如果单副本所在的osd故障无法启动,如何让pg完成peering恢复业务?这个问题在下面的异常场景3的设计讨论时进行解释。
  • 在线修改配置项(从false到true):这个场景与从关到开相反,因此interval的maybe_went_rw=false的场景变得更加宽泛,也即把原来为true的场景变成了false,带来的问题是可能这个interval是有IO写入的,但peering过程中却跳过了,就可能导致数据丢失风险。导致这一问题的根本原因是我们跳过了WaitUpThru阶段,也即判断maybe_went_rw=true的条件是不准确的(根据主osd的up_thru或者pg的info.history.last_epoch_clean版本判断,但由于peering转到active之前没有等待新的osdmap到来,所以这两个值有可能是不准确的),因此我们需要在修改配置项之前,检查osd的up_thru值是否更新完毕,并且pg的状态是否为active,只有满足这两个条件才可以进行配置项更改。为了统一配置项修改条件以简化代码逻辑,我们把在线修改配置项从关到开的修改条件也限制为与从开到关同样的条件。补充:修改配置项与peering触发流程不能并发,加锁控制
  • 针对离线配置文件中配置项的修改:可参考下面的非功能性设计相关内容

针对频繁的OSD up、down场景设计如下:

  • 首先,在配置项为false场景,由于基本上每个interval都被我们认为是有IO写入的,因此会导致某些没有IO写入的interval的osd也需要被查询(pg的info和log),因此某些单副本运行的interval虽然没有IO写入,也需要被查询,导致无法跳过,pg状态可能变成down+peering,但此时只要把该osd启动起来即可恢复,如果无法启动,则需要进行手工的恢复,恢复流程见:pg down+peering状态处理方案,由于实际上这个interval并没有IO写入,因此手工恢复也不会导致数据丢失。如果单副本运行的interval有IO写入,那这种场景跟官方场景是一样的,都可能导致数据丢失,这种场景下的数据丢失并不是本次改动引入的。

针对单副本运行过程中坏盘场景设计如下:

  • 如果只是一个副本坏盘,其他一个或两个副本正常运行(min_size=1),那么这个场景是可以正常完成peering的。如果是单副本运行过程中坏盘,这个场景又分为单副本运行的interval有无IO写入,这个问题与上面的osd频繁up、down中的类似,可以参考上面的说明。

非功能性设计

升级

升级过程比较简单,只要把代码打包,然后安装、启动即可(代码中osd_wait_up_thru配置项默认为false,也即让interval的maybe_went_rw=true的条件变宽泛,让更少的interval被跳过,以保证数据可靠性)。ceph.conf配置文件中的osd_wait_up_thru,也配置为false即可。

配置项修改

配置项修改分为在线和离线两种,在线修改已经在代码中进行相应的设计和处理,只有在条件满足时才能修改成功。

离线的配置项修改,需要先完成在线修改,然后再修改离线的ceph.conf配置文件,这么做的原因是,一旦离线的ceph.conf修改完毕,尤其是从false改为true的场景,此时如果在线配置项没有修改而osd异常down掉并重启(当然我们当前的运维场景下不会发生),那么有些interval可能被错误的标记为没有IO写入而跳过,导致数据丢失。如果我们先修改了进程内存中的配置,并且判断已经成功,那么之后无论是在ceph.conf是否修改时发生osd重启,均不会导致interval错误的标记为没有IO写入。

补充:生成prior_set过程中会首先把当前的acting和up的osd列表加入进去,在我们的场景下,这两个列表里的osd已经有所有需要的pg info和log,因此即使错误的跳过一些interval(不给这个interval里的osd列表发生查询pg info和log请求),也不会导致pg信息获取不足(导致数据丢失)。

if (lastmap->get_up_thru(i.primary) >= i.first && // 不等up_thru,意味着本地的osdmap可能是旧的,所以这个判断条件可能是不正确的
    lastmap->get_up_from(i.primary) <= i.first) { // interval可能与自己无关,这里主要关心主是否可写
        i.maybe_went_rw = true;
} else if (last_epoch_clean >= i.first && // 不等up_thru,意味着本地的osdmap可能是旧的,所以这个判断条件可能是不正确的
           last_epoch_clean <= i.last) {  // 因为last_epoch_clean也是在mark_clean的时候用本地最新的osdmap的epoch设置的
        // If the last_epoch_clean is included in this interval, then
        // the pg must have been rw (for recovery to have completed).
        // This is important because we won't know the _real_
        // first_epoch because we stop at last_epoch_clean, and we
        // don't want the oldest interval to randomly have
        // maybe_went_rw false depending on the relative up_thru vs
        // last_epoch_clean timing.
        i.maybe_went_rw = true;
} else {
        i.maybe_went_rw = false;
}

回退

  • 通过配置项回退,把osd_wait_up_thru配置项的值从false改为true即可(包括在线和离线)
  • 通过重装到老版本回退,在active状态下修改osd_wait_up_thru配置项为true,成功后停掉osd,重装版本,之后启动即可,按副本域顺序对osd操作。

风险

  • 单副本运行interval坏盘场景下,如果该interval并没有IO写入,但在osd_wait_up_thru=false的情况下,这个interval无法被跳过,可能导致pg错误的变成down+peering,需要手工修复。