ES-调优策略

/ 默认分类分布式 / 没有评论 / 642浏览

1. Index(写)调优

一般数据首先都是进入MySQL集群的,我们从MySQL的原始表里面抽取并存储到 ES 的Index,而MySQL的原始数据也是经常在变化的,所以快速写入Elasticsearch、以保持 Elasticsearch 和 MySQL 的数据及时同步也是很重要的。总结下面几个方面优化来提高写入的速度:

1.1 副本数置0

如果是集群首次灌入数据,可以将副本数设置为0,写入完毕再调整回去,这样副本分片只需要拷 贝,节省了索引过程。

 PUT /my_temp_index/_settings {
"number_of_replicas": 0 }

1.2 自动生成doc ID

通过Elasticsearch写入流程可以看出,写入doc时如果外部指定了id,则Elasticsearch会先尝试 读取原来doc的版本号,以判断是否需要更新。这会涉及一次读取磁盘的操作,通过自动生成doc ID可 以避免这个环节。

1.3 合理设置mappings

将不需要建立索引的字段index属性设置为not_analyzed或no。对字段不分词,或者不索引,可以 减少很多运算操作,降低CPU占用。 尤其是binary类型,默认情况下占用CPU非常高,而这种类 型进行分词通常没有什么意义。 减少字段内容长度,如果原始数据的大段内容无须全部建立 索引,则可以尽量减少不必要的内 容。 使用不同的分析器(analyzer),不同的分析器在索引过程中 运算复杂度也有较大的差异。

1.4 调整_source字段

source doc includes excludes source禁用,一般用于索引和数据分离,这样可以降低 I/O 的压力,不过实际场景中大多不

会禁用_source。

