记一次Elasticsearch优化总结

10,154 阅读13分钟

目录

背景介绍

JVM知识回顾

ES配置说明回顾

现状分析

调优实战

总结与展望

一. 背景介绍

项目中的服务集成了springboot-admin做服务监控,最近一直收到邮件告警,提示es出错。错误信息如下:

org.elasticsearch.ElasticsearchTimeoutException: java.util.concurrent.TimeoutException: Timeout waiting for task.

频繁收到这个告警,所以决定花时间研究一下。从报错信息看,并发超时异常。ES作为java开发的中间件,我们没有对任何代码做过修改,所以就从JVM开始着手尝试解决,同时还涉及到部分ES知识和springboot的知识。

二. JVM知识回顾

可参考另一篇学习笔记: 深入理解java虚拟机

1. JVM内存模型

  • JVM gc的对象:堆

2. 堆内存

2.1 堆内存划分

  • 堆区分为新生代和老年代
  • 新生代又分为Eden区,from survivor区,to survivor区
  • Eden区和两块较小的survivor空间。大小比例为8:1:1
  • java8已经没有持久代了,改为元数据区,主要存放元数据,例如Class、Method的元信息,与垃圾回收要回收的Java对象关系不大

2.2 堆内存查看

使用 jstat -gc(-gccapacity, -gcutil)命令查看堆分配情况

  • S0C:survivor0区总内存大小(Capacity)
  • S1C: survivor1区总内存大小
  • S0U: survivor0区当前内存大小(Used)
  • S1U: survivor1区当前内存大小
  • EC:Eden区总内存大小
  • EU:Eden区当前内存大小
  • OC:老年代总内存大小
  • OU:老年代当前内存大小
  • MC:meta data区总内存大小
  • MU:meta data区当前内存大小

2.3 内存分配和回收策略

2.3.1 分配策略

  • 大部分对象创建时,在eden区分配
  • 大的对象直接进入老年代,比如很长的字符串或数组。这些对象对垃圾回收不友好。
  • 长期存活的对象,将从新生代晋升到老年代

2.3.2 回收策略

  • eden区满:触发一次minor gc,存活的对象复制到其中一个survivor。对象的年龄+1
  • 一个survivor区满:满足晋升条件的,进入老年代。不满足的,复制到另一个survivor区

2.3.3 晋升条件的判断

  • Serial和ParNew GC中通过MaxTenuringThreshold参数设定,默认为15
  • Parallel收集器自动调整年龄:survivor空间中相同年龄所有对象大小大于空间的一半,大于等于该年龄的对象就直接进入老年代

2.3 关于堆划分的思考

2.3.1 大堆和小堆堆程序的影响

  • 堆太大:垃圾回收时STW的时间过长,影响程序响应时间。据说ZGC(java11发布)回收器能解决这个问题。java11中ZGC的介绍
  • 堆太小:垃圾回收太频繁

2.3.2 为什么要划分为不同的年代

  • 每个对象的生命周期是不一样的,将不同存活时间的对象划分到不同的区,然后采用不同的垃圾回收算法
  • java很多对象都是朝生夕死的,这些对象不会进入老年代。

2.3.3 为什么要有survivor区

  • 没有survivor区,只有eden区的话,每进行一次minor gc,对象就被送入老年代。很容易触发full gc,影响性能
  • survivor存在的目的就是减少送入老年代的对象数量,减少full gc的发生

2.3.4 为什么要设置两个survivor区

每次minor gc,通过将eden和一个survivor的内容复制到另一个survivor, 避免碎片化问题

3. 垃圾回收算法

3.1 标记-清除算法

  • 最基础的收集算法
  • 分为标记和清除两个阶段
  • 不足之处:
    • 效率问题
    • 产生大量不连续的内存碎片

3.2 复制算法

  • 将内存分为大小相等的两块,每次使用其中的一块
  • 一块用完时,将存活的对象复制到另一块
  • 现代虚拟机新生代都用该算法
  • 不足:
    • 内存利用率不高

