Elasticsearch 性能调优

Elasticsearch(ES)作为NOSQL+搜索引擎的有机结合体,不仅有近实时的查询能力,还具有强大的聚合分析能力。本文基于ES 5.6.4,从性能和稳定性两方面,从linux参数调优、ES节点配置和ES使用技巧三个角度入手,介绍ES调优的基本方案。当然,ES的调优绝不能一概而论,需要根据实际业务场景做适当的取舍和调整。

当我们发现es使用还是非常慢,需要优先关注在以下这两类的运行情况。

hot_threads

hot_threadsGET /_nodes/hot_threads&interval=30,抓取30s的节点上占用资源的热线程,并通过排查占用资源最多的TOP线程来判断对应的资源消耗是否正常,一般情况下,bulk,search类的线程占用资源都可能是业务造成的,但是如果是merge线程占用了大量的资源,就应该考虑是不是创建index或者刷磁盘间隔太小,批量写入size太小造成的。

pending_tasks

pending_tasksGET /_cluster/pending_tasks,有一些任务只能由主节点去处理,比如创建一个新的 索引或者在集群中移动分片,由于一个集群中只能有一个主节点,所以只有这一master节点可以处理集群级别的元数据变动。在99.9999%的时间里,这不会有什么问题,元数据变动的队列基本上保持为零。在一些罕见的集群里,元数据变动的次数比主节点能处理的还快,这会导致等待中的操作会累积成队列。这个时候可以通过pending_tasks api分析当前什么操作阻塞了es的队列,比如,集群异常时,会有大量的shard在recovery,如果集群在大量创建新字段,会出现大量的put_mappings的操作,所以正常情况下,需要禁用动态mapping。

目录

1. Linux参数调优

1.1 关闭交换分区

1.2  磁盘挂载选项

1.3 其他

2. ES节点配置

2.1 buffer和bulk队列长度

2.2 新建shard时扫描元数据

2.3  jvm.options

2.4 设置内存熔断参数

2.4 query cache

3. ES使用技巧

3.1 字段的存储

3.2 开启最佳压缩

3.3 bulk

3.4 调整translog同步策略

3.5 调整refresh_interval

3.6 merge并发控制

3.7 不要指定_id

3.8 使用routing

3.9 text or keyword

3.10 使用query-bool-filter组合取代普通query

3.11 index按日期滚动存储

3.12 分片数和副本数按需控制

3.13 Segment Memory优化

 3.14 禁止动态mapping


1. Linux参数调优

1.1 关闭交换分区

防止内存置换降低性能

sed -i '/swap/s/^/#/' /etc/fstab
swapoff -a

1.2  磁盘挂载选项

  • noatime:禁止记录访问时间戳,提高文件系统读写性能

  • data=writeback: 不记录data journal,提高文件系统写入性能

  • barrier=0:barrier保证journal先于data刷到磁盘,上面关闭了journal,这里的barrier也就没必要开启了

  • nobh:关闭buffer_head,防止内核打断大块数据的IO操作

mount -o noatime,data=writeback,barrier=0,nobh /dev/sda /es_data

1.3 其他

# 修改系统资源限制,单用户可以打开的最大文件数量,可以设置为官方推荐的65536或更大些
echo "* - nofile 655360" >>/etc/security/limits.conf

# 单用户内存地址空间
echo "* - as unlimited" >>/etc/security/limits.conf

# 单用户线程数
echo "* - nproc 2056474" >>/etc/security/limits.conf

# 单用户文件大小
echo "* - fsize unlimited" >>/etc/security/limits.conf

# 单用户锁定内存
echo "* - memlock unlimited" >>/etc/security/limits.conf

# 单进程可以使用的最大map内存区域数量
echo "vm.max_map_count = 655300" >>/etc/sysctl.conf

# TCP全连接队列参数设置, 这样设置的目的是防止节点数较多(比如超过100)的ES集群中,节点异常重启时全连接队列在启动瞬间打满,造成节点hang住,整个集群响应迟滞的情况
echo "net.ipv4.tcp_abort_on_overflow = 1" >>/etc/sysctl.conf
echo "net.core.somaxconn = 2048" >>/etc/sysctl.conf

