飞天发布时刻:2024年 Forrester 公有云平台Wave™评估报告解读 了解详情
写点什么

滴滴 Ceph 分布式存储系统优化之锁优化

  • 2020-09-08
  • 本文字数:3508 字

    阅读完需:约 12 分钟

滴滴Ceph分布式存储系统优化之锁优化

1. 背景

在支撑一些延迟敏感的在线应用过程中,我们发现 Ceph 的尾延迟较差,当应用并发负载较高时,Ceph 很容易出现延迟的毛刺,对延迟敏感的应用造成超时甚至崩溃。我们对 Ceph 的尾延迟问题进行了深入细致的分析和优化。造成尾延迟的一个重要原因就是代码中锁的使用问题,下面根据锁问题的类型分别介绍我们的优化工作。本文假设读者已熟悉 Ceph 的基本读写代码流程,代码的版本为 Luminous。

2. 持锁时间过长

2.1 异步读优化

Ceph 的 osd 处理客户端请求的线程池为 osd_op_tp,在处理操作请求的时候,线程会先锁住操作对应 pg 的 lock。其中,处理对象读请求的代码如下图所示,在锁住对象所属 pg 的 lock 后,对于最常用的多副本存储方式,线程会同步进行读操作,直到给客户端发送返回的数据后,才会释放 pg lock。


在进行读操作时,如果数据没有命中 page cache 而需要从磁盘读,是一个耗时的操作,并且 pg lock 是一个相对粗粒度的锁,在 pg lock 持有期间,其它同属一个 pg 的对象的读写操作都会在加锁上等待,增大了读写延迟,降低了吞吐率。同步读的另一个缺点是读操作没有参与流量控制。



我们对线上集群日志的分析也验证了上述问题,例如,一个日志片段如下图所示,图中列举了两个 op 的详细耗时信息,这两个 op 均为同一个 osd 的线程所执行,且操作的是同一个 pg 的对象。根据时间顺序,第一个 op 为 read,总耗时为 56ms。第二个 op 为 write,总耗时为 69ms。图中信息显示,第二个 op 处理的一个中间过程,即副本写的完成消息在处理之前,在 osd 请求队列中等待了 36ms。结合上图的代码可以知道,这 36ms 都是耗在等待 pg lock 上,因为前一个 read 操作持有 pg lock,而两个对象属于相同 pg。



我们的优化如下图所示,我们创建了独立的读线程,负责处理读请求,osd_op_tp 线程只需将读请求提交到读线程的队列即可返回解锁,大大减少了 pg lock 的持有时间。读线程完成磁盘读之后,将结果放到 finisher 线程的队列,finisher 线程重新申请 pg lock 后负责后续处理,这样将耗时的磁盘访问放在了不持有 pg lock 的流程中,结合我们在流量控制所做的优化,读写操作可以在统一的框架下进行流量控制,从而精准控制磁盘的利用率,以免磁盘访问拥塞造成尾延迟。



我们用 fio 进行了异步读优化效果的测试,测试方法:对同一个 pool 的两个 rbd,一个做随机读,另一个同时做随机写操作,将 pg number 配置为 1,这样所有对象读写会落到同一个 osd 的同一个 pg。异步读优化后,随机写平均延迟下降了 53% 。下图为某业务的 filestore 集群异步读上线前后读吞吐率的数据,箭头所指为上线时间,可见上线之后,集群承载的读操作的吞吐率增加了 120%



上述优化在使用 filestore 存储后端时取得了明显的效果,但在使用 bluestore 存储后端时,bluestore 代码中还存在持有 pg 粒度锁同步读的问题,具体见 BlueStore::read 的代码。我们对 bluestore 的读也进行了异步的优化,这里就不详细介绍了。

3. 锁粒度过粗

3.1 object cache lock 优化

Ceph 在客户端实现了一个基于内存的 object cache,供 rbd 和 cephfs 使用。但 cache 只有一把大的互斥锁,任何 cache 中对象的读写都需要先获得这把锁。在使用写回模式时,cache flusher 线程在写回脏数据之前,也会锁住这个锁。这时对 cache 中缓存对象的读写都会因为获取锁而卡住,使读写延迟增加,限制了吞吐率。


我们实现了细粒度的对象粒度的锁,在进行对象的读写操作时,只需获取对应的对象锁,无需获取全局锁。只有访问全局数据结构时,才需要获取全局锁,大大增加了对象间操作的并行。并且对象锁采用读写锁,增加了同一对象上读的并行。测试表明,高并发下 rbd 的吞吐率增加了超过 20%

4. 不必要的锁竞争

4.1 减少 pg lock 竞争

Ceph 的 osd 对客户端请求的处理流程为,messenger 线程收到请求后,将请求放入 osd_op_tp 线程池的缓存队列。osd_op_tp 线程池的线程从请求缓存队列中出队一个请求,然后根据该请求操作的对象对应的 pg 将请求放入一个与 pg 一一对应的 pg slot 队列的尾部。然后获取该 pg 的 pg lock,从 pg slot 队列首部出队一个元素处理。


可见,如果 osd_op_tp 线程池的请求缓存队列中连续两个请求操作的对象属于相同的 pg,则一个 osd_op_tp 线程出队前一个请求加入 pg slot 队列后,获取 pg lock,从 pg slot 队列首部出队一个请求开始处理。另一个 osd_op_tp 线程从请求缓存队列出队第二个请求,因为两个请求是对应相同的 pg,则它会加入相同的 pg slot 队列,然后,第二个线程在获取 pg lock 时会阻塞。这降低了 osd_op_tp 线程池的吞吐率,增加了请求的延迟。


我们的优化方式是保证任意时刻每个 pg slot 队列只有一个线程处理。因为在处理 pg slot 队列中的请求之前需要获取 pg lock,因此同一个 pg slot 队列的请求是无法并行处理的。我们在每个 pg slot 队列增加一个标记,记录当前正在处理该 pg slot 的请求的线程。当有线程正在处理一个 pg slot 的请求时,别的线程会跳过处理该 pg slot,继续从 osd_op_tp 线程池的请求缓存队列出队请求。

4.2 log lock 优化

Ceph 的日志系统实现是有一个全局的日志缓存队列,由一个全局锁保护,由专门的日志线程从日志缓存队列中取日志打印。工作线程提交日志时,需要获取全局锁。日志线程在获取日志打印之前,也需要获取全局锁,然后做一个交换将队列中的日志交换到一个临时队列。另外,当日志缓存队列长度超过阈值时,提交日志的工作线程需要睡眠等待日志线程打印一些日志后,再提交。锁的争抢和等待都增加了工作线程的延迟。


我们为每个日志提交线程引入一个线程局部日志缓存队列,该队列为经典的单生产者单消费者无锁队列。线程提交日志直接提交到自己的局部日志缓存队列,该过程是无锁的。只有队列中的日志数超过阈值后,才会通知日志线程。日志线程也会定期轮询各个日志提交线程的局部日志缓存队列,打印一些日志,该过程也是无锁的。通过上述优化,基本避免了日志提交过程中因为锁竞争造成的等待,降低了日志的提交延迟。测试在高并发日志提交时,日志的提交延迟可降低接近 90%

4.3 filestore apply lock 优化

对于 Ceph filestore 存储引擎,同一个 pg 的 op 需要串行 apply。每个 pg 有一个 OpSequencer(简称 osr),用于控制 apply 顺序,每个 osr 有一个 apply lock 以及一个 op 队列。对于每个待 apply 的 op,首先加入对应 pg 的 osr 的队列,然后把 osr 加到 filestore 的负责 apply 的线程池 op_tp 的队列,简称为 apply 队列。op_tp 线程从 apply 队列中取出一个 osr,加上它的 apply lock,再从 osr 的队列里取出一个 op apply,逻辑代码如下图左所示。可见,每个 op 都会把其对应的 osr 加入到 apply 队列一次。如果多个 op 是针对同一个 pg 的对象,则这个 pg 的 osr 可能多次加入到 apply 队列。如果 apply 队列中连续两个 osr 是同一个 pg 的,也就是同一个 osr,则前一个 op 被一个线程进行 apply 时,osr 的 apply lock 已经加锁,另一个线程会在该 osr 的 apply lock 上阻塞等待,降低了并发度。



这个问题也体现在日志中。一个线上集群日志片段如下图,有两个 op_tp 线程 6700 和 5700,apply 队列里三个对象依次来自 pg: 1.1833, 1.1833. 1.5f2。线程 6700 先拿到第一个对象进行 apply, 线程 5700 拿第二个对象进行 apply 时卡在 apply lock 上,因为两个对象都来自 pg 1.1833,直到 6700 做完才开始 apply。而 6700 拿到第三个对象,即 1.5f2 的对象进行 apply 即写 page cache 只用了不到 1ms,但实际 apply 延迟 234ms,可见第三个对象在队列里等待了 233ms。如果 5700 不用等待 apply lock,则第二和第三个对象的 apply 延迟可以大大缩短。