3.3 标记-整理算法

  • 对象存活率高时大量的复制会影响效率,老年代使用该算法
  • 标记过程与标记-清除算法一样
  • 后续步骤并不是清理对象,而是让所有存活的对象都向一段移动,清理边界以外的内存

3.4 分代收集算法

  • 根据对象存活周期不同,采用不同的收集算法
  • 新生代大量对象死亡,少量存活,采用复制算法
  • 老年代对象存活率高,采用标记-清理或者标记-收集算法

4. 垃圾回收器

4.1 年代划分

  • 新生代收集器有:Serial,ParNew,Paraller Scavenge
  • 老年代收集器有:CMS Serial old,Parallel Old
  • G1收集器可作用与新生代和老年代
  • 没有连线的两个收集器不能共存,比如CMS和Paraller Scavenge

4.2 工作机制划分

  • 串行收集器:Serial,Serial Old,单线程的一个回收器,简单、易实现、效率高
  • 并行收集器:ParNew,Serial的多线程版,可以充分的利用CPU资源,减少回收的时间
  • 吞吐量优先收集器:Parallel Scavenge
  • 并发收集器:CMS(Concurrent Mark Sweep),停顿时间少优先,基于“标记-清除”算法实现。

4.3 其他说明

  • java11 新出了一款ZGC收集器,性能比G1更高效(还在实验阶段)
  • java5默认采用CMS收集器,java9默认收集器被G1代替
  • 用户可自己指定使用哪种垃圾收集器
  • 各个垃圾收集器详细介绍参考深入理解java虚拟机

4.4 CMS工作原理

  • 不会等到老年代空间快满了才回收(和用户线程并发,留内存给用户线程)。配置参数为-XX:CMSInitiazingOccupanyFraction。默认为75%
  • 使用标记-清除算法。整个过程分为四步:
    • 初始标记:STW,标记GC Roots能关联到的对象,速度很快
    • 并发标记:GC Roots Tracing过程。耗时。和用户线程一起执行(并行)
    • 重新标记:STW,标记并发标记过程中程序运行导致标记变化的对象,时间比初始标记长,远比并发标记短
    • 并发清除:耗时。和用户线程一起执行(并行)

三. ES配置说明回顾

可参考另外一篇笔记:Elasticsearch学习笔记

主要介绍es官网手册特别说明的一些注意点

1. 关于配置的说明

1.1 ES使用的垃圾回收器

  • 默认为CMS,2.x版本官方推荐不要修改为G1,某些版本JAVA G1存在的Bug,会造成Lucene的段文件损坏。
  • 不过5.x以及之后版本,没有明确说推荐或不推荐G1,默认还是用的CMS

1.2 ES内存分配要求

  • 不超过32G。因为每个对象的指针都变长了,就会使用更多的 CPU 内存带宽,也就是说你实际上失去了更多的内存。
  • 不要超过内存的一半,因为Lucene也需要内存,且这些内存不被JVM管理
  • 如果不需要对分词做聚合运算,可降低堆内存。堆内存越小,Elasticsearch(更快的 GC)和 Lucene(更多的内存用于缓存)的性能越好。

2. 关于滚动重启的说明

  • 保证不停集群功能的情况下逐一对每个节点进行升级或维护
  • 先停止索引新的数据
  • 禁止分片分配。cluster.routing.allocation.enable" : "none"
    curl -XPUT http://{ip}:9200/_cluster/settings -d'
    {
        "transient" : {
            "cluster.routing.allocation.enable" : "none"
        }
    }'
    
  • 关闭单个节点,并执行升级维护
  • 启动节点,并等待加入集群
  • 重启分片分配。cluster.routing.allocation.enable" : "all"
    curl -XPUT http://{ip}:9200/_cluster/settings -d'
    {
        "transient" : {
            "cluster.routing.allocation.enable" : "all"
        }
    }'
    
  • 对其他节点重复以上步骤
  • 恢复索引更新数据

四. 现状分析

1. 版本及硬件情况介绍

  • java:1.8.0_131
  • elasticsearch:5.5.1
  • es集群:4个数据节点
  • os: centos7 24核 128G
  • 垃圾回收器:老年代(CMS)+ 新生代(ParNew)

2. 目前堆分配情况

要针对jvm调优,必不可少的是先查看堆内存状况,有以下几种查看方法

2.1 jstat -gc命令查看堆分配情况

2.2 统计ES各个节点堆分配信息

节点 堆总大小 新生代 survivor eden 老年代 元数据区
节点A 32G 1.46G 0.146G 1.16G 30.5G 81M
节点B 32G 1.46G 0.146G 1.16G 30.5G 85M
节点C 32G 1.46G 0.146G 1.16G 30.5G 81M
节点D 20G 1.46G 0.146G 1.16G 18.5G 76M

3. 监控工具对比

工具名称 各分区情况 数据是否直观 是否可查看历史数据 是否免费 备注
jstat 主要用于查看各分区大小
ElasticHQ 主要用于浏览es整体信息
cerebro 主要用于浏览es整体信息
x-pack 试用期一年 试用期到相关功能不可用,不影响现有功能。6.3版本x-pack已经开源,后续版本可能会免费
  • 由于线上报异常邮件的时间是不确定的,不可能随时盯着监控面板看,所有必须有查看历史数据的功能,因此x-pack是我们监控的首选工具
  • x-pack监控功能只是其中之一,但是真的非常强大,强烈推荐!!同时期待ES官方尽快使之免费
  • 网上有破解x-pack的方法,将jar包反编译之后修改代码,再打包回去,还没做尝试。

x-pack安装过程的一些小问题总结

第一步:证书申请

curl -XPUT 'http://{ip}:9200/_xpack/license?acknowledge=true' -H "Content-Type: application/json" -d @sivabalan-nagarajan-2327c0fa-f56b-443a-a3d6-abef7ecf2220-v5.json

第二步:安装x-pack插件, 包括es和kibana

./bin/kibana-plugin install x-pack 安装很慢,先把文件下载下来,用下一个命令安装
./bin/elasticsearch-plugin install file:///home/breakpad/softs/x-pack-5.5.1.zip

第三步:修改配置文件

es配置文件里,只启用监控功能

xpack.security.enabled: false
xpack.monitoring.enabled: true
xpack.graph.enabled: false
xpack.watcher.enabled: false

kibana配置文件里,只启用监控功能

xpack.security.enabled: false
xpack.monitoring.enabled: true
xpack.graph.enabled: false
xpack.reporting.enabled: false

报错问题解决

rpm安装后,systemctl方式启动kibana报权限不足的问题?

  1. 卸载x-pack,以kibana用户去安装 sudo -u kibana bin/kibana-plugin install file:///usr/share/kibana/x-pack-5.5.1.zip
  2. 还是报权限错误,修改报错的文件权限都为kibana
  3. 安装包权限报错,修改安装包权限为kibana

kibana启动后网页打不开怎么解决?

  1. 在config/kibana.yml里配置日志路径:logging.dest: /var/log/kibana.log
  2. 修改日志权限 touch /var/log/kibana.log chown kibana:kibana /var/log/kibana.log
  3. 日志也没有错,但是浏览器就是打不开。最后无意间换了个浏览器竟然正常了,再重新把之前打不开的chrome浏览器升级之后,也能正常打开了!!

4. x-pack监控情况分析(以节点B,周期为7天为例)

曲线中每一天大概24个点,即计算的是每个小时的数据

4.1 gc次数:平均值为250次/h 左右

4.2 minor gc耗时:平均值为10000ms/h

4.3 full gc后:剩余堆大小:4.2G,两次full gc的时间分别为2.224s,2.438s

