1. ElasticSearch优化系列七:优化建议
a) JVM内存设置不要超过机器的一半内存,并且不超过32G。(./bin/elasticsearch -Xmx10g -Xms10g或者修改./bin/elasticsearch.in.sh文件:
** 一般分配主机1/4-1/2的内存**
设置每个线程的堆栈大小, ES单线程承载的数据量比较大 JAVA_OPTS="$JAVA_OPTS -Xss128m"
b) 修改swapping参数,内存不够用时才进行swapping(vm.swappiness= 1) c) 暂时不要修改GC方法 d)锁定内存,不让JVM写入swapping,避免降低ES的 性能 bootstrap.mlockall: true e)缓存类型设置为Soft Reference,只有当内存不够时才会进行回收 index.cache.field.max_size: 50000 index.cache.field.expire: 10m index.cache.field.type: soft
4.权衡建索引的性能和检索的时效性,修改以下参数。
5.倒排词典的索引需要常驻内存,无法GC,需要监控data node上segment memory增长趋势。
定期对不再更新的索引做optimize (ES2.0以后更改为force merge api)。这Optimze的实质是对segment file强制做合并,可以节省大量的segment memory
6.根据机器数,磁盘数,索引大小等硬件环境,根据测试结果,设置最优的分片数和备份数,单个分片最好不超过10GB,定期删除不用的索引,做好冷数据的迁移。
7.保守配置内存限制参数,尽量使用doc value存储以减少内存消耗,查询时限制size、from参数。
8.如果不使用_all字段最好关闭这个属性,否则在创建索引和增大索引大小的时候会使用额外更多的CPU,如果你不受限CPU计算能力可以选择压缩文档的_source。这实际上就是整行日志,所以开启压缩可以减小索引大小。
9.避免返回大量结果集的搜索与聚合。缺失需要大量拉取数据可以采用scan & scroll api来实现。
10.熟悉各类缓存作用,如field cache, filter cache, indexing cache, bulk queue等等,要设置合理的大小,并且要应该根据最坏的情况来看heap是否够用。
11.必须结合实际应用场景,并对集群使用情况做持续的监控。
2. Elasticsearch性能优化
注:文本整理自《ELKstack权威指南》
在 CRUD 章节,我们已经知道 ES 的数据写入是如何操作的了。喜欢自己动手的读者可能已经迫不及待的自己写了程序开始往 ES 里写数据做测试。这时候大家会发现:程序的运行速度非常一般,即使 ES 服务运行在本机,一秒钟大概也就能写入几百条数据。
这种速度显然不是 ES 的极限。事实上,每条数据经过一次完整的 HTTP POST 请求和 ES indexing 是一种极大的性能浪费,为此,ES 设计了批量提交方式。在数据读取方面,叫 mget 接口,在数据变更方面,叫 bulk 接口。mget 一般常用于搜索时 ES 节点之间批量获取中间结果集,对于 Elastic Stack 用户,更常见到的是 bulk 接口。
bulk 接口采用一种比较简朴的数据积累格式,示例如下:
格式是,每条 JSON 数据的上面,加一行描述性的元 JSON,指明下一行数据的操作类型,归属索引信息等。
采用这种格式,而不是一般的 JSON 数组格式,是因为接收到 bulk 请求的 ES 节点,就可以不需要做完整的 JSON 数组解析处理,直接按行处理简短的元 JSON,就可以确定下一行数据 JSON 转发给哪个数据节点了。这样,一个固定内存大小的 network buffer 空间,就可以反复使用,又节省了大量 JVM 的 GC。
事实上,产品级的 logstash、rsyslog、spark 都是默认采用 bulk 接口进行数据写入的。对于打算自己写程序的读者,建议采用 Perl 的 Search::Elasticsearch::Bulk 或者 Python 的 elasticsearch.helpers.* 库。
在配置 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 。
gateway 是 ES 设计用来长期存储索引数据的接口。一般来说,大家都是用本地磁盘来存储索引数据,即 gateway.type 为 local 。
数据恢复中,有很多策略调整我们已经在之前分片控制小节讲过。除开分片级别的控制以外,gateway 级别也还有一些可优化的地方:
注意:gateway 中说的节点,仅包括主节点和数据节点,纯粹的 client 节点是不算在内的。如果你有更明确的选择,也可以按需求写:
虽然 ES 对 gateway 使用 NFS,iscsi 等共享存储的方式极力反对,但是对于较大量级的索引的副本数据,ES 从 1.5 版本开始,还是提供了一种节约成本又不特别影响性能的方式:影子副本(shadow replica)。
首先,需要在集群各节点的 elasticsearch.yml 中开启选项:
同时,确保各节点使用相同的路径挂载了共享存储,且目录权限为 Elasticsearch 进程用户可读可写。
然后,创建索引:
针对 shadow replicas ,ES 节点不会做实际的索引操作,而是单纯的每次 flush 时,把 segment 内容 fsync 到共享存储磁盘上。然后 refresh 让其他节点能够搜索该 segment 内容。
如果你已经决定把数据放到共享存储上了,采用 shadow replicas 还是有一些好处的:
但是请注意:主分片节点还是要承担一个副本的写入过程,并不像 Lucene 的 FileReplicator 那样通过复制文件完成,所以达不到完全节省 CPU 的效果。
shadow replicas 只是一个在某些特定环境下有用的方式。在资源允许的情况下,还是应该使用 local gateway。而另外采用 snapshot 接口来完成数据长期备份到 HDFS 或其他共享存储的需要。
我们都知道,ES 中的 master 跟一般 MySQL、Hadoop 的 master 是不一样的。它即不是写入流量的唯一入口,也不是所有数据的元信息的存放地点。所以,一般来说,ES 的 master 节点负载很轻,集群性能是可以近似认为随着 data 节点的扩展线性提升的。
但是,上面这句话并不是完全正确的。
ES 中有一件事情是只有 master 节点能管理的,这就是集群状态(cluster state)。
集群状态中包括以下信息:
这些信息在集群的任意节点上都存放着,你也可以通过 /_cluster/state 接口直接读取到其内容。注意这最后一项信息,之前我们已经讲过 ES 怎么通过简单地取余知道一条数据放在哪个分片里,加上现在集群状态里又记载了分片在哪个节点上,那么,整个集群里,任意节点都可以知道一条数据在哪个节点上存储了。所以,数据读写才可以发送给集群里任意节点。
至于修改,则只能由 master 节点完成!显然,集群状态里大部分内容是极少变动的,唯独有一样除外——索引的映射。因为 ES 的 schema-less 特性,我们可以任意写入 JSON 数据,所以索引中随时可能增加新的字段。这个时候,负责容纳这条数据的主分片所在的节点,会暂停写入操作,将字段的映射结果传递给 master 节点;master 节点合并这段修改到集群状态里,发送新版本的集群状态到集群的所有节点上。然后写入操作才会继续。一般来说,这个操作是在一二十毫秒内就可以完成,影响也不大。
但是也有一些情况会是例外。
在较大规模的 Elastic Stack 应用场景中,这是比较常见的一个情况。因为 Elastic Stack 建议采用日期时间作为索引的划分方式,所以定时(一般是每天),会统一产生一批新的索引。而前面已经讲过,ES 的集群状态每次更新都是阻塞式的发布到全部节点上以后,节点才能继续后续处理。
这就意味着,如果在集群负载较高的时候,批量新建新索引,可能会有一个显著的阻塞时间,无法写入任何数据。要等到全部节点同步完成集群状态以后,数据写入才能恢复。
不巧的是,中国使用的是北京时间,UTC +0800。也就是说,默认的 Elastic Stack 新建索引时间是在早上 8 点。这个时间点一般日志写入量已经上涨到一定水平了(当然,晚上 0 点的量其实也不低)。
对此,可以通过定时任务,每天在最低谷的早上三四点,提前通过 POST mapping 的方式,创建好之后几天的索引。就可以避免这个问题了。
如果你的日志是比较严重的非结构化数据,这个问题在 2.0 版本后会变得更加严重。 Elasticsearch 从 2.0 版本开始,对 mapping 更新做了重构。为了防止字段类型冲突和减少 master 定期下发全量 cluster state 导致的大流量压力,新的实现和旧实现的区别在:
也就是说,一旦你日志中字段数量较多,在新创建索引的一段时间内,可能长达几十分钟一直被反复锁死!
这是另一种常见的滥用。在使用 Elastic Stack 处理访问日志时,为了查询更方便,可能会采用 logstash-filter-kv 插件,将访问日志中的每个 URL 参数,都切分成单独的字段。比如一个 "/index.do?uid=1234567890&action=payload" 的 URL 会被转换成如下 JSON:
但是,因为集群状态是存在所有节点的内存里的,一旦 URL 参数过多,ES 节点的内存就被大量用于存储字段映射内容。这是一个极大的浪费。如果碰上 URL 参数的键内容本身一直在变动,直接撑爆 ES 内存都是有可能的!
以上是真实发生的事件,开发人员莫名的选择将一个 UUID 结果作为 key 放在 URL 参数里。直接导致 ES 集群 master 节点全部 OOM。
如果你在 ES 日志中一直看到有新的 updating mapping [logstash-2015.06.01] 字样出现的话,请郑重考虑一下自己是不是用的上如此细分的字段列表吧。
好,三秒钟过去,如果你确定一定以及肯定还要这么做,下面是一个变通的解决办法。
用 nested object 来存放 URL 参数的方法稍微复杂,但还可以接受。单从 JSON 数据层面看,新方式的数据结构如下:
没错,看起来就是一个数组。但是 JSON 数组在 ES 里是有两种处理方式的。
如果直接写入数组,ES 在实际索引过程中,会把所有内容都平铺开,变成 Arrays of Inner Objects 。整条数据实际类似这样的结构:
这种方式最大的问题是,当你采用 urlargs.key:"uid" AND urlargs.value:"0987654321" 语句意图搜索一个 uid=0987654321 的请求时,实际是整个 URL 参数中任意一处 value 为 0987654321 的,都会命中。
要想达到正确搜索的目的,需要在写入数据之前,指定 urlargs 字段的映射类型为 nested object。命令如下:
这样,数据实际是类似这样的结构:
当然,nested object 节省字段映射的优势对应的是它在使用的复杂。Query 和 Aggs 都必须使用专门的 nested query 和 nested aggs 才能正确读取到它。
nested query 语法如下:
nested aggs 语法如下:
ES 内针对不同阶段,设计有不同的缓存。以此提升数据检索时的响应性能。主要包括节点层面的 filter cache 和分片层面的 request 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 上下文的区别,简单来说:
所以,选择也就出来了:
不过我们要怎么写,才能让 ES 正确判断呢?看下面这个请求:
在这个请求中,
需要注意的是,filter cache 是节点层面的缓存设置,每个节点上所有数据在响应请求时,是共用一个缓存空间的。当空间用满,按照 LRU 策略淘汰掉最冷的数据。
可以用 indices.cache.filter.size 配置来设置这个缓存空间的大小,默认是 JVM 堆的 10%,也可以设置一个绝对值。注意这是一个静态值,必须在 elasticsearch.yml 中提前配置。
ES 还有另一个分片层面的缓存,叫 shard request cache。5.0 之前的版本中,request cache 的用途并不大,因为 query cache 要起作用,还有几个先决条件:
以 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_all query 加 2 个短时间的 date_range query。
现在你的仪表盘过 5 分钟自动刷新一次,再提交上来一次最近 7 天的请求,中间这 5 个 match_all 就完全一样了,直接从 request cache 返回即可,需要重新请求的,只有两头真正在变动的 date_range 了。
注1: match_all 不用遍历倒排索引,比直接查询 @timestamp:* 要快很多。 注2:判断覆盖修改为 match_all 并不是真的按照索引名称,而是 ES 从 2.x 开始提供的 field_stats 接口可以直接获取到 @timestamp 在本索引内的 max/min 值。当然从概念上如此理解也是可以接受的。
响应结果如下:
和 filter cache 一样,request cache 的大小也是以节点级别控制的,配置项名为 indices.requests.cache.size ,其默认值为 1% 。
字段数据(fielddata),在 Lucene 中又叫 uninverted index。我们都知道,搜索引擎会使用倒排索引(inverted index)来映射单词到文档的 ID 号。而同时,为了提供对文档内容的聚合,Lucene 还可以在运行时将每个字段的单词以字典序排成另一个 uninverted index,可以大大加速计算性能。
作为一个加速性能的方式,fielddata 当然是被全部加载在内存的时候最为有效。这也是 ES 默认的运行设置。但是,内存是有限的,所以 ES 同时也需要提供对 fielddata 内存的限额方式:
Elasticsearch 在 total,fielddata,request 三个层面上都设计有 circuit breaker 以保护进程不至于发生 OOM 事件。在 fielddata 层面,其设置为:
但是相比较集群庞大的数据量,内存本身是远远不够的。为了解决这个问题,ES 引入了另一个特性,可以对精确索引的字段,指定 fielddata 的存储方式。这个配置项叫: doc_values 。
所谓 doc_values ,其实就是在 ES 将数据写入索引的时候,提前生成好 fielddata 内容,并记录到磁盘上。因为 fielddata 数据是顺序读写的,所以即使在磁盘上,通过文件系统层的缓存,也可以获得相当不错的性能。
注意:因为 doc_values 是在数据写入时即生成内容,所以,它只能应用在精准索引的字段上,因为索引进程没法知道后续会有什么分词器生成的结果。
由于在 Elastic Stack 场景中, doc_values 的使用极其频繁,到 Elasticsearch 5.0 以后,这两者的区别被彻底强化成两个不同字段类型: text 和 keyword 。
等同于过去的:
而
等同于过去的:
也就是说,以后的用户,已经不太需要在意 fielddata 的问题了。不过依然有少数情况,你会需要对分词字段做聚合统计的话,你可以在自己接受范围内,开启这个特性:
你可以看到在上面加了一段 fielddata_frequency_filter 配置,这个配置是 segment 级别的。上面示例的意思是:只有这个 segment 里的文档数量超过 500 个,而且含有该字段的文档数量占该 segment 里的文档数量比例超过 10% 时,才加载这个 segment 的 fielddata。
下面是一个可能有用的对分词字段做聚合的示例:
这个示例可以对经过了 logstash-filter-punct 插件处理的数据,获取每种 punct 类型日志的关键词和对应的代表性日志原文。其效果类似 Splunk 的事件模式功能:
[图片上传失败...(image-b0b69f-1511752650964)]
如果经过之前章节的一系列优化之后,数据确实超过了集群能承载的能力,除了拆分集群以外,最后就只剩下一个办法了:清除废旧索引。
为了更加方便的做清除数据,合并 segment,备份恢复等管理任务,Elasticsearch 在提供相关 API 的同时,另外准备了一个命令行工具,叫 curator 。curator 是 Python 程序,可以直接通过 pypi 库安装:
注意,是 elasticsearch-curator 不是 curator。PyPi 原先就有另一个项目叫这个名字
和 Elastic Stack 里其他组件一样,curator 也是被 Elastic.co 收购的原开源社区周边。收编之后同样进行了一次重构,命令行参数从单字母风格改成了长单词风格。新版本的 curator 命令可用参数如下:
Options 包括:
--host TEXT Elasticsearch host. --url_prefix TEXT Elasticsearch http url prefix. --port INTEGER Elasticsearch port. --use_ssl Connect to Elasticsearch through SSL. --http_auth TEXT Use Basic Authentication ex: user:pass --timeout INTEGER Connection timeout in seconds. --master-only Only operate on elected master node. --dry-run Do not perform any changes. --debug Debug mode --loglevel TEXT Log level --logfile TEXT log file --logformat TEXT Log output format [default|logstash]. --version Show the version and exit. --help Show this message and exit.
Commands 包括: alias Index Aliasing allocation Index Allocation bloom Disable bloom filter cache close Close indices delete Delete indices or snapshots open Open indices optimize Optimize Indices replicas Replica Count Per-shard show Show indices or snapshots snapshot Take snapshots of indices (Backup)
针对具体的 Command,还可以继续使用 --help 查看该子命令的帮助。比如查看 close 子命令的帮助,输入 curator close --help ,结果如下:
在使用 1.4.0 以上版本的 Elasticsearch 前提下,curator 曾经主要的一个子命令 bloom 已经不再需要使用。所以,目前最常用的三个子命令,分别是 close , delete 和 optimize ,示例如下:
这一顿任务,结果是:
logstash-mweibo-nginx-yyyy.mm.dd 索引保存最近 5 天, logstash-mweibo-client-yyyy.mm.dd 保存最近 10 天, logstash-mweibo-yyyy.mm.dd 索引保存最近 30 天;且所有七天前的 logstash-* 索引都暂时关闭不用;最后对所有非当日日志做 segment 合并优化。
profiler 是 Elasticsearch 5.0 的一个新接口。通过这个功能,可以看到一个搜索聚合请求,是如何拆分成底层的 Lucene 请求,并且显示每部分的耗时情况。
启用 profiler 的方式很简单,直接在请求里加一行即可:
可以看到其中对 query 和 aggs 部分的返回是不太一样的。
query 部分包括 collectors、rewrite 和 query 部分。对复杂 query,profiler 会拆分 query 成多个基础的 TermQuery,然后每个 TermQuery 再显示各自的分阶段耗时如下:
我们可以很明显的看到聚合统计在初始化阶段、收集阶段、构建阶段、汇总阶段分别花了多少时间,遍历了多少数据。
注意其中 reduce 阶段还没实现完毕,所有都是 0。因为目前 profiler 只能在 shard 级别上做统计。
collect 阶段的耗时,有助于我们调整对应 aggs 的 collect_mode 参数选择。目前 Elasticsearch 支持 breadth_first 和 depth_first 两种方式。
initialise 阶段的耗时,有助于我们调整对应 aggs 的 execution_hint 参数选择。目前 Elasticsearch 支持 map 、 global_ordinals_low_cardinality 、 global_ordinals 和 global_ordinals_hash 四种选择。在计算离散度比较大的字段统计值时,适当调整该参数,有益于节省内存和提高计算速度。
对高离散度字段值统计性能很关注的读者,可以关注 https://github.com/elastic/elasticsearch/pull/21626 这条记录的进展。
(本文完)
文本整理自《ELKstack权威指南》
3. ElasticSearch海量数据使用简述
应用场景当中经常会遇到模糊查询或多条件匹配查询,数据量较小的情况下通过简单的数据库模糊查询是可以解决的,但是对于数据量庞大的情况,数据库模糊查询就会出现性能问题。这种情况下的一种解决方案就是根据查询内容构建反向索引,借助搜索引擎进行查询,提升查询性能。
目前使用比较多的分布式搜索引擎是ElasticSearch。那么项目中如何使用ES?如何保证ES的数据更新?下面简单做个描述。
Elasticsearch使用可以简单分为两个阶段。数据初始化阶段、数据更新阶段。
数据初始化阶段。数据初始化常见的方式如下:
一、通过应用程序手动将数据库中的数据,调用ES接口API插入ES索引库中。
二、同过数据迁移工具将数据初始化到ES数据库。目前常用的ES同步工具有logstash-input-jdbc、DataX。通过同步迁移工具可以全量将数据库数据初始化到ES索引库中。
数据更新阶段。数据更新阶段常见的处理方式如下:
一、通过应用服务直接调用ES更新接口。这种方式实现比较简单但是对业务侵入性比较大。
二、对于实时性要求不高的可以采用定时任务监控数据表变化然后调用ES接口实现数据更新。
三、业务应用中通过发送消息异步更新数据。
四、通过DataX同步工具定时将修改的数据同步到ES库中。
上述是ElasticSearch使用的简单描述。使用的关键还是数据库与ES间的数据同步。能否用的好关键也是数据间的同步。
4. 亿级 Elasticsearch 性能优化
最近一年使用 Elasticsearch 完成亿级别日志搜索平台「ELK」,亿级别的分布式跟踪系统。在设计这些系统的过程中,底层都是采用 Elasticsearch 来做数据的存储,并且数据量都超过亿级别,甚至达到百亿级别。
所以趁着有空,就花点时间整理一下具体怎么做 Elasticsearch 性能优化,希望能对 Elasticsearch 感兴趣的同学有所帮助。
Elasticsearch 是一个基于 Lucene 的搜索服务器。它提供了一个分布式多用户能力的全文搜索引擎,基于 RESTful web 接口。Elasticsearch 是用 Java 开发的,并作为 Apache 许可条款下的开放源码发布,是当前流行的企业级搜索引擎。设计用于云计算中,能够达到实时搜索,稳定,可靠,快速,安装使用方便。
作为一个开箱即用的产品,在生产环境上线之后,我们其实不一定能确保其的性能和稳定性。如何根据实际情况提高服务的性能,其实有很多技巧。
下面我就从三个方面分别来讲解下优化服务的性能:
索引优化主要是在 Elasticsearch 插入层面优化,如果瓶颈不在这块,而是在产生数据部分,比如 DB 或者 Hadoop 上,那么优化方向就需要改变下。同时,Elasticsearch 本身索引速度其实还是蛮快的,具体数据,我们可以参考官方的 benchmark 数据。
当有大量数据提交的时候,建议采用批量提交。
比如在做 ELK 过程中 ,Logstash indexer 提交数据到 Elasticsearch 中 ,batch size 就可以作为一个优化功能点。但是优化 size 大小需要根据文档大小和服务器性能而定。
像 Logstash 中提交文档大小超过 20MB ,Logstash 会请一个批量请求切分为多个批量请求。
如果在提交过程中,遇到 EsRejectedExecutionException 异常的话,则说明集群的索引性能已经达到极限了。这种情况,要么提高服务器集群的资源,要么根据业务规则,减少数据收集速度,比如只收集 Warn、Error 级别以上的日志。
优化硬件设备一直是最快速有效的手段。
为了提高索引性能,Elasticsearch 在写入数据时候,采用延迟写入的策略,即数据先写到内存中,当超过默认 1 秒 (index.refresh_interval)会进行一次写入操作,就是将内存中 segment 数据刷新到操作系统中,此时我们才能将数据搜索出来,所以这就是为什么 Elasticsearch 提供的是 近实时 搜索功能,而不是实时搜索功能。
当然像我们的内部系统对数据延迟要求不高的话,我们可以通过延长 refresh 时间间隔,可以有效的减少 segment 合并压力,提供索引速度。在做全链路跟踪的过程中,我们就将 index.refresh_interval 设置为 30s,减少 refresh 次数。
同时,在进行全量索引时,可以将 refresh 次数临时关闭,即 index.refresh_interval 设置为 -1,数据导入成功后再打开到正常模式,比如 30s。
Elasticsearch 默认副本数量为 3 个,虽然这样会提高集群的可用性,增加搜索的并发数,但是同时也会影响写入索引的效率。
在索引过程中,需要把更新的文档发到副本节点上,等副本节点生效后在进行返回结束。使用 Elasticsearch 做业务搜索的时候,建议副本数目还是设置为 3 个,但是像内部 ELK 日志系统、分布式跟踪系统中,完全可以将副本数目设置为 1 个。
当我们查询文档的时候,Elasticsearch 如何知道一个文档应该存放到哪个分片中呢?它其实是通过下面这个公式来计算出来
routing 默认值是文档的 id,也可以采用自定义值,比如用户 id。
在查询的时候因为不知道要查询的数据具体在哪个分片上,所以整个过程分为 2 个步骤
查询的时候,可以直接根据 routing 信息定位到某个分配查询,不需要查询所有的分配,经过协调节点排序。
向上面自定义的用户查询,如果 routing 设置为 userid 的话,就可以直接查询出数据来,效率提升很多。
Ebay 曾经分享过他们使用 Elasticsearch 的经验中说到:
Elasticsearch 针对 Filter 查询只需要回答「是」或者「否」,不需要像 Query 查询一下计算相关性分数,同时 Filter 结果可以缓存。
在使用 Elasticsearch 过程中,应尽量避免大翻页的出现。
正常翻页查询都是从 From 开始 Size 条数据,这样就需要在每个分片中查询打分排名在前面的 From + Size 条数据。协同节点收集每个分配的前 From + Size 条数据。协同节点一共会受到 N * ( From + Size )条数据,然后进行排序,再将其中 From 到 From + Size 条数据返回出去。
如果 From 或者 Size 很大的话,导致参加排序的数量会同步扩大很多,最终会导致 CPU 资源消耗增大。
可以通过使用 Elasticsearch scroll 和 scroll-scan 高效滚动的方式来解决这样的问题。具体写法,可以参考 Elasticsearch: 权威指南 - scroll 查询
Elasticsearch 默认安装后设置的堆内存是 1 GB。 对于任何一个业务部署来说, 这个设置都太小了。
比如机器有 64G 内存,那么我们是不是设置的越大越好呢?
其实不是的。
主要 Elasticsearch 底层使用 Lucene。Lucene 被设计为可以利用操作系统底层机制来缓存内存数据结构。 Lucene 的段是分别存储到单个文件中的。因为段是不可变的,这些文件也都不会变化,这是对缓存友好的,同时操作系统也会把这些段文件缓存起来,以便更快的访问。
如果你把所有的内存都分配给 Elasticsearch 的堆内存,那将不会有剩余的内存交给 Lucene。 这将严重地影响全文检索的性能。
标准的建议是把 50% 的可用内存作为 Elasticsearch 的堆内存,保留剩下的 50%。当然它也不会被浪费,Lucene 会很乐意利用起余下的内存。
同时了解过 ES 的同学都听过过「不要超过 32G」的说法吧。
其实主要原因是 :JVM 在内存小于 32 GB 的时候会采用一个内存对象指针压缩技术。
在 Java 中,所有的对象都分配在堆上,并通过一个指针进行引用。 普通对象指针(OOP)指向这些对象,通常为 CPU 字长 的大小:32 位或 64 位,取决于你的处理器。指针引用的就是这个 OOP 值的字节位置。
对于 32 位的系统,意味着堆内存大小最大为 4 GB。对于 64 位的系统, 可以使用更大的内存,但是 64 位的指针意味着更大的浪费,因为你的指针本身大了。更糟糕的是, 更大的指针在主内存和各级缓存(例如 LLC,L1 等)之间移动数据的时候,会占用更多的带宽.
所以最终我们都会采用 31 G 设置
假设你有个机器有 128 GB 的内存,你可以创建两个节点,每个节点内存分配不超过 32 GB。 也就是说不超过 64 GB 内存给 ES 的堆内存,剩下的超过 64 GB 的内存给 Lucene
5. ES检索优化实践篇
我们搭建了一个涵盖全国企业信息的企业库,涵盖4000w的工商注册企业以及8000w的个体工商信息。用户使用此库的主要场景是检索与用户业务相关的企业,以寻找销售机会。
怎样帮用户更好地查找到心仪的企业?
首先,本着寻找销售机会的目标,分析用户检索的常见场景:
对产品而言,特定企业查询,是企业库检索的面子工程,有心里预期的检索结果过差,会造成极差的第一印象;同类企业查询,是我们项目的目标,能够查找符合用户需求的一类企业,才能更好地为用户提供价值
惭愧而言,现状距离上述两种目标都相差甚远
当前企业库支持公司名称、法人、地址和经营范围四个内容的检索,但是基于ES现有的评分结果,综合排序效果很差。结合用户使用场景,我们拟订以下优化方案:
1. 补充检索内容
巧妇难为无米炊,缺少数据的情况下查询效果一定差。补充企业产品、品牌词等内容,满足用户查找线索的常用场景。
2. 补充检索词库
ES检索提供通用词库,但是我们场景下需要识别的企业信息与通用词有一定差异,只有词库够强大,分词能力才能更强。
3. 调整检索评分
检索评分直接影响检索排序,而排序是给用户的第一观感。
在实操过程中,我们是3,1,2的顺序来进行的优化,以下也将按实操过程来给大家展示优化效果。
在检索评分上,我们做了两方面的调整:
一方面,针对不同检索内容项,设置不同权重,将公司名称设置最高权重,法人其次,地址和经营范围权重最低。权重调整的效果如下:
调整前
调整后
检索词【开店】,调整前,所有检索内容权重相同,由于法人姓名长度较短,检索词若命中法人姓名,得分较高,因此大部分命中法人的信息会排在前面。在调整后,我们增加了公司名称的权重,减少了由于法人字段过短造成的高分影响,排在前位的较多是公司名称命中的数据。
上述检索虽调整了检索范围的权重,但是从检索效果来看并不理想。主要原因在于公司名称中个体工商一般名称较短,命中检索词的时候会获得更高的评分,导致排序靠前的数据大部分为个体工商户。
我们又做了第二步调整,增加「企业」类型的检索评分(_score*2),同时增加注册资本大于100w的公司得分(_score*2)。效果如下:
经过两轮调整,目前的检索效果基本符合预期。
优化2:补充检索内容
在销售机会查找的业务场景下,用户经常使用产品词、品牌词等进行搜索。为此,我们首先引入了商标数据,补充检索内容。
引入数据量400w+
引入前效果:
引入后:
优化3:补充检索词库
为了让ES更加准确的识别用户输入的信息,我们拟订从公司名称中拆解出一些分词,补充到检索词库中。
针对公司名称的拆词,使用现有策略模型,公司名称拆词的效果如下:
拟订将【K】【B】类输入到ES词库中。
效果,未完待续。。。。
(1) [endif]入库分词和检索词使用不同粒度:入库分词存储时,公司名称、法人、地址采用细粒度分词,主营业务采用粗粒度分词。检索时采用粗粒度分词。
避免拆词过细,减少了match的总条数
(1) ES检索词库补充
(2) 增加更多检索内容,品牌、产品等
6. 深入研究查询Elasticsearch,过滤查询和全文搜索
或如何了解缺少哪些官方文件
如果我不得不用一个短语来描述Elasticsearch,我会说:
目前,Elasticsearch在十大最受欢迎的开源技术中。 公平地说,它结合了许多本身并不独特的关键功能,但是,当结合使用时,它可以成为最佳的搜索引擎/分析平台。
更准确地说,由于以下功能的结合,Elasticsearch变得如此流行:
· 搜索相关性评分
· 全文搜索
· 分析(汇总)
· 无模式(对数据模式无限制),NoSQL,面向文档
· 丰富的数据类型选择
· 水平可扩展
· 容错的
通过与Elasticsearch进行合作,我很快意识到,官方文档看起来更像是所谓文档的"挤压"。 我不得不在Google上四处搜寻,并且大量使用stackowerflow,所以我决定编译这篇文章中的所有信息。
在本文中,我将主要撰写有关查询/搜索Elasticsearch集群的文章。 您可以通过多种不同的方式来实现大致相同的结果,因此,我将尝试说明每种方法的利弊。
更重要的是,我将向您介绍两个重要的概念-查询和过滤器上下文-在文档中没有很好地解释。 我将为您提供一组规则,以决定何时使用哪种方法更好。
在阅读本文后,如果我只想让您记住一件事,那就是:
当我们谈论Elasticsearch时,总会有一个相关性分数。 相关性分数是严格的正浮点数,表示每个文档满足搜索标准的程度。 该分数是相对于分配的最高分数的,因此,分数越高,文档与搜索条件的相关性越好。
但是,过滤器和查询是您在编写查询之前应该能够理解的两个不同概念。
一般来说,过滤器上下文是一个"是/否"选项,其中每个文档都与查询匹配或不匹配。 一个很好的例子是SQL WHERE,后面是一些条件。 SQL查询总是返回严格符合条件的行。 SQL查询无法返回歧义结果。
另一方面,Elasticsearch查询上下文显示了每个文档与您的需求的匹配程度。 为此,查询使用分析器查找最佳匹配。
经验法则是将过滤器用于:
· 是/否搜索
· 搜索精确值(数字,范围和关键字)
将查询用于:
· 结果不明确(某些文档比其他文档更适合)
· 全文搜索
此外,Elasticsearch将自动缓存过滤器的结果。
在第1部分和第2部分中,我将讨论查询(可以转换为过滤器)。 请不要将结构化和全文与查询和过滤器混淆-这是两件事。
结构化查询也称为术语级查询,是一组查询方法,用于检查是否应选择文档。 因此,在很多情况下,没有真正必要的相关性评分-文档匹配或不匹配(尤其是数字)。
术语级查询仍然是查询,因此它们将返回分数。
名词查询 Term Query
返回字段值与条件完全匹配的文档。 查询一词是SQL select * from table_name where column_name =...的替代方式
名词查询直接进入倒排索引,这可以使其快速进行。 在处理文本数据时,最好仅将term用于keyword字段。
名词查询默认情况下在查询上下文中运行,因此,它将计算分数。 即使所有返回的文档的分数相同,也将涉及其他计算能力。
带有过滤条件的 名词 查询
如果我们想加速名词查询并使其得到缓存,则应将其包装在constant_score过滤器中。
还记得经验法则吗? 如果您不关心相关性得分,请使用此方法。
现在,该查询没有计算任何相关性分数,因此,它更快。 而且,它是自动缓存的。
快速建议-对文本字段使用匹配而不是名词。
请记住,名词查询直接进入倒排索引。名词查询采用您提供的值并按原样搜索它,这就是为什么它非常适合查询未经任何转换存储的keyword字段。
多名词查询 Terms query
如您所料,多名词查询使您可以返回至少匹配一个确切名词的文档。
多名词查询在某种程度上是SQL select * from table_name where column_name is in...的替代方法
重要的是要了解,Elasticsearch中的查询字段可能是一个列表,例如{“ name”:[“ Odin”,“ Woden”,“ Wodan”]}。如果您执行的术语查询包含以下一个或多个,则该记录将被匹配-它不必匹配字段中的所有值,而只匹配一个。
与名词查询相同,但是这次您可以在查询字段中指定多少个确切术语。
您指定必须匹配的数量-一,二,三或全部。 但是,此数字是另一个数字字段。 因此,每个文档都应包含该编号(特定于该特定文档)。
返回查询字段值在定义范围内的文档。
等价于SQL select * from table_name where column_name is between...
范围查询具有自己的语法:
· gt 大于
· gte 大于或等于
· lt 小于
· lte 小于或等于
一个示例,该字段的值应≥4且≤17
范围查询也可以很好地与日期配合使用。
正则表达式查询返回其中字段与您的正则表达式匹配的文档。
如果您从未使用过正则表达式,那么我强烈建议您至少了解一下它是什么以及何时可以使用它。
Elasticsearch的正则表达式是Lucene的正则表达式。 它具有标准的保留字符和运算符。 如果您已经使用过Python的re软件包,那么在这里使用它应该不是问题。 唯一的区别是Lucene的引擎不支持^和$等锚运算符。
您可以在官方文档中找到regexp的完整列表。
除正则表达式查询外,Elsticsearch还具有通配符和前缀查询。从逻辑上讲,这两个只是regexp的特殊情况。
不幸的是,我找不到关于这三个查询的性能的任何信息,因此,我决定自己对其进行测试,以查看是否发现任何重大差异。
在比较使用rehexp和通配符查询时,我找不到性能上的差异。如果您知道有什么不同,请给我发消息。
由于Elasticsearch是无模式的(或没有严格的模式限制),因此当不同的文档具有不同的字段时,这是一种很常见的情况。 结果,有很多用途来了解文档是否具有某些特定字段。
全文查询适用于非结构化文本数据。 全文查询利用了分析器。 因此,我将简要概述Elasticsearch的分析器,以便我们可以更好地分析全文查询。
每次将文本类型数据插入Elasticsearch索引时,都会对其进行分析,然后存储在反向索引中。根据分析器的配置方式,这会影响您的搜索功能,因为分析器也适用于全文搜索。
分析器管道包括三个阶段:
总有一个令牌生成器和零个或多个字符和令牌过滤器。
1)字符过滤器按原样接收文本数据,然后可能在对数据进行标记之前对其进行预处理。 字符过滤器用于:
· 替换与给定正则表达式匹配的字符
· 替换与给定字符串匹配的字符
· 干净的HTML文字
2)令牌生成器将字符过滤器(如果有)之后接收到的文本数据分解为令牌。 例如,空白令牌生成器只是将文本分隔为空白(这不是标准的)。 因此,Wednesday is called after Woden, 将被拆分为[Wednesday, is, called, after, Woden.]。 有许多内置标记器可用于创建自定义分析器。
删除标点符号后,标准令牌生成器将使用空格分隔文本。 对于绝大多数语言来说,这是最中立的选择。
除标记化外,标记化器还执行以下操作:
· 跟踪令牌顺序,
· 注释每个单词的开头和结尾
· 定义令牌的类型
3)令牌过滤器对令牌进行一些转换。您可以选择将许多不同的令牌过滤器添加到分析器中。一些最受欢迎的是:
· 小写
· 词干(存在多种语言!)
· 删除重复
· 转换为等效的ASCII
· 模式的解决方法
· 令牌数量限制
· 令牌的停止列表(从停止列表中删除令牌)
标准分析器是默认分析器。 它具有0个字符过滤器,标准令牌生成器,小写字母和停止令牌过滤器。 您可以根据需要组成自定义分析器,但是内置分析器也很少。
语言分析器是一些最有效的即用型分析器,它们利用每种语言的细节来进行更高级的转换。 因此,如果您事先知道数据的语言,建议您从标准分析器切换为数据的一种语言。
全文查询将使用与索引数据时使用的分析器相同的分析器。更准确地说,您查询的文本将与搜索字段中的文本数据进行相同的转换,因此两者处于同一级别。
匹配查询是用于查询文本字段的标准查询。
我们可以将匹配查询称为名词查询的等效项,但适用于文本类型字段(而在处理文本数据时,名词应仅用于关键字类型字段)。
默认情况下,传递给查询参数的字符串(必需的一个)将由与应用于搜索字段的分析器相同的分析器处理。 除非您自己使用analyzer参数指定分析器。
当您指定要搜索的短语时,将对其进行分析,并且结果始终是一组标记。默认情况下,Elasticsearch将在所有这些标记之间使用OR运算符。这意味着至少应该有一场比赛-更多的比赛虽然会得分更高。您可以在运算符参数中将其切换为AND。在这种情况下,必须在文档中找到所有令牌才能将其返回。
如果要在OR和AND之间输入某些内容,则可以指定minimum_should_match参数,该参数指定应匹配的子句数。 可以数字和百分比指定。
模糊参数(可选)可让您忽略错别字。 Levenshtein距离用于计算。
如果您将匹配查询应用于关键字keyword字段,则其效果与词条查询相同。 更有趣的是,如果将存储在反向索引中的令牌的确切值传递给term查询,则它将返回与匹配查询完全相同的结果,但是会更快地返回到反向索引。
与匹配相同,但顺序和接近度很重要。 匹配查询不了解序列和接近度,因此,只有通过其他类型的查询才能实现词组匹配。
match_phrase查询具有slop参数(默认值为0),该参数负责跳过术语。 因此,如果您指定斜率等于1,则短语中可能会省略一个单词。
多重比对查询的功能与比对相同,唯一的不同是多重比对适用于多个栏位
· 字段名称可以使用通配符指定
· 默认情况下,每个字段均加权
· 每个领域对得分的贡献都可以提高
· 如果没有在fields参数中指定任何字段,那么将搜索所有符合条件的字段
有多种类型的multi_match。 我不会在这篇文章中描述它们,但是我将解释最受欢迎的:
best_fields类型(默认值)更喜欢在一个字段中找到来自搜索值的令牌的结果,而不是将搜索的令牌分配到不同字段中的结果。
most_fields与best_fields类型相反。
phrase类型的行为与best_fields相同,但会搜索与match_phrase类似的整个短语。
我强烈建议您查阅官方文档,以检查每个字段的得分计算准确度。
复合查询将其他查询包装在一起。 复合查询:
· 结合分数
· 改变包装查询的行为
· 将查询上下文切换到过滤上下文
· 以上任意一项
布尔查询将其他查询组合在一起。 这是最重要的复合查询。
布尔查询使您可以将查询上下文中的搜索与过滤器上下文搜索结合在一起。
布尔查询具有四个可以组合在一起的出现(类型):
· must或"必须满足该条款"
· should或"如果满足条款,则对相关性得分加分"
· 过滤器filter或"必须满足该条款,但不计算相关性得分"
· must_not或“与必须相反”,不会有助于相关度得分
必须和应该→查询上下文
过滤器和must_not→过滤器上下文
对于那些熟悉SQL的人,必须为AND,而应为OR运算符。 因此,必须满足must子句中的每个查询。
对于大多数查询,提升查询与boost参数相似,但并不相同。 增强查询将返回与肯定子句匹配的文档,并降低与否定子句匹配的文档的得分。
如我们在术语查询示例中先前看到的,constant_score查询将任何查询转换为相关性得分等于boost参数(默认值为1)的过滤器上下文。
让我知道是否您想阅读另一篇文章,其中提供了所有查询的真实示例。
我计划在Elasticsearch上发布更多文章,所以不要错过。
你已经读了很长的内容,所以如果你阅读到这里:
综上所述,Elasticsearch符合当今的许多用途,有时很难理解什么是最佳工具。
如果不需要相关性分数来检索数据,请尝试切换到过滤器上下文。
另外,了解Elasticsearch的工作原理也至关重要,因此,我建议您始终了解分析器的功能。
Elasticsearch中还有许多其他查询类型。 我试图描述最常用的。 我希望你喜欢它。
(本文翻译自kotartemiy ✔️的文章《Deep Dive into Querying Elasticsearch. Filter vs Query. Full-text search》,参考:https://towardsdatascience.com/deep-pe-into-querying-elasticsearch-filter-vs-query-full-text-search-b861b06bd4c0)
7. ES大数据量下的查询优化
filesystem类似于我们在mysql上建立一层redis缓存;
es的搜索引擎严重依赖于底层的filesystem cache,如果给filesystem cache更多的内存,尽量让内存可以容纳所有的indx segment file索引数据文件,那么你搜索的时候就基本都是走内存的,性能会非常高。
两者差距非常大,走磁盘和走systenfile cache的读取的性能差距可以说是秒级和毫秒级的差距了;
要让es性能要好,最佳的情况下,就是我们的机器的内存,至少可以容纳你的数据量的一半
最佳的情况下,是仅仅在es中就存少量的数据,存储要用来搜索的那些索引,内存留给filesystem cache的,如果就100G,那么你就控制数据量在100gb以内,相当于是,你的数据几乎全部走内存来搜索,性能非常之高,一般可以在1秒以内
的少数几个字段就可以了,比如说,就写入es id name age三个字段就可以了,然后你可以把其他的字段数据存在mysql里面,我们一般是建议用 es + hbase 的一个架构。 hbase的特点是适用于海量数据的在线存储,就是对hbase可以写入海量数据,不要做复杂的搜索,就是做很简单的一些根据id或者范围进行查询的这么一个操作就可以了
如果确实内存不足,但是我们又存储了比较多的数据,比如只有30g给systemfile cache,但是存储了60g数据情况,这种情况可以做数据预热;
我们可以将一些高频访问的热点数据(比如微博知乎的热榜榜单数据,电商的热门商品(旗舰版手机,榜单商品信息)等等)提前预热,定期访问刷到我们es里;(比如定期访问一下当季苹果旗舰手机关键词,比如现在的iphone12)
对于那些你觉得比较热的,经常会有人访问的数据,最好做一个专门的缓存预热子系统,就是对热数据,每隔一段时间,提前访问一下,让数据进入filesystem cache里面去。这样下次别人访问的时候,一定性能会好一些。
我们可以将冷数据写入一个索引中,然后热数据写入另外一个索引中,这样可以确保热数据在被预热之后,尽量都让他们留在filesystem os cache里,别让冷数据给冲刷掉。
尽量做到设计document的时候就把需要数据结构都做好,这样搜索的数据写入的时候就完成。对于一些太复杂的操作,比如join,nested,parent-child搜索都要尽量避免,性能都很差的。
es的分页是较坑的 ,为啥呢?举个例子吧,假如你每页是10条数据,你现在要查询第100页,实际上是会把 每个shard上存储的前1000条数据都查到 一个协调节点上,如果你有个5个shard,那么就有5000条数据,接着 协调节点对这5000条数据进行一些合并、处理,再获取到最终第100页的10条数据。
因为他是分布式的,你要查第100页的10条数据,你是不可能说从5个shard,每个shard就查2条数据?最后到协调节点合并成10条数据?这样肯定不行,因为我们从单个结点上拿的数据几乎不可能正好是所需的数据。我们必须得从每个shard都查1000条数据过来,然后根据你的需求进行排序、筛选等等操作,最后再次分页,拿到里面第100页的数据。
你翻页的时候,翻的越深,每个shard返回的数据就越多,而且协调节点处理的时间越长。非常坑爹。所以用es做分页的时候,你会发现越翻到后面,就越是慢。
我们之前也是遇到过这个问题,用es作分页,前几页就几十毫秒,翻到10页之后,几十页的时候,基本上就要5~10秒才能查出来一页数据了
你系统不允许他翻那么深的页,或者产品同意翻的越深,性能就越差
如果是类似于微博中,下拉刷微博,刷出来一页一页的,可以用scroll api scroll api1 scroll api2 scroll会一次性给你生成所有数据的一个快照,然后每次翻页就是通过游标移动 ,获取下一页下一页这样子,性能会比上面说的那种分页性能也高很多很多
scroll的原理实际上是保留一个数据快照,然后在一定时间内,你如果不断的滑动往后翻页的时候,类似于你现在在浏览微博,不断往下刷新翻页。那么就用scroll不断通过游标获取下一页数据,这个性能是很高的,比es实际翻页要好的多的多。
缺点: