ElasticSearch 性能

1.querystring语法

GET 127.0.0.1:9200/people/man/_search?q=jack

?q=后面写的,就是querystring语法.鉴于这部分内容会在Kibana上经常使用,这里详细解析一下语法:

  • 全文检索: 直接写搜索的单词,如上例中的 first
  • 单字段的全文检索: 在搜索单词之前加上字段名和冒号,比如如果知道单词 first 肯定出现在 mesg 字段,可以写作 mesg:first
  • 单字段的精确检索: 在搜索单词前后加双引号,比如 user:"chenlin7"
  • 多个检索条件的组合: 可以使用 NOT, ANDOR 来组合检索,注意必须是大写.比如 user:("chenlin7" OR "chenlin") AND NOT mesg:first
  • 字段是否存在: _exists_:user 表示要求 user 字段存在,_missing_:user 表示要求 user 字段不存在
  • 通配符: 用 ?表示单字母,*表示任意个字母.比如 fir?t mess*
  • 正则: 需要比通配符更复杂一点的表达式,可以使用正则.比如 mesg:/mes{2}ages?/.注意ES中正则性能很差,而且支持的功能也不是特别强大,尽量不要使用.
  • 近似搜索: 用 ~ 表示搜索单词可能有一两个字母写的不对,请ES按照相似度返回结果.比如 frist~
  • 范围搜索: 对数值和时间,ES 都可以使用范围搜索,比如: rtt:>300,date:["now-6h" TO "now"} 等.其中,[] 表示端点数值包含在范围内,{} 表示端点数值不包含在范围内

2.核心类型

mapping中主要就是针对字段设置类型以及类型相关参数.那么,我们首先来了解一下 Elasticsearch 支持的核心类型:

  • JSON 基础类型
    • 字符串: text, keyword
    • 数字: byte, short, integer, long, float, double,half_float
    • 时间: date
    • 布尔值: true, false
    • 数组: array
    • 对象: object
  • ES 独有类型
    • 多重: multi
    • 经纬度: geo_point
    • 网络地址: ip
    • 堆叠对象: nested object
    • 二进制: binary
    • 附件: attachment

前面提到,ES 是根据收到的 JSON 数据里的类型来猜测的.所以,一个内容为"123"的数据,猜测出来的类型应该是字符串而不是数值.除非这个字段已经有了确定为long的映射关系,那么ES会尝试做一次转换.如果转换失败,这条数据写入就会报错.

2.1 精确索引

字段都有几个基本的映射选项,类型(type)、存储(store)和索引方式(index).默认来说,store 是 false 而 index 是 true.因为 ES 会直接在 _source 里存储全部 JSON,不用每个 field 单独存储了.

不过在非日志场景,比如用作监控存储的TSDB使用的时候,我们就可以关闭 _source,只存储有关metric名称的字段store;同时也关闭所有数值字段的index,只使用它们的 doc_values.

2.2 时间格式

稍微见过Elastic Stack示例的人,都对其中 @timestamp 字段的特殊格式有深刻的印象.这个时间格式在 Nginx 中叫 $time_iso8601,在 Rsyslog 中叫 date-rfc3339,在ES中叫 dateOptionalTime.但事实上,ES 完全可以接收其他时间格式作为时间字段的内容.对于 ES 来说,时间字段内容实际都是转换成 long 类型作为内部存储的.所以,接收段的时间格式,可以任意配置:

"@timestamp" : {
    "type" : "date"
    "format" : "dd/MMM/YYYY:HH:mm:ss Z",
}

而 ES 默认的时间字段格式,除了 dateOptionalTime 以外,还有一种,就是 epoch_millis,毫秒级的 UNIX 时间戳.因为这个数值 ES 可以直接毫不修改的存成内部实际的 long 数值.此外,从 ES 2.0 开始,新增了对秒级 UNIX 时间戳的支持,其 format 定义为:epoch_second.

注意: 从 ES 2.x 开始,同名 date 字段的 format 也必须保持一致.

2.3 特殊字段

上面介绍的,都是对普通数据字段的一些常用设置.而实际上,ES 默认还有一些特殊字段,在默默的发挥着作用.这些字段,统一以 _ 下划线开头.

