参考文档: Ceph 作为一款开源的分布式存储软件,可以利用 X86 服务器自身的本地存储资源,创建一个或多个存储资源池,并基于资源池对用户提供统一存储服务,包括块存储、对象存储、文件存储,满足企业对存储高可靠性、高性能、高可扩展性方面的需求,并越来越受到企业的青睐。经过大量的生产实践证明,Ceph 的设计理念先进,功能全面,使用灵活,富有弹性。不过,Ceph 的这些优点对企业来说也是一把双刃剑,驾驭的好可以很好地服务于企业,倘若功力不够,没有摸清 Ceph 的脾性,有时也会制造不小的麻烦,下面我要给大家分享的正是这样的一个案例。
A 公司通过部署 Ceph 对象存储集群,对外提供云存储服务,提供 SDK 帮助客户快速实现对图片、视频、apk 安装包等非结构化数据的云化管理。在业务正式上线前,曾经对 Ceph 做过充分的功能测试、异常测试和性能测试。
集群规模不是很大,使用的是社区 0.80 版本,总共有 30 台服务器,每台服务器配置 32GB 内存,10 块 4T 的 SATA 盘和 1 块 160G 的 Intel S3700 SSD 盘。300 块 SATA 盘组成一个数据资源池(缺省配置情况下就是名称为.rgw.buckets 的 pool),存放对象数据;30 块 SSD 盘组成一个元数据资源池(缺省配置情况下就是名称为.rgw.buckets.index 的 pool),存放对象元数据。有过 Ceph 对象存储运维部署经验的朋友都知道,这样的配置也算是国际惯例,因为 Ceph 对象存储支持多租户,多个用户在往同一个 bucket(用户的一个逻辑空间)中 PUT 对象的时候,会向 bucket 索引对象中写入对象的元数据,由于是共享同一个 bucket 索引对象,访问时需要对这个索引对象加锁,将 bucket 索引对象存放到高性能盘 SSD 组成的资源池中,减少每一次索引对象访问的时间,提升 IO 性能,提高对象存储的整体并发量。
系统上线后,客户数据开始源源不断地存入到 Ceph 对象存储集群,在前面的三个月中,一切运行正常。期间也出现过 SATA 磁盘故障,依靠 Ceph 自身的故障检测、修复机制轻松搞定,运维的兄弟感觉很轻松。进入到 5 月份,运维兄弟偶尔抱怨说 SSD 盘对应的 OSD 有时会变的很慢,导致业务卡顿,遇到这种情况他们简单有效的办法就是重新启动这个 OSD,又能恢复正常。大概这种现象零星发生过几次,运维兄弟询问是不是我们在 SSD 的使用上有什么不对。我们分析后觉得 SSD 盘的应用没有什么特别的地方,除了将磁盘调度算法修改成 deadline,这个已经修改过了,也就没有太在意这个事情。
5 月 28 日晚上 21:30,运维兄弟手机上接到系统告警,少部分文件写入失败,马上登陆系统检查,发现是因为一台服务器上的 SSD 盘对应的 OSD 读写缓慢引起的。按照之前的经验,此类情况重启 OSD 进程后就能恢复正常,毫不犹豫地重新启动该 OSD 进程,等待系统恢复正常。但是这一次,SSD 的 OSD 进程启动过程非常缓慢,并引发同台服务器上的 SATA 盘 OSD 进程卡顿,心跳丢失,一段时间后,又发现其它服务器上开始出现 SSD 盘 OSD 进程卡顿缓慢。继续重启其它服务器上 SSD 盘对应的 OSD 进程,出现了类似情况,这样反复多次重启 SSD 盘 OSD 进程后,起不来的 SSD 盘 OSD 进程越来越多。运维兄弟立即将此情况反馈给技术研发部门,要求火速前往支援。
到办公室后,根据运维兄弟的反馈,我们登上服务器,试着启动几个 SSD 盘对应的 OSD 进程,反复观察比对进程的启动过程:
1、 用 top 命令发现这个 OSD 进程启动后就开始疯狂分配内存,高达 20GB 甚至有时达到 30GB;有时因系统内存耗尽,开始使用 swap 交换分区;有时即使最后进程被成功拉起,但 OSD 任然占用高达 10GB 的内存。
2、 查看 OSD 的日志,发现进入 FileJournal::_open 阶段后就停止了日志输出。经过很长时间(30 分钟以上)后才输出进入 load_pg 阶段;进入 load_pg 阶段后,再次经历漫长的等待,虽然 load_pg 完成,但进程仍然自杀退出。
3、 在上述漫长启动过程中,用 pstack 查看进程调用栈信息,FileJournal::_open 阶段看到的调用栈是在做 OSD 日志回放,事务处理中是执行 levelDB 的记录删除;在 load_pg 阶段看到的调用栈信息是在利用 levelDB 的日志修复 levelDB 文件。
4、 有时候一个 SSD 盘 OSD 进程启动成功,运行一段时间后会导致另外的 SSD 盘 OSD 进程异常死掉。
从这些现象来看,都是跟 levelDB 有关系,内存大量分配是不是跟这个有关系呢?进一步查看 levelDB 相关的代码后发现,在一个事务处理中使用 levelDB 迭代器,迭代器访问记录过程中会不断分配内存,直到迭代器使用完才会释放全部内存。从这一点上看,如果迭代器访问的记录数非常大,就会在迭代过程中分配大量的内存。根据这一点,我们查看 bucket 中的对象数,发现有几个 bucket 中的对象数量达到了 2000 万、3000 万、5000 万,而且这几个大的 bucket 索引对象存储位置刚好就是出现问题的那几个 SSD 盘 OSD。内存大量消耗的原因应该是找到了,这是一个重大突破,此时已是 30 日 21:00,这两天已经有用户开始电话投诉,兄弟们都倍感“鸭梨山大”。已经持续奋战近 48 小时,兄弟们眼睛都红肿了,必须停下来休息,否则会有兄弟倒在黎明前。
31 日 8:30,兄弟们再次投入战斗。
还有一个问题,就是有些 OSD 在经历漫长启动过程,最终在 load_pg 完成后仍然自杀退出。通过走读 ceph 代码,确认是有些线程因长时间没有被调度(可能是因 levelDB 的线程长时间占用了 CPU 导致)而超时自杀所致。在 ceph 的配置中有一个 filestore_op_thread_suicide_timeout 参数,通过测试验证,将这个参数设置成一个很大的值,可以避免这种自杀。又看到了一点点曙光,时钟指向 12:30。
有些进程起来后,仍然会占用高达 10GB 的内存,这个问题不解决,即使 SSD 盘 OSD 拉起来了,同台服务器上的其它 SATA 盘 OSD 运行因内存不足都要受到影响。兄弟们再接再厉啊,这是黎明前的黑暗,一定要挺过去。有人查资料,有人看代码,终于在 14:30 从 ceph 资料文档查到一个强制释放内存的命令:ceph tell osd.* heap release,可以在进程启动后执行此命令释放 OSD 进程占用的过多内存。大家都格外兴奋,立即测试验证,果然有效。
一个 SSD 盘 OSD 起来后运行一会导致其它 SSD 盘 OSD 进程退出,综合上面的分析定位,这主要是因为发生数据迁移,有数据迁出的 OSD,在数据迁出后会删除相关记录信息,触发 levelDB 删除对象元数据记录,一旦遇到一个超大的 bucket 索引对象,levelDB 使用迭代器遍历对象的元数据记录,就会导致过度内存消耗,从而导致服务器上的 OSD 进程异常。
根据上述分析,经过近 2 个小时的反复讨论论证,我们制定了如下应急措施: 1、 给集群设置 noout 标志,不允许做 PG 迁移,因为一旦出现 PG 迁移,有 PG 迁出的 OSD,就会在 PG 迁出后删除 PG 中的对象数据,触发 levelDB 删除对象元数据记录,遇到 PG 中有一个超大的 bucket 索引对象就会因迭代器遍历元数据记录而消耗大量内存。 2、 为了能救活 SSD 对应的 OSD,尽快恢复系统,在启动 SSD 对应的 OSD 进程时,附加启动参数 filestore_op_thread_suicide_timeout,设置一个很大的值。由于故障 OSD 拉起时,LevelDB 的一系列处理会抢占 CPU,导致线程调度阻塞,在 Ceph 中有线程死锁检测机制,超过这个参数配置的时间线程仍然没有被调度,就判定为线程死锁。为了避免因线程死锁导致将进程自杀,需要设置这个参数。 3、 在目前内存有限的情况下,异常的 OSD 启动会使用 swap 交换分区,为了加快 OSD 进程启动,将 swap 分区调整到 SSD 盘上。 4、 启动一个定时任务,定时执行命令 ceph tell osd.* heap release,强制释放 OSD 占用的内存。 5、 SSD 对应的 OSD 出现问题的时候,按如下步骤处理: a) 先将该服务器上的所有 OSD 进程都停掉,以腾出全部内存。 b) 然后启动 OSD 进程,并携带 filestore_op_thread_suicide_timeout 参数,给一个很大的值,如 72000。 c) 观察 OSD 的启动过程,一旦 load_pgs 执行完毕,可以立即手动执行 ceph tell osd.N heap release 命令,将其占用的内存强制释放。 d) 观察集群状态,当所有 PG 的状态都恢复正常时,再将其他 SATA 盘对应的 OSD 进程启动起来。
按照上述步骤,我们从 17:30 开始逐个恢复 OSD 进程,在恢复过程中,那几个超大 bucket 索引对象在做 backfilling 的时候需要较长时间,在此期间访问这个 bucket 的请求都被阻塞,导致应用业务请求出现超时,这也是单 bucket 存储大量对象带来的负面影响。
5 月 31 日 23:00,终于恢复了全部 OSD 进程,从故障到系统全部成功恢复,我们惊心动魄奋战了 72 小时,大家相视而笑,兴奋过度,再接再厉,一起讨论制定彻底解决此问题的方案: 1、 扩大服务器内存到 64GB。 2、 对新建 bucket,限制存储对象的最大数量。 3、 Ceph 0.94 版本经过充分测试后,升级到 0.94 版本,解决单 bucket 索引对象过大问题。 4、 优化 Ceph 对 levelDB 迭代器的使用,在一个大的事务中,通过分段迭代,一个迭代器在完成一定数量的记录遍历后,记录其当前迭代位置,将其释放,再重新创建一个新的迭代器,从上次迭代的位置开始继续遍历,如此可以控制迭代器的内存使用量。
前事不忘后事之师,汲取经验教训,我们总结如下几点: 1、 系统上线前必须经过充分的测试 A 公司的系统上线前,虽然对 ceph 做了充分的功能、性能、异常测试,但却没有大量数据的压力测试,如果之前单 bucket 灌入了几千万对象测试,也许就能提前发现这个隐患。 2、 运维过程中的每一个异常都要及时引起重视 此案例中,在问题爆发前一段时间,运维部门已经有反馈 SSD 异常的问题,可惜没有引起我们重视,倘若当时就深入分析,也许可以找到问题根由,提前制定规避措施。 3、 摸清 ceph 的脾性 任何软件产品都有相应的规格限制,ceph 也不例外。如果能提前深入了解 ceph 架构及其实现原理,了解单 bucket 过度存放大量对象所带来的负面影响,提前规划,也不会出现本案例中遇到的问题。RGW 对配额的支持非常全面,包括用户级别的、bucket 级别的,都可以配置单个 bucket 允许存放的最大对象数量。 4、 时刻跟踪社区最新进展 在 Ceph 的 0.94 版本中,已经支持了 bucket 索引对象的 shard 功能,一个 bucket 索引对象可以分成多个 shard 对象存储,可有效缓解单 bucket 索引对象过大问题。 自己解决问题的思路,因为没有 ceph 所以才会占用如此大内存,进行限制,保证计算机系统稳定性。 通过限制 cpu 和内存