我们优化后的逻辑代码如上图右所示,同一个 osr 只加入 apply 队列一次,取消 apply lock,利用原子操作实现无锁算法。上面的算法可以进一步优化,在将一个 osr 出队之后,可以一次从它的队列中取 m(m>1)个 op 进行 apply,在 op apply 完成阶段,改为如果 atomic::fetch_sub(osr->queue_length, m) > m,则将 osr 重新入队以提高吞吐率。


我们用 fio 进行了 apply lock 优化效果测试,方法为建两个 pool,每个 pool 的 pg number 为 1,每个 pool 一个 rbd, 对两个 rbd 同时进行随机写的操作,一个 pool 写入数据的量为 31k*10k,另一个 pool 写入数据的量为 4k*100k, 衡量所有请求 apply 的总耗时。优化前总耗时 434ks, 优化后总耗时 45ks,减少 89.6%


作者介绍


汪黎,滴滴首席工程师


负责滴滴在线非结构化存储研发,曾任国防科技大学计算机学院副研究员,教研室主任,天河云存储负责人


本文转载自公众号滴滴技术(ID:didi_tech)。


原文链接


滴滴Ceph分布式存储系统优化之锁优化


2020-09-08 10:082482

评论

发布
暂无评论
发现更多内容

华为云FusionInsight助力宇宙行打造金融数据湖新标杆

华为云开发者联盟

数据湖 云原生 存储 FusionInsight 华为云

SpringCloud 从入门到精通15---Sentinel搭建和服务监控

Felix

数据库表数据量大读写缓慢如何优化(4)【分库分表】

我爱娃哈哈😍

数据库 架构

挖矿区块链_什么是挖矿 带你详细了解挖矿基础知识

v16629866266

EXCEL数据如何去重? Python:这事我比你熟

智分析

Python

程序员成长第一篇:机会与趋势

石云升

28天写作 2月春节不断更 机会与趋势

《我想进大厂》之Zookeeper夺命连环9问

艾小仙

zookeeper

2021年云计算面临的5大网络安全威胁

云计算 云安全

即构自研海量有序数据网络MSDN,构建全球可靠的多云通讯链路

ZEGO即构

即日起 Jira、Confluence 正式停售本地版,中国客户将无法购买

万事ONES

项目管理 开发者 研发管理 团队协作 CTO

华为18A架构师共享:Netty+Redis+zookeeper+高并发技术栈

996小迁

redis zookeeper 架构 Netty 高并发

第三周产品经理训练营总结

产品经理训练营

用RabbitMQ了好几年之后,我总结出来5点RabbitMQ的使用心得

四猿外

MQ RabbitMQ 消息队列

产品训练营 第三次作业

Wangyunnfei

托管节点池助力用户构建稳定自愈的 Kubernetes 集群

阿里巴巴云原生

Docker 容器 运维 云原生 k8s

MySQL安装教程&问题解决

Mars

MySQL 运维

玩转IDEA项目结构Project Structure,打Jar包、模块/依赖管理全搞定

YourBatman

Module IntelliJ IDEA Project Structure

关于自己的一个梦(飞翔)

Yuchen

说说常常被研发忽略的原型模式

后台技术汇

28天写作 2月春节不断更

使用pgBackRest并行归档解决wal堆积问题

PostgreSQLChina

数据库 postgresql 开源 开源社区

ICE暴雪正悄悄崛起

DT极客

极客时间产品训练营第三周作业

云随心

产品 作业 产品训练营

后疫情时代,企业如何实现数字化增长?

字节跳动 Kubernetes 容器 云原生

HTTPS是怎么保证数据安全传输的?

面试 HTTP

【mybatis】- MyBatis基础篇

双木之林

anyRTC2020年 年终总结

anyRTC开发者

音视频 WebRTC RTC sdk

官宣了!Apache ECharts 毕业成为 Apache 软件基金会顶级项目!

百度Geek说

百度 基金会

Kafka基础简介

架构精进之路

kafka 七日更 28天写作 2月春节不断更

Idea工具的各种查找快捷键

小马哥

IntelliJ IDEA 七日更 2月春节不断更

利用 Python 分析了某化妆品企业的销售情况,我得出的结论是?

JackTian

Python 数据分析 数据可视化 化妆品 2月春节不断更

Linux-Lab 入门:使用开发板

贾献华

嵌入式 Linux Kenel 开发板 arm boot

滴滴Ceph分布式存储系统优化之锁优化_数据库_滴滴技术_InfoQ精选文章