1.5 对analyzed的字段禁用norms Norms用于在搜索时计算doc的评分,如果不需要评分,则可以将其禁用:

  "title": {
  "type": "string",
  "norms": {
    "enabled": false
  }

1.6 调整索引的刷新间隔

该参数缺省是1s,强制ES每秒创建一个新segment,从而保证新写入的数据近实时的可见、可被搜索 到。比如该参数被调整为30s,降低了刷新的次数,把刷新操作消耗的系统资源释放出来给index操作使 用。 PUT /my_index/_settings

{
 "index" : {
      "refresh_interval": "30s"     }
}

这种方案以牺牲可见性的方式,提高了index操作的性能。

1.7 批处理

批处理把多个index操作请求合并到一个batch中去处理,和mysql的jdbc的bacth有类似之处。如图: 将者或,滤 过过通以可,段字的储存要需不分部于对,据数始原储存于用段字

比如每批1000个documents是一个性能比较好的size。每批中多少document条数合适,受很多因素 影响而不同,如单个document的大小等。ES官网建议通过在单个node、单个shard做性能基准测试来 确定这个参数的最优值。

1.8 Document的路由处理 当对一批中的documents进行index操作时,该批index操作所需的线程的个数由要写入的目的shard的个数决定。看下图:

alt

上图中,有2批documents写入ES, 每批都需要写入4个shard,所以总共需要8个线程。如果能减少 shard的个数,那么耗费的线程个数也会减少。例如下图,两批中每批的shard个数都只有2个,总共线 程消耗个数4个,减少一半。 默认的routing就是id routing value put /index/doc/id?routing=user_id 说如比, 个一定指动手,候时的求请送发在以可也, alt 值得注意的是线程数虽然降低了,但是单批的处理耗时可能增加了。和提高刷新间隔方法类似,这有可 能会延长数据不见的时间。

2. Search(读)调优

在存储的Document条数超过10亿条后,我们如何进行搜索调优。

2.1 数据分组

很多人拿ES用来存储日志,日志的索引管理方式一般基于日期的,基于天、周、月、年建索引。如下 图,基于天建索引: 当搜索单天的数据,只需要查询一个索引的shards就可以。当需要查询多天的数据时,需要查询多个索 引的shards。这种方案其实和数据库的分表、分库、分区查询方案相比,思路类似,小数据范围查询而 不是大海捞针。 开始的方案是建一个index,当数据量增大的时候,就扩容增加index的shard的个数。当shards增大 时,要搜索的shards个数也随之显著上升。基于数据分组的思路,可以基于client进行数据分组,每一 个client只需依赖自己的index的数据shards进行搜索,而不是所有的数据shards,大大提高了搜索的性 能,如下图: alt

2.2 使用Filter替代Query

在搜索时候使用Query,需要为Document的相关度打分。使用Filter,没有打分环节处理,做的事情更少,而且filter理论上更快一些。 如果搜索不需要打分,可以直接使用filter查询。如果部分搜索需要打分,建议使用'bool'查询。这种方 式可以把打分的查询和不打分的查询组合在一起使用,如: GET /_search

{
  "query": {
    "bool": {
      "must": {
        "term": {
          "user": "kimchy"
        }
      },
      "filter": {
        "term": {
          "tag": "tech"
          } 
      }
    } 
  }
}

2.3 ID字段定义为keyword

一般情况,如果ID字段不会被用作Range 类型搜索字段,都可以定义成keyword类型。这是因为 keyword会被优化,以便进行terms查询。Integers等数字类的mapping类型,会被优化来进行range类型搜索。将integers改成keyword类型之后,搜索性能大约能提升30%。

2.4 别让用户的无约束的输入拖累了ES集群的性能

注意用户查询输入的条件中夹带了很多'OR'语句以及通配符“*”开头的字符串,可通过对Slow Logs分析发现为了不让用户无约束的查询语句拖累ES集群的查询性能,可以限制用户用来查询的keywords。对于可以用来查询的keyworkds,也可以写成文档来帮助用户更正确的使用。

3 ES 关联关系处理

目前ES主要有以下4种常用的方法来处理数据实体间的关联关系:

3.1 Application-side joins

索引之间完全独立(利于对数据进行标准化处理),由应用端的多次查询来实现近似关联关 系查询。这种方法适用于关联的实体只有少量的文档记录的情况(使用ES的terms查询具有上限,默认 1024,具体可在elasticsearch.yml中修改),并且最好它们很少改变。这将允许应用程序对结果进行缓 存,并避免经常运行第一次查询。

PUT /user/_doc/1
{
  "name":     "John Smith",
  "email":    "john@smith.com",
  "dob":      "1970/10/24"
}
PUT /blogpost/_doc/2
{
  "title":    "Relationships",
  "body":     "It's complicated...",
  "user":     1
}
GET /user/_search
{
  "query": {
    "match": {
    "name": "John"
    } 
  }
}
GET /blogpost/_search
{
  "query": {
      "terms": { "user": [1] }
  } 
}

3.2 Data denormalization(数据的非规范化)

通俗点就是通过字段冗余,以一张大宽表来实现粗粒度的index,这样可以充分发挥扁平化 的优势。但是这是以牺牲索引性能及灵活度为代价的。使用的前提:冗余的字段应该是很少改变的,比 较适合与一对少量关系的处理。当业务数据库并非采用非规范化设计时,这时要将数据同步到作为二级 索引库的ES中,就需要进行定制化开发,基于特定业务进行应用开发来处理join关联和实体拼接。 说明:宽表处理在处理一对多、多对多关系时,会有字段冗余问题,适合“一对少量”且这个“一”更新不 频繁的应用场景。

PUT /user/_doc/1
{
  "name":     "John Smith",
  "email":    "john@smith.com",
  "dob":      "1970/10/24"
}
PUT /blogpost/_doc/2
{
  "title":    "Relationships",
  "body":     "It's complicated...",
  "user":     {
    "id": 1,
    "name":     "John Smith"
  }
}
GET /blogpost/_search
{
  "query": {
    "bool": {
      "must": [
        { "match": { "title":
        { "match": { "user.name": "John"          }
        }
      ] 
      }
  } 
}

3.3 Nested objects(嵌套文档)

索引性能和查询性能二者不可兼得,必须进行取舍。嵌套文档将实体关系嵌套组合在单文档内部,这种 方式牺牲建立索引性能(文档内任一属性变化都需要重新索引该文档)来换取查询性能,比较适合于一 对少量的关系处理。 } } } GET /blogpost/_search { "query": { "terms": { "user": [1] } } } 当使用嵌套文档时,使用通用的查询方式是无法访问到的,必须使用合适的查询方式(nested query、 nested filter、nested facet等),很多场景下,使用嵌套文档的复杂度在于索引阶段对关联关系的组 织拼装。

