记一次ES写入优化
背景:
我们系统通过ES存储告警数据,性能测试过程中测试反馈有一台单机环境ES数据有延迟,随着运行时间增长,延迟越来越明显,影响了正常功能需要解决。于是我们开始对整个数据出流程进行分析
定位问题:
第一反应系统可能是因为在进行性能测试数据量比较大某个数据处理环节性能达不到出现了数据堆积,于是对数据处理的流水线进行分析,查看了数据采集组件、数据标准化模块、数据分析组件、关联分析组件,发现数据处理都没有延迟,数据写入kafka也很正常没有时延,最后发现是数据写入ES时候出现了延迟。通过查看kafka消费者状态
消费者lag非常多,消费能力不足,导致数据延迟,现象也很明显,消费者速度跟不上生成数据速度导致数据积压发生延迟。消费者处理数据流程是将数据按条件进行聚合然后写入ES,通过日志分析发现,聚合处理速度非常快无性能问题,而聚合后数据写入ES占据了大部分的数据处理时间,基本可以确定是数据写入ES速度较慢,导致数据积压,时间延迟。
优化过程:
ES数据写入是既涉及到CPU的计算也涉及到IO处理。首先考虑从代码层面进行优化
- 使用批量写入。ES批量写入性能比单条写入性能更高,当有大量数据写入ES的时候应该使用bulk api写入数据,查看代码发现已经使用了bulk写入数据。针对批量请求合理的控制每个批量请求大小,虽然批量请求能提高效率,但过大的批请求可能会导致内存问题或阻塞。根据集群性能和节点配置,通常建议一个请求包含数千条记录。
- 减少锁的粒度。因为告警数据写入ES后支持修改、删除和重计算逻辑,数据聚合生成告警数据过程需要保证安全性,原本会通过redis实现一个全局锁来保证整个数据聚合处理的独占性,如果正在对告警数据进行其他操作时会导致聚合处理逻辑阻塞
根据业务逻辑分析告警都是以资产为单位,在处理、重计算的时候也是根据资产进行处理,于是我们对之前的全局锁进行优化,修改为以资产为维度进行加锁,这样可以缩小锁的范围,减少数据处理阻塞的时间 - 使用线程池。将原来数据ES写入逻辑改为多线程处理,我们将数据进行分批,交给不同的线程池进行写入,经过测试性能有所提示,但是在测试过程中发现服务有出现内存溢出问题,当前平台内存仍有剩余,经过评估可以适当扩大内存,长稳测试后服务运行正常
- 使用缓存(经过评估方案没有落地)。数据流经过关联分析后会生成一次安全事件,但是由于数据流中会存在相同时间相同类型等条件的重复一次告警,不利于当前状态展示。于是对一次告警进行二次分组聚合,聚合后的二次告警会先查询ES中是否已经存在,如果存在则对历史数据部分字段进行修改,如果不存在则直接写入ES。在一次告警比较少时无明显影响,但是当一次告警比较多的时候每一条告警都需要查询一次ES影响较大。单条查询几毫秒-几十毫秒之间,但每批数据量在2000条左右,整个处理流程需要几十秒,导致数据积压。每条数据既需要对ES进行查询操作也要进行写入操作,肯定会影响写入性能。那哪种技术适合当前场景呢,首先考虑到使用布隆过滤器判断ES中是否已经存在,不用直接查询ES,但是我们的需求有过期数据清理的逻辑,而布隆过滤器对删除支持不好,如果支持删除需要使用计数布隆过滤器,但是空间使用效率不是很高。经过调研可以使用布谷鸟过滤器,对删除支持比较好,空间使用效率也比较高,实现方案其实也是通过redis实现。
ES配置调整 - 修改elasticsearch.yml配置。根据自己的环境进行修改,我们主要修改了两个参数
thread_pool.bulk.size
thread_pool.bulk.queue_size
- 修改jvm.options。同样根据自己的环境进行配置,我们主要是修改了,针对netty的优化
-Dio.netty.noUnsafe
-Dio.netty.noKeySetOptimization
-Dio.netty.recycler.maxCapacityPerThread
上面两种参数配置在我们环境修改后有所提升但是不太明显,可以根据自己情况进行调整
ES索引配置调整
- 修改索引的refresh间隔
curl -XPUT "http://elastic1:9200/index_name/_settings?timeout=60s" -H 'Content-Type: application/json' -d '
{
"index": {
"refresh_interval": "5s"
}
}'
ES中有个很重要的概念就是refresh,数据写入内存需要执行refresh操作后才能变为被搜索到,默认情况下ES会每秒执行一次自动刷新,这些在内存中的数据会被写入一个新的段,新写入的数据会在1秒钟内变为可搜索的,这个过程就是refresh。因为refresh会带来I/O操作和CPU负载,频繁的刷新也会影响索引性能,如果是不需要近实时的搜索场景可以将refresh适当放大一点。在我们系统中聚合数据写入ES后会通知下游处理逻辑,为了数据可见性我们将数据refresh间隔设置为5s,并且会延迟5s后发送消息通知下游组件防止数据查询不到。
- 修改索引translog提交方式为异步提交
ES通过translog来保证操作不丢失也叫事务日志,translog在索引恢复的时候也可以缩短恢复时间,在每一次对 ES进行操作时均进行了日志记录,translog是顺序写入的所以写入性能很高。一个文档在写入索引之后,就会被添加到内存缓冲区,并且追加到translog中,每隔一段时间随着translog文件越来越大,一个新的translog文件会被创建并执行一次全量的提交及将内存中的数据写入磁盘。在translog文件被 fsync 到磁盘前,如果发生重启则被写入的文件就会丢失,因此如果你的系统对于丢失 sync_interval 时间段的数据也无所谓,那将translog的刷盘操作修改为异步对写入性能提高很有帮助,虽然sync_interval设置的很小,但是如果是批量写入数据系统宕机情况下丢失数据量也是很大的,需要慎重依据自己的场景使用,避免数据丢失。
curl -XPUT "http://elastic1:9200/index_name/_settings?timeout=60s" -H 'Content-Type: application/json' -d '
{
"index": {
"translog.durability": "async",
"translog.sync_interval": "1s"
}
}'
选择合适的I/O调度算法
我们使用工具对操作系统运行状态进行分析:服务器CPU内存磁盘运行指标监控工具
内存使用正常,cpu使用率正常,但是可以看到有大量的iowait说明磁盘写入有性能瓶颈,查看磁盘的调度算法
cat /sys/block/vdb/queue/scheduler
使用分析工具对none和deadline两种调度算法进行采样
然后对比两种不同调度算法I/O数据,两种调度算法结果如下:
none调度算法:
deadline调度算法:
经过对比我们发现
- BLKavg: 从平均块大小看,Deadline 的 228 比 None 的 136 大,这表示每次合并的请求更大
- NSEEKS: Deadline 调度的寻道次数为 891384,略少于 None 的 1187328。
- MODE: 空闲模式的频率(0)在 Deadline 中下降至 858293,表明更多请求是顺序或近似顺序的
- NSEEKS: Deadline 的寻道次数是 791544,比 None 的 1179096 少,显示出在物理层面请求的有效调度
总结:
Deadline 调度算法倾向于通过增加合并和较大块的请求来提高效率,同时在一些情况下可能导致队列等待时间的增大。Deadline 调度着重于减少 I/O 请求延迟、优化吞吐量,并保持系统的响应速度,在可能增加一定排队延迟的情况下提升整体 I/O 性能。