5. 观察到的现象

  • 新生代和老年代分配比例不合理,新生代太小,老年代太大
    • 网上很多文章指出新生代和老年代的默认比例为1:2,但是通过观察发现并不是这样(我们的机器上约是1:20)。
    • 具体原因在网上目前只找到这样一篇文章有过说明。CMS默认新生代是多大?
    • 大致就是:取默认NewRatio计算出来的值和另外一个公式计算出来的值对比,取小的那一个
    • 计算公式为:计算机核数*某个参数(64M)*13/10。我们的机器算出来的值为2G,勉强符合这个说法。
  • 新生代垃圾回收频繁
  • 老年代收集后:有效内存只达4G左右(活跃数据)

6. 针对观察到现象的初步分析和解决

  • 新生代太小:导致minior gc回收频繁,可适当加大新生代大小
  • 老年代太大:导致major gc或full gc回收时间过长,可适当减少老年代大小
  • 如何确定新生代老年代大小:根据美团gc优化实战文章所述:
    • 总大小:3-4倍活跃数据大小:
      • 节点B为4.2G*4,我们设置为20G。
      • 其他机器大概为3.6G*4,我们设置为16G
    • 新生代:1-1.5倍活跃数据大小
    • 老年代:总大小-新生代。综上,我们设置新生代:老年代=1:4

五. 调优实战

通过以上分析发现的问题,然后尝试调整参数,并且观察调整后的监控结果,验证我们的推测是否正确。

1. 修改配置参数

  • 文件为{es_home}/config/jvm.options

2. 修改后的堆配置参数

节点 堆总大小 新生代 survivor eden 老年代 元数据区
节点A 16G 3.2G 320M 2.56G 12.8G 77M
节点B 20G 4G 400M 3.2G 16G 66M
节点C 16G 3.2G 320M 2.56G 12.8G 77M
节点D 16G 3.2G 320M 2.56G 12.8G 77M

3. 修改后的监控信息

3.1 gc次数整体呈下降趋势

3.2 gc耗时整体呈下降趋势,full gc时间大致在900ms左右

4. springboot参数的调整

  • 通过对JVM参数调整后,发现还是有不定时报警的情况。于是研究了一下springboot-admin的原理,它是基于springboot-actuator做了封装,然后看了一下springboot-actuator的原理,找到了其中一个很重要的参数:
    management.health.elasticsearch.response-timeout
    
  • 该参数表示监控程序向es集群发送心跳,允许最大的响应时间的多少? 聪明的你应该明白了,如果发送心跳的时候,es的JVM正在执行垃圾回收,STW导致响应迟迟得不到回复,就会收到邮件告警。它的默认值是100ms,所以必须将该值设置为至少超过minior gc的时间。
  • 但是,如果发送心跳的时候,恰好JVM正在执行full gc,因为STW的时间一般比较长,所以你必然会收到告警邮件,除非你把response-timeout的值设置为比full gc的时间还长
  • 综上分析,需要根据垃圾回收的时间,给该值设置合理的值。

六. 总结与展望

1. 最终优化总结

1.1 关于JVM的调优

  • 减少ES节点分配给JVM的堆大小
  • 调整新生代和老年代的比例

1.2 关于springboot的调优

  • 加大springboot-actuator针对Elasticsearch健康检查时的响应时间(默认为100ms)

2. 展望

  • 业务优化:此次优化仅仅从JVM的角度做了参数调整,es官方文档其实给出很多高效使用es的方法。后续优化可以从业务的角度去分析,包括:
    • 很多不需要分词的字段,都没有做配置,默认都分词了。特别影响性能。
    • 很多查询没有用filter,用了query。无法缓存。
  • 同时,期待将来ZGC投入使用,同时ES很好的兼容ZGC,或许那时就不需要任何调优了(不管多大的堆,gc时间在10ms内)。

七. 参考