PUT /drivers
{
    "mappings":{
        "properties":{
            "driver":{
                "type":"nested",
                "properties":{
                    "last_name":{
                        "type":"text"
                    },
                    "vehicle":{
                        "type":"nested",
                        "properties":{
                            "make":{
                                "type":"text"
                            },
                            "model":{
                                "type":"text"
                            }
                        }
                    }
                }
            }
        }
    }
}

PUT /drivers/_doc/1
{
  "driver" : {
        "last_name" : "McQueen",
        "vehicle" : [
            {
                "make" : "Powell Motors",
                "model" : "Canyonero"
            },
            {
                "make" : "Miller-Meteor",
                "model" : "Ecto-1"
            } 
        ]
  }
}
PUT /drivers/_doc/2?refresh
{
  "driver" : {
        "last_name" : "Hudson",
        "vehicle" : [
            {
                "make" : "Mifune",
                "model" : "Mach Five"
            }, 
            {
                "make" : "Miller-Meteor"
                "model" : "Ecto-1"
            }
        ] 
    }
}
GET /drivers/_search
{
    "query":{
        "nested":{
            "path":"driver",
            "query":{
                "nested":{
                    "path":"driver.vehicle",
                    "query":{
                        "bool":{
                            "must":[
                                {
                                    "match":{
                                        "driver.vehicle.make":"Powell Motors"
                                    }
                                },
                                {
                                    "match":{
                                        "driver.vehicle.model":"Canyonero"
                                    }
                                }
                            ]
                        }
                    }
                }
            }
        }
    }
}

3.4 Parent/child relationships(父子文档)

父子文档牺牲了一定的查询性能来换取索引性能,适用于写多读少的场景。父子文档相比嵌套文档较灵 活,适用于“一对大量”且这个“一”不是海量的应用场景,该方式比较耗内存和CPU,这种方式查询比嵌 套方式慢5~10倍,且需要使用特定的has_parent和has_child过滤器查询语法,查询结果不能同时返回 父子文档(一次join查询只能返回一种类型的文档)。受限于父子文档必须在同一分片上(可以通过 routing指定父文档id即可)操作子文档时需要指定routing。

PUT my_index
{
  "mappings": {
    "properties": {
      "my_join_field": {
        "type": "join",
        "relations": {
          "question": "answer"
        }
      } 
    }
  } 
}
# 插入父文档
PUT /my_index/_doc/1?refresh
{
  "text": "This is a question",
  "my_join_field": {
    "name": "question"
  }
}
PUT /my_index/_doc/2?refresh
{
  "text": "This is a question2",
  "my_join_field": "question"
}
# 插入子文档
PUT /my_index/_doc/3?routing=1 {
  "text": "This is an answer",
  "my_join_field": {
    "name": "answer",
    "parent": "1"
  }
}

查询那个文档有子文档

POST my_index/_search
{
  "query": {
    "has_child": {
      "type": "answer",
      "query" : {
        "match": {
          "text" : "this"
        } 
      }
    } 
   }
}

根据父文档id查询子文档

GET my_index/_search
{
  "query": {
    "parent_id": {
      "type": "answer",
      "id": "1" 
      }
  } 
}