# 降低tcp alive time,防止无效链接占用链接数
echo 300 >/proc/sys/net/ipv4/tcp_keepalive_time

2. ES节点配置

2.1 buffer和bulk队列长度

适当增大写入buffer和bulk队列长度,提高写入性能和稳定性

conf/elasticsearch.yml文件

indices.memory.index_buffer_size: 15%
thread_pool.bulk.queue_size: 1024

2.2 新建shard时扫描元数据

在规模比较大的集群中,可以防止新建shard时扫描所有shard的元数据,提升shard分配速度。

conf/elasticsearch.yml文件

cluster.routing.allocation.disk.include_relocations: false

2.3  jvm.options

-Xms和-Xmx设置为相同的值,推荐设置为机器内存的一半左右,剩余一半留给系统cache使用。

  • jvm内存建议不要低于2G,否则有可能因为内存不足导致ES无法正常启动或OOM
  • jvm建议不要超过32G,否则jvm会禁用内存对象指针压缩技术,造成内存浪费

2.4 设置内存熔断参数

设置内存熔断参数,防止写入或查询压力过高导致OOM,具体数值可根据使用场景调整。

conf/elasticsearch.yml文件

indices.breaker.total.limit: 30%
indices.breaker.request.limit: 6%
indices.breaker.fielddata.limit: 3%

2.4 query cache

调小查询使用的cache,避免cache占用过多的jvm内存,具体数值可根据使用场景调整。

conf/elasticsearch.yml文件

indices.queries.cache.count: 500
indices.queries.cache.size: 5%

3. ES使用技巧

3.1 字段的存储

ES底层使用Lucene存储数据,主要包括行存(StoreFiled)、fielddata、列存(DocValues)和倒排索引(InvertIndex)。大多数使用场景中,没有必要同时存储这四个部分。

当前用得最多的就是doc_values,列存储,对于不需要进行分词的字段,都可以开启doc_values来进行存储(且只保留keyword字段),节约内存,当然,开启doc_values会对查询性能有一定的影响,但是,这个性能损耗是比较小的,而且是值得的。

可以通过下面的参数来做适当调整:

StoreFiled

行存,其中占比最大的是_source字段,它控制doc原始数据的存储。在写入数据时,ES把doc原始数据的整个json结构体当做一个string,存储为source字段。查询时,可以通过source字段拿到当初写入时的整个json结构体。 所以,如果没有取出整个原始json结构体的需求,可以通过下面的命令,在mapping中关闭source字段或者只在source中存储部分字段,数据查询时仍可通过ES的docvaluefields获取所有字段的值。

注意:关闭source后, update, updatebyquery, reindex等接口将无法正常使用,所以有update等需求的index不能关闭source。

// 关闭 _source
PUT my_index 
{
    "mappings": {
        "my_type": {
            "_source": {
                "enabled": false
            }
        }
    }
}

// _source只存储部分字段,通过includes指定要存储的字段或者通过excludes滤除不需要的字段
PUT my_index
{
    "mappings": {
        "_doc": {
            "_source": {
                "includes": [
                    "*.count",
                    "meta.*"
                ],
                "excludes": [
                    "meta.description",
                    "meta.other.*"
                ]
            }
        }
    }
}

fielddata构建和管理 100% 在内存中,常驻于 JVM 内存堆,所以可用于快速查询,但是这也意味着它本质上是不可扩展的,有很多边缘情况下要提防,如果对于字段没有分析需求,可以关闭fielddata
docvalues

控制列存。ES主要使用列存来支持sorting, aggregations和scripts功能,对于没有上述需求的字段,可以通过下面的命令关闭docvalues,降低存储成本。

PUT my_index
{
    "mappings": {
        "my_type": {
            "properties": {
                "session_id": {
                    "type": "keyword",
                    "doc_values": false
                }
            }
        }
    }
}

index

控制倒排索引。ES默认对于所有字段都开启了倒排索引,用于查询。对于没有查询需求的字段,可以通过下面的命令关闭倒排索引。

  • all:ES的一个特殊的字段,ES把用户写入json的所有字段值拼接成一个字符串后,做分词,然后保存倒排索引,用于支持整个json的全文检索。这种需求适用的场景较少,可以通过下面的命令将all字段关闭,节约存储成本和cpu开销。(ES 6.0+以上的版本不再支持_all字段,不需要设置)
  • fieldnames:该字段用于exists查询,来确认某个doc里面有无一个字段存在。若没有这种需求,可以将其关闭

PUT my_index
{
    "mappings": {
        "my_type": {
            "properties": {
                "session_id": {
                    "type": "keyword",
                    "index": false
                }
            }
        }
    }
}
PUT my_index
{
    "mapping": {
        "my_type": {
            "_all": {
                "enabled": false
            }
        }
    }
}
PUT my_index
{
    "mapping": {
        "my_type": {
            "_field_names": {
                "enabled": false
            }
        }
    }
}

3.2 开启最佳压缩

对于_source字段,可以通过下面的命令来把lucene适用的压缩算法替换成 DEFLATE,提高数据压缩率

PUT /my_index/_settings
{
    "index.codec": "best_compression"
}

3.3 bulk

写入数据时尽量使用下面的bulk接口批量写入,提高写入效率。每个bulk请求的doc数量设定区间推荐为1k~1w,具体可根据业务场景选取一个适当的数量。

3.4 调整translog同步策略

Elasticsearch 2.0之后为了保证不丢数据,translog的持久化策略是,对于每个 index、bulk、delete、update请求都做一次flush(刷新translog数据到磁盘上)。这种频繁的磁盘IO操作是严重影响写入性能的,如果可以接受一定概率的数据丢失(这种硬件故障的概率很小),可以通过下面的命令调整 translog 持久化策略为异步周期性执行,并适当调整translog的刷盘周期。

PUT my_index
{
    "settings": {
        "index": {
            "translog": {
                "sync_interval": "5s",
                "durability": "async"
            }
        }
    }
}

3.5 调整refresh_interval

写入Lucene的数据,并不是实时可搜索的,ES必须通过refresh的过程把内存中的数据转换成Lucene的完整segment后,才可以被搜索。

要不要秒级响应?最快1s(index.refresh_interval【默认为一秒】)写入的数据可以被查询到,势必会产生大量的segment,检索性能会受到影响。所以,非实时的场景可以调大,设置为30s,降低系统开销。

3.6 merge并发控制

ES的一个index由多个shard组成,而一个shard其实就是一个Lucene的index,它又由多个segment组成,且Lucene会不断地把一些小的segment合并成一个大的segment,这个过程被称为段merge。执行索引操作时,ES会先生成小的segment,ES有离线的逻辑对小的segment进行合并,优化查询性能。但是合并过程中会消耗较多磁盘IO,会影响查询性能。

index.merge.scheduler.max_thread_count控制并发的merge线程数,如果存储是并发性能较好的SSD,可以用系统默认的max(1, min(4, availableProcessors / 2)),当节点配置的cpu核数较高时,merge占用的资源可能会偏高,影响集群的性能,普通磁盘的话设为1。可以通过下面的命令调整某个index的merge过程的并发度:

PUT /my_index/_settings
{
    "index.merge.scheduler.max_thread_count": 2
}

3.7 不要指定_id

当用户显示指定id写入数据时,ES会先发起查询来确定index中是否已经有相同id的doc存在,若有则先删除原有doc再写入新doc。这样每次写入时,ES都会耗费一定的资源做查询。如果用户写入数据时不指定doc,ES则通过内部算法产生一个随机的id,并且保证id的唯一性,这样就可以跳过前面查询id的步骤,提高写入效率。所以,在不需要通过id字段去重、update的使用场景中,写入不指定id可以提升写入速率。基础架构部数据库团队的测试结果显示,无id的数据写入性能可能比有_id的高出近一倍,实际损耗和具体测试场景相关。

3.8 使用routing

对于数据量较大的index,一般会配置多个shard来分摊压力。这种场景下,一个查询会同时搜索所有的shard,然后再将各个shard的结果合并后,返回给用户。对于高并发的小查询场景,每个分片通常仅抓取极少量数据,此时查询过程中的调度开销远大于实际读取数据的开销,且查询速度取决于最慢的一个分片。开启routing功能后,ES会将routing相同的数据写入到同一个分片中(也可以是多个,由index.routingpartitionsize参数控制)。如果查询时指定routing,那么ES只会查询routing指向的那个分片,可显著降低调度开销,提升查询效率。

// 写入
PUT my_index/my_type/1?routing=user1
{
    "title": "This is a document"
}
//查询
GET my_index/_search?routing=user1,user2 
{
    "query": {
        "match": {
            "title": "document"
        }
    }
}

3.9 text or keyword

为string类型的字段选取合适的存储方式,text或者keywork类型

3.10 使用query-bool-filter组合取代普通query

默认情况下,ES通过一定的算法计算返回的每条数据与查询语句的相关度,并通过score字段来表征。但对于非全文索引的使用场景,用户并不care查询结果与查询条件的相关度,只是想精确的查找目标数据。此时,可以通过query-bool-filter组合来让ES不计算score,并且尽可能的缓存filter的结果集,供后续包含相同filter的查询使用,提高查询效率。

// 普通查询
POST my_index/_search
{
    "query": {
        "term": {
            "user": "Kimchy"
        }
    }
}
// query-bool-filter 加速查询
POST my_index/_search
{
    "query": {
        "bool": {
            "filter": {
                "term": {
                    "user": "Kimchy"
                }
            }
        }
    }
}

3.11 index按日期滚动存储

写入ES的数据最好通过某种方式做分割,存入不同的index。常见的做法是将数据按模块/功能分类,写入不同的index,然后按照时间去滚动生成index。这样做的好处是各种数据分开管理不会混淆,也易于提高查询效率。同时index按时间滚动,数据过期时删除整个index,要比一条条删除数据或deletebyquery效率高很多,因为删除整个index是直接删除底层文件,而deletebyquery是查询-标记-删除。

// module_a
PUT module_a@2018_01_01
{
   "settings" : {
       "index" : {
           "number_of_shards" : 3,
           "number_of_replicas" : 2
       }
   }
}
PUT module_a@2018_01_02
{
   "settings" : {
       "index" : {
           "number_of_shards" : 3,
           "number_of_replicas" : 2
       }
   }
}
GET module_a@*/_search

// module_b
PUT module_b@2018_01_01
{
   "settings" : {
       "index" : {
           "number_of_shards" : 3,
           "number_of_replicas" : 2
       }
   }
}
PUT module_b@2018_01_02
{
   "settings" : {
       "index" : {
           "number_of_shards" : 3,
           "number_of_replicas" : 2
       }
   }
}
GET module_b@*/_search

3.12 分片数和副本数按需控制

对于每个index的shard数量,可以根据数据总量、写入压力、节点数量等综合考量后设定,然后根据数据增长状态定期检测下shard数量是否合理。多少合适

3.13 Segment Memory优化

ES底层采用Lucene做存储,而Lucene的一个index又由若干segment组成,每个segment都会建立自己的倒排索引用于数据查询。Lucene为了加速查询,为每个segment的倒排做了一层前缀索引,这个索引在Lucene4.0以后采用的数据结构是FST (Finite State Transducer)。Lucene加载segment的时候将其全量装载到内存中,加快查询速度。这部分内存被称为SegmentMemory, 常驻内存,占用heap,无法被GC。前面提到,为利用JVM的对象指针压缩技术来节约内存,通常建议JVM内存分配不要超过32G。当集群的数据量过大时,SegmentMemory会吃掉大量的堆内存,而JVM内存空间又有限,此时就需要想办法降低SegmentMemory的使用量了,常用方法有下面几个:

  • 定期删除不使用的index
  • 对于不常访问的index,可以通过close接口将其关闭,用到时再打开
  • 通过force_merge接口强制合并segment,降低segment数量

 3.14 禁止动态mapping

动态mapping的坏处:

  1. 造成集群元数据一直变更,导致集群不稳定
  2. 可能造成数据类型与实际类型不一致
  3. 对于一些异常字段或者是扫描类的字段,也会频繁的修改mapping,导致业务不可控
  • 7
    点赞
  • 31
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值