Ceph社区跟踪(2020-10-01 ~ 2020-10-15)

本文作者: 吴 宏 松 https://zhuanlan.zhihu.com/c_1267088333848641536

youtube channel

https://www.youtube.com/c/Cephstorage/videos

  • Ceph Tech Talk: Karan Singh – Scale Testing Ceph with 10Billion+ Objects
    介绍了10Billion+级对象下对象存储的测试

Ceph Performance Meeting 2020-10-01

  • 讨论新pr:
    • https://github.com/ceph/ceph/pull/37274 (ceph-volume: retrieve device data concurrently)
    • https://github.com/ceph/ceph/pull/37496 (osd: optimize PG removal (part1&2 of 2))
    • 由于rocksdb的wal以及compaction的影响,所以考虑用myrock替代rocksdb(目前对myrock的理解应该是有限的,准备接下来好好研究下myrock)

Ceph Docubetter Meeting 2020-10-14

  • 新pr:
    https://github.com/ceph/ceph/pull/37451/(make cephadm faster and more scalable )
  • 改变了guthub的习惯,以后提doc相关的pr signed off by可以不再需要了
  • ceph文档的链接出问题了,不过他已经修复好了
  • 提到cephfs文档的问题,想parick donney检查并更新一下

Ceph Orchestrator Meeting 2020-10-05

  • 当前rook module(https://docs.ceph.com/en/latest/mgr/rook/)默认还没有enable,原因是还有一些bug没有解决。
  • redhat的人说他们那边只有3个人工作在编排这块,所以没有足够的人力同时进行cephadm以及rook的工作,他们会把工作重心几乎都放在cephadm
  • 目前在进行nfs ganesha在rook上工作的测试
  • 一些编排的指令不能工作(比如不能list host和进程),已经修复了这些bug

邮件列表

https://lists.ceph.io/hyperkitty/list/ceph-users@ceph.io/

  • https://lists.ceph.io/hyperkitty/list/ceph-users@ceph.io/ ceph大规模(10Billion+ Objects)下的测试情况
  • https://lists.ceph.io/hyperkitty/list/ceph-users@ceph.io/thread/7IMIWCKIHXNULEBHVUIXQQGYUDJAO2SF/ osd_pglog导致的osd内存增大,所以考虑要不要增加一个配置项osd_pg_log_memory_limit,使得当pg_log对应的内存空间较大时,可以主动减少
  • https://www.mail-archive.com/ceph-users@ceph.io/msg06745.html 发现集群有slow request,进一步测试发现当把读和写分开放到两个pool则不会出现这种问题(我觉得可能是对象读写锁的问题)
  • https://www.spinics.net/lists/ceph-users/msg62766.html 有人询问cephfs多文件系统是否成熟, Patrick Donnelly说期望在P版本落实这个事情
  • https://lists.ceph.io/hyperkitty/list/ceph-users@ceph.io/thread/PRSXG7THZXRO3UE45S6NC6Y5PU2JMLZF/ go-ceph v0.6.0发布

社区博客

https://ceph.io/community/blog/

  • https://ceph.io/releases/v14-2-13-nautilus-released/ v14.2.13 Nautilus发布了

master近期合入代码

  • bug修复相关:
    • https://github.com/ceph/ceph/pull/37594 (librbd: fix race on watcher unregister)
    • https://github.com/ceph/ceph/pull/37581 (librbd: avoid failing IO with -ESHUTDOWN when disabling exclusive-lock)
    • https://github.com/ceph/ceph/pull/37580(os/bluestore: fix memory accounting in TwoQBufferCacheShard)
    • https://tracker.ceph.com/issues/47751 的修复代码,Normal级别bug,影响M/N/O (os/bluestore: fix segfault on out-of-bound offset provided)
    • https://tracker.ceph.com/issues/47734 的修复bug,Urgent级别,影响N/O (client: hang after statfs)
    • https://tracker.ceph.com/issues/47605 的修复bug,nornam级别,影响N/O (mds: purge_queue’s _calculate_ops is inaccurate)
    • https://tracker.ceph.com/issues/46024 的修复bug,影响N,master(larger osd_scrub_max_preemptions values cause Floating point exception)
    • https://tracker.ceph.com/issues/47608的修复bug,normal级别,影响N/O (mds: OpenFileTable::prefetch_inodes during rejoin can cause out-of-memory)
  • 其他:
    • https://github.com/ceph/ceph/pull/37504 (octopus: erasure-code: enable isa-l EC for aarch64 platform)

Curve vs Ceph

本文属于入门科普类型,适合对分布式存储系统(ceph、curve等)感兴趣的运维、测试、开发人员,或者对底层存储系统感兴趣的云计算平台开发人员,可添加微信号opencurve邀请您加入curve用户群进行沟通交流。

curve新版本介绍

9月下旬我们发布了curve的最新版本v1.1,这个版本重点是性能优化,不论是单client还是10 client场景下,相比v1.0版本,都有至少30%以上的提升,其中提升尤其明显的是4k随机读写场景下的性能(具体可参考releasenote),大部分场景下的提升都达到70%以上,部分场景性能几乎提升一倍,可谓是飞跃性的进步。这一个版本兑现了7月份curve发布时许下的提升30%的承诺,并带来了一点惊喜。

当然我们不会满足于当前的性能,我们可以清晰的看到,在大IO场景下curve还有很大的提升空间,这也是curve团队小伙伴们正在努力优化的,另外小IO场景下我们观察到也有较大的提升空间,期待curve下一个版本也能给大家带来惊喜。

下面的性能对比数据中curve部分如无特殊说明都是基于v1.1版本测试的。

参考:https://github.com/opencurve/curve/blob/master/CHANGELOG-1.1.md

架构

Ceph

架构图参考官网:https://docs.ceph.com/en/latest/architecture/

ceph是基于crush算法的去中心化分布式存储架构,最大的优势是不需要元数据服务器,client每次下发IO请求可以自己通过crush算法(输入osdmap、对象名等)计算存储节点和存储设备(osd)以及数据存储的大概位置(pg),不需要元数据服务器的好处是不会引起性能瓶颈和可用性、可扩展性之类的问题,没有元数据分区、失效等问题,但无中心化的设计也会带来容量不均衡等问题。

但ceph也有一个mon集群,用来管理基本的存储集群拓扑结构(monmap、osdmap、pgmap等)、容量、存储集群各组件状态等信息,并通过Paxos协议保持多mon节点的可用性、可靠性和一致性,在集群稳定运行过程中,正常IO请求是不需要与mon集群交互的,但是在集群有异常,比如节点、磁盘上下线等场景下,还是要mon介入以便确认副本状态等信息,否则client和osd之间无法达成一致,或者说没办法确认应该跟谁通信。对应到存储节点,每个节点上一般都有多个磁盘(osd)来提供物理存储空间,每个osd上面又承载多个pg(管理数据副本一致性的最小单位),每个pg管理多个对象(rbd、fs、rgw等存储类型的数据存储对应到osd上都是已对象的形式来存储的,因此不考虑前面的3种具体形态,只有osd和mon的存储系统又叫rados)。

pg里的对象在磁盘上的管理形式当前主要是直接存储在裸盘上(老版本是文件系统如xfs),为了能找到对象数据的位置(逻辑位置到物理位置映射关系),每个osd还要维护一份自己的元数据信息(当然还可以保存对象的其他信息如omap,以及osd自身的空间使用情况等,还有pg的log信息),通常使用rocksdb(老版本是leveldb)。由于rocksdb本身属于LSM类型的存储引擎,因此需要经常性的进行compaction操作,这一操作过程中就会导致性能波动(磁盘压力或rocksdb本身性能影响)。

在集群中存储节点或存储设备上下线过程中,pg为了保证数据的一致性,需要经由mon来确认各个副本的状态,如果有副本下线,需要补充新的副本,如果副本上线则要同步落后的数据给他,在数据同步之前pg需要经历一个叫peering的阶段(osdmap可能会变化),用来找到权威的副本并且告知mon当前pg可以对外提供IO读写服务了,peering阶段完成前不能对外服务,这一阶段如果花费的时间稍长就会导致client端IO时延变长(俗称IO抖动,表现一般为util升高)。

节点下线的时候如果没有及时通知mon进行peering(osdmap还是旧的),client端和主osd就会认为下线的osd还处于在线状态,继续等待其返回或直到超时,新版本优化了下线流程不是死等超时,可以主动发现osd网络故障进而推断出其已下线,加速进入peering阶段,减少IO抖动时长。

ceph另一个问题是由于其要求3副本写入强一致,必须等待所有副本写入(至少是wal写入完毕)才能返回给client成功消息,所以一旦有一个副本(含主)磁盘响应慢或者存储机异常,就会导致IO请求时延飙高导致IO抖动,但是这个机制带来的好处是,ceph可以支持任意副本数的存储池(比如常用的1~3)。另外在副本增量恢复过程中(recovery),有读(主副本)写(任意副本)请求的时候也要等恢复完毕之后才能提供服务,也是会在一定程度上影响IO时延。

在集群规模达到几百台的场景下,ceph的抖动问题非常突出,因此针对H版本我们定制开发及优化了很多功能,比如数据均衡工具、backport下线主动发现机制、优化peering耗时,恢复速度控制、实现异步恢复等,另外我们也配置了数量众多的监控告警,以便及时发现问题节点、osd进行故障处理。

参考资料:
1. https://docs.ceph.com/en/latest/
2. http://aspirer.wang/?cat=1

Curve

架构图参考官网:https://opencurve.github.io/

curve是网易完全自主研发的新一代分布式存储系统,其架构中包含一个中心化的元数据管理服务MDS(通过ectd选出主从实现高可用),配合etcd存储引擎来保存集群的元数据信息,包括系统的拓扑信息(类似ceph的osdmap);文件系统的Namespace ( 树形目录结构,文件和目录,目录元信息等 ),类似ceph的crush ;Copyset ( Raft 复制组) 位置信息,copyset类似ceph的pg。MDS在一定程度上与ceph的mon集群相似。MDS还感知集群状态并进行合理调度,包括感知Chunkserver上下线、收集Chunkserver负载信息、集群负载均衡与故障修复,中心化架构的优势在于数据均衡比较容易实现,劣势在于容易带来扩展性的瓶颈,curve通过避免核心IO路径上与MDS交互的方案来规避这一问题(只有管控操作如创建删除卷等需要MDS),我们模拟过上百近千台存储机场景下,MDS都不是瓶颈,更大规模的场景还需要进一步验证。

curve的chunkserver类似ceph的osd,都是通过管理物理磁盘并存储用户数据的,当前1.0版本的chunkserver更接近于FileStore实现,通过ext4文件系统来管理chunk元数据,不需要依赖rocksdb等嵌入式数据库(因此每个chunk扩展属性还不支持,但针对块存储场景目前还没有此类需求)。curve数据存储的最小单元是 chunk(类似ceph的对象),管理chunk的基本单位是Copyset(类似ceph的pg),每个copyset搭配一个raft复制组来保证数据多副本的一致性和容灾能力。raft是多数一致性协议,好处是不需要等所有副本写入(至少是wal),大多数写入即可返回client端,极大的减少了长尾效应带来的影响,同时也避免了单个节点或磁盘异常(慢盘坏盘、节点超载等)场景下的IO抖动问题,不足是不能支持单副本场景(3副本中有2副本损坏的紧急场景),也即3副本至多容忍损坏一个,否则就不能提供服务(ceph配置min_size为1可以支持但不太建议,单副本长期运行一旦坏盘会导致数据丢失)。curve可以在节点、chunkserver上下线时主动进行数据的再均衡,并且支持恢复带宽控制(这一点ceph也做的不太好,尤其是L及之前的版本,都是通过控制并发及强制sleep来控制,很难精确控制带宽)。chunkserver的wal写入和一致性保证都是由braft来管理的,因此其业务逻辑非常简单,代码量也较少,开发者很容易上手(当然要深入理解brpc、braft也会有一些挑战)。

curve client端实现了一些卷读写接口如AioWrite、AioRead、Open、Close、StatFile、Extend等,用来给qemu虚拟机、curve-nbd等应用提供卷的IO操作接口,client还要跟MDS交互实现对元数据的增删改查(如卷的增删改查等,接口不列出来了)。client还会对io进行切分,能对inflight io数量进行控制。Client还支持热升级,可以在上层应用(qemu、curve-nbd等)无感知的情况下进行client sdk版本变更。

curve的快照系统与ceph相比也不太一样,ceph快照是保存在ceph自身存储系统里面(rados层)的,支持以对象或者存储池为粒度进行快照,curve则是要导出到S3接口兼容的存储系统。两种架构各有优劣,保存本系统好处是不需要导出导入,节省时间,但会带来性能开销,可靠性也会有一定影响(如果丢数据则快照也不能幸免),保存到外部系统,则可以做更多的事情,比如快照压缩等,性能也更优(多级快照场景更加明显,不需要逐层查找对象进行IO读写)。

curve常用链接汇总(含设计文档、分享PPT、FAQ、Roadmap等):https://github.com/opencurve/curve/wiki

参考:
1. https://opencurve.github.io/
2. https://github.com/opencurve/curve/blob/master/README.md
3. https://docs.ceph.com/en/latest/architecture/

功能

从功能方面来对比Curve和Ceph肯定是不公平的,毕竟ceph已经发展了10年了,curve才刚刚开源出来,不过可以简单看下二者在块存储方面的功能对比,让大家对curve有个更直观的了解。

常用功能方面,curve和ceph都支持卷创建、删除,支持挂载到qemu、物理机(nbd)使用,支持扩容,支持快照及恢复。

ceph提供了一些高级功能(L版本),比如rbd-mirror、layering support、striping、exclusive lock、object map等,大部分一般用户是用不到的,或者是针对快照等场景进行的优化。更高版本的ceph提供了QoS功能。

curve提供的高级功能包括client QoS(通过限制inflight io实现)、client sdk热升级等,都是非常实用的功能。ceph的client端qos老版本还不太支持(比如L版本),当前我们H版本都是在应用层做的(比如qemu、nbd设备的cgroup等)。

ceph支持EC pool,但块存储场景下一般不太会使用到。

ceph的控制台功能比较简单不能满足产品化需求,当然curve还没有开发控制台,但是监控告警由于是基于bvar来做的,因此配合grafana和prometheus可以方便的实现可视化监控,并且展示的信息量非常丰富。ceph的监控更多的是通过ceph-mgr配合prometheus来做但可展示的监控项不是很丰富,或者基于perf counter功能自己封装,总体来说不够便利。

其他有些功能方面的差异已经在架构部分做了说明,这里不再复述。

参考:http://aspirer.wang/?p=1456

性能

性能方面,小IO场景下curve具有比较大的优势,尤其是最近发布的v1.1版本,性能更是提升了一大截。大IO情况下,curve还有较大的提升空间,尤其是时延方面还比较高,这也是curve团队当前的重点工作方向。我相信有了小IO场景优化经验,大IO场景优化起来也会比较得心应手。

项目 配置
存储机 Dell R730xd * 6台
CPU Intel(R) Xeon(R) CPU E5-2680 v4 @ 2.40GHz * 2
MEM 256G
NIC Intel Corporation 82599EB 10-Gigabit SFI/SFP+ * 2
OS Debian GNU/Linux 9
Kernel 4.9.65-netease #1 SMP Thu Aug 2 03:02:48
curve版本 v1.1-0beta
ceph版本 Luminous 12.2.13
client host Dell R730xd * 2台
fio版本 3.13

我们内部测试对比数据如下(由于集群软硬件配置和ceph调优手段不尽相同,数据仅供参考):

单卷:

项目 Ceph Curve
4k randwrite (128 iodepth) iops: 39700, latency-avg: 3.2ms, latency-99: 4ms iops:109000, latency-avg: 1.1ms, latency-99: 2ms
4k randread (128 iodepth) iops: 41700, latency-avg: 3.1ms, latency-99: 3.4ms iops:128000, latency-avg: 1.0ms, latency-99: 1.5ms
512k write (128 iodepth) bps: 2076MB/s(单节点带宽已打满), latency-avg: 31ms, latency-99: 116ms bps: 204MB/s, latency-avg: 314ms, latency-99: 393ms
512k read (128 iodepth) bps: 2056MB/(单节点带宽已打满)s, latency-avg: 31ms, latency-99: 144ms bps: 995MB/s, latency-avg: 64ms, latency-99: 92ms

10卷:

项目 Ceph Curve
4k randwrite (128 iodepth) iops: 185000, latency-avg: 7ms, latency-99: 24ms iops:262000, latency-avg: 4.9ms, latency-99: 32ms
4k randread (128 iodepth) iops: 330000, latency-avg: 3.7ms, latency-99: 6ms iops:497000, latency-avg: 2.7ms, latency-99: 6ms
512k write (128 iodepth) bps: 3172MB/s(单节点带宽已打满), latency-avg: 44ms, latency-99: 182ms bps: 1122MB/s, latency-avg: 569ms, latency-99: 1100ms
512k read (128 iodepth) bps: 4440MB/(单节点带宽已打满)s, latency-avg: 28ms, latency-99: 129ms bps: 3241MB/s, latency-avg: 200ms, latency-99: 361ms

注:Ceph版本为L,N版本经测试与L相差不大,相关配置项为默认值。

参考:https://github.com/opencurve/curve/blob/master/CHANGELOG-1.1.md

运维

运维这方面,主要讨论部署和日常维护两块:

项目 ceph curve
部署 ceph-deploy,新版本是ceph-adm ansible
维护 命令集(ceph/rbd/rados/…) curve_ops_tool

两个项目把集群部署起来都还算简单,ceph-deploy有个问题,部署比较慢,要串行操作,为此我们还优化了一下ceph-deploy工具。ceph部署完服务才算第一步,后面还要创建crush rule(这个如果没有经过深入研究,还是有比较高的门槛的),配置pool的各项参数等操作。

curve应该只需要修改几个比较简单的配置文件(playbook的yaml),就可以一键部署搞定了,相对比较简单。

维护的话,ceph一大问题是经常遇到慢盘、坏盘,影响client的IO,用户经常抱怨(会收到一堆util超过90%告警),长时间抖动要帮用户手工解决(停掉慢盘osd),或者短时间的抖动要帮用户确认是否ceph导致的util飙高。为此我们也做了大量的工作来搞定这一场景(如增加各种监控,进行巡检等及时发现问题,甚至发现慢盘自动停止osd等等),但总归是治标不治本,只能尽量规避。

除了抖动,还有容量均衡问题(没有中心化的调度节点,只能根据使用情况进行osd的pg数量调整),集群缩容问题等比较难处理,老版本的pg数量不支持减少,是个比较大的问题(L版本也只能增加pg,更新的版本才支持减少pg),以及缩容后,即使只剩下少量的卷,也很难迁移到其他存储池(qemu用到了存储池,可以迁移到其他root rule,但是pg数量无法减少也是个问题),导致存储池使用的存储节点很难下线。

扩容场景下,除非新建root rule和存储池,否则ceph需要对已有数据进行迁移均衡到新增加的节点上,这方面curve也是类似的(物理池逻辑与ceph的root rule类似),curve的好处是可以做到精确的带宽控制,减少对正常业务的影响。

日常的坏盘替换场景下,ceph可以做到只迁移一次数据(L版本,H版本还不太行),也即坏盘下线的时候不做数据迁出,新盘上线时进行数据恢复即可(当然也可以做到自动迁出和迁入)。curve的话当前是下线自动迁出,上线自动迁入。二者区别不大,curve好处依然是精确带宽控制。

压力检测是一个比较困难的场景(如何找出压力比较大的client,并及时对其进行限制防止雪崩效应?),也是一个强需求(虽然client有qos,但集群一般都有性能超售,部分client压力飙升可能影响整个集群),ceph场景下,由于只能看到集群压力情况,很难做到所有client的汇集(当前是qemu端做的监控)和分析(当然可能跟我们使用的自研监控服务不支持这种场景有一定关系),导致很难找出那个或者那些影响了整个集群的client。一般我们都是用土方法,查看压力大的osd,打开message日志,找出消息比较多client ip,然后再根据client的监控情况判断是否跑满,同时找出同样业务的其他虚拟机,然后逐个进行限速处理。这一过程肯定比较耗时耗力,还不一定准确(可能新版本已经支持了单个rbd卷的性能统计)。curve场景下就比较简单了,看下grafana的client监控,找到压力大的client对其进行限速即可。

监控告警也是一大运维重点,ceph的话可以基于perf counter机制来做一部分,做不了的可以自己定制化扩展。也可以基于ceph-mgr+prometheus+grafana来做,一般还要配合存储节点的NODE_EXPORTER来做会比较全面。我们内部没有用这套机制,是基于自研的监控系统来做的,主要是用到了perf counter+监控脚本模式。

相比ceph的模式,curve基于bvar+prometheus+grafana,监控指标拉取更及时,都是秒级的,可以实时显示性能、时延曲线。bvar另外一个好处是它对外提供的是http api,不是perf counter那种命令行,也就是说不需要在本地部署agent或者脚本即可拉取数据,管理起来比较方便。

参考:
1. https://github.com/opencurve/curve/blob/master/docs/cn/monitor.md
2. https://github.com/opencurve/curve/blob/master/docs/cn/deploy.md#%E5%A4%9A%E6%9C%BA%E9%83%A8%E7%BD%B2
3. https://docs.ceph.com/en/latest/mgr/prometheus/

学习曲线

纯使用

如果是运维人员想使用ceph或curve,或者对比2者进行选型,那我这篇文章基本上可以做为参考了。

ceph运维上手还是比较复杂的,要对各种概念、配置参数(L版本单单osd配置项我粗略看了下就有400多个,大部分配置项都要通过阅读源码来真正理解其意义和用途)、各种监控指标有比较清晰的理解,还要对各种pg状态有深入理解,各种异常要能找到对应的解决方案,当然网上各种调优文档也有很多可以拿来参考,如果没有深入的使用经验和测试比较,这些调优文档也有可能与你的使用场景不符,甚至可能带来副作用。

curve的代码注释(含配置项)非常完善,配置项也比较少(粗略看了下client、chunkserver、mds等全部加起来跟osd的配置项数量差不多),每个配置都有详细的解释,即使不明白也可以随时咨询我们的开发人员。集群状态也比较容易理解,没有那么多的incomplete、peering、degraded、undersize、recoverying、remapped、stale…

运维相关的部署维护监控告警方面,也是curve更容易上手一些,上面已有对比说明。

开发者

上面的架构对比已经说明了问题,curve架构非常简洁易懂(raft也比Paxos简单多了),不需要花费太多时间就能理解代码逻辑,上手进行开发测试。

更重要的是,不论对纯使用的运维人员还是开发者来说,curve的资料原文都是中文版本的,对国内用户来说都特别友好(另外还有微信群可以即时沟通)。

可扩展性/可塑性

功能

ceph的功能已经足够多了,号称统一存储平台。因此功能这块扩展性或者可塑性已经不大。

curve的话还非常年轻,它从架构设计之初就考虑到后续支持扩展各种存储场景的(块、对象、EC、其他引擎等等),当前的发展方向也还在逐步摸索,我们会根据公司内部和存储业界最新动向来及时调整规划,紧跟IT架构演进趋势。我们也欢迎各路同仁加入讨论和开发,一起发展curve。

我们近期的roadmap主要集中在如下几个方向:
1. 云原生数据库支持:为MySQL提供分布式存储后端
2. 云原生容器平台支持:容器云、中间件的存算分离(curve-nbd替换本地data盘、支持类似cephfs的文件存储)
3. 作为ceph的前端缓存集群:类似ceph的cache tier
4. 发展开源社区:吸引个人、企业开发者,加入TOP开源基金会

各个方向都能拆分出非常多的功能、性能、稳定性等方面的需求。中长期的规划可能会发生比较大的变化,这里就不列出来了。

性能

性能一直是curve重点关注的方向,除了上面已经给出的数据,后续curve还会花费大量资源在性能优化上,近两年的目标是支持云原生数据库和给k8s集群提供存储服务(前期可能考虑用nbd盘替换k8s节点本地盘,后期可能考虑开发类似cephfs的文件系统功能),虽然v1.1版本相比v1.0有了很大提升,当前离目标还是有一定差距的。io_uring、QUIC、spdk、rdma、25G网卡等新技术都在考虑当中,nvme/傲腾、40G网络等新硬件也在调研。curve近几年没有商业化或者盈利的打算,因此不会把各种优化手段和方案保留自用,都会毫无保留的开源出来。

ceph的性能也一直在改进,但由于其架构和功能比较复杂,牵一发而动全身,所以只能用全新的版本和架构来做(基于spdk/seastor等的SEASTORE),因为要保持功能的兼容性,当前看起来还需要较长时间才能正式发布。大部分sds厂商都是基于现有的架构进行优化改进,具体优化到什么程度,要看各家的实力和人力投入如何,但是一般来说他们的优化都是作为竞争力保留的,不太会开放给外部。

参考:https://docs.ceph.com/en/latest/dev/seastore/

社区

ceph的社区活跃度肯定是很高的,curve刚刚开源,很多人还不了解。

ceph是一个成熟的国际化的开源社区,新人想参与进去提交pr还是有较高的难度的(一般是从编写测试用例、简单的bug修复入手),功能性的pr一般都是比较核心的开发人员才容易被接受(这一点跟Linux内核社区很相似)。

而curve刚刚起步,目前重点还是吸引国内开发者,一是便于沟通交流(可以微信添加opencurve拉你进交流群),二是鉴于当前国际形式也更倾向于为国内用户服务。刚刚起步的好处是,我们对所有的pr都非常欢迎,并且要求会相对比较低(可能你提交的pr不太符合规范,我们可以帮忙修改完善,可能你的pr没有考虑全面,我们可以给你提供修改建议和设计思路),有任何想法都可以在群里或者issue上提出来,目的是鼓励大家积极参与进来。企业用户如果有交流的想法,也可以与我们联系面谈,如果roadmap或需求有匹配的地方可以合作开发,提高研发效率、节约人力成本。

后续curve也会考虑加入某个开源组织(基金会),但目前主要还是关注自身的功能和性能问题,待条件成熟会有进一步的动作。

curve的Roadmap可参考:https://github.com/opencurve/curve/wiki/Roadmap

应用规模

当前来说二者也没有可比性,毕竟ceph已经发展了10年,而curve才刚刚开源3个多月(内部开发2年多)。单就网易内部使用情况来说,ceph上线将近5年了,节点最多时数千台(随着业务从OpenStack转向k8s,集群规模有所减少),curve上线1年多,卷数量数千,并且一直稳定运行无任何SLA损失,这一点据我了解ceph当初上线时也没有做到的,curve的开发质量可见一斑。业务方经过这一年多时间测试观察,已经逐步建立起了对curve的信心,这一点从近期curve卷的使用量大幅增加可以明显感觉出来(我们给业务方提供了ceph、curve两种卷类型,他们可以自行选择创建哪种类型的卷),近2个月以来curve卷数量已翻了一番,我们预估明年集团内部使用量会有爆发式增长。

Ceph社区跟踪(2020-09-19 ~ 2020-09-30)

本文作者: 胡 遥 ( https://my.oschina.net/u/2257799 )

youtube channel

  • Ceph Crimson/SeaStor OSD 09-23
    • 修复测试用例发现的bug,bug的场景是:一个request因为拿不到object context而被pengding,再次重新执行的时候可能乱序。
    • 完善cstore垃圾回收方面的基础开发,目前已经可以在一个随机的工作负载下一直
    • 集成了hobject的读写流程,以及对应的单元测试
  • Ceph Science Working Group 09-23
    • 讨论了ceph的一个线上问题,某一次大停电,3个数据中心,其中2个中心完全掉电。最后没有丢数据,并且通过均衡恢复了。
    • 报告了一个osd pg迁移走之后,使用率还是很高的问题。没找到问题的原因,最后通过重建解决
    • 计划将第二个s3的环境从L版本升级到N版本,并且都进行了测试,之前已经升级成功一个。比较担心region的设置从前一个版本的设置无法在下一个版本中生效
    • 讨论了s3 单bucket文件数量过大的问题,目前已经把aoto reshard功能给关闭了。问题原因是做list file操作的时候,如果shard达到512个,而每次如果list 1000条记录,则需要512个shard同时返回1000条记录,则有几十万条记录进行排序,耗时非常严重,对此进行了优化。
  • Ceph Performance Meeting 09-24
    • 讨论了2个新的pr
      • https://github.com/ceph/ceph/pull/37156
      • https://github.com/ceph/ceph/pull/36266
      • https://github.com/ceph/ceph/pull/37314
    • 分析rocksdb社区做的rocksdb基准测试,用不同工作负载的模式进行测试。有人提出rbd下的rockdb工作负载和rgw下的rocksdb工作负载差别很大,要把工作负载模型都测试到,工作量很大
    • 分析rocksdb的文档,代码以及案例。目前只熟悉了文档,还没进行代码的深入分析,并且觉得这很耗时,如果有人可以提供rocksdb代码速成的方法,会很高兴
    • 讨论了bluestore是否有更好的并行处理能力,kv-sync线程一直是瓶颈,导致当初的bluestore非常慢,现在已经大大改善了,如果想要进一步改善,需要比单线程使用更多的并行化
    • 提到删除调用pglog的相关的代码,来测试pglog的开销
  • Ceph Code Walkthrough: Patrick Donnelly – Metadata Servers 2020-09-29
    • 这个主要是代码讲解,自己看就好了
  • 邮件列表
    • https://lists.ceph.io/hyperkitty/list/dev@ceph.io/thread/M67UHRHBOBO64FGG5U3OTLH2MOR43PEX/ (报告一些代码backport到了N版本导致FreeBSD环境编译不通过)
    • https://lists.ceph.io/hyperkitty/list/dev@ceph.io/thread/JBMAEEMXZWTHF5XA4MBFJDWLDCOFH5DH/ (ceph社区实习报名)
    • https://lists.ceph.io/hyperkitty/list/dev@ceph.io/thread/DIQTY4AYHRE3RSOCA7WHEG2TE2M6C7WW/ (有人说他开发了一个性能更好的块存储,并且可以对接qemu,https://vitastor.io)
    • https://lists.ceph.io/hyperkitty/list/dev@ceph.io/thread/UJTZE3JRQ56M2CM7C3TU25Y6P2VA2OY5/ (ceph-mgr升级到15.2.4,总是需要很长时间才能得到ceph pg结果)
    • https://lists.ceph.io/hyperkitty/list/dev@ceph.io/thread/DXM3Z2BE3PBDPIZHE7XJKQOVMTJ322XM/ (ceph-fuse 多client之间存在不一致的问题)
  • master近期合入代码(0919~0930)
    • bug修复相关:
      • https://github.com/ceph/ceph/pull/37331 修复 https://tracker.ceph.com/issues/47461
      • https://github.com/ceph/ceph/pull/37334 修复mds在session连接关闭后不恢复文件caps的问题
      • https://github.com/ceph/ceph/pull/37382 降低了mds的open file table的内存使用

Ceph社区跟踪(2020-08-27 ~ 2020-09-09)

本文作者:本人

youtube channel

  • Ceph Crimson/SeaStor OSD 08-26
    • 在osd层屏蔽不支持的特性,补充测试用例
    • 每个主要开发人员开发进度同步:extent/onode tree、omap、dirty extent/segment管理方案pr讨论
    • 解决EC相关bug及messenger层导致的心跳相关bug
    • 讨论美光新开源的存储引擎(https://www.micron.com/about/blog/2020/june/what-is-a-heterogeneous-storage-engine ),tiger、rocksdb等LSM存储引擎的性能问题,吐槽rocksdb非常slow,用它是因为它流行 【这块可以关注下新开源的存储引擎】
    • 讨论使用io_uring实现异步IO
    • 讨论的相关pr:https://github.com/ceph/ceph/pull/36779、https://github.com/ceph/ceph/pull/35865
  • Ceph Performance Meeting 08-27
    • 跟踪rocksdb社区进展,每2周的论文阅读情况(连摘要都没时间看。。。),有些论文作者还不想把最新进展同步给他们(因为论文还没发表。。。)
    • onode、rocksdb等相关的议题讨论:rbd使用更简单的内存数据结构,其他的如cephfs可以复杂点,以及精简其他内存数据结构(如blob、extent等)以减少内存占用?把一个key拆分成多个,以减少提交到rocksdb的数据量?
      • 另外一个人评论:这个改动要先做好POC,改动会比较大,想法是好的,不知道收益如何
      • 另外讨论了这些优化是否会影响crimson/seastor架构
    • 64k的blob size是否合适,是否要调小到16k或者调大到512k?还需要继续调研,不同场景下的性能表现(单client、多client、db场景等),只靠benchmark场景来测试得到合适的blob size是不合理的。
  • Ceph tech talk 08-27
    • 主题:Secure Token Service(STS) in Ceph Rados Gateway
      • 主要参考了AWS STS APIs以及AWS IAM APIs,N版本已经实现了一部分
  • Ceph Orchestrator Meeting 08-31
    • nfs兼容性问题:N版本实现的dashboard的nfs管理功能与O版本不兼容,要想办法解决(也不是完全不兼容,只是两个不能同时用)
    • 想办法支持从N版本迁移用户到O版本
    • rook问题
      • ceph daemon僵尸进程没有处理好
      • ceph daemon进程的core dump文件没办法生成
    • 配置文件模板问题:grafana、告警服务,使用jinja管理模板文件,比较灵活但用户体验较差(相比UI方式)
    • 不需要编译ceph或者部署一个ceph集群就可以体验cephadm,跑测试用例
    • cephadm的安装方式(包管理?集群升级问题?要尽快确定一个cephadm本身的最小命令行集合,以便保持兼容性)
  • Ceph Crimson/SeaStor OSD 09-02
    • 测试用例失败问题讨论及分工;interruption问题修复方案讨论(应该是指osd正常退出过程中的停止pg服务和snapshot终止问题)
    • rgw omap offload代码开发及review
    • seastor onode tree功能联调完毕,准备提PR
    • lambda函数使用问题(主要是讨论其性能问题)
  • Ceph Performance Meeting 09-02
    • 讨论新PR
      • https://github.com/ceph/ceph/pull/36961(mon/AuthMonitor: speed up caps updates)
      • https://github.com/ceph/ceph/pull/36914 (change the default value of option osd_async_recovery_min_cost from 100 to 10,经过测试rgw+EC pool场景下10比较合适)
    • onode等数据结构重构问题
      • 讨论怎么减少内存占用以及提升rocksdb性能
      • 基于column family来分离onode、omap、extent等BlueStore的metadata到不同的block cache?
    • 降低osd的cpu占用方案讨论:目标是占用1~2cores,现实是nvme场景下BlueStore引擎osd占用10~14cores,节省下的cores可以给其他进程使用
      • BlueStore+rocksdb在nvme场景下比FileStore性能好,但也遇到了cpu性能瓶颈问题,要想办法降低cpu开销
      • seastor会在一两年内搞定,会解决这个问题?潜在的意思是说我们还要不要花费时间在BlueStore上做优化?
      • 降低cpu利用率需要先确定硬件类型,nvme、optane。。。
    • rocksdb性能问题:
      • Intel的人提出了一个原型,把pg log写入bluefs,绕过rocksdb,还讨论了一坨优化pg log在rocksdb的性能问题的方案
      • 有人提出ceph的rocksdb里的key string用的太多(前缀、后缀、比较等等)影响性能,考虑用binary方式替换?
      • 参考zfs的元数据管理方式改进ceph这边使用rocksdb的姿势?
      • nvme和Intel ssd对比benchmark性能,没有提升多少,需要继续研究
    • pg lock/log(pg lock还是pg log,没听太清。。。根据上下文应该是log)性能问题,禁用pg lock可以提升20~30%的性能(fio测试BlueStore engine)
  • Ceph Developer Monthly 09-02
    • 讨论teuthology等CI问题(出现一批失败用例,以及job排队问题,需要机器?),有些问题比较难复现,出现了要尽快上去看,比较难解决的问题可以先尝试用bisect找到问题
    • teuthology每天晚上可以跑300个用例,这样才可以快速发现新引入的问题(意思是近期teuthology不太稳定,要尽快修复)
    • debug level可以配置高点,方便定位问题?

邮件列表

  • https://lists.ceph.io/hyperkitty/list/ceph-users@ceph.io/thread/TPIFMPQ6YHEK4GYH5LA6NWGRFXVW44MB/ (13.2.8版本有用户反馈osd可能有内存泄露,还在讨论)
  • https://lists.ceph.io/hyperkitty/list/ceph-users@ceph.io/thread/OLFOSYULOTC4HFVF37YSASSAFYFE372A/ (ceph tell osd.0 bench之后怎么删除测试数据?)
  • https://lists.ceph.io/hyperkitty/list/ceph-users@ceph.io/thread/XJICICKXGMGUSH2KDP4TSVRYR2SELHYH/ (cephfs怎么跨集群同步数据,提到了2个工具,我们有空可以研究下)
    • https://docs.ceph.com/docs/master/dev/cephfs-mirroring/【推荐】
    • https://github.com/oliveiradan/cephfs-sync
  • https://lists.ceph.io/hyperkitty/list/ceph-users@ceph.io/thread/6TW27ZYQ3PRW6QEWGT5EZXADOVRYYT77/ (怎么解决”inconsistent+failed_repair”这个pg状态,cephfs的pool,暂无人回复)
    https://lists.ceph.io/hyperkitty/list/ceph-users@ceph.io/thread/ZOPBOY6XQOYOV6CQMY27XM37OC6DKWZ7/ (14.2.8升级到14.2.10之后db/wal性能下降明显,貌似还没有结论,没仔细看)
  • https://lists.ceph.io/hyperkitty/list/dev@ceph.io/thread/QWZHT4MQBVSHVYXUEFJMBDO2MA65PZLF/ (ceph-deploy还可以继续用吗?答复是ceph-ansible和cephadm不一定能满足所有用户需求,deploy还会继续支持,比如python3支持还会发版本)

社区博客

  • https://ceph.io/community/blog/

没有新增文章

master近期合入代码(08-27~09-09)

大部分都是crimson/seastor相关的,也有少量rpm打包、文档、dashboard相关的。

bug修复相关:
– https://tracker.ceph.com/issues/47302 的修复代码,minor级别bug,影响N/O版本(include/encoding: Fix encode/decode of float types on big-endian systems)
– https://tracker.ceph.com/issues/47290 的修复代码,major级别bug,影响N/O版本(osdmaps aren’t being cleaned up automatically on healthy cluster)
– https://tracker.ceph.com/issues/47293 的修复代码,minor级别,影响master版本(client: osdmap wait not protected by mounted mutex)
– https://tracker.ceph.com/issues/47201 的修复代码,minor级别bug,影响N/O版本(mds: CDir::_omap_commit(int): Assertion `committed_version == 0′ failed.)
– https://tracker.ceph.com/issues/46842 的修复代码,影响N/O版本(librados: add LIBRBD_SUPPORTS_GETADDRS support)

其他:
– https://github.com/ceph/ceph/pull/36955 (os/bluestore: Switch from libzbc library to libzbd library,libzbd是libzbc的升级版。。。)
– https://github.com/ceph/ceph/pull/36850 (mds: add performance counter for cap messages)

Ceph社区跟踪(2020-08-10 ~ 2020-08-23)

本文作者: 徐 桑 迪

Youtube Channel

https://www.youtube.com/c/Cephstorage/videos

  • Ceph Orchestrator Meeting 08-10
    • Rook 1.4发布
    • 更新了删除OSD的设计文档
  • Ceph DocuBetter Meeting 08-12
    • 优先编写Installation guide和development guild
    • 后续考虑录制短视频(~5mins)演示如何安装Ceph
  • Ceph Crimson/SeaStor OSD 08-12
    • 编写新的OSD实现crimson-osd,讨论功能设计和开发进度
    • 关键词:NVMe,SeaStore,Multi-cores,Cache,LBA tree
  • Ceph Performance Meeting 08-13
    • 开发中:根据buffer原大小动态调整append buffer的大小
    • Need QA:(ma jianpeng)RocksDB Env中避免一次性下刷过多的数据
    • Need Review:d3m caching
    • 开发中:(ma jianpeng)优化BlueFS中BufferList重建流程,正在考虑将其应用到通用BufferList中
    • 研究中:有人提出了一个CRUSH算法的优化版本,用来降低集群扩容时的数据迁移影响
    • Paper中仅测试了HDDs,可能是因为在这个硬件上效果更好
    • Sam提出他更关注减少数据迁移,从而可以提升NVMe设备的使用寿命
    • https://www.usenix.org/system/files/fast20-wang_li.pdf
  • Ceph Orchestrator Meeting 08-17
    • 考虑重构Cephadm工具
    • 从原有的Ceph-ansible中拷贝部分代码过来,应该是想后续完全替代掉ceph-ansible
    • 编写使用Cephadm工具部署Ceph集群的指导文档
  • Ceph Crimson/SeaStor OSD 08-19
    • OSD EIO处理
    • 使用crimson关键字跟踪相关BUG
  • Ceph Performance Meeting 08-20
    • 讨论之前提出的CRUSH扩展算法论文;其对新PG使用了新的rule,可能不是很好
      • 要么最终仍然需要迁移数据以达到集群平衡,这样带来了额外的PG合并开销
      • 要么得一直保留PG映射的特殊信息,而且这会随着集群的扩容不断增大
    • 正在积极开发QoS功能,初步验证效果显著
    • PG priming?(可能是高版本特性,暂不了解)
    • 持续关注研究领域的Ceph/CRUSH相关论文,定期一起阅读讨论

邮件列表

IRC

  • https://tracker.ceph.com/projects/ceph/wiki/Code_Walkthroughs
    • 2020-08-25 @ 7am PDT: KRBD I/O Flow – Ilya Dryomov
    • 2020-10-27 @ 7am PDT: Librbd I/O Flow – Jason Dillaman
  • Proposal: Encrypted Namespaces
  • CEPH dmclock per-client QoS control
    • not implemented, still need a lot of work
  • https://lists.ceph.io/hyperkitty/list/dev@ceph.io/thread/IMYRL55PURREJXFLVX3FKHJ4QQX57JA7/
    • 33% possible performance regression between 15.2.4 and HEAD?
    • checking bluestore disk allocator work
  • https://github.com/ceph/go-ceph/releases/tag/v0.5.0
    • go-ceph v0.5.0 released

社区博客

https://ceph.io/community/blog/

  • https://ceph.io/releases/v14-2-11-nautilus-released/
    • abort scrub/deep-scrub by setting certain osd flag
    • implement Hybrid allocator for bluestore
    • do not raise “client failing to respond to cap release” when client working set is reasonable

代码合入

主要通过新版本的Release Notes看合入的commits:https://docs.ceph.com/docs/master/releases/general/#release-timeline

V14.2.11 NAUTILUS
标签 Commits数 值得关注的修复/特性
bluestore 2
build/ops 1
ceph-volume 1
cephfs 27
core:mon 1
core:mgr 2
core:osd 6
mgr modules 10 nautilus: New msgr2 crc and secure modes (msgr2.1)
msgr 1
rbd 3
rgw 15
tools 1

其它

  • https://github.com/ceph/cbt
    • Ceph benchmarking tool(CBT)
  • https://docs.ceph.com/docs/master/dev/crimson/
  • https://docs.ceph.com/docs/master/dev/seastore/

ceph rgw vs minio vs swift

个人观点,能力有限,仅供参考

结论

  1. Ceph显然是云原生标准存储组件,其他两个都有很多功能缺失,比如不支持块和文件存储,这会导致一套云环境要部署多套存储系统
  2. MinIO只支持EC纠删码,不支持副本存储模式,性能和可靠性会有损耗
  3. MinIO扩容不方便,不适合规模多变的生产环境
  4. 从非功能方面比较,Ceph也是最佳选择
  5. 从我们内部使用情况来看,Ceph已经使用多年,有成熟的运维经验,也有很强的二次开发能力,其他两个都没有使用经验,更没有开发经验

功能对比

功能 ceph minio swift 备注
S3接口 Y Y Y 都是常用接口,高级接口一般都不支持
私有接口 Y N Y ceph同时支持swift接口
多语言SDK Y Y Y S3 SDK支持多种语言
bucket name跨账号同名 N N Y  
命令行工具 Y Y Y  
支持的操作系统 超多 minio支持windows、mac,其他两个基本都是Linux发行版
硬件绑定情况 不需要特定硬件
数据加密 Y Y Y  
多副本 Y N Y  
EC Y Y Y  
快照 Y? N N ceph支持pool级别快照
故障域 Y Y Y 副本域或EC分片域
多租户支持 Y Y? Y minio多用户ok,多租户需要单独部署server
LDAP Y Y Y  
bucket ACL Y Y Y  
multisite Y Y Y 跨区灾备
数据压缩 Y Y N swift官方不支持,但可以自己开发插件
NFS导出/NAS Y Y N? swift官方不支持,猜测可以通过NFS-Ganesha配合S3 api实现?
配额 Y N Y minio支持限制租户使用的磁盘数量
块存储 Y N N  
文件存储 Y N N  
dashboard Y Y Y? swift需要使用OpenStack horizon作为dashboard
监控告警 Y Y Y ceph、minio支持prometheus,swift支持statsd,也有第三方的prometheus exporter
分块上传 Y Y Y  
最大对象大小 单次上传5G(可配置),分片上传最多10000个 单次5TB,分片最多10000,每片最大5G 单次上传5G(可配置),分片上传最多1000个(可配置)  
最大元数据大小 512KB 8KB 4KB(可配置)  
静态站点托管 Y Y Y ceph需要通过代理服务实现(比如nginx、haproxy)
bucket lifecycle Y Y Y  
bucket policy Y Y Y  
临时url Y Y Y  
object versioning Y N? Y minio看起来还在开发这个功能
Bitrot Protection Y Y Y  
WORM

(一写多读)

N Y N  
bucket notification N Y N  
object tags Y Y Y  
第三方备份 N Y N  
元数据搜索 Y N Y  

 

非功能对比

  ceph minio swift 备注
稳定性  
数据一致性 强一致 强一致 最终一致 minio数据落盘不是direct?
集群可扩展性 minio不支持存储池级别扩容
数据可靠性 中高  
性能 未实际测试,但估计相差不大
运维便利性 后2者未实际使用,网上查询比较简单
可维护性 后2者没有使用经验
可订制化 极高 后2者没有二次开发经验
云原生适配能力 极高 ceph已经是云原生标配开源存储组件,OpenStack也推荐使用,swift在OpenStack的IaaS平台上只能用来保存镜像,minio几乎没有在云环境的使用案例
企业使用情况 普遍 极少 一般 国内几乎所有SDS存储厂商都是基于Ceph在做,几乎没有基于swift的,minio更没有听说过;另外国内大厂也都有ceph使用场景(中国移动、腾讯、阿里、网易、乐视、携程、今日头条、中国电信、中兴、恒丰银行、平安科技、YY、B站、360等),使用OpenStack的企业很多,相信其中也有部分公司用到swift,minio几乎没听说过
商业版本 Y Y Y 同上
开源社区活跃度 极高 minio几乎就几个人在做
软件业内顶级公司参与度 普遍 极少 一般 同上
文档完善度  
开发运维人员招聘难度 适中 稍高  
开源协议 LGPL-2.1 or LGPL-3 Apache License 2.0 Apache License 2.0  
项目发起时间 2008.01 2015.06 2010.07 以第一个版本发布时间为准
版本发布频率 每年1个大版本,小版本比较频繁 7天 6个月一个大版本,小版本比较频繁  
CI自动化程度及覆盖率 非常完善 未知 非常完善  
项目贡献参与难度(学习曲线) 中? ceph整体代码量及复杂度都很高

 

 

 

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,需要手工修复。

ceph merged commits – 0301~0318

后面尽量每两周抽时间看下ceph社区merge的pr,水平有限,很多也不理解是做什么的,如有错误请见谅。

看了这半个月大部分提交都是在搞cephadm相关的,也有很多mgr相关的。
我这边更多的是关注核心项目(rbd、rados、cephfs、osd、mon)

mon/MgrMonitor: make ‘mgr fail’ work with no arguments

https://github.com/ceph/ceph/pull/33997

这个commit比较简单,就是原来要fail一个mgr,要传入mgr的名称,现在不需要了,免去了查询active mgr的步骤。

librbd: optimize image copy state machine to use fast-diff

https://github.com/ceph/ceph/pull/33867

这个提交是在rbd-mirror同步image时,充分利用fast-diff这个特性,基于object-map来检查两个snapshot的dirty object,只把dirty的object同步过去,对对象数比较多的size比较大的image来说,会大大提升同步速度。

crimson/heartbeat: report to monitors about osd failure

https://github.com/ceph/ceph/pull/33836

这个是基于crimson实现的新功能,增加了osd故障上报流程。

crimson/osd: add tell command support

https://github.com/ceph/ceph/pull/33847

也是增加新功能,支持tell命令。

nautilus: mon: fix/improve mon sync over small keys

https://github.com/ceph/ceph/pull/33765

这个是backport的提交,用来改进mon启动时同步数据的速度,原来只限制payload size,没有限制每次同步的key的数量,如果数量很多并且size很小,就要花费很多时间。这个提交就是增加限制每次同步的key的数量,进而提高速度。

mon/MonClient: send logs to mon on separate schedule than pings

https://github.com/ceph/ceph/pull/33732

支持单独设置提交cluster log到mon的时间间隔,原来是跟ping消息相同。默认还是1s,增加时延应该可以降低mon(Paxos)的负载。

nautilus: common/blkdev: compilation of telemetry and device backports

https://github.com/ceph/ceph/pull/33726

这个是backport的提交,获取设备metadata的时候支持/dev/disk/by-path这种链接的设备路径。具体是什么用处还没太了解,应该是一堆提交中的一个。

CephFS用户IO流程

基于L版本代码(v12.2.12)分析。本人还在CephFs学习入门阶段,分析过程仅供参考,如有错误请谅解!

用户IO发送客户端

当前有3种方式可以与cephfs集群进行数据交互:
– libcephfs:提供与cephfs集群交互的C语言API,需要自己开发客户端,与ceph-fuse使用的下层接口相同
– ceph-fuse:sudo ceph-fuse -m 192.168.0.1:6789 /mnt/mycephfs
– kernel client:sudo mount -t ceph 192.168.0.1:6789:/ /mnt/mycephfs

ceph-fuse客户端启动流程

ceph-fuse依赖libfuse项目,很多都是调用的libfuse提供的接口,所以要对libfuse的API比较熟悉才能更好的理解启动过程。

// sudo ceph-fuse -m 192.168.0.1:6789 /mnt/mycephfs
ceph_fuse.cc:
    -> main()
        -> global_init()  // 生成ceph context
        -> fuse_parse_cmdline()  // 解析命令行参数
        -> forker.prefork(err)  // daemonize,成为守护进程
        -> new MonClient(g_ceph_context); mc->build_initial_monmap();  // 初始化monitor client及monmap
        -> Messenger::create_client_messenger()  // 创建client messenger
        -> client = new StandaloneClient(messenger, mc)  // 创建client,用来收发用户IO请求
        -> cfuse = new CephFuse(); cfuse->init()  // 创建CephFuse对象并初始化
            -> _handle->init()  // 初始化CephFuse::Handle对象,fuse_parse_cmdline是libfuse的api,用来解析libfuse需要的参数
        -> messenger->start()   // 启动messenger线程,开始接收消息
        -> init_async_signal_handler(); register_async_signal_handler(SIGHUP, sighup_handler)  // 注册SIGHUP信号处理函数
        -> client->init()  // 初始化client的定时器,启动objectcacher(对象缓存管理),初始化objecter并启动,objecter是跟osd打交道的client,添加dispatcher到messenger
        -> client->mount()  // ,与mds交互检查目录权限?
            -> authenticate()  // 通过monitor完成认证流程
            -> monclient->sub_want(want, 0, 0); monclient->renew_subs()  // 订阅mdsmap,并请求更新
            -> tick()  // 启动定时任务
            -> if (require_mds)  // 等待mds可用
            -> make_request()  // 发送请求给mds,循环检查mount的目录权限
        -> cfuse->start()
            -> fuse_mount(mountpoint, &args)  // 调用libfuse接口完成目录挂载
            -> fuse_lowlevel_new(&args, &fuse_ll_oper, sizeof(fuse_ll_oper), this)  // 创建lowlevel fuse session,其中fuse_ll_oper是定义好的各种posix接口的用户态实现,这里涉及到libfuse的两种用法,参考:https://www.lijiaocn.com/%E6%8A%80%E5%B7%A7/2019/01/21/linux-fuse-filesystem-in-userspace-usage.html
            -> fuse_set_signal_handlers(se)  // Exit session on HUP, TERM and INT signals and ignore PIPE signal
            -> fuse_session_add_chan(se, ch)  // Assign a channel to a session
            -> client->ll_register_callbacks(&args)  // 给client注册回调,包括inode invalidate callback、remount callback、dentry invalidate callback等
        -> tester.init(cfuse, client);  tester.create("tester");   // 初始化并启动remount的test线程(执行RemountTest::entry函数检查是否支持invalidate dentry,如果内核版本大于3.18并且配置项里设置了client_try_dentry_invalidate=true,则检查是否注册了dentry invalidate callback;反之则需要通过remount操作来强制invalidate dentry,执行的命令是"mount -i -o remount $mountpoint",如果remount失败并且配置项client_die_on_failed_dentry_invalidate=true则执行"fusermount -u -z $mountpoint"命令umount掉,下面的loop()就会失败,以达到退出进程的目的)
        -> cfuse->loop()
            -> fuse_session_loop_mt(se)  // Enter a multi-threaded event loop,开始处理IO请求
        -> tester.join(&tester_rp)  // loop()结束后,检查tester线程返回值

ceph-fuse用户IO流程

按数据类型可以分为两种IO,一种是操作metadata的,一种是操作文件内容的。

元数据IO流程

以mkdir操作为例进行说明。

client端

-> 用户在cephfs挂载目录下执行mkdir命令
    -> ceph-fuse进程调用fuse_lowlevel_new函数注册的用户态mkdir实现:fuse_ll_mkdir(ll应该是lowlevel的缩写,因为调用的是libfuse的lowlevel api)
        -> fuse_ll_req_prepare(req)
            -> fuse_req_userdata(req)  // 提取用户数据
        -> fuse_req_ctx(req)    // Get the context from the request
        -> UserPerm perm(ctx->uid, ctx->gid)  // 初始化用户权限
        -> get_fuse_groups(perm, req)  // 从req中获取用户组信息
            -> getgroups()
                -> fuse_req_getgroups(req)  // Get the current supplementary group IDs for the specified request, Similar to the getgroups(2) system call, except the return value is always the total number of group IDs, even if it is larger than the specified size.
            -> perms.init_gids(gids, count)  // 把用户组信息设置到perms
        -> i1 = cfuse->iget(parent);  // 获取父目录的inode
            -> client->get_root() 或 client->ll_get_inode(vino)   // 增加相关inode引用计数
        -> cfuse->client->ll_mkdir(i1, name, mode, &fe.attr, &i2, perm)
            -> _mkdir(parent, name, mode, perm, &in)
                -> is_quota_files_exceeded()  // 检查文件数量配额
                -> _posix_acl_create(dir, &mode, xattrs_bl, perm)   // 创建acl
                -> get_or_create(dir, name, &de)  // 创建新建目录的dentry
                -> make_request(req, perm, inp)  // 发送请求给mds执行创建目录操作,应该是同步请求
                -> trim_cache()  // 清理lru缓存中的dentry
            -> fill_stat(in, attr);    _ll_get(in.get());  // 填充stat信息,增加inode引用计数
        -> fuse_reply_entry(req, &fe)  // Reply with a directory entry

server端

void Server::dispatch_client_request(MDRequestRef& mdr)
{
  ...
    case CEPH_MDS_OP_MKDIR:
      handle_client_mkdir(mdr);
      break;
  ...
}

// MKDIR
/* This function takes responsibility for the passed mdr*/
-> Server::handle_client_mkdir(MDRequestRef& mdr)
    -> is_last_dot_or_dotdot()  // 检查末级目录是否为.或..
    -> rdlock_path_xlock_dentry()  // 遍历并创建新文件夹的dentry,获取非新建目录的rdlock,以及新目录的xlock
    -> check_access()  // 检查目录操作权限
    -> check_fragment_space()  // 检查mds上的目录分片大小是否超出限制
    -> prepare_new_inode()  // 创建新inode,填充信息后存入mdcache
    -> push_projected_linkage()  // 没看懂,猜测是链接新目录到父目录链表?
    -> mdlog->start_entry(le)   // 准备mdlog相关操作,新增一条log
    -> mds->locker->issue_new_caps()  // 新建目录的cap,并且清空它(初始化操作)
    -> journal_and_reply()  // 写入mdlog
        -> early_reply()  // 在提交mdlog之前先返回给client结果,应该是为了加速请求返回
        -> submit_mdlog_entry()
        -> mdlog->flush()

文件数据IO流程

以write操作为例进行说明。

client端

-> 用户在cephfs挂载目录下写入文件
    -> fuse_ll_write(fuse_req_t req, fuse_ino_t ino, const char *buf, size_t size, off_t off, struct fuse_file_info *fi)
        -> fuse_ll_req_prepare(req)  // 提取handle
        -> cfuse->client->ll_write(fh, off, size, buf)
            -> Client::_write()
                -> mdsmap->get_max_filesize()  // 检查文件大小是否越界
                -> objecter->osdmap_pool_full(in->layout.pool_id)  // 检查后的存储池是否满了
                -> f->mode & CEPH_FILE_MODE_WR  // 检查句柄是否可写
                -> is_quota_bytes_exceeded()  // 检查配额
                -> bl.append()  // 把写入的内容转存到bufferlist
                -> get_caps()  // 获取操作文件的cap,这个流程比较长,可以参考上面的元数据client端操作流程
                -> 根据写入的字节数判断是否可以执行inline写入,inline应该是指写入inode里面,也就是写入元数据池
                -> cct->_conf->client_oc  // 是否可以执行buffered write
                    -> objectcacher->file_write()  // async, caching, non-blocking
                    -> if O_SYNC||O_DSYNC; _flush_range()  // 刷数据
                -> 否则执行
                    -> if O_DIRECT; _flush_range   // 刷之前的数据
                    -> filer->write_trunc()
                        -> Striper::file_to_extents()   // 根据文件的偏移量和写入的长度找到对应的后端对象
                        -> objecter->sg_write_trunc()  // 把数据写入后端对象
                            -> write_trunc()
                                -> o = new Op(oid, oloc, ops, flags | global_op_flags | CEPH_OSD_FLAG_WRITE, oncommit, objver)
                                -> op_submit(o, &tid)
                                    -> _send_op()
                                        -> send_message()  // 发送对象写消息给osd
        -> fuse_reply_write(req, r)  // Reply with number of bytes written

server端

与rbd场景下osd端处理IO写请求的流程相同,不再分析。

Ceph mds启动及切换流程

基于L版本代码(v12.2.12)分析。本人还在CephFs学习入门阶段,分析过程仅供参考,如有错误请谅解!

mds启动过程

单纯的启动过程比较简单,比较复杂的地方是如何加入mds集群,尤其是多主模式,这部分暂时没有分析。

因此只讨论单主模式的启动,可以分为主启动和备启动两种情况。

通用流程

-> ceph_mds.cc:main()
    -> global_init()  // 创建cct
    -> ceph_argparse_witharg  // 解析进程启动参数hot-standby
    -> pick_addresses  // 解析监听地址
    -> Messenger::create  // 创建messenger,之后就是设置一堆参数,最终绑定ip和端口:msgr->bind(g_conf->public_addr)
    -> global_init_daemonize  // daemon化,守护进程
    -> mc.build_initial_monmap()  // 创建monitor client并且根据配置项里的mon地址初始化monmap
    -> msgr->start()  // 启动messenger线程
    -> mds = new MDSDaemon()  // 创建MDSDaemon实例,后面初始化参数mds->init()
    -> init_async_signal_handler、*_signal_handler  // 注册信号处理函数
    -> msgr->wait()   // 阻塞,等待stop信号


-> MDSDaemon::init()
    -> messenger->add_dispatcher_tail(&beacon);  messenger->add_dispatcher_tail(this);  // 添加dispatcher,一个是心跳、一个是mds自己
    -> monc->set_messenger(messenger)  // mon client也用同一个messenger收发消息
    -> monc->init()  // monitor client初始化,添加dispatcher到messenger,准备与monitor的认证参数,下面monc->authenticate()是完成认证
    -> mgrc.init(); messenger->add_dispatcher_head(&mgrc)  // mgr client的初始化
    -> monc->sub_want("mdsmap", 0, 0);  monc->sub_want("mgrmap", 0, 0);  monc->renew_subs();  // 设置订阅map(mdsmap、mgrmap),并且发送订阅更新请求
    -> set_up_admin_socket()  // 设置admin socket
    -> timer.init()  // mds定时器,主要调用了MDSRankDispatcher::tick()
    -> beacon.init(mdsmap)  // 初始化心跳服务
    -> reset_tick()   // 启动mdsdaemon的定时任务(定期调用MDSRankDispatcher::tick())
// 这个函数比较重要,这个函数是用来定期检查mds运行状态的
-> MDSRankDispatcher::tick()
    -> heartbeat_reset()  // 重置心跳超时时间,一般在执行耗时较长的任务前调用
    -> check_ops_in_flight()  // 检查正在执行的op,slow request就是这里发现的
    -> mdlog->flush()  // mdlog落盘,也就是写入到osd,会调用到Journaler::flush()
    -> is_active() || is_stopping()状态下,执行:
        -> server->recall_client_state(nullptr, Server::RecallFlags::ENFORCE_MAX)  // 发消息给client清理cap,并且unpin一些mdcache里的inode,主要是是释放内存,ENFORCE_MAX表示释放超过最大caps数量的sessions的cap??
        -> mdcache->trim();  // 清理mdcache
        -> mdcache->trim_client_leases();  // 清理无用client链接
        -> mdcache->check_memory_usage();  // 检查是否需要释放内存,如需要则调用recall_client_state进行释放(不指定ENFORCE_MAX,应该是尽量释放?),如果用的是tcmalloc,还会调用ceph_heap_release_free_memory进一步释放内存。
        -> mdlog->trim();  // 清理mdlog
    -> is_clientreplay() || is_active() || is_stopping()状态下,执行:
        -> server->find_idle_sessions()  // 清理空闲的session,g_conf->mds_session_blacklist_on_timeout为true会调用mds->evict_client(),否则kill_session(),前者是加入黑名单,后者只是清理session
        -> server->evict_cap_revoke_non_responders()  // 也是调用evict_client
        -> locker->tick()  // 没看明白
    -> is_reconnect()状态会执行server->reconnect_tick(),reconnect_tick会在mds_reconnect_timeout之后检查session重连情况,对重连超时的session执行evict_client或者kill_session(检查的配置项同上)
    -> is_active()状态会执行(这部分应该都是多mds相关的操作):
        -> balancer->tick()  // 多mds负载均衡
        -> mdcache->find_stale_fragment_freeze()  // 没看明白,大概是说把freeze超时的目录分片unfreeze掉
        -> mdcache->migrator->find_stale_export_freeze()  // 没看明白,大概是执行迁移故障目录
    -> is_active() || is_stopping()状态下,执行update_targets(),这个没看明白,看注释是更新当前mds的mdsmap里的export_targets,主要是这个export_targets不了解是啥,看类型应该是mdsrank的计数集合,计数越大表示mds越重要?
    -> beacon.notify_health(this)  // 将mds的内部状态信息同步给心跳服务,上报给monitor

备mds启动

class Beacon : public Dispatcher
{
  ...
  MDSMap::DaemonState want_state = MDSMap::STATE_BOOT; // 默认state就是BOOT
  ...
}

启动后Beacon会上报mds状态给monitor,默认是BOOT:
/**
 * Call periodically, or when you have updated the desired state
 */
bool Beacon::_send()
{
  ...
    MMDSBeacon *beacon = new MMDSBeacon(
      monc->get_fsid(), mds_gid_t(monc->get_global_id()),
      name,
      epoch,
      want_state,   // 默认就是STATE_BOOT
      last_seq,
      CEPH_FEATURES_SUPPORTED_DEFAULT);

  beacon->set_standby_for_rank(standby_for_rank);
  beacon->set_standby_for_name(standby_for_name);
  beacon->set_standby_for_fscid(standby_for_fscid);
  beacon->set_standby_replay(standby_replay);
  beacon->set_health(health);
  beacon->set_compat(compat);
  // piggyback the sys info on beacon msg
  if (want_state == MDSMap::STATE_BOOT) {
    map<string, string> sys_info;
    collect_sys_info(&sys_info, cct);
    sys_info["addr"] = stringify(monc->get_myaddr());
    beacon->set_sys_info(sys_info);
  }
  monc->send_mon_message(beacon);
  last_send = now;
  return true;
}
}

mds启动后,第一次通过beacon上报mds信息给monitor的时候,monitor如果发现mds不在已有的mds_roles列表,则分配给mds的角色就是STATE_STANDBY。

bool MDSMonitor::prepare_beacon(MonOpRequestRef op)
{
  ...
  // boot?
  if (state == MDSMap::STATE_BOOT) {
    // zap previous instance of this name?
    if (g_conf->mds_enforce_unique_name) {
      bool failed_mds = false;
      while (mds_gid_t existing = pending.find_mds_gid_by_name(m->get_name())) {
        if (!mon->osdmon()->is_writeable()) {
          mon->osdmon()->wait_for_writeable(op, new C_RetryMessage(this, op));
          return false;
        }
        const MDSMap::mds_info_t &existing_info =
          pending.get_info_gid(existing);
        mon->clog->info() << existing_info.human_name() << " restarted";
        fail_mds_gid(pending, existing);
        failed_mds = true;
      }
      if (failed_mds) {
        assert(mon->osdmon()->is_writeable());
        request_proposal(mon->osdmon());
      }
    }

    // Add this daemon to the map
    if (pending.mds_roles.count(gid) == 0) {
      MDSMap::mds_info_t new_info;
      new_info.global_id = gid;
      new_info.name = m->get_name();
      new_info.addr = addr;
      new_info.mds_features = m->get_mds_features();
      new_info.state = MDSMap::STATE_STANDBY;
      new_info.state_seq = seq;
      new_info.standby_for_rank = m->get_standby_for_rank();
      new_info.standby_for_name = m->get_standby_for_name();
      new_info.standby_for_fscid = m->get_standby_for_fscid();
      new_info.standby_replay = m->get_standby_replay();
      pending.insert(new_info);
    }
  ...
}

// 后面monitor会进行Paxos决议,把pending状态的mdsmap持久化,然后分发给mds,mds收到mdsmap后,在MDSDaemon::handle_core_message()里根据消息类型调用MDSDaemon::handle_mds_map()进一步处理。

void MDSDaemon::handle_mds_map(MMDSMap *m)
{
  ...
  if (whoami == MDS_RANK_NONE) {
    if (mds_rank != NULL) {
      const auto myid = monc->get_global_id();
      // We have entered a rank-holding state, we shouldn't be back
      // here!
      if (g_conf->mds_enforce_unique_name) {
        if (mds_gid_t existing = mdsmap->find_mds_gid_by_name(name)) {
          const MDSMap::mds_info_t& i = mdsmap->get_info_gid(existing);
          if (i.global_id > myid) {
            ...
            return;
          }
        }
      }

      dout(1) << "Map removed me (mds." << whoami << " gid:"
              << myid << ") from cluster due to lost contact; respawning" << dendl;
      respawn();
    }
    // MDSRank not active: process the map here to see if we have
    // been assigned a rank.
    dout(10) <<  __func__ << ": handling map in rankless mode" << dendl;
    _handle_mds_map(oldmap);
  } 
  ...
}

void MDSDaemon::_handle_mds_map(MDSMap *oldmap)
{
  MDSMap::DaemonState new_state = mdsmap->get_state_gid(mds_gid_t(monc->get_global_id()));

  // Normal rankless case, we're marked as standby
  if (new_state == MDSMap::STATE_STANDBY) {
    beacon.set_want_state(mdsmap, new_state);
    dout(1) << "Map has assigned me to become a standby" << dendl;

    return;
  }
  ...
}

从上面可以看出STANDBY状态mds启动比较简单,基本啥都不用做。

主mds启动

monitor这边主mds启动有两种情况,一种是扩容mds,比如第一个mds启动,会从BOOT进入STARTING状态:

// Beacon上报的mds状态之后,MDSMonitor会通过tick函数进行定期的检查,根据mds集群状态决定下一步的动作,比如是否需要扩容mds集群,是否需要替换mds,是否需要进行故障恢复等。
void MDSMonitor::tick()
{
  ...
    // expand mds cluster (add new nodes to @in)?
  for (auto &p : pending.filesystems) {
    do_propose |= maybe_expand_cluster(pending, p.second->fscid);  // 扩容mds,单mds下一般为第一个mds,多mds则可能是其他新mds
  }
  ...
  if (since_last.count() >= g_conf->mds_beacon_grace) {
      auto &info = pending.get_info_gid(gid);
      dout(1) << "no beacon from mds." << info.rank << "." << info.inc
              << " (gid: " << gid << " addr: " << info.addr
              << " state: " << ceph_mds_state_name(info.state) << ")"
              << " since " << since_last.count() << "s" << dendl;
      // If the OSDMap is writeable, we can blacklist things, so we can
      // try failing any laggy MDS daemons.  Consider each one for failure.
      if (osdmap_writeable) {
        maybe_replace_gid(pending, gid, info, &do_propose, &propose_osdmap);   // 主mds超时,进行主备转换,用备替换主
      }
    }
  ...
  for (auto &p : pending.filesystems) {
    auto &fs = p.second;
    if (!fs->mds_map.test_flag(CEPH_MDSMAP_DOWN)) {
      do_propose |= maybe_promote_standby(pending, fs);  // 这个场景没太看明白,也是把备变成主,但是不知道是什么场景,跟上面的maybe_replace_gid有啥区别?
    }
  }
  ...
  if (do_propose) {
    propose_pending();  // 触发Paxos决议mdsmap
  }
}

void MDSMonitor::maybe_replace_gid(FSMap &fsmap, mds_gid_t gid,
    const MDSMap::mds_info_t& info, bool *mds_propose, bool *osd_propose)
{
  ...
  mds_gid_t sgid;
  if (info.rank >= 0 &&
      info.state != MDSMap::STATE_STANDBY &&
      info.state != MDSMap::STATE_STANDBY_REPLAY &&
      may_replace &&
      !fsmap.get_filesystem(fscid)->mds_map.test_flag(CEPH_MDSMAP_DOWN) &&
      (sgid = fsmap.find_replacement_for({fscid, info.rank}, info.name,
                g_conf->mon_force_standby_active)) != MDS_GID_NONE)
  {
    ...
    // Promote the replacement
    auto fs = fsmap.filesystems.at(fscid);
    fsmap.promote(sgid, fs, info.rank);

    *mds_propose = true;
  }
  ...
}


void FSMap::promote(
    mds_gid_t standby_gid,
    const std::shared_ptr<Filesystem> &filesystem,
    mds_rank_t assigned_rank)
{
  ...
  if (mds_map.stopped.erase(assigned_rank)) {
    // The cluster is being expanded with a stopped rank  // 我理解这就是启动首个mds情况
    info.state = MDSMap::STATE_STARTING;
  } else if (!mds_map.is_in(assigned_rank)) {
    // The cluster is being expanded with a new rank  // 这个是扩容mds集群情况(多主mds)
    info.state = MDSMap::STATE_CREATING;
  } else {
    // An existing rank is being assigned to a replacement  // 这个是主备转换(备转主)
    info.state = MDSMap::STATE_REPLAY;    // 注意这个状态,mds进程那边会用到
    mds_map.failed.erase(assigned_rank);
  }
  ...
}

MDS进程这边,主和备启动的差别是在MDSDaemon::handle_mds_map(MMDSMap *m)里:

void MDSDaemon::handle_mds_map(MMDSMap *m)
{
  ...
  if (whoami == MDS_RANK_NONE) {
    ...
  } else {
    ...
    // Did I previously not hold a rank?  Initialize!
    if (mds_rank == NULL) {
      mds_rank = new MDSRankDispatcher(whoami, mds_lock, clog,
          timer, beacon, mdsmap, messenger, monc,
          new FunctionContext([this](int r){respawn();}),
          new FunctionContext([this](int r){suicide();}));
      dout(10) <<  __func__ << ": initializing MDS rank "
               << mds_rank->get_nodeid() << dendl;
      mds_rank->init();
    }

    // MDSRank is active: let him process the map, we have no say.
    dout(10) <<  __func__ << ": handling map as rank "
             << mds_rank->get_nodeid() << dendl;
    mds_rank->handle_mds_map(m, oldmap);
  }
  ...
}
void MDSRankDispatcher::handle_mds_map(
    MMDSMap *m,
    MDSMap *oldmap)
{
  ...
  // did it change?
  if (oldstate != state) {
    dout(1) << "handle_mds_map state change "
        << ceph_mds_state_name(oldstate) << " --> "
        << ceph_mds_state_name(state) << dendl;
    beacon.set_want_state(mdsmap, state);

    if (oldstate == MDSMap::STATE_STANDBY_REPLAY) {
        dout(10) << "Monitor activated us! Deactivating replay loop" << dendl;
        assert (state == MDSMap::STATE_REPLAY);
    } else {
      // did i just recover?
      if ((is_active() || is_clientreplay()) &&
          (oldstate == MDSMap::STATE_CREATING ||
       oldstate == MDSMap::STATE_REJOIN ||
       oldstate == MDSMap::STATE_RECONNECT))
        recovery_done(oldstate);

      if (is_active()) {
        active_start();
      } else if (is_any_replay()) {   // 从这个状态开始,FSMap::promote里面设置的STATE_REPLAY状态
        replay_start();   // 经过状态申请消息及回调过程,完成boot的MDS_BOOT_INITIAL、MDS_BOOT_OPEN_ROOT、MDS_BOOT_PREPARE_LOG、MDS_BOOT_REPLAY_DONE阶段,进入MDSRank::replay_done(),这个函数里面最终会调用request_state(MDSMap::STATE_RECONNECT)(单主mds)或request_state(MDSMap::STATE_RESOLVE)(多主mds)
      } else if (is_resolve()) {  // mdsmap里返回STATE_RESOLVE状态后,进入resolve_start
        resolve_start();
      } else if (is_reconnect()) {  // mdsmap里返回STATE_RECONNECT状态后,进入reconnect_start,后面其他状态也类似
        reconnect_start();
      } else if (is_rejoin()) {
        rejoin_start();
      } else if (is_clientreplay()) {
        clientreplay_start();
      } else if (is_creating()) {
        boot_create();
      } else if (is_starting()) {
        boot_start();
      } else if (is_stopping()) {
        assert(oldstate == MDSMap::STATE_ACTIVE);
        stopping_start();
      }
    }
  }
  ...
}

通过上述分析可以发现,mds的状态转化是要经过多个mdsmap变化来完成的,每个阶段或状态都请求一个mdsmap,被决议通过后才会继续进行下一个阶段。

mds状态列表

可以参考: https://github.com/ceph/ceph/blob/master/doc/cephfs/mds-states.rst#mds-states