1.一般建议

  a.不要返回过大的结果集。这个建议对一般数据库都是适用的,如果要获取大量结果,可以使用search_after api,或者scroll (新版本中已经不推荐)。

  b.避免大的文档。

2. 如何提高索引速度

  a.使用批量请求。为了达到最好的效果,可以进行测试,递增地提高bulk的数量,比如从100,到200,再到400,达到一个吞吐量和响应时间的平衡。

  b.使用多线程发送数据。

  c.关闭或者减小refresh_interval。从内存缓存写入磁盘缓存(memorybuffer -> filesystem cache),这个过程叫做refresh。在这个过程之前内存缓存里面的文档是不可被搜索的,这也是为什么es被称为近实时索引的原因。

    在索引初始化(大量导入文档)的时候,可以关闭refresh_interval。当产品允许较大的不可搜索时间,可以将index.refresh_interval设置为30s,提高索引速度。

  d.初始化时关闭复制分片。索引时设置index.number_of_replicas为0,避免主分片复制数据,索引完毕后再调整到正常的复制分片数。

  e.关闭swapping。swap会极大地降低es的索引速度。

Swap分区(即交换区)在系统的物理内存不够用的时候,把硬盘空间中的一部分空间释放出来,以供当前运行的程序使用。那些被释放的空间可能来自一些很长时间没有什么操作的程序,这些被释放的空间被临时保存到Swap分区中,等到那些程序要运行时,再从Swap分区中恢复保存的数据到内存中。

  f.给文件系统缓存分配足够多的内存。文件系统换行用来处理io操作,至少要将物理机一半的内存分配给文件系统缓存。比如物理机内存64g,那么至少分配32g给文件系统缓存,剩下的内存才考虑分配给es。

  g.使用自动生成的id。如果使用指定的id,es会检查这个id是否已经存在,而且随着文档数越多,这个判重操作越耗时。索引的时候,如果没有指定id,es会自动生成id。

{    "_index": "sales",    "_type": "_doc",    "_id": "xb7IY4cB6Rdc8HbDycuE", // auto-generated id    "_version": 1,    "result": "created",    "_shards": {        "total": 2,        "successful": 1,        "failed": 0    },    "_seq_no": 10,    "_primary_term": 1}

  h.使用更好的硬件。比如SSD,或者Amazon的Elastic Block Storage。

  i.调整索引缓存大小。确保每个索引分片能获得512M的缓存,即indices.memory.index_buffer_size= 512M,大于512M没有更多提升效果。

  j.使用cross-cluster replication来实现读写分离,这样让索引集群压力更小。这和mysql中的读写分离很类似。

3.如何提到搜索速度

  a.给文件系统缓存分配足够多的内存。

  b.在linux环境中设置合适的readahead。但是es中的查询更多的是随机io,过大的readahead反而使文件系统的页缓存严重抖动,从而使查询性能下降。

Linux的文件预读readahead,指Linux系统内核将指定文件的某区域预读进页缓存起来,便于接下来对该区域进行读取时,不会因缺页(page fault)而阻塞。因为从内存读取比从磁盘读取要快很多。预读可以有效的减少磁盘的寻道次数和应用程序的I/O等待时间,是改进磁盘读I/O性能的重要优化手段之一。使用命令lsblk查看readahead值。

  c.使用更好的硬件。

  d.好的文档模型。酌情使用nested query, parent query, 避免使用join query。

文档模型对比普通查询
nested query慢几倍
parent query慢几百倍
join query应当避免

  e.尽可能少的查询字段。在越多的字段上匹配,查询速度就越慢。在索引的时候可以将需要查询的多个字段聚合到一个字段中。使用copy_to 可以自动实现这一功能,以下示例将name和plot字段聚合到name_and_plot字段中。

PUT movies{  "mappings": {    "properties": {      "name_and_plot": {        "type": "text"      },      "name": {        "type": "text",        "copy_to": "name_and_plot"      },      "plot": {        "type": "text",        "copy_to": "name_and_plot"      }    }  }}

  f.预先索引数据。比如如果想对price字段做range聚合,那么预先计算出单个文档的price范围,那么就能将range聚合转化成terms聚合。这样确实能提高效率,但是不太灵活。

插入文档:

PUT index/_doc/1{  "designation": "spoon",  "price": 13}

range聚合查询:

GET index/_search{  "aggs": {    "price_ranges": {      "range": {        "field": "price",        "ranges": [          { "to": 10 },          { "from": 10, "to": 100 },          { "from": 100 }        ]      }    }  }}

另一种做法,预先计算price_range:

PUT index{  "mappings": {    "properties": {      "price_range": {        "type": "keyword"      }    }  }}PUT index/_doc/1{  "designation": "spoon",  "price": 13,  "price_range": "10-100"}

使用terms聚合:

GET index/_search{  "aggs": {    "price_ranges": {      "terms": {        "field": "price_range"      }    }  }}

  g.尽可能将字段自定义为keyword。对于数字类型的字段,es对其range查询做了优化。在term层级的查询下,keyword字段比数字类型要好。

    在以下两种情况下可以考虑将数字类型定义为keyword:

      1.不需要对这些数据进行range查询

      2.有很高的查询速度要求。

    如果实在不清楚哪个好,可以用multi-field为数字类型的字段同时定义数字类型和keyword类型。

  h.避免使用脚本。如果可能,避免使用脚本排序,使用脚本聚合,以及script_scorequery。

  i.使用四舍五入的日期。这样有助于es进行缓存,精确到秒级别的查询有时候并无必要。

实时查询(秒级):

PUT index/_doc/1{  "my_date": "2016-05-11T16:30:55.328Z"}GET index/_search{  "query": {    "constant_score": {      "filter": {        "range": {          "my_date": {            "gte": "now-1h",            "lte": "now"          }        }      }    }  }}

分钟级查询:

GET index/_search{  "query": {    "constant_score": {      "filter": {        "range": {          "my_date": {            "gte": "now-1h/m",            "lte": "now/m"          }        }      }    }  }}

  j.对只读索引进行force-merge。在时序索引中,过期的索引都是只读的,将其合并成一个段能加快查询速度。

  k.预热global ordinals。ordinals 是doc values的具体存储形式。一般情况下一个字段的global ordinals是懒加载的。如果某个字段在聚合上用到很多,我们可以先将其预热(加载到heap),当做field data cache.的一部分。

PUT index{  "mappings": {    "properties": {      "foo": {        "type": "keyword",        "eager_global_ordinals": true      }    }  }}

  l.预热文件系统缓存。设置index.store.preload参数即可。注意,必须确保文件系统缓存足够大,否则会让查询变得更慢。

  m.使用索引排序来加速连接查询。比如我们要进行过滤 a AND b AND …​,然后a是low-cardinality(低区分度)。那么我们可以先对a进行排序,那么一旦a的某个值不匹配这个表达式,那么有相同的值的文档都可以跳过。

  n.使用preference进行缓存使用优化。es中有非常多的缓存,比如文件系统缓存(最重要),请求缓存,查询缓存,但是这些缓存都是在节点层面。默认情况下es会使用round-robin算法分配查询到不同的分片上去,这样缓存就失效了。

    如果可以,使用preference参数将用户的请求和对应的分片或者节点绑定起来,这样缓存就不会失效。例如:

GET /_search?preference=_shards:2,3{    "query": {        "match": {            "title": "elasticsearch"        }    }}

  o.更多的复制分片会提升吞吐量(但并不一定)。在系统资源充足的情况下,复制分片越多吞吐量会越高。但是过多的分片会让故障恢复变得更慢。

  p.使用profile api优化查询语句。和mysql中的explain类似,例如:

GET /my-index-000001/_search{  "profile": true,  "query" : {    "match" : { "message" : "GET /search" }  }}{  "took": 25,  "timed_out": false,  "_shards": {    "total": 1,    "successful": 1,    "skipped": 0,    "failed": 0  },  "hits": {    "total": {      "value": 5,      "relation": "eq"    },    "max_score": 0.17402273,    "hits": [...]   },  "profile": {    "shards": [      {        "id": "[2aE02wS1R8q_QFnYu6vDVQ][my-index-000001][0]",        "searches": [          {            "query": [              {                "type": "BooleanQuery",                "description": "message:get message:search",                "time_in_nanos" : 11972972,                "breakdown" : {                  "set_min_competitive_score_count": 0,                  "match_count": 5,                  "shallow_advance_count": 0,                  "set_min_competitive_score": 0,                  "next_doc": 39022,                  "match": 4456,                  "next_doc_count": 5,                  "score_count": 5,                  "compute_max_score_count": 0,                  "compute_max_score": 0,                  "advance": 84525,                  "advance_count": 1,                  "score": 37779,                  "build_scorer_count": 2,                  "create_weight": 4694895,                  "shallow_advance": 0,                  "create_weight_count": 1,                  "build_scorer": 7112295                },...

  q.使用index_phrases 加速phrase query。index_phrases,会将两个单词的组合单独索引,这样可以加速phrase query。

  r.使用index_phrases 加速prefix query。同上。

  s.使用constant_keyword加速过滤。如果某个字段的大多数情况下的值是个常量,但是我们又经常要对其进行过滤,我们可以将其拆分成两个索引,一个使用constant_keyword,一个不使用。

mapping如下:

UT bicycles{  "mappings": {    "properties": {      "cycle_type": {        "type": "constant_keyword",        "value": "bicycle"      },      "name": {        "type": "text"      }    }  }}PUT other_cycles{  "mappings": {    "properties": {      "cycle_type": {        "type": "keyword"      },      "name": {        "type": "text"      }    }  }}

查询语句:

GET bicycles,other_cycles/_search{  "query": {    "bool": {      "must": {        "match": {          "description": "dutch"        }      },      "filter": {        "term": {          "cycle_type": "bicycle"        }      }    }  }}

在查询bicycles索引时,es会将查询语句自动转换为:

GET bicycles,other_cycles/_search{  "query": {    "match": {      "description": "dutch"    }  }}

4.磁盘优化

  a.禁用不需要的特性。

    比如数字类型的字段如果不需要进行过滤,可以不对其进行索引。

PUT index{  "mappings": {    "properties": {      "foo": {        "type": "integer",        "index": false      }    }  }}

    es会对text类型的字段存储一些打分信息,如果不需要对这些字段进行打分,可以将其设置为match_only_text类型

  b.不要使用默认动态字符串映射。默认动态字符串映射会将字符串类型映射为text和keyword类型,这样很浪费空间。可以预先配置所有字符串映射类型为keyword。

PUT index{  "mappings": {    "dynamic_templates": [      {        "strings": {          "match_mapping_type": "string",          "mapping": {            "type": "keyword"          }        }      }    ]  }}

  c.监控分片大小。越大的分片能更有效地存储数据。但是分片越大,故障恢复也会越慢。

  d.禁用_source字段。_source会存储原始的json数据,如果不需要,就将其禁用。

  e.使用best_compression进行压缩。es默认使用 LZ4 进行压缩,使用best_compression可以提升压缩比率,但是会影响数据存取性能。

  f.force-merge.强制合并段能提升存储效率。注意,force-merge应当在没有文件写入后进行, 比如在过期的时序索引节点上。

  g.shrink 索引。即收缩索引,将当前索引重新索引成分片数更少的索引。分片越大,存储效率越高。

    shrink索引有如下条件。

    1.索引必须只读。

    2.节点必须包含索引的所有分片(主分片,或者复制分片都可以)

    3.索引状态必须是健康的。

  h.使用能满足需求的最小的数字类型。比如能用byte, 不用short。这个在其他db比如mysql中也适用。

  i.使用索引排序来提升文档的压缩性能。排序后相似的文档会放在一起,es能根据他们的特性有效地进行压缩。

    设定索引排序:

PUT my-index-000001{  "settings": {    "index": {      "sort.field": "date",       "sort.order": "desc"      }  },  "mappings": {    "properties": {      "date": {        "type": "date"      }    }  }}

  j.索引文档时保证json字段顺序一致。es在存储的时候将多个文档压缩成一成block,如果json文档顺序一致,es能更好的对更长的相同的字符串进行压缩。

  k.roll-up历史数据。使用roll up api来归档历史数据,他们依然可以访问,但是有着更高的存储效率。

5.分片大小

  1.将索引分片大小保持在10G~50G之间

  2.平均下来每G堆内存下不要超过20个分片。