性能调优
Elasticsearch 性能调优¶
从linux参数调优、ES节点配置和ES使用技巧三个角度入手,介绍ES调优的基本方案。
当我们发现es使用还是非常慢,需要优先关注在以下这两类的运行情况:
- hot_threads hot_threads(GET /_nodes/hot_threads&interval=30),抓取30s的节点上占用资源的热线程,并通过排查占用资源最多的TOP线程来判断对应的资源消耗是否正常,一般情况下,bulk,search类的线程占用资源都可能是业务造成的,但是如果是merge线程占用了大量的资源,就应该考虑是不是创建index或者刷磁盘间隔太小,批量写入size太小造成的。
- pending_tasks pending_tasks(GET /_cluster/pending_tasks),有一些任务只能由主节点去处理,比如创建一个新的 索引或者在集群中移动分片,由于一个集群中只能有一个主节点,所以只有这一master节点可以处理集群级别的元数据变动。在99.9999%的时间里,这不会有什么问题,元数据变动的队列基本上保持为零。在一些罕见的集群里,元数据变动的次数比主节点能处理的还快,这会导致等待中的操作会累积成队列。这个时候可以通过pending_tasks api分析当前什么操作阻塞了es的队列,比如,集群异常时,会有大量的shard在recovery,如果集群在大量创建新字段,会出现大量的put_mappings的操作,所以正常情况下,需要禁用动态mapping。
Linux参数调优¶
关闭交换分区¶
防止内存置换降低性能
sed -i '/swap/s/^/#/' /etc/fstab swapoff -a
磁盘挂载选项¶
-
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
其他¶
// 修改系统资源限制,单用户可以打开的最大文件数量,可以设置为官方推荐的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
ES节点配置¶
buffer和bulk队列长度¶
适当增大写入buffer和bulk队列长度,提高写入性能和稳定性
# cat conf/elasticsearch.yml
indices.memory.index_buffer_size: 15%
thread_pool.bulk.queue_size: 1024
新建shard时扫描元数据¶
在规模比较大的集群中,可以防止新建shard时扫描所有shard的元数据,提升shard分配速度。
jvm.options¶
-Xms和-Xmx设置为相同的值,推荐设置为机器内存的一半左右,剩余一半留给系统cache使用。
- jvm内存建议不要低于2G,否则有可能因为内存不足导致ES无法正常启动或OOM
- jvm建议不要超过32G,否则jvm会禁用内存对象指针压缩技术,造成内存浪费
设置内存熔断参数¶
设置内存熔断参数,防止写入或查询压力过高导致OOM,具体数值可根据使用场景调整。
// cat conf/elasticsearch.yml
indices.breaker.total.limit: 30%
indices.breaker.request.limit: 6%
indices.breaker.fielddata.limit: 3%
query cache¶
调小查询使用的cache,避免cache占用过多的jvm内存,具体数值可根据使用场景调整。
ES使用技巧¶
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
}
}
}
}
开启最佳压缩¶
对于_source字段,可以通过下面的命令来把lucene适用的压缩算法替换成 DEFLATE,提高数据压缩率
bulk¶
写入数据时尽量使用下面的bulk接口批量写入,提高写入效率。每个bulk请求的doc数量设定区间推荐为1k~1w,具体可根据业务场景选取一个适当的数量。
调整translog同步策略¶
为了保证不丢数据,translog的持久化策略是,对于每个 index、bulk、delete、update请求都做一次flush(刷新translog数据到磁盘上)。这种频繁的磁盘IO操作是严重影响写入性能的,如果可以接受一定概率的数据丢失(这种硬件故障的概率很小),可以通过下面的命令调整 translog 持久化策略为异步周期性执行,并适当调整translog的刷盘周期。
PUT my_index
{
"settings": {
"index": {
"translog": {
"sync_interval": "5s",
"durability": "async"
}
}
}
}
调整refresh_interval¶
写入Lucene的数据,并不是实时可搜索的,ES必须通过refresh的过程把内存中的数据转换成Lucene的完整segment后,才可以被搜索。
要不要秒级响应?最快1s(index.refresh_interval【默认为一秒】)写入的数据可以被查询到,势必会产生大量的segment,检索性能会受到影响。所以,非实时的场景可以调大,设置为30s,降低系统开销。
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过程的并发度:
不要指定_id¶
当用户显示指定id写入数据时,ES会先发起查询来确定index中是否已经有相同id的doc存在,若有则先删除原有doc再写入新doc。这样每次写入时,ES都会耗费一定的资源做查询。如果用户写入数据时不指定doc,ES则通过内部算法产生一个随机的id,并且保证id的唯一性,这样就可以跳过前面查询id的步骤,提高写入效率。所以,在不需要通过id字段去重、update的使用场景中,写入不指定id可以提升写入速率。基础架构部数据库团队的测试结果显示,无id的数据写入性能可能比有_id的高出近一倍,实际损耗和具体测试场景相关。
使用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"
}
}
}
text or keyword¶
为string类型的字段选取合适的存储方式,text或者keywork类型。
使用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"
}
}
}
}
}
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
分片数和副本数按需控制¶
对于每个index的shard数量,可以根据数据总量、写入压力、节点数量等综合考量后设定,然后根据数据增长状态定期检测下shard数量是否合理。多少合适?
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数量
禁止动态mapping¶
动态mapping的坏处:
- 造成集群元数据一直变更,导致集群不稳定
- 可能造成数据类型与实际类型不一致
- 对于一些异常字段或者是扫描类的字段,也会频繁的修改mapping,导致业务不可控