2025上半年,最新 AI实践都在这!20+ 应用案例,任听一场议题就值回票价 了解详情
写点什么

滴滴 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:009568

评论

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

使用SpringCloudConfig进行分布式配置:存储库后端类型

Java 程序员 后端

全新演绎!美团内部疯传Spring Boot速成手册也太香了

Java 程序员 后端

区块链将会深度应用于乡村振兴

CECBC

使用Spring

Java 程序员 后端

ajax跨域问题

加里都好

JavaScript ajax HTTP

活用向量数据库,普通散户也能找到潜力股!

Zilliz

数据库 Milvus 股票

先知道怎么手写一个分页查询,再去使用PageHelper吧

Java 程序员 后端

这本现代魔法原理指南,把计算机体系掰开揉碎讲清楚了

Zilliz

编码

和12岁小同志搞创客开发:手撕代码,点亮LED灯

不脱发的程序猿

少儿编程 智能硬件 创客开发 Arduino

全网最全Spring面试题之基础篇整理总结(共69题,附超详细解答)

Java 程序员 后端

你能读懂微服务架构深度解析:架构设计背后的哲学吗?

Java 程序员 后端

你还在使用 try-catch-finally 关闭资源?

Java 程序员 后端

做一名程序员的基本要求

Java 程序员 后端

先到先得!价值百万的的ELk+Lucene笔记,技术点拉满

Java 程序员 后端

和12岁小同志搞创客开发:手撕代码,Arduino IDE 软件下载和环境搭建

不脱发的程序猿

少儿编程 智能硬件 创客开发 Arduino

你还搞不定生涩难懂的Spring-Aop?梳理完毕以后原来如此简单

Java 程序员 后端

使用Spring Data JPA的Specification构建数据库查询

Java 程序员 后端

全是精华!阿里最新出品的“SpringCloud架构笔记” GitHub已爆火

Java 程序员 后端

使用Redis和Java进行数据库缓存 - DZone数据库

Java 程序员 后端

毕业之后,开源给了我第一份工作

Zilliz

数据库 vim 开源 成长

全网最新最全面Java程序员面试清单(12专题5000解析)

Java 程序员 后端

全网最热Vue入门教程你不看就吃亏了哦

Java 程序员 后端

Redis 6.0 新特性篇:客户端缓存全面揭秘

码哥字节

redis 缓存 NoSQL 数据库 11月日更

使用Spring Cloud Config进行分布式配置:Vault 后端

Java 程序员 后端

Python Qt GUI设计:Python调用UI文件的两种方法(基础篇—3)

不脱发的程序猿

Python qt PyQt 调用UI文件 上位机开发

和12岁小同志搞创客开发:如何驱动 12864 OLED液晶显示屏?

不脱发的程序猿

少儿编程 智能硬件 创客开发 12864 OLED液晶显示屏

全新演绎!美团内部疯传Spring Boot速成手册也太香了(1)

Java 程序员 后端

入秋的第一篇数据结构算法:看看归并与快排的风采

Java 程序员 后端

全文检索工具solr:第一章:理论知识

Java 程序员 后端

难以置信!数字货币袭来!这类人都要富起来了 !

CECBC

35w奖金池,腾讯云TDSQL精英挑战赛正式开赛!

科技热闻

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