写点什么

滴滴 ElasticSearch 千万级 TPS 写入性能翻倍技术剖析

  • 2020-08-25
  • 本文字数:6078 字

    阅读完需:约 20 分钟

滴滴ElasticSearch千万级TPS写入性能翻倍技术剖析

1. 背景

前段时间,为了降低用户使用 ElasticSearch 的存储成本,我们做了数据的冷热分离。为了保持集群磁盘利用率不变,我们减少了热节点数量。ElasticSearch 集群开始出现写入瓶颈,节点产生大量的写入 rejected,大量从 kafka 同步的数据出现写入延迟。我们深入分析写入瓶颈,找到了突破点,最终将 Elasticsearch 的写入性能提升一倍以上,解决了 ElasticSearch 瓶颈导致的写入延迟。这篇文章介绍了我们是如何发现写入瓶颈,并对瓶颈进行深入分析,最终进行了创新性优化,极大的提升了写入性能。

2. 写入瓶颈分析

2.1 发现瓶颈

我们去分析这些延迟问题的时候,发现了一些不太好解释的现象。之前做性能测试时,ES 节点 cpu 利用率能超过 80%,而生产环境延迟索引所在的节点 cpu 资源只使用了不到 50%,集群平均 cpu 利用率不到 40%,这时候 IO 和网络带宽也没有压力。通过提升写入资源,写入速度基本没增加。于是我们开始一探究竟,我们选取了一个索引进行验证,该索引使用 10 个 ES 节点。从下图看到,写入速度不到 20w/s,10 个 ES 节点的 cpu,峰值在 40-50%之间。



为了确认客户端资源是足够的,在客户端不做任何调整的情况下,将索引从 10 个节点,扩容到 16 个节点,从下图看到,写入速度来到了 30w/s 左右。



这证明了瓶颈出在服务端,ES 节点扩容后,性能提升,说明 10 个节点写入已经达到瓶颈。但是上图可以看到,CPU 最多只到了 50%,而且此时 IO 也没达到瓶颈。

2.2 ES 写入模型说明

这里要先对 ES 写入模型进行说明,下面分析原因会跟写入模型有关。



客户端一般是准备好一批数据写入 ES,这样能极大减少写入请求的网络交互,使用的是 ES 的 BULK 接口,请求名为 BulkRequest。这样一批数据写入 ES 的 ClientNode。ClientNode 对这一批数据按数据中的 routing 值进行分发,组装成一批 BulkShardRequest 请求,发送给每个 shard 所在的 DataNode。发送 BulkShardRequest 请求是异步的,但是 BulkRequest 请求需要等待全部 BulkShardRequest 响应后,再返回客户端。

2.3 寻找原因

我们在 ES ClientNode 上有记录 BulkRequest 写入 slowlog。


  • items是一个 BulkRequest 的发送请求数

  • totalMills是 BulkRequest 请求的耗时

  • max记录的是耗时最长的 BulkShardRequest 请求

  • avg记录的是所有 BulkShardRequest 请求的平均耗时。


我这里截取了部分示例。


[xxx][INFO ][o.e.m.r.RequestTracker   ] [log6-clientnode-sf-5aaae-10] bulkDetail||requestId=null||size=10486923||items=7014||totalMills=2206||max=2203||avg=37
[xxx][INFO ][o.e.m.r.RequestTracker ] [log6-clientnode-sf-5aaae-10] bulkDetail||requestId=null||size=210506||items=137||totalMills=2655||max=2655||avg=218
复制代码


从示例中可以看到,2 条记录的 avg 相比 max 都小了很多。一个 BulkRequest 请求的耗时,取决于最后一个 BulkShardRequest 请求的返回。这就很容易联想到分布式系统的长尾效应。



接下来再看一个现象,我们分析了某个节点的 write 线程的状态,发现节点有时候 write 线程全是 runnable 状态,有时候又有大量在 waiting。此时写入是有瓶颈的,runnable 状态可以理解,但却经常出现 waiting 状态。所以这也能印证了 CPU 利用率不高。同时也论证长尾效应的存在,因为长尾节点繁忙,ClientNode 在等待繁忙节点返回 BulkShardRequest 请求,其他节点可能出现相对空闲的状态。下面是一个节点 2 个时刻的线程状态:


时刻一:



时刻二:


2.4 瓶颈分析

谷歌大神 Jeffrey Dean《The Tail At Scale》介绍了长尾效应,以及导致长尾效应的原因。总结下来,就是正常请求都很快,但是偶尔单次请求会特别慢。这样在分布式操作时会导致长尾效应。我们从 ES 原理和实现中分析,造成 ES 单次请求特别慢的原因。发现了下面几个因素会造成长尾问题:

2.4.1 lucene refresh

我们打开 lucene 引擎内部的一些日志,可以看到:



write 线程是用来处理 BulkShardRequest 请求的,但是从截图的日志可以看到,write 线程也会会进行 refresh 操作。这里面的实现比较复杂,简单说,就是 ES 定期会将写入 buffer 的数据 refresh 成 segment,ES 为了防止 refresh 不过来,会在 BulkShardRequest 请求的时候,判断当前 shard 是否有正在 refresh 的任务,有的话,就会帮忙一起分摊 refresh 压力,这个是在 write 线程中进行的。这样的问题就是造成单次 BulkShardRequest 请求写入很慢。还导致长时间占用了 write 线程。在 write queue 的原因会具体介绍这种危害。


2.4.2 translog ReadWriteLock

ES 的 translog 类似 LSM-Tree 的 WAL log。ES 实时写入的数据都在 lucene 内存 buffer 中,所以需要依赖写入 translog 保证数据的可靠性。ES translog 具体实现中,在写 translog 的时候会上 ReadLock。在 translog 过期、翻滚的时候会上 WriteLock。这会出现,在 WriteLock 期间,实时写入会等待 ReadLock,造成了 BulkShardRequest 请求写入变慢。我们配置的 tranlog 写入模式是 async,正常开销是非常小的,但是从图中可以看到,写 translog 偶尔可能超过 100ms。


2.4.3 write queue


ES DataNode 的写入是用标准的线程池模型是,提供一批 active 线程,我们一般配置为跟 cpu 个数相同。然后会有一个 write queue,我们配置为 1000。DataNode 接收 BulkShardRequest 请求,先将请求放入 write queue,然后 active 线程有空隙的,就会从 queue 中获取 BulkShardRequest 请求。这种模型下,当写入 active 线程繁忙的时候,queue 中会堆积大量的请求。这些请求在等待执行,而从 ClientNode 角度看,就是 BulkShardRequest 请求的耗时变长了。下面日志记录了 action 的 slowlog,其中 waitTime 就是请求等待执行的时间,可以看到等待时间超过了 200ms。


[xxx][INFO ][o.e.m.r.RequestTracker   ] [log6-datanode-sf-4f136-100] actionStats||action=indices:data/write/bulk[s][p]||requestId=546174589||taskId=6798617657||waitTime=231||totalTime=538
[xxx][INFO ][o.e.m.r.RequestTracker ] [log6-datanode-sf-4f136-100] actionStats||action=indices:data/write/bulk[s][p]||requestId=546174667||taskId=6949350415||waitTime=231||totalTime=548
复制代码

2.4.4 JVM GC

ES 正常一次写入请求基本在亚毫秒级别,但是 jvm 的 gc 可能在几十到上百毫秒,这也增加了 BulkShardRequest 请求的耗时。这些加重长尾现象的 case,会导致一个情况就是,有的节点很繁忙,发往这个节点的请求都 delay 了,而其他节点却空闲下来,这样整体 cpu 就无法充分利用起来。

2.5 论证结论

长尾问题主要来自于 BulkRequest 的一批请求会分散写入多个 shard,其中有的 shard 的请求会因为上述的一些原因导致响应变慢,造成了长尾。如果每次 BulkRequest 只写入一个 shard,那么就不存在写入等待的情况,这个 shard 返回后,ClientNode 就能将结果返回给客户端,那么就不存在长尾问题了。


我们做了一个验证,修改客户端 SDK,在每批 BulkRequest 写入的时候,都传入相同的 routing 值,然后写入相同的索引,这样就保证了 BulkRequest 的一批数据,都写入一个 shard 中。



优化后,第一个平稳曲线是,每个 bulkRequest 为 10M 的情况,写入速度在 56w/s 左右。之后将 bulkRequest 改为 1M(10M 差不多有 4000 条记录,之前写 150 个 shard,所以 bulkSize 比较大)后,性能还有进一步提升,达到了 65w/s。


从验证结果可以看到,每个 bulkRequest 只写一个 shard 的话,性能有很大的提升,同时 cpu 也能充分利用起来,这符合之前单节点压测的 cpu 利用率预期。

3. 性能优化

从上面的写入瓶颈分析,我们发现了 ES 无法将资源用满的原因来自于分布式的长尾问题。于是我们着重思考如何消除分布式的长尾问题。然后也在探寻其他的优化点。整体性能优化,我们分成了三个方向:


  • 横向优化,优化写入模型,消除分布式长尾效应。

  • 纵向优化,提升单节点写入能力。

  • 应用优化,探究业务节省资源的可能。


这次的性能优化,我们在这三个方向上都取得了一些突破。

3.1 优化写入模型

写入模型的优化思路是将一个 BulkRequest 请求,转发到尽量少的 shard,甚至只转发到一个 shard,来减少甚至消除分布式长尾效应。我们完成的写入模型优化,最终能做到一个 BulkRequest 请求只转发到一个 shard,这样就消除了分布式长尾效应。


写入模型的优化分成两个场景。一个是数据不带 routing 的场景,这种场景用户不依赖数据分布,比较容易优化的,可以做到只转发到一个 shard。另一个是数据带了 routing 的场景,用户对数据分布有依赖,针对这种场景,我们也实现了一种优化方案。

3.1.1 不带 routing 场景

由于用户对 routing 分布没有依赖,ClientNode 在处理 BulkRequest 请求中,给 BulkRequest 的一批请求带上了相同的随机 routing 值,而我们生成环境的场景中,一批数据是写入一个索引中,所以这一批数据就会写入一个物理 shard 中。


3.1.2 带 routing 场景

下面着重介绍下我们在带 routing 场景下的实现方案。这个方案,我们需要在 ES Server 层和 ES SDK 都进行优化,然后将两者综合使用,来达到一个 BulkRequest 上的一批数据写入一个物理 shard 的效果。优化思路 ES SDK 做一次数据分发,在 ES Server 层做一次随机写入来让一批数据写入同一个 shard。


先介绍下 Server 层引入的概念,我们在 ES shard 之上,引入了逻辑 shard 的概念,命名为number_of_routing_size 。ES 索引的真实 shard 我们称之为物理 shard,命名是number_of_shards


物理 shard 必须是逻辑 shard 的整数倍,这样一个逻辑 shard 可以映射到多个物理 shard。一组逻辑 shard,我们命名为 slot,slot 总数为number_of_shards / number_of_routing_size


数据在写入 ClientNode 的时候,ClientNode 会给 BulkRequest 的一批请求生成一个相同的随机值,目的是为了让写入的一批数据,都能写入相同的 slot 中。数据流转如图所示:



最终计算一条数据所在 shard 的公式如下:


slot = hash(random(value)) % (number_of_shards/number_of_routing_size)
shard_num = hash(_routing) % number_of_routing_size + number_of_routing_size * slot
复制代码


然后我们在 ES SDK 层进一步优化,在 BulkProcessor 写入的时候增加逻辑 shard 参数,在 add 数据的时候,可以按逻辑 shard 进行 hash,生成多个 BulkRequest。这样发送到 Server 的一个 BulkRequest 请求,只有一个逻辑 shard 的数据。最终,写入模型变为如下图所示:



经过 SDK 和 Server 的两层作用,一个 BulkRequest 中的一批请求,写入了相同的物理 shard。


这个方案对写入是非常友好的,但是对查询会有些影响。由于 routing 值是对应的是逻辑 shard,一个逻辑 shard 要对应多个物理 shard,所以用户带 routing 的查询时,会去一个逻辑 shard 对应的多个物理 shard 中查询。


我们针对优化的是日志写入的场景,日志写入场景的特征是写多读少,而且读写比例差别很大,所以在实际生产环境中,查询的影响不是很大。

3.2 单节点写入能力提升

单节点写入性能提升主要有以下优化:


backport 社区优化,包括下面 2 方面:


  • merge 社区 flush 优化特性:[#27000] Don’t refresh on _flush _force_merge and _upgrade

  • merge 社区 translog 优化特性,包括下面 2 个:

  • [#45765] Do sync before closeIntoReader when rolling generation to improve index performance

  • [#47790] sync before trimUnreferencedReaders to improve index preformance


这些特性我们在生产环境验证下来,性能大概可以带来 18%的性能提升。


我们还做了 2 个可选性能优化点:


  • 优化 translog,支持动态开启索引不写 translog,不写 translog 的话,我们可以不再触发 translog 的锁问题,也可以缓解了 IO 压力。但是这可能带来数据丢失,所以目前我们做成动态开关,可以在需要追数据的时候临时开启。后续我们也在考虑跟 flink 团队结合,通过 flink checkpoint 保证数据可靠性,就可以不依赖写入 translog。从生产环境我们验证的情况看,在写入压力较大的索引上开启不写 translog,能有 10-30%不等的性能提升。

  • 优化 lucene 写入流程,支持在索引上配置在 write 线程不同步 flush segment,解决前面提到长尾原因中的 lucene refresh 问题。在生产环境上,我们验证下来,能有 7-10%左右的性能提升。

3.2.1 业务优化

在本次进行写入性能优化探究过程中,我们还和业务一起发现了一个优化点,业务的日志数据中存在 2 个很大的冗余字段(args、response),这两个字段在日志原文中存在,还另外用了 2 个字段存储,这两个字段并没有加索引,日志数据写入 ES 时可以不从日志中解析出这 2 个字段,在查询的时候直接从日志原文中解析出来。


不清洗大的冗余字段,我们验证下来,能有 20%左右的性能提升,该优化同时还带来了 10%左右存储空间节约。

4. 生产环境性能提升结果

4.1 写入模型优化

我们重点看下写入模型优化的效果,下面的优化,都是在客户端、服务端资源没做任何调整的情况下的生产数据。


下图所示索引开启写入模型优化后,写入 tps 直接从 50w/s,提升到 120w/s。



生产环境索引写入性能的提升比例跟索引混部情况、索引所在资源大小(长尾问题影响程度)等因素影响。从实际优化效果看,很多索引都能将写入速度翻倍,如下图所示:


4.2 写入拒绝量(write rejected)下降

然后再来看一个关键指标,写入拒绝量(write rejected)。ES datanode queue 满了之后就会出现 rejected。


rejected 异常带来个危害,一个是个别节点出现 rejected,说明写入队列满了,大量请求在队列中等待,而 region 内的其他节点却可能很空闲,这就造成了 cpu 整体利用率上不去。


rejected 异常另一个危害是造成失败重试,这加重了写入负担,增加了写入延迟的可能。


优化后,由于一个 bulk 请求不再分到每个 shard 上,而是写入一个 shard。一来减少了写入请求,二来不再需要等待全部 shard 返回。


4.3 延迟情况缓解

最后再来看下写入延迟问题。经过优化后,写入能力得到大幅提升后,极大的缓解了当前的延迟情况。下面截取了集群优化前后的延迟情况对比。


5. 总结

这次写入性能优化,滴滴 ES 团队取得了突破性进展。写入性能提升后,我们用更少的 SSD 机器支撑了数据写入,支撑了数据冷热分离和大规格存储物理机的落地,在这过程中,我们下线了超过 400 台物理机,节省了每年千万左右的服务器成本。在整个优化过程中,我们深入分析 ES 写入各个环节的耗时情况,去探寻每个耗时环节的优化点,对 ES 写入细节有了更加深刻的认识。我们还在持续探寻更多的优化方式。而且我们的优化不仅在写入性能上。在查询的性能和稳定性,集群的元数据变更性能等等方面也都在不断探索。我们也在持续探究如何给用户提交高可靠、高性能、低成本、更易用的 ES,未来会有更多干货分享给大家。


作者介绍


魏子珺,滴滴专家工程师。


滴滴 Elasticsearch 引擎负责人,负责带领引擎团队深入 Elasticsearch 内核,解决在海量规模下 Elasticsearch 遇到的稳定性、性能、成本方面的问题。曾在盛大、网易工作,有丰富的引擎建设经验。


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


原文链接


滴滴ElasticSearch千万级TPS写入性能翻倍技术剖析$


2020-08-25 14:009046

评论

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

数字化时代,企业如何创新自己的客户服务

Baklib

软件要想做的好,测试必定少不了

华为云开发者联盟

测试 开发 华为云 企业号十月 PK 榜

区块链≠绿色?波卡或成Web3“生态环保”标杆

One Block Community

区块链 环保 波卡生态

在世界舞台MBBF一骑绝尘:永远更快一步的北京5G是怎样炼成的?

脑极体

前端面试中小型公司都考些什么

loveX001

JavaScript

前端面试指南之JS面试题总结

loveX001

JavaScript

阿里是如何使用分布式架构的?阿里内部学习手册分享

Java全栈架构师

架构 分布式 微服务 后端 高并发

2022最新CSS高频面试题指南

CoderBin

CSS 前端 面试题 秋招 10月月更

36氪|元年科技发布新版数字化PaaS平台,更新多个组件

元年技术洞察

方舟 PaaS 中台战略 企业数字化

华为数通HCIA小型拓扑综合实验,运用OSPF动态路由协议、ACL访问控制列表,交换机生成树协议,修改交换机根桥、交换机划分vlan、链路聚合等相关数通技术、NAT地址转换以及NAT网络地址转换的配置

Python-派大星

10月月更

报名倒计时1天!平头哥、中科院软件所PLCT实验室等技术专家解读最新RISC-V技术

OpenAnolis小助手

报名 risc-v 云栖大会 Workshop 龙蜥峰会

KubeVela 插件指南:轻松扩展你的平台专属能力

阿里巴巴云原生

阿里云 开源 容器 云原生 KubeVela

从清华大学到苏州经贸,双一流和普通高校都在使用的数据科学教学实训平台

ModelWhale

大数据 人才培养 数据竞赛 实训 教学

软件测试丨接口测试该怎么做?持证上岗的Charles,可以帮你做什么?

测试人

软件测试 接口测试 charles 测试开发

JUC 浅析(四)

Andy

深入理解JS作用域链与执行上下文

loveX001

JavaScript

阿里云云边一体容器架构创新论文被云计算顶会 ACM SoCC 录用

阿里巴巴云原生

阿里云 云原生 容器服务

专访韩向东|元年科技:专业与技术并重,赋能财务数字化转型

元年技术洞察

数字化转型 财务数字化

手把手教你从安装CentOS7.4镜像开始,搭建IoT视频监控系统

华为云开发者联盟

后端 开发 华为云 企业号十月 PK 榜

问:你是如何进行react状态管理方案选择的?

beifeng1996

React

FlyFish一周年,社区大咖邀你共话开源!

云智慧AIOps社区

低代码 可视化 数据可视化 大屏可视化 无代码

要努力,但也别焦虑

源字节1号

程序人生

“程”风破浪的开发者|学习中的境界

林冲

学习方法 “程”风破浪的开发者

NAT基础:NAT技术原理,静态NAT、动态NAT、NAPT、Easy IP、NAT Server的原理,以及各NAT的配置方法和转换示例。

Python-派大星

10月月更

教你处理数仓慢SQL常见定位问题

华为云开发者联盟

数据库 后端 华为云 企业号十月 PK 榜

如何提升研发效能?我们先从指标谈起

Kyligence

数据分析 指标管理

对话创始人:团队研发效能应该如何管理和度量?

LigaAI

团队管理 敏捷开发 研发管理 研发效能 企业号十月PK榜

JUC 浅析(三)

Andy

云小课|MRS基础原理之Hudi介绍

华为云开发者联盟

大数据 华为云 企业号十月 PK 榜

订单中心架构设计与实践

小小怪下士

Java 程序员 系统架构 架构设计

ACL访问控制列表 基础、创建ACL访问控制列表的两种方式、配置ACL访问控制列表规则、修改ACL规则的默认步长。子网掩码、反掩码、通配符掩码的区别和作用。

Python-派大星

10月月更

滴滴ElasticSearch千万级TPS写入性能翻倍技术剖析_软件工程_滴滴技术_InfoQ精选文章