_all _all 里存储了各字段的数据内容.其作用是,在检索的时候,如果无法或者未指明具体搜索哪个字段的数据,那么 ES 默认就会是从 _all 里去查找. 对于日志场景,如果你的日志划分出来的字段比较少且数目固定.那么,完全可以关闭掉 _all 功能,节省这部分 IO 和 CPU.

"_all" : {
    "enabled" : false
}

Elastic.co 甚至考虑在 6.0 版本中废弃掉 _all,由用户自定义字段来完成类似工作(比如日志场景中的 message 字段).因为 _all 采用的分词器和用户自定义字段可能是不一致的,某些场景下会产生误解.

_field_names _field_names 里存储的是每条数据里的字段名,你可以认为它是 _all 的补集.其主要作用是在做 _missing__exists_ 查询的时候,不用检索数据本身,直接获取字段名对应的文档 ID.听起来似乎蛮不错的,但是文档较多的时候,就意味着这个倒排链非常长!而且几乎每次索引写入操作,都需要往这个倒排里加入文档 ID,这点是实际使用中非常损耗写入性能的地方.

除非有必要理由,关闭 _field_names 可以提升大概 20% 的写入性能.

_source _source里存储了该条记录的 JSON 源数据内容.这部分内容只是按照ES接收到的内容原样存储下来,并不经过索引过程.对于ES的请求过程来说,它不参与 Query 阶段,而只用于 Fetch 阶段.我们在GET或者 /_search 时看到的数据内容,都是从 _source 里获取到的.

所以,虽然 _source 也重复了一遍索引中的数据,一般我们并不建议关闭这个功能.因为一旦关闭,你搜索的结果除了一个 _id,啥都看不到.对于日志场景,意义不是很大.

当然,也有少数场景是可以关闭 _source 的:

  • 把ES作为时间序列数据库使用,只要聚合统计结果,不要源数据内容
  • 把ES作为纯检索工具使用,_id 对应的内容在 HDFS 上另外存储,搜索后使用所得 _idHDFS上读取内容

2.4 多重索引

多重索引是logstash用户最习惯的一个映射,因为这是 logstash 默认设置开启的配置:

"title": {
    "type": "text",
    "fields": {
        "raw": { "type": "keyword" }
    }
}

其作用是,在title字段数据写入的时候,ES 会自动生成两个字段,分别是 titletitle.raw.这样,在可能同时需要分词与不分词结果的环境下,就可以很灵活的使用不同的索引字段了.比如,查看标题中最常用的单词,应该使用 title 字段;查看阅读数最多的文章标题,应该使用 title.raw 字段.

注意: raw 这个名字你可以自己随意取.比如说,如果你绝大多数时候用的是精确索引,那么你完全可以为了方便反过来定义.

"title": {
    "type": "keyword",
    "fields": {
        "alz": { "type": "text" }
    }
}

Textkeyword

  • Text: 会分词,然后进行索引
    • 支持模糊查询
    • 不支持排序和聚合
    • eg: 被用来索引长文本,比如说电子邮件的主体部分或者一款产品的介绍.这些文本会被分析,在建立索引前会将这些文本进行分词,转化为词的组合,建立索引.允许ES来检索这些词语
  • keyword: 不进行分词,直接索引
    • 支持精确查询
    • 支持过滤、排序和聚合
    • eg: 用来建立电子邮箱地址、姓名、邮政编码和标签等数据,不需要进行分词

2.5 自定义字段映射

大家可以通过上面一个现存的映射发现其实所有的字段都有好几个属性,这些都是我们可以自己定义修改的.除了已经看到的这些基本内容外,ES 还支持其他一些可能会比较常用的映射属性: -索引还是存储 -自定义分词器 -自定义日期格式

3.映射与模板的定制

3.1 创建和更新映射

ES可以随时根据数据中的新字段来创建新的映射关系.所以,我们也可以自己在还没有正式数据写入之前,先创建一个基础的映射.等后续数据有其他字段时,ES 也一样会自动处理.

映射的的创建方式如下:

# curl -XPUT http://127.0.0.1:9200/indexd/_mapping -d '
{
  "mappings": {
    "syslog" : {
      "properties" : {
        "@timestamp" : {
          "type" : "date"
        },
        "message" : {
          "type" : "text"
        },
        "pid" : {
          "type" : "long"
        }
      }
    }
  }
}'

注意:对于已存在的映射,ES 的自动处理仅限于新字段出现.已经生成的字段映射,是不可变更的.

而如果是新增一个字段映射的更新,那还是可以通过 /_mapping 接口直接完成的:

# curl -XPUT http://127.0.0.1:9200/indexd/_mapping/syslog -d '
{
  "properties" : {
    "syslogtag" : {
      "type" : "keyword",
    }
  }
}'

这里只需要单独写这个新字段的内容就够了. ES会自动合并进去.

3.2 查看已有数据的映射

学习索引映射最直接的方式,就是查看已有数据索引的映射.我们用logstash写入ES的数据,都会根据logstash自带的template,生成一个很有学习意义的映射:

# curl -XGET http://127.0.0.1:9200/indexd/_mapping/tweet
{
   "gb": {
      "mappings": {
         "tweet": {
            "properties": {
               "date": {
                  "type": "date",
                  "format": "dateOptionalTime"
               },
               "name": {
                  "type": "keyword"
               },
               "tweet": {
                  "type": "text"
               },
               "user_id": {
                  "type": "long"
               }
            }
         }
      }
   }
}

3.3 删除映射

删除数据并不代表会删除数据的映射.

# curl -XDELETE http://127.0.0.1:9200/indexd/syslog

删除了索引下 syslog 的全部数据,但是 syslog 的映射还在.删除映射(同时也就删掉了数据)的命令是:

# curl -XDELETE http://127.0.0.1:9200/indexd/_mapping/syslog

当然,如果删除整个索引,那映射也是同时被清除的.

3.4 动态模板映射

不想使用默认识别的结果,单独设置一个字段的映射的方法,上面已经介绍完毕.那么,如果你有一类相似的数据字段,想要统一设置其映射,就可以用到下一项功能:动态模板映射(dynamic_templates).

    "_default_" : {
      "dynamic_templates" : [ {
        "message_field" : {
          "mapping" : {
            "omit_norms" : true,
            "store" : false,
            "type" : "text"
          },
          "match" : "*msg",
          "match_mapping_type" : "string"
        }
      }, {
        "string_fields" : {
          "mapping" : {
            "ignore_above" : 256,
            "store" : false,
            "type" : "keyword"
          },
          "match" : "*",
          "match_mapping_type" : "string"
        }
      } ],
      "properties" : {
      }
    }

这样,只要字符串类型字段名以 msg 结尾的,都会经过全文索引,其他字符串字段则进行精确索引.同理,还可以继续书写其他类型(long, float, date 等)的 match_mapping_typematch.

3.5 索引模板

对每个希望自定义映射的索引,都要定时提前通过发送 PUT 请求的方式创建索引的话,未免太过麻烦. ES对此设计了索引模板功能.我们可以针对同一类索引,定制相同的模板.

模板中的内容包括两大类,setting(设置)mapping(映射). setting部分,多为在elasticsearch.yml中可以设置全局配置的部分,而mapping部分,则是这节之前介绍的内容.

如下为定义所有以 te 开头的索引的模板:

# curl -XPUT http://localhost:9200/_template/template_1 -d '
{
    "template" : "te*",
    "settings" : {
        "number_of_shards" : 1
    },
    "mappings" : {
        "type1" : {
            "_source" : { "enabled" : false }
        }
    }
}'

同时,索引模板是有序合并的.如果我们在同一类索引里,又想单独修改某一小类索引的一两处单独设置,可以再累加一层模板:

# curl -XPUT http://localhost:9200/_template/template_2 -d '
{
    "order" : 1,
    "template" : "tete*",
    "settings" : {
        "number_of_shards" : 2
    },
    "mappings" : {
        "type1" : {
            "_all" : { "enabled" : false }
        }
    }
}'

默认的order是0,那么新创建的order为 1 的template_2在合并时优先级大于template_1.最终,对tete*/type1的索引模板效果相当于:

{
    "settings" : {
        "number_of_shards" : 2
    },
    "mappings" : {
        "type1" : {
            "_source" : { "enabled" : false },
            "_all" : { "enabled" : false }
        }
    }
}

4.批量提交

4.1 bulk接口

ES 设计了批量提交方式.在数据读取方面,叫mget接口,在数据变更方面,叫bulk接口mget一般常用于搜索时ES节点之间批量获取中间结果集,对于Elastic Stack用户,更常见到的是bulk接口.

bulk接口采用一种比较简朴的数据积累格式

# curl -XPOST http://127.0.0.1:9200/_bulk -d'
{ "create" : { "_index" : "test", "_type" : "type1"  } }
{ "field1" : "value1" }
{ "delete" : { "_index" : "test", "_type" : "type1" } }
{ "index" : { "_index" : "test", "_type" : "type1", "_id" : "1" } }
{ "field1" : "value2" }
{ "update" : {"_id" : "1", "_type" : "type1", "_index" : "test"} }
{ "doc" : {"field2" : "value2"} }
'

格式是,每条JSON数据的上面,加一行描述性的元JSON,指明下一行数据的操作类型,归属索引信息等.

采用这种格式,而不是一般的JSON数组格式,是因为接收到bulk请求的 ES 节点,就可以不需要做完整的JSON数组解析处理,直接按行处理简短的元JSON,就可以确定下一行数据JSON转发给哪个数据节点了.这样,一个固定内存大小的 network buffer 空间,就可以反复使用,又节省了大量 JVM 的 GC.

事实上,产品级的logstashrsyslogspark都是默认采用bulk接口进行数据写入的.

4.2 bulk size

在配置bulk数据的时候,一般需要注意的就是请求体大小(bulk size).

这里有一点细节上的矛盾,我们知道,HTTP 请求,是可以通过 HTTP 状态码100 Continue来持续发送数据的.但对于 ES 节点接收 HTTP 请求体的Content-Length来说,是按照整个大小来计算的.所以,首先,要确保bulk数据不要超过 http.max_content_length 设置.

那么,是不是尽量让bulk size接近这个数值呢?当然不是.

依然是请求体的问题,因为请求体需要全部加载到内存,而JVM Heap一共就那么多(按31GB算),过大的请求体,会挤占其他线程池的空间,反而导致写入性能的下降. 再考虑网卡流量,磁盘转速的问题,所以一般来说,建议bulk请求体的大小,在15MB左右,通过实际测试继续向上探索最合适的设置.

注意: 这里说的15MB是请求体的字节数,而不是程序里里设置的bulk size.bulk size一般指数据的条目数.不要忘了,bulk请求体中,每条数据还会额外带上一行元JSON.

以logstash默认的bulk_size => 5000为例,假设单条数据平均大小200B,一次bulk请求体的大小就是1.5MB.那么我们可以尝试bulk_size => 50000;而如果单条数据平均大小是20KB,一次bulk大小就是100MB,显然超标了,需要尝试下调至bulk_size => 500.

5.缓存

ES 内针对不同阶段,设计有不同的缓存.以此提升数据检索时的响应性能.主要包括节点层面的 filter cache 和分片层面的 request cache.下面分别讲述.

5.1 filter cache

ES的query DSL在2.0版本之前分为query和filter两种,很多检索语法,是同时存在query和filter里的.比如最常用的term、prefix、range等.怎么选择是使用query还是filter成为很多用户头疼的难题.于是从2.0版本开始,ES干脆合并了filter统一归为query.但是具体的检索语法本身,依然有query和filter上下文的区别.ES依靠这个上下文判断,来自动决定是否启用filter cache.

query跟filter的区别:

  • query
    • 是要相关性评分的
      • 结果无法缓存
      • 全文搜索、评分排序
  • filter
    • 不要相关性评分的
      • 结果可以缓存
    • 是非过滤,精确匹配

所以,选择也就出来了:

curl -XGET http://127.0.0.1:9200/_search -d '
{
    "query": {
        "bool": {
            "must_not": [
                { "match": { "title": "Search" } }
            ],
            "must": [
                { "match": { "content": "Elasticsearch" } }
            ],
            "filter": [
                { "term":  { "status": "published" } },
                { "range": { "publish_date": { "gte": "2015-01-01" } } }
            ]
        }
    }
}'

在这个请求中

  • ES 先看到一个 query,那么进入query上下文.
  • 然后在bool里看到一个must_not,那么改进入filter上下文,这个有关title字段的查询不参与评分.
  • 然后接着是一个mustmatch,这个又属于query上下文,这个有关content字段的查询会影响评分.
  • 最后碰到filter,还属于filter上下文,这个有关status和publish_date字段的查询不参与评分.

注意: filter cache是节点层面的缓存设置,每个节点上所有数据在响应请求时,是共用一个缓存空间的.当空间用满,按照LRU策略淘汰掉最冷的数据.

可以用indices.cache.filter.size配置来设置这个缓存空间的大小,默认是JVM堆的10%,也可以设置一个绝对值.注意这是一个静态值,必须在elasticsearch.yml中提前配置.

5.2 shard request cache

ES还有另一个分片层面的缓存,叫shard request cache.5.0 之前的版本中,request cache的用途并不大,因为query cache要起作用,还有几个先决条件:

  • 分片数据不再变动,也就是对当天的索引是无效的(如果 refresh_interval 很大,那么在这个间隔内倒也算有效)
  • 使用了"now"语法的请求无法被缓存,因为这个是要即时计算的
  • 缓存的键是请求的整个JSON字符串,整个字符串发生任何字节变动,缓存都无效

Elastic Stack场景来说,Kibana 里几乎所有的请求,都是有 @timestamp 作为过滤条件的,而且大多数是以最近 N 小时/分钟这样的选项,也就是说,页面每次刷新,发出的请求JSON里的时间过滤部分都是在变动的.query cache在处理 Kibana 发出的请求时,完全无用.

而 5.0 版本的一大特性,叫instant aggregation.解决了这个先决条件的一大阻碍. 在之前的版本,Elasticsearch 接收到请求之后,直接把请求原样转发给各分片,由各分片所在的节点自行完成请求的解析,进行实际的搜索操作.所以缓存的键是原始 JSON 串.

而 5.0 的重构后,接收到请求的节点先把请求的解析做完,发送到各节点的是统一拆分修改好的请求,这样就不再担心 JSON 串多个空格啥的了.

其次,上面说的『拆分修改』是怎么回事呢? 比如,我们在 Kibana 里搜索一个最近 7 天(@timestamp:["now-7d" TO "now"])的数据,ES 就可以根据按天索引的判断,知道从 6 天前到昨天这5个索引是肯定全覆盖的.那么这个横跨7天的date range query就变成了5个match_allquery加2个短时间的date_rangequery.

现在你的仪表盘过5分钟自动刷新一次,再提交上来一次最近7天的请求,中间这5个match_all就完全一样了,直接从request cache返回即可,需要重新请求的,只有两头真正在变动的date_range了.

注意

  • match_all不用遍历倒排索引,比直接查询@timestamp:*要快很多
  • 判断覆盖修改为match_all并不是真的按照索引名称,而是ES从2.x开始提供的field_stats接口可以直接获取到@timestamp在本索引内的max/min值.当然从概念上如此理解也是可以接受的.*

5.3 field_stats接口

curl -XGET "http://localhost:9200/indexd/_field_stats?fields=timestamp"

响应结果如下:

{
    "_shards": {
        "total": 1,
        "successful": 1,
        "failed": 0
    },
    "indices": {
        "indexd": {
            "fields": {
                "timestamp": {
                    "max_doc": 1326564,
                    "doc_count": 564633,
                    "density": 42,
                    "sum_doc_freq": 2258532,
                    "sum_total_term_freq": -1,
                    "min_value": "2008-08-01T16:37:51.513Z",
                    "max_value": "2013-06-02T03:23:11.593Z",
                    "is_searchable": "true",
                    "is_aggregatable": "true"
                }
            }
        }
    }
}

和 filter cache 一样,request cache 的大小也是以节点级别控制的,配置项名为 indices.requests.cache.size,其默认值为 1%.

results matching ""

    No results matching ""