Elastic Search
2024-10-29 17:44:49 1 举报
AI智能生成
Elastic Search是一个高度可扩展的开源搜索和分析引擎,用于处理海量数据。它支持全文搜索,结构化查询,和复杂的数据分析。Elastic Search以JSON格式存储数据,并允许进行实时索引和搜索。它具有出色的性能和易于使用的RESTful API,使其成为构建现代数据驱动应用的理想选择。
作者其他创作
大纲/内容
ES基础
是什么
Elaticsearch简称为ES,是一个开源的可扩展的<b>分布式的全文检索引擎</b>,它可以近乎实时的存储、检索数据。本身扩展性很好,可扩展到上百台服务器,处理<b>PB级别</b>的数据。ES使用Java开发并使用L<b>ucene作为其核心</b>来实现索引和搜索的功能,但是它通过简单的<b>RestfulAPI</b>和<b>javaAPI</b>来隐藏Lucene的复杂性。
<br>
<b>为什么要有ES?(基于数据库查询的问题)</b><br>
查询效率低,模糊查询数据库不会使用索引 (有些查询会导致索引失效)
查询的准确率不高 (功能弱)
<b>功能</b><br>
<b>分布式搜索引擎</b>
分布式:Elasticsearch<b>自动将海量数据分散到多台服务器上去存储和检索</b> 搜索:百度、谷歌,站内搜索
<b>全文检索</b>
提供模糊搜索等自动度很高的查询方式,并进行相关性排名,高亮等功能
<b>数据的分析引擎(分组聚合)</b><br>
电商网站,最近一周笔记本电脑这种商品销量排名top10的商家有哪些?新闻网站,最近1个月访 问量排名top3的新闻板块是哪些
<b>对海量数据进行实时的处理</b><br>
海量数据的处理:因为是分布式架构,Elasticsearch可以采用大量的服务器去存储和检索数据,自 然而然就可以实现海量数据的处理 近实时:Elasticsearch可以实现秒级别的数据搜索和分析
<b>特点</b>
<b>speed 高速</b>
相比较其它 的一些大数据引擎,Elasticsearch可以实现秒级的搜索,速度非常有优势。
<b>scale 易扩展性</b>
<b>relevance 相关性</b><br>
Elasticsearch是它搜索的结果可以按照分数进行排序,它能提供我们最相关的搜索结果
<b>节点对等</b><br>
<b>支持超大量数据</b>
可以扩展到 PB 级的结构化和非结构化数据 海量数据的近实时处理
企业使用场景
<b>使用场景</b>
<b>搜索类场景</b>
比如说电商网站、招聘网站、新闻资讯类网站、各种app内的搜索。
<b>日志分析类场景</b>
经典的ELK组合(Elasticsearch/Logstash/Kibana),可以完成日志收集,日志存储,日志分析查 询界面基本功能,目前该方案的实现很普及,大部分企业日志分析系统使用了该方案。
<b>数据预警平台及数据分析场景</b>
例如电商价格预警,在支持的电商平台设置价格预警,当优惠的价格低于某个值时,触发通知消 息,通知用户购买。
数据分析常见的比如分析电商平台销售量top 10的品牌,分析博客系统、头条网站top 10关注度、 评论数、访问量的内容等等。
<b>商业BI(Business Intelligence)系统</b>
比如大型零售超市,需要分析上一季度用户消费金额,年龄段,每天各时间段到店人数分布等信息,输出相应的报表数据,并预测下一季度的热卖商品,根据年龄段定向推荐适宜产品。 Elasticsearch执行数据分析和挖掘,Kibana做数据可视化。
<b>常见案例</b>
- 维基百科、百度百科:有全文检索、高亮、搜索推荐功能
- stack overflow 、CSDN:有全文检索,可以根据报错关键信息,去搜索解决方法。
- github:从上千亿行代码中搜索你想要的关键代码和项目。
- 日志分析系统:各企业内部搭建的ELK平台。
<b>通用数据处理流程</b><br>
<br>
<b>主流全文搜索方案对比</b>
Lucene
Lucene是Apache基金会维护的一套完全<b>使用Java编写的信息搜索工具包(Jar包)</b>,它包含了索引结构、读写索引工具、相关性工具、排序等功能,因此在使用Lucene时仍需要我们自己进一步开 发搜索引擎系统,例如数据获取、解析、分词等方面的东西。 注意:<b>Lucene只是一个框架,我们需要在Java程序中集成它再使用。而且需要很多的学习才能明 白它是如何运行的,熟练运用Lucene非常复杂。</b>
Solr
Solr是一个有HTTP接口的基于Lucene的查询服务器,是一个搜索引擎系统,封装了很多Lucene细 节,Solr可以直接利用HTTP GET/POST请求去查询,维护修改索引。
ES
Elasticsearch也是一个建立在全文搜索引擎 Apache Lucene基础上的搜索引擎。采用的策略是分布式实时文件存储,并将每一个字段都编入索引,使其可以被搜索
<b><font color="#314aa4">三者之间的区别和联系</font></b>
<b>Solr和Elasticsearch都是基于Lucene实现的。但Solr和Elasticsearch之间也是有区别的 </b><br><br><ul><li>1)Solr利用Zookpper进行分布式管理,而Elasticsearch自身带有分布式协调管理功能 。</li><li>2)<b>Solr比Elasticsearch实现更加全面,Solr官方提供的功能更多</b>,而<b>Elasticsearch本身更注重于核心功能</b>, 高级功能多由第三方插件提供。</li><li><b>3)Solr在传统的搜索应用中表现好于Elasticsearch,而Elasticsearch在实时搜索应用方面比Solr表现好。</b></li></ul>
<b><font color="#314aa4">Elasticsearch 与 Lucene 核心库竞争的优势在于</font></b>
完美封装了 Lucene 核心库,设计了友好的 Restful-API,开发者无需过多关注底层机制,直接开箱即用。
分片与副本机制,直接解决了集群下性能与高可用问题
<b>ES 与其他软件兼容</b><br>
操作系统
JVM
Elastic Stack生态和场景方案
<b>Elastic Stack生态</b>
<b>Beats + Logstash + ElasticSearch + Kibana</b>
由于Elastic X-Pack是面向收费的,所以我们不妨也把<b>X-Pack</b>放进去,看看哪些是由X-Pack带来的,在阅读官网文档时将方便你甄别重点:
<b>Beats</b>
<b>Beats是一个面向轻量型采集器的平台</b>,这些采集器可以从边缘机器向Logstash、ElasticSearch发送数据,它是由Go语言进行开发的,运行效率方面比较快。从下图中可以看出,不同Beats的套件是针对不同的数据源。
<b>Logstash</b><br>
Logstash是<b>动态数据收集管道</b>,拥有可扩展的插件生态系统,<b>支持从不同来源采集数据,转换数据,并将数据发送到不同的存储库中</b>。其能够与ElasticSearch产生强大的协同作用,后被Elastic公司在2013年收购<br>
它具有如下特性:<br><b>1)实时解析和转换数据</b>;<br><b>2)可扩展,具有200多个插件;</b><br><b>3)可靠性、安全性</b>。Logstash会通过持久化队列来保证至少将运行中的事件送达一次,同时将数据进行传输加密;<br><b>4)监控;</b><br>
<b>ElasticSearch</b>
ElasticSearch对数据进行<b>搜索、分析和存储</b>,其<b>是基于JSON的分布式搜索和分析引擎</b>,专门为实现水平可扩展性、高可靠性和管理便捷性而设计的。<br><br>它的实现原理主要分为以下几个步骤:<br>1)首先用户将数据提交到ElasticSearch数据库中;<br>2)再通过分词控制器将对应的语句分词;<br>3)将分词结果及其权重一并存入,以备用户在搜索数据时,根据权重将结果排名和打分,将返回结果呈现给用户;<br>
<b>Kibana</b><br>
<b>Kibana实现数据可视化</b>,其作用就是在ElasticSearch中进行民航。Kibana能够以图表的形式呈现数据,并且具有可扩展的用户界面,可以全方位的配置和管理ElasticSearch。<br><br>Kibana最早的时候是基于Logstash创建的工具,后被Elastic公司在2013年收购。<br>1)Kibana可以提供各种可视化的图表;<br>2)可以通过机器学习的技术,对异常情况进行检测,用于提前发现可疑问题<br>
从日志收集系统看ES Stack的发展
我们看下ELK技术栈的演化,通常体现在日志收集系统中。
一个典型的日志系统包括:<br><b>(1)收集</b>:能够采集多种来源的日志数据<br><b>(2)传输</b>:能够稳定的把日志数据解析过滤并传输到存储系统<br><b>(3)存储</b>:存储日志数据<br><b>(4)分析</b>:支持 UI 分析<br><b>(5)警告</b>:能够提供错误报告,监控机制<br>
<b>beats+elasticsearch+kibana</b>
Beats采集数据后,存储在ES中,有Kibana可视化的展示。
<b>beats+logstath+elasticsearch+kibana</b>
该框架是在上面的框架的基础上引入了logstash,引入logstash带来的好处如下:<br><br><b>(1)Logstash具有<font color="#e74f4c">基于磁盘的自适应缓冲系统,该系统将吸收传入的吞吐量,从而减轻背压</font>。</b><br><b>(2)从其他数据源(例如数据库,S3或消息传递队列)中提取</b>。<br><b>(3)将数据发送到多个目的地</b>,例如S3,HDFS或写入文件。<br><b>(4)使用条件数据流逻辑组成更复杂的处理管道。</b><br>
<b>beats结合logstash带来的优势</b>
<b>(1)水平可扩展性,高可用性和可变负载处理</b>:beats和logstash可以实现节点之间的负载均衡,多个logstash可以实现logstash的高可用
<b>(2)消息持久性与至少一次交付保证</b>:使用beats或Winlogbeat进行日志收集时,可以保证至少一次交付。从Filebeat或Winlogbeat到Logstash以及从Logstash到Elasticsearch的两种通信协议都是同步的,并且支持确认。Logstash持久队列提供跨节点故障的保护。对于Logstash中的磁盘级弹性,确保磁盘冗余非常重要。
<b>(3)具有身份验证和有线加密的端到端安全传输:</b>从Beats到Logstash以及从 Logstash到Elasticsearch的传输都可以使用加密方式传递 。与Elasticsearch进行通讯时,有很多安全选项,包括基本身份验证,TLS,PKI,LDAP,AD和其他自定义领域
<b>增加更多的数据源: </b>比如:TCP,UDP和HTTP协议是将数据输入Logstash的常用方法<br>
<b>beats+MQ+logstash+elasticsearch+kibana</b>
在如上的基础上我们可以在beats和logstash中间添加一些组件redis、kafka、RabbitMQ等,添加中间件将会有如下好处:<br><br><b>(1)降低对日志所在机器的影响</b>,这些机器上一般都部署着反向代理或应用服务,本身负载就很重了,所以尽可能的在这些机器上少做事;<br>(2)如果有很多台机器需要做日志收集,那么让每台机器都向Elasticsearch持续写入数据,必然会对Elasticsearch造成压力,因此需要<b>对数据进行缓冲</b>,同时,这样的缓冲也可以一定程度的保护数据不丢失;<br><b>(3)将日志数据的格式化与处理放到Indexer中统一做,可以在一处修改代码、部署,避免需要到多台机器上去修改配置</b><br>
Elastic Stack最佳实践
日志收集系统
基础的日志系统<br>
增加数据源,和使用MQ<br>
Metric收集和APM性能监控<br>
<br>
多数据中心方案<br>
通过冗余实现数据高可用
两个数据采集中心(比如采集两个工厂的数据),采集数据后的汇聚
数据分散,跨集群的搜索<br>
ES入门使用
核心概念
索引(Index)
ElasticSearch存储数据的地方,可以理解成关系型数据库中的<b>数据库</b>概念。
映射(Mapping)
mapping定义了每个字段的类型、字段所使用的分词器等。相当于关系型数据库中的<b>表结构</b>。
文档(Ducoment)
Elasticsearch中的最小数据单元,常以json格式显示。<b>一个document相当于关系型数据库中的一行数据(一条记录)</b>。
域(Feild)
列
倒排索引
<b>一个倒排索引由文档中所有不重复词的列表构成</b>,对于其中每个词,对应一个包含它的文档id列表。<br>
类型(Type)
一种type就像一类`表`。如用户表、 <b>在Elasticsearch7.X默认type为_doc</b>
ES 5.x中一个index可以有多种type。<br> ES 6.x中一个index只能有一种type。<br> ES 7.x以后,将逐步移除type这个概念,现在的操作已经不再使用,默认_doc
文档元数据
<br>
<b>_index</b>:文档所属的索引名
<b>_type</b>:文档所属的类型名
<b>_id</b>:文档唯—ld
<b>_source</b>: 文档的原始Json数据
<b>_version:</b> 文档的版本号,修改删除操作_version都会自增1
<b>_seq_no</b>: 和_version一样,一旦数据发生更改,数据也一直是累计的<b>。Shard级别严格递增,保证后写入的Doc的_seq_no大于先写入的Doc的_seq_no。</b>
<b>primary_term</b>: <font color="#e74f4c">_primary_term 主要是用来恢复数据时处理当多个文档的 _seq_no 一样时的冲突,避免Primary Shard上的写入被覆盖</font>。<b>每当Primary Shard发生重新分配时,比如重启,Primary选举等,_primary_term会递增1。</b>
倒排索引<br>
<b>基于数据库查询的问题</b>
<ul><li>- 查询效率低,模糊查询数据库不会使用索引 (有些查询会导致索引失效)</li><li>- 查询的准确率不高 (功能弱)</li></ul>
<b><font color="#e74f4c">将文档进行分词,形成词条和文档id的对应关系即为倒排索引</font></b>
<br>
<b>ES存储和查询的原理</b><br>
<b>ES解决数据库查询功能弱: 通过对数据进行分词来解决</b>
<b>ES解决数据库查询效率低:对分词的结构进行排序,然后进行了一个树形结构</b><br>
IK分词器
<b>IKAnalyzer是一个开源的,基于java语言开发的轻量级的中文分词工具包</b>。从2006年12月推出1.0版 开始,IKAnalyzer已经推出 了3个大版本。最初,它是以开源项目Lucene为应用主体的,结合词典分词 和文法分析算法的中文分词组件。新版本的IKAnalyzer3.0则发展为 面向Java的公用分词组件,独立于 Lucene项目,同时提供了对Lucene的默认优化实现。<br><br>IK分词器3.0的特性如下:<br>1)采用了特有的“<b>正向迭代最细粒度切分算法</b>“,具有60万字/秒的高速处理能力。<br>2)采用了多子处理器分析模式,支持:英文字母(IP地址、Email、URL)、数字(日期,常用中文数 量词,罗马数字,科学计数法),中文词汇(姓名、地名处理)等分词处理。<br>3)支持个人词条的优化的词典存储,更小的内存占用。<br>4)支持用户词典扩展定义。<br>5)针对Lucene全文检索优化的查询分析器IKQueryParser;采用歧义分析算法优化查询关键字的搜索 排列组合,能极大的提高Lucene检索的命中率。<br>
<b>ElasticSearch 默认使用的分词器是 <font color="#e74f4c">standard Analyzer</font> , 会把中文一个字分成一个词</b>
<b>分词类型</b>
<b>ik_max_word</b>
细粒度,分的词条多
<b>ik_smart</b>
粗粒度, 分的词条少
<ul><li><font color="#e74f4c">被搜索的内容想被更多的匹配到就可以使用`细粒度`</font></li><li><font color="#e74f4c">搜索关键字,更像精准匹配到搜索的内容,关键字可以使用`粗粒度`</font></li></ul>
<b>扩展词典</b>
<b>停用词典</b>
<b>同义词典</b><br>
语言博大精深,有很多相同意思的词,我们称之为同义词,比如“番茄”和“西红柿”,“馒头”和“馍”等。在 搜索的时候,我们输入的可能是“番茄”,但是应该把含有“西红柿”的数据一起查询出来,这种情况叫做 同义词查询。<br><font color="#e74f4c"><b>注意:扩展词和停用词是在索引的时候使用,而同义词是检索时候使用。</b></font>
索引操作
Restful
Java API
ES数据结构
简单数据类型
字符串
<b>- text: </b> 会分词,不支持聚合 <br><b>- keyword:</b> 不会分词,将全部内容作为一个词条,支持聚合<br><b># 聚合:</b>相当于mysql 中的聚合函数 => sum(求和)<br>
数值
<br>
布尔(Boolean)<br>
二进制(Binary)
范围类型
integer_range, float_range, long_range, double_range, date_range
日期(Date)<br>
复杂数据类型<br>
数组 []
对象 {}
ES映射操作
索引创建之后,等于有了关系型数据库中的database。Elasticsearch7.x取消了索引type类型的设置, 不允许指定类型,默认为_doc,但字段仍然是有的,我们需要设置字段的约束信息,叫做字段映射 (mapping)<br>字段的约束包括但不限于:<br><ul><li><b>字段的数据类型</b></li><li><b>是否要存储</b></li><li><b>是否要索引</b></li><li><b>分词器</b></li></ul><b></b>
<b>创建映射字段</b>
字段名:任意填写,下面指定许多属性,例如:<br><ul><li><b>type:类型</b>,可以是text、long、short、date、integer、object等 </li><li><b>index:是否索引</b>,默认为true</li><li><b>store:是否存储</b>,默认为false </li><li><b>analyzer:指定分词器</b></li></ul>
<b>映射属性详解</b>
<b>type:Elasticsearch中支持的数据类型非常丰富</b>
<b>String类型</b>
<b>- text:</b>可分词,不可参与聚合
<b>- keyword</b>:不可分词,数据会作为完整字段进行匹配,可以参与聚合
<b>Numerical:数值类型</b>
<b>- 基本数据类型:</b>long、interger、short、byte、double、float、half_float
<b>- 浮点数的高精度类型:scaled_float</b>
- 需要指定一个精度因子,比如10或100。elasticsearch会把真实值乘以这个因子后存储,取出时再原。
<b>Date:日期类型</b>
elasticsearch可以对日期格式化为字符串存储,但是建议我们存储为毫秒值,存储为long,节省空间。
<b>Array:数组类型</b>
- 进行匹配时,任意一个元素满足,都认为满足
- 排序时,如果升序则用数组中的最小值来排序,如果降序则用数组中的最大值来排序
<b>Object:对象</b>
如果存储到索引库的是对象类型,例如上面的girl,会把girl变成两个字段:girl.name和girl.age
<b>index :index影响字段的索引情况</b>
<ul><li>true:<b>字段会被索引,则可以用来进行搜索</b>。默认值就是true </li><li>false:字段不会被索引,不能用来搜索</li></ul>
index的默认值就是true,也就是说你不进行任何配置,所有字段都会被索引。 但是有些字段是我们不希望被索引的,比如企业的logo图片地址,就需要手动设置index为false。<br>
<b>store : 是否将数据进行独立存储</b>
<span style="font-size:inherit;"><b>原始的文本会存储在 <font color="#e74f4c">_source</font>里面,默认情况下其他提取出来的字段都不是独立存储的,是从 _source里面提取出来的</b>。当然你也可以独立的存储某个字段,只要设置 store:true 即可,<b>获取独立存储的字段要比从_source中解析快得多,但是也会占用更多的空间</b>,所以要根据实际业务需求来设置, 默认为false</span>
<b>analyzer</b><br>
指定分词器 一般我们处理中文会选择ik分词器 ik_max_word、ik_smart
<b>查看映射关系</b><br>
修改映射增加字段 做其它更改只能删除索引 重新建立映射
<b>一次性创建索引和映射</b><br>
刚才 的案例中我们是把创建索引库和映射分开来做,其实也可以在创建索引库的同时,直接制定索引库 中的索引,基本语法
ES文档的增删改查(见笔记)
JAVA API 操作 索引/文档(见笔记)
高级应用
映射高级
<b>Mapping 设置流程图</b><br>
<b>地理坐标点数据类型(geo_point)</b><br>
<ul><li>字符串形式以半角逗号分割,如 "lat,lon"</li><li>对象形式显式命名为 lat 和 lon</li><li>数组形式表示为 [lon,lat] </li></ul>
<b>通过地理坐标点过滤</b><br>
<b>geo_bounding_box</b>
这是目前为止最有效的地理坐标过滤器了,因为它计算起来非常简单。 你指定一个矩形的顶部 , 底部 , 左边界和右边界,然后过滤器只需判断坐标的经度是否在左右边界之间,纬度是否在上下边界之间
location这些坐标也可以用 bottom_left 和 top_right 来表示<br>
<b>geo_distance</b>
过滤仅包含与地理位置相距特定距离内的匹配的文档。假设以下映射和索引文档 然后可以使用 geo_distance 过滤器执行以下查询
<b>动态映射</b>
<b>Elasticsearch在遇到文档中以前未遇到的字段</b>,可以使用<b>dynamic mapping(动态映射机制)</b> 来确定字段的数据类型并自动把新的字段添加到类型映射。 <br>Elastic的动态映射机制可以进行开关控制,通过设置<b>mappings</b>的<font color="#e74f4c">dynamic</font>属性,dynamic有如下设置项<br>
true:遇到陌生字段就执行dynamic mapping处理机制
false:遇到陌生字段就忽略
strict:遇到陌生字段就报错
<b>自定义动态映射(<font color="#e74f4c">dynamic_date_formats</font>)</b>
<b>自动映射可能会出现的问题</b>
<b>使用 <font color="#e74f4c">dynamic_templates </font>可以完全控制新生成字段的映射,甚至可以通过字段名称或数据类型来应用不同的映射</b>
每个模板都有一个名称,你可以用来描述这个模板的用途,一个 mapping 来指定映射应 该怎样使用,以及至少一个参数 (如 match) 来定义这个模板适用于哪个字段。<br><b>模板按照顺序来检测;第一个匹配的模板会被启用</b>。例如,我们给 string 类型字段定义两个模板:<br><ul><li><b>es </b>:以 _es 结尾的字段名需要使用 spanish 分词器。</li><li><b>en</b> :所有其他字段使用 english 分词器。</li></ul>
<br>
Query DSL(Domain Specific Language)
<b>Elasticsearch提供了基于JSON的完整查询DSL(Domain Specific Language 特定域的语言)来定义查询。将查询DSL视为查询的 AST(抽象语法树)</b>,它由两种子句组成:<br><ul><li><b>叶子查询子句: </b>叶子查询子句在特定域中寻找特定的值,如 match,term或 range查询。</li><li><b>复合查询子句:</b>复合查询子句包装其他叶子查询或复合查询,并用于以逻辑方式组合多个查询(例如 bool或 dis_max查询),或更改其行为(例如 constant_score查询)。</li></ul>
查询所有(match_all query)
<ul><li><b>query </b>:代表查询对象 </li><li><b>match_all </b>:代表查询所有</li></ul>
查询结果<br>
<br>
全文搜索(full-text query)<br>
匹配搜索(match query)<br>
全文查询的标准查询,它可以对一个字段进行模糊、短语查询。 match queries 接收 text/numerics/dates, 对它们进行分词分析, 再组织成一个boolean查询。可通过<font color="#e74f4c">operator </font>指定bool组合操作(<b>or、and 默认是 or</b> )。
or关系<br>
match 类型查询,会把查询条件进行分词,然后进行查询,多个词条之间是or的关系
and关系<br>
某些情况下,我们需要更精确查找,我们希望这个关系变成 and ,可以这样做<br>
短语搜索(match phrase query)<br>
match_phrase 查询用来对一个字段进行短语查询,可以指定 <b>analyzer</b>、<b>slop移动因子<br>完全匹配可能比较严,我们会希望有个可调节因子,少匹配一个也满足</b>,那就需要使用到slop。<b></b>
<br>
<br>
<br>
query_string 查询<br>
Query String Query提供了无需指定某字段而对文档全文进行匹配查询的一个高级查询,<b>同时可以指定在哪些字段上进行匹配</b>。
多字段匹配搜索(multi match query)<br>
如果你需要<b>在多个字段上进行文本搜索</b>,可用multi_match 。multi_match在 match的基础上支持对多 个字段进行文本查询。
词条级搜索(term-level queries)<br>
<b>可以使用term-level queries根据结构化数据中的精确值查找文档</b>。结构化数据的值包括日期范围、IP 地址、价格或产品ID。 <br><b>与全文查询不同,<font color="#e74f4c">term-level queries不分析搜索词。相反,词条与存储在字段级别中的术语完全匹配</font></b>。<br>
<b>词条搜索(term query)</b><br>
term 查询用于查询指定字段包含某个词项的文档
词条集合搜索(terms query)<br>
terms 查询用于查询指定字段包含某些词项的文档<br>
范围搜索(range query)
<ul><li>- gte:大于等于 </li><li>- gt:大于 </li><li>- lte:小于等于 </li><li>- lt:小于 </li><li>- boost:查询权重</li></ul>
<b>boost:用于影响返回结果的相关性评分,在原评分基础上*boost值。如果是2的话,就是2倍打分值,如果是0.5就是原打分的一半。</b>
不为空搜索(exists query)<br>
查询指定字段值不为空的文档。相当 SQL 中的 <font color="#ed9745">column is not null</font>
词项前缀搜索(prefix query)<br>
<br>
通配符搜索(wildcard query)<br>
<br>
正则搜索(regexp query)<br>
regexp允许使用正则表达式进行term查询.<b>注意regexp如果使用不正确,会给服务器带来很严重的性能压力</b>。比如.*开头的查询,将会匹配所有的倒排索引中的关键字,这几乎相当于全表扫描,会很慢。因 此如果可以的话,最好在使用正则前,加上匹配的前缀
模糊搜索(fuzzy query)<br>
在实际的搜索中,我们有时候会打错字,从而导致搜索不到。在Elasticsearch中,我们可以使用<font color="#e74f4c">fuzziness</font>属性来进行模糊查询,从而达到搜索有错别字的情形。 <br><br><font color="#e74f4c">match查询具有“fuziness”属性。它可以被设置为“0”, “1”, “2”或“auto”。“auto”是推荐的选项,它会根据查询词的长度定义距离。</font><br>
ids搜索(id集合查询)<br>
<br>
复核搜索(compound query)<br>
<b>constant_score query</b>
用来包装另一个查询,将查询匹配的文档的评分设为一个常值
<b>布尔搜索(bool query) bool</b>
查询用bool操作来组合多个查询字句为一个查询。 可用的关键字:<br><ul><li><b>must</b><span style="font-size:inherit;">:必须满足</span></li><li><b>filter</b><span style="font-size:inherit;">:必须满足,但执行的是filter上下文,<font color="#e74f4c">不参与、不影响评分</font></span></li><li><b>should</b><span style="font-size:inherit;">:或</span></li><li><b>must_not</b><span style="font-size:inherit;">:必须不满足,在filter上下文中执行,<font color="#e74f4c">不参与、不影响评分</font></span></li></ul>
<font color="#ed9745">minimum_should_match</font>代表了最小匹配精度,如果设置 minimum_should_match=1,那么should 语句中至少需要有一个条件满足,如果没有should语句说明没有一个满足。
在Elasticsearch中,有Query和 Filter两种不同的Context<br><ul><li><b>Query Context: 相关性算分</b></li><li><b>Filter Context: 不需要算分 ,可以利用Cache,获得更好的性能</b></li></ul>
相关性并不只是全文本检索的专利,也适用于yes | no 的子句,匹配的子句越多,相关性评分越高。<br>如果多条查询子句被合并为一条复合查询语句,比如 bool查询,则每个查询子句计算得出的评分会被合并到总的相关性评分中。<br>
bool查询语法<br><ul><li> 子查询可以任意顺序出现</li><li> 可以嵌套多个查询</li><li> 如果你的bool查询中,没有must条件,should中必须至少满足一条查询</li></ul>
排序<br>
相关性评分排序<br>
<b>默认情况下,返回的结果是按照 相关性 进行排序的——最相关的文档排在最前</b>
为了按照相关性来排序,需要将相关性表示为一个数值。在 Elasticsearch 中, 相关性得分 由一 个浮点数进行表示,并在搜索结果中通过 <b>_score </b>参数返回, 默认排序是 <b>_score </b>降序,按照相 关性评分升序排序如下<br>
字段值排序<br>
<br>
多级排序<br>
假定我们想要结合使用 price和 _score(得分) 进行查询,并且匹配的结果首先按照价格排序, 然后按照相关性得分排序:
分页
<ul><li>- <b>size:每页显示多少条 </b></li><li>- <b>from:当前页起始索引</b>, start = (pageNum - 1) * size</li></ul>
高亮
在使用match查询的同时,加上一个highlight属性:<br><ul><li><b> pre_tags:前置标签 </b></li><li><b> post_tags:后置标签 </b></li><li><b> fields:需要高亮的字段</b></li></ul> name:这里声明title字段需要高亮,后面可以为这个字段设置特有配置,也可以空<br>
<br>
结果
文档批量操作(bulk 和 mget)
mget 批量查询
单条查询 GET /test_index/_doc/1,如果查询多个id的文档一条一条查询,网络开销太大
<b>同一索引下批量查询</b>
bulk 批量增删改
<b>Bulk 操作解释将文档的增删改查一些列操作,通过一次请求全都做完。减少网络传输次数</b>
实际用法:<b>bulk请求一次不要太大,否则一下积压到内存中,性能会下降</b>。所以,一次请求几千个操 作、大小在几M正好。<br><br>bulk会将要处理的数据载入内存中,所以数据量是有限的,最佳的数据量不是一个确定的数据,它取决于你的硬件,你的文档大小以及复杂性,你的索引以及搜索的负载。 <b>一般建议是 1000-5000个文档,大小建议是5-15MB,默认不能超过100M</b>,可以在es的配置文件(ES的 config下的 elasticsearch.yml)中配置。 <font color="#ed9745">http.max_content_length: 10mb</font><br>
Filter DSL
<b>Elasticsearch中的所有的查询都会触发相关度得分的计算</b>。对于那些我们不需要相关度得分的场景下, Elasticsearch以过滤器的形式提供了另一种查询功能,过滤器在概念上类似于查询,但是它们有非常快的执行速度,执行速度快主要有以下两个原因:<br><ul><li><b>- 过滤器不会计算相关度的得分,所以它们在计算上更快一些。 </b></li><li>- <b>过滤器可以被缓存到内存中,这使得在重复的搜索查询上,其要比相应的查询快出许多。</b></li></ul>
定位非法搜索及原因(_validate)
在开发的时候,我们可能会写到上百行的查询语句,如果出错的话,找起来很麻烦,Elasticsearch提供了帮助开发人员定位不合法的查询的api <font color="#ed9745"><b>_validate</b></font>
聚合分析
聚合分析是数据库中重要的功能特性,完成对一个查询的数据集中数据的聚合计算,如:找出某字段 (或计算表达式的结果)的最大值、最小值,计算和、平均值等。Elasticsearch作为搜索引擎兼数据库,同样提供了强大的聚合分析能力。<br><b>对一个数据集求最大、最小、和、平均值等指标的聚合,在ES中称为指标聚合 metric, 而关系型数据库中除了有聚合函数外,还可以对查询出的数据进行分组group by,再在组上进行指标聚 合。在 ES 中group by 称为分桶,桶聚合 bucketing<br></b><br>Elasticsearch聚合分析语法:(说明:aggregations 也可简写为 aggs)<br>
指标聚合(metrics)
单值分析:标准stat类型
max min sum avg
文档计数 count
value_count 统计某字段有值的文档数
单值分析:其他类型
cardinality 值去重计数基数 (distinct)
weighted_avg 带权重的avg
median_absolute_deviation 中位值
非单值分析: stats类型<br>
stats 统计 count max min avg sum 5个值
<br>
extended-stats: 高级统计,比stats多4个统计结果: 平方和、方差、标准差、平均值加/减两个标准差的区间
<br>
matrix_stats 针对矩阵模型
<br>
string_stats 针对字符串<br>
<b>用于计算从聚合文档中提取的字符串值的统计信息</b>。这些值可以从特定的关键字字段中检索
<br>
非单值分析:百分数类型<br>
Percentiles 占比百分位对应的值统计<br>
<br>
指定分位值
<br>
Percentiles rank 统计值小于等于指定值的文档占比
<br>
非单值分析:地理位置型
geo_bounds Geo bounds
geo_centroid Geo-centroid
geo_line Geo-Line
非单值分析:Top型
top_hits 分桶后的top hits
top_metrics
桶聚合(Bucketing)<br>
<b>Bucket Aggregations,桶聚合。</b><br>它执行的是对<b>文档分组</b>的操作(与sql中的group by类似),把满足相关特性的文档分到一个桶里,即 桶分,输出结果往往是一个个包含多个文档的桶(一个桶就是一个group)。<br><ul><li><b>bucket</b>:一个数据分组 </li><li><b>metric</b>:对一个数据分组执行的统计</li></ul>
<ul><li>- Terms,需要字段支持filedata</li><li>- keyword: 默认支持fielddata</li><li>- text :需要在Mapping 中开启fielddata,会按照分词后的结果进行分桶</li><li>- 数字类型</li><li>- Range / Data Range</li><li>- Histogram(直方图) / Date Histogram</li><li>- 支持嵌套: 也就在桶里再做分桶</li></ul>
Terms
数字类型
Range<br><ul><li><b> 按照数字的范围,进行分桶</b></li><li><b> 在Range Aggregation中,可以自定义Key</b></li></ul>
嵌套聚合
<br>
<b>对IP类型聚合:IP Range</b>
<b>对日期类型聚合-date-range</b>
<br>
结果
此聚合与Range聚合之间的主要区别在于 from和to值可以在Date Math中表示,并且还可以指定日期格式,通过该日期格式将返回from and to响应字段。请注意,此聚合包括from值,但<b>不包括to每个范围的值</b>
对柱状图功能:Histrogram
<b>直方图 histogram 本质上是就是为柱状图功能设计的。</b><br><br>创建直方图需要指定一个区间,如果我们要为售价创建一个直方图,可以将间隔设为 20,000。这样做将会在每个 $20,000 档创建一个新桶,然后文档会被分到对应的桶中。<br>对于仪表盘来说,我们希望知道每个售价区间内汽车的销量。我们还会想知道每个售价区间内汽车所带来的收入,可以通过对每个区间内已售汽车的售价求和得到。<br>可以用 histogram 和一个嵌套的 sum 度量得到我们想要的答案:<br>
1. histogram 桶要求两个参数:一个数值字段以及一个定义桶大小间隔。<br>2. sum 度量嵌套在每个售价区间内,用来显示每个区间内的总收入。<br>
如我们所见,查询是围绕 price 聚合构建的,它包含一个 histogram 桶。它要求字段的类型必须是数值型的同时需要设定分组的间隔范围。 间隔设置为 20,000 意味着我们将会得到如 [0-19999, 20000-39999, ...] 这样的区间。<br>接着,我们在直方图内定义嵌套的度量,这个 sum 度量,它会对落入某一具体售价区间的文档中 price 字段的值进行求和。 这可以为我们提供每个售价区间的收入,从而可以发现到底是普通家用车赚钱还是奢侈车赚钱。<br>
响应结果
<br>
对应报表:<br>
<br>
<b>Histogram示例:按照工资的间隔分桶</b><br>
<br>
<b>top_hits</b>
<b>top_hits应用场景: 当获取分桶后,桶内最匹配的顶部文档列表</b>
管道聚合 Pipeline Aggregation<br>
管道机制的常见场景
如何理解管道聚合呢?最重要的是要站在设计者角度看这个功能的要实现的目的:<b>让上一步的聚合结果成为下一个聚合的输入,这就是管道</b>。
责任链模式
通过责任链模式, 你可以为某个请求创建一个对象链. 每个对象依序检查此请求并对其进行处理或者将它传给链中的下一个对象
ElasticSearch 设计管道机制<br>
简单而言:<b>让上一步的聚合结果成为下一个聚合的输入,这就是管道</b>。<br>接下来,无非就是对不同类型的聚合有接口的支撑,比如<b>:</b><br>
第一个维度:管道聚合有很多不同<b>类型</b>,每种类型都与其他聚合计算不同的信息,但是可以将这些类型分为两类:<br><ul><li><b>父级</b> 父级聚合的输出提供了一组管道聚合,它可以计算新的存储桶或新的聚合以添加到现有存储桶中。</li><li><b>兄弟</b> 同级聚合的输出提供的管道聚合,并且能够计算与该同级聚合处于同一级别的新聚合。</li></ul>
第二个维度:<b>根据功能设计的意图</b><br>比如前置聚合可能是Bucket聚合,后置的可能是基于Metric聚合,那么它就可以成为一类管<br>
例子<br>
Average bucket 聚合
<br>
输出结果
聚合的作用范围<br>
ES聚合分析的默认作用范围是query的查询结果集,同时ES还支持以下方式改变聚合的作用范围:<br><ul><li><b> Filter</b></li><li><b> Post Filter</b></li><li><b> Global</b></li></ul>
Query
<br>
Filter
<br>
Post Filter
<br>
global
<br>
排序<br>
指定order,按照count和key进行排序:<br><ul><li> 默认情况,按照count降序排序</li><li> 指定size,就能返回相应的桶</li></ul>
ES聚合分析不精准原因分析<br>
<b>ElasticSearch在对海量数据进行聚合分析的时候会损失搜索的精准度来满足实时性的需求</b>
<b>Terms聚合分析的执行流程</b>
<br>
<b>不精准的原因: 数据分散到多个分片,聚合是每个分片的取 Top X,导致结果不精准。ES 可以不每个分片Top X,而是全量聚合,但势必这会有很大的性能问题。</b><br>
<b><font color="#314aa4">思考:如何提高聚合精确度?</font></b>
方案1:设置主分片为1
<b>注意7.x版本已经默认为1。</b><br>
<b>适用场景:数据量小的小集群规模业务场景</b>
方案2:调大 shard_size 值
<b>设置 shard_size 为比较大的值,官方推荐:size*1.5+10。shard_size 值越大,结果越趋近于精准聚合结果值</b>。此外,还可以通过<font color="#ed9745">show_term_doc_count_error</font>参数显示最差情况下的错误值,用于辅助确定 shard_size 大小。<br><ul><li><b>size</b>:是聚合结果的返回值,客户期望返回聚合排名前三,size值就是 3。</li><li><b>shard_size</b>: 每个分片上聚合的数据条数。shard_size 原则上要大于等于 size</li></ul>
<b>适用场景:数据量大、分片数多的集群业务场景。</b>
方案3:将size设置为全量值,来解决精度问题<br>
<b>将size设置为 </b><span class="equation-text" contenteditable="false" data-index="0" data-equation="2^{32} -1"><span></span><span></span></span><b> 也就是分片支持的最大值,来解决精度问题。<br><br></b>原因:1.x版本,size等于 0 代表全部,高版本取消 0 值,所以设置了最大值(大于业务的全量值)。<br><br>全量带来的弊端就是:<b>如果分片数据量极大,这样做会耗费巨大的CPU 资源来排序,而且可能会阻塞网络</b><br>
<b>适用场景:对聚合精准度要求极高的业务场景,由于性能问题,不推荐使用。</b>
方案4:使用Clickhouse/ Spark 进行精准聚合
适用场景:数据量非常大、聚合精度要求高、响应速度快的业务场景。
Elasticsearch 聚合性能优化<br>
启用 eager global ordinals 提升高基数聚合性能<br>
适用场景:<b>高基数聚合 。高基数聚合场景中的高基数含义:一个字段包含很大比例的唯一值</b>
global ordinals 中文翻译成全局序号,是一种数据结构,应用场景如下:<br><ul><li><span style="font-size:inherit;"> 基于 keyword,ip 等字段的分桶聚合,包含:terms聚合、composite 聚合等。</span></li><li><span style="font-size:inherit;"> 基于text 字段的分桶聚合(前提条件是:fielddata 开启)。</span></li><li><span style="font-size:inherit;"> 基于父子文档 Join 类型的 has_child 查询和 父聚合。</span></li></ul>global ordinals 使用一个数值代表字段中的字符串值,然后为每一个数值分配一个 bucket(分桶)。<br>
<b>global ordinals 的本质是:启用 eager_global_ordinals 时,会在刷新(refresh)分片时构建全局序号。这将构建全局序号的成本从搜索阶段转移到了数据索引化(写入)阶段。</b>
创建索引的同时开启:eager_global_ordinals。<br>
注意:<b>开启 eager_global_ordinals 会影响写入性能,因为每次刷新时都会创建新的全局序号。为了最大程度地减少由于频繁刷新建立全局序号而导致的额外开销,请调大刷新间隔 refresh_interval。</b>
插入数据时对索引进行预排序
<ul><li><b>Index sorting (索引排序)可用于在插入时对索引进行预排序</b>,而不是在查询时再对索引进行排序,这将提高范围查询(range query)和排序操作的性能。</li><li>在 Elasticsearch 中创建新索引时,可以配置如何对每个分片内的段进行排序。</li><li>这是 Elasticsearch 6.X 之后版本才有的特性。</li></ul>
<b>注意:预排序将增加 Elasticsearch 写入的成本</b>。在某些用户特定场景下,开启索引预排序会导致大约 40%-50% 的写性能下降。也就是说,如果用户场景更关注写性能的业务,开启索引预排序不是一个很好的选择。<br>
使用节点查询缓存<br>
<b>节点查询缓存(Node query cache)可用于有效缓存过滤器(filter)操作的结果</b>。如果多次执行同一 filter 操作,这将很有效,但是即便更改过滤器中的某一个值,也将意味着需要计算新的过滤器结果。
例如,由于 “now” 值一直在变化,因此无法缓存在过滤器上下文中使用 “now” 的查询。<br>那怎么使用缓存呢?通过在 now 字段上应用 datemath 格式将其四舍五入到最接近的分钟/小时等,可以使此类请求更具可缓存性,以便可以对筛选结果进行缓存。<br>
使用分片请求缓存<br>
聚合语句中,设置:size:0,就会使用分片请求缓存缓存结果。size = 0 的含义是:只返回聚合结果,不返回查询结果。
拆分聚合,使聚合并行化<br>
Elasticsearch 查询条件中同时有多个条件聚合,默认情况下聚合不是并行运行的。当为每个聚合提供自己的查询并执行 msearch 时,性能会有显著提升。因此,在 CPU 资源不是瓶颈的前提下,如果想缩短响应时间,可以将多个聚合拆分为多个查询,借助:<b>msearch 实现并行聚合</b>
ES零停机索引重建
Elasticsearch是一个实时的分布式搜索引擎,为用户提供搜索服务,当我们决定存储某种数据时,在创建索引的时候需要数据结构完整确定下来,与此同时索引的设定和很多固定配置将不能改变。当需要改变数据结构时就需要重建索引,为此,Elasticsearch团队提供了辅助工具帮助开发人员进行索引重建。
方案一: 外部数据导入方案
系统架构设计中,有关系型数据库用来存储数据,Elasticsearch在系统架构里起到查询加速的作用,如果遇到索引重建的操作,<b>待系统模块发布新版本后,可以从数据库将数据查询出来,重新灌到 Elasticsearch即可</b>。
<b> 数据库 + MQ + 应用模块 + Elasticsearch</b>
<b>操作步骤</b>
<ol><li><span style="font-size:inherit;">通过MQ的web控制台或cli命令行,发送指定的MQ消息</span></li><li><span style="font-size:inherit;">MQ消息被微服务模块的消费者消费,触发ES数据重新导入功能 </span></li><li><span style="font-size:inherit;">微服务模块从数据库里查询数据的总数及批次信息,并将每个数据批次的分页信息重新发送给MQ 消息,分页信息包含查询条件和偏移量,此MQ消息还是会被微服务的MQ消息者接收处理。</span></li><li><span style="font-size:inherit;">微服务根据接收的查询条件和分页信息,从数据库获取到数据后,根据索引结构的定义,将数据组装成ES支持的JSON格式,并执行bulk命令,将数据发送给Elasticsearch集群。 这样就可以完成索引的重建工作。</span></li></ol>
方案特点
MQ中间件的选型不做具体要求,常见的rabitmq、activemq、rocketmq等均可。
在微服务模块方面,提供MQ消息处理接口、数据处理模块需要事先开发的,一般是创建新的索引时, 配套把重建的功能也一起做好。整体功能共用一个topic,针对每个索引,有单独的结构定义和MQ消息 处理tag,代码尽可能复用。处理的批次大小需要根据实际的情况设置。
微服务模块实例会部署多个,数据是分批处理的,批次信息会一次性全部先发送给MQ,各个实例处理的数据相互不重叠,利用MQ消息的异步处理机制,可以充分利用并发的优势,加快数据重建的速度。
缺点
对数据库造成读取压力,短时间内大量的读操作,会占用数据库的硬件资源,严重时可能引起数据库性能下降<br>
网络带宽占用多,数据毕竟是从一个库传到另一个库,虽说是内网,但大量的数据传输带宽占用
数据重建时间稍长,跟迁移的数据量大小有关
方案二:基于scroll+bulk+索引别名方案
利用Elasticsearch自带的一些工具完成索引的重建工作,当然在方案实际落地时,可能也会依赖客户端的一些功能<b>,比如用Java客户端持续的做scroll查询、bulk命令的封装等。数据完全自给自足,不依赖 其他数据源</b><br>
执行步骤
<br>
特点
在数据传输上基本自给自足,<b>不依赖于其他数据源,Java客户端不需要停机等待数据迁移,网络传输占 用带宽较小</b>。只是scroll查询和bulk提交这部分,数据量大时需要依赖一些客户端工具。<br><b><font color="#e74f4c">在Java客户端或其他客户端访问Elasticsearch集群时,使用别名是一个好习惯</font></b><br>
方案三:Reindex API方案
<b>Elasticsearch v6.3.1</b>已经支持Reindex API,<b>它对scroll、bulk做了一层封装,能够 对文档重建索引而不需要任何插件或外部工具。</b>
命令
响应结果为:
<br>
version_type 属性<br>
使用reindex api也是创建快照后再执行迁移的,这样目标索引的数据可能会与原索引有差异, version_type属性可以决定乐观锁并发处理的规则<br>reindex api可以设置version_type属性,如下:<br>
version_type属性含义如下:<br><ul><li><b>internal</b>:直接拷贝文档到目标索引,对相同的type、文档ID直接进行覆盖,默认值 </li><li><b>external</b>:迁移文档到目标索引时,保留version信息,对目标索引中不存在的文档进行创建,已存在的文档按version进行更新,<b>遵循乐观锁机制。</b></li></ul>
op_type 属性和conflicts 属性
如果<b>op_type</b>设置为<b>create</b>,那么迁移时<b>只在目标索引中创建ID不存在的文档,已存在的文档,会提示错误</b>,如下请求
有错误提示的响应,节选部分:
如果加上"conflicts": "proceed"配置项,那么冲突信息将不展示,只展示冲突的文档数量<br>
<br>
query支持
reindex api支持数据过滤、数据排序、size设置、_source选择等,也支持脚本执行
零停机索引重建操作的三个方案,从自研功能、scroll+bulk到reindex,我们<b>作为Elasticsearch的使用者,三个方案的参与度是逐渐弱化的,但稳定性却是逐渐上升的</b>,我们需要清楚地去了解各个方案的优劣,适宜的场景,然后根据实际的情况去权衡,哪个方案更适合我们的业务模型.
ES Suggester智能搜索建议
什么是智能搜索?
现代的搜索引擎,一般会具备"Suggest As You Type"功能,即在用户输入搜索的过程中,进行自动补全 或者纠错。 通过协助用户输入更精准的关键词,提高后续全文搜索阶段文档匹配的程度。例如在京东上 输入部分关键词,甚至输入拼写错误的关键词时,它依然能够提示出用户想要输入的内容
如果自己亲手去试一下,可以看到京东在用户刚开始输入的时候是自动补全的,而当输入到一定长度, 如果因为单词拼写错误无法补全,就开始尝试提示相似的词。 那么类似的功能在Elasticsearch里如何实现呢?<br>答案就在<b>Suggesters API</b>。 <br>
Suggesters API
<b>Suggesters基本的运作原理是将输入的文本分解为token,然后在索引的字典里查找相似的term并返回</b>。 根据使用场景的不同, Elasticsearch里设计了4种类别的Suggester
Term Suggester
suggest就是一种特殊类型的搜索,DSL内部的"text"指的是api调用方提供的文本,也就是通常用户界面上用户输入的内容。这里的lucne是错误的拼写,模拟用户输入错误。<b> "term"表示这是一个term suggester。 "field"指定suggester针对的字段,另外有一个可选的"suggest_mode"</b>
suggest_mode
missing:默认值,仅为不在索引中的词项生成建议词
popular:仅返回与搜索词文档词频或文档词频更高的建议词
always:根据 建议文本中的词项 推荐 任何匹配的建议词
Phrase Suggester
<b>Phrase suggester在Term suggester的基础上,<font color="#e74f4c">会考量多个term之间的关系</font>,比如<font color="#e74f4c">是否同时出现在索引的原文里,相邻程度,以及词频</font>等等</b>
例子
<br>
返回结果
options直接返回一个phrase列表,由于加了highlight选项,被替换的term会被高亮。因为lucene和 elasticsearch曾经在同一条原文里出现过,同时替换2个term的可信度更高,所以打分较高,排在第一 位返回。Phrase suggester有相当多的参数用于控制匹配的模糊程度,需要根据实际应用情况去挑选和 调试
Completion Suggester
它主要针对的应用场景就是"Auto Completion"。 <b>此场景下用户每输入一个字符的时候,就需要即时发送一次查询请求到后端查找匹配项,在用户输入速度较高的情况下对后端响应速度要求比较苛刻。</b>因此实现上它和前面两个Suggester采用了不同的数据结构,<font color="#e74f4c">索引并非通过倒排来完成,而是将analyze过的数据编码成FST和索引一起存放。对于一个open状态的索引, FST会被ES整个装载到内存里的,进行前缀查找速度极快。但是FST只能用于前缀查找</font>,这也是 Completion Suggester的局限所在。
例子
<br>
结果
值得注意的一点是<b>Completion Suggester在索引原始数据的时候也要经过analyze阶段,取决于选用的 analyzer不同,某些词可能会被转换,某些词可能被去除,这些会影响FST编码结果,也会影响查找匹 配的效果</b>
比如:将analyzer更改为"english" 插入数据<br>
bulk api索引同样的数据后,执行下面的查询,<br>
居然没有匹配结果了,多么费解! 原来我们用的english analyzer会剥离掉stop word,而is就是其中 一个,被剥离掉了! 用analyze api测试一下:<br>
<b>FST(Finite State Transducers)</b>只编码了这3个token,并且默认的还会记录他们在文档中的位置和分隔符。 用户输入"elastic i"进行查找的时候,输入被分解成"elastic"和"i",FST没有编码这个“i” , 匹配 败。 好吧,如果你现在还足够清醒的话,试一下搜索"elastic is",会发现又有结果,why? 因为这次输入的 text经过english analyzer的时候is也被剥离了,只需在FST里查询"elastic"这个前缀,自然就可以匹配到 了。 其他能影响completion suggester结果的,还有 如"<font color="#ed9745">preserve_separators</font>","<font color="#ed9745">preserve_position_increments</font>"等等<b>mapping参数来控制匹配的模糊程度</b>。<b>以及搜索时可以选用Fuzzy Queries,使得上面例子里的"elastic i"在使用english analyzer的情况下 依然可以匹配到结果</b>
<ul><li><font color="#ed9745">preserve_separators</font>: false , 这个设置为false,将忽略空格之类的分隔符</li><li><font color="#ed9745">preserve_position_increments</font>: true ,如果建议词第一个词是停用词,并且我们使用了过滤停用词的分析器,需要将此设置为false。</li></ul>
在实际应用开发过程中,需要根据数据特性和业务 需要,灵活搭配analyzer和mapping参数,反复调试才可能获得理想的补全效果。 回到篇首京东或者百度搜索框的补全/纠错功能,如果用ES怎么实现呢?
我能想到的一个的实现方式:<b>在用户刚开始输入的过程中,使用Completion Suggester进行关键词前缀匹配,刚开始匹配项会比较多,随着用户输入字符增多,匹配项越来越少。如果用户输入比较精准,可能Completion Suggester的 结果已经够好,用户已经可以看到理想的备选项了。</b><br><br><b>如果Completion Suggester已经到了零匹配,那么可以猜测是否用户有输入错误,这时候可以尝试一下 Phrase Suggester</b>。如果Phrase Suggester没有找到任何option,开始尝试term Suggester。 <b>精准程度上(Precision)看: Completion > Phrase > term, 而召回率上(Recall)则反之</b>。从性能上看, Completion Suggester是最快的,如果能满足业务需求,只用Completion Suggester做前缀匹配是最理想的。 Phrase和Term由于是做倒排索引的搜索,相比较而言性能应该要低不少,应尽量控制 suggester用到的索引的数据量,最理想的状况是经过一定时间预热后,索引可以全量map到内存。<br>
Context Suggester
Completion Suggester 的扩展
<b>可以在搜索中加入更多的上下文信息,然后根据不同的上下文信息,对相同的输入</b>,比如"star",<b>提供不同的建议值</b>,比如: 咖啡相关:starbucks 电影相关:star wars<br>
ES Java API(见笔记)
索引模板
<b>索引模板是一种告诉Elasticsearch在创建索引时如何配置索引的方法<br>在创建索引之前可以先配置模板,这样在创建索引(手动创建索引或通过对文档建立索引)时,模板设置将用作创建索引的基础</b>
<b>模板类型</b>
<b>组件模板</b>是可重用的构建块,用于配置映射,设置和别名;它们不会直接应用于一组索引。
<b>索引模板</b>可以包含组件模板的集合,也可以直接指定设置,映射和别名。
索引模板中的优先级
<ul><li><b>可组合模板优先于旧模板</b>。如果没有可组合模板匹配给定索引,则旧版模板可能仍匹配并被应用。</li><li>如果使用显式设置创建索引并且该索引也与索引模板匹配,则创建索引请求中的设置将优先于索引模板及其组件模板中指定的设置。</li><li>如果新数据流或索引与多个索引模板匹配,则使用优先级最高的索引模板。</li></ul>
内置索引模板
<b>Elasticsearch具有内置索引模板,每个索引模板的优先级为100,适用于以下索引模式</b>:<br>1. logs-*-*<br>2. metrics-*-*<br>3. synthetics-*-*<br><b>所以在涉及内建索引模板时,要避免索引模式冲突</b>。https://www.elastic.co/guide/en/elasticsearch/reference/current/index-templates.html<br>
案例<br>
首先创建两个索引组件模板:
执行结果如下
创建使用组件模板的索引模板<br>
执行结果如下<br>
<br>
模拟多组件模板<br>
由于模板不仅可以由多个组件模板组成,还可以由索引模板自身组成;那么最终的索引设置将是什么呢?ElasticSearch设计者考虑到这个,提供了API进行模拟组合后的模板的配置。
高可用分布式集群
核心概念
集群(Cluster)
一个Elasticsearch集群由多个节点(Node)组成,每个集群都有一个共同的集群名称作为标识
<ul><li>- 不同的集群通过不同的名字来区分,默认名字“elasticsearch“</li><li>- 通过配置文件修改,或者在命令行中 -E cluster.name=es-cluster进行设定</li></ul>
节点(Node)
<b>一个Elasticsearch实例即一个Node(本质上就是一个JVM进程)</b>,一台机器可以有多个实例,正常使用下每个实例都应该 会部署在不同的机器上。Elasticsearch的配置文件中可以通过<font color="#ed9745">node.master</font>、<font color="#ed9745">node.data</font>来设置节点类型<br><br><ul><li>- 每一个节点都有名字,通过配置文件配置,或者启动时候 -E node.name=node1指定</li><li>- 每一个节点在启动之后,会分配一个UID,保存在data目录下</li></ul>
<font color="#ed9745"><b>node.master</b></font>
是否有成为主节点资格 true/false<br>
<font color="#ed9745"><b>node.data</b></font>
表示节点是否存储数据 true/false<br>
节点的类型
主节点+数据节点(master+data)(默认)
节点既有成为主节点的资格,又存储数据<br>
数据节点(data)<br>
节点没有成为主节点的资格,不参与选举,只会存储数据
客户端节点(client | coordinating Node)
<b>不会成为主节点,也不会存储数据,主要是针对海量请求的时候可以进行负载均衡</b>
其他数据节点
<b>- Hot & Warm Node:</b>不同硬件配置 的Data Node,用来实现Hot &amp; Warm架构,降低集群部署的成本
<b>- Ingest Node</b>:数据前置处理转换节点,支持pipeline管道设置,可以使用ingest对数据进行过滤、转换等操作
<b>- Machine Learning Node</b>:负责跑机器学习的Job,用来做异常检测
<b>- Tribe Node</b>:Tribe Node连接到不同的Elasticsearch集群,并且支持将这些集群当成一个单独的集群处理
<br>
分片<br>
主分片
<ul><li><b> 用以解决数据水平扩展的问题</b>。通过主分片,可以将数据分布到集群内的所有节点之上</li><li><font color="#e74f4c"> 一个分片是一个运行的Lucene的实例</font></li><li><b> 主分片数在索引创建时指定,后续不允许修改,除非Reindex</b></li></ul>
复制分片
<ul><li>- <b>用以解决数据高可用的问题</b>。 副本分片是主分片的拷贝</li><li>- <font color="#e74f4c">副本分片数,可以动态调整</font></li><li><b>- 增加副本数,还可以在一定程度上提高服务的可用性(读取的吞吐)</b></li></ul>
副本<br>
这里指主分片的副本分片(主分片的拷贝)
<ul><li><b>提高恢复能力</b>:当主分片挂掉时,某个复制分片可以变成主分片;</li><li><b>提高性能</b>:<b>get 和 search 请求既可以由主分片又可以由复制分片处理;</b></li></ul>
设置分片和副本
<br>
<b><font color="#314aa4">思考:增加一个节点或改大主分片数对系统有什么影响?</font></b>
<b>分片的设定:对于生产环境中分片的设定,需要提前做好容量规划</b><br>
<b>分片数设置过小</b>
- 导致后续无法增加节点实现水平扩展
- 单个分片的数据量太大,导致数据重新分配耗时
分片数设置过大<br>
7.0 开始,默认主分片设置成1,解决了over-sharding(分片过度)的问题<br><ul><li><b>- 影响搜索结果的相关性打分,影响统计结果的准确性</b></li><li><b>- 单个节点上过多的分片,会导致资源浪费,同时也会影响性能</b></li></ul>
// 查看集群的健康状况 <br><font color="#ed9745"><b>GET _cluster/health</b></font>
集群status<br>
<ul><li><b>Green</b>:主分片与副本都正常分配</li><li><b>Yellow</b>: 主分片全部正常分配,有副本分片未能正常分配</li><li><b>Red</b>: 有主分片未能分配。例如,当服务器的磁盘容量超过85%时,去创建了一个新的索引</li></ul>
CAT API查看集群信息<br>
ES 分布式架构<br>
Elasticseasrch的架构遵循其基本概念:一个采用<b>Restful API</b>标准的<b>高扩展性</b>和<b>高可用性</b>的<b>实时数据分析</b>的<b>全文搜索引擎</b>。
特性
<b>高扩展性</b>:体现在Elasticsearch添加节点非常简单,新节点无需做复杂的配置,只要配置好集群信息将会被集群自动发现。
<b>高可用性</b>:因为Elasticsearch是分布式的,每个节点都会有备份,所以宕机一两个节点也不会出现问题,集群会通过备份进行自动复盘。
<b>实时性</b>:使用倒排索引来建立存储结构,搜索时常在百毫秒内就可完成。
分层
<br>
<b>第一层 —— Gateway</b>
Elasticsearch支持的索引快照的存储格式,<b>es默认是先把索引存放到内存中,当内存满了之后再持久化到本地磁盘</b>。gateway对索引快照进行存储,当Elasticsearch关闭再启动的时候,它就会从这个 gateway里面读取索引数据;支持的格式有:<br><b>本地的Local FileSystem、分布式的Shared FileSystem、 Hadoop的文件系统HDFS、Amazon(亚马逊)的S3。</b><br>
<b>第二层 —— Lucene框架</b>
Elasticsearch基于Lucene(基于Java开发)框架
<b>第三层 —— Elasticsearch数据的加工处理方式</b>
index Module(创建Index模块)、Search Module(搜索模块)、Mapping(映射)、River 代表es的一个数据源(运行在Elasticsearch集群内部的一个插件,主要用来从外部获取获取异构数据,然后在Elasticsearch里创建索引;常见的插件有RabbitMQ River、Twitter River)。
<b>第四层 —— Elasticsearch发现机制、脚本</b><br>
Discovery 是Elasticsearch<b>自动发现节点的机制的模块</b>,Zen Discovery和 EC2 discovery。
- EC2:亚马逊弹性计算云 EC2 discovery主要在亚马云平台中使用。Zen Discovery作用就相当于solrcloud中的 zookeeper。
- zen Discovery 从功能上可以分为两部分,第一部分是集群刚启动时的选主,或者是新加入集群的节点发现当前集群的Master。第二部分是选主完成后,Master 和 Folower 的相互探活。
Scripting 是脚本执行功能,有这个功能能很方便对查询出来的数据进行加工处理。
3rd Plugins 表示Elasticsearch支持安装很多第三方的插件,例如elasticsearch-ik分词插件、 elasticsearch-sql sql插件。
<b>第五层 —— Elasticsearch的交互方式</b><br>
有Thrift、Memcached、Http三种协议,默认的是用Http协议传输
<b>第六层 —— Elasticsearch的API支持模式:</b><br>
RESTFul Style API风格的API接口标准是当下十分流行的。<font color="#e74f4c"><b>Elasticsearch作为分布式集群,客户端到服务端,节点与节点间通信有TCP和Http通信协议,底层实现为Netty框架</b></font>
解析ES分布式架构
分布式架构的透明隐藏特性
- <b>分片机制</b>:将文本数据切割成n个小份存储在不同的节点上,减少大文件存储在单个节点上对设备带来的压力。
<b>- 分片的副本</b>:在集群中某个节点宕掉后,通过副本可以快速对缺失数据进行复盘
- <b>集群发现机制(cluster discovery)</b>:在当前启动了一个Elasticsearch进程,在启动第二个 Elasticsearch进程时,这个进程将作为一个node自动就发现了集群,并自动加入,前提是这些 node都必须配置一套集群信息。
-<b> Shard负载均衡</b>:例如现在由10个 shard (分片),集群中由三个节点,Elasticsearch会进行均 衡的分配,以保持每个节点均衡的负载请求。
扩展机制
<b>- 垂直扩容</b>:用新机器替换已有的机器,服务器台数不变容量增加。
<b>- 水平扩容</b>:直接增加新机器,服务器台数和容量都增加。
rebalance
增加或减少节点时会自动负载
主节点
主节点的主要职则是和集群操作的相关内容,如创建或删除索引,跟踪哪些节点是集群的一部分,并决定哪些分片分配给相关的节点。稳定的主节点对集群的健康是非常重要的。
节点对等<br>
每个节点都能接受请求,每个节点接受到请求后都能把该请求路由到有相关数据的其它节点上,接受原始请求的节点负责采集数据并返回给客户端。
集群搭建(见笔记)
集群规划<br>
需要多大规模的集群 ?
需要从以下两个方面考虑
当前的数据量有多大?数据增长情况如何?
你的机器配置如何?cpu、多大内存、多大硬盘容量?
推算的依据
<b>Elasticsearch JVM heap 最大可以设置32G</b> 。 <b>30G heap 大概能处理的数据量 10 T</b>。如果内存很大如128G,可在一台机器上运行多个ES节点实例。 备注:集群规划满足当前数据规模+适量增长规模即可,后续可按需扩展。
两类应用场景
用于构建业务搜索功能模块,且多是垂直领域的搜索。数据量级几千万到数十亿级别。一般2-4台机 器的规模。
用于大规模数据的实时OLAP(联机处理分析),经典的如ELK Stack,数据规模可能达到千亿或更 多。几十到上百节点的规模
集群中的数据节点怎么分配 ?
<b>- Master </b>:node.master: true 节点可以作为主节点
<b>- DataNode</b>: node.data: true 默认是数据节点
<b>- Coordinate node 协调节点</b>:<b>一个节点只作为接收请求、转发请求到其他节点、汇总各个节点返回数据等功能的节点</b>,就叫协调节点,如果仅担任协调节点,将上两个配置设为false。
<b><font color="#314aa4">节点角色如何分配</font></b>
小规模集群,不需严格区分。
中大规模集群(十个以上节点),应考虑单独的角色充当。特别并发查询量大,查询的合并量大,可以增加独立的协调节点。角色分开的好处是分工分开,不互影响。如不会因协调角色负载过高而影响数据节点的能力。
如何避免脑裂问题 ?
脑裂问题: 一个集群中只有一个A主节点,A主节点因为需要处理的东西太多或者网络过于繁忙,从而导致其他从节点ping不通A主节点,这样其他从节点就会认为A主节点不可用了,就会重新选出一个新的主节点B。过了一会A主节点恢复正常了,这样就出现了两个主节点,导致一部分数据来源于A主节点,另外一部分数据来源于B主节点,出现数据不一致问题,这就是脑裂
6.x和之前版本 尽量避免脑裂,需要添加最小数量的主节点配置:<font color="#ed9745"><b> discovery.zen.minimum_master_nodes</b></font>: <b>(有master资格节点数/2) + 1</b>,这个参数控制的是,<b>选举主节点时需要看到最少多少个具有master资格的活节点,才能进行选举</b>。官方的推荐值是(N/2)+1,其中N是具有master资格的节点的数量。<br><br>在新版7.X的ES中,对es的集群发现系统做了调整,不再有<font color="#ed9745"><b>discovery.zen.minimum_master_nodes</b></font>这个控制集群脑裂的配置,转而<b>由集群自主控制,并且新版在启动一个新的集群的时候需要有 <font color="#ed9745">cluster.initial_master_nodes</font>初始化集群列表</b>。 在es7中,discovery.zen.* 开头的参数,有些已经失效<br>
<b>常用做法(中大规模集群)</b>
① Master 和 dataNode 角色分开,配置奇数个master,如3
② 单播发现机制,配置master资格节点(5.0之前):discovery.zen.ping.multicast.enabled: false —— 关闭多播发现机制,默认是关闭的
<b>③ 延长ping master的等待时长</b><font color="#ed9745">discovery.zen.ping_timeout</font>: <b>30(默认值是3秒)</b>——其他节点ping主节点多久时间没有响应就认为主节点不可用了。es7中换成了 <font color="#ed9745"> discovery.request_peers_timeout</font><br>
索引应该设置多少个分片 ?
<font color="#e74f4c">分片数指定后不可变,除非重建索引</font>
分片设置的可参考原则:<br><b>ElasticSearch推荐的最大JVM堆空间是30~32G</b>,所以把你的分片最大容量限制为30GB, 然后再对分片数量做合理估算。例如, 你认为你的数据能达到200GB, 推荐你最多分配7到8个分片。 在开始阶段, 一个好的方案是根据你的节点数量按照1.5~3倍的原则来创建分片,例如,如果你有3个节点, 则推荐你创建的分片数最多不超过9(3x3)个。当性能下降时,增加节点,ES会平衡分片的放置。 对于基于日期的索引需求, 并且对索引数据的搜索场景非常少. 也许这些索引量将达到成百上千, 但每个索引的数据量只有1GB甚至更小. 对于这种类似场景, 建议只需要为索引分配1个分片。如日志管理就是 一个日期的索引需求,日期索引会很多,但每个索引存放的日志数据量就很少<br>
<b>分片数设置过小</b>
- 导致后续无法增加节点实现水平扩展
- 单个分片的数据量太大,导致数据重新分配耗时
分片数设置过大<br>
7.0 开始,默认主分片设置成1,解决了over-sharding(分片过度)的问题<br><ul><li><b>- 影响搜索结果的相关性打分,影响统计结果的准确性</b></li><li><b>- 单个节点上过多的分片,会导致资源浪费,同时也会影响性能</b></li></ul>
// 查看集群的健康状况 <br><font color="#ed9745"><b>GET _cluster/health</b></font>
分片应该设置几个副本 ?<br>
<b>副本设置基本原则: 为保证高可用,副本数设置为2即可</b>。要求集群至少要有3个节点,来分开存放主分片、副本。 如发现并发量大时,查询性能会下降,可增加副本数,来提升并发查询能力。<br><b><font color="#e74f4c">注意:新增副本时主节点会自动协调,然后拷贝数据到新增的副本节点,副本数是可以随时调整的!</font></b>
分布式集群调优策略
Index 写调优
拉勾网的职位数据和简历数据,首先都是进入MySQL集群的,我们<b>从MySQL的原始表里面抽取并存储到ES 的Index</b>,而MySQL的原始数据也是经常在变化的,所以快速写入Elasticsearch、以保持 Elasticsearch和MySQL的数据及时同步也是很重要的。 拉勾网的工程师主要是下面几个方面优化来提高写入的速度:
副本数为0<br>
如果是集群首次灌入数据,可以将副本数设置为0,<b>写入完毕再调整回去,这样副本分片只需要拷贝,节省了索引过程</b>。
自动生成doc ID
通过Elasticsearch写入流程可以看出,<b>如果写入doc时如果外部指定了id,则Elasticsearch会先尝试读取原来doc的版本号,以判断是否需要更新。这会涉及一次读取磁盘的操作</b>,通过自动生成doc ID可 以避免这个环节
合理设置mappings
将<b>不需要建立索引的字段index属性设置为not_analyzed或no</b>。对字段不分词,或者不索引,可以减少很多运算操作,降低CPU占用。<font color="#e74f4c"> 尤其是binary类型,默认情况下占用CPU非常高</font>,而这种类型进行分词通常没有什么意义。
<b>减少字段内容长度</b>,如果原始数据的大段内容无须全部建立索引,则可以尽量减少不必要的内容
<b>使用不同的分析器(analyzer)</b>,不同的分析器在索引过程中运算复杂度也有较大的差异
调整_source字段<br>
<font color="#e74f4c">_<b>source </b></font><b>字段用于存储 doc 原始数据,对于部分不需要存储的字段,可以通过 includes excludes过滤,或者将source禁用</b>,<b>一般用于索引和数据分离,这样可以降低 I/O 的压力</b>,不过实际场景中大多不会禁用_source
<b><font color="#314aa4">_source 字段默认是存储的,什么情况下不用保留_source字段?</font></b>
如果某个业务内容非常多,业务里面只需要能对该字段进行搜索,最后返回文档id,查看文档内容再次到Mysql或者Hbase中取数据,把大字段的内容在ElasticSearch 中只会增大索引,这一点文档数量越大越明显。<br>
对analyzed的字段禁用norms(禁止评分)
Norms用于在搜索时计算doc的评分,如果不需要评分,则可以将其禁用
调整索引的刷新间隔
该参数缺省是<font color="#ed9745">1s</font>,<b>强制ES每秒创建一个新segment,从而保证新写入的数据近实时的可见、可被搜索到</b>。比如该参数被调整为<font color="#ed9745">30s</font>,<b>降低了刷新的次数,把刷新操作消耗的系统资源释放出来给index操作使用。</b>
<font color="#e74f4c">这种方案以牺牲可见性的方式,提高了index操作的性能。</font>
批处理
批处理把多个index操作请求合并到一个batch中去处理,和mysql的jdbc的bacth有类似之处。如图:
<b>比如每批1000个documents是一个性能比较好的size</b>。每批中多少document条数合适,受很多因素 影响而不同,如单个document的大小等。ES官网建议通过在单个node、单个shard做性能基准测试来 确定这个参数的最优值
<b>Document的路由处理</b>
当对一批中的documents进行index操作时,<b>该批index操作所需的线程的个数由要写入的目的shard的个数决定</b>。看下图:
上图中,有2批documents写入ES, 每批都需要写入4个shard,所以总共需要8个线程。<b>如果能减少 shard的个数,那么耗费的线程个数也会减少</b>。例如下图,两批中每批的shard个数都只有2个,总共线 程消耗个数4个,减少一半。<br><b>默认的routing就是id,也可以在发送请求的时候,手动指定一个routing value</b>,比如说put /index/doc/id?routing=user_id<br>
值得注意的是线程数虽然降低了,但是单批的处理耗时可能增加了。和提高刷新间隔方法类似,这有可能会延长数据不见的时间。
Search 读调优
在存储的Document条数超过10亿条后,我们如何进行搜索调优。
数据分组
可以基于日期,或者基于租户/用户分组
使用Filter替代Query(不打分))<br>
ID字段定义为keyword
<b>一般情况,如果ID字段不会被用作Range 类型搜索字段,都可以定义成keyword类型</b>。这是因为<font color="#e74f4c"><b> keyword会被优化,以便进行terms查询</b></font>。Integers等数字类的mapping类型,会被优化来进行range类 型搜索。<br><br><b>将integers改成keyword类型之后,搜索性能大约能提升30%。</b><br>
别让用户的无约束的输入拖累了ES集群的性能
生产环境常见集群部署方案<br>
不同角色的节点:<b>Master eligible / Data / Ingest / Coordinating /Machine Learning</b><br>
一个节点只承担一个角色的配置<br>
<br>
<b>单一 master eligible nodes: 负责集群状态(cluster state)的管理</b>
<b>使用低配置的CPU,RAM和磁盘</b>
单一 data nodes: 负责数据存储及处理客户端请求
使用高配置的CPU,RAM和磁盘
单一ingest nodes: 负责数据处理
使用高配置CPU; 中等配置的RAM; 低配置的磁盘
单一 Coordinating Only Nodes(Client Node)
使用高配置CPU; 高配置的RAM; 低配置的磁盘
生产环境中,建议为一些大的集群配置Coordinating Only Nodes
- 扮演Load Balancers,降低Master和 Data Nodes的负载
- 负责搜索结果的Gather/Reduce
- 有时候无法预知客户端会发送怎么样的请求。比如大量占用内存的操作,一个深度聚合可能会引发OOM
单一 master eligible nodes
从高可用 & 避免脑裂的角度出发:
一般在生产环境中配置3台
一个集群只有1台活跃的主节点(master node)
- 负责分片管理,索引创建,集群管理等操作
如果和数据节点或者Coordinate节点混合部署
- 数据节点相对有比较大的内存占用
- Coordinate节点有时候可能会有开销很高的查询,导致OOM
- 这些都有可能影响Master节点,导致集群的不稳定
增加节点水平扩展场景
当磁盘容量无法满足需求时,可以增加数据节点;
磁盘读写压力大时,增加数据节点
当系统中有大量的复杂查询及聚合时候,增加Coordinating节点,增加查询的性能
<br>
读写分离架构
<br>
异地多活架构(两地三中心)<br>
集群处在三个数据中心,数据三写,GTM分发读请求<br>
全局流量管理(GTM)和负载均衡(SLB)的区别<br>
<b>GTM 是通过DNS将域名解析到多个IP地址,不同用户访问不同的IP地址,来实现应用服务流量的分配</b>。同时通过健康检查动态更新DNS解析IP列表,实现故障隔离以及故障切换。<b>最终用户的访问直接连接服务的IP地址</b>,并不通过GTM。而 <b>SLB 是通过代理用户访问请求的形式将用户访问请求实时分发到不同的服务器,最终用户的访问流量必须要经过SLB</b>。 一般来说,<font color="#e74f4c"><b>相同Region使用SLB进行负载均衡,不同region的多个SLB地址时,则可以使用GTM进行负载均衡。</b></font>
<b>ES 跨集群复制 (Cross-Cluster Replication)</b>是ES 6.7的的一个全局高可用特性。CCR允许不同的索引复制到一个或多个ES 集群中。
Hot & Warm 架构
<b><font color="#314aa4">为什么要设计Hot & Warm 架构?</font></b>
<ul><li><b>- ES数据通常不会有 Update操作;</b></li><li><b>- 适用于Time based索引数据,同时数据量比较大的场景。</b></li><li><b>- 引入 Warm节点,低配置大容量的机器存放老数据,以降低部署成本</b></li></ul>
两类数据节点,不同的硬件配置:<br><ul><li><b>Hot节点(通常使用SSD)︰索引不断有新文档写入。</b></li><li><b>Warm 节点(通常使用HDD)︰索引不存在新数据的写入,同时也不存在大量的数据查询</b></li></ul>
<b>Hot Node</b>
<b>用于数据的写入:</b><br><ul><li>lndexing 对 CPU和IO都有很高的要求,所以需要使用高配置的机器</li><li>存储的性能要好,建议使用SSD</li></ul>
<b>Warm Nodes</b>
<b>用于保存只读的索引,比较旧的数据</b>。通常使用大容量的磁盘<br>
<b><font color="#314aa4">配置Hot & Warm 架构</font></b>
<br>
如何对集群的容量进行规划<br>
ES跨集群搜索(CCS)<br>
ES水平扩展存在的问题
单集群水平扩展时,节点数不能无限增加
<font color="#e74f4c">当集群的meta 信息(节点,索引,集群状态)过多会导致更新压力变大,单个Active Master会成为性能瓶颈,</font>导致整个集群无法正常工作
早期版本,通过Tribe Node可以实现多集群访问的需求,但是还存在一定的问题
- Tribe Node会以Client Node的方式加入每个集群,集群中Master节点的任务变更需要Tribe Node 的回应才能继续。
- Tribe Node 不保存Cluster State信息,一旦重启,初始化很慢
- 当多个集群存在索引重名的情况时,只能设置一种 Prefer 规则
跨集群搜索实战
早期Tribe Node 的方案存在一定的问题,现已被弃用。Elasticsearch 5.3引入了跨集群搜索的功能(Cross Cluster Search),推荐使用<br><ul><li><span style="font-size:inherit;"><b>允许任何节点扮演联合节点,以轻量的方式,将搜索请求进行代理</b></span></li><li><span style="font-size:inherit;"><b>不需要以Client Node的形式加入其他集群</b></span></li></ul>
配置集群
<br>
CCS的配置
<br>
<br>
分片的设计和管理<br>
单个分片
<b>7.0开始,新创建一个索引时,默认只有一个主分片</b>。单个分片,查询算分,聚合不准的问题都可以得以避免
<b>单个索引,单个分片时候,集群无法实现水平扩展</b>。即使增加新的节点,无法实现水平扩展
两个分片
集群增加一个节点后,Elasticsearch 会自动进行分片的移动,也叫 <b>Shard Rebalanci</b>
<font color="#314aa4"><b>算分不准的原因</b></font>
<font color="#e74f4c">相关性算分在分片之间是相互独立的,每个分片都基于自己的分片上的数据进行相关度计算</font>。这会导致打分偏离的情况,特别是数据量很少时。<b>当文档总数很少的情况下,如果主分片大于1,主分片数越多,相关性算分会越不准</b>
<b><font color="#314aa4">解决算分不准的方法</font></b>
数据量不大的时候,可以将主分片数设置为1。当数据量足够大时候,只要保证文档均匀分散在各个分片上,结果一般就不会出现偏差
使用<b>DFS Query Then Fetch</b>
搜索的URL中指定参数“<font color="#ed9745"><b>_search?search_type=dfs_query_then_fetch</b></font>"
到每个分片把各分片的词频和文档频率进行搜集,然后完整的进行一次相关性算分,耗费更加多的CPU和内存,执行性能低下,—般不建议使用
如何设计分片数
当分片数 > 节点数时
一旦集群中有新的数据节点加入,分片就可以自动进行分配
分片在重新分配时,系统不会有downtime
多分片的好处: <b>一个索引如果分布在不同的节点,多个节点可以并行执行</b>
- 查询可以并行执行
- 数据写入可以分散到多个机器
分片过多所带来的副作用
<b>Shard是Elasticsearch 实现集群水平扩展的最小单位</b>。过多设置分片数会带来一些潜在的问题:<br><ul><li><b>每个分片是一个Lucene的索引,会使用机器的资源。过多的分片会导致额外的性能开销。</b></li><li><b>每次搜索的请求,需要从每个分片上获取数据</b></li><li><b>分片的Meta 信息由Master节点维护</b>。过多,会增加管理的负担。经验值,控制分片总数在10W以内</li></ul>
如何确定主分片数
从存储的物理角度看
搜索类应用,单个分片不要超过20 GB
日志类应用,单个分片不要大于50 GB
为什么要控制分片存储大小
提高Update 的性能
进行Merge 时,减少所需的资源
丢失节点后,具备更快的恢复速度
便于分片在集群内 Rebalancing
如何确定副本分片数
副本是主分片的拷贝
- 提高系统可用性︰响应查询请求,防止数据丢失
- 需要占用和主分片一样的资源
对性能的影响
<b>副本会降低数据的索引速度</b>: 有几份副本就会有几倍的CPU资源消耗在索引上
<b>会减缓对主分片的查询压力,但是会消耗同样的内存资源</b>。如果机器资源充分,提高副本数,可以提高整体的查询QPS
ES的分片策略会尽量保证节点上的分片数大致相同,但是有些场景下会导致分配不均匀
扩容的新节点没有数据,导致新索引集中在新的节点
热点数据过于集中,可能会产生性能问题
可以通过调整分片总数,避免分配不均衡<br>
- "index.routing.allocation.total_shards_per_node",index级别的,表示这个index每个Node总共允许存在多少个shard,默认值是-1表示无穷多个;
- "cluster.routing.allocation.total_shards_per_node",cluster级别,表示集群范围内每个Node允许存在有多少个shard。默认值是-1表示无穷多个。
<b>如果目标Node的Shard数超过了配置的上限,则不允许分配Shard到该Node上。注意:index级别的配置会覆盖cluster级别的配置。</b>
<b><font color="#314aa4">思考:5个节点的集群。索引有5个主分片,1个副本,index.routing.allocation.total_shards_per_node应该如何设置?</font></b>
- (5+5)/ 5= 2
- 生产环境中要适当调大这个数字,避免有节点下线时,分片无法正常迁移
数据模型构建
什么是数据模型
数据模型是抽象描述现实世界的一种工具和方法,是通过抽象实体及实体之间联系的形式,用图形化的形式去描述业务规则的过程,从而表示现实世界中事务以及相互关系的一种映射。<br><br>核心概念: <br><ul><li><b>实体:</b>现实世界中存在的可以相互区分的事物或概念称为实体。<br> 实体可以分为事物实体和概念实体。例如:一个学生、一个程序员等是事物实体。一门课、一个班级等称为概念实体。</li><li><b>实体的属性:</b>每个实体都有自己的特征,利用实体的属性可以描述不同的实体。例如。学生实体的 属性为姓名、性别、年龄等。</li></ul>
数据建模的过程
数据建模大致分为三个阶段,<b>概念建模阶段,逻辑建模阶段和物理建模阶段</b>。
① 概念建模阶段
概念建模阶段,主要做三件事: 客户交流、理解需求、形成实体
确定系统的核心需求和范围边界,设计实体与实体之间的关系。 在概念建模阶段,我们只需要关注实体即可,不用关注任何实现细节。很多人都希望在这个阶段把具体 表结构,索引,约束,甚至是存储过程都想好,没必要!因为这些东西是我们在物理建模阶段需要考虑 的东西,这个时候考虑还为时尚早。
概念模型在整个数据建模时间占比:10%左右。
② 逻辑建模阶段
逻辑建模阶段,主要做二件事:<br><ul><li><b>进一步梳理业务需求</b></li><li><b>确定每个实体的属性、关系和约束等</b>。</li></ul>
逻辑模型是对概念模型的进一步分解和细化,描述了实体、实体属性以及实体之间的关系,是概念模型延伸,一般的逻辑模型有第三范式,星型模型和雪花模型。模型的主要元素为主题、实体、实体属性和 关系
逻辑模型的作用主要有两点<br>
一是便于技术开发人员和业务人员以及用户进行沟通交流,使得整个概念模型更易于理解,进一 步明确需求。
二是作为物理模型设计的基础,由于逻辑模型不依赖于具体的数据库实现,使用逻辑模型可以生成 针对具体 数据库管理系统的物理模型,保证物理模型充分满足用户的需求。
逻辑模型在整个数据建模时间占比:60—70%左右。
<b>③ 物理建模阶段</b>
物理建模阶段,主要做一件事: 结合具体的数据库产品(mysql/oracle/mongo/elasticsearch),在满足业务读写性能等需求的前提下 确定最终的定义
物理模型是在逻辑模型的基础上描述模型实体的细节,包括数据库产品对应的数据类型、长度、索引等因素,为逻辑模型选择一个最优的物理存储环境。 逻辑模型转化为物理模型的过程也就是实体名转化为表名,属性名转化为物理列名的过程。 在设计物理模型时,还需要考虑数据存储空间的分配,包括对列属性必须做出明确的定义
数据建模的意义
<b>数据模型支撑了系统和数据,系统和数据支撑了业务系统。</b>
<span style="font-size:inherit;">一个好的数据模型:</span><br><ul><li><span style="font-size:inherit;">能让系统更好的集成、能简化接口。</span></li><li><span style="font-size:inherit;">能简化数据冗余 、减少磁盘空间、提升传输效率。</span></li><li><span style="font-size:inherit;">兼容更多的数据,不会因为数据类型的新增而导致实现逻辑更改。</span></li><li><span style="font-size:inherit;">能帮助更多的业务机会,提高业务效率。</span></li><li><span style="font-size:inherit;">能减少业务风险、降低业务成本。</span></li></ul>
ES数据建模Mapping设置
<br>
ES Mapping 属性<br>
<br>
ES Mapping 字段设置流程图
<br>
ES Mapping 样例
这个索引 Mapping中,<font color="#ed9745">_source</font>设置为false,同时各个字段的store根据需求设置了true和false。 url的 <font color="#ed9745">doc_values</font>设置为false,该字段url不用于聚合和排序操作。<br>建 mapping 时,可以为字符串(专指 keyword) 指定 <font color="#ed9745">ignore_above </font>,用来限定字符长度。<b>超过 ignore_above 的字符会被存储,但不会被索引。</b><br>
注意,是字符长度,一个英文字母是一个字符,一个汉字也是一个字符。 <b>在动态生成的 mapping 中, keyword 类型会被设置 ignore_above: 256</b> 。 ignore_above 可以在创建 mapping 时指定。<br>
ES关联关系处理
<b>关系型数据库范式化(Normalize)</b>设计的主要目标是减少不必要的更新,往往会带来一些副作用:<br><ul><li>- 一个完全范式化设计的数据库会经常面临“查询缓慢”的问题。数据库越范式化,就需要Join越多的表;</li><li>- 范式化节省了存储空间,但是存储空间已经变得越来越便宜;</li><li>- 范式化简化了更新,但是数据读取操作可能更多。</li></ul>
<b>反范式化(Denormalize)</b>的设计不使用关联关系,而是在文档中保存冗余的数据拷贝。<br><ul><li>- 优点: <b>无需处理Join操作,数据读取性能好</b>。Elasticsearch可以通过压缩_source字段,减少磁盘空间的开销</li><li>- 缺点: <b>不适合在数据频繁修改的场景</b>。 一条数据的改动,可能会引起很多数据的更新</li></ul>
<font color="#e74f4c"><b>关系型数据库,一般会考虑 Normalize 数据;在Elasticsearch,往往考虑Denormalize 数据。</b></font>
Application-side joins(应用端关联)<br>
<font color="#e74f4c"><b>类似于Mysql Join</b></font><br>这种方式,索引之间完全独立(利于对数据进行标准化处理),<b>由应用端的多次查询来实现近似关联关系查询</b>。这种方法适用于关联的实体只有少量的文档记录的情况(<b>使用ES的terms查询具有上限,默认 1024,具体可在elasticsearch.yml中修改</b>),并且最好它们很少改变。这将允许应用程序对结果进行缓存,并避免经常运行第一次查询<br>
<br>
应用端自己程序逻辑进行回表
Data denormalization(数据的非规范化)
这种方式,通俗点就是<b>通过字段冗余,以一张大宽表来实现粗粒度的index,这样可以充分发挥扁平化的优势</b>。但是这是以牺牲索引性能及灵活度为代价的。使用的前提:<font color="#e74f4c">冗余的字段应该是很少改变的,比较适合与一对少量关系的处理</font>。当业务数据库并非采用非规范化设计时,这时要将数据同步到作为二级索引库的ES中,就需要进行定制化开发,基于特定业务进行应用开发来处理join关联和实体拼接。 <br><font color="#e74f4c"><b>说明:宽表处理在处理一对多、多对多关系时,会有字段冗余问题,适合“一对少量”且这个“一”更新不 频繁的应用场景</b>。</font><br>
<br>
Nested objects(嵌套文档)<br>
索引性能和查询性能二者不可兼得,必须进行取舍。<b>嵌套文档将实体关系嵌套组合在单文档内部,这种方式牺牲建立索引性能(文档内任一属性变化都需要重新索引该文档)来换取查询性能,比较适合于一对少量的关系处理</b>。<br>当使用嵌套文档时,使用通用的查询方式是无法访问到的,必须使用合适的查询方式(<font color="#e74f4c">nested query、 nested filter、nested facet</font>等),很多场景下,使用嵌套文档的复杂度在于索引阶段对关联关系的组织拼装<br>
<br>
Parent/child relationships(父子文档)<br>
父子文档牺牲了一定的<b>查询性能来换取索引性能</b>,适用于<font color="#e74f4c">写多读少</font>的场景。父子文档相比嵌套文档较灵活,<b>适用于“一对大量”且这个“一”不是海量的应用场景,该方式比较耗内存和CPU</b>,这种方式查询比嵌套方式慢5~10倍,且需要使用特定的has_parent和has_child过滤器查询语法,查询结果不能同时返回 父子文档(一次join查询只能返回一种类型的文档)。<b>受限于父子文档必须在同一分片上(可以通过 routing指定父文档id即可)操作子文档时需要指定routing。</b>
<br>
<b><font color="#314aa4">嵌套文档 VS 父子文档</font></b>
<br>
ingest Pipeline & Painless Script
<font color="#e74f4c"><b>应用场景: 修复与增强写入数据</b></font>
案例
<br>
Ingest Node
Elasticsearch 5.0后,引入的一种新的节点类型。<b>默认配置下,每个节点都是Ingest Node:</b><br><ul><li><b>- 具有预处理数据的能力,可拦截lndex或 Bulk API的请求</b></li><li><b>- 对数据进行转换,并重新返回给Index或 Bulk APl</b></li></ul><br>无需Logstash,就可以进行数据的预处理,例如:<br><ul><li>- 为某个字段设置默认值;重命名某个字段的字段名;对字段值进行Split 操作</li><li>- 支持设置Painless脚本,对数据进行更加复杂的加工</li></ul>
Pipeline & Processor
<ul><li><b>Pipeline ——管道会对通过的数据(文档),按照顺序进行加工</b></li><li><b>Processor——Elasticsearch 对一些加工的行为进行了抽象包装</b></li></ul>
一些内置的Processors
- <b>Split Processor </b>: 将给定字段值分成一个数组
- <b>Remove / Rename Processor</b> :移除一个重命名字段
- <b>Append </b>: 为商品增加一个新的标签
- <b>Convert</b>:将商品价格,从字符串转换成float 类型
- <b>Date / JSON</b>:日期格式转换,字符串转JSON对象
- <b>Date lndex Name Processor</b>︰将通过该处理器的文档,分配到指定时间格式的索引中
- <b>Fail Processor</b>︰一旦出现异常,该Pipeline 指定的错误信息能返回给用户
- <b>Foreach Process</b>︰数组字段,数组的每个元素都会使用到一个相同的处理器
- <b>Grok Processor</b>︰日志的日期格式切割)
- <b>Gsub / Join / Split</b>︰字符串替换│数组转字符串/字符串转数组
- <b>Lowercase / upcase</b>︰大小写转换
<b><font color="#314aa4">Ingest Node VS Logstash</font></b>
<br>
Painless
自<font color="#569230">Elasticsearch 5.x</font>后引入,专门为Elasticsearch 设计,扩展了Java的语法。<b>6.0开始,ES只支持 Painless。Groovy,JavaScript和 Python 都不再支持</b>。<b>Painless支持所有Java 的数据类型及Java API子集。</b>
Painless Script具备以下特性:<br><ul><li><b>高性能/安全</b></li><li><b>支持显示类型或者动态定义类型</b></li></ul>
Painless的用途
<b>可以对文档字段进行加工处理</b>
更新或删除字段,处理数据聚合操作
Script Field:对返回的字段提前进行计算
Function Score:对文档的算分进行处理
<b>在lngest Pipeline中执行脚本</b>
<b>在Reindex APl,Update By Query时,对数据进行处理</b>
ElasticSearch数据建模最佳实践
如何处理关联关系
<ul><li>- Object: 优先考虑反范式(Denormalization)</li><li>- Nested: 当数据包含多数值对象,同时有查询需求</li><li>- Child/Parent:关联文档更新非常频繁时</li></ul>
避免过多字段
一个文档中,最好避免大量的字段
过多的字段数不容易维护
Mapping 信息保存在Cluster State 中,数据量过大,对集群性能会有影响
删除或者修改数据需要reindex
<b>默认最大字段数是1000</b>,可以设置<font color="#ed9745">index.mapping.total_fields.limit</font>限定最大字段数。
<b><font color="#314aa4">思考:什么原因会导致文档中有成百上千的字段?</font></b>
生产环境中,尽量不要打开 Dynamic,可以使用Strict控制新增字段的加入<br><ul><li>- true :未知字段会被自动加入</li><li>- false :新字段不会被索引,但是会保存在_source</li><li>- strict :新增字段不会被索引,文档写入失败</li></ul><br>对于多属性的字段,比如cookie,商品属性,可以考虑使用Nested<br>
避免正则,通配符,前缀查询
正则,通配符查询,前缀查询属于Term查询,但是性能不够好。特别是将通配符放在开头,会导致性能的灾难
避免空值引起的聚合不准
<br>
为索引的Mapping加入Meta 信息
Mappings设置非常重要,需要从两个维度进行考虑
功能︰<b>搜索,聚合,排序</b>
性能︰<b>存储的开销; 内存的开销; 搜索的性能</b>
Mappings设置是一个迭代的过程
加入新的字段很容易(必要时需要update_by_query)
更新删除字段不允许(需要Reindex重建数据)
最好能对Mappings 加入Meta 信息,更好的进行版本管理
可以考虑将Mapping文件上传git进行管理
<br>
深度应用及原理<br>
ES整体结构
ElasticSearch整体结构
<ul><li><b> 一个 ES Index 在集群模式下,有多个 Node (节点)组成。每个节点就是 ES 的Instance (实例)。</b></li><li><b> 每个节点上会有多个 shard (分片), P1 P2 是主分片, R1 R2 是副本分片</b></li><li><b> 每个分片上对应着就是一个 Lucene Index(底层索引文件)</b></li></ul><br><b>Lucene Index 是一个统称 </b><br><ul><li> 由多个 Segment (段文件,就是倒排索引)组成。每个段文件存储着就是 Doc 文档集合。</li><li> commit point记录了所有 segments 的信息</li></ul>
Lucene索引结构<br>
更多文件类型可参考 <font color="#7bd144">http://lucene.apache.org/core/7_2_1/core/org/apache/lucene/codecs/lucene70/package-summary.html#package.description</font><br>
<br>
<b>文件的关系如下:</b><br>
Lucene处理流程<br>
<br>
创建索引的过程:<br><ul><li>准备待索引的原文档,数据来源可能是文件、数据库或网络</li><li>对文档的内容进行分词组件处理,形成一系列的Term</li><li>索引组件对文档和Term处理,形成字典和倒排表</li></ul>
搜索索引的过程:<br><ul><li>对查询语句进行分词处理,形成一系列Term</li><li>根据倒排索引表查找出包含Term的文档,并进行合并形成符合结果的文档集</li><li>比对查询语句与各个文档相关性得分,并按照得分高低返回</li></ul>
ElasticSearch分析器<br>
内置分析器
<br>
什么时候使用分析器
<br>
索引文档写入和近实时搜索原理
基本概念
Segments in Lucene
众所周知,<b>Elasticsearch 存储的基本单元是 shard</b> , ES 中一个 Index 可能分为多个 shard, 事实上 <b><font color="#e74f4c">每个 shard 都是一个 Lucence 的 Index,并且每个 Lucence Index 由多个 Segment 组成, 每个 Segment 事实上是一些倒排索引的集合, 每次创建一个新的 Document , 都会归属于一个新的 Segment, 而不会去修改原来的 Segment </font></b>。<b>且每次的文档删除操作,会仅仅标记 Segment 中该文档 为删除状态, 而不会真正的立马物理删除</b>, 所以说 ES 的 index 可以理解为一个抽象的概念。 就像下图所示:
Commits in Lucene
Commit 操作意味着将 <b>Segment 合并,并写入磁盘</b>。保证内存数据尽量不丢。但刷盘是很重的 IO 操作, 所以为了机器性能和近实时搜索, 并不会刷盘那么及时。
Translog(事务日志)
<b>新文档被索引意味着文档会被首先写入内存 <font color="#e74f4c">buffer </font>和 <font color="#e74f4c">translog</font> 文件。每个 shard 都对应一个 translog 文件</b>
Refresh in Elasticsearch
在 Elasticsearch 中, <font color="#e74f4c">_refresh </font>操作默认<font color="#e74f4c"><b>每秒</b></font>执行一次, 意味着<b>将内存 buffer 的数据写入到一个新的 Segment 中</b>,这个时候索引变成了可被检索的。写入新Segment后 会清空内存buffer。
Flush in Elasticsearch
<b>Flush 操作意味着将内存 buffer 的数据全都写入新的 Segments 中</b>, 并将内存中所有的 Segments 全部刷盘, 并且<b>清空 translog 日志</b>的过程。
近实时搜索
概述
提交(Commiting)一个新的段到磁盘需要一个 <font color="#e74f4c">fsync </font>来确保段被物理性地写入磁盘,这样在断电的时候就不会丢失数据。 但是 <b>fsync 操作代价很大; 如果每次索引一个文档都去执行一次的话会造成很大的性能问题。</b><br><br> 我们需要的是一个更轻量的方式来使一个文档可被搜索,这意味着 <font color="#e74f4c">fsync </font>要从整个过程中被移除。 在 Elasticsearch 和磁盘之间是文件系统缓存。 像之前描述的一样, <b>在内存索引缓冲区中的文档会被写入到一个新的段中</b>。 但是这里<b>新段会被先写入到文件系统缓存</b>--这一步代价会比较低,<b>稍后再被刷新到 磁盘</b>--这一步代价比较高。不过<font color="#e74f4c"><b>只要文件已经在系统缓存中, 就可以像其它文件一样被打开和读取了<br></b></font><br><font color="#e0c431">ps: 在内存缓冲区中包含了新文档的 Lucene 索引</font>
<b>Lucene 允许新段被写入和打开——使其包含的文档在未进行一次完整提交时便对搜索可见</b>。 这种方式比进行一次提交代价要小得多,并且在不影响性能的前提下可以被频繁地执行。<br><br><font color="#e0c431">ps:缓冲区的内容已经被写入一个可被搜索的段中,但还没有进行提交</font>
原理
下图表示是 es 写操作流程,当一个写请求发送到 es 后,es 将数据写入<b> <font color="#e74f4c">memory buffer</font> </b>中,并添加<b><font color="#e74f4c">事务日志( translog )</font></b>。如果每次一条数据写入内存后立即写到硬盘文件上,由于写入的数据肯定是离散的,因此写入硬盘的操作也就是随机写入了。硬盘随机写入的效率相当低,会严重降低es的性能。 因此 <b>es 在设计时在 memory buffer 和硬盘间加入了 Linux 的高速缓存( File system cache )来提高 es 的写效率。<br><br> 当写请求发送到 es 后,es 将数据暂时写入 <font color="#e74f4c">memory buffer</font> 中,此时写入的数据还不能被查询到。默认设置下,es <font color="#e74f4c">每1秒</font>钟将 memory buffer 中的数据 refresh 到 Linux 的 File system cache ,并清空 memory buffer ,此时写入的数据就可以被查询到了。</b><br>
Refresh API
在 Elasticsearch 中,写入和打开一个新段的轻量的过程叫做 <font color="#e74f4c">refresh </font>。 <b>默认情况下每个分片会每秒自动刷新一次</b>。这就是为什么我们说 Elasticsearch 是近实时搜索: <b>文档的变化并不是立即对搜索可见, 但会在一秒之内变为可见。</b>
强制让某次请求直接refresh<br>
<b>并不是所有的情况都需要每秒刷新</b>。可能你正在使用 Elasticsearch 索引大量的日志文件, 你可能想优化索引速度而不是近实时搜索, 可以通过设置 <font color="#e74f4c">refresh_interval </font>, 降低每个索引的刷新频率
<font color="#e74f4c">refresh_interval </font>可以在既存索引上进行动态更新。 在生产环境中,当你正在建立一个大的新索引时,可以先关闭自动刷新,待开始使用该索引时,再把它们调回来:
持久化变更
原理
如果没有用 <font color="#e74f4c">fsync </font>把数据从文件系统缓存刷(flush)到硬盘,我们不能保证数据在断电甚至是程序正常退出之后依然存在。为了保证 Elasticsearch 的可靠性,需要确保数据变化被持久化到磁盘。<br><b>在动态更新索引时,我们说一次完整的提交会将段刷到磁盘,并写入一个包含所有段列表的提交点</b>。 Elasticsearch 在启动或重新打开一个索引的过程中使用这个提交点来判断哪些段隶属于当前分片。 <b>即使通过每秒刷新(refresh)实现了近实时搜索,我们仍然需要经常进行完整提交来确保能从失败中恢复</b>。但在两次提交之间发生变化的文档怎么办?我们也不希望丢失掉这些数据。<br>
<b><font color="#569230">Elasticsearch 增加了一个 translog ,或者叫事务日志,在每一次对 Elasticsearch 进行操作时均进行了 日志记录</font></b>。通过 translog ,整个流程看起来是下面这样:<br><b>新的文档被添加到内存缓冲区并且被追加到了事务日志</b><br>
① 一个文档被索引之后,就会被添加到内存缓冲区,并且 追加到了 translog
<b>② 刷新(<font color="#e74f4c">refresh</font>)使分片处于 下图 描述的状态,分片每秒被刷新(<font color="#e74f4c">refresh</font>)一次</b>: <br><ul><li> 这些在内存缓冲区的文档被写入到一个新的段中,且没有进行 fsync 操作。 </li><li> 这个段被打开,使其可被搜索。 </li><li> 内存缓冲区被清空。</li></ul><br>
③ 这个进程继续工作,更多的文档被添加到内存缓冲区和追加到事务日志<br>
<b>④ 每隔一段时间--例如 translog 变得越来越大--索引被刷新(flush);一个新的 translog 被创建, 并且一个全量提交被执行</b><br><ul><li> 所有在内存缓冲区的文档都被写入一个新的段。 </li><li> 缓冲区被清空。 </li><li> 一个提交点被写入硬盘。 </li><li> 文件系统缓存通过 fsync 被刷新(flush)。 </li><li><span style="font-size: inherit;"> 老的 translog 被删除。</span></li></ul><b style="font-size: inherit;"><br></b><b style="font-size: inherit;">translog 提供所有还没有被刷到磁盘的操作的一个持久化纪录。当 Elasticsearch 启动的时候, 它会从磁盘中使用最后一个提交点去恢复已知的段,并且会重放 translog 中所有在最后一次提交后发生的变更操作。</b><br style="font-size: inherit;"><span style="font-size: inherit;"> translog 也被用来提供实时 CRUD 。当你试着通过 ID 查询、更新、删除一个文档,它会在尝试从相应的段中检索之前, 首先检查 translog 任何最近的变更。这意味着它总是能够实时地获取到文档的最新版本。</span><br>
flush API<br>
<b>执行一个提交并且截断 translog 的行为在 Elasticsearch 被称作一次 flush 。 分片每<font color="#e74f4c"> 30 分钟</font>被自动刷新(flush),或者<font color="#e74f4c">在 translog 太大的时候(默认大小 512MB)也会刷新</font>。</b><br>
<font color="#ed9745">flush API</font> 可以 被用来执行一个手工的刷新(flush):
<ul><li> <b>刷新(flush) blogs 索引。 </b></li><li> <b>刷新(flush)所有的索引并且等待所有刷新在返回前完成</b>。 我们很少需要自己手动执行一个的 flush 操作;通常情况下,自动刷新就足够了。</li></ul>
<b>这就是说,在重启节点或关闭索引之前执行 flush有益于你的索引。</b>当 Elasticsearch 尝试恢复或重新打 开一个索引, 它需要重放 translog 中所有的操作,所以如果日志越短,恢复越快。
<font color="#314aa4">TransLog 有多安全 ? </font>
在文件被 <font color="#e74f4c">fsync </font>到磁盘前,被写入的文件在重启之后就会丢失。<b>默认<font color="#e74f4c"> translog 是每 5 秒被 fsync 刷新到硬盘</font>, 或者在每次写请求完成之后执行(e.g. index, delete, update, bulk)。这个过程在主分片和复制分片都会发生</b>。最终, 基本上,这意味着在<b><font color="#e74f4c">整个请求被 fsync 到主分片和复制分片的 translog 之前,你的客户端不会得到一个 200 OK 响应</font></b>。 在每次写请求后都执行一个 fsync 会带来一些性能损失,尽管实践表明这种损失相对较小(特别 是 bulk 导入,它在一次请求中平摊了大量文档的开销)。
但是对于一些大容量的偶尔丢失几秒数据问题也并不严重的集群,使用异步的 fsync 还是比较有益的。比如,写入的数据被缓存到内存中,再每 5 秒执行一次 fsync 。<br>这个行为可以通过设置 durability 参数为 async 来启用:<br>
这个选项可以针对索引单独设置,并且可以动态进行修改。<b>如果你决定使用异步 translog 的话,你需要保证在发生 crash 时,丢失掉 sync_interval 时间段的数据也无所谓</b>。请在决定前知晓这个特性。如果你不确定这个行为的后果,最好是使用默认的参数<font color="#ed9745"><b>( "index.translog.durability": "request" )</b></font>来避免数据丢失。
<br>
索引文档存储段合并机制
段合并机制(segment merge)
由于<b>自动刷新流程每秒会创建一个新的段 ,这样会导致短时间内的段数量暴增</b>。而段数目太多会带来较大的麻烦。 <b>每一个段都会消耗文件句柄、内存和 CPU 运行周期。更重要的是,每个搜索请求都必须轮 流检查每个段;所以<font color="#e74f4c">段越多,搜索也就越慢</font>。</b><br><br>Elasticsearch 通过在后台进行段合并来解决这个问题。<b>小的段被合并到大的段,然后这些大的段再被合 并到更大的段。段合并的时候会将那些旧的已删除文档 从文件系统中清除。 被删除的文档(或被更新 文档的旧版本)不会被拷贝到新的大段中</b><br>
启动段合并在进行索引和搜索时会自动进行<br>
<b>1、 当索引的时候,刷新(refresh)操作会创建新的段并将段打开以供搜索使用。</b>
<b>2、 合并进程选择一小部分大小相似的段,并且在后台将它们合并到更大的段中。这并不会中断索引和 搜索</b>。<br>
<b>3、合并完成时的活动:</b> <br><ul><li>新的段被刷新(<font color="#e74f4c">flush</font>)到了磁盘。 写入一个包含新段且排除旧的和较小的段的新提交点。 </li><li>新的段被打开用来搜索。</li><li>老的段被删除。</li></ul>
<b>合并大的段需要消耗大量的 I/O 和 CPU 资源,如果任其发展会影响搜索性能</b>。Elasticsearch 在默认情 况下会对合并流程进行资源限制,所以搜索仍然 有足够的资源很好地执行。默认情况下,归并线程的限速配置<font color="#ed9745"><b> indices.store.throttle.max_bytes_per_sec</b></font> 是 <b>20MB</b>。<b>对于写入量较大,磁盘转速较高,甚至 使用 SSD 盘的服务器来说,这个限速是明显过低的。对于 ELK Stack 应用,建议可以适当调大到 100MB或者更高。</b>
<b>用于控制归并线程的数目,推荐设置为cpu核心数的一半</b>。 如果觉得自己磁盘性能跟不上,可以降低配置,免得IO情况瓶颈<br><font color="#ed9745"><b>index.merge.scheduler.max_thread_count</b></font><br>
归并策略 policy
optimize API
<b>optimize API 大可看做是 强制合并 API</b>。它会将一个分片强制合并到 <font color="#ed9745">max_num_segments </font>参数指定 大小的段数目。 这样做的意图是<b>减少段的数量(通常减少到一个),来提升搜索性能。</b><br> 在特定情况下,使用 optimize API 颇有益处。例如在日志这种用例下,每天、每周、每月的日志被存 储在一个索引中。 老的索引实质上是只读的;它们也并不太可能会发生变化。在这种情况下,使用optimize 优化老的索引,将每一个分片合并为一个单独的段就很有用了;这样既可以节省资源,也可以 使搜索更加快速<br>
并发冲突处理
悲观锁
乐观锁
Elasticsearch的乐观锁(_version)
<b>Elasticsearch的多线程异步并发修改是基于自己的<font color="#e74f4c">_version</font>版本号进行乐观锁并发控制的</b>。 在后修改的先到时,比较版本号,版本号相同修改可以成功,而当先修改的后到时,也会比较一下 _version版本号,如果不相等就再次读取新的数据修改。这样结果会就会保存为一个正确状态,<b>删除操作也会对这条数据的版本号加1</b>
<b>在删除一个document之后,可以从一个侧面证明,它不是立即物理删除掉的,因为它的一些版本号等信息还是保留着的。先删除一条document,再重新创建这条document,其实会在delete version基础 之上,再把version号加1</b>
基于external version进行乐观锁并发控制
es提供了一个feature,就是说,你可以不用它提供的内部<font color="#e74f4c">_version</font>版本号来进行并发控制,可以基于你 自己维护的一个版本号来进行并发控制。
区别在于,<font color="#ed9745">_version</font>方式,只有当你提供的version与es中的version一模一样的时候,才可以进行修改, 只要不一样,就报错;当<b>version_type=external的时候,只有当你提供的version比es中的_version大 的时候,才能完成修改</b><br>
es,<font color="#ed9745"><b>if_seq_no=0&if_primary_term=1 </b></font>和 文档中的值相等 才能更新成功<br>es,<font color="#ed9745"><b>_version=1,?version>1&version_type=external</b></font>,才能成功,比如说? version=2&version_type=external<br>
分布式数据一致性保证
ES5.0之前
命令:<font color="#ed9745"><b>PUT /index/indextype/id?consistency=quorum</b></font>
参数
<b>One(primary shard)</b><br>
要求我们这个写操作,只要有一个primary shard是active状态,就可以执行
<b>All(all shard)</b>
要求我们这个写操作,必须所有的primary shard和replica shard都是活跃的,才可以执行这个写 操作。
<b>quorum(default)</b>
默认值,要求所有的shard中,<b>必须是法定数的shard都是活跃的</b>,可用的,才可以执行这个写操作。
quorum机制
写之前必须确保法定数shard可用
timeout机制
<b>quorum不齐全时,会wait(等待)1分钟(默认)</b>
等待期间,期望活跃的shard数量可以增加,最后无法满足shard数量就会timeout,我们其实可以在写操作的时候,加一个timeout参数,比如说 <font color="#ed9745">PUT /index/_doc/id?timeout=30s</font>,这个就是说自己去设定,quorum不齐全的时候,ES的timeout时长。<b>默认是毫秒</b>,加个s代表秒
ES5.0以及以后
从ES5.0后,<b>原先执行put 带 consistency=all / quorum 参数的,都报错了,提示语法错误。</b><br>原因是consistency检查是在Put之前做的。然而,虽然检查的时候,shard满足quorum,但是真正从 primary shard写到replica之前,仍会出现shard挂掉,但Update Api会返回succeed。因此,这个检查并不能保证replica成功写入,甚至这个primary shard是否能成功写入也未必能保证。<br><br>因此,修改了语法,用了 下面的 <font color="#ed9745">wait_for_active_shards</font>,因为这个更能清楚表述,而没有歧义。<br>
Query文档搜索机制剖析
2.0之前四种 <b>QUERY_AND_FETCH、 DFS_QUERY_AND_FETCH、QUERY_THEN_FETCH、 DFS_QUERY_THEN_FETCH</b><br>2.0版本之后 只有两种了:<b>DFS_QUERY_THEN_FETCH、QUERY_THEN_FETCH</b><br>
可以通过java的API 设置
<br>
query and fetch
<b>向索引的所有分片 ( shard)都发出查询请求, 各分片返回的时候把元素文档 ( document)和 计算后的排名信息一起返回</b>。 这种搜索方式是最快的。 因为相比下面的几种搜索方式, 这种查询方法<b>只需要去 shard查询一次</b>。 但是各个 shard 返回的结果的数量之和可能是用户要求的 size 的 n 倍。<br>
<b>优点</b>:这种搜索方式是最快的。因为相比后面的几种es的搜索方式,这种查询方法只需要去shard 查询一次。<br>
<b>缺点</b>:返回的数据量不准确, 可能返回(N*分片数量)的数据并且数据排名也不准确,同时各个 shard返回的结果的数量之和可能是用户要求的size的n倍。
DFS(distributed frequency scatter) query and fetch
这个<b>D是Distributed,F是frequency的缩写,至于S是Scatter的缩写</b>,<font color="#e74f4c">整个DFS是分布式词频率和文档频率散发的缩写</font>。 DFS 其实就是<b>在进行真正的查询之前, 先把各个分片的词频率和文档频率收集 一下, 然后进行词搜索的时候, 各分片依据全局的词频率和文档频率进行搜索和排名</b>。这种方式比第 一种方式多了一个 DFS 步骤(初始化散发(initial scatter)),<b>可以更精确控制搜索打分和排名</b>。也就是在进行查询之前,先对所有分片发送请求,把所有分片中的词频和文档频率等打分依据全部汇总到一块,再执行后面的操作。
<b>优点</b>:数据排名准确
<b>缺点</b>: 性能一般 返回的数据量不准确, 可能返回(N*分片数量)的数据
query then fetch(es 默认的搜索方式)
如果你搜索时, 没有指定搜索方式, 就是使用的这种搜索方式。 这种搜索方式, 大概分两个步骤: <br><br><b>第一步, 先向所有的 shard 发出请求, 各分片只返回文档 id(注意, 不包括文档 document)和排名相关的信息(也就是文档对应的分值)</b>, 然后按照各分片返回的文档的分数进行重新排序和排名, 取前 size 个文档。 <br><b>第二步, 根据文档 id 去相关的 shard 取 document</b>。 这种方式返回的 document 数量与用户要求的大小是相等的。<br>
<b>详细步骤</b>
<span style="font-size: inherit;"> 1.发送查询到每个shard</span><br><span style="font-size: inherit;"> 2.找到所有匹配的文档,<b>并使用本地的Term/Document Frequency信息进行打分</b></span><br><span style="font-size: inherit;"> 3.<b>对结果构建一个优先队列</b>(排序,标页等)</span><br><span style="font-size: inherit;"> 4.返回关于结果的元数据到请求节点。注意,实际文档还没有发送,只是分数</span><br><span style="font-size: inherit;"> 5.来自所有shard的分数合并起来,并在请求节点上进行排序,文档被按照查询要求进行选择</span><br><span style="font-size: inherit;"> 6.最终,<b>实际文档从他们各自所在的独立的shard上检索出来</b></span><br><span style="font-size: inherit;"> 7.结果被返回给用户</span>
<b>优点:</b>返回的数据量是准确的。
<b>缺点:</b>性能一般,并且数据排名不准确
DFS query then fetch<br>
比第 3 种方式多了一个 DFS 步骤。 也就是<b>在进行查询之前, 先对所有分片发送请求, 把所有分片中的词频和文档频率等打分依据全部汇总到一块, 再执行后面的操作。</b>
详细步骤
1.预查询每个shard,询问Term和Document frequency<br>2.发送查询到每个shard<br>3.找到所有匹配的文档,并使用全局的Term/Document Frequency信息进行打分<br>4.对结果构建一个优先队列(排序,标页等)<br>5.返回关于结果的元数据到请求节点。注意,实际文档还没有发送,只是分数<br>6.来自所有shard的分数合并起来,并在请求节点上进行排序,文档被按照查询要求进行选择<br>7.最终,实际文档从他们各自所在的独立的shard上检索出来<br>8.结果被返回给用户
<b>优点: </b>返回的数据量是准确的、数据排名准确
<b>缺点:</b> 性能最差【 这个最差只是表示在这四种查询方式中性能最慢, 也不至于不能忍受,如果对查询性能要求不是非常高, 而对查询准确度要求比较高的时候可以考虑这个】
文档增删改和搜索请求过程
写入/索引文件(增删改)
<b>增删改流程</b><br>(1)客户端首先会选择一个节点node发送请求过去,这个节点node可能是协调节点coordinating node<br>(2)协调节点coordinating node会对document数据进行路由,将请求转发给对应的node(含有 primary shard)<br>(3)实际上node的primary shard会处理请求,然后将数据同步到对应的含有replica shard的node<br>(4)协调节点coordinating node如果发现含有primary shard的node和所有的含有replica shard的 node符合要求的数量之后,就会返回响应结果给客户端<br>
文档索引过程详解<br>
整体流程
<br>
① 协调节点默认使用文档ID参与计算(也支持通过routing),以便为路由提供合适的分片。<br> <font color="#ed9745"> shard = hash(document_id) % (num_of_primary_shards)</font>
② 当分片所在的节点接收到来自协调节点的请求后,会将请求写入到<b>Memory Buffer</b>,然后<b>定时(默认是每隔1秒)</b>写入到 <b>Filesystem Cache</b>,这个<b>从Momery Buffer到Filesystem Cache的过程就叫做<font color="#e74f4c">refresh</font>;</b><br>
③ 当然在某些情况下,存在Momery Buffer和Filesystem Cache的数据可能会丢失,<b>ES是通过<font color="#ed9745">translog</font>的机制来保证数据的可靠性的。其实现机制是接收到请求后,同时也会写入到translog中,当Filesystem cache中的数据写入到磁盘中时,才会清除掉,这个过程叫做<font color="#ed9745">flush</font>。</b><br>
④ 在<font color="#ed9745">flush</font>过程中,内存中的缓冲将被清除,内容被写入一个新段,段的fsync将创建一个新的提交点,并将内容刷新到磁盘,旧的translog将被删除并开始一个新的translog。<b> flush触发的时机是定时触发(默认30分钟)或者translog变得太大(默认为512M)时。</b><br>
分步骤看数据持久化过程
write 过程
<b>一个新文档过来,会存储在 in-memory buffer 内存缓存区中,顺便会记录 Translog</b>(Elasticsearch 增加了一个 translog ,或者叫事务日志,在每一次对 Elasticsearch 进行操作时均进行了日志记录)。<br>这时候数据还没到 segment ,是搜不到这个新文档的。数据只有被 <font color="#e74f4c">refresh </font>后,才可以被搜索到。<br>
refresh 过程
<font color="#e74f4c">refresh </font>默认 1 秒钟,执行一次上图流程。ES 是支持修改这个值的,通过 <font color="#ed9745">index.refresh_interval</font> 设置 <font color="#e74f4c">refresh </font>(冲刷)间隔时间。refresh 流程大致如下:<br><ol><li><b>in-memory buffer 中的文档写入到新的 segment 中,但 segment 是存储在文件系统的缓存中。此时文档可以被搜索到</b></li><li>最后清空 in-memory buffer。注意: <b>Translog 没有被清空,为了将 segment 数据写到磁盘</b></li><li>文档经过 refresh 后, segment 暂时写到文件系统缓存,这样避免了性能 IO 操作,又可以使文档搜索到。refresh 默认 1 秒执行一次,性能损耗太大。一般建议稍微延长这个 refresh 时间间隔,比如 5 s。因此,<b>ES 其实就是准实时,达不到真正的实时。</b></li></ol>
flush 过程
每隔一段时间—例如 translog 变得越来越大—索引被刷新(flush);一个新的 translog 被创建,并且一个全量提交被执行
上个过程中 segment 在文件系统缓存中,会有意外故障文档丢失。那么,为了保证文档不会丢失,需要将文档写入磁盘。那么<b>文档从文件缓存写入磁盘的过程就是 flush。写入磁盘后,清空 translog</b>。具体过程如下:<br><ol><li>所有在内存缓冲区的文档都被写入一个新的段。</li><li>缓冲区被清空。</li><li>一个<b>Commit Point</b>被写入硬盘。</li><li>文件系统缓存通过 fsync 被刷新(flush)。</li><li>老的 translog 被删除。</li></ol>
<b>merge 过程</b>
由于自动刷新流程每秒会创建一个新的段 ,这样会导致短时间内的段数量暴增。而段数目太多会带来较大的麻烦。 <b>每一个段都会消耗文件句柄、内存和cpu运行周期。更重要的是,每个搜索请求都必须轮流检查每个段;所以<font color="#e74f4c">段越多,搜索也就越慢</font></b>。<br><br>Elasticsearch通过在后台进行<b>Merge Segment</b>来解决这个问题。小的段被合并到大的段,然后这些大的段再被合并到更大的段。<br>当索引的时候,<b>刷新(refresh)操作会创建新的段并将段打开以供搜索使用</b>。合并进程选择一小部分大小相似的段,并且在后台将它们合并到更大的段中。这并不会中断索引和搜索。<br>
一旦合并结束,老的段被删除:<br><ol><li>新的段被刷新(flush)到了磁盘。 写入一个包含新段且排除旧的和较小的段的新提交点。</li><li>新的段被打开用来搜索。</li><li>老的段被删除。</li></ol>
<b>合并大的段需要消耗大量的I/O和CPU资源,如果任其发展会影响搜索性能。Elasticsearch在默认情况下会对合并流程进行资源限制,所以搜索仍然 有足够的资源很好地执行</b><br>
深入ElasticSearch索引文档的实现机制
写操作的关键点
<b>可靠性:</b>或者是持久性,数据写入系统成功后,数据不会被回滚或丢失。
<b>一致性</b>:数据写入成功后,再次查询时必须能保证读取到最新版本的数据,不能读取到旧数据。
<b>原子性</b>:一个写入或者更新操作,要么完全成功,要么完全失败,不允许出现中间状态。
<b>隔离性:</b>多个写入操作相互不影响。
<b>实时性:</b>写入后是否可以立即被查询到。
<b>性能:</b>写入性能,吞吐量到底怎么样。
Lucene的写
众所周知,Elasticsearch内部使用了Lucene完成索引创建和搜索功能,Lucene中写操作主要是通过<font color="#ed9745">IndexWriter</font>类实现,IndexWriter提供三个接口:
通过这三个接口可以完成单个文档的写入,更新和删除功能,包括了分词,倒排创建,正排创建等等所有搜索相关的流程。只要Doc通过IndesWriter写入后,后面就可以通过IndexSearcher搜索了,看起来功能已经完善了,但是仍然有一些问题没有解:<br><ul><li><b><font color="#314aa4">- 上述操作是单机的,而不是我们需要的分布式。</font></b></li><li><b><font color="#314aa4">- 文档写入Lucene后并不是立即可查询的,需要生成完整的Segment后才可被搜索,如何保证实时性?</font></b></li><li><b><font color="#314aa4">- Lucene生成的Segment是在内存中,如果机器宕机或掉电后,内存中的Segment会丢失,如何保证数据可靠性 ?</font></b></li><li><b><font color="#314aa4">- Lucene不支持部分文档更新,但是这又是一个强需求,如何支持部分更新?</font></b></li></ul>
Elasticsearch的写
Elasticsearch采用多Shard方式,通过配置<b> routing规则</b> 将数据分成多个数据子集,每个数据子集提供独立的索引和搜索功能。当写入文档的时候,根据routing规则,将文档发送给特定Shard中建立索引。这样就能实现分布式了。<br>此外,Elasticsearch整体架构上采用了一主多副的方式:<br>
<b>每个Index由多个Shard组成,每个Shard有一个主节点和多个副本节点,副本个数可配</b>。但每次写入的时候,写入请求会先根据<font color="#ed9745">_routing</font>规则选择发给哪个Shard,Index Request中可以设置使用哪个Filed的值作为路由参数,如果没有设置,则使用Mapping中的配置,如果mapping中也没有配置,则使用<font color="#ed9745">_id</font>作为路由参数,然后通过<font color="#e74f4c">_routing</font>的Hash值选择出Shard(在OperationRouting类中),最后从集群的Meta中找出出该Shard的Primary节点。<br><br>请求接着会发送给Primary Shard,在Primary Shard上执行成功后,再从Primary Shard上将请求同时发送给多个Replica Shard,请求在多个Replica Shard上执行成功并返回给Primary Shard后,写入请求执行成功,返回结果给客户端。<br><br>这种模式下,<b>写入操作的延时就等于 latency = Latency(Primary Write) + Max(Replicas Write)</b>。只要有副本在,<b>写入延时最小也是两次单Shard的写入时延总和,写入效率会较低,但是这样的好处也很明显,避免写入后,单机或磁盘故障导致数据丢失</b>,在数据重要性和性能方面,一般都是优先选择数据,除非一些允许丢数据的特殊场景。<br><br>采用多个副本后,避免了单机或磁盘故障发生时,对已经持久化后的数据造成损害,但是Elasticsearch里为了减少磁盘IO保证读写性能,一般是每隔一段时间(比如5分钟)才会把Lucene的Segment写入磁盘持久化,对于写入内存,但还未Flush到磁盘的Lucene数据,如果发生机器宕机或者掉电,那么内存中的数据也会丢失,这时候如何保证?<br><br>对于这种问题,Elasticsearch学习了数据库中的处理方式:<b>增加CommitLog模块,Elasticsearch中叫TransLog。</b><br>
在每一个Shard中,<b>写入流程分为两部分,先写入Lucene,再写入TransLog。</b><br><br>写入请求到达Shard后,<b>先写Lucene文件,创建好索引,此时索引还在内存里面,接着去写TransLog,写完TransLog后,刷新TransLog数据到磁盘上,写磁盘成功后,请求返回给用户</b>。这里有几个关键点:<br><br><ul><li>一是和数据库不同,<b>数据库是先写CommitLog,然后再写内存</b>,而<b>Elasticsearch是先写内存,最后才写TransLog</b>,一种可能的原因是<b>Lucene的内存写入会有很复杂的逻辑,很容易失败,比如分词,字段长度超过限制等,比较重,为了避免TransLog中有大量无效记录,减少recover的复杂度和提高速度,所以就把写Lucene放在了最前面。</b></li><li>二是写Lucene内存后,并不是可被搜索的,需要通过Refresh把内存的对象转成完整的Segment后,然后再次reopen后才能被搜索,一般这个时间设置为1秒钟,导致写入Elasticsearch的文档,最快要1秒钟才可被从搜索到,所以Elasticsearch在搜索方面是<b>NRT(Near Real Time)近实时的系统</b>。</li><li>三是当Elasticsearch作为NoSQL数据库时,查询方式是GetById,这种查询可以直接从TransLog中查询,这时候就成了RT(Real Time)实时系统。</li><li>四是每隔一段比较长的时间,比如30分钟后,L<b>ucene会把内存中生成的新Segment刷新到磁盘上,刷新后索引文件已经持久化了,历史的TransLog就没用了,会清空掉旧的TransLog。</b></li></ul>
上面介绍了Elasticsearch在写入时的两个关键模块,Replica和TransLog,接下来,我们看一下<b><font color="#e74f4c">Update流程:</font></b>
Lucene中不支持部分字段的Update,所以需要在Elasticsearch中实现该功能,具体流程如下:<br><br><ol><li><span style="font-size: inherit;">收到Update请求后,从Segment或者TransLog中读取同id的完整Doc,记录版本号为V1。</span></li><li><span style="font-size: inherit;"><b>将版本V1的全量Doc和请求中的部分字段Doc合并为一个完整的Doc,同时更新内存中的VersionMap。获取到完整Doc后,Update请求就变成了Index请求。 加锁。</b></span></li><li><span style="font-size: inherit;">再次从versionMap中读取该id的最大版本号V2,如果versionMap中没有,则从Segment或者TransLog中读取,这里基本都会从versionMap中获取到。</span></li><li><span style="font-size: inherit;">检查版本是否冲突(V1==V2),如果冲突,则回退到开始的“Update doc”阶段,重新执行。如果不冲突,则执行最新的Add请求。</span></li><li><span style="font-size: inherit;"><b>在Index Doc阶段,首先将Version + 1得到V3,再将Doc加入到Lucene中去,Lucene中会先删同id下的已存在doc id,然后再增加新Doc。写入Lucene成功后,将当前V3更新到versionMap中。</b></span></li><li><span style="font-size: inherit;">释放锁,部分更新的流程就结束了。</span></li></ol>
Elasticsearch写入请求类型
Elasticsearch中的写入请求类型,主要包括下列几个:<b>Index(Create),Update,Delete和Bulk</b>,其中前3个是单文档操作,后一个Bulk是多文档操作,其中Bulk中可以包括Index(Create),Update和Delete。<br>在6.0.0及其之后的版本中,前3个单文档操作的实现基本都和Bulk操作一致,甚至有些就是通过调用Bulk的接口实现的。估计接下来几个版本后,Index(Create),Update,Delete都会被当做Bulk的一种特例化操作被处理。这样,代码和逻辑都会更清晰一些。
<ul><li>- 红色:Client Node。</li><li>- 绿色:Primary Node。</li><li>- 蓝色:Replica Node。</li></ul>
Client Node
① Ingest Pipeline
在这一步可以<b>对原始文档做一些处理</b>,比如HTML解析,自定义的处理,具体处理逻辑可以通过插件来实现。在Elasticsearch中,由于Ingest Pipeline会比较耗费CPU等资源,可以设置专门的Ingest Node,专门用来处理Ingest Pipeline逻辑。<br>如果当前Node不能执行Ingest Pipeline,则会将请求发给另一台可以执行Ingest Pipeline的Node。<br>
② Auto Create Index
判断当前Index是否存在,如果不存在,则需要自动创建Index,这里需要和Master交互。也可以通过配置关闭自动创建Index的功能。
③ Set Routing
设置路由条件,如果Request中指定了路由条件,则直接使用Request中的Routing,否则使用Mapping中配置的,如果Mapping中无配置,则使用默认的<font color="#e74f4c">_id</font>字段值。<br>在这一步中,<b>如果没有指定id字段,则会自动生成一个唯一的_id字段,目前使用的是UUID。</b><br>
④ Construct BulkShardRequest<br>
由于Bulk Request中会包括多个(Index/Update/Delete)请求,这些请求根据routing可能会落在多个Shard上执行,这一步会按Shard挑拣Single Write Request,同一个Shard中的请求聚集在一起,构建BulkShardRequest,每个BulkShardRequest对应一个Shard
⑤ Send Request To Primary
将每一个BulkShardRequest请求发送给相应Shard的Primary Node
<b>Primary Node</b>
<b>① Index or Update or Delete</b>
<b>循环执行每个Single Write Request,对于每个Request,根据操作类型(CREATE/INDEX/UPDATE/DELETE)选择不同的处理逻辑</b>。<br><br>其中,<b>Create/Index是直接新增Doc,Delete是直接根据_id删除Doc,Update会稍微复杂些</b>,我们下面就以Update为例来介绍<br>
<b>② Translate Update To Index or Delete</b>
这一步是Update操作的特有步骤,在这里,<b><font color="#e74f4c">会将Update请求转换为Index或者Delete请求</font></b>。首先,<b>会通过GetRequest查询到已经存在的同_id Doc(如果有)的完整字段和值(依赖_source字段),然后和请求中的Doc合并</b>。同时,这里会获取到读到的Doc版本号,记做V1
<b>③ Parse Doc</b>
这里会解析Doc中各个字段。生成ParsedDocument对象,同时会生成uid Term。在Elasticsearch中,<font color="#e74f4c">_uid = type # _id</font>,<b>对用户,<font color="#e74f4c">_Id</font>可见,而Elasticsearch中存储的是<font color="#e74f4c">_uid</font></b>。这一部分生成的ParsedDocument中也有Elasticsearch的系统字段,大部分会根据当前内容填充,部分未知的会在后面继续填充ParsedDocument<br>
<b>④ Update Mapping</b>
Elasticsearch中有个自动更新Mapping的功能,就在这一步生效。会先挑选出Mapping中未包含的新Field,然后判断是否运行自动更新Mapping,如果允许,则更新Mapping。
<b>⑤ Get Sequence Id and Version</b>
由于当前是Primary Shard,则会<b>从SequenceNumber Service获取一个sequenceID和Version</b>。SequenceID在Shard级别每次递增1,<font color="#e74f4c">SequenceID在写入Doc成功后,会用来初始化LocalCheckpoint</font>。Version则是根据当前Doc的最大Version递增1。
<b>⑥ Add Doc To Lucene</b>
这一步开始的时候会给特定<font color="#e74f4c">_uid</font>加锁,然后判断该<font color="#e74f4c">_uid</font>对应的Version是否等于之前Translate Update To Index步骤里获取到的Version,如果不相等,则说明刚才读取Doc后,该Doc发生了变化,出现了版本冲突,这时候会抛出一个<b>VersionConflict</b>的异常,该异常会在Primary Node最开始处捕获,重新从“Translate Update To Index or Delete”开始执行。<br><br>如果Version相等,则继续执行,如果已经存在同id的Doc,<b>则会调用Lucene的UpdateDocument(uid, doc)接口,先根据uid删除Doc,然后再Index新Doc。如果是首次写入,则直接调用Lucene的AddDocument接口完成Doc的Index,AddDocument也是通过UpdateDocument实现</b>。<br>
这一步中有个问题是,<font color="#314aa4"><b>如何保证Delete-Then-Add的原子性,怎么避免中间状态时被Refresh?</b></font><br>答案是<font color="#e74f4c">在开始Delete之前,会加一个Refresh Lock,禁止被Refresh,只有等Add完后释放了Refresh Lock后才能被Refresh,这样就保证了Delete-Then-Add的原子性。</font><br>
Lucene的UpdateDocument接口中就只是处理多个Field,会遍历每个Field逐个处理,处理顺序是<b>invert index,store field,doc values,point dimension</b>,后续会有文章专门介绍Lucene中的写入。
<b>⑦ Write Translog</b>
写完Lucene的Segment后,<b>会以keyvalue的形式写TransLog</b>,Key是<font color="#e74f4c">_id</font>,<b>Value是Doc内容</b>。当查询的时候,如果请求是<font color="#e74f4c">GetDocByID</font>,则<b>可以直接根据_id从TransLog中读取到,满足NoSQL场景下的实时性要去。</b><br>
需要注意的是,<b>这里只是写入到内存的TransLog,是否Sync到磁盘的逻辑还在后面。</b><br>这一步的最后,会标记当前SequenceID已经成功执行,接着会更新当前Shard的LocalCheckPoint。<br>
<b>⑧ Renew Bulk Request</b>
这里会重新构造Bulk Request,原因是前面已经将UpdateRequest翻译成了Index或Delete请求,则后续所有Replica中只需要执行Index或Delete请求就可以了,不需要再执行Update逻辑,一是保证Replica中逻辑更简单,性能更好,二是保证同一个请求在Primary和Replica中的执行结果一样
<b>⑨ Flush Translog</b>
这里会根据TransLog的策略,选择不同的执行方式,要么是立即Flush到磁盘,要么是等到以后再Flush。<b>Flush的频率越高,可靠性越高,对写入性能影响越大。</b>
<b>⑩ Send Requests To Replicas</b>
这里会将刚才构造的新的Bulk Request并行发送给多个Replica,然后等待Replica的返回,这里需要等待所有Replica返回后(可能有成功,也有可能失败),Primary Node才会返回用户。如果某个Replica失败了,则Primary会给Master发送一个Remove Shard请求,要求Master将该Replica Shard从可用节点中移除。<br><br>这里,同时会将SequenceID,PrimaryTerm,GlobalCheckPoint等传递给Replica。<br><br>发送给Replica的请求中,Action Name等于原始ActionName + [R],这里的R表示Replica。通过这个[R]的不同,可以找到处理Replica请求的Handler。<br>
<b>11. Receive Response From Replicas</b>
Replica中请求都处理完后,会更新Primary Node的<b>LocalCheckPoint</b>。
<b>Replica Node</b><br>
① Index or Delete
根据请求类型是Index还是Delete,选择不同的执行逻辑。这里没有Update,是因为<font color="#e74f4c">在Primary Node中已经将Update转换成了Index或Delete请求了</font>。
<b>② Parse Doc</b>
<b>③ Update Mapping</b>
以上都和Primary Node中逻辑一致。
<b>④ Get Sequence Id and Version</b>
Primary Node中会生成Sequence ID和Version,然后放入ReplicaRequest中,这里只需要从Request中获取到就行
<b>⑤ Add Doc To Lucene</b>
由于已经在Primary Node中将部分Update请求转换成了Index或Delete请求,这里只需要处理Index和Delete两种请求,不再需要处理Update请求了。比Primary Node会更简单一些。
<b>⑥ Write Translog</b>
<b>⑦ Flush Translog</b>
<font color="#314aa4">介绍了Elasticsearch的写入流程及其各个流程的工作机制,<br>我们在这里再次总结下之前提出的分布式系统中的六大特性</font>
<b>可靠性</b>:由于Lucene的设计中不考虑可靠性,在Elasticsearch中通过<b>Replica和TransLog两套机制保证数据的可靠性</b>。
<b>一致性</b>:Lucene中的Flush锁只保证Update接口里面Delete和Add中间不会Flush,但是Add完成后仍然有可能立即发生Flush,导致Segment可读。这样就没法保证Primary和所有其他Replica可以同一时间Flush,就会出现查询不稳定的情况,这里只能实现最终一致性。
<b>原子性</b>:Add和Delete都是直接调用Lucene的接口,是原子的。当部分更新时,<font color="#e74f4c">使用Version和锁保证更新</font>是原子的。
<b>隔离性</b>:仍然采用<font color="#e74f4c">Version和局部锁</font>来保证更新的是特定版本的数据。
<b>实时性</b>:<font color="#e74f4c">使用定期Refresh Segment到内存,并且Reopen Segment方式保证搜索可以在较短时间(比如1秒)内被搜索到。通过将未刷新到磁盘数据记入TransLog,保证对未提交数据可以通过ID实时访问到。</font>
<b>性能</b>:性能是一个系统性工程,所有环节都要考虑对性能的影响,在Elasticsearch中,在很多地方的设计都考虑到了性能
一是不需要所有Replica都返回后才能返回给用户,只需要返回特定数目的就行;
二是生成的Segment现在内存中提供服务,等一段时间后才刷新到磁盘,Segment在内存这段时间的可靠性由TransLog保证;
三是TransLog可以配置为周期性的Flush,但这个会给可靠性带来伤害;
四是<b>每个线程持有一个Segment,多线程时相互不影响,相互独立,性能更好;</b>
五是系统的写入流程对版本依赖较重,读取频率较高,因此采用了versionMap,减少热点数据的多次磁盘IO开销。Lucene中针对性能做了大量的优化
文件查询流程(查)<br>
<b>search流程</b><br>(1)客户端首先会选择一个节点node发送请求过去,这个节点node可能是协调节点coordinating node<br>(2)协调节点将搜索请求转发到所有的shard对应的primary shard 或 replica shard ,都可以。<br>(3)<b>query phase</b>:每个shard将自己的搜索结果的元数据到请求节点(其实就是一些doc id和 打分信 息等返回给协调节点),由协调节点进行数据的合并、排序、分页等操作,产出最终结果。<br>(4)<b>fetch phase</b>:接着由协调节点根据doc id去各个节点上拉取实际的document数据,最终返回给客户端。<br>
文档读取过程详解
<b>所有的搜索系统一般都是两阶段查询,第一阶段查询到匹配的DocID,第二阶段再查询DocID对应的完整文档</b>,这种在Elasticsearch中称为<font color="#e74f4c"><b>query_then_fetch</b></font>。(这里主要介绍最常用的2阶段查询,其它方式可以参考这里 https://zhuanlan.zhihu.com/p/34674517 )
① <b>在初始查询阶段时,查询会广播到索引中每一个分片拷贝(主分片或者副本分片)</b>。 每个分片在本地执行搜索并构建一个匹配文档的大小为 <b>from + size 的优先队列</b>。PS:在2. 搜索的时候是会查询Filesystem Cache的,但是有部分数据还在Memory Buffer,所以搜索是近实时的。
② 每个分片返回各自优先队列中 所有文档的 ID 和排序值 给协调节点,它合并这些值到自己的优先队列中来产生一个<b>全局排序后的结果列表</b>。<br>
③ 接下来就是 <b>取回阶段</b>,协调节点辨别出哪些文档需要被取回并向相关的分片提交多个 GET 请求。每个分片加载并丰富文档,如果有需要的话,接着返回文档给协调节点。一旦所有的文档都被取回了,协调节点返回结果给客户端。
深入ElasticSearch读取文档的实现机制
读操作
<b>一致性指的是写入成功后,下次读操作一定要能读取到最新的数据。</b>对于搜索,这个要求会低一些,可以有一些延迟。但是对于NoSQL数据库,则一般要求最好是强一致性的。<br><br><ul><li><b>结果匹配上</b>,NoSQL作为数据库,查询过程中只有符合不符合两种情况,而搜索里面还有是否相关,类似于NoSQL的结果只能是0或1,而搜索里面可能会有0.1,0.5,0.9等部分匹配或者更相关的情况。</li><li><b>结果召回上</b>,搜索一般只需要召回最满足条件的Top N结果即可,而NoSQL一般都需要返回满足条件的所有结果。</li></ul>
<b>搜索系统一般都是两阶段查询,第一个阶段查询到对应的Doc ID,也就是PK;第二阶段再通过Doc ID去查询完整文档,而NoSQL数据库一般是一阶段就返回结果。在Elasticsearch中两种都支持。</b><br>目前NoSQL的<b>查询,聚合、分析和统计</b>等功能上都是要比搜索弱的。<br>
Lucene的读
Elasticsearch使用了Lucene作为搜索引擎库,通过Lucene完成特定字段的搜索等功能,在Lucene中这个功能是通过<b>IndexSearcher</b>的下列接口实现的
第一个search接口实现搜索功能,返回最满足Query的N个结果;第二个doc接口通过doc id查询Doc内容;第三个count接口通过Query获取到命中数。<br><br>这三个功能是搜索中的最基本的三个功能点,对于大部分Elasticsearch中的查询都是比较复杂的,直接用这个接口是无法满足需求的,比如分布式问题。这些问题都留给了Elasticsearch解决,我们接下来看Elasticsearch中相关读功能的剖析。<br>
<b>Elasticsearch的读</b>
Elasticsearch中每个Shard都会有多个Replica,主要是为了保证数据可靠性,除此之外,还可以增加读能力,<b>因为写的时候虽然要写大部分Replica Shard,但是查询的时候只需要查询Primary和Replica中的任何一个就可以</b>了。
在上图中,该Shard有1个Primary和2个Replica Node,当查询的时候,从三个节点中根据Request中的<font color="#e74f4c">preference</font>参数选择一个节点查询。preference可以设置 <b>_local,_primary,_replica</b> 以及其他选项。<b>如果选择了primary,则每次查询都是直接查询Primary,可以保证每次查询都是最新的</b>。如果设置了其他参数,那么可能会查询到R1或者R2,这时候就有可能查询不到最新的数据。<br>
<b>Elasticsearch中通过分区实现分布式</b>,数据写入的时候根据<b>_routing规则</b>将数据写入某一个Shard中,这样就能将海量数据分布在多个Shard以及多台机器上,已达到分布式的目标。这样就导致了查询的时候,潜在数据会在当前index的所有的Shard中,所以Elasticsearch查询的时候需要查询所有Shard,同一个Shard的Primary和Replica选择一个即可,<b>查询请求会分发给所有Shard,每个Shard中都是一个独立的查询引擎,比如需要返回Top 10的结果,那么每个Shard都会查询并且返回Top 10的结果</b>,然后在Client Node里面会接收所有Shard的结果,然后<b>通过优先级队列二次排序,选择出Top 10的结果返回给用户</b>。<br><br>这里有一个问题就是请求膨胀,用户的一个搜索请求在Elasticsearch内部会变成Shard个请求,这里有个优化点,虽然是Shard个请求,但是这个Shard个数不一定要是当前Index中的Shard个数,只要是当前查询相关的Shard即可,这个需要基于业务和请求内容优化,通过这种方式可以优化请求膨胀数。<br><br>Elasticsearch中的查询主要分为两类,<b><font color="#e74f4c">Get请求</font>:通过ID查询特定Doc;<font color="#e74f4c">Search请求</font>:通过Query查询匹配Doc。</b><br>
<font color="#a66a30">上图中内存中的Segment是指刚Refresh Segment,但是还没持久化到磁盘的新Segment,而非从磁盘加载到内存中的Segment<br><br></font>对于Search类请求,查询的时候是一起<b>查询内存和磁盘上的Segment</b>,最后将结果合并后返回。这种查询是近实时(Near Real Time)的,主要是由于内存中的Index数据需要一段时间后才会刷新为Segment。<br><br><b>对于Get类请求,查询的时候是先查询内存中的TransLog,如果找到就立即返回,如果没找到再查询磁盘上的TransLog,如果还没有则再去查询磁盘上的Segment。这种查询是实时(Real Time)的</b>。这种查询顺序可以保证查询到的Doc是最新版本的Doc,这个功能也是为了保证<b>NoSQL场景下的实时性要求。</b><br>
所有的搜索系统一般都是两阶段查询,第一阶段查询到匹配的DocID,第二阶段再查询DocID对应的完整文档,这种在Elasticsearch中称为<font color="#e74f4c">query_then_fetch</font>,还有一种是一阶段查询的时候就返回完整Doc,在Elasticsearch中称作<font color="#e74f4c">query_and_fetch</font>,一般第二种适用于只需要查询一个Shard的请求。<br><br>除了一阶段,两阶段外,还有一种三阶段查询的情况。搜索里面有一种算分逻辑是根据<b>TF(Term Frequency)和DF(Document Frequency)</b>计算基础分,但是Elasticsearch中查询的时候,是在每个Shard中独立查询的,每个Shard中的TF和DF也是独立的,虽然在写入的时候通过_routing保证Doc分布均匀,但是没法保证TF和DF均匀,那么就有会导致局部的TF和DF不准的情况出现,这个时候基于TF、DF的算分就不准。为了解决这个问题,Elasticsearch中引入了DFS查询,比如<font color="#e74f4c"><b>DFS_query_then_fetch</b></font>,<b>会先收集所有Shard中的TF和DF值,然后将这些值带入请求中,再次执行query_then_fetch,这样算分的时候TF和DF就是准确的,类似的有DFS_query_and_fetch。这种查询的优势是算分更加精准,但是效率会变差</b>。另一种选择是用BM25代替TF/DF模型。<br>
<b>Elasticsearch查询流程</b>
Elasticsearch中的大部分查询,以及核心功能都是Search类型查询,上面我们了解到查询分为一阶段,二阶段和三阶段,这里我们就以最常见的的二阶段查询为例来介绍查询流程。
<b>Client Node</b>
<b>① Get Remove Cluster Shard</b>
判断是否需要跨集群访问,如果需要,则获取到要访问的Shard列表
<b>② Get Search Shard Iterator</b>
<b>获取当前Cluster中要访问的Shard</b>,和上一步中的Remove Cluster Shard合并,构建出最终要访问的完整Shard列表。<br>这一步中,会根据Request请求中的参数从Primary Node和多个Replica Node中选择出一个要访问的Shard<br>
<b>③ For Every Shard:Perform</b>
遍历每个Shard,对每个Shard执行后面逻辑。
<b>④ Send Request To Query Shard</b>
将查询阶段请求发送给相应的Shard。
<b>⑤ Merge Docs</b>
上一步将请求发送给多个Shard后,这一步就是异步等待返回结果,然后对结果合并。这里的<b>合并策略是维护一个Top N大小的优先级队列</b>,每当收到一个shard的返回,就把结果放入优先级队列做一次排序,直到所有的Shard都返回<br><br><b>翻页逻辑也是在这里,如果需要取Top 30~ Top 40的结果,这个的意思是所有Shard查询结果中的第30到40的结果,那么在每个Shard中无法确定最终的结果,每个Shard需要返回Top 40的结果给Client Node,然后Client Node中在merge docs的时候,计算出Top 40的结果,最后再去除掉Top 30,剩余的10个结果就是需要的Top 30~ Top 40的结果。</b><br><br>上述翻页逻辑有一个明显的缺点就是每次Shard返回的数据中包括了已经翻过的历史结果,如果翻页很深,则在这里需要排序的Docs会很多,比如Shard有1000,取第9990到10000的结果,那么这次查询,Shard总共需要返回1000 * 10000,也就是一千万Doc,这种情况很容易导致OOM。<br><br>另一种翻页方式是使用<b><font color="#e74f4c">search_after</font></b>,这种方式会更轻量级,如果每次只需要返回10条结构,则每个Shard只需要返回search_after之后的10个结果即可,返回的总数据量只是和Shard个数以及本次需要的个数有关,和历史已读取的个数无关。这种方式更安全一些,推荐使用这种。<br><br>如果有aggregate,也会在这里做聚合,但是不同的aggregate类型的merge策略不一样,具体的可以在后面的aggregate文章中再介绍。<br>
<b>⑥ Send Request To Fetch Shard</b>
选出Top N个Doc ID后发送给这些Doc ID所在的Shard执行Fetch Phase,最后会返回Top N的Doc的内容。<br>
<b>Query Phase</b>
<b>① Create Search Context</b>
创建<b>SearchContext</b>,之后Search过程中的所有中间状态都会存在Context中,这些状态总共有50多个,具体可以查看DefaultSearchContext或者其他SearchContext的子类。
<b>② Parse Query</b>
<b>解析Query的Source,将结果存入Search Context</b>。这里会根据请求中Query类型的不同创建不同的Query对象,比如TermQuery、FuzzyQuery等,最终真正执行TermQuery、FuzzyQuery等语义的地方是在Lucene中<br><br>这里包括了dfsPhase、queryPhase和fetchPhase三个阶段的preProcess部分,只有queryPhase的preProcess中有执行逻辑,其他两个都是空逻辑,执行完preProcess后,所有需要的参数都会设置完成。<br><br>由于Elasticsearch中有些请求之间是相互关联的,并非独立的,比如scroll请求,所以这里同时会设置Context的生命周期。<br><br>同时会设置lowLevelCancellation是否打开,这个参数是集群级别配置,同时也能动态开关,打开后会在后面执行时做更多的检测,检测是否需要停止后续逻辑直接返回<br>
<b>③ Get From Cache</b>
判断<b>请求是否允许被Cache</b>,如果允许,则检查Cache中是否已经有结果,如果有则直接读取Cache,如果没有则继续执行后续步骤,执行完后,再将结果加入Cache。
<b>④ Add Collectors</b>
<b>Collector主要目标是收集查询结果,实现排序,对自定义结果集过滤和收集等</b>。这一步会增加多个Collectors,多个Collector组成一个List。
<b>FilteredCollector</b>:先判断请求中是否有Post Filter,Post Filter用于Search,Agg等结束后再次对结果做Filter,希望Filter不影响Agg结果。如果有Post Filter则创建一个FilteredCollector,加入Collector List中。
<b>PluginInMultiCollector</b>:判断请求中是否制定了自定义的一些Collector,如果有,则创建后加入Collector List。
<b>MinimumScoreCollector</b>:判断请求中是否制定了最小分数阈值,如果指定了,则创建MinimumScoreCollector加入Collector List中,在后续收集结果时,会过滤掉得分小于最小分数的Doc。
<b>EarlyTerminatingCollector</b>:判断请求中是否提前结束Doc的Seek,如果是则创建EarlyTerminatingCollector,加入Collector List中。在后续Seek和收集Doc的过程中,当Seek的Doc数达到Early Terminating后会停止Seek后续倒排链。
<b>CancellableCollector</b>:判断当前操作是否可以被中断结束,比如是否已经超时等,如果是会抛出一个TaskCancelledException异常。该功能一般用来提前结束较长的查询请求,可以用来保护系统。
<b>EarlyTerminatingSortingCollector</b>:如果Index是排序的,那么可以提前结束对倒排链的Seek,相当于在一个排序递减链表上返回最大的N个值,只需要直接返回前N个值就可以了。这个Collector会加到Collector List的头部。EarlyTerminatingSorting和EarlyTerminating的区别是,EarlyTerminatingSorting是一种对结果无损伤的优化,而EarlyTerminating是有损的,人为掐断执行的优化。
<b>TopDocsCollector</b>:这个是最核心的Top N结果选择器,会加入到Collector List的头部。TopScoreDocCollector和TopFieldCollector都是TopDocsCollector的子类,TopScoreDocCollector会按照固定的方式算分,排序会按照分数+doc id的方式排列,如果多个doc的分数一样,先选择doc id小的文档。而TopFieldCollector则是根据用户指定的Field的值排序。
<b>⑤ lucene::search</b>
这一步会调用Lucene中IndexSearch的search接口,执行真正的搜索逻辑。每个Shard中会有多个Segment,每个Segment对应一个LeafReaderContext,这里会遍历每个Segment,到每个Segment中去Search结果,然后计算分数。<br><br><b>搜索里面一般有两阶段算分,第一阶段是在这里算的,会对每个Seek到的Doc都计算分数,为了减少CPU消耗,一般是算一个基本分数。这一阶段完成后,会有个排序。然后在第二阶段,再对Top 的结果做一次二阶段算分,在二阶段算分的时候会考虑更多的因子。二阶段算分在后续操作中。</b><br>
<b>⑥ rescore</b>
根据Request中是否包含rescore配置决定是否进行二阶段排序,如果有则执行二阶段算分逻辑,会考虑更多的算分因子。二阶段算分也是一种计算机中常见的多层设计,是一种资源消耗和效率的折中。<br><br>Elasticsearch中支持配置多个Rescore,这些rescore逻辑会顺序遍历执行。每个rescore内部会先按照请求参数window选择出Top window的doc,然后对这些doc排序,排完后再合并回原有的Top 结果顺序中。<br>
<b>⑦ suggest::execute()</b>
<b>如果有推荐请求,则在这里执行推荐请求</b>。如果请求中只包含了推荐的部分,则很多地方可以优化
<b>⑧ aggregation::execute()</b>
如果<b>含有聚合统计请求</b>,则在这里执行。Elasticsearch中的aggregate的处理逻辑也类似于Search,通过多个Collector来实现。在Client Node中也需要对aggregation做合并。aggregate逻辑更复杂一些,就不在这里赘述了,后面有需要就再单独开文章介绍。
<font color="#569230">上述逻辑都执行完成后,如果当前查询请求只需要查询一个Shard,那么会直接在当前Node执行Fetch Phase。</font>
<b>Fetch Phase</b>
Elasticsearch作为搜索系统时,或者任何搜索系统中,除了Query阶段外,还会有一个Fetch阶段,这个Fetch阶段在数据库类系统中是没有的,是搜<b>索系统中额外增加的阶段</b>。<b>搜索系统中额外增加Fetch阶段的原因是搜索系统中数据分布导致的</b>,在搜索中,数据通过<font color="#e74f4c">routing</font>分Shard的时候,只能根据一个主字段值来决定,但是查询的时候可能会根据其他非主字段查询,那么这个时候所有Shard中都可能会存在相同非主字段值的Doc,所以需要查询所有Shard才能不会出现结果遗漏。同时如果查询主字段,那么这个时候就能直接定位到Shard,就只需要查询特定Shard即可,这个时候就类似于数据库系统了。另外,数据库中的二级索引又是另外一种情况,但类似于查主字段的情况,这里就不多说了<br>
Fetch阶段的目的是通过DocID获取到用户需要的完整Doc内容。这些内容包括了DocValues,Store,Source,Script和Highlight等,具体的功能点是在SearchModule中注册的,系统默认注册的有:<br><ul><li> ExplainFetchSubPhase</li><li> DocValueFieldsFetchSubPhase</li><li> ScriptFieldsFetchSubPhase</li><li> FetchSourceSubPhase</li><li> VersionFetchSubPhase</li><li> MatchedQueriesFetchSubPhase</li><li> HighlightPhase</li><li> ParentFieldSubFetchPhase</li></ul>
除了系统默认的8种外,还有通过插件的形式注册自定义的功能,这些SubPhase中最重要的是Source和Highlight,Source是加载原文,Highlight是计算高亮显示的内容片断。<br>上述<b>多个SubPhase会针对每个Doc顺序执行,可能会产生多次的随机IO</b>,这里会有一些优化方案,但是都是针对特定场景的,不具有通用性。<br>
相关性评分算法BM25<br>
<b>BM25(Best Match25)是在信息检索系统中根据提出的query对document进行评分的算法</b>。<br><font color="#e74f4c">TF-IDF</font> 算法是一个可用的算法,但并不太完美。而BM25算法则是在此之上做出改进之后的算法。<br>
Lucene中的TF-IDF评分公式:<br>
<ul><li><b>TF:是词频(Term Frequency) </b>: 检索词在文档中出现的频率越高,相关性也越高。</li><li><b>IDF 是逆向文本频率(Inverse Document Frequency)</b>: 每个检索词在索引中出现的频率,频率越高,相关性越低。</li><li><b>字段长度归一值( field-length norm)</b></li></ul>
字段的长度是多少?字段越短,字段的权重越高。检索词出现在一个内容短的 title 要比同样的词出现在一个内容长的 content 字段权重更大。<br>以上三个因素——<b>词频(term frequency)、逆向文档频率(inverse document frequency)和字段长度归一值(field-length norm)——是在索引时计算并存储的,最后将它们结合在一起计算单个词在特定文档中的权重</b><br>
1. 当两篇描述“人工智能”的文档A和B,其中A出现“人工智能”100次,B出现“人工智能”200次。两篇文章的单词数量都是10000,那么按照 TF-IDF 算法,A的 tf 得分是:0.01,B的 tf 得分是0.02。 得分上B比A多了一倍,但是两篇文章都是再说人工智能, tf 分数不应该相差这么多。可见单纯统 计的 tf 算法在文本内容多的时候是不可靠的 <br>2. 多篇文档内容的长度长短不同,对 tf 算法的结果也影响很大,所以需要将文本的平均长度也考虑 到算法当中去。<br>
基于上面两点,BM25算法做出了改进:
<ul><li><b>k1:词语频率饱和度(term frequency saturation)</b>:它用于调节饱和度变化的速率。它的值一般 介于 1.2 到 2.0 之间。数值越低则饱和的过程越快速。(意味着两个上面A、B两个文档有相同的分数,因为他们都包含大量的“人工智能”这个词语都达到饱和程度)。在ES应用中为1.2 </li><li><b>b:字段长度归约</b>:将文档的长度归约化到全部文档的平均长度,它的值在 0 和 1 之间,1 意味着全部归约化,0 则不进行归约化。在ES的应用中为0.75。</li></ul>
<b>k1:用来控制公式对词项频率 tf 的敏感程度。</b>((k1 + 1) * tf) / (k1 + tf) 的上限是 (k1+1),也即饱和值。当 k1=0 时,不管 tf 如何变化,BM25 后一项都是 1;随着 k1 不断增大,虽然上限值依然是 (k1+1),但到 达饱和的 tf 值也会越大;当 k1 无限大时,BM25 后一项就是原始的词项频率。一句话,k1 就是衡量 高频 term 所在文档和低频 term 所在文档的相关性差异,在我们的场景下,term 频次并不重要,该 值可以设小。ES 中默认 k1=1.2,可调整为 k1=0.3。<br>
<b>b : 单个文档长度对相关性的影响力与它和平均长度的比值有关系,用来控制文档长度 L 对权值的惩罚程度</b>。b=0,则文档长度对权值无影响,b=1,则文档长度对权值达到完全的惩罚作用。ES 中默认 b=0.75,可调整为 b=0.1。<br>
ES调整BM25
<br>
排序之内核级DocValues机制
为什么要有 Doc Values
我们都知道 ElasticSearch 之所以搜索这么快速,归功于他的<b>倒排索引</b>的设计,然而它也不是万能的,<font color="#e74f4c"><b>倒排索引的检索性能是非常快的,但是在字段值排序时却不是理想的结构</b></font>。下面是一个简单的倒排索引的结构
如上表便可以看出,他只有词对应的 doc ,但是并不知道每一个 doc 中的内容,那么如果想要排序的 话每一个 doc 都去获取一次文档内容岂不非常耗时?DocValues 的出现使得这个问题迎刃而解。<br><br><b>字段的 doc_values 属性有两个值, true、false。默认为 true ,即开启。</b><br><ul><li><font color="#e74f4c">当 doc_values 为 fasle 时,无法基于该字段排序、聚合、在脚本中访问字段值。 </font></li><li><font color="#e74f4c">当 doc_values 为 true 时,<b>ES 会增加一个相应的正排索引</b>,这增加的磁盘占用,也会导致索引数据速度慢一些</font></li></ul>
Doc Values 是什么<br>
<b>正排索引:</b><font color="#e74f4c">Docvalues 通过转置倒排索引和正排索引两者间的关系来解决这个问题</font>。倒排索引将词项映射到包含它们的文档, Docvalues 将文档映射到它们包含的词项:<br>
当数据被转置之后,<b>想要收集到每个文档行,获取所有的词项就非常简单了</b>。所以搜索使用倒排索引查 找文档,聚合操作收集和聚合 DocValues 里的数据,这就是 ElasticSearch 。
深入理解 ElasticSearch Doc Values
<font color="#e74f4c"><b>DocValues 是在索引时与倒排索引同时生成。也就是说 DocValues 和 倒排索引 一样,基于 Segement 生成并且是不可变的。同时 DocValues 和 倒排索引 一样序列化到磁盘</b></font>,这样对性能和扩展性有很大帮助。 <br><br><b>DocValues 通过序列化把数据结构持久化到磁盘,我们可以充分利用操作系统的内存,而不是 JVM 的 Heap </b>。 <font color="#e74f4c">当 workingset 远小于系统的可用内存,系统会自动将 DocValues 保存在内存中,使得其读写十分高速; 不过,当其远大于可用内存时,操作系统会自动把 DocValues 写入磁盘</font>。很显然,这样性能会比在内存中差很多,但是它的大小就不再局限于服务器的内存了。如果是使用 JVM 的 Heap 来实现是因为容易 OutOfMemory 导致程序崩溃了。<br>
Doc Values 压缩<br>
<b>从广义来说, DocValues 本质上是一个序列化的 列式存储,这个结构非常适用于聚合、排序、脚本等 操作</b>。而且,这种存储方式也非常便于压缩,特别是数字类型。这样可以减少磁盘空间并且提高访问速 度。下面来看一组数字类型的 DocValues
你会注意到这里每个数字都是 100 的倍数, DocValues 会检测一个段里面的所有数值,并使用一个 最大公约数 ,方便做进一步的数据压缩。我们可以对每个数字都除以 100,然后得到: [1,10,15,12,3,19,42] 。现在这些数字变小了,只需要很少的位就可以存储下,也减少了磁盘存放 的大小
DocValues 在压缩过程中使用如下技巧。它会按依次检测以下压缩模式:
- 如果所有的数值各不相同(或缺失),设置一个标记并记录这些值
- 如果这些值小于 256,将使用一个简单的编码表
- 如果这些值大于 256,检测是否存在一个最大公约数
- 如果没有存在最大公约数,从最小的数值开始,统一计算偏移量进行编码
当然如果存储 String 类型,其一样可以通过顺序表对 String 类型进行数字编码,然后再把数字类型 构建 DocValues
禁用 Doc Values
<b>DocValues 默认对所有字段启用,除了 analyzed strings </b>。也就是说所有的数字、地理坐标、日 期、IP 和不分析( not_analyzed )字符类型都会默认开启
<b>analyzed strings 暂时还不能使用 DocValues ,是因为经过分析以后的文本会生成大量的 Token ,这样非常影响性能</b>。 虽然 DocValues 非常好用,但是如果你存储的数据确实不需要这个特性,就不如禁用他,这样不仅节省磁盘空间,也许会提升索引的速度。<br><br>要禁用 DocValues ,在字段的映射(mapping)设置 <font color="#e74f4c">doc_values:false </font>即可。例如,这里我们创 建了一个新的索引,字段 "session_id" 禁用了 DocValues :<br>
通过设置<font color="#ed9745"> doc_values:false</font> ,这个字段将不能被用于聚合、排序以及脚本操作
控制搜索精确度
基于boost的细粒度搜索的条件权重控制
<b>boost,搜索条件权重。可以将某个搜索条件的权重加大</b>,此时匹配这个搜索条件的document,在计算relevance score时,权重更大的搜索条件的document对应的relevance score会更高,当然也就会优先被返回回来。<b>默认情况下,搜索条件的权重都是1。</b>
利用<font color="#e74f4c">negative_boost</font>降低相关性
<ul><li>negative_boost 对 negative部分query生效</li><li>计算评分时,boosting部分评分不修改,negative部分query乘以negative_boost值</li><li><font color="#e74f4c">negative_boost取值: 0-1.0</font>,举例:0.3</li></ul>
<b>对某些返回结果不满意,但又不想排除掉( must_not),可以考虑boosting query的negative_boost</b>
constant_score(固定分数查询)
查询某个条件时,固定的返回指定的score;显然当不需要计算score时,只需要filter条件即可,因为filter context忽略score。
<br>
基于dis_max实现best fields策略<br>
<b>best fields策略:搜索到的结果,应该是某一个field中匹配到了尽可能多的关键词,被排在前面;而不是尽可能多的field匹配到了少数的关键词,排在了前面</b>
dis_max语法,直接取多个query中,分数最高的那一个query的分数即可
例子<br>
<br>
<br>
使用 best fields策略
基于function_score自定义相关度分数算法
在使用ES进行全文搜索时,<b>搜索结果默认会以文档的相关度进行排序,而这个 "文档的相关度",是可以通过 function_score 自己定义的,也就是说我们可以通过使用<font color="#e74f4c">function_score</font>,来控制 "怎样的文档相 关度得分更高" 这件事</b>
比如对 book 进行随机打分 如果没有给函数提供过滤,则等效于指定 "match_all":{} 要排除不符合特定分数阈值的文档,可以将min_score参数设置为所需分数阈值。
function_score 查询提供了几种类型的得分函数:<br><ul><li><b>script_score</b> 使用自定义的脚本来完全控制分值计算逻辑。如果你需要以上预定义函数之外的功能,可以根据需要通过脚本进行实现。</li><li><b>weight </b>对每份文档适用一个简单的提升,且该提升不会被归约:当weight为2时,结果为2 * _score。</li><li><b>random_score </b>使用一致性随机分值计算来对每个用户采用不同的结果排序方式,对相同用户仍然使用相同的排序方式。</li><li><b>field_value_factor </b>使用文档中某个字段的值来改变_score,比如将受欢迎程度或者投票数量考虑在内。</li><li><b>衰减函数(Decay Function) - linear,exp,gauss</b></li></ul>
Field Value factor<br>
<b>field_value_factor 函数可以使用文档中的字段来影响得分</b>。与使用 script_score 函数类似,但是它避免了脚本编写的开销。如果用于多值字段,则在计算中仅使用该字段的第一个值。
field_value_factor函数有许多选项:
<br>
modifiler取值
<br>
<font color="#358e90">field_value_score函数产生的分数必须为非负数,否则将引发错误</font>。如果在0到1之间的值上使用 log和ln修饰符将产生负值。请确保使用范围过滤器限制该字段的值以避免这种情况,或者使用 log1p 和 ln1p
Decay functions
<b>衰减函数对文档进行评分,该函数的衰减取决于文档的数字字段值与用户给定原点的距离</b>。这类似于范围查询,但具有平滑的边缘而不是框。 <br>要在具有数字字段的查询上使用距离计分,用户必须为每个字段定义 <b>origin </b>和 <b>scale </b>。需要 origin 来定义从中间计算距离的“中心点”,并需要 scale 来定义衰减率。衰减函数指定为<br>
DECAY_FUNCTION 必须是 linear , exp , gauss 其中一个 指定的字段必须是<b>数字,日期或地理点字段</b><br>
在上面的例子中,该字段是 geo_point ,可以以地理格式提供起点。在这种情况下,必须使用 scale 和 offset 。如果您的字段是日期字段,则可以将比例和偏移量设置为天,周等。如下:<br>
<b>原点的日期格式取决于映射中定义的格式</b>。如果未定义原点,则使用当前时间 offset 和 decay 参数是可选的
在第一个示例中,您的文档可能代表酒店,并且包含地理位置字段。您要根据酒店距指定位置的距离来计算衰减函数。 您可能不会立即看到为高斯功能选择哪种比例,但是您可以说:“在距所需位置2公里的距离处,分数应降低到0.33。” 然后将自动调整参数“规模”,以确保得分功能为距离期望位置2公里的酒 店计算出高于0.33的得分。
在第二个示例中,字段值在2013-09-12和2013-09-22之间的文档的权重为1.0,从该日期起15天的文档 的权重为0.5。
<b>支持的衰减函数</b>
<br>
详细例子
假设您正在寻找某个城镇的酒店。您的预算有限。另外,您希望酒店离市中心很近,因此酒店距离理想位置越远,您入住的可能性就越小。 您希望根据距市中心的距离以及价格来对与您的条件相匹配的查询结果(例如“酒店,南希,不吸烟者”)进行评分。 直观地讲,您想将市中心定义为起点,也许您愿意从酒店步行2公里到市中心。在这种情况下,您的位 置字段的来源为镇中心,范围为0〜2km。 如果您的预算低,您可能更喜欢便宜的东西而不是昂贵的东西。对于价格字段,原点为0欧元,小数位 数取决于您愿意支付的金额,例如20欧元
酒店的数据
在此示例中,字段可能被称为“价格”作为酒店价格,而“位置”则称为该酒店的坐标。<br>在这种情况下,价格字段定义为:<br>
<b>正常衰减,gauss</b>
<br>
<b>指数衰减,exp</b><br>
<br>
<b>线性衰减,linear</b><br>
<br>
bulk操作的api json格式与底层性能优化的关系
<br>
<b>两种格式对比,为什么ES选择丑陋的格式</b>
优雅格式:
<b>耗费更多的内存,更多的JVM GC开销</b>。我们之前提到过 bulk size最佳大小的问题,一般建议说在几千条那样,然后大小在10MB左右,所以说,可怕的事情来了,假设说现在100个bulk请求发送到了一个 节点上去,然后每个请求是10MB,100个请求就是1000MB=1GB。然后每个请求的json都copy一份为 JSONArray对象,此时内存中的占用就会翻倍,就会占用2GB内存,甚至还不止,因为弄成JSONArray 后,还可能会多搞一些其他的数据结构,2GB+的内存占用。 占用更多的内存可能就会积压其他请求的内存使用量,比如说最重要的搜索请求,分析请求,等等,此 时就可能会导致其他请求的性能急速下降另外的话,占用内存更多,就会导致ES的java虚拟机的垃圾回 收次数更多,更频繁,每次要回收的垃圾对象更多,耗费的时间更多,导致ES的java虚拟机停止工作线 程的时间更多。
丑陋的JSON格式:
最大的优势在于,<b>不需要将JSON数组解析为一个JSONArray对象,形成一份大数据的拷贝,浪费内存 空间,尽可能的保证性能。</b>
deep paging性能问题 和 解决方案
深度分页问题
ES 默认采用的分页方式是 <b>from+ size </b>的形式,类似于mysql的分页limit。当请求数据量比较大时, Elasticsearch会对分页做出限制,因为此时性能消耗会很大。举个例子,一个索引 分10个 shards,然 后,一个搜索请求,from=990,size=10,这时候,会带来严重的性能问题:<br><ul><li>- CPU </li><li>- 内存 </li><li>- IO </li><li>- 网络带宽</li></ul>
CPU、内存和IO消耗容易理解,网络带宽问题稍难理解一点。<b>在 query 阶段,每个shard需要返回 1000条数据给 coordinating node,而 coordinating node 需要接收 10*1000 条数据</b>,即使每条数据 只有 _doc _id 和 _score,这数据量也很大了,而且,这才一个查询请求,那如果再乘以100呢? <br><br>es中有个设置<font color="#ed9745"> index.max_result_window</font> ,默认是<b>10000条数据</b>,如果<b>分页的数据超过第1万条,就拒绝返回结果</b>了。如果你觉得自己的集群还算可以,可以适当的放大这个参数,比如100万。 我们意识到,有时这种深度分页的请求并不合理,因为我们是很少人为的看很后面的请求的,在很多的业务场景中,都直接限制分页,比如只能看前100页。 不过,这种深度分页确实存在,比如有1千万粉丝的微信大V,要给所有粉丝群发消息,或者给某省粉丝群发,这时候就需要取得所有符合条件的粉丝,而最容易想到的就是利用 from + size 来实现,但这是不现实的,我们需要使用下面的解决方案。<br>
深度分页解决方案
scroll 遍历方式
scroll 分为初始化和遍历两步,<b>初始化时将所有符合搜索条件的搜索结果缓存起来,可以想象成快照, 在遍历时,从这个快照里取数据,也就是说,在初始化后对索引插入、删除、更新数据都不会影响遍历结果</b>。因此,scroll 并不适合用来做实时搜索,而更适用于后台批处理任务,比如群发
① 初始化
<br>
初始化时需要像普通 search 一样,指明 index 和 type (当然,search 是可以不指明 index 和 type 的),然后,<b>加上参数 scroll,表示暂存搜索结果的时间</b>,其它就像一个普通的search请求一样。<br><b>初始化返回一个 scroll_id,scroll_id 用来下次取数据用。</b><br>
② 遍历
这里的 scroll_id 即 上一次遍历取回的 _scroll_id 或者是初始化返回的 _scroll_id,同样的,需要带 scroll 参数。 重复这一步骤,直到返回的数据为空,即遍历完成。<br><b>注意,每次都要传参数 scroll,刷新搜索结果的缓存时间</b>。另外,不需要指定 index 和 type。设置scroll的时候,需要使搜索结果缓存到下 一次遍历完成,同时,也不能太长,毕竟空间有限
search after方式
满足实时获取下一页的文档信息,search_after 分页的方式是根据上一页的最后一条数据来确定下一页的 位置,同时在分页请求的过程中,如果有索引数据的增删改,这些变更也会实时的反映到游标上,这种方式是在es-5.X之后才提供的。为了找到每一页最后一条数据,每个文档的排序字段必须有一个全局唯 一值 使用 <font color="#e74f4c">_id</font> 就可以了。
<b>下一页的数据依赖上一页的最后一条的信息 所以不能跳页。</b>
<b>优化:ElasticSearch性能优化详解</b>
硬件配置优化
CPU 配置<br>
一般说来,CPU 繁忙的原因有以下几个:<br><ul><li>线程中有无限空循环、无阻塞、正则匹配或者单纯的计算;</li><li>发生了频繁的 GC;</li><li>多线程的上下文切换;</li></ul><br><b>大多数 Elasticsearch 部署往往对 CPU 要求不高</b>。因此,相对其它资源,具体配置多少个(CPU)不是那么关键。你应该选择具有多个内核的现代处理器,常见的集群使用 2 到 8 个核的机器。如果你要在更快的 CPUs 和更多的核数之间选择,选择更多的核数更好。多个内核提供的额外并发远胜过稍微快一点点的时钟频率。<br>
内存配置<br>
<b>如果有一种资源是最先被耗尽的,它可能是内存。排序和聚合都很耗内存,所以有足够的堆空间来应付它们是很重要的</b>。即使堆空间是比较小的时候,也能为操作系统文件缓存提供额外的内存。因为 Lucene 使用的许多数据结构是基于磁盘的格式,Elasticsearch 利用操作系统缓存能产生很大效果<br> 由于 ES 构建基于 lucene,而 <b>lucene 设计强大之处在于 lucene 能够很好的利用操作系统内存来缓存索引数据,以提供快速的查询性能</b>。lucene 的索引文件 segements 是存储在单文件中的,并且不可变,对于 OS 来说,能够很友好地将索引文件保持在 cache 中,以便快速访问;因此,我们很有必要<b>将一半的物理内存留给 lucene;另一半的物理内存留给 ES(JVM heap)。</b>
内存分配<br>
<b>当机器内存小于 64G 时</b>,遵循通用的原则,50% 给 ES,50% 留给 lucene。
<b>当机器内存大于 64G 时</b>,遵循以下原则:<br><ul><li>如果<b>主要的使用场景是全文检索</b>,那么建议给 ES Heap 分配 4~32G 的内存即可;其它内存留给操作系统,供 lucene 使用(segments cache),以提供更快的查询性能。</li><li>如果<b>主要的使用场景是聚合或排序</b>,并且大多数是 numerics,dates,geo_points 以及 not_analyzed 的字符类型,建议分配给 ES Heap 分配 4~32G 的内存即可,其它内存留给操作系统,供 lucene 使用,提供快速的基于文档的聚类、排序性能。</li><li>如果<b>使用场景是聚合或排序</b>,并且都是基于 analyzed 字符数据,这时需要更多的 heap size,建议机器上运行多 ES 实例,每个实例保持不超过 50% 的 ES heap 设置(但不超过 32 G,堆内存设置 32 G 以下时,JVM 使用对象指标压缩技巧节省空间),50% 以上留给 lucene。</li></ul>
禁止 swap
禁止 swap,一旦允许内存与磁盘的交换,会引起致命的性能问题。可以通过在 elasticsearch.yml 中<font color="#ed9745"> bootstrap.memory_lock: true</font>,以<b>保持 JVM 锁定内存,保证 ES 的性能。</b>
GC 设置
CMS/G1
磁盘<br>
在经济压力能承受的范围下,尽量使用固态硬盘(SSD)
索引优化设置
<b>索引优化主要是在 Elasticsearch 的插入层面优化</b>,Elasticsearch 本身索引速度其实还是蛮快的,具体数据,我们可以参考官方的 benchmark 数据。我们可以根据不同的需求,针对索引优化。
① 批量提交
当有大量数据提交的时候,建议采用批量提交(Bulk 操作);此外使用 bulk 请求时,每个请求不超过几十M,因为太大会导致内存使用过大。
② 增加 Refresh 时间间隔
为了提高索引性能,Elasticsearch 在写入数据的时候,采用延迟写入的策略,即数据先写到内存中,当超过默认<font color="#ed9745">1秒(index.refresh_interval)</font>会进行一次写入操作,就是将内存中 segment 数据刷新到磁盘中,此时我们才能将数据搜索出来,所以这就是为什么 Elasticsearch 提供的是近实时搜索功能,而不是实时搜索功能。
如果我们的系统对数据延迟要求不高的话,我们可以<b>通过延长 refresh 时间间隔,可以有效地减少 segment 合并压力,提高索引速度</b>。比如在做全链路跟踪的过程中,我们就将<font color="#ed9745"> index.refresh_interval </font>设置为<b>30s</b>,减少 <font color="#ed9745">refresh </font>次数。再如,在进行全量索引时,可以将 refresh 次数临时关闭,即 <font color="#ed9745">index.refresh_interval </font>设置为-1,数据导入成功后再打开到正常模式,比如30s。<br>
<b>在加载大量数据时候可以暂时不用 refresh 和 repliccas</b>,<font color="#ed9745">index.refresh_interval</font> 设置为-1,<font color="#ed9745">index.number_of_replicas</font> 设置为0。<br>
③ 修改 index_buffer_size 的设置
索引缓冲的设置可以控制多少内存分配给索引进程。这是一个全局配置,会应用于一个节点上所有不同的分片上。
<font color="#ed9745">indices.memory.index_buffer_size</font> <b>接受一个百分比或者一个表示字节大小的值。默认是10%</b>,意味着<b>分配给节点的总内存的10%用来做索引缓冲的大小</b>。<b>这个数值被分到不同的分片(shards)上</b>。如果设置的是百分比,还可以设置 <font color="#ed9745">min_index_buffer_size</font> (默认 48mb)和 <font color="#ed9745">max_index_buffer_size</font>(默认没有上限)
<b>④ 修改 translog 相关的设置</b>
<b>一是控制数据从内存到硬盘的操作频率,以减少硬盘 IO</b>。可将 <font color="#ed9745">sync_interval </font>的时间设置大一些。默认为5s。
也可以<b>控制 tranlog 数据块的大小</b>,达到 threshold 大小时,才会 flush 到 lucene 索引文件。默认为<b>512m</b>。<br>
<b>⑤ 注意 _id 字段的使用</b>
_id 字段的使用,应尽可能避免自定义 _id,以避免针对 ID 的版本管理;<b>建议使用 ES 的默认 ID 生成策略或使用数字类型 ID 做为主键</b>
<b>⑥ 注意 _all 字段及 _source 字段的使用</b>
_all 字段及 _source 字段的使用,应该注意场景和需要,<b>_all 字段包含了所有的索引字段,方便做全文检索,如果无此需求,可以禁用</b>;_source 存储了原始的 document 内容,如果没有获取原始文档数据的需求,可通过设置 includes、excludes 属性来定义放入 _source 的字段。
<b>⑦ 合理的配置使用 index 属性</b>
合理的配置使用 index 属性,analyzed 和 not_analyzed,根据业务需求来控制字段<b>是否分词或不分词</b>。只有 groupby 需求的字段,配置时就设置成 not_analyzed,以提高查询或聚类的效率。
<b>⑧ 减少副本数量</b>
Elasticsearch 默认副本数量为3个,虽然这样会提高集群的可用性,增加搜索的并发数,但是同时也会影响写入索引的效率。<br><br><b>在索引过程中,需要把更新的文档发到副本节点上,等副本节点生效后在进行返回结束</b>。使用 Elasticsearch 做业务搜索的时候,建议副本数目还是设置为3个,但是像内部 ELK 日志系统、分布式跟踪系统中,完全可以将副本数目设置为1个。<br>
查询方面优化
路由优化
当我们查询文档的时候,Elasticsearch 如何知道一个文档应该存放到哪个分片中呢?它其实是通过下面这个公式来计算出来的。
<b>routing 默认值是文档的 id</b>,也可以采用自定义值,比如用户 ID。
不带 routing 查询
在查询的时候因为不知道要查询的数据具体在哪个分片上,所以整个过程分为2个步骤:<br><ul><li><b>分发</b>:请求到达协调节点后,协调节点将查询请求分发到每个分片上。</li><li><b>聚合</b>:协调节点搜集到每个分片上查询结果,再将查询的结果进行排序,之后给用户返回结果。</li></ul>
带 routing 查询
查询的时候,可以直接根据 routing 信息定位到某个分配查询,不需要查询所有的分配,经过协调节点排序。<br><br>向上面自定义的用户查询,如果 routing 设置为 userid 的话,就可以直接查询出数据来,效率提升很多。<br>
Filter VS Query
尽可能使用过滤器上下文(Filter)替代查询上下文(Query)<br><ul><li>- <b>Query</b>:此文档与此查询子句的匹配程度如何?</li><li>- <b>Filter</b>:此文档和查询子句匹配吗?</li></ul>
深度翻页
脚本(script)合理使用
我们知道脚本使用主要有 3 种形式,内联动态编译方式、_script 索引库中存储和文件脚本存储的形式;一般脚本的使用场景是粗排,尽量用第二种方式先将脚本存储在 _script 索引库中,起到提前编译,然后通过引用脚本 id,并结合 params 参数使用,即可以达到模型(逻辑)和数据进行了分离,同时又便于脚本模块的扩展与维护。
<b>Cache的设置及使用</b>
<b>QueryCache</b>
ES查询的时候,使用filter查询会使用query cache, 如果业务场景中的过滤查询比较多,建议将querycache设置大一些,以提高查询速度。
<font color="#ed9745">indices.queries.cache.size: 10%(默认),可设置成百分比,也可设置成具体值,如256mb。</font>
当然也可以禁用查询缓存(默认是开启), 通过<font color="#ed9745">index.queries.cache.enabled:false</font>设置。
<b>FieldDataCache</b><br>
在聚类或排序时,field data cache会使用频繁,因此,设置字段数据缓存的大小,在聚类或排序场景较多的情形下很有必要,可通过<b> indices.fielddata.cache.size:30% 或具体值10GB来设置</b>。但是如果场景或数据变更比较频繁,设置cache并不是好的做法,因为缓存加载的开销也是特别大的<br>
<b>ShardRequestCache</b>
查询请求发起后,每个分片会将结果返回给协调节点(Coordinating Node), 由协调节点将结果整合。 如果有需求,可以设置开启; 通过设置<font color="#ed9745">index.requests.cache.enable: true</font>来开启。 <br>不过,shard request cache只缓存hits.total, aggregations, suggestions类型的数据,并不会缓存hits的内容。也可以通过设置indices.requests.cache.size: 1%(默认)来控制缓存空间大小。
<b>更多查询优化经验</b>
<b>query_string 或 multi_match的查询字段越多</b>, 查询越慢。可以在mapping阶段,利用copy_to属性将多字段的值索引到一个新字段,multi_match时,用新的字段查询。
<b>日期字段的查询, 尤其是用now 的查询实际上是不存在缓存的</b>,因此, 可以从业务的角度来考虑是否一定要用now, 毕竟利用query cache 是能够大大提高查询效率的。
查询结果集的大小不能随意设置成大得离谱的值, 如query.setSize不能设置成 Integer.MAX_VALUE, 因为ES内部需要建立一个数据结构来放指定大小的结果集数据。
<b>避免层级过深的聚合查询</b>, 层级过深的aggregation , 会导致内存、CPU消耗,建议在服务层通过程序来组装业务,也可以通过pipeline的方式来优化。
复用预索引数据方式来提高AGG性能:
<b>通过开启慢查询配置定位慢查询</b>
不论是数据库还是搜索引擎,对于问题的排查,开启慢查询日志是十分必要的,ES 开启慢查询的方式有多种,但是最常用的是调用模板 API 进行全局设置:
数据结构优化
尽量减少不需要的字段
避免使用动态值作字段,动态递增的 mapping,会导致集群崩溃;同样,也需要控制字段的数量,业务中不使用的字段,就不要索引。控制索引的字段数量、mapping 深度、索引字段的类型,对于 ES 的性能优化是重中之重。<br><br>以下是 ES 关于字段数、mapping 深度的一些默认设置:<br>
Nested Object vs Parent/Child 少用<br>
<b>尽量避免使用 nested 或 parent/child 的字段,能不用就不用;nested query 慢,parent/child query 更慢,比 nested query 慢上百倍</b>;因此能在 mapping 设计阶段搞定的(大宽表设计或采用比较 smart 的数据结构),就不要用父子关系的 mapping<br><br>如果一定要使用 nested fields,<b>保证 nested fields 字段不能过多,目前 ES 默认限制是 50</b>。因为针对 1 个 document,每一个 nested field,都会生成一个独立的 document,这将使 doc 数量剧增,影响查询效率,尤其是 JOIN 的效率。
选择静态映射,非必需时,禁止动态映射
<b>尽量避免使用动态映射,这样有可能会导致集群崩溃</b>,此外,动态映射有可能会带来不可控制的数据类型,进而有可能导致在查询端出现相关异常,影响业务。
<b>document 模型设计</b>
对于 MySQL,我们经常有一些复杂的关联查询。在 es 里该怎么玩儿,es 里面的复杂的关联查询尽量别用,一旦用了性能一般都不太好。<br>最好是<b>先在 Java 系统里就完成关联,将关联好的数据直接写入 es 中。搜索的时候,就不需要利用 es 的搜索语法来完成 join 之类的关联搜索</b>了。<br><br>document 模型设计是非常重要的,很多操作,不要在搜索的时候才想去执行各种复杂的乱七八糟的操作。es 能支持的操作就那么多,不要考虑用 es 做一些它不好操作的事情。如果真的有那种操作,尽量在 document 模型设计的时候,写入的时候就完成。另外对于一些太复杂的操作,比如 join/nested/parent-child 搜索都要尽量避免,性能都很差的。
集群架构设计
主节点、数据节点和协调节点分离
关闭 data 节点服务器中的 http 功能
针对 Elasticsearch 集群中的所有数据节点,不用开启 http 服务。将其中的配置参数这样设置,<font color="#ed9745">http.enabled:false</font>,同时也不要安装 head, bigdesk, marvel 等监控插件,这样保证 data 节点服务器只需处理创建/更新/删除/查询索引数据等操作<br><br>http 功能可以在非数据节点服务器上开启,上述相关的监控插件也安装到这些服务器上,用于监控 Elasticsearch 集群状态等数据信息。这样做一来出于数据安全考虑,二来出于服务性能考虑。
<b>一台服务器上最好只部署一个 node</b>
一台物理服务器上可以启动多个 node 服务器节点(通过设置不同的启动 port),但一台服务器上的 CPU、内存、硬盘等资源毕竟有限,从服务器性能考虑,不建议一台服务器上启动多个 node 节点。
<b>集群分片设置</b>
<b>ES 一旦创建好索引后,就无法调整分片的设置</b>,而在 ES 中,一个分片实际上对应一个 lucene 索引,而 lucene 索引的读写会占用很多的系统资源,因此,分片数不能设置过大;所以,在创建索引时,合理配置分片数是非常重要的。一般来说,我们遵循一些原则:<br><br><b>控制每个分片占用的硬盘容量不超过 ES 的最大 JVM 的堆空间设置</b>(一般设置不超过 32 G,参考上面的 JVM 内存设置原则),因此,如果索引的总容量在 500 G 左右,那分片大小在 16 个左右即可;当然,最好同时考虑原则 2。 考虑一下 node 数量,一般一个节点有时候就是一台物理机,如果分片数过多,大大超过了节点数,很可能会导致一个节点上存在多个分片,一旦该节点故障,即使保持了 1 个以上的副本,同样有可能会导致数据丢失,集群无法恢复。所以,一般都设置分片数不超过节点数的 3 倍。<br>
Elastic Stack
详细信息接口参考官方文档: https://www.elastic.co/
没有日志收集系统运维工作的日常“痛点”概述<br>
Elastic Stack 分布式日志系统
EFK 架构图解
ELK架构图解
ELFK 架构图解
ELFK + Kafka架构
ELFK + Kafka 架构演变
具体搭建、日志 收集流程看 Elastic Stack (运维).md
0 条评论
下一页