Loading... # [《从菜鸟到大师之路 ElasticSearch 篇》](https://segmentfault.com/a/1190000043886875) ## (一):ElasticSearch 基础概念、生态和应用场景 ### 为什么需要学习 ElasticSearch 根据 **DB Engine** 的排名显示, **ElasticSearch** 是最受欢迎的 **企业级搜索引擎** 。下图红色勾选的是我们前面的系列详解的,除此之外你可以看到搜索库ElasticSearch在前十名内: ![lmu3gnmt.png](http://flt-pan.58heshihu.com/blog/typecho/lmu3gnmt.png) #### 所以为什么要学习 ElasticSearch 呢? 1、在当前软件行业中,搜索是一个软件系统或平台的基本功能, 学习ElasticSearch就可以为相应的软件打造出良好的搜索体验。 2、其次,ElasticSearch具备非常强的大数据分析能力。虽然Hadoop也可以做大数据分析,但是ElasticSearch的分析能力非常高,具备Hadoop不具备的能力。比如有时候用Hadoop分析一个结果,可能等待的时间比较长。 3、ElasticSearch可以很方便的进行使用,可以将其安装在个人的笔记本电脑,也可以在生产环境中,将其进行水平扩展。 4、国内比较大的互联网公司都在使用,比如小米、滴滴、携程等公司。另外,在腾讯云、阿里云的云平台上,也都有相应的ElasticSearch云产品可以使用。 5、在当今大数据时代,掌握近实时的搜索和分析能力,才能掌握核心竞争力,洞见未来。 ### 什么是ElasticSearch ElasticSearch是一款非常强大的、基于Lucene的开源搜索及分析引擎;它是一个实时的分布式搜索分析引擎,它能让你以前所未有的速度和规模,去探索你的数据。 它被用作全文检索、结构化搜索、分析以及这三个功能的组合: * Wikipedia_使用 Elasticsearch 提供带有高亮片段的全文搜索,还有 search-as-you-type 和 did-you-mean 的建议。 * 卫报使用 Elasticsearch 将网络社交数据结合到访客日志中,为它的编辑们提供公众对于新文章的实时反馈。 * Stack Overflow_将地理位置查询融入全文检索中去,并且使用 more-like-this 接口去查找相关的问题和回答。 * GitHub_使用 Elasticsearch 对1300亿行代码进行查询。 * ... 除了搜索,结合Kibana、Logstash、Beats开源产品,Elastic Stack(简称ELK)还被广泛运用在大数据近实时分析领域,包括: **日志分析、指标监控、信息安全** 等。它可以帮助你 **探索海量结构化、非结构化数据,按需创建可视化报表,对监控数据设置报警阈值,通过使用机器学习,自动识别异常状况。** ElasticSearch是基于Restful WebApi,使用Java语言开发的搜索引擎库类,并作为Apache许可条款下的开放源码发布,是当前流行的企业级搜索引擎。其客户端在Java、C#、PHP、Python等许多语言中都是可用的。 所以,ElasticSearch具备两个优势: * 天生支持分布式,可水平扩展; * 提供了Restful接口,降低全文检索的学习曲线,因为Restful接口,所以可以被任何编程语言调用; #### ElasticSearch的由来 ElasticSearch背后的小故事。 许多年前,一个刚结婚的名叫 Shay Banon 的失业开发者,跟着他的妻子去了伦敦,他的妻子在那里学习厨师。在寻找一个赚钱的工作的时候,为了给他的妻子做一个食谱搜索引擎,他开始使用 Lucene 的一个早期版本。 直接使用 Lucene 是很难的,因此 Shay 开始做一个抽象层,Java 开发者使用它可以很简单的给他们的程序添加搜索功能。他发布了他的第一个开源项目 Compass。 后来 Shay 获得了一份工作,主要是高性能,分布式环境下的内存数据网格。这个对于高性能,实时,分布式搜索引擎的需求尤为突出, 他决定重写 Compass,把它变为一个独立的服务并取名 Elasticsearch。 第一个公开版本在2010年2月发布,从此以后,Elasticsearch 已经成为了 Github 上最活跃的项目之一,他拥有超过300名 contributors(目前736名 contributors )。一家公司已经开始围绕 Elasticsearch 提供商业服务,并开发新的特性,但是,Elasticsearch 将永远开源并对所有人可用。 据说,Shay 的妻子还在等着她的食谱搜索引擎…。 #### 为什么不是直接使用Lucene ElasticSearch是基于Lucene的,那么为什么不是直接使用Lucene呢? Lucene 可以说是当下最先进、高性能、全功能的搜索引擎库。 但是 Lucene 仅仅只是一个库。为了充分发挥其功能,你需要使用 Java 并将 Lucene 直接集成到应用程序中。更糟糕的是,您可能需要获得信息检索学位才能了解其工作原理。Lucene 非常 复杂。 Elasticsearch 也是使用 Java 编写的,它的内部使用 Lucene 做索引与搜索,但是它的目的是使全文检索变得简单,通过隐藏 Lucene 的复杂性,取而代之的提供一套简单一致的 RESTful API。 然而,Elasticsearch 不仅仅是 Lucene,并且也不仅仅只是一个全文搜索引擎。它可以被下面这样准确的形容: * 一个分布式的实时文档存储,每个字段 可以被索引与搜索 * 一个分布式实时分析搜索引擎 * 能胜任上百个服务节点的扩展,并支持 PB 级别的结构化或者非结构化数据 ### ElasticSearch的主要功能及应用场景 我们在哪些场景下可以使用ES呢? #### 主要功能 * 1)海量数据的分布式存储以及集群管理,达到了服务与数据的高可用以及水平扩展; * 2)近实时搜索,性能卓越。对结构化、全文、地理位置等类型数据的处理; * 3)海量数据的近实时分析(聚合功能) #### 应用场景 * 1)网站搜索、垂直搜索、代码搜索; * 2)日志管理与分析、安全指标监控、应用性能监控、Web抓取舆情分析; ### ElasticSearch的基础概念 我们还需对比结构化数据库,看看ES的基础概念,为我们后面学习作铺垫。 * Near Realtime(NRT) 近实时。数据提交索引后,立马就可以搜索到。 * Cluster 集群,一个集群由一个唯一的名字标识,默认为“elasticsearch”。集群名称非常重要,具有相同集群名的节点才会组成一个集群。集群名称可以在配置文件中指定。 * Node 节点:存储集群的数据,参与集群的索引和搜索功能。像集群有名字,节点也有自己的名称,默认在启动时会以一个随机的UUID的前七个字符作为节点的名字,你可以为其指定任意的名字。通过集群名在网络中发现同伴组成集群。一个节点也可是集群。 * Index 索引: 一个索引是一个文档的集合(等同于solr中的集合)。每个索引有唯一的名字,通过这个名字来操作它。一个集群中可以有任意多个索引。 * Type 类型:指在一个索引中,可以索引不同类型的文档,如用户数据、博客数据。从6.0.0 版本起已废弃,一个索引中只存放一类数据。 * Document 文档:被索引的一条数据,索引的基本信息单元,以JSON格式来表示。 * Shard 分片:在创建一个索引时可以指定分成多少个分片来存储。每个分片本身也是一个功能完善且独立的“索引”,可以被放置在集群的任意节点上。 * Replication 备份: 一个分片可以有多个备份(副本) 为了方便理解,作一个ES和数据库的对比 ![lmu3rxwk.png](http://flt-pan.58heshihu.com/blog/typecho/lmu3rxwk.png) #### 文档 ElasticSearch是面向文档的,文档是所有可搜索数据的最小单位。例如: * 日志文件中的日志项; * 一张唱片的详细信息; * 一篇文章中的具体内容; 在ElasticSearch中,文档会被序列化成Json格式: * Json对象是由字段组成的; * 每个字段都有对应的字段类型(如:字符串、数值、日期类型等); 每个文档都有一个唯一的ID(Unique ID) * 可以自己指定此ID; * 也可以通过ElasticSearch自动生成; 我们可以将文档理解成关系型数据库中的一条数据记录,一条记录包含了一系列的字段。 Json文档的格式不需要预先定义 * 字段的类型可以指定或者由ElasticSearch自动推算; * Json支持数组、支持嵌套; 每一个文档中都包含有一份元数据,元数据的作用主要是用来标注文档的相关信息,如: * `_index`:文档所属的索引名; * `_type`:文档所属的类型名(从7.0开始,每一个索引只能创建一个`Type:_doc`,在此之前一个索引是可以设置多个Type的); * `_id`:文档的Unqie Id; * `_source`:文档的原始Json数据; * `_version`:文档的版本信息; * `_score`:文档的相关性算分; #### 索引(Index) 索引指的就是一类文档的集合,相当于文档的容器。 * 索引体现了逻辑空间的概念,每个索引都有自己的Mapping定义,用来定义所包含的文档的字段名和字段类型; * 索引中的数据(文档)分散在Shard(分片)上,Shard体现了物理空间的概念; ###### 索引的Mapping与Setting Mapping定义文档字段的类型; ![lmu3sl2x.png](http://flt-pan.58heshihu.com/blog/typecho/lmu3sl2x.png) Setting定义不同的数据分布; ![lmu3tjcs.png](http://flt-pan.58heshihu.com/blog/typecho/lmu3tjcs.png) ###### 索引的含义 一般说“索引文档到ElasticSearch的索引中”,前面的索引指的是一个动词的含义,也就是保存一个文档到ElasticSearch中。后面的索引是指在ElasticSearch集群中,可以创建很多个不同的索引; 索引分为:B树索引和倒排索引,而倒排索引在ElasticSearch中是非常重要的; #### ElasticSearch与RDBMS的代入理解与类比如下 ![lmu3txih.png](http://flt-pan.58heshihu.com/blog/typecho/lmu3txih.png) 两者相对比,若对数据进行全文检索,以及进行算分时,ElasticSearch更加合适;当涉及的数据事务比较高时,那RDBMS更加合适。在实际生产中,一般是两者进行结合使用。 #### 集群 ElasticSearch集群实际上是一个分布式系统,而分布式系统需要具备两个特性: * 高可用性 * 服务可用性:允许有节点停止服务; * 数据可用性:部分节点丢失,不会丢失数据; * 可扩展性 * 随着请求量的不断提升,数据量的不断增长,系统可以将数据分布到其他节点,实现水平扩展; ElasticSearch的集群通过不同的名字来进行区分,默认名字“elasticsearch”;可以通过配置文件修改或者命令行修改:`-E cluster.name=test`一个集群中可以有一个或者多个节点。 #### 节点 节点是什么? * 节点是一个ElasticSearch的实例,其本质就是一个Java进程; * 一台机器上可以运行多个ElasticSearch实例,但是建议在生产环境中一台机器上只运行一个ElasticSearch实例; 每个节点都有名字,可以通过配置文件进行配置,也可以通过命令行进行指定,如:**-E node.name=node1** 每个节点在启动之后,会被分配一个UID,保存在data目录下; Master-Eligible【有资格、胜任者】 Node与Master Node的说明: * 每个节点启动之后,默认就是一个Master Eligible节点,当然可以在配置文件中将其禁止,node.master:false * Master-Eligible Node可以参加选主流程,成为Master Node; * 当第一个节点启动时,它会将其选举为Master Node; * 每个节点都保存了集群状态,但只有Master Node才能修改集群的状态,包括如下: * 所有的节点信息; * 所有的索引和其相关的Mapping与Setting信息; * 分片的路由信息; Data Node与Coordinating【协调、整合】 Node的说明: * Data Node:可以保存数据的节点,负责保存分片数据,在数据扩展上起到至关重要的作用; * Coordinating Node:它通过接受Rest Client的请求,会将请求分发到合适的节点,最终将结果汇集到一起,再返回给Client; * 每个节点都默认起到Coordinating Node的职责; Hot Node(热节点)与Warm Node(冷节点)的说明: * Hot Node:有更好配置的节点,其有更好的资源配置,如磁盘吞吐、CPU速度; * Warm Node:资源配置较低的节点; Machine Learning Node:负责机器学习的节点,常用来做异常检测; 配置节点类型 * 每个节点在启动时,会读取elasticsearch.yml配置文件,来确定当前节点扮演什么角色。在生产环境中,应该将节点设置为单一的角色节点,这样可以有更好的性能,更清晰的职责,可以针对节点的不同给予不能的机器配置。 ![lmu3v5yx.png](http://flt-pan.58heshihu.com/blog/typecho/lmu3v5yx.png) #### 分片 ###### Primary Shard(主分片) 可以解决数据水平扩展的问题,通过主分片,可以将数据分布到集群内的所有节点之上。 一个主分片是一个运行的Lucene的实例; 注意: * 一个节点对应一个ES实例; * 一个节点可以有多个index(索引); * 一个index可以有多个shard(分片); * 一个分片是一个lucene index(此处的index是lucene自己的概念,与ES的index不是一回事); 主分片数是在索引创建时指定,后续不允许修改,除非Reindex; ###### Replica Shard(副本) 可以解决数据高可用的问题,它是主分片的拷贝。 * 副本分片数可以动态调整; * 增加副本数,在一定程度上可以提高服务的可用性; ###### 分片的设定 对于生产环境中分片的设定,需要提前做好容量规划,因为主分片数是在索引创建时预先设定的,后续无法修改。 * 分片数设置过小 * 导致后续无法增加节点进行水平扩展。 * 导致分片的数据量太大,数据在重新分配时耗时; * 分片数设置过大 * 影响搜索结果的相关性打分,影响统计结果的准确性; * 单个节点上过多的分片,会导致资源浪费,同时也会影响性能; ### 学习ElasticSearch的入手层面 #### 开发层面 * 了解ElasticSearch有基本功能; * 底层分布式工作原理; * 针对数据进行数据建模; #### 运维层面 * 进行集群的容量规划; * 对集群进行滚动升级; * 对性能的优化; * 出现问题后,对问题的诊断与解决; #### 方案层面 * 学习ElasticSearch后,可以针对实际情况,解决搜索的相关问题; * 可以将ELK运用到大数据分析场景中; ### Elastic Stack生态 ``` Beats + Logstash + ElasticSearch + Kibana ``` 如下是我从官方博客中找到图,这张图展示了ELK生态以及基于ELK的场景(最上方)。 ![lmu3vqzw.png](http://flt-pan.58heshihu.com/blog/typecho/lmu3vqzw.png) 由于Elastic X-Pack是面向收费的,所以我们不妨也把X-Pack放进去,看看哪些是由X-Pack带来的,在阅读官网文档时将方便你甄别重点: ![lmu3w7xo.png](http://flt-pan.58heshihu.com/blog/typecho/lmu3w7xo.png) #### Beats Beats是一个面向轻量型采集器的平台,这些采集器可以从边缘机器向Logstash、ElasticSearch发送数据,它是由Go语言进行开发的,运行效率方面比较快。从下图中可以看出,不同Beats的套件是针对不同的数据源。 ![lmu3whut.png](http://flt-pan.58heshihu.com/blog/typecho/lmu3whut.png) #### Logstash Logstash是动态数据收集管道,拥有可扩展的插件生态系统,支持从不同来源采集数据,转换数据,并将数据发送到不同的存储库中。其能够与ElasticSearch产生强大的协同作用,后被Elastic公司在2013年收购。 它具有如下特性: * 1)实时解析和转换数据; * 2)可扩展,具有200多个插件; * 3)可靠性、安全性。Logstash会通过持久化队列来保证至少将运行中的事件送达一次,同时将数据进行传输加密; * 4)监控; #### ElasticSearch ElasticSearch对数据进行搜索、分析和存储,其是基于JSON的分布式搜索和分析引擎,专门为实现水平可扩展性、高可靠性和管理便捷性而设计的。 它的实现原理主要分为以下几个步骤: * 1)首先用户将数据提交到ElasticSearch数据库中; * 2)再通过分词控制器将对应的语句分词; * 3)将分词结果及其权重一并存入,以备用户在搜索数据时,根据权重将结果排名和打分,将返回结果呈现给用户; ElasticSearch与DB的集成 ![lmu3wu5b.png](http://flt-pan.58heshihu.com/blog/typecho/lmu3wu5b.png) 针对上图,可以分为两种情况: * 将ElasticSearch当成数据库来存储数据,好处是架构比较简单; * 若数据更新比较频繁,同时需要考虑数据事务性时,应该先将数据存入数据库,然后建立一个合适的同步机制,将数据同步到ElasticSearch中; #### Kibana Kibana实现数据可视化,其作用就是在ElasticSearch中进行民航。Kibana能够以图表的形式呈现数据,并且具有可扩展的用户界面,可以全方位的配置和管理ElasticSearch。 Kibana最早的时候是基于Logstash创建的工具,后被Elastic公司在2013年收购。 * 1)Kibana可以提供各种可视化的图表; * 2)可以通过机器学习的技术,对异常情况进行检测,用于提前发现可疑问题; ### 从日志收集系统看 ES Stack的发展 我们看下ELK技术栈的演化,通常体现在日志收集系统中。 一个典型的日志系统包括: * 收集:能够采集多种来源的日志数据 * 传输:能够稳定的把日志数据解析过滤并传输到存储系统 * 存储:存储日志数据 * 分析:支持 UI 分析 * 警告:能够提供错误报告,监控机制 #### beats+elasticsearch+kibana Beats采集数据后,存储在ES中,有Kibana可视化的展示。 ![lmu3xc85.png](http://flt-pan.58heshihu.com/blog/typecho/lmu3xc85.png) #### beats+logstath+elasticsearch+kibana ![lmu3xzb1.png](http://flt-pan.58heshihu.com/blog/typecho/lmu3xzb1.png) 该框架是在上面的框架的基础上引入了logstash,引入logstash带来的好处如下: * (1)Logstash具有基于磁盘的自适应缓冲系统,该系统将吸收传入的吞吐量,从而减轻背压。 * (2)从其他数据源(例如数据库,S3或消息传递队列)中提取。 * (3)将数据发送到多个目的地,例如S3,HDFS或写入文件。 * (4)使用条件数据流逻辑组成更复杂的处理管道。 beats结合logstash带来的优势: * (1)水平可扩展性,高可用性和可变负载处理:beats和logstash可以实现节点之间的负载均衡,多个logstash可以实现logstash的高可用 * (2)消息持久性与至少一次交付保证:使用beats或Winlogbeat进行日志收集时,可以保证至少一次交付。从Filebeat或Winlogbeat到Logstash以及从Logstash到Elasticsearch的两种通信协议都是同步的,并且支持确认。Logstash持久队列提供跨节点故障的保护。对于Logstash中的磁盘级弹性,确保磁盘冗余非常重要。 * (3)具有身份验证和有线加密的端到端安全传输:从Beats到Logstash以及从 Logstash到Elasticsearch的传输都可以使用加密方式传递 。与Elasticsearch进行通讯时,有很多安全选项,包括基本身份验证,TLS,PKI,LDAP,AD和其他自定义领域 增加更多的数据源 比如:TCP,UDP和HTTP协议是将数据输入Logstash的常用方法。 ![lmu3yt5y.png](http://flt-pan.58heshihu.com/blog/typecho/lmu3yt5y.png) #### beats+MQ+logstash+elasticsearch+kibana ![lmu3yyqa.png](http://flt-pan.58heshihu.com/blog/typecho/lmu3yyqa.png) 在如上的基础上我们可以在beats和logstash中间添加一些组件redis、kafka、RabbitMQ等,添加中间件将会有如下好处: * (1)降低对日志所在机器的影响,这些机器上一般都部署着反向代理或应用服务,本身负载就很重了,所以尽可能的在这些机器上少做事; * (2)如果有很多台机器需要做日志收集,那么让每台机器都向Elasticsearch持续写入数据,必然会对Elasticsearch造成压力,因此需要对数据进行缓冲,同时,这样的缓冲也可以一定程度的保护数据不丢失; * (3)将日志数据的格式化与处理放到Indexer中统一做,可以在一处修改代码、部署,避免需要到多台机器上去修改配置; ### Elastic Stack最佳实践 我们再看下官方开发成员分享的最佳实践。 #### 日志收集系统 基本的日志系统 ![lmu3zksi.png](http://flt-pan.58heshihu.com/blog/typecho/lmu3zksi.png) 增加数据源,和使用MQ ![lmu3ztzg.png](http://flt-pan.58heshihu.com/blog/typecho/lmu3ztzg.png) #### Metric收集和APM性能监控 ![lmu3zzn1.png](http://flt-pan.58heshihu.com/blog/typecho/lmu3zzn1.png) #### 多数据中心方案 通过冗余实现数据高可用 ![lmu409jt.png](http://flt-pan.58heshihu.com/blog/typecho/lmu409jt.png) 两个数据采集中心(比如采集两个工厂的数据),采集数据后的汇聚 ![lmu40iv7.png](http://flt-pan.58heshihu.com/blog/typecho/lmu40iv7.png) 数据分散,跨集群的搜索 ![lmu40qdd.png](http://flt-pan.58heshihu.com/blog/typecho/lmu40qdd.png) > 来源: > https://www.pdai.tech/md/db/nosql-es/elasticsearch-x-introduce-2.html ------------- ## (二):ElasticSearch 技术原理图解 ### 图解 ElasticSearch * 云上的集群 ![lmu9jxay.png](http://flt-pan.58heshihu.com/blog/typecho/lmu9jxay.png) * 集群里的盒子 云里面的每个白色正方形的盒子代表一个节点——Node。 ![lmu9ku56.png](http://flt-pan.58heshihu.com/blog/typecho/lmu9ku56.png) * 节点之间 在一个或者多个节点直接,多个绿色小方块组合在一起形成一个 ElasticSearch 的索引。 ![lmu9l4s3.png](http://flt-pan.58heshihu.com/blog/typecho/lmu9l4s3.png) * 索引里的小方块 在一个索引下,分布在多个节点里的绿色小方块称为分片——Shard。 ![lmu9le1l.png](http://flt-pan.58heshihu.com/blog/typecho/lmu9le1l.png) * Shard=Lucene Index 一个ElasticSearch的Shard本质上是一个Lucene Index。 ![lmu9losr.png](http://flt-pan.58heshihu.com/blog/typecho/lmu9losr.png) Lucene是一个Full Text 搜索库(也有很多其他形式的搜索库),ElasticSearch是建立在Lucene之上的。接下来的故事要说的大部分内容实际上是ElasticSearch如何基于Lucene工作的。 ### 图解 Lucene #### Segment * Mini索引——segment 在Lucene里面有很多小的segment,我们可以把它们看成Lucene内部的mini-index。 ![lmu9m1kj.png](http://flt-pan.58heshihu.com/blog/typecho/lmu9m1kj.png) * Segment内部(有着许多数据结构) * Inverted Index * Stored Fields * Document Values * Cache ![lmu9mh4o.png](http://flt-pan.58heshihu.com/blog/typecho/lmu9mh4o.png) #### Inverted Index 最最重要的Inverted Index ![lmu9mw15.png](http://flt-pan.58heshihu.com/blog/typecho/lmu9mw15.png) Inverted Index主要包括两部分: * 一个有序的数据字典Dictionary(包括单词Term和它出现的频率)。 * 与单词Term对应的Postings(即存在这个单词的文件)。 当我们搜索的时候,首先将搜索的内容分解,然后在字典里找到对应Term,从而查找到与搜索相关的文件内容。 ![lmu9nh2f.png](http://flt-pan.58heshihu.com/blog/typecho/lmu9nh2f.png) * 查询“the fury” ![lmu9nmso.png](http://flt-pan.58heshihu.com/blog/typecho/lmu9nmso.png) * 自动补全(AutoCompletion-Prefix) 如果想要查找以字母“c”开头的字母,可以简单的通过二分查找(Binary Search)在Inverted Index表中找到例如“choice”、“coming”这样的词(Term)。 ![lmu9obeq.png](http://flt-pan.58heshihu.com/blog/typecho/lmu9obeq.png) * 昂贵的查找 如果想要查找所有包含“our”字母的单词,那么系统会扫描整个Inverted Index,这是非常昂贵的。 ![lmu9oo17.png](http://flt-pan.58heshihu.com/blog/typecho/lmu9oo17.png) 在此种情况下,如果想要做优化,那么我们面对的问题是如何生成合适的Term。 * 问题的转化 ![lmu9p0lm.png](http://flt-pan.58heshihu.com/blog/typecho/lmu9p0lm.png) 对于以上诸如此类的问题,我们可能会有几种可行的解决方案: 1.`* suffix -> xiffus *`如果我们想以后缀作为搜索条件,可以为Term做反向处理。 2.`(60.6384, 6.5017) -> u4u8gyykk` 对于GEO位置信息,可以将它转换为GEO Hash。 3.`123 -> {1-hundreds, 12-tens, 123}` 对于简单的数字,可以为它生成多重形式的Term。 * 解决拼写错误 一个Python库为单词生成了一个包含错误拼写信息的树形状态机,解决拼写错误的问题。 ![lmu9ph36.png](http://flt-pan.58heshihu.com/blog/typecho/lmu9ph36.png) #### Stored Field字段查找 当我们想要查找包含某个特定标题内容的文件时,Inverted Index就不能很好的解决这个问题,所以Lucene提供了另外一种数据结构Stored Fields来解决这个问题。本质上,Stored Fields是一个简单的键值对key-value。默 ![lmu9q890.png](http://flt-pan.58heshihu.com/blog/typecho/lmu9q890.png) #### Document Values 为了排序,聚合 即使这样,我们发现以上结构仍然无法解决诸如:排序、聚合、facet,因为我们可能会要读取大量不需要的信息。 所以,另一种数据结构解决了此种问题:Document Values。这种结构本质上就是一个列式的存储,它高度优化了具有相同类型的数据的存储结构。 ![lmu9qifl.png](http://flt-pan.58heshihu.com/blog/typecho/lmu9qifl.png) 为了提高效率,ElasticSearch可以将索引下某一个Document Value全部读取到内存中进行操作,这大大提升访问速度,但是也同时会消耗掉大量的内存空间。 总之,这些数据结构Inverted Index、Stored Fields、Document Values及其缓存,都在segment内部。 ###### 搜索发生时 搜索时,Lucene会搜索所有的segment然后将每个segment的搜索结果返回,最后合并呈现给客户。 Lucene的一些特性使得这个过程非常重要: * Segments是不可变的(immutable) * Delete? 当删除发生时,Lucene做的只是将其标志位置为删除,但是文件还是会在它原来的地方,不会发生改变 * Update? 所以对于更新来说,本质上它做的工作是:先删除,然后重新索引(Re-index) * 随处可见的压缩 * Lucene非常擅长压缩数据,基本上所有教科书上的压缩方式,都能在Lucene中找到。 * 缓存所有的所有 * Lucene也会将所有的信息做缓存,这大大提高了它的查询效率。 ###### 缓存的故事 当ElasticSearch索引一个文件的时候,会为文件建立相应的缓存,并且会定期(每秒)刷新这些数据,然后这些文件就可以被搜索到。 ![lmu9r316.png](http://flt-pan.58heshihu.com/blog/typecho/lmu9r316.png) 随着时间的增加,我们会有很多segments, ![lmu9rc3m.png](http://flt-pan.58heshihu.com/blog/typecho/lmu9rc3m.png) 所以ElasticSearch会将这些segment合并,在这个过程中,segment会最终被删除掉 ![lmu9rl94.png](http://flt-pan.58heshihu.com/blog/typecho/lmu9rl94.png) 这就是为什么增加文件可能会使索引所占空间变小,它会引起merge,从而可能会有更多的压缩。 * 举个栗子 有两个segment将会merge ![lmu9rwbz.png](http://flt-pan.58heshihu.com/blog/typecho/lmu9rwbz.png) 这两个segment最终会被删除,然后合并成一个新的segment ![lmu9s67c.png](http://flt-pan.58heshihu.com/blog/typecho/lmu9s67c.png) 这时这个新的segment在缓存中处于cold状态,但是大多数segment仍然保持不变,处于warm状态。 以上场景经常在Lucene Index内部发生的。 ![lmu9se6y.png](http://flt-pan.58heshihu.com/blog/typecho/lmu9se6y.png) #### 在Shard中搜索 ElasticSearch从Shard中搜索的过程与Lucene Segment中搜索的过程类似。 ![lmu9t528.png](http://flt-pan.58heshihu.com/blog/typecho/lmu9t528.png) 与在Lucene Segment中搜索不同的是,Shard可能是分布在不同Node上的,所以在搜索与返回结果时,所有的信息都会通过网络传输。 需要注意的是:1次搜索查找2个shard = 2次分别搜索shard ![lmu9td6a.png](http://flt-pan.58heshihu.com/blog/typecho/lmu9td6a.png) * 对于日志文件的处理 当我们想搜索特定日期产生的日志时,通过根据时间戳对日志文件进行分块与索引,会极大提高搜索效率。 当我们想要删除旧的数据时也非常方便,只需删除老的索引即可。 ![lmu9tm9k.png](http://flt-pan.58heshihu.com/blog/typecho/lmu9tm9k.png) 在上种情况下,每个index有两个shards * 如何Scale ![lmu9two8.png](http://flt-pan.58heshihu.com/blog/typecho/lmu9two8.png) shard不会进行更进一步的拆分,但是shard可能会被转移到不同节点上 ![lmu9u5gy.png](http://flt-pan.58heshihu.com/blog/typecho/lmu9u5gy.png) 所以,如果当集群节点压力增长到一定的程度,我们可能会考虑增加新的节点,这就会要求我们对所有数据进行重新索引,这是我们不太希望看到的,所以我们需要在规划的时候就考虑清楚,如何去平衡足够多的节点与不足节点之间的关系。 * 节点分配与Shard优化 * 为更重要的数据索引节点,分配性能更好的机器 * 确保每个shard都有副本信息replica ![lmu9umbd.png](http://flt-pan.58heshihu.com/blog/typecho/lmu9umbd.png) * 路由Routing 每个节点,每个都存留一份路由表,所以当请求到任何一个节点时,ElasticSearch都有能力将请求转发到期望节点的shard进一步处理。 ![lmu9uv54.png](http://flt-pan.58heshihu.com/blog/typecho/lmu9uv54.png) ### ElasticSearch整体结构 通过上文,在通过图解了解了ES整体的原理后,我们梳理下ES的整体结构 ![lmu9v7ba.png](http://flt-pan.58heshihu.com/blog/typecho/lmu9v7ba.png) * 一个 ES Index 在集群模式下,有多个 Node (节点)组成。每个节点就是 ES 的Instance (实例)。 * 每个节点上会有多个 shard (分片), P1 P2 是主分片, R1 R2 是副本分片 * 每个分片上对应着就是一个 Lucene Index(底层索引文件) * Lucene Index 是一个统称 * 由多个 Segment (段文件,就是倒排索引)组成。每个段文件存储着就是 Doc 文档。 * commit point记录了所有 segments 的信息 ### Lucene索引结构 上图中Lucene的索引结构中有哪些文件呢? ![lmu9vn0s.png](http://flt-pan.58heshihu.com/blog/typecho/lmu9vn0s.png) 文件的关系如下: ![lmu9vup1.png](http://flt-pan.58heshihu.com/blog/typecho/lmu9vup1.png) #### Lucene处理流程 上文图解过程,还需要理解Lucene处理流程, 这将帮助你更好的索引文档和搜索文档。 ![lmu9w7mp.png](http://flt-pan.58heshihu.com/blog/typecho/lmu9w7mp.png) 创建索引的过程: * 准备待索引的原文档,数据来源可能是文件、数据库或网络 * 对文档的内容进行分词组件处理,形成一系列的Term * 索引组件对文档和Term处理,形成字典和倒排表 搜索索引的过程: * 对查询语句进行分词处理,形成一系列Term * 根据倒排索引表查找出包含Term的文档,并进行合并形成符合结果的文档集 * 比对查询语句与各个文档相关性得分,并按照得分高低返回 ### ElasticSearch分析器 上图中很重要的一项是语法分析/语言处理, 所以我们还需要补充ElasticSearch分析器知识点。 分析 包含下面的过程: * 首先,将一块文本分成适合于倒排索引的独立的 词条 , * 之后,将这些词条统一化为标准格式以提高它们的“可搜索性”,或者 recall 分析器执行上面的工作。`分析器`实际上是将三个功能封装到了一个包里: * 字符过滤器 首先,字符串按顺序通过每个`字符过滤器`。他们的任务是在分词前整理字符串。一个字符过滤器可以用来去掉HTML,或者将 & 转化成 and。 * 分词器 其次,字符串被`分词器`分为单个的词条。一个简单的分词器遇到空格和标点的时候,可能会将文本拆分成词条。 * Token 过滤器 最后,词条按顺序通过每个 token 过滤器 。这个过程可能会改变词条(例如,小写化 Quick ),删除词条(例如, 像 a, and, the 等无用词),或者增加词条(例如,像 jump 和 leap 这种同义词)。 Elasticsearch提供了开箱即用的字符过滤器、分词器和token 过滤器。这些可以组合起来形成自定义的分析器以用于不同的目的。 #### 内置分析器 Elasticsearch还附带了可以直接使用的预包装的分析器。接下来我们会列出最重要的分析器。为了证明它们的差异,我们看看每个分析器会从下面的字符串得到哪些词条: ``` "Set the shape to semi-transparent by calling set_trans(5)" ``` * 标准分析器 标准分析器是Elasticsearch默认使用的分析器。它是分析各种语言文本最常用的选择。它根据 Unicode 联盟 定义的 单词边界 划分文本。删除绝大部分标点。最后,将词条小写。它会产生。 ``` set, the, shape, to, semi, transparent, by, calling, set_trans, 5 ``` * 简单分析器 简单分析器在任何不是字母的地方分隔文本,将词条小写。它会产生 ``` set, the, shape, to, semi, transparent, by, calling, set, trans ``` * 空格分析器 空格分析器在空格的地方划分文本。它会产生 ``` Set, the, shape, to, semi-transparent, by, calling, set_trans(5) ``` * 语言分析器 特定语言分析器可用于很多语言。它们可以考虑指定语言的特点。例如,英语 分析器附带了一组英语无用词(常用单词,例如 and 或者 the ,它们对相关性没有多少影响),它们会被删除。由于理解英语语法的规则,这个分词器可以提取英语单词的词干 。 英语 分词器会产生下面的词条: ``` set, shape, semi, transpar, call, set_tran, 5 ``` 注意看 transparent、 calling 和 set_trans 已经变为词根格式。 ###### 什么时候使用分析器 当我们索引一个文档,它的全文域被分析成词条以用来创建倒排索引。但是,当我们在全文域搜索的时候,我们需要将查询字符串通过相同的分析过程,以保证我们搜索的词条格式与索引中的词条格式一致。 全文查询,理解每个域是如何定义的,因此它们可以做正确的事: * 当你查询一个`全文域`时, 会对查询字符串应用相同的分析器,以产生正确的搜索词条列表。 * 当你查询一个`精确值域`时,不会分析查询字符串,而是搜索你指定的精确值。 ###### 举个例子 ES中每天一条数据, 按照如下方式查询: ``` GET /_search?q=2014 # 12 results GET /_search?q=2014-09-15 # 12 results ! GET /_search?q=date:2014-09-15 # 1 result GET /_search?q=date:2014 # 0 results ! ``` 为什么返回那样的结果? * date 域包含一个精确值:单独的词条 2014-09-15。 * _all 域是一个全文域,所以分词进程将日期转化为三个词条:2014, 09, 和 15。 当我们在`_all`域查询 2014,它匹配所有的12条推文,因为它们都含有 2014 : ``` GET /_search?q=2014 # 12 results ``` 当我们在`_all`域查询 2014-09-15,它首先分析查询字符串,产生匹配 2014,09, 或 15 中`任意`词条的查询。这也会匹配所有12条推文,因为它们都含有 2014 : ``` GET /_search?q=2014-09-15 # 12 results ! ``` 当我们在 date 域查询 2014-09-15,它寻找`精确`日期,只找到一个推文: ``` GET /_search?q=date:2014-09-15 # 1 result ``` 当我们在 date 域查询 2014,它找不到任何文档,因为没有文档含有这个精确日志: ``` GET /_search?q=date:2014 # 0 results ! ``` > 来源: > https://www.pdai.tech/md/db/nosql-es/elasticsearch-y-th-2.html ------------- ## (三):ElasticSearch 安装与基础使用 ### 安装 ElasticSearch ElasticSearch 是基于Java平台的,所以先要安装Java。 #### 平台确认 这里我准备了一台Centos7虚拟机, 为方便选择后续安装的版本,所以需要看下系统版本信息。 ``` [root@centos ~]# uname -a Linux centos 3.10.0-862.el7.x86_64 #1 SMP Fri Apr 20 16:44:24 UTC 2018 x86_64 x86_64 x86_64 GNU/Linux ``` #### 安装Java 安装 Elasticsearch 之前,你需要先安装一个较新的版本的 Java,最好的选择是,你可以从http://www.java.com/获得官方提供的最新版本的 Java。安装以后,确认是否安装成功: ``` [root@centos ~]# java --version openjdk 14.0.2 2020-07-14 OpenJDK Runtime Environment 20.3 (slowdebug build 14.0.2+12) OpenJDK 64-Bit Server VM 20.3 (slowdebug build 14.0.2+12, mixed mode, sharing) ``` #### 下载ElasticSearch 从https://www.elastic.co/cn/downloads/elasticsearch下载ElasticSearch 比如可以通过curl下载 ``` [root@centos opt]# curl -O https://artifacts.elastic.co/downloads/elasticsearch/elasticsearch-7.12.0-linux-x86_64.tar.gz % Total % Received % Xferd Average Speed Time Time Time Current Dload Upload Total Spent Left Speed ``` 解压 ``` [root@centos opt]# tar zxvf /opt/elasticsearch-7.12.0-linux-x86_64.tar.gz ... [root@centos opt]# ll | grep elasticsearch drwxr-xr-x 9 root root 4096 Mar 18 14:21 elasticsearch-7.12.0 -rw-r--r-- 1 root root 327497331 Apr 5 21:05 elasticsearch-7.12.0-linux-x86_64.tar.gz ``` ###### 增加elasticSearch用户 必须创建一个非root用户来运行ElasticSearch(ElasticSearch5及以上版本,基于安全考虑,强制规定不能以root身份运行。) 如果你使用root用户来启动ElasticSearch,则会有如下错误信息: ``` [root@centos opt]# cd elasticsearch-7.12.0/ [root@centos elasticsearch-7.12.0]# ./bin/elasticsearch [2021-04-05T21:36:46,510][ERROR][o.e.b.ElasticsearchUncaughtExceptionHandler] [centos] uncaught exception in thread [main] org.elasticsearch.bootstrap.StartupException: java.lang.RuntimeException: can not run elasticsearch as root at org.elasticsearch.bootstrap.Elasticsearch.init(Elasticsearch.java:163) ~[elasticsearch-7.12.0.jar:7.12.0] at org.elasticsearch.bootstrap.Elasticsearch.execute(Elasticsearch.java:150) ~[elasticsearch-7.12.0.jar:7.12.0] at org.elasticsearch.cli.EnvironmentAwareCommand.execute(EnvironmentAwareCommand.java:75) ~[elasticsearch-7.12.0.jar:7.12.0] at org.elasticsearch.cli.Command.mainWithoutErrorHandling(Command.java:116) ~[elasticsearch-cli-7.12.0.jar:7.12.0] at org.elasticsearch.cli.Command.main(Command.java:79) ~[elasticsearch-cli-7.12.0.jar:7.12.0] at org.elasticsearch.bootstrap.Elasticsearch.main(Elasticsearch.java:115) ~[elasticsearch-7.12.0.jar:7.12.0] at org.elasticsearch.bootstrap.Elasticsearch.main(Elasticsearch.java:81) ~[elasticsearch-7.12.0.jar:7.12.0] Caused by: java.lang.RuntimeException: can not run elasticsearch as root at org.elasticsearch.bootstrap.Bootstrap.initializeNatives(Bootstrap.java:101) ~[elasticsearch-7.12.0.jar:7.12.0] at org.elasticsearch.bootstrap.Bootstrap.setup(Bootstrap.java:168) ~[elasticsearch-7.12.0.jar:7.12.0] at org.elasticsearch.bootstrap.Bootstrap.init(Bootstrap.java:397) ~[elasticsearch-7.12.0.jar:7.12.0] at org.elasticsearch.bootstrap.Elasticsearch.init(Elasticsearch.java:159) ~[elasticsearch-7.12.0.jar:7.12.0] ... 6 more uncaught exception in thread [main] java.lang.RuntimeException: can not run elasticsearch as root at org.elasticsearch.bootstrap.Bootstrap.initializeNatives(Bootstrap.java:101) at org.elasticsearch.bootstrap.Bootstrap.setup(Bootstrap.java:168) at org.elasticsearch.bootstrap.Bootstrap.init(Bootstrap.java:397) at org.elasticsearch.bootstrap.Elasticsearch.init(Elasticsearch.java:159) at org.elasticsearch.bootstrap.Elasticsearch.execute(Elasticsearch.java:150) at org.elasticsearch.cli.EnvironmentAwareCommand.execute(EnvironmentAwareCommand.java:75) at org.elasticsearch.cli.Command.mainWithoutErrorHandling(Command.java:116) at org.elasticsearch.cli.Command.main(Command.java:79) at org.elasticsearch.bootstrap.Elasticsearch.main(Elasticsearch.java:115) at org.elasticsearch.bootstrap.Elasticsearch.main(Elasticsearch.java:81) For complete error details, refer to the log at /opt/elasticsearch-7.12.0/logs/elasticsearch.log 2021-04-05 13:36:46,979269 UTC [8846] INFO Main.cc@106 Parent process died - ML controller exiting ``` 所以我们增加一个独立的elasticsearch用户来运行 ``` # 增加elasticsearch用户 [root@centos elasticsearch-7.12.0]# useradd elasticsearch [root@centos elasticsearch-7.12.0]# passwd elasticsearch Changing password for user elasticsearch. New password: BAD PASSWORD: The password contains the user name in some form Retype new password: passwd: all authentication tokens updated successfully. # 修改目录权限至新增的elasticsearch用户 [root@centos elasticsearch-7.12.0]# chown -R elasticsearch /opt/elasticsearch-7.12.0 # 增加data和log存放区,并赋予elasticsearch用户权限 [root@centos elasticsearch-7.12.0]# mkdir -p /data/es [root@centos elasticsearch-7.12.0]# chown -R elasticsearch /data/es [root@centos elasticsearch-7.12.0]# mkdir -p /var/log/es [root@centos elasticsearch-7.12.0]# chown -R elasticsearch /var/log/es ``` 然后修改上述的data和log路径,`vi /opt/elasticsearch-7.12.0/config/elasticsearch.yml` ``` # -------------------------------------- Paths --------------------------------------- # # Path to directory where to store the data (separate multiple locations by comma): # path.data: /data/es # # Path to log files: # path.logs: /var/log/es ``` #### 修改Linux系统的限制配置 * 1.修改系统中允许应用最多创建多少文件等的限制权限。Linux默认来说,一般限制应用最多创建的文件是65535个。但是ES至少需要65536的文件创建权限。 * 2.修改系统中允许用户启动的进程开启多少个线程。默认的Linux限制root用户开启的进程可以开启任意数量的线程,其他用户开启的进程可以开启1024个线程。必须修改限制数为4096+。因为ES至少需要4096的线程池预备。ES在5.x版本之后,强制要求在linux中不能使用root用户启动ES进程。所以必须使用其他用户启动ES进程才可以。 * 3.Linux低版本内核为线程分配的内存是128K。4.x版本的内核分配的内存更大。如果虚拟机的内存是1G,最多只能开启3000+个线程数。至少为虚拟机分配1.5G以上的内存。 修改如下配置 ``` [root@centos elasticsearch-7.12.0]# vi /etc/security/limits.conf elasticsearch soft nofile 65536 elasticsearch hard nofile 65536 elasticsearch soft nproc 4096 elasticsearch hard nproc 4096 ``` #### 启动ElasticSearch ``` [root@centos elasticsearch-7.12.0]# su elasticsearch [elasticsearch@centos elasticsearch-7.12.0]$ ./bin/elasticsearch -d [2021-04-05T22:03:38,332][INFO ][o.e.n.Node ] [centos] version[7.12.0], pid[13197], build[default/tar/78722783c38caa25a70982b5b042074cde5d3b3a/2021-03-18T06:17:15.410153305Z], OS[Linux/3.10.0-862.el7.x86_64/amd64], JVM[AdoptOpenJDK/OpenJDK 64-Bit Server VM/15.0.1/15.0.1+9] [2021-04-05T22:03:38,348][INFO ][o.e.n.Node ] [centos] JVM home [/opt/elasticsearch-7.12.0/jdk], using bundled JDK [true] [2021-04-05T22:03:38,348][INFO ][o.e.n.Node ] [centos] JVM arguments [-Xshare:auto, -Des.networkaddress.cache.ttl=60, -Des.networkaddress.cache.negative.ttl=10, -XX:+AlwaysPreTouch, -Xss1m, -Djava.awt.headless=true, -Dfile.encoding=UTF-8, -Djna.nosys=true, -XX:-OmitStackTraceInFastThrow, -XX:+ShowCodeDetailsInExceptionMessages, -Dio.netty.noUnsafe=true, -Dio.netty.noKeySetOptimization=true, -Dio.netty.recycler.maxCapacityPerThread=0, -Dio.netty.allocator.numDirectArenas=0, -Dlog4j.shutdownHookEnabled=false, -Dlog4j2.disable.jmx=true, -Djava.locale.providers=SPI,COMPAT, --add-opens=java.base/java.io=ALL-UNNAMED, -XX:+UseG1GC, -Djava.io.tmpdir=/tmp/elasticsearch-17264135248464897093, -XX:+HeapDumpOnOutOfMemoryError, -XX:HeapDumpPath=data, -XX:ErrorFile=logs/hs_err_pid%p.log, -Xlog:gc*,gc+age=trace,safepoint:file=logs/gc.log:utctime,pid,tags:filecount=32,filesize=64m, -Xms1894m, -Xmx1894m, -XX:MaxDirectMemorySize=993001472, -XX:G1HeapRegionSize=4m, -XX:InitiatingHeapOccupancyPercent=30, -XX:G1ReservePercent=15, -Des.path.home=/opt/elasticsearch-7.12.0, -Des.path.conf=/opt/elasticsearch-7.12.0/config, -Des.distribution.flavor=default, -Des.distribution.type=tar, -Des.bundled_jdk=true] ``` #### 查看安装是否成功 ``` [root@centos ~]# netstat -ntlp | grep 9200 tcp6 0 0 127.0.0.1:9200 :::* LISTEN 13549/java tcp6 0 0 ::1:9200 :::* LISTEN 13549/java [root@centos ~]# curl 127.0.0.1:9200 { "name" : "centos", "cluster_name" : "elasticsearch", "cluster_uuid" : "ihttW8b2TfWSkwf_YgPH2Q", "version" : { "number" : "7.12.0", "build_flavor" : "default", "build_type" : "tar", "build_hash" : "78722783c38caa25a70982b5b042074cde5d3b3a", "build_date" : "2021-03-18T06:17:15.410153305Z", "build_snapshot" : false, "lucene_version" : "8.8.0", "minimum_wire_compatibility_version" : "6.8.0", "minimum_index_compatibility_version" : "6.0.0-beta1" }, "tagline" : "You Know, for Search" } ``` #### 安装Kibana Kibana是界面化的查询数据的工具,下载时尽量下载与ElasicSearch一致的版本。 * 下载Kibana 从https://www.elastic.co/cn/downloads/kibana下载Kibana * 解压 ``` [root@centos opt]# tar -vxzf kibana-7.12.0-linux-x86_64.tar.gz ``` * 使用elasticsearch用户权限 ``` [root@centos opt]# chown -R elasticsearch /opt/kibana-7.12.0-linux-x86_64 #配置Kibana的远程访问 [root@centos opt]# vi /opt/kibana-7.12.0-linux-x86_64/config/kibana.yml server.host: 0.0.0.0 ``` * 启动 需要切换至elasticsearch用户。 ``` [root@centos opt]# su elasticsearch [elasticsearch@centos opt]$ cd /opt/kibana-7.12.0-linux-x86_64/ [elasticsearch@centos kibana-7.12.0-linux-x86_64]$ ./bin/kibana log [22:30:22.185] [info][plugins-service] Plugin "osquery" is disabled. log [22:30:22.283] [warning][config][deprecation] Config key [monitoring.cluster_alerts.email_notifications.email_address] will be required for email notifications to work in 8.0." log [22:30:22.482] [info][plugins-system] Setting up [100] plugins: [taskManager,licensing,globalSearch,globalSearchProviders,banners,code,usageCollection,xpackLegacy,telemetryCollectionManager,telemetry,telemetryCollectionXpack,kibanaUsageCollection,securityOss,share,newsfeed,mapsLegacy,kibanaLegacy,translations,legacyExport,embeddable,uiActionsEnhanced,expressions,charts,esUiShared,bfetch,data,home,observability,console,consoleExtensions,apmOss,searchprofiler,painlessLab,grokdebugger,management,indexPatternManagement,advancedSettings,fileUpload,savedObjects,visualizations,visTypeVislib,visTypeVega,visTypeTimelion,features,licenseManagement,watcher,canvas,visTypeTagcloud,visTypeTable,visTypeMetric,visTypeMarkdown,tileMap,regionMap,visTypeXy,graph,timelion,dashboard,dashboardEnhanced,visualize,visTypeTimeseries,inputControlVis,discover,discoverEnhanced,savedObjectsManagement,spaces,security,savedObjectsTagging,maps,lens,reporting,lists,encryptedSavedObjects,dashboardMode,dataEnhanced,cloud,upgradeAssistant,snapshotRestore,fleet,indexManagement,rollup,remoteClusters,crossClusterReplication,indexLifecycleManagement,enterpriseSearch,beatsManagement,transform,ingestPipelines,eventLog,actions,alerts,triggersActionsUi,stackAlerts,ml,securitySolution,case,infra,monitoring,logstash,apm,uptime] log [22:30:22.483] [info][plugins][taskManager] TaskManager is identified by the Kibana UUID: xxxxxx ... ``` 如果是后台启动: ``` [elasticsearch@centos kibana-7.12.0-linux-x86_64]$ nohup ./bin/kibana & ``` * 界面访问 ![lmua6wrf.png](http://flt-pan.58heshihu.com/blog/typecho/lmua6wrf.png) 可以导入simple data ![lmua7a0j.png](http://flt-pan.58heshihu.com/blog/typecho/lmua7a0j.png) 查看数据 ![lmua7m4b.png](http://flt-pan.58heshihu.com/blog/typecho/lmua7m4b.png) 其实以上的安装各个版本大同小异,都相差不大,所以,没有按目前的新版本来进行安装演示。官方网站也有具体的安装操作步骤,也可以参考。 ### 配置密码访问 使用基本许可证时,默认情况下禁用Elasticsearch安全功能。由于我测试环境是放在公网上的,所以需要设置下密码访问。相关文档可以参考:https://www.elastic.co/guide/en/elasticsearch/reference/7.12/security-minimal-setup.html * 停止kibana和elasticsearch服务 * 将`xpack.security.enabled`设置添加到ES\_PATH\_CONF/elasticsearch.yml文件并将值设置为true * 启动elasticsearch (`./bin/elasticsearch -d`) * 执行如下密码设置器,`./bin/elasticsearch-setup-passwords interactive`来设置各个组件的密码 * 将elasticsearch.username设置添加到KIB\_PATH\_CONF/kibana.yml 文件并将值设置给elastic用户: `elasticsearch.username: "elastic"` * 创建kibana keystore, `./bin/kibana-keystore create` * 在kibana keystore 中添加密码 `./bin/kibana-keystore add elasticsearch.password` * 重启kibana 服务即可 `nohup ./bin/kibana &` 然后就可以使用密码登录了: ![lmua8b9e.png](http://flt-pan.58heshihu.com/blog/typecho/lmua8b9e.png) ### 查询和聚合的基础使用 安装完ElasticSearch 和 Kibana后,为了快速上手,我们通过官网GitHub提供的一个数据进行入门学习,主要包括查询数据和聚合数据。 #### 从索引文档开始 * 索引一个文档 ``` PUT /customer/_doc/1 { "name": "John Doe" } ``` 为了方便测试,我们使用kibana的dev tool来进行学习测试: ![lmua8w4c.png](http://flt-pan.58heshihu.com/blog/typecho/lmua8w4c.png) 查询刚才插入的文档 ![lmua934y.png](http://flt-pan.58heshihu.com/blog/typecho/lmua934y.png) #### 学习准备:批量索引文档 ES 还提供了批量操作,比如这里我们可以使用批量操作来插入一些数据,供我们在后面学习使用。使用批量来批处理文档操作比单独提交请求要快得多,因为它减少了网络往返。 ###### 下载测试数据 数据是index为bank,accounts.json 下载地址:https://github.com/elastic/elasticsearch/blob/v6.8.18/docs/src/test/resources/accounts.json(如果你无法下载,也可以clone ES的官方仓库:https://github.com/elastic/elasticsearch,选择本文中使用的版本分支,然后进入/docs/src/test/resources/accounts.json目录获取)。 数据的格式如下: ``` { "account_number": 0, "balance": 16623, "firstname": "Bradshaw", "lastname": "Mckenzie", "age": 29, "gender": "F", "address": "244 Columbus Place", "employer": "Euron", "email": "bradshawmckenzie@euron.com", "city": "Hobucken", "state": "CO" } ``` ###### 批量插入数据 将accounts.json拷贝至指定目录,我这里放在`/opt/`下面,然后执行: ``` curl -H "Content-Type: application/json" -XPOST "localhost:9200/bank/_bulk?pretty&refresh" --data-binary "@/opt/accounts.json" ``` ###### 查看状态 ``` [elasticsearch@centos root]$ curl "localhost:9200/_cat/indices?v=true" | grep bank % Total % Received % Xferd Average Speed Time Time Time Current Dload Upload Total Spent Left Speed 100 1524 100 1524 0 0 119k 0 --:--:-- --:--:-- --:--:-- 124k yellow open bank yq3eSlAWRMO2Td0Sl769rQ 1 1 1000 0 379.2kb 379.2kb ``` #### 查询数据 我们通过kibana来进行查询测试。 ###### 查询所有 `match_all`表示查询所有的数据,`sort`即按照什么字段排序 ``` GET /bank/_search { "query": { "match_all": {} }, "sort": [ { "account_number": "asc" } ] } ``` 结果 ![lmua9ma8.png](http://flt-pan.58heshihu.com/blog/typecho/lmua9ma8.png) 相关字段解释 * `took` – Elasticsearch运行查询所花费的时间(以毫秒为单位) * `timed_out` –搜索请求是否超时 * `_shards` \- 搜索了多少个碎片,以及成功,失败或跳过了多少个碎片的细目分类。 * `max_score` – 找到的最相关文档的分数 * `hits.total.value` \- 找到了多少个匹配的文档 * `hits.sort` \- 文档的排序位置(不按相关性得分排序时) * `hits._score` \- 文档的相关性得分(使用match_all时不适用) ###### 分页查询(from+size) 本质上就是from和size两个字段 ``` GET /bank/_search { "query": { "match_all": {} }, "sort": [ { "account_number": "asc" } ], "from": 10, "size": 10 } ``` 结果 ![lmua9xuy.png](http://flt-pan.58heshihu.com/blog/typecho/lmua9xuy.png) ###### 指定字段查询:match 如果要在字段中搜索特定字词,可以使用`match`; 如下语句将查询address 字段中包含 mill 或者 lane的数据。 ``` GET /bank/_search { "query": { "match": { "address": "mill lane" } } } ``` 结果 ![lmuaad9k.png](http://flt-pan.58heshihu.com/blog/typecho/lmuaad9k.png) (由于ES底层是按照分词索引的,所以上述查询结果是address 字段中包含 mill 或者 lane的数据)。 ###### 查询段落匹配:match_phrase 如果我们希望查询的条件是 address字段中包含 "mill lane",则可以使用`match_phrase`。 ``` GET /bank/_search { "query": { "match_phrase": { "address": "mill lane" } } } ``` 结果 ![lmuaamj5.png](http://flt-pan.58heshihu.com/blog/typecho/lmuaamj5.png) ###### 多条件查询: bool 如果要构造更复杂的查询,可以使用`bool`查询来组合多个查询条件。例如,以下请求在bank索引中搜索40岁客户的帐户,但不包括居住在爱达荷州(ID)的任何人。 ``` GET /bank/_search { "query": { "bool": { "must": [ { "match": { "age": "40" } } ], "must_not": [ { "match": { "state": "ID" } } ] } } } ``` 结果 ![lmuaayoo.png](http://flt-pan.58heshihu.com/blog/typecho/lmuaayoo.png) `must`, `should`, `must_not` 和 `filter` 都是`bool`查询的子句。那么`filter`和上述`query`子句有啥区别呢? ###### 查询条件:query or filter 先看下如下查询, 在`bool`查询的子句中同时具备query/must 和 filter。 ``` GET /bank/_search { "query": { "bool": { "must": [ { "match": { "state": "ND" } } ], "filter": [ { "term": { "age": "40" } }, { "range": { "balance": { "gte": 20000, "lte": 30000 } } } ] } } } ``` 结果 ![lmuabapq.png](http://flt-pan.58heshihu.com/blog/typecho/lmuabapq.png) 两者都可以写查询条件,而且语法也类似。区别在于,query 上下文的条件是用来给文档打分的,匹配越好 `_score` 越高;filter 的条件只产生两种结果:符合与不符合,后者被过滤掉。 所以,我们进一步看只包含filter的查询。 ``` GET /bank/_search { "query": { "bool": { "filter": [ { "term": { "age": "40" } }, { "range": { "balance": { "gte": 20000, "lte": 30000 } } } ] } } } ``` 结果,显然无_score ![lmuablhl.png](http://flt-pan.58heshihu.com/blog/typecho/lmuablhl.png) #### 聚合查询:Aggregation 我们知道SQL中有group by,在ES中它叫Aggregation,即聚合运算。 ###### 简单聚合 比如我们希望计算出account每个州的统计数量, 使用`aggs`关键字对`state`字段聚合,被聚合的字段无需对分词统计,所以使用`state.keyword`对整个字段统计。 ``` GET /bank/_search { "size": 0, "aggs": { "group_by_state": { "terms": { "field": "state.keyword" } } } } ``` 结果 ![lmuabujw.png](http://flt-pan.58heshihu.com/blog/typecho/lmuabujw.png) 因为无需返回条件的具体数据, 所以设置size=0,返回hits为空。 `doc_count`表示bucket中每个州的数据条数。 ###### 嵌套聚合 ES还可以处理个聚合条件的嵌套。比如承接上个例子, 计算每个州的平均结余。涉及到的就是在对state分组的基础上,嵌套计算avg(balance): ``` GET /bank/_search { "size": 0, "aggs": { "group_by_state": { "terms": { "field": "state.keyword" }, "aggs": { "average_balance": { "avg": { "field": "balance" } } } } } } ``` 结果 ![lmuac3y2.png](http://flt-pan.58heshihu.com/blog/typecho/lmuac3y2.png) ###### 对聚合结果排序 可以通过在aggs中对嵌套聚合的结果进行排序。比如承接上个例子, 对嵌套计算出的avg(balance),这里是average_balance,进行排序。 ``` GET /bank/_search { "size": 0, "aggs": { "group_by_state": { "terms": { "field": "state.keyword", "order": { "average_balance": "desc" } }, "aggs": { "average_balance": { "avg": { "field": "balance" } } } } } } ``` 结果 ![lmuaccg1.png](http://flt-pan.58heshihu.com/blog/typecho/lmuaccg1.png) > 来源: > https://www.pdai.tech/md/db/nosql-es/elasticsearch-x-usage.html ------------- ## (四):ElasticSearch 索引管理 ### 索引管理的引入 我们在前文中增加文档时,如下的语句会动态创建一个customer的index: ``` PUT /customer/_doc/1 { "name": "John Doe" } ``` 而这个index实际上已经自动创建了它里面的字段(name)的类型。我们不妨看下它自动创建的mapping: ``` { "mappings": { "_doc": { "properties": { "name": { "type": "text", "fields": { "keyword": { "type": "keyword", "ignore_above": 256 } } } } } } } ``` 那么如果我们需要对这个建立索引的过程做更多的控制:比如想要确保这个索引有数量适中的主分片,并且在我们索引任何数据之前,分析器和映射已经被建立好。那么就会引入两点:第一个禁止自动创建索引,第二个是手动创建索引。 * 禁止自动创建索引 可以通过在 config/elasticsearch.yml 的每个节点下添加下面的配置: ``` action.auto_create_index: false ``` 手动创建索引就是接下来文章的内容。更多关于 ElasticSearch 数据库的学习文章,请参阅:[NoSQL 数据库之 ElasticSearch ](https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MzI0MDQ4MTM5NQ==&action=getalbum&album_id=2931171682919415809#wechat_redirect),本系列持续更新中。 ### 索引的格式 在请求体里面传入设置或类型映射,如下所示: ``` PUT /my_index { "settings": { ... any settings ... }, "mappings": { "properties": { ... any properties ... } } } ``` * settings: 用来设置分片,副本等配置信息 * mappings: 字段映射,类型等 * properties: 由于type在后续版本中会被Deprecated, 所以无需被type嵌套 ### 索引管理操作 我们通过kibana的devtool来学习索引的管理操作。 #### 创建索引 我们创建一个user 索引`test-index-users`,其中包含三个属性:name,age, remarks; 存储在一个分片一个副本上。 ``` PUT /test-index-users { "settings": { "number_of_shards": 1, "number_of_replicas": 1 }, "mappings": { "properties": { "name": { "type": "text", "fields": { "keyword": { "type": "keyword", "ignore_above": 256 } } }, "age": { "type": "long" }, "remarks": { "type": "text" } } } } ``` 执行结果 ![lmuaf8g3.png](http://flt-pan.58heshihu.com/blog/typecho/lmuaf8g3.png) * 插入测试数据 ![lmuafftv.png](http://flt-pan.58heshihu.com/blog/typecho/lmuafftv.png) 查看数据 ![lmuaflxh.png](http://flt-pan.58heshihu.com/blog/typecho/lmuaflxh.png) * 我们再测试下不匹配的数据类型(age): ``` POST /test-index-users/_doc { "name": "test user", "age": "error_age", "remarks": "hello eeee" } ``` 你可以看到无法类型不匹配的错误: ![lmuafuh9.png](http://flt-pan.58heshihu.com/blog/typecho/lmuafuh9.png) #### 修改索引 查看刚才的索引,`curl 'localhost:9200/_cat/indices?v' | grep users` ``` yellow open test-index-users LSaIB57XSC6uVtGQHoPYxQ 1 1 1 0 4.4kb 4.4kb ``` 我们注意到刚创建的索引的状态是yellow的,因为我测试的环境是单点环境,无法创建副本,但是在上述`number_of_replicas`配置中设置了副本数是1;所以在这个时候我们需要修改索引的配置。 修改副本数量为0: ``` PUT /test-index-users/_settings { "settings": { "number_of_replicas": 0 } } ``` ![lmuag2x4.png](http://flt-pan.58heshihu.com/blog/typecho/lmuag2x4.png) 再次查看状态: ``` green open test-index-users LSaIB57XSC6uVtGQHoPYxQ 1 1 1 0 4.4kb 4.4kb ``` #### 打开/关闭索引 * 关闭索引 一旦索引被关闭,那么这个索引只能显示元数据信息,不能够进行读写操作。 ![lmuagh5c.png](http://flt-pan.58heshihu.com/blog/typecho/lmuagh5c.png) 当关闭以后,再插入数据时: ![lmuago6y.png](http://flt-pan.58heshihu.com/blog/typecho/lmuago6y.png) * 打开索引 ![lmuah5a0.png](http://flt-pan.58heshihu.com/blog/typecho/lmuah5a0.png) 打开后又可以重新写数据了 ![lmuahcsb.png](http://flt-pan.58heshihu.com/blog/typecho/lmuahcsb.png) #### 删除索引 最后我们将创建的test-index-users删除。 ``` DELETE /test-index-users ``` ![lmuahi3n.png](http://flt-pan.58heshihu.com/blog/typecho/lmuahi3n.png) #### 查看索引 由于test-index-users被删除,所以我们看下之前bank的索引的信息。 * mapping ``` GET /bank/_mapping ``` ![lmuahqz5.png](http://flt-pan.58heshihu.com/blog/typecho/lmuahqz5.png) * settings ``` GET /bank/_settings ``` ![lmuai0kw.png](http://flt-pan.58heshihu.com/blog/typecho/lmuai0kw.png) ### Kibana 管理索引 在Kibana如下路径,我们可以查看和管理索引 ![lmuai9ia.png](http://flt-pan.58heshihu.com/blog/typecho/lmuai9ia.png) 前文介绍了索引的一些操作,特别是手动创建索引,但是批量和脚本化必然需要提供一种模板方式快速构建和管理索引,这就是本文要介绍的索引模板(Index Template),它是一种告诉Elasticsearch在创建索引时如何配置索引的方法。为了更好的复用性,在7.8中还引入了组件模板。 ### 索引模板 索引模板是一种告诉Elasticsearch在创建索引时如何配置索引的方法。 * 使用方式 在创建索引之前可以先配置模板,这样在创建索引(手动创建索引或通过对文档建立索引)时,模板设置将用作创建索引的基础。 #### 模板类型 模板有两种类型:索引模板和组件模板。 * 组件模板是可重用的构建块,用于配置映射,设置和别名;它们不会直接应用于一组索引。 * 索引模板可以包含组件模板的集合,也可以直接指定设置,映射和别名。 #### 索引模板中的优先级 * 可组合模板优先于旧模板。如果没有可组合模板匹配给定索引,则旧版模板可能仍匹配并被应用。 * 如果使用显式设置创建索引并且该索引也与索引模板匹配,则创建索引请求中的设置将优先于索引模板及其组件模板中指定的设置。 * 如果新数据流或索引与多个索引模板匹配,则使用优先级最高的索引模板。 #### 内置索引模板 Elasticsearch具有内置索引模板,每个索引模板的优先级为100,适用于以下索引模式: * `logs-*-*` * `metrics-*-*` * `synthetics-*-*` 所以在涉及内建索引模板时,要避免索引模式冲突。更多可以参考https://www.elastic.co/guide/en/elasticsearch/reference/current/index-templates.html。 #### 案例 * 首先创建两个索引组件模板: ``` PUT _component_template/component_template1 { "template": { "mappings": { "properties": { "@timestamp": { "type": "date" } } } } } PUT _component_template/runtime_component_template { "template": { "mappings": { "runtime": { "day_of_week": { "type": "keyword", "script": { "source": "emit(doc['@timestamp'].value.dayOfWeekEnum.getDisplayName(TextStyle.FULL, Locale.ROOT))" } } } } } } ``` 执行结果如下 ![lmuaivmh.png](http://flt-pan.58heshihu.com/blog/typecho/lmuaivmh.png) * 创建使用组件模板的索引模板 ``` PUT _index_template/template_1 { "index_patterns": ["bar*"], "template": { "settings": { "number_of_shards": 1 }, "mappings": { "_source": { "enabled": true }, "properties": { "host_name": { "type": "keyword" }, "created_at": { "type": "date", "format": "EEE MMM dd HH:mm:ss Z yyyy" } } }, "aliases": { "mydata": { } } }, "priority": 500, "composed_of": ["component_template1", "runtime_component_template"], "version": 3, "_meta": { "description": "my custom" } } ``` 执行结果如下 ![lmuaj31o.png](http://flt-pan.58heshihu.com/blog/typecho/lmuaj31o.png) * 创建一个匹配`bar*`的索引`bar-test` ``` PUT /bar-test ``` 然后获取mapping ``` GET /bar-test/_mapping ``` 执行结果如下 ![lmuajb7h.png](http://flt-pan.58heshihu.com/blog/typecho/lmuajb7h.png) #### 模拟多组件模板 由于模板不仅可以由多个组件模板组成,还可以由索引模板自身组成;那么最终的索引设置将是什么呢?ElasticSearch设计者考虑到这个,提供了API进行模拟组合后的模板的配置。 ###### 模拟某个索引结果 比如上面的template_1, 我们不用创建`bar*`的索引(这里模拟bar-pdai-test),也可以模拟计算出索引的配置: ``` POST /_index_template/_simulate_index/bar-pdai-test ``` 执行结果如下 ![lmuajjx0.png](http://flt-pan.58heshihu.com/blog/typecho/lmuajjx0.png) ###### 模拟组件模板结果 当然,由于template\_1模板是由两个组件模板组合的,我们也可以模拟出template\_1被组合后的索引配置: ``` POST /_index_template/_simulate/template_1 ``` 执行结果如下: ``` { "template" : { "settings" : { "index" : { "number_of_shards" : "1" } }, "mappings" : { "runtime" : { "day_of_week" : { "type" : "keyword", "script" : { "source" : "emit(doc['@timestamp'].value.dayOfWeekEnum.getDisplayName(TextStyle.FULL, Locale.ROOT))", "lang" : "painless" } } }, "properties" : { "@timestamp" : { "type" : "date" }, "created_at" : { "type" : "date", "format" : "EEE MMM dd HH:mm:ss Z yyyy" }, "host_name" : { "type" : "keyword" } } }, "aliases" : { "mydata" : { } } }, "overlapping" : [ ] } ``` ###### 模拟组件模板和自身模板结合后的结果 * 新建两个模板 ``` PUT /_component_template/ct1 { "template": { "settings": { "index.number_of_shards": 2 } } } PUT /_component_template/ct2 { "template": { "settings": { "index.number_of_replicas": 0 }, "mappings": { "properties": { "@timestamp": { "type": "date" } } } } } ``` 模拟在两个组件模板的基础上,添加自身模板的配置 ``` POST /_index_template/_simulate { "index_patterns": ["my*"], "template": { "settings" : { "index.number_of_shards" : 3 } }, "composed_of": ["ct1", "ct2"] } ``` 执行的结果如下 ``` { "template" : { "settings" : { "index" : { "number_of_shards" : "3", "number_of_replicas" : "0" } }, "mappings" : { "properties" : { "@timestamp" : { "type" : "date" } } }, "aliases" : { } }, "overlapping" : [ ] } ``` ![lmuajsft.png](http://flt-pan.58heshihu.com/blog/typecho/lmuajsft.png) > 链接: > https://pdai.tech/md/db/nosql-es/elasticsearch-x-index-template.html ------------- ## (五):ElasticSearch DSL 查询原理与实践 ### DSL 查询之复合查询 在查询中会有多种条件组合的查询,在 ElasticSearch 中叫复合查询。它提供了`5种`复合查询方式:bool query(布尔查询)、boosting query(提高查询)、constant\_score(固定分数查询)、dis\_max(最佳匹配查询)、function_score(函数查询)。 #### 复合查询引入 在前文中,我们使用`bool`查询来组合多个查询条件。比如之前介绍的语句: ``` GET /bank/_search { "query": { "bool": { "must": [ { "match": { "age": "40" } } ], "must_not": [ { "match": { "state": "ID" } } ] } } } ``` 这种查询就是本文要介绍的复合查询,并且bool查询只是复合查询一种。 #### bool query(布尔查询) 通过布尔逻辑将较小的查询组合成较大的查询。 ###### 概念 Bool查询语法有以下特点 * 子查询可以任意顺序出现 * 可以嵌套多个查询,包括bool查询 * 如果bool查询中没有must条件,should中必须至少满足一条才会返回结果。 bool查询包含四种操作符,分别是must,should,must_not,filter。他们均是一种数组,数组里面是对应的判断条件。 * `must`: 必须匹配。贡献算分 * `must_not`:过滤子句,必须不能匹配,但不贡献算分 * `should`: 选择性匹配,至少满足一条。贡献算分 * `filter`: 过滤子句,必须匹配,但不贡献算分 ###### 一些例子 看下官方举例 * 例子1 ``` POST _search { "query": { "bool" : { "must" : { "term" : { "user.id" : "kimchy" } }, "filter": { "term" : { "tags" : "production" } }, "must_not" : { "range" : { "age" : { "gte" : 10, "lte" : 20 } } }, "should" : [ { "term" : { "tags" : "env1" } }, { "term" : { "tags" : "deployed" } } ], "minimum_should_match" : 1, "boost" : 1.0 } } } ``` 在filter元素下指定的查询对评分没有影响 , 评分返回为0。分数仅受已指定查询的影响。 * 例子2 ``` GET _search { "query": { "bool": { "filter": { "term": { "status": "active" } } } } } ``` 这个例子查询查询为所有文档分配0分,因为没有指定评分查询。 * 例子3 ``` GET _search { "query": { "bool": { "must": { "match_all": {} }, "filter": { "term": { "status": "active" } } } } } ``` 此bool查询具有match_all查询,该查询为所有文档指定1.0分。 * 例子4 ``` GET /_search { "query": { "bool": { "should": [ { "match": { "name.first": { "query": "shay", "_name": "first" } } }, { "match": { "name.last": { "query": "banon", "_name": "last" } } } ], "filter": { "terms": { "name.last": [ "banon", "kimchy" ], "_name": "test" } } } } } ``` 每个query条件都可以有一个`_name`属性,用来追踪搜索出的数据到底match了哪个条件。 #### boosting query(提高查询) 不同于bool查询,bool查询中只要一个子查询条件不匹配那么搜索的数据就不会出现。而boosting query则是降低显示的权重/优先级(即score)。 ###### 概念 比如搜索逻辑是 name = 'apple' and type ='fruit',对于只满足部分条件的数据,不是不显示,而是降低显示的优先级(即score)。 ###### 例子 首先创建数据 ``` POST /test-dsl-boosting/_bulk { "index": { "_id": 1 }} { "content":"Apple Mac" } { "index": { "_id": 2 }} { "content":"Apple Fruit" } { "index": { "_id": 3 }} { "content":"Apple employee like Apple Pie and Apple Juice" } ``` 对匹配`pie`的做降级显示处理 ``` GET /test-dsl-boosting/_search { "query": { "boosting": { "positive": { "term": { "content": "apple" } }, "negative": { "term": { "content": "pie" } }, "negative_boost": 0.5 } } } ``` 执行结果如下 ![lmuaw5fh.png](http://flt-pan.58heshihu.com/blog/typecho/lmuaw5fh.png) #### constant_score(固定分数查询) 查询某个条件时,固定的返回指定的score;显然当不需要计算score时,只需要filter条件即可,因为filter context忽略score。 ###### 例子 首先创建数据 ``` POST /test-dsl-constant/_bulk { "index": { "_id": 1 }} { "content":"Apple Mac" } { "index": { "_id": 2 }} { "content":"Apple Fruit" } ``` 查询apple ``` GET /test-dsl-constant/_search { "query": { "constant_score": { "filter": { "term": { "content": "apple" } }, "boost": 1.2 } } } ``` 执行结果如下 ![lmuawctj.png](http://flt-pan.58heshihu.com/blog/typecho/lmuawctj.png) #### dis_max(最佳匹配查询) 分离最大化查询(Disjunction Max Query)指的是: 将任何与任一查询匹配的文档作为结果返回,但只将最佳匹配的评分作为查询的评分结果返回。更多关于 ElasticSearch 数据库的学习文章,请参阅:[NoSQL 数据库之 ElasticSearch ](https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MzI0MDQ4MTM5NQ==&action=getalbum&album_id=2931171682919415809#wechat_redirect),本系列持续更新中。 ###### 例子 假设有个网站允许用户搜索博客的内容,以下面两篇博客内容文档为例: ``` POST /test-dsl-dis-max/_bulk { "index": { "_id": 1 }} {"title": "Quick brown rabbits","body": "Brown rabbits are commonly seen."} { "index": { "_id": 2 }} {"title": "Keeping pets healthy","body": "My quick brown fox eats rabbits on a regular basis."} ``` 用户输入词组 “Brown fox” 然后点击搜索按钮。事先,我们并不知道用户的搜索项是会在 title 还是在 body 字段中被找到,但是,用户很有可能是想搜索相关的词组。用肉眼判断,文档 2 的匹配度更高,因为它同时包括要查找的两个词: 现在运行以下 bool 查询: ``` GET /test-dsl-dis-max/_search { "query": { "bool": { "should": [ { "match": { "title": "Brown fox" }}, { "match": { "body": "Brown fox" }} ] } } } ``` ![lmuawsgh.png](http://flt-pan.58heshihu.com/blog/typecho/lmuawsgh.png) 为了理解导致这样的原因,需要看下如何计算评分的。 * should 条件的计算分数 ``` GET /test-dsl-dis-max/_search { "query": { "bool": { "should": [ { "match": { "title": "Brown fox" }}, { "match": { "body": "Brown fox" }} ] } } } ``` 要计算上述分数,首先要计算match的分数 * 1.第一个match 中 `brown的分数` ``` doc 1 分数 = 0.6931471 ``` ![lmuawzui.png](http://flt-pan.58heshihu.com/blog/typecho/lmuawzui.png) * 2.title中没有fox,所以第一个match 中 `brown fox 的分数 = brown分数 + 0 = 0.6931471` ``` doc 1 分数 = 0.6931471 + 0 = 0.6931471 ``` ![lmuax4w3.png](http://flt-pan.58heshihu.com/blog/typecho/lmuax4w3.png) * 3.第二个 match 中 `brown分数` ``` doc 1 分数 = 0.21110919 doc 2 分数 = 0.160443 ``` ![lmuax8u1.png](http://flt-pan.58heshihu.com/blog/typecho/lmuax8u1.png) * 4.第二个 match 中 `fox分数` ``` doc 1 分数 = 0 doc 2 分数 = 0.60996956 ``` ![lmuaxe3m.png](http://flt-pan.58heshihu.com/blog/typecho/lmuaxe3m.png) * 5.所以第二个 match 中 `brown fox分数 = brown分数 + fox分数` ``` doc 1 分数 = 0.21110919 + 0 = 0.21110919 doc 2 分数 = 0.160443 + 0.60996956 = 0.77041256 ``` ![lmuaxkkm.png](http://flt-pan.58heshihu.com/blog/typecho/lmuaxkkm.png) * 6.所以整个语句分数, `should分数 = 第一个match + 第二个match分数` ``` doc 1 分数 = 0.6931471 + 0.21110919 = 0.90425634 doc 2 分数 = 0 + 0.77041256 = 0.77041256 ``` ![lmuaxppp.png](http://flt-pan.58heshihu.com/blog/typecho/lmuaxppp.png) * 引入了dis_max 不使用 bool 查询,可以使用 dis_max 即分离 最大化查询(Disjunction Max Query) 。分离(Disjunction)的意思是 或(or) ,这与可以把结合(conjunction)理解成 与(and) 相对应。分离最大化查询(Disjunction Max Query)指的是: 将任何与任一查询匹配的文档作为结果返回,但只将最佳匹配的评分作为查询的评分结果返回 : ``` GET /test-dsl-dis-max/_search { "query": { "dis_max": { "queries": [ { "match": { "title": "Brown fox" }}, { "match": { "body": "Brown fox" }} ], "tie_breaker": 0 } } } ``` ![lmuay11e.png](http://flt-pan.58heshihu.com/blog/typecho/lmuay11e.png) 0.77041256怎么来的呢? 下文给你解释它如何计算出来的。 * dis_max 条件的计算分数 分数 = 第一个匹配条件分数`+ tie_breaker *`第二个匹配的条件的分数 ``` GET /test-dsl-dis-max/_search { "query": { "dis_max": { "queries": [ { "match": { "title": "Brown fox" }}, { "match": { "body": "Brown fox" }} ], "tie_breaker": 0 } } } ``` > doc 1 分数 = 0.6931471 + 0.21110919 \* 0 = 0.6931471 > doc 2 分数 = 0.77041256 = 0.77041256 ![lmuaygb2.png](http://flt-pan.58heshihu.com/blog/typecho/lmuaygb2.png) 这样你就能理解通过dis_max将doc 2 置前了, 当然这里如果缺省`tie_breaker`字段的话默认就是0,你还可以设置它的比例(在0到1之间)来控制排名。(显然值为1时和should查询是一致的)。 #### function_score(函数查询) 简而言之就是用自定义function的方式来计算_score。可以ES有哪些自定义function呢? * `script_score` 使用自定义的脚本来完全控制分值计算逻辑。如果你需要以上预定义函数之外的功能,可以根据需要通过脚本进行实现。 * `weight` 对每份文档适用一个简单的提升,且该提升不会被归约:当weight为2时,结果为2 * _score。 * `random_score` 使用一致性随机分值计算来对每个用户采用不同的结果排序方式,对相同用户仍然使用相同的排序方式。 * `field_value_factor` 使用文档中某个字段的值来改变_score,比如将受欢迎程度或者投票数量考虑在内。 * `衰减函数(Decay Function)` - `linear`,`exp`,`gauss` ###### 例子 以最简单的random_score 为例。 ``` GET /_search { "query": { "function_score": { "query": { "match_all": {} }, "boost": "5", "random_score": {}, "boost_mode": "multiply" } } } ``` 进一步的,它还可以使用上述function的组合(functions)。 ``` GET /_search { "query": { "function_score": { "query": { "match_all": {} }, "boost": "5", "functions": [ { "filter": { "match": { "test": "bar" } }, "random_score": {}, "weight": 23 }, { "filter": { "match": { "test": "cat" } }, "weight": 42 } ], "max_boost": 42, "score_mode": "max", "boost_mode": "multiply", "min_score": 42 } } } ``` script_score 可以使用如下方式。 ``` GET /_search { "query": { "function_score": { "query": { "match": { "message": "elasticsearch" } }, "script_score": { "script": { "source": "Math.log(2 + doc['my-int'].value)" } } } } } ``` 更多相关内容,可以参考官方文档,PS: 形成体系化认知以后,具体用的时候查询下即可。 ### DSL 查询之全文搜索 DSL查询极为常用的是对文本进行搜索,我们叫全文搜索,本文主要对全文搜索进行详解。 #### 谈谈如何从官网学习 很多读者在看官方文档学习时存在一个误区,以DSL中full text查询为例,其实内容是非常多的, 没有取舍/没重点去阅读,要么需要花很多时间,要么头脑一片浆糊。所以这里重点谈谈我的理解。 * 第一点:全局观,即我们现在学习内容在整个体系的哪个位置? 如下图,可以很方便的帮助你构筑这种体系: ![lmub3lxd.png](http://flt-pan.58heshihu.com/blog/typecho/lmub3lxd.png) * 第二点: 分类别,从上层理解,而不是本身 比如Full text Query中,我们只需要把如下的那么多点分为3大类,你的体系能力会大大提升。 ![lmub3tn7.png](http://flt-pan.58heshihu.com/blog/typecho/lmub3tn7.png) * 第三点: 知识点还是API? API类型的是可以查询的,只需要知道大致有哪些功能就可以了。 ![lmub439e.png](http://flt-pan.58heshihu.com/blog/typecho/lmub439e.png) #### Match类型 第一类:match 类型 ###### match 查询的步骤 在前文中我们已经介绍了match查询。 * 准备一些数据 这里我们准备一些数据,通过实例看match 查询的步骤。 ``` PUT /test-dsl-match { "settings": { "number_of_shards": 1 }} POST /test-dsl-match/_bulk { "index": { "_id": 1 }} { "title": "The quick brown fox" } { "index": { "_id": 2 }} { "title": "The quick brown fox jumps over the lazy dog" } { "index": { "_id": 3 }} { "title": "The quick brown fox jumps over the quick dog" } { "index": { "_id": 4 }} { "title": "Brown fox brown dog" } ``` * 查询数据 ``` GET /test-dsl-match/_search { "query": { "match": { "title": "QUICK!" } } } ``` Elasticsearch 执行上面这个 match 查询的步骤是: 1.检查字段类型 。 标题 title 字段是一个 string 类型( analyzed )已分析的全文字段,这意味着查询字符串本身也应该被分析。 2.分析查询字符串 。 将查询的字符串 QUICK! 传入标准分析器中,输出的结果是单个项 quick 。因为只有一个单词项,所以 match 查询执行的是单个底层 term 查询。 3.查找匹配文档 。 用 term 查询在倒排索引中查找 quick 然后获取一组包含该项的文档,本例的结果是文档:1、2 和 3 。 4.为每个文档评分 。 用 term 查询计算每个文档相关度评分 `_score `,这是种将词频(term frequency,即词 quick 在相关文档的 title 字段中出现的频率)和反向文档频率(inverse document frequency,即词 quick 在所有文档的 title 字段中出现的频率),以及字段的长度(即字段越短相关度越高)相结合的计算方式。 * 验证结果 ![lmub4h1n.png](http://flt-pan.58heshihu.com/blog/typecho/lmub4h1n.png) ###### match多个词深入 我们在上文中复合查询中已经使用了match多个词,比如“Quick pets”; 这里我们通过例子带你更深入理解match多个词。 * match多个词的本质 查询多个词"BROWN DOG!" ``` GET /test-dsl-match/_search { "query": { "match": { "title": "BROWN DOG" } } } ``` ![lmub4r4b.png](http://flt-pan.58heshihu.com/blog/typecho/lmub4r4b.png) 因为 match 查询必须查找两个词(`["brown","dog"]`),它在内部实际上先执行两次 term 查询,然后将两次查询的结果合并作为最终结果输出。为了做到这点,它将两个 term 查询包入一个 bool 查询中。 所以上述查询的结果,和如下语句查询结果是等同的。 ``` GET /test-dsl-match/_search { "query": { "bool": { "should": [ { "term": { "title": "brown" } }, { "term": { "title": "dog" } } ] } } } ``` ![lmub507p.png](http://flt-pan.58heshihu.com/blog/typecho/lmub507p.png) * match多个词的逻辑 上面等同于should(任意一个满足),是因为 match还有一个operator参数,默认是or, 所以对应的是should。 所以上述查询也等同于: ``` GET /test-dsl-match/_search { "query": { "match": { "title": { "query": "BROWN DOG", "operator": "or" } } } } ``` 那么我们如果是需要and操作呢,即同时满足呢? ``` GET /test-dsl-match/_search { "query": { "match": { "title": { "query": "BROWN DOG", "operator": "and" } } } } ``` 等同于 ``` GET /test-dsl-match/_search { "query": { "bool": { "must": [ { "term": { "title": "brown" } }, { "term": { "title": "dog" } } ] } } } ``` ![lmub6qza.png](http://flt-pan.58heshihu.com/blog/typecho/lmub6qza.png) ###### 控制match的匹配精度 如果用户给定 3 个查询词,想查找至少包含其中 2 个的文档,该如何处理?将 operator 操作符参数设置成 and 或者 or 都是不合适的。 match 查询支持 `minimum_should_match` 最小匹配参数,这让我们可以指定必须匹配的词项数用来表示一个文档是否相关。我们可以将其设置为某个具体数字,更常用的做法是将其设置为一个百分数,因为我们无法控制用户搜索时输入的单词数量: ``` GET /test-dsl-match/_search { "query": { "match": { "title": { "query": "quick brown dog", "minimum_should_match": "75%" } } } } ``` 当给定百分比的时候, `minimum_should_match` 会做合适的事情:在之前三词项的示例中,75% 会自动被截断成 66.6% ,即三个里面两个词。无论这个值设置成什么,至少包含一个词项的文档才会被认为是匹配的。 ![lmub76ru.png](http://flt-pan.58heshihu.com/blog/typecho/lmub76ru.png) 当然也等同于: ``` GET /test-dsl-match/_search { "query": { "bool": { "should": [ { "match": { "title": "quick" }}, { "match": { "title": "brown" }}, { "match": { "title": "dog" }} ], "minimum_should_match": 2 } } } ``` ![lmub7df2.png](http://flt-pan.58heshihu.com/blog/typecho/lmub7df2.png) ###### 其它match类型 * match_pharse match_phrase在前文中我们已经有了解,我们再看下另外一个例子。 ``` GET /test-dsl-match/_search { "query": { "match_phrase": { "title": { "query": "quick brown" } } } } ``` ![lmub7lv3.png](http://flt-pan.58heshihu.com/blog/typecho/lmub7lv3.png) 很多人对它仍然有误解的,比如如下例子: ``` GET /test-dsl-match/_search { "query": { "match_phrase": { "title": { "query": "quick brown f" } } } } ``` 这样的查询是查不出任何数据的,因为前文中我们知道了match本质上是对term组合,match_phrase本质是连续的term的查询,所以f并不是一个分词,不满足term查询,所以最终查不出任何内容了。 ![lmub7uhz.png](http://flt-pan.58heshihu.com/blog/typecho/lmub7uhz.png) * match\_pharse\_prefix 那有没有可以查询出`quick brown f`的方式呢?ELasticSearch在match_phrase基础上提供了一种可以查最后一个词项是前缀的方法,这样就可以查询`quick brown f`了 ``` GET /test-dsl-match/_search { "query": { "match_phrase_prefix": { "title": { "query": "quick brown f" } } } } ``` ![lmub9cpf.png](http://flt-pan.58heshihu.com/blog/typecho/lmub9cpf.png) (ps: prefix的意思不是整个text的开始匹配,而是最后一个词项满足term的prefix查询而已)。 * match\_bool\_prefix 除了`match_phrase_prefix`,ElasticSearch还提供了`match_bool_prefix`查询 ``` GET /test-dsl-match/_search { "query": { "match_bool_prefix": { "title": { "query": "quick brown f" } } } } ``` ![lmubakut.png](http://flt-pan.58heshihu.com/blog/typecho/lmubakut.png) 它们两种方式有啥区别呢?`match_bool_prefix`本质上可以转换为: ``` GET /test-dsl-match/_search { "query": { "bool" : { "should": [ { "term": { "title": "quick" }}, { "term": { "title": "brown" }}, { "prefix": { "title": "f"}} ] } } } ``` 所以这样你就能理解,`match_bool_prefix`查询中的quick,brown,f是无序的。 * multi_match 如果我们期望一次对多个字段查询,怎么办呢?ElasticSearch提供了multi_match查询的方式 ``` { "query": { "multi_match" : { "query": "Will Smith", "fields": [ "title", "*_name" ] } } } ``` `*`表示前缀匹配字段。 #### query string类型 第二类:query string 类型 ###### query_string 此查询使用语法根据运算符(例如AND或)来解析和拆分提供的查询字符串NOT。然后查询在返回匹配的文档之前独立分析每个拆分的文本。 可以使用该query_string查询创建一个复杂的搜索,其中包括通配符,跨多个字段的搜索等等。尽管用途广泛,但查询是严格的,如果查询字符串包含任何无效语法,则返回错误。 例如: ``` GET /test-dsl-match/_search { "query": { "query_string": { "query": "(lazy dog) OR (brown dog)", "default_field": "title" } } } ``` 这里查询结果,你需要理解本质上查询这四个分词(term)or的结果而已,所以doc 3和4也在其中。对构筑知识体系已经够了,但是它其实还有很多参数和用法。 ![lmudh222.png](http://flt-pan.58heshihu.com/blog/typecho/lmudh222.png) ###### query\_string\_simple 该查询使用一种简单的语法来解析提供的查询字符串并将其拆分为基于特殊运算符的术语。然后查询在返回匹配的文档之前独立分析每个术语。 尽管其语法比query_string查询更受限制 ,但simple\_query\_string 查询不会针对无效语法返回错误。而是,它将忽略查询字符串的任何无效部分。 举例: ``` GET /test-dsl-match/_search { "query": { "simple_query_string" : { "query": "\"over the\" + (lazy | quick) + dog", "fields": ["title"], "default_operator": "and" } } } ``` ![lmudhkun.png](http://flt-pan.58heshihu.com/blog/typecho/lmudhkun.png) #### Interval类型 第三类:interval类型 Intervals是时间间隔的意思,本质上将多个规则按照顺序匹配。比如: ``` GET /test-dsl-match/_search { "query": { "intervals" : { "title" : { "all_of" : { "ordered" : true, "intervals" : [ { "match" : { "query" : "quick", "max_gaps" : 0, "ordered" : true } }, { "any_of" : { "intervals" : [ { "match" : { "query" : "jump over" } }, { "match" : { "query" : "quick dog" } } ] } } ] } } } } } ``` ![lmudhyjr.png](http://flt-pan.58heshihu.com/blog/typecho/lmudhyjr.png) 因为interval之间是可以组合的,所以它可以表现的很复杂。 ### DSL 查询之 Term DSL查询另一种极为常用的是对词项进行搜索,官方文档中叫”term level“查询,本文主要对term level搜索进行详解。 #### Term查询引入 如前文所述,查询分基于文本查询和基于词项的查询:本文主要讲基于词项的查询。 ![lmudij4k.png](http://flt-pan.58heshihu.com/blog/typecho/lmudij4k.png) ![lmudjgf7.png](http://flt-pan.58heshihu.com/blog/typecho/lmudjgf7.png) #### Term查询 很多比较常用,也不难,就是需要结合实例理解。这里综合官方文档的内容,我设计一个测试场景的数据,以覆盖所有例子。 准备数据 ``` PUT /test-dsl-term-level { "mappings": { "properties": { "name": { "type": "keyword" }, "programming_languages": { "type": "keyword" }, "required_matches": { "type": "long" } } } } POST /test-dsl-term-level/_bulk { "index": { "_id": 1 }} {"name": "Jane Smith", "programming_languages": [ "c++", "java" ], "required_matches": 2} { "index": { "_id": 2 }} {"name": "Jason Response", "programming_languages": [ "java", "php" ], "required_matches": 2} { "index": { "_id": 3 }} {"name": "Dave Pdai", "programming_languages": [ "java", "c++", "php" ], "required_matches": 3, "remarks": "hello world"} ``` ###### 字段是否存在:exist 由于多种原因,文档字段的索引值可能不存在: * 源JSON中的字段是null或\[\] * 该字段已"index" : false在映射中设置 * 字段值的长度超出ignore_above了映射中的设置 * 字段值格式错误,并且ignore_malformed已在映射中定义 所以exist表示查找是否存在字段。 ![lmudjxas.png](http://flt-pan.58heshihu.com/blog/typecho/lmudjxas.png) ###### id查询:ids ids 即对id查找 ``` GET /test-dsl-term-level/_search { "query": { "ids": { "values": [3, 1] } } } ``` ![lmudk5yt.png](http://flt-pan.58heshihu.com/blog/typecho/lmudk5yt.png) ###### 前缀:prefix 通过前缀查找某个字段 ``` GET /test-dsl-term-level/_search { "query": { "prefix": { "name": { "value": "Jan" } } } } ``` ![lmudkb9l.png](http://flt-pan.58heshihu.com/blog/typecho/lmudkb9l.png) ###### 分词匹配:term 前文最常见的根据分词查询 ``` GET /test-dsl-term-level/_search { "query": { "term": { "programming_languages": "php" } } } ``` ![lmudkq4g.png](http://flt-pan.58heshihu.com/blog/typecho/lmudkq4g.png) ###### 多个分词匹配:terms 按照读个分词term匹配,它们是or的关系。 ``` GET /test-dsl-term-level/_search { "query": { "terms": { "programming_languages": ["php","c++"] } } } ``` ![lmudkx6p.png](http://flt-pan.58heshihu.com/blog/typecho/lmudkx6p.png) ###### 按某个数字字段分词匹配:term set 设计这种方式查询的初衷是用文档中的数字字段动态匹配查询满足term的个数。 ``` GET /test-dsl-term-level/_search { "query": { "terms_set": { "programming_languages": { "terms": [ "java", "php" ], "minimum_should_match_field": "required_matches" } } } } ``` ![lmudl2j8.png](http://flt-pan.58heshihu.com/blog/typecho/lmudl2j8.png) ###### 通配符:wildcard 通配符匹配,比如`*` ``` GET /test-dsl-term-level/_search { "query": { "wildcard": { "name": { "value": "D*ai", "boost": 1.0, "rewrite": "constant_score" } } } } ``` ![lmudl9p6.png](http://flt-pan.58heshihu.com/blog/typecho/lmudl9p6.png) ###### 范围:range 常常被用在数字或者日期范围的查询。 ``` GET /test-dsl-term-level/_search { "query": { "range": { "required_matches": { "gte": 3, "lte": 4 } } } } ``` ![lmudlheq.png](http://flt-pan.58heshihu.com/blog/typecho/lmudlheq.png) ###### 正则:regexp 通过正则表达式查询。以"Jan"开头的name字段。 ``` GET /test-dsl-term-level/_search { "query": { "regexp": { "name": { "value": "Ja.*", "case_insensitive": true } } } } ``` ![lmudmmti.png](http://flt-pan.58heshihu.com/blog/typecho/lmudmmti.png) ###### 模糊匹配:模糊 官方文档对模糊匹配:编辑距离是将一个术语转换为另一个术语所需的一个字符更改的次数。这些更改可以包括: * 更改字符(box→ fox) * 删除字符(black→ lack) * 插入字符(sic→ sick) * 转置两个相邻字符(act→ cat) ``` GET /test-dsl-term-level/_search { "query": { "fuzzy": { "remarks": { "value": "hell" } } } } ``` ![lmudmuhj.png](http://flt-pan.58heshihu.com/blog/typecho/lmudmuhj.png) > 来源: > https://www.pdai.tech/md/db/nosql-es/elasticsearch-x-dsl-term.html > https://www.pdai.tech/md/db/nosql-es/elasticsearch-x-dsl-full-text.html ------------- ## (六):ElasticSearch 聚合查询原理与实践 除了查询之外,最常用的聚合了,ElasticSearch提供了三种聚合方式:桶聚合(Bucket Aggregation),指标聚合(Metric Aggregation) 和 管道聚合(Pipline Aggregation)。本文主要讲讲桶聚合(Bucket Aggregation)。 ### 聚合查询之Bucket聚合 #### 聚合的引入 我们在SQL结果中常有: ``` SELECT COUNT(color) FROM table GROUP BY color ``` ElasticSearch中桶在概念上类似于 SQL 的分组(`GROUP BY`),而指标则类似于 `COUNT()` 、 `SUM()` 、 `MAX()` 等统计方法。 进而引入了两个概念: * 桶(Buckets) 满足特定条件的文档的集合 * 指标(Metrics) 对桶内的文档进行统计计算 所以ElasticSearch包含3种聚合(Aggregation)方式 * 桶聚合(Bucket Aggregation) \- 本文中详解 * 指标聚合(Metric Aggregation) \- 下文中讲解 * 管道聚合(Pipline Aggregation) \- 再下一篇讲解 * 聚合管道化,简单而言就是上一个聚合的结果成为下个聚合的输入; (PS:指标聚合和桶聚合很多情况下是组合在一起使用的,其实你也可以看到,桶聚合本质上是一种特殊的指标聚合,它的聚合指标就是数据的条数count)。 #### 如何理解Bucket聚合 如果你直接去看文档,大概有几十种: ![lmudu0fq.png](http://flt-pan.58heshihu.com/blog/typecho/lmudu0fq.png) 要么你需要花大量时间学习,要么你已经迷失或者即将迷失在知识点中... 所以你需要稍微站在设计者的角度思考下,不难发现设计上大概分为三类(当然有些是第二和第三类的融合)。 ![lmuduhiu.png](http://flt-pan.58heshihu.com/blog/typecho/lmuduhiu.png) (图中并没有全部列出内容,因为图要表达的意图我觉得还是比较清楚的,这就够了;有了这种思虑和认知,会大大提升你的认知效率。)。 #### 按知识点学习聚合 我们先按照官方权威指南中的一个例子,学习Aggregation中的知识点。 ###### 准备数据 让我们先看一个例子。我们将会创建一些对汽车经销商有用的聚合,数据是关于汽车交易的信息:车型、制造商、售价、何时被出售等。 首先我们批量索引一些数据: ``` POST /test-agg-cars/_bulk { "index": {}} { "price" : 10000, "color" : "red", "make" : "honda", "sold" : "2014-10-28" } { "index": {}} { "price" : 20000, "color" : "red", "make" : "honda", "sold" : "2014-11-05" } { "index": {}} { "price" : 30000, "color" : "green", "make" : "ford", "sold" : "2014-05-18" } { "index": {}} { "price" : 15000, "color" : "blue", "make" : "toyota", "sold" : "2014-07-02" } { "index": {}} { "price" : 12000, "color" : "green", "make" : "toyota", "sold" : "2014-08-19" } { "index": {}} { "price" : 20000, "color" : "red", "make" : "honda", "sold" : "2014-11-05" } { "index": {}} { "price" : 80000, "color" : "red", "make" : "bmw", "sold" : "2014-01-01" } { "index": {}} { "price" : 25000, "color" : "blue", "make" : "ford", "sold" : "2014-02-12" } ``` ###### 标准的聚合 有了数据,开始构建我们的第一个聚合。汽车经销商可能会想知道哪个颜色的汽车销量最好,用聚合可以轻易得到结果,用 terms 桶操作: ``` GET /test-agg-cars/_search { "size" : 0, "aggs" : { "popular_colors" : { "terms" : { "field" : "color.keyword" } } } } ``` * 聚合操作被置于顶层参数 aggs 之下(如果你愿意,完整形式 aggregations 同样有效)。 * 然后,可以为聚合指定一个我们想要名称,本例中是:popular_colors 。 * 最后,定义单个桶的类型 terms。 结果如下: ![lmuduv9x.png](http://flt-pan.58heshihu.com/blog/typecho/lmuduv9x.png) * 因为我们设置了 size 参数,所以不会有 hits 搜索结果返回。 * popular_colors 聚合是作为 aggregations 字段的一部分被返回的。 * 每个桶的 key 都与 color 字段里找到的唯一词对应。它总会包含 doc_count 字段,告诉我们包含该词项的文档数量。 * 每个桶的数量代表该颜色的文档数量。 ###### 多个聚合 同时计算两种桶的结果:对color和对make。 ``` GET /test-agg-cars/_search { "size" : 0, "aggs" : { "popular_colors" : { "terms" : { "field" : "color.keyword" } }, "make_by" : { "terms" : { "field" : "make.keyword" } } } } ``` 结果如下: ![lmudv4ce.png](http://flt-pan.58heshihu.com/blog/typecho/lmudv4ce.png) ###### 聚合的嵌套 这个新的聚合层让我们可以将 avg 度量嵌套置于 terms 桶内。实际上,这就为每个颜色生成了平均价格。 ``` GET /test-agg-cars/_search { "size" : 0, "aggs": { "colors": { "terms": { "field": "color.keyword" }, "aggs": { "avg_price": { "avg": { "field": "price" } } } } } } ``` 结果如下: ![lmudvem5.png](http://flt-pan.58heshihu.com/blog/typecho/lmudvem5.png) ###### 动态脚本的聚合 这个例子告诉你,ElasticSearch还支持一些基于脚本(生成运行时的字段)的复杂的动态聚合。 ``` GET /test-agg-cars/_search { "runtime_mappings": { "make.length": { "type": "long", "script": "emit(doc['make.keyword'].value.length())" } }, "size" : 0, "aggs": { "make_length": { "histogram": { "interval": 1, "field": "make.length" } } } } ``` 结果如下: ![lmudvo4h.png](http://flt-pan.58heshihu.com/blog/typecho/lmudvo4h.png) #### 按分类学习Bucket聚合 我们在具体学习时,也无需学习每一个点,基于上面图的认知,我们只需用20%的时间学习最为常用的80%功能即可,其它查查文档而已。 ###### 前置条件的过滤:filter 在当前文档集上下文中定义与指定过滤器(Filter)匹配的所有文档的单个存储桶。通常,这将用于将当前聚合上下文缩小到一组特定的文档。 ``` GET /test-agg-cars/_search { "size": 0, "aggs": { "make_by": { "filter": { "term": { "type": "honda" } }, "aggs": { "avg_price": { "avg": { "field": "price" } } } } } } ``` 结果如下: ![lmudw2gv.png](http://flt-pan.58heshihu.com/blog/typecho/lmudw2gv.png) ###### 对filter进行分组聚合:filters 设计一个新的例子, 日志系统中,每条日志都是在文本中,包含warning/info等信息。 ``` PUT /test-agg-logs/_bulk?refresh { "index" : { "_id" : 1 } } { "body" : "warning: page could not be rendered" } { "index" : { "_id" : 2 } } { "body" : "authentication error" } { "index" : { "_id" : 3 } } { "body" : "warning: connection timed out" } { "index" : { "_id" : 4 } } { "body" : "info: hello pdai" } ``` 我们需要对包含不同日志类型的日志进行分组,这就需要filters: ``` GET /test-agg-logs/_search { "size": 0, "aggs" : { "messages" : { "filters" : { "other_bucket_key": "other_messages", "filters" : { "infos" : { "match" : { "body" : "info" }}, "warnings" : { "match" : { "body" : "warning" }} } } } } } ``` 结果如下: ![lmudwl3j.png](http://flt-pan.58heshihu.com/blog/typecho/lmudwl3j.png) ###### 对number类型聚合:Range 基于多桶值源的聚合,使用户能够定义一组范围-每个范围代表一个桶。在聚合过程中,将从每个存储区范围中检查从每个文档中提取的值,并“存储”相关/匹配的文档。请注意,此聚合包括 from 值,但不包括 to 每个范围的值。 ``` GET /test-agg-cars/_search { "size": 0, "aggs": { "price_ranges": { "range": { "field": "price", "ranges": [ { "to": 20000 }, { "from": 20000, "to": 40000 }, { "from": 40000 } ] } } } } ``` 结果如下: ![lmudx6z0.png](http://flt-pan.58heshihu.com/blog/typecho/lmudx6z0.png) ###### 对IP类型聚合:IP Range 专用于IP值的范围聚合。 ``` GET /ip_addresses/_search { "size": 10, "aggs": { "ip_ranges": { "ip_range": { "field": "ip", "ranges": [ { "to": "10.0.0.5" }, { "from": "10.0.0.5" } ] } } } } ``` 返回 ``` { ... "aggregations": { "ip_ranges": { "buckets": [ { "key": "*-10.0.0.5", "to": "10.0.0.5", "doc_count": 10 }, { "key": "10.0.0.5-*", "from": "10.0.0.5", "doc_count": 260 } ] } } } ``` * CIDR Mask分组 此外还可以用CIDR Mask分组 ``` GET /ip_addresses/_search { "size": 0, "aggs": { "ip_ranges": { "ip_range": { "field": "ip", "ranges": [ { "mask": "10.0.0.0/25" }, { "mask": "10.0.0.127/25" } ] } } } } ``` 返回 ``` { ... "aggregations": { "ip_ranges": { "buckets": [ { "key": "10.0.0.0/25", "from": "10.0.0.0", "to": "10.0.0.128", "doc_count": 128 }, { "key": "10.0.0.127/25", "from": "10.0.0.0", "to": "10.0.0.128", "doc_count": 128 } ] } } } ``` * 增加key显示 ``` GET /ip_addresses/_search { "size": 0, "aggs": { "ip_ranges": { "ip_range": { "field": "ip", "ranges": [ { "to": "10.0.0.5" }, { "from": "10.0.0.5" } ], "keyed": true // here } } } } ``` 返回 ``` { ... "aggregations": { "ip_ranges": { "buckets": { "*-10.0.0.5": { "to": "10.0.0.5", "doc_count": 10 }, "10.0.0.5-*": { "from": "10.0.0.5", "doc_count": 260 } } } } } ``` * 自定义key显示 ``` GET /ip_addresses/_search { "size": 0, "aggs": { "ip_ranges": { "ip_range": { "field": "ip", "ranges": [ { "key": "infinity", "to": "10.0.0.5" }, { "key": "and-beyond", "from": "10.0.0.5" } ], "keyed": true } } } } ``` 返回 ``` { ... "aggregations": { "ip_ranges": { "buckets": { "infinity": { "to": "10.0.0.5", "doc_count": 10 }, "and-beyond": { "from": "10.0.0.5", "doc_count": 260 } } } } } ``` ###### 对日期类型聚合:Date Range 专用于日期值的范围聚合。 ``` GET /test-agg-cars/_search { "size": 0, "aggs": { "range": { "date_range": { "field": "sold", "format": "yyyy-MM", "ranges": [ { "from": "2014-01-01" }, { "to": "2014-12-31" } ] } } } } ``` 结果如下: ![lmudxlfp.png](http://flt-pan.58heshihu.com/blog/typecho/lmudxlfp.png) 此聚合与Range聚合之间的主要区别在于 from和to值可以在Date Math表达式中表示,并且还可以指定日期格式,通过该日期格式将返回from and to响应字段。请注意,此聚合包括from值,但不包括to每个范围的值。 ###### 对柱状图功能:Histrogram 直方图 histogram 本质上是就是为柱状图功能设计的。 创建直方图需要指定一个区间,如果我们要为售价创建一个直方图,可以将间隔设为 20,000。这样做将会在每个 $20,000 档创建一个新桶,然后文档会被分到对应的桶中。 对于仪表盘来说,我们希望知道每个售价区间内汽车的销量。我们还会想知道每个售价区间内汽车所带来的收入,可以通过对每个区间内已售汽车的售价求和得到。 可以用 histogram 和一个嵌套的 sum 度量得到我们想要的答案: ``` GET /test-agg-cars/_search { "size" : 0, "aggs":{ "price":{ "histogram":{ "field": "price.keyword", "interval": 20000 }, "aggs":{ "revenue": { "sum": { "field" : "price" } } } } } } ``` * histogram 桶要求两个参数:一个数值字段以及一个定义桶大小间隔。 * sum 度量嵌套在每个售价区间内,用来显示每个区间内的总收入。 如我们所见,查询是围绕 price 聚合构建的,它包含一个 histogram 桶。它要求字段的类型必须是数值型的同时需要设定分组的间隔范围。间隔设置为 20,000 意味着我们将会得到如 `[0-19999, 20000-39999, ...]`这样的区间。 接着,我们在直方图内定义嵌套的度量,这个 sum 度量,它会对落入某一具体售价区间的文档中 price 字段的值进行求和。这可以为我们提供每个售价区间的收入,从而可以发现到底是普通家用车赚钱还是奢侈车赚钱。 响应结果如下: ![lmudxzxx.png](http://flt-pan.58heshihu.com/blog/typecho/lmudxzxx.png) 结果很容易理解,不过应该注意到直方图的键值是区间的下限。键 0 代表区间 0-19,999 ,键 20000 代表区间 20,000-39,999 ,等等。 ![lmudy7ok.png](http://flt-pan.58heshihu.com/blog/typecho/lmudy7ok.png) 当然,我们可以为任何聚合输出的分类和统计结果创建条形图,而不只是 直方图 桶。让我们以最受欢迎 10 种汽车以及它们的平均售价、标准差这些信息创建一个条形图。我们会用到 terms 桶和 extended_stats 度量: ``` GET /test-agg-cars/_search { "size" : 0, "aggs": { "makes": { "terms": { "field": "make.keyword", "size": 10 }, "aggs": { "stats": { "extended_stats": { "field": "price" } } } } } } ``` 上述代码会按受欢迎度返回制造商列表以及它们各自的统计信息。我们对其中的 stats.avg 、 stats.count 和 stats.std_deviation 信息特别感兴趣,并用 它们计算出标准差: ``` std_err = std_deviation / count ``` ![lmudygz5.png](http://flt-pan.58heshihu.com/blog/typecho/lmudygz5.png) 报表: ![lmudynlc.png](http://flt-pan.58heshihu.com/blog/typecho/lmudynlc.png) ### 聚合查询之 Metric 聚合 前文主要讲了 ElasticSearch提供的三种聚合方式之桶聚合(Bucket Aggregation),本文主要讲讲指标聚合(Metric Aggregation)。 #### 如何理解metric聚合 在bucket聚合中,我画了一张图辅助你构筑体系,那么metric聚合又如何理解呢? 如果你直接去看官方文档,大概也有十几种: ![lmudz0cs.png](http://flt-pan.58heshihu.com/blog/typecho/lmudz0cs.png) 那么metric聚合又如何理解呢?我认为从两个角度: * 从分类看:Metric聚合分析分为单值分析和多值分析两类 * 从功能看:根据具体的应用场景设计了一些分析api, 比如地理位置,百分数等等 融合上述两个方面,我们可以梳理出大致的一个mind图: * 单值分析: 只输出一个分析结果 * `cardinality` 基数(distinct去重) * `weighted_avg` 带权重的avg * `median_absolute_deviation` 中位值 * `avg` 平均值 * `max` 最大值 * `min` 最小值 * `sum` 和 * `value_count` 数量 * 标准stat型 * 其它类型 * 多值分析: 单值之外的 * `top_hits` 分桶后的top hits * `top_metrics` * `geo_bounds` Geo bounds * `geo_centroid` Geo-centroid * `geo_line` Geo-Line * `percentiles` 百分数范围 * `percentile_ranks` 百分数排行 * `stats` 包含avg,max,min,sum和count * `matrix_stats` 针对矩阵模型 * `extended_stats` * `string_stats` 针对字符串 * stats型 * 百分数型 * 地理位置型 * Top型 通过上述列表(我就不画图了),我们构筑的体系是基于`分类`和`功能`,而不是具体的项(比如avg,percentiles...);这是不同的认知维度: 具体的项是碎片化,分类和功能这种是你需要构筑的体系。 #### 单值分析: 标准stat类型 ###### `avg` 平均值 计算班级的平均分 ``` POST /exams/_search?size=0 { "aggs": { "avg_grade": { "avg": { "field": "grade" } } } } ``` 返回 ``` { ... "aggregations": { "avg_grade": { "value": 75.0 } } } ``` ###### `max` 最大值 计算销售最高价 ``` POST /sales/_search?size=0 { "aggs": { "max_price": { "max": { "field": "price" } } } } ``` 返回 ``` { ... "aggregations": { "max_price": { "value": 200.0 } } } ``` ###### `min` 最小值 计算销售最低价 ``` POST /sales/_search?size=0 { "aggs": { "min_price": { "min": { "field": "price" } } } } ``` 返回 ``` { ... "aggregations": { "min_price": { "value": 10.0 } } } ``` ###### `sum` 和 计算销售总价 ``` POST /sales/_search?size=0 { "query": { "constant_score": { "filter": { "match": { "type": "hat" } } } }, "aggs": { "hat_prices": { "sum": { "field": "price" } } } } ``` 返回 ``` { ... "aggregations": { "hat_prices": { "value": 450.0 } } } ``` ###### `value_count` 数量 销售数量统计 ``` POST /sales/_search?size=0 { "aggs" : { "types_count" : { "value_count" : { "field" : "type" } } } } ``` 返回 ``` { ... "aggregations": { "types_count": { "value": 7 } } } ``` #### 单值分析: 其它类型 ###### `weighted_avg` 带权重的avg ``` POST /exams/_search { "size": 0, "aggs": { "weighted_grade": { "weighted_avg": { "value": { "field": "grade" }, "weight": { "field": "weight" } } } } } ``` 返回 ``` { ... "aggregations": { "weighted_grade": { "value": 70.0 } } } ``` ###### `cardinality` 基数(distinct去重) ``` POST /sales/_search?size=0 { "aggs": { "type_count": { "cardinality": { "field": "type" } } } } ``` 返回 ``` { ... "aggregations": { "type_count": { "value": 3 } } } ``` ###### `median_absolute_deviation` 中位值 ``` GET reviews/_search { "size": 0, "aggs": { "review_average": { "avg": { "field": "rating" } }, "review_variability": { "median_absolute_deviation": { "field": "rating" } } } } ``` 返回 ``` { ... "aggregations": { "review_average": { "value": 3.0 }, "review_variability": { "value": 2.0 } } } ``` #### 非单值分析:stats型 ###### `stats` 包含avg,max,min,sum和count ``` POST /exams/_search?size=0 { "aggs": { "grades_stats": { "stats": { "field": "grade" } } } } ``` 返回 ``` { ... "aggregations": { "grades_stats": { "count": 2, "min": 50.0, "max": 100.0, "avg": 75.0, "sum": 150.0 } } } ``` ###### `matrix_stats` 针对矩阵模型 以下示例说明了使用矩阵统计量来描述收入与贫困之间的关系。 ``` GET /_search { "aggs": { "statistics": { "matrix_stats": { "fields": [ "poverty", "income" ] } } } } ``` 返回 ``` { ... "aggregations": { "statistics": { "doc_count": 50, "fields": [ { "name": "income", "count": 50, "mean": 51985.1, "variance": 7.383377037755103E7, "skewness": 0.5595114003506483, "kurtosis": 2.5692365287787124, "covariance": { "income": 7.383377037755103E7, "poverty": -21093.65836734694 }, "correlation": { "income": 1.0, "poverty": -0.8352655256272504 } }, { "name": "poverty", "count": 50, "mean": 12.732000000000001, "variance": 8.637730612244896, "skewness": 0.4516049811903419, "kurtosis": 2.8615929677997767, "covariance": { "income": -21093.65836734694, "poverty": 8.637730612244896 }, "correlation": { "income": -0.8352655256272504, "poverty": 1.0 } } ] } } } ``` ###### `extended_stats` 根据从汇总文档中提取的数值计算统计信息。 ``` GET /exams/_search { "size": 0, "aggs": { "grades_stats": { "extended_stats": { "field": "grade" } } } } ``` 上面的汇总计算了所有文档的成绩统计信息。聚合类型为extended_stats,并且字段设置定义将在其上计算统计信息的文档的数字字段。 ``` { ... "aggregations": { "grades_stats": { "count": 2, "min": 50.0, "max": 100.0, "avg": 75.0, "sum": 150.0, "sum_of_squares": 12500.0, "variance": 625.0, "variance_population": 625.0, "variance_sampling": 1250.0, "std_deviation": 25.0, "std_deviation_population": 25.0, "std_deviation_sampling": 35.35533905932738, "std_deviation_bounds": { "upper": 125.0, "lower": 25.0, "upper_population": 125.0, "lower_population": 25.0, "upper_sampling": 145.71067811865476, "lower_sampling": 4.289321881345245 } } } } ``` ###### `string_stats` 针对字符串 用于计算从聚合文档中提取的字符串值的统计信息。这些值可以从特定的关键字字段中检索。 ``` POST /my-index-000001/_search?size=0 { "aggs": { "message_stats": { "string_stats": { "field": "message.keyword" } } } } ``` 返回 ``` { ... "aggregations": { "message_stats": { "count": 5, "min_length": 24, "max_length": 30, "avg_length": 28.8, "entropy": 3.94617750050791 } } } ``` #### 非单值分析:百分数型 ###### `percentiles` 百分数范围 针对从聚合文档中提取的数值计算一个或多个百分位数。 ``` GET latency/_search { "size": 0, "aggs": { "load_time_outlier": { "percentiles": { "field": "load_time" } } } } ``` 默认情况下,百分位度量标准将生成一定范围的百分位:`[1,5,25,50,75,95,99]`。 ``` { ... "aggregations": { "load_time_outlier": { "values": { "1.0": 5.0, "5.0": 25.0, "25.0": 165.0, "50.0": 445.0, "75.0": 725.0, "95.0": 945.0, "99.0": 985.0 } } } } ``` ###### `percentile_ranks` 百分数排行 根据从汇总文档中提取的数值计算一个或多个百分位等级。 ``` GET latency/_search { "size": 0, "aggs": { "load_time_ranks": { "percentile_ranks": { "field": "load_time", "values": [ 500, 600 ] } } } } ``` 返回 ``` { ... "aggregations": { "load_time_ranks": { "values": { "500.0": 90.01, "600.0": 100.0 } } } } ``` 上述结果表示90.01%的页面加载在500ms内完成,而100%的页面加载在600ms内完成。 #### 非单值分析:地理位置型 ###### `geo_bounds` Geo bounds ``` PUT /museums { "mappings": { "properties": { "location": { "type": "geo_point" } } } } POST /museums/_bulk?refresh {"index":{"_id":1}} {"location": "52.374081,4.912350", "name": "NEMO Science Museum"} {"index":{"_id":2}} {"location": "52.369219,4.901618", "name": "Museum Het Rembrandthuis"} {"index":{"_id":3}} {"location": "52.371667,4.914722", "name": "Nederlands Scheepvaartmuseum"} {"index":{"_id":4}} {"location": "51.222900,4.405200", "name": "Letterenhuis"} {"index":{"_id":5}} {"location": "48.861111,2.336389", "name": "Musée du Louvre"} {"index":{"_id":6}} {"location": "48.860000,2.327000", "name": "Musée d'Orsay"} POST /museums/_search?size=0 { "query": { "match": { "name": "musée" } }, "aggs": { "viewport": { "geo_bounds": { "field": "location", "wrap_longitude": true } } } } ``` 上面的汇总展示了如何针对具有商店业务类型的所有文档计算位置字段的边界框。 ``` { ... "aggregations": { "viewport": { "bounds": { "top_left": { "lat": 48.86111099738628, "lon": 2.3269999679178 }, "bottom_right": { "lat": 48.85999997612089, "lon": 2.3363889567553997 } } } } } ``` ###### `geo_centroid` Geo-centroid ``` PUT /museums { "mappings": { "properties": { "location": { "type": "geo_point" } } } } POST /museums/_bulk?refresh {"index":{"_id":1}} {"location": "52.374081,4.912350", "city": "Amsterdam", "name": "NEMO Science Museum"} {"index":{"_id":2}} {"location": "52.369219,4.901618", "city": "Amsterdam", "name": "Museum Het Rembrandthuis"} {"index":{"_id":3}} {"location": "52.371667,4.914722", "city": "Amsterdam", "name": "Nederlands Scheepvaartmuseum"} {"index":{"_id":4}} {"location": "51.222900,4.405200", "city": "Antwerp", "name": "Letterenhuis"} {"index":{"_id":5}} {"location": "48.861111,2.336389", "city": "Paris", "name": "Musée du Louvre"} {"index":{"_id":6}} {"location": "48.860000,2.327000", "city": "Paris", "name": "Musée d'Orsay"} POST /museums/_search?size=0 { "aggs": { "centroid": { "geo_centroid": { "field": "location" } } } } ``` 上面的汇总显示了如何针对所有具有犯罪类型的盗窃文件计算位置字段的质心。 ``` { ... "aggregations": { "centroid": { "location": { "lat": 51.00982965203002, "lon": 3.9662131341174245 }, "count": 6 } } } ``` ###### `geo_line` Geo-Line ``` PUT test { "mappings": { "dynamic": "strict", "_source": { "enabled": false }, "properties": { "my_location": { "type": "geo_point" }, "group": { "type": "keyword" }, "@timestamp": { "type": "date" } } } } POST /test/_bulk?refresh {"index": {}} {"my_location": {"lat":37.3450570, "lon": -122.0499820}, "@timestamp": "2013-09-06T16:00:36"} {"index": {}} {"my_location": {"lat": 37.3451320, "lon": -122.0499820}, "@timestamp": "2013-09-06T16:00:37Z"} {"index": {}} {"my_location": {"lat": 37.349283, "lon": -122.0505010}, "@timestamp": "2013-09-06T16:00:37Z"} POST /test/_search?filter_path=aggregations { "aggs": { "line": { "geo_line": { "point": {"field": "my_location"}, "sort": {"field": "@timestamp"} } } } } ``` 将存储桶中的所有 geo_point 值聚合到由所选排序字段排序的 LineString 中。 ``` { "aggregations": { "line": { "type" : "Feature", "geometry" : { "type" : "LineString", "coordinates" : [ [ -122.049982, 37.345057 ], [ -122.050501, 37.349283 ], [ -122.049982, 37.345132 ] ] }, "properties" : { "complete" : true } } } } ``` #### 非单值分析:Top型 ###### `top_hits` 分桶后的 top hits。 ``` POST /sales/_search?size=0 { "aggs": { "top_tags": { "terms": { "field": "type", "size": 3 }, "aggs": { "top_sales_hits": { "top_hits": { "sort": [ { "date": { "order": "desc" } } ], "_source": { "includes": [ "date", "price" ] }, "size": 1 } } } } } } ``` 返回 ``` { ... "aggregations": { "top_tags": { "doc_count_error_upper_bound": 0, "sum_other_doc_count": 0, "buckets": [ { "key": "hat", "doc_count": 3, "top_sales_hits": { "hits": { "total" : { "value": 3, "relation": "eq" }, "max_score": null, "hits": [ { "_index": "sales", "_type": "_doc", "_id": "AVnNBmauCQpcRyxw6ChK", "_source": { "date": "2015/03/01 00:00:00", "price": 200 }, "sort": [ 1425168000000 ], "_score": null } ] } } }, { "key": "t-shirt", "doc_count": 3, "top_sales_hits": { "hits": { "total" : { "value": 3, "relation": "eq" }, "max_score": null, "hits": [ { "_index": "sales", "_type": "_doc", "_id": "AVnNBmauCQpcRyxw6ChL", "_source": { "date": "2015/03/01 00:00:00", "price": 175 }, "sort": [ 1425168000000 ], "_score": null } ] } } }, { "key": "bag", "doc_count": 1, "top_sales_hits": { "hits": { "total" : { "value": 1, "relation": "eq" }, "max_score": null, "hits": [ { "_index": "sales", "_type": "_doc", "_id": "AVnNBmatCQpcRyxw6ChH", "_source": { "date": "2015/01/01 00:00:00", "price": 150 }, "sort": [ 1420070400000 ], "_score": null } ] } } } ] } } } ``` ###### `top_metrics` ``` POST /test/_bulk?refresh {"index": {}} {"s": 1, "m": 3.1415} {"index": {}} {"s": 2, "m": 1.0} {"index": {}} {"s": 3, "m": 2.71828} POST /test/_search?filter_path=aggregations { "aggs": { "tm": { "top_metrics": { "metrics": {"field": "m"}, "sort": {"s": "desc"} } } } } ``` 返回 ``` { "aggregations": { "tm": { "top": [ {"sort": [3], "metrics": {"m": 2.718280076980591 } } ] } } } ``` ### 聚合查询之Pipline聚合 前文主要讲了 ElasticSearch提供的三种聚合方式之指标聚合(Metric Aggregation),本文主要讲讲管道聚合(Pipeline Aggregation)。简单而言就是让上一步的聚合结果成为下一个聚合的输入,这就是管道。 #### 如何理解pipeline聚合 如何理解管道聚合呢?最重要的是要站在设计者角度看这个功能的要实现的目的:让上一步的聚合结果成为下一个聚合的输入,这就是管道。 #### 管道机制的常见场景 首先回顾下,我们之前在Tomcat管道机制中向你介绍的常见的管道机制设计中的应用场景。 ###### 责任链模式 管道机制在设计模式上属于责任链模式,如果你不理解,请参看如下文章: 责任链模式: 通过责任链模式, 你可以为某个请求创建一个对象链. 每个对象依序检查此请求并对其进行处理或者将它传给链中的下一个对象。 ###### FilterChain 在软件开发的常接触的责任链模式是FilterChain,它体现在很多软件设计中: * 比如Spring Security框架中 ![lmue0lyp.png](http://flt-pan.58heshihu.com/blog/typecho/lmue0lyp.png) * 比如HttpServletRequest处理的过滤器中 当一个request过来的时候,需要对这个request做一系列的加工,使用责任链模式可以使每个加工组件化,减少耦合。也可以使用在当一个request过来的时候,需要找到合适的加工方式。当一个加工方式不适合这个request的时候,传递到下一个加工方法,该加工方式再尝试对request加工。 网上找了图,这里我们后文将通过Tomcat请求处理向你阐述。 ![lmue0r12.png](http://flt-pan.58heshihu.com/blog/typecho/lmue0r12.png) ###### ElasticSearch设计管道机制 简单而言:让上一步的聚合结果成为下一个聚合的输入,这就是管道。 接下来,无非就是对不同类型的聚合有接口的支撑,比如: ![lmue18ud.png](http://flt-pan.58heshihu.com/blog/typecho/lmue18ud.png) 第一个维度:管道聚合有很多不同类型,每种类型都与其他聚合计算不同的信息,但是可以将这些类型分为两类: * 父级 父级聚合的输出提供了一组管道聚合,它可以计算新的存储桶或新的聚合以添加到现有存储桶中。 * 兄弟 同级聚合的输出提供的管道聚合,并且能够计算与该同级聚合处于同一级别的新聚合。 第二个维度:根据功能设计的意图 比如前置聚合可能是Bucket聚合,后置的可能是基于Metric聚合,那么它就可以成为一类管道 进而引出了:`xxx bucket`(是不是很容易理解了) * Bucket聚合 -> Metric聚合:bucket聚合的结果,成为下一步metric聚合的输入 * Average bucket * Min bucket * Max bucket * Sum bucket * Stats bucket * Extended stats bucket 对构建体系而言,理解上面的已经够了,其它的类型不过是锦上添花而言。 #### 一些例子 这里我们通过几个简单的例子看看即可,具体如果需要使用看看文档即可。 ###### Average bucket 聚合 ``` POST _search { "size": 0, "aggs": { "sales_per_month": { "date_histogram": { "field": "date", "calendar_interval": "month" }, "aggs": { "sales": { "sum": { "field": "price" } } } }, "avg_monthly_sales": { // tag::avg-bucket-agg-syntax[] "avg_bucket": { "buckets_path": "sales_per_month>sales", "gap_policy": "skip", "format": "#,##0.00;(#,##0.00)" } // end::avg-bucket-agg-syntax[] } } } ``` * 嵌套的bucket聚合:聚合出按月价格的直方图 * Metic聚合:对上面的聚合再求平均值。 字段类型: * buckets_path:指定聚合的名称,支持多级嵌套聚合。 * gap\_policy 当管道聚合遇到不存在的值,有点类似于term等聚合的(missing)时所采取的策略,可选择值为:skip、insert\_zeros。 * skip:此选项将丢失的数据视为bucket不存在。它将跳过桶并使用下一个可用值继续计算。 * format 用于格式化聚合桶的输出(key)。 输出结果如下 ``` { "took": 11, "timed_out": false, "_shards": ..., "hits": ..., "aggregations": { "sales_per_month": { "buckets": [ { "key_as_string": "2015/01/01 00:00:00", "key": 1420070400000, "doc_count": 3, "sales": { "value": 550.0 } }, { "key_as_string": "2015/02/01 00:00:00", "key": 1422748800000, "doc_count": 2, "sales": { "value": 60.0 } }, { "key_as_string": "2015/03/01 00:00:00", "key": 1425168000000, "doc_count": 2, "sales": { "value": 375.0 } } ] }, "avg_monthly_sales": { "value": 328.33333333333333, "value_as_string": "328.33" } } } ``` ###### Stats bucket 聚合 进一步的stat bucket也很容易理解了。 ``` POST /sales/_search { "size": 0, "aggs": { "sales_per_month": { "date_histogram": { "field": "date", "calendar_interval": "month" }, "aggs": { "sales": { "sum": { "field": "price" } } } }, "stats_monthly_sales": { "stats_bucket": { "buckets_path": "sales_per_month>sales" } } } } ``` 返回 ``` { "took": 11, "timed_out": false, "_shards": ..., "hits": ..., "aggregations": { "sales_per_month": { "buckets": [ { "key_as_string": "2015/01/01 00:00:00", "key": 1420070400000, "doc_count": 3, "sales": { "value": 550.0 } }, { "key_as_string": "2015/02/01 00:00:00", "key": 1422748800000, "doc_count": 2, "sales": { "value": 60.0 } }, { "key_as_string": "2015/03/01 00:00:00", "key": 1425168000000, "doc_count": 2, "sales": { "value": 375.0 } } ] }, "stats_monthly_sales": { "count": 3, "min": 60.0, "max": 550.0, "avg": 328.3333333333333, "sum": 985.0 } } } ``` > 来源: > https://pdai.tech/md/db/nosql-es/elasticsearch-x-agg-bucket.html > https://www.pdai.tech/md/db/nosql-es/elasticsearch-x-agg-metric.html > https://pdai.tech/md/db/nosql-es/elasticsearch-x-agg-pipeline.html ------------- ## (七):ElasticSearch 文档索引与读取流程详解 ElasticSearch 中最重要原理是文档的索引和读取! ### 索引文档流程 #### 文档索引步骤顺序 ###### 单个文档 新建单个文档所需要的步骤顺序: ![lmue4fdi.png](http://flt-pan.58heshihu.com/blog/typecho/lmue4fdi.png) * 客户端向 Node 1 发送新建、索引或者删除请求。 * 节点使用文档的 `_id` 确定文档属于分片 0 。请求会被转发到 Node 3,因为分片 0 的主分片目前被分配在 Node 3 上。 * Node 3 在主分片上面执行请求。如果成功了,它将请求并行转发到 Node 1 和 Node 2 的副本分片上。一旦所有的副本分片都报告成功, Node 3 将向协调节点报告成功,协调节点向客户端报告成功。 ###### 多个文档 使用 bulk 修改多个文档步骤顺序: ![lmue4lhl.png](http://flt-pan.58heshihu.com/blog/typecho/lmue4lhl.png) * 客户端向 Node 1 发送 bulk 请求。 * Node 1 为每个节点创建一个批量请求,并将这些请求并行转发到每个包含主分片的节点主机。 * 主分片一个接一个按顺序执行每个操作。当每个操作成功时,主分片并行转发新文档(或删除)到副本分片,然后执行下一个操作。一旦所有的副本分片报告所有操作成功,该节点将向协调节点报告成功,协调节点将这些响应收集整理并返回给客户端。 #### 文档索引过程详解 先看下整体的索引流程 ![lmue51dk.png](http://flt-pan.58heshihu.com/blog/typecho/lmue51dk.png) * 协调节点默认使用文档ID参与计算(也支持通过routing),以便为路由提供合适的分片。 ``` shard = hash(document_id) % (num_of_primary_shards) ``` * 当分片所在的节点接收到来自协调节点的请求后,会将请求写入到Memory Buffer,然后定时(默认是每隔1秒)写入到Filesystem Cache,这个从Momery Buffer到Filesystem Cache的过程就叫做refresh; * 当然在某些情况下,存在Momery Buffer和Filesystem Cache的数据可能会丢失,ES是通过translog的机制来保证数据的可靠性的。其实现机制是接收到请求后,同时也会写入到translog中,当Filesystem cache中的数据写入到磁盘中时,才会清除掉,这个过程叫做flush。 * 在flush过程中,内存中的缓冲将被清除,内容被写入一个新段,段的fsync将创建一个新的提交点,并将内容刷新到磁盘,旧的translog将被删除并开始一个新的translog。flush触发的时机是定时触发(默认30分钟)或者translog变得太大(默认为512M)时。 ###### 分步骤看数据持久化过程 通过分步骤看数据持久化过程:write -> refresh -> flush -> merge * write 过程 ![lmue5ct3.png](http://flt-pan.58heshihu.com/blog/typecho/lmue5ct3.png) 一个新文档过来,会存储在 in-memory buffer 内存缓存区中,顺便会记录 Translog(Elasticsearch 增加了一个 translog ,或者叫事务日志,在每一次对 Elasticsearch 进行操作时均进行了日志记录)。 这时候数据还没到 segment ,是搜不到这个新文档的。数据只有被 refresh 后,才可以被搜索到。 * refresh 过程 ![lmue5ji4.png](http://flt-pan.58heshihu.com/blog/typecho/lmue5ji4.png) refresh 默认 1 秒钟,执行一次上图流程。ES 是支持修改这个值的,通过 `index.refresh_interval` 设置 refresh (冲刷)间隔时间。refresh 流程大致如下: * in-memory buffer 中的文档写入到新的 segment 中,但 segment 是存储在文件系统的缓存中。此时文档可以被搜索到 * 最后清空 in-memory buffer。注意: Translog 没有被清空,为了将 segment 数据写到磁盘 * 文档经过 refresh 后,segment 暂时写到文件系统缓存,这样避免了性能 IO 操作,又可以使文档搜索到。refresh 默认 1 秒执行一次,性能损耗太大。一般建议稍微延长这个 refresh 时间间隔,比如 5s。因此,ES 其实就是准实时,达不到真正的实时。 * flush 过程 每隔一段时间—例如 translog 变得越来越大—索引被刷新(flush);一个新的 translog 被创建,并且一个全量提交被执行。 ![lmue5vle.png](http://flt-pan.58heshihu.com/blog/typecho/lmue5vle.png) 上个过程中 segment 在文件系统缓存中,会有意外故障文档丢失。那么,为了保证文档不会丢失,需要将文档写入磁盘。那么文档从文件缓存写入磁盘的过程就是 flush。写入磁盘后,清空 translog。具体过程如下: * 所有在内存缓冲区的文档都被写入一个新的段。 * 缓冲区被清空。 * 一个Commit Point被写入硬盘。 * 文件系统缓存通过 fsync 被刷新(flush)。 * 老的 translog 被删除。 * merge 过程 由于自动刷新流程每秒会创建一个新的段 ,这样会导致短时间内的段数量暴增。而段数目太多会带来较大的麻烦。每一个段都会消耗文件句柄、内存和cpu运行周期。更重要的是,每个搜索请求都必须轮流检查每个段;所以段越多,搜索也就越慢。 Elasticsearch通过在后台进行Merge Segment来解决这个问题。小的段被合并到大的段,然后这些大的段再被合并到更大的段。 当索引的时候,刷新(refresh)操作会创建新的段并将段打开以供搜索使用。合并进程选择一小部分大小相似的段,并且在后台将它们合并到更大的段中。这并不会中断索引和搜索。 ![lmue67qw.png](http://flt-pan.58heshihu.com/blog/typecho/lmue67qw.png) 一旦合并结束,老的段被删除: * 新的段被刷新(flush)到了磁盘。** 写入一个包含新段且排除旧的和较小的段的新提交点。 * 新的段被打开用来搜索。 * 老的段被删除。 ![lmue6k6o.png](http://flt-pan.58heshihu.com/blog/typecho/lmue6k6o.png) 合并大的段需要消耗大量的I/O和CPU资源,如果任其发展会影响搜索性能。Elasticsearch在默认情况下会对合并流程进行资源限制,所以搜索仍然 有足够的资源很好地执行。更多关于 ElasticSearch 数据库的学习文章,请参阅:[搜索引擎 ElasticSearch ](https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MzI0MDQ4MTM5NQ==&action=getalbum&album_id=2931171682919415809#wechat_redirect),本系列持续更新中。 #### 深入ElasticSearch索引文档的实现机制 ###### 写操作的关键点 在考虑或分析一个分布式系统的写操作时,一般需要从下面几个方面考虑: * 可靠性:或者是持久性,数据写入系统成功后,数据不会被回滚或丢失。 * 一致性:数据写入成功后,再次查询时必须能保证读取到最新版本的数据,不能读取到旧数据。 * 原子性:一个写入或者更新操作,要么完全成功,要么完全失败,不允许出现中间状态。 * 隔离性:多个写入操作相互不影响。 * 实时性:写入后是否可以立即被查询到。 * 性能:写入性能,吞吐量到底怎么样。 Elasticsearch作为分布式系统,也需要在写入的时候满足上述的四个特点,我们在后面的写流程介绍中会涉及到上述四个方面。 接下来,我们一层一层剖析Elasticsearch内部的写机制。 ###### Lucene的写 众所周知,Elasticsearch 内部使用了 Lucene 完成索引创建和搜索功能,Lucene中写操作主要是通过 IndexWriter 类实现,IndexWriter 提供三个接口: ``` public long addDocument(); public long updateDocuments(); public long deleteDocuments(); ``` 通过这三个接口可以完成单个文档的写入,更新和删除功能,包括了分词,倒排创建,正排创建等等所有搜索相关的流程。只要Doc通过IndesWriter写入后,后面就可以通过IndexSearcher搜索了,看起来功能已经完善了,但是仍然有一些问题没有解: * 上述操作是单机的,而不是我们需要的分布式。 * 文档写入Lucene后并不是立即可查询的,需要生成完整的Segment后才可被搜索,如何保证实时性? * Lucene生成的Segment是在内存中,如果机器宕机或掉电后,内存中的Segment会丢失,如何保证数据可靠性 ? * Lucene不支持部分文档更新,但是这又是一个强需求,如何支持部分更新? 上述问题,在Lucene中是没有解决的,那么就需要Elasticsearch中解决上述问题。 我们再来看Elasticsearch中的写机制。 ###### Elasticsearch的写 Elasticsearch采用多Shard方式,通过配置routing规则将数据分成多个数据子集,每个数据子集提供独立的索引和搜索功能。当写入文档的时候,根据routing规则,将文档发送给特定Shard中建立索引。这样就能实现分布式了。 此外,Elasticsearch整体架构上采用了一主多副的方式: ![lmue7350.png](http://flt-pan.58heshihu.com/blog/typecho/lmue7350.png) 每个Index由多个Shard组成,每个Shard有一个主节点和多个副本节点,副本个数可配。但每次写入的时候,写入请求会先根据\_routing规则选择发给哪个Shard,Index Request中可以设置使用哪个Filed的值作为路由参数,如果没有设置,则使用Mapping中的配置,如果mapping中也没有配置,则使用\_id作为路由参数,然后通过_routing的Hash值选择出Shard(在OperationRouting类中),最后从集群的Meta中找出出该Shard的Primary节点。 请求接着会发送给Primary Shard,在Primary Shard上执行成功后,再从Primary Shard上将请求同时发送给多个Replica Shard,请求在多个Replica Shard上执行成功并返回给Primary Shard后,写入请求执行成功,返回结果给客户端。 这种模式下,写入操作的延时就等于latency = Latency(Primary Write) + Max(Replicas Write)。只要有副本在,写入延时最小也是两次单Shard的写入时延总和,写入效率会较低,但是这样的好处也很明显,避免写入后,单机或磁盘故障导致数据丢失,在数据重要性和性能方面,一般都是优先选择数据,除非一些允许丢数据的特殊场景。 采用多个副本后,避免了单机或磁盘故障发生时,对已经持久化后的数据造成损害,但是Elasticsearch里为了减少磁盘IO保证读写性能,一般是每隔一段时间(比如5分钟)才会把Lucene的Segment写入磁盘持久化,对于写入内存,但还未Flush到磁盘的Lucene数据,如果发生机器宕机或者掉电,那么内存中的数据也会丢失,这时候如何保证? 对于这种问题,Elasticsearch学习了数据库中的处理方式:增加CommitLog模块,Elasticsearch中叫TransLog。 ![lmue7jcj.png](http://flt-pan.58heshihu.com/blog/typecho/lmue7jcj.png) 在每一个Shard中,写入流程分为两部分,先写入Lucene,再写入TransLog。 写入请求到达Shard后,先写Lucene文件,创建好索引,此时索引还在内存里面,接着去写TransLog,写完TransLog后,刷新TransLog数据到磁盘上,写磁盘成功后,请求返回给用户。这里有几个关键点: * 一是和数据库不同,数据库是先写CommitLog,然后再写内存,而Elasticsearch是先写内存,最后才写TransLog,一种可能的原因是Lucene的内存写入会有很复杂的逻辑,很容易失败,比如分词,字段长度超过限制等,比较重,为了避免TransLog中有大量无效记录,减少recover的复杂度和提高速度,所以就把写Lucene放在了最前面。 * 二是写Lucene内存后,并不是可被搜索的,需要通过Refresh把内存的对象转成完整的Segment后,然后再次reopen后才能被搜索,一般这个时间设置为1秒钟,导致写入Elasticsearch的文档,最快要1秒钟才可被从搜索到,所以Elasticsearch在搜索方面是NRT(Near Real Time)近实时的系统。 * 三是当Elasticsearch作为NoSQL数据库时,查询方式是GetById,这种查询可以直接从TransLog中查询,这时候就成了RT(Real Time)实时系统。四是每隔一段比较长的时间,比如30分钟后,Lucene会把内存中生成的新Segment刷新到磁盘上,刷新后索引文件已经持久化了,历史的TransLog就没用了,会清空掉旧的TransLog。 上面介绍了Elasticsearch在写入时的两个关键模块,Replica和TransLog,接下来,我们看一下Update流程: ![lmue7yfq.png](http://flt-pan.58heshihu.com/blog/typecho/lmue7yfq.png) Lucene中不支持部分字段的Update,所以需要在Elasticsearch中实现该功能,具体流程如下: * 收到Update请求后,从Segment或者TransLog中读取同id的完整Doc,记录版本号为V1。 * 将版本V1的全量Doc和请求中的部分字段Doc合并为一个完整的Doc,同时更新内存中的VersionMap。获取到完整Doc后,Update请求就变成了Index请求。加锁。 * 再次从versionMap中读取该id的最大版本号V2,如果versionMap中没有,则从Segment或者TransLog中读取,这里基本都会从versionMap中获取到。 * 检查版本是否冲突(V1==V2),如果冲突,则回退到开始的“Update doc”阶段,重新执行。如果不冲突,则执行最新的Add请求。 * 在Index Doc阶段,首先将Version + 1得到V3,再将Doc加入到Lucene中去,Lucene中会先删同id下的已存在doc id,然后再增加新Doc。写入Lucene成功后,将当前V3更新到versionMap中。 * 释放锁,部分更新的流程就结束了。 介绍完部分更新的流程后,大家应该从整体架构上对Elasticsearch的写入有了一个初步的映象,接下来我们详细剖析下写入的详细步骤。 ###### Elasticsearch写入请求类型 Elasticsearch中的写入请求类型,主要包括下列几个:Index(Create),Update,Delete和Bulk,其中前3个是单文档操作,后一个Bulk是多文档操作,其中Bulk中可以包括Index(Create),Update和Delete。 在6.0.0及其之后的版本中,前3个单文档操作的实现基本都和Bulk操作一致,甚至有些就是通过调用Bulk的接口实现的。估计接下来几个版本后,Index(Create),Update,Delete都会被当做Bulk的一种特例化操作被处理。这样,代码和逻辑都会更清晰一些。 下面,我们就以Bulk请求为例来介绍写入流程。 ![lmue8scj.png](http://flt-pan.58heshihu.com/blog/typecho/lmue8scj.png) * 红色:Client Node。 * 绿色:Primary Node。 * 蓝色:Replica Node。 ###### Client Node Client Node 也包括了前面说过的Parse Request,这里就不再赘述了,接下来看一下其他的部分。 1.Ingest Pipeline 在这一步可以对原始文档做一些处理,比如HTML解析,自定义的处理,具体处理逻辑可以通过插件来实现。在Elasticsearch中,由于Ingest Pipeline会比较耗费CPU等资源,可以设置专门的Ingest Node,专门用来处理Ingest Pipeline逻辑。 如果当前Node不能执行Ingest Pipeline,则会将请求发给另一台可以执行Ingest Pipeline的Node。 2.Auto Create Index 判断当前Index是否存在,如果不存在,则需要自动创建Index,这里需要和Master交互。也可以通过配置关闭自动创建Index的功能。 3.Set Routing 设置路由条件,如果Request中指定了路由条件,则直接使用Request中的Routing,否则使用Mapping中配置的,如果Mapping中无配置,则使用默认的_id字段值。 在这一步中,如果没有指定id字段,则会自动生成一个唯一的_id字段,目前使用的是UUID。 4.Construct BulkShardRequest 由于Bulk Request中会包括多个(Index/Update/Delete)请求,这些请求根据routing可能会落在多个Shard上执行,这一步会按Shard挑拣Single Write Request,同一个Shard中的请求聚集在一起,构建BulkShardRequest,每个BulkShardRequest对应一个Shard。 5.Send Request To Primary 这一步会将每一个BulkShardRequest请求发送给相应Shard的Primary Node。 ###### Primary Node Primary 请求的入口是在PrimaryOperationTransportHandler的messageReceived,我们来看一下相关的逻辑流程。 1.Index or Update or Delete 循环执行每个Single Write Request,对于每个Request,根据操作类型(CREATE/INDEX/UPDATE/DELETE)选择不同的处理逻辑。 其中,Create/Index是直接新增Doc,Delete是直接根据_id删除Doc,Update会稍微复杂些,我们下面就以Update为例来介绍。 2.Translate Update To Index or Delete 这一步是Update操作的特有步骤,在这里,会将Update请求转换为Index或者Delete请求。首先,会通过GetRequest查询到已经存在的同`_id Doc`(如果有)的完整字段和值(依赖_source字段),然后和请求中的Doc合并。同时,这里会获取到读到的Doc版本号,记做V1。 3.Parse Doc 这里会解析Doc中各个字段。生成ParsedDocument对象,同时会生成uid Term。在Elasticsearch中,`_uid = type # _id`,对用户,`_Id`可见,而Elasticsearch中存储的是`_uid`。这一部分生成的ParsedDocument中也有Elasticsearch的系统字段,大部分会根据当前内容填充,部分未知的会在后面继续填充ParsedDocument。 4.Update Mapping Elasticsearch中有个自动更新Mapping的功能,就在这一步生效。会先挑选出Mapping中未包含的新Field,然后判断是否运行自动更新Mapping,如果允许,则更新Mapping。 5.Get Sequence Id and Version 由于当前是Primary Shard,则会从SequenceNumber Service获取一个sequenceID和Version。SequenceID在Shard级别每次递增1,SequenceID在写入Doc成功后,会用来初始化LocalCheckpoint。Version则是根据当前Doc的最大Version递增1。 6.Add Doc To Lucene 这一步开始的时候会给特定\_uid加锁,然后判断该\_uid对应的Version是否等于之前Translate Update To Index步骤里获取到的Version,如果不相等,则说明刚才读取Doc后,该Doc发生了变化,出现了版本冲突,这时候会抛出一个VersionConflict的异常,该异常会在Primary Node最开始处捕获,重新从“Translate Update To Index or Delete”开始执行。 如果Version相等,则继续执行,如果已经存在同id的Doc,则会调用Lucene的UpdateDocument(uid, doc)接口,先根据uid删除Doc,然后再Index新Doc。如果是首次写入,则直接调用Lucene的AddDocument接口完成Doc的Index,AddDocument也是通过UpdateDocument实现。 这一步中有个问题是,如何保证Delete-Then-Add的原子性,怎么避免中间状态时被Refresh?答案是在开始Delete之前,会加一个Refresh Lock,禁止被Refresh,只有等Add完后释放了Refresh Lock后才能被Refresh,这样就保证了Delete-Then-Add的原子性。 Lucene的UpdateDocument接口中就只是处理多个Field,会遍历每个Field逐个处理,处理顺序是invert index,store field,doc values,point dimension,后续会有文章专门介绍Lucene中的写入。 7.Write Translog 写完Lucene的Segment后,会以keyvalue的形式写TransLog,Key是\_id,Value是Doc内容。当查询的时候,如果请求是GetDocByID,则可以直接根据\_id从TransLog中读取到,满足NoSQL场景下的实时性要去。 需要注意的是,这里只是写入到内存的TransLog,是否Sync到磁盘的逻辑还在后面。 这一步的最后,会标记当前SequenceID已经成功执行,接着会更新当前Shard的LocalCheckPoint。 8.Renew Bulk Request 这里会重新构造Bulk Request,原因是前面已经将UpdateRequest翻译成了Index或Delete请求,则后续所有Replica中只需要执行Index或Delete请求就可以了,不需要再执行Update逻辑,一是保证Replica中逻辑更简单,性能更好,二是保证同一个请求在Primary和Replica中的执行结果一样。 9.Flush Translog 这里会根据TransLog的策略,选择不同的执行方式,要么是立即Flush到磁盘,要么是等到以后再Flush。Flush的频率越高,可靠性越高,对写入性能影响越大。 10.Send Requests To Replicas 这里会将刚才构造的新的Bulk Request并行发送给多个Replica,然后等待Replica的返回,这里需要等待所有Replica返回后(可能有成功,也有可能失败),Primary Node才会返回用户。如果某个Replica失败了,则Primary会给Master发送一个Remove Shard请求,要求Master将该Replica Shard从可用节点中移除。 这里,同时会将SequenceID,PrimaryTerm,GlobalCheckPoint等传递给Replica。 发送给Replica的请求中,Action Name等于原始`ActionName + [R]`,这里的R表示Replica。通过这个`[R]`的不同,可以找到处理Replica请求的Handler。 11.Receive Response From Replicas Replica中请求都处理完后,会更新Primary Node的LocalCheckPoint。 ###### Replica Node Replica 请求的入口是在ReplicaOperationTransportHandler的messageReceived,我们来看一下相关的逻辑流程。 1.Index or Delete 根据请求类型是Index还是Delete,选择不同的执行逻辑。这里没有Update,是因为在Primary Node中已经将Update转换成了Index或Delete请求了。 2.Parse Doc 3.Update Mapping 以上都和Primary Node中逻辑一致。 4.Get Sequence Id and Version Primary Node中会生成Sequence ID和Version,然后放入ReplicaRequest中,这里只需要从Request中获取到就行。 5.Add Doc To Lucene 由于已经在Primary Node中将部分Update请求转换成了Index或Delete请求,这里只需要处理Index和Delete两种请求,不再需要处理Update请求了。比Primary Node会更简单一些。 6.Write Translog 7.Flush Translog 以上都和Primary Node中逻辑一致。 #### 最后 上面详细介绍了Elasticsearch的写入流程及其各个流程的工作机制,我们在这里再次总结下之前提出的分布式系统中的六大特性: * 可靠性:由于Lucene的设计中不考虑可靠性,在Elasticsearch中通过Replica和TransLog两套机制保证数据的可靠性。 * 一致性:Lucene中的Flush锁只保证Update接口里面Delete和Add中间不会Flush,但是Add完成后仍然有可能立即发生Flush,导致Segment可读。这样就没法保证Primary和所有其他Replica可以同一时间Flush,就会出现查询不稳定的情况,这里只能实现最终一致性。 * 原子性:Add和Delete都是直接调用Lucene的接口,是原子的。当部分更新时,使用Version和锁保证更新是原子的。 * 隔离性:仍然采用Version和局部锁来保证更新的是特定版本的数据。 * 实时性:使用定期Refresh Segment到内存,并且Reopen Segment方式保证搜索可以在较短时间(比如1秒)内被搜索到。通过将未刷新到磁盘数据记入TransLog,保证对未提交数据可以通过ID实时访问到。 * 性能:性能是一个系统性工程,所有环节都要考虑对性能的影响,在Elasticsearch中,在很多地方的设计都考虑到了性能,一是不需要所有Replica都返回后才能返回给用户,只需要返回特定数目的就行;二是生成的Segment现在内存中提供服务,等一段时间后才刷新到磁盘,Segment在内存这段时间的可靠性由TransLog保证;三是TransLog可以配置为周期性的Flush,但这个会给可靠性带来伤害;四是每个线程持有一个Segment,多线程时相互不影响,相互独立,性能更好;五是系统的写入流程对版本依赖较重,读取频率较高,因此采用了versionMap,减少热点数据的多次磁盘IO开销。Lucene中针对性能做了大量的优化。 前文介绍了索引文档流程,本文带你理解 ES 文档的读取过程。 ### 读取文档流程 #### 文档查询步骤顺序 先看下整体的查询流程 ###### 单个文档 以下是从主分片或者副本分片检索文档的步骤顺序: ![lmue9r7o.png](http://flt-pan.58heshihu.com/blog/typecho/lmue9r7o.png) * 客户端向 Node 1 发送获取请求。 * 节点使用文档的 _id 来确定文档属于分片 0 。分片 0 的副本分片存在于所有的三个节点上。在这种情况下,它将请求转发到 Node 2 。 * Node 2 将文档返回给 Node 1 ,然后将文档返回给客户端。 在处理读取请求时,协调结点在每次请求的时候都会通过轮询所有的副本分片来达到负载均衡。 在文档被检索时,已经被索引的文档可能已经存在于主分片上但是还没有复制到副本分片。在这种情况下,副本分片可能会报告文档不存在,但是主分片可能成功返回文档。一旦索引请求成功返回给用户,文档在主分片和副本分片都是可用的。 ###### 多个文档 使用 mget 取回多个文档的步骤顺序: ![lmuea38y.png](http://flt-pan.58heshihu.com/blog/typecho/lmuea38y.png) 以下是使用单个 mget 请求取回多个文档所需的步骤顺序: * 客户端向 Node 1 发送 mget 请求。 * Node 1 为每个分片构建多文档获取请求,然后并行转发这些请求到托管在每个所需的主分片或者副本分片的节点上。一旦收到所有答复, Node 1 构建响应并将其返回给客户端。 #### 文档读取过程详解 所有的搜索系统一般都是两阶段查询,第一阶段查询到匹配的DocID,第二阶段再查询DocID对应的完整文档,这种在Elasticsearch中称为query\_then\_fetch。 ![lmueahom.png](http://flt-pan.58heshihu.com/blog/typecho/lmueahom.png) * 在初始查询阶段时,查询会广播到索引中每一个分片拷贝(主分片或者副本分片)。每个分片在本地执行搜索并构建一个匹配文档的大小为 from + size 的优先队列。PS:在2. 搜索的时候是会查询Filesystem Cache的,但是有部分数据还在Memory Buffer,所以搜索是近实时的。 * 每个分片返回各自优先队列中 所有文档的 ID 和排序值 给协调节点,它合并这些值到自己的优先队列中来产生一个全局排序后的结果列表。 * 接下来就是 取回阶段,协调节点辨别出哪些文档需要被取回并向相关的分片提交多个 GET 请求。每个分片加载并丰富文档,如果有需要的话,接着返回文档给协调节点。一旦所有的文档都被取回了,协调节点返回结果给客户端。 #### 深入ElasticSearch读取文档的实现机制 ###### 读操作 一致性指的是写入成功后,下次读操作一定要能读取到最新的数据。对于搜索,这个要求会低一些,可以有一些延迟。但是对于NoSQL数据库,则一般要求最好是强一致性的。 结果匹配上,NoSQL作为数据库,查询过程中只有符合不符合两种情况,而搜索里面还有是否相关,类似于NoSQL的结果只能是0或1,而搜索里面可能会有0.1,0.5,0.9等部分匹配或者更相关的情况。 结果召回上,搜索一般只需要召回最满足条件的Top N结果即可,而NoSQL一般都需要返回满足条件的所有结果。 搜索系统一般都是两阶段查询,第一个阶段查询到对应的Doc ID,也就是PK;第二阶段再通过Doc ID去查询完整文档,而NoSQL数据库一般是一阶段就返回结果。在Elasticsearch中两种都支持。 目前NoSQL的查询,聚合、分析和统计等功能上都是要比搜索弱的。 ###### Lucene的读 Elasticsearch使用了Lucene作为搜索引擎库,通过Lucene完成特定字段的搜索等功能,在Lucene中这个功能是通过IndexSearcher的下列接口实现的: ``` public TopDocs search(Query query, int n); public Document doc(int docID); public int count(Query query); ......(其他) ``` 第一个search接口实现搜索功能,返回最满足Query的N个结果;第二个doc接口通过doc id查询Doc内容;第三个count接口通过Query获取到命中数。 这三个功能是搜索中的最基本的三个功能点,对于大部分Elasticsearch中的查询都是比较复杂的,直接用这个接口是无法满足需求的,比如分布式问题。这些问题都留给了Elasticsearch解决,我们接下来看Elasticsearch中相关读功能的剖析。 ###### Elasticsearch的读 Elasticsearch中每个Shard都会有多个Replica,主要是为了保证数据可靠性,除此之外,还可以增加读能力,因为写的时候虽然要写大部分Replica Shard,但是查询的时候只需要查询Primary和Replica中的任何一个就可以了。 ![lmueb7zn.png](http://flt-pan.58heshihu.com/blog/typecho/lmueb7zn.png) 在上图中,该Shard有1个Primary和2个Replica Node,当查询的时候,从三个节点中根据Request中的preference参数选择一个节点查询。preference可以设置`_local`,`_primary`,`_replica`以及其他选项。如果选择了primary,则每次查询都是直接查询Primary,可以保证每次查询都是最新的。如果设置了其他参数,那么可能会查询到R1或者R2,这时候就有可能查询不到最新的数据。 PS: 上述代码逻辑在 OperationRouting.Java 的 searchShards 方法中。 接下来看一下,Elasticsearch中的查询是如何支持分布式的。 ![lmuebpd8.png](http://flt-pan.58heshihu.com/blog/typecho/lmuebpd8.png) Elasticsearch中通过分区实现分布式,数据写入的时候根据_routing规则将数据写入某一个Shard中,这样就能将海量数据分布在多个Shard以及多台机器上,已达到分布式的目标。这样就导致了查询的时候,潜在数据会在当前index的所有的Shard中,所以Elasticsearch查询的时候需要查询所有Shard,同一个Shard的Primary和Replica选择一个即可,查询请求会分发给所有Shard,每个Shard中都是一个独立的查询引擎,比如需要返回Top 10的结果,那么每个Shard都会查询并且返回Top 10的结果,然后在Client Node里面会接收所有Shard的结果,然后通过优先级队列二次排序,选择出Top 10的结果返回给用户。 这里有一个问题就是请求膨胀,用户的一个搜索请求在Elasticsearch内部会变成Shard个请求,这里有个优化点,虽然是Shard个请求,但是这个Shard个数不一定要是当前Index中的Shard个数,只要是当前查询相关的Shard即可,这个需要基于业务和请求内容优化,通过这种方式可以优化请求膨胀数。 Elasticsearch中的查询主要分为两类,Get请求:通过ID查询特定Doc;Search请求:通过Query查询匹配Doc ![lmuebz4s.png](http://flt-pan.58heshihu.com/blog/typecho/lmuebz4s.png) PS:上图中内存中的Segment是指刚Refresh Segment,但是还没持久化到磁盘的新Segment,而非从磁盘加载到内存中的Segment。 对于Search类请求,查询的时候是一起查询内存和磁盘上的Segment,最后将结果合并后返回。这种查询是近实时(Near Real Time)的,主要是由于内存中的Index数据需要一段时间后才会刷新为Segment。 对于Get类请求,查询的时候是先查询内存中的TransLog,如果找到就立即返回,如果没找到再查询磁盘上的TransLog,如果还没有则再去查询磁盘上的Segment。这种查询是实时(Real Time)的。这种查询顺序可以保证查询到的Doc是最新版本的Doc,这个功能也是为了保证NoSQL场景下的实时性要求 ![lmuecg7i.png](http://flt-pan.58heshihu.com/blog/typecho/lmuecg7i.png) 所有的搜索系统一般都是两阶段查询,第一阶段查询到匹配的DocID,第二阶段再查询DocID对应的完整文档,这种在Elasticsearch中称为query\_then\_fetch,还有一种是一阶段查询的时候就返回完整Doc,在Elasticsearch中称作query\_and\_fetch,一般第二种适用于只需要查询一个Shard的请求。 除了一阶段,两阶段外,还有一种三阶段查询的情况。搜索里面有一种算分逻辑是根据TF(Term Frequency)和DF(Document Frequency)计算基础分,但是Elasticsearch中查询的时候,是在每个Shard中独立查询的,每个Shard中的TF和DF也是独立的,虽然在写入的时候通过_routing保证Doc分布均匀,但是没法保证TF和DF均匀,那么就有会导致局部的TF和DF不准的情况出现,这个时候基于TF、DF的算分就不准。为了解决这个问题,Elasticsearch中引入了DFS查询,比如`DFS_query_then_fetch`,会先收集所有Shard中的TF和DF值,然后将这些值带入请求中,再次执行`query_then_fetch`,这样算分的时候TF和DF就是准确的,类似的有`DFS_query_and_fetch`。这种查询的优势是算分更加精准,但是效率会变差。另一种选择是用BM25代替TF/DF模型。 在新版本Elasticsearch中,用户没法指定`DFS_query_and_fetch`和`query_and_fetch`,这两种只能被Elasticsearch系统改写。 #### Elasticsearch查询流程 Elasticsearch中的大部分查询,以及核心功能都是Search类型查询,上面我们了解到查询分为一阶段,二阶段和三阶段,这里我们就以最常见的的二阶段查询为例来介绍查询流程。 ###### Client Node ![lmuecvgm.png](http://flt-pan.58heshihu.com/blog/typecho/lmuecvgm.png) Client Node 也包括了前面说过的Parse Request,这里就不再赘述了,接下来看一下其他的部分。 1.Get Remove Cluster Shard 判断是否需要跨集群访问,如果需要,则获取到要访问的Shard列表。 2.Get Search Shard Iterator 获取当前Cluster中要访问的Shard,和上一步中的Remove Cluster Shard合并,构建出最终要访问的完整Shard列表。 这一步中,会根据Request请求中的参数从Primary Node和多个Replica Node中选择出一个要访问的Shard。 3.For Every Shard:Perform 遍历每个Shard,对每个Shard执行后面逻辑。 4.Send Request To Query Shard 将查询阶段请求发送给相应的Shard。 5.Merge Docs 上一步将请求发送给多个Shard后,这一步就是异步等待返回结果,然后对结果合并。这里的合并策略是维护一个Top N大小的优先级队列,每当收到一个shard的返回,就把结果放入优先级队列做一次排序,直到所有的Shard都返回。 翻页逻辑也是在这里,如果需要取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的结果。 上述翻页逻辑有一个明显的缺点就是每次Shard返回的数据中包括了已经翻过的历史结果,如果翻页很深,则在这里需要排序的Docs会很多,比如Shard有1000,取第9990到10000的结果,那么这次查询,Shard总共需要返回1000 * 10000,也就是一千万Doc,这种情况很容易导致OOM。 另一种翻页方式是使用search\_after,这种方式会更轻量级,如果每次只需要返回10条结构,则每个Shard只需要返回search\_after之后的10个结果即可,返回的总数据量只是和Shard个数以及本次需要的个数有关,和历史已读取的个数无关。这种方式更安全一些,推荐使用这种。 如果有aggregate,也会在这里做聚合,但是不同的aggregate类型的merge策略不一样,具体的可以在后面的aggregate文章中再介绍。 6.Send Request To Fetch Shard 选出Top N个Doc ID后发送给这些Doc ID所在的Shard执行Fetch Phase,最后会返回Top N的Doc的内容。 ###### Query Phase 接下来我们看第一阶段查询的步骤: 1.Create Search Context 创建Search Context,之后Search过程中的所有中间状态都会存在Context中,这些状态总共有50多个,具体可以查看DefaultSearchContext或者其他SearchContext的子类。 2.Parse Query 解析Query的Source,将结果存入Search Context。这里会根据请求中Query类型的不同创建不同的Query对象,比如TermQuery、FuzzyQuery等,最终真正执行TermQuery、FuzzyQuery等语义的地方是在Lucene中。 这里包括了dfsPhase、queryPhase和fetchPhase三个阶段的preProcess部分,只有queryPhase的preProcess中有执行逻辑,其他两个都是空逻辑,执行完preProcess后,所有需要的参数都会设置完成。 由于Elasticsearch中有些请求之间是相互关联的,并非独立的,比如scroll请求,所以这里同时会设置Context的生命周期。 同时会设置lowLevelCancellation是否打开,这个参数是集群级别配置,同时也能动态开关,打开后会在后面执行时做更多的检测,检测是否需要停止后续逻辑直接返回。 3.Get From Cache 判断请求是否允许被Cache,如果允许,则检查Cache中是否已经有结果,如果有则直接读取Cache,如果没有则继续执行后续步骤,执行完后,再将结果加入Cache。 4.Add Collectors Collector主要目标是收集查询结果,实现排序,对自定义结果集过滤和收集等。这一步会增加多个Collectors,多个Collector组成一个List。 * FilteredCollector:先判断请求中是否有Post Filter,Post Filter用于Search,Agg等结束后再次对结果做Filter,希望Filter不影响Agg结果。如果有Post Filter则创建一个FilteredCollector,加入Collector List中。 * PluginInMultiCollector:判断请求中是否制定了自定义的一些Collector,如果有,则创建后加入Collector List。 * MinimumScoreCollector:判断请求中是否制定了最小分数阈值,如果指定了,则创建MinimumScoreCollector加入Collector List中,在后续收集结果时,会过滤掉得分小于最小分数的Doc。 * EarlyTerminatingCollector:判断请求中是否提前结束Doc的Seek,如果是则创建EarlyTerminatingCollector,加入Collector List中。在后续Seek和收集Doc的过程中,当Seek的Doc数达到Early Terminating后会停止Seek后续倒排链。 * CancellableCollector:判断当前操作是否可以被中断结束,比如是否已经超时等,如果是会抛出一个TaskCancelledException异常。该功能一般用来提前结束较长的查询请求,可以用来保护系统。 * EarlyTerminatingSortingCollector:如果Index是排序的,那么可以提前结束对倒排链的Seek,相当于在一个排序递减链表上返回最大的N个值,只需要直接返回前N个值就可以了。这个Collector会加到Collector List的头部。EarlyTerminatingSorting和EarlyTerminating的区别是,EarlyTerminatingSorting是一种对结果无损伤的优化,而EarlyTerminating是有损的,人为掐断执行的优化。 * TopDocsCollector:这个是最核心的Top N结果选择器,会加入到Collector List的头部。TopScoreDocCollector和TopFieldCollector都是TopDocsCollector的子类,TopScoreDocCollector会按照固定的方式算分,排序会按照分数+doc id的方式排列,如果多个doc的分数一样,先选择doc id小的文档。而TopFieldCollector则是根据用户指定的Field的值排序。 5.lucene::search 这一步会调用Lucene中IndexSearch的search接口,执行真正的搜索逻辑。每个Shard中会有多个Segment,每个Segment对应一个LeafReaderContext,这里会遍历每个Segment,到每个Segment中去Search结果,然后计算分数。 搜索里面一般有两阶段算分,第一阶段是在这里算的,会对每个Seek到的Doc都计算分数,为了减少CPU消耗,一般是算一个基本分数。这一阶段完成后,会有个排序。然后在第二阶段,再对Top 的结果做一次二阶段算分,在二阶段算分的时候会考虑更多的因子。二阶段算分在后续操作中。 具体请求,比如TermQuery、WildcardQuery的查询逻辑都在Lucene中,后面会有专门文章介绍。 6.rescore 根据Request中是否包含rescore配置决定是否进行二阶段排序,如果有则执行二阶段算分逻辑,会考虑更多的算分因子。二阶段算分也是一种计算机中常见的多层设计,是一种资源消耗和效率的折中。 Elasticsearch中支持配置多个Rescore,这些rescore逻辑会顺序遍历执行。每个rescore内部会先按照请求参数window选择出Top window的doc,然后对这些doc排序,排完后再合并回原有的Top 结果顺序中。 7.suggest::execute() 如果有推荐请求,则在这里执行推荐请求。如果请求中只包含了推荐的部分,则很多地方可以优化。推荐不是今天的重点,这里就不介绍了,后面有机会再介绍。 8.aggregation::execute() 如果含有聚合统计请求,则在这里执行。Elasticsearch中的aggregate的处理逻辑也类似于Search,通过多个Collector来实现。在Client Node中也需要对aggregation做合并。aggregate逻辑更复杂一些,就不在这里赘述了,后面有需要就再单独开文章介绍。 上述逻辑都执行完成后,如果当前查询请求只需要查询一个Shard,那么会直接在当前Node执行Fetch Phase。 ###### Fetch Phase Elasticsearch作为搜索系统时,或者任何搜索系统中,除了Query阶段外,还会有一个Fetch阶段,这个Fetch阶段在数据库类系统中是没有的,是搜索系统中额外增加的阶段。搜索系统中额外增加Fetch阶段的原因是搜索系统中数据分布导致的,在搜索中,数据通过routing分Shard的时候,只能根据一个主字段值来决定,但是查询的时候可能会根据其他非主字段查询,那么这个时候所有Shard中都可能会存在相同非主字段值的Doc,所以需要查询所有Shard才能不会出现结果遗漏。同时如果查询主字段,那么这个时候就能直接定位到Shard,就只需要查询特定Shard即可,这个时候就类似于数据库系统了。另外,数据库中的二级索引又是另外一种情况,但类似于查主字段的情况,这里就不多说了。 基于上述原因,第一阶段查询的时候并不知道最终结果会在哪个Shard上,所以每个Shard中管都需要查询完整结果,比如需要Top 10,那么每个Shard都需要查询当前Shard的所有数据,找出当前Shard的Top 10,然后返回给Client Node。如果有100个Shard,那么就需要返回100 * 10 = 1000个结果,而Fetch Doc内容的操作比较耗费IO和CPU,如果在第一阶段就Fetch Doc,那么这个资源开销就会非常大。所以,一般是当Client Node选择出最终Top N的结果后,再对最终的Top N读取Doc内容。通过增加一点网络开销而避免大量IO和CPU操作,这个折中是非常划算的。 Fetch阶段的目的是通过DocID获取到用户需要的完整Doc内容。这些内容包括了DocValues,Store,Source,Script和Highlight等,具体的功能点是在SearchModule中注册的,系统默认注册的有: * ExplainFetchSubPhase * DocValueFieldsFetchSubPhase * ScriptFieldsFetchSubPhase * FetchSourceSubPhase * VersionFetchSubPhase * MatchedQueriesFetchSubPhase * HighlightPhase * ParentFieldSubFetchPhase 除了系统默认的8种外,还有通过插件的形式注册自定义的功能,这些SubPhase中最重要的是Source和Highlight,Source是加载原文,Highlight是计算高亮显示的内容片断。 上述多个SubPhase会针对每个Doc顺序执行,可能会产生多次的随机IO,这里会有一些优化方案,但是都是针对特定场景的,不具有通用性。 Fetch Phase执行完后,整个查询流程就结束了。 > 来源: > https://www.pdai.tech/md/db/nosql-es/elasticsearch-y-th-3.html > https://www.pdai.tech/md/db/nosql-es/elasticsearch-y-th-4.html ------------- ## (八):ElasticSearch 集群部署、分片与故障转移 ### 相关概念 #### 单机 & 集群 * 单台 Elasticsearch 服务器提供服务,往往都有最大的负载能力,超过这个阈值,服务器性能就会大大降低甚至不可用,所以生产环境中,一般都是运行在指定服务器集群中。 * 除了负载能力,单点服务器也存在其他问题: * 单台机器存储容量有限 * 单服务器容易出现单点故障,无法实现高可用 * 单服务的并发处理能力有限 配置服务器集群时,集群中节点数量没有限制, **大于等于 2 个节点就可以看做是集群了。** 一般出于高性能及高可用方面来考虑集群中节点 **数量都是 3 个以上。** #### 集群 Cluster 一个集群就是由一个或多个服务器节点组织在一起,共同持有整个的数据,并一起提供索引和搜索功能。一个 Elasticsearch 集群有一个`唯一的名字标识`,这个名字默认就是”`elasticsearch`”。这个名字是重要的,因为一个节点只能通过指定某个集群的名字,来加入这个集群。 #### 节点 Node 集群中包含很多服务器,`一个节点就是其中的一个服务器`。作为集群的一部分,它存储数据,参与集群的索引和搜索功能。 一个节点也是由一个名字来标识的,默认情况下,`这个名字是一个随机的漫威漫画角色 的名字`,这个名字会在启动的时候赋予节点。这个名字对于管理工作来说挺重要的,因为在这个管理过程中,你会去确定网络中的哪些服务器对应于 Elasticsearch集群中的哪些节点。 一个节点可以通过配置集群名称的方式来加入一个指定的集群。默认情况下,每个节点都会被安排加入到一个叫做“elasticsearch”的集群中,这意味着,如果你在你的网络中启动了若干个节点,并假定它们能够相互发现彼此,它们将会自动地形成并加入到一个叫做 “elasticsearch”的集群中。 在一个集群里,只要你想,可以拥有任意多个节点。而且,如果当前你的网络中没有运行任何 Elasticsearch 节点,这时启动一个节点,会默认创建并加入一个叫做“elasticsearch”的集群。 #### 为什么要部署 Elasticsearch 集群 单机部署的 Elasticsearch 在做数据存储时会遇到存储数据上线和机器故障问题,因此对于 Elasticsearch 集群的部署是有必要的。搭建 Elasticsearch 集群,可以将创建的索引库拆分成多个分片(索引可以被拆分为不同的部分进行存储,称为分片。 在集群环境下,一个索引的不同分片可以拆分到不同的节点中),存储到不同的节点上,以此来解决海量数据存储问题;将分片上的数据分布在不同的节点上可以解决单点故障问题。 ### Elasticsearch集群职责 在Elasticsearch集群中,不同的节点可以承担不同的职责,例如: * Master节点:负责集群的管理和调度,包括分配和重新分配分片、节点的加入和退出、索引的创建和删除等。 * Data节点:负责存储数据和执行搜索请求,包括分片的读写、搜索请求的处理等。 * Ingest节点:负责对文档进行预处理,例如对文档进行解析、转换、过滤等操作。 * Coordinating节点:负责协调搜索请求,将请求转发给适当的Data节点进行处理,并将结果汇总返回给客户端。 在实际的生产环境中,可以根据集群的规模和负载情况来决定节点的职责划分。例如,在小型集群中,可以将所有节点都设置为Master节点和Data节点;在大型集群中,可以将一部分节点设置为Master节点,一部分节点设置为Data节点,同时还可以设置一些Coordinating节点和Ingest节点来协调搜索请求和处理文档预处理。 ### Windows 集群 #### 部署集群 创建 elasticsearch-cluster 文件夹,在内部复制三个 elasticsearch 服务 ![lmuei48b.png](http://flt-pan.58heshihu.com/blog/typecho/lmuei48b.png) 修改集群文件目录中每个节点的 `config/elasticsearch.yml` 配置文件 node-1001 节点 ``` #节点 1 的配置信息: #集群名称,节点之间要保持一致 cluster.name: my-elasticsearch #节点名称,集群内要唯一 node.name: node-1001 node.master: true node.data: true #ip 地址 network.host: localhost #http 端口 http.port: 1001 #tcp 监听端口 transport.tcp.port: 9301 #discovery.seed_hosts: ["localhost:9301", "localhost:9302","localhost:9303"] #discovery.zen.fd.ping_timeout: 1m #discovery.zen.fd.ping_retries: 5 #集群内的可以被选为主节点的节点列表 #cluster.initial_master_nodes: ["node-1", "node-2","node-3"] #跨域配置 #action.destructive_requires_name: true http.cors.enabled: true http.cors.allow-origin: "*" ``` node-1002 节点 ``` #节点 2 的配置信息: #集群名称,节点之间要保持一致 cluster.name: my-elasticsearch #节点名称,集群内要唯一 node.name: node-1002 node.master: true node.data: true #ip 地址 network.host: localhost #http 端口 http.port: 1002 #tcp 监听端口 transport.tcp.port: 9302 discovery.seed_hosts: ["localhost:9301"] discovery.zen.fd.ping_timeout: 1m discovery.zen.fd.ping_retries: 5 #集群内的可以被选为主节点的节点列表 #cluster.initial_master_nodes: ["node-1", "node-2","node-3"] #跨域配置 #action.destructive_requires_name: true http.cors.enabled: true http.cors.allow-origin: "*" ``` node-1003 节点 ``` #节点 3 的配置信息: #集群名称,节点之间要保持一致 cluster.name: my-elasticsearch #节点名称,集群内要唯一 node.name: node-1003 node.master: true node.data: true #ip 地址 network.host: localhost #http 端口 http.port: 1003 #tcp 监听端口 transport.tcp.port: 9303 #候选主节点的地址,在开启服务后可以被选为主节点 discovery.seed_hosts: ["localhost:9301", "localhost:9302"] discovery.zen.fd.ping_timeout: 1m discovery.zen.fd.ping_retries: 5 #集群内的可以被选为主节点的节点列表 #cluster.initial_master_nodes: ["node-1", "node-2","node-3"] #跨域配置 #action.destructive_requires_name: true http.cors.enabled: true http.cors.allow-origin: "*" ``` #### 启动集群 启动前先删除每个节点中的 data 目录中所有内容(如果存在) ![lmueih45.png](http://flt-pan.58heshihu.com/blog/typecho/lmueih45.png) 分别双击执行 `bin/elasticsearch.bat`, 启动节点服务器,启动后,会自动加入指定名称的集群 #### 测试集群 查看集群状态 * node-1001 节点 ![lmueivgu.png](http://flt-pan.58heshihu.com/blog/typecho/lmueivgu.png) * node-1002 节点 ![lmuej1vw.png](http://flt-pan.58heshihu.com/blog/typecho/lmuej1vw.png) * node-1003 节点 ![lmueot70.png](http://flt-pan.58heshihu.com/blog/typecho/lmueot70.png) 向集群中的 `node-1001` 节点增加索引 ![lmuep1ps.png](http://flt-pan.58heshihu.com/blog/typecho/lmuep1ps.png) 向集群中的 `node-1002` 节点查询索引 ![lmuep8o8.png](http://flt-pan.58heshihu.com/blog/typecho/lmuep8o8.png) ### Linux 集群 编写内容如下的docker-compose文件,将其上传到Linux的/root目录下: ``` version: '2.2' services: es01: image: elasticsearch:7.12.1 container_name: es01 environment: - node.name=es01 - cluster.name=es-docker-cluster # 集群名称相同 - discovery.seed_hosts=es02,es03 # 可以发现的其他节点 - cluster.initial_master_nodes=es01,es02,es03 # 可以选举为主节点 - "ES_JAVA_OPTS=-Xms512m -Xmx512m" volumes: - data01:/usr/share/elasticsearch/data # 数据卷 ports: - 9200:9200 # 容器内外端口映射 networks: - elastic es02: image: elasticsearch:7.12.1 container_name: es02 environment: - node.name=es02 - cluster.name=es-docker-cluster - discovery.seed_hosts=es01,es03 - cluster.initial_master_nodes=es01,es02,es03 - "ES_JAVA_OPTS=-Xms512m -Xmx512m" volumes: - data02:/usr/share/elasticsearch/data ports: - 9201:9200 networks: - elastic es03: image: elasticsearch:7.12.1 container_name: es03 environment: - node.name=es03 - cluster.name=es-docker-cluster - discovery.seed_hosts=es01,es02 - cluster.initial_master_nodes=es01,es02,es03 - "ES_JAVA_OPTS=-Xms512m -Xmx512m" volumes: - data03:/usr/share/elasticsearch/data networks: - elastic ports: - 9202:9200 volumes: data01: driver: local data02: driver: local data03: driver: local networks: elastic: driver: bridge ``` es运行需要修改一些linux系统权限,进入并修改`/etc/sysctl.conf`文件 ``` vi /etc/sysctl.conf ``` 在文件中添加下面的内容: ``` vm.max_map_count=262144 ``` 然后执行命令,让配置生效: ``` sysctl -p ``` 通过docker-compose启动集群: ``` docker-compose up -d ``` 启动完成后,使用docker查看运行的容器,可以看到已启动Elasticsearch集群: ![lmuepshg.png](http://flt-pan.58heshihu.com/blog/typecho/lmuepshg.png) ### Elasticsearch集群健康状态 Elasticsearch集群的健康状态可以通过以下命令或API来查看: #### 命令行方式: 可以使用`curl`命令或者`httpie`命令来访问Elasticsearch的API来获取集群健康状态,例如: ``` curl -X GET "localhost:9200/_cat/health?v" ``` 或者 ``` http GET localhost:9200/_cat/health?v ``` 其中,`localhost:9200`是Elasticsearch的地址和端口号,`_cat/health`是API的路径,`v`表示显示详细信息。执行以上命令后,会返回如下信息: ``` epoch timestamp cluster status node.total node.data shards pri relo init unassign pending_tasks max_task_wait_time active_shards_percent 1578318307 02:38:27 elasticsearch green 1 1 6 3 0 0 0 0 - 100.0% ``` 其中,`status`字段表示集群的健康状态,有以下几种取值: * green:所有主分片和副本分片都正常分配到节点上。 * yellow:所有主分片都正常分配到节点上,但是有一些副本分片还没有分配到节点上。 * red:有一些主分片没有分配到节点上,导致数据不可用。 #### API方式: 可以使用Elasticsearch的API来获取集群健康状态,例如: ``` GET /_cluster/health ``` 执行以上命令后,会返回如下信息: ``` { "cluster_name" : "my_cluster", "status" : "green", "timed_out" : false, "number_of_nodes" : 1, "number_of_data_nodes" : 1, "active_primary_shards" : 6, "active_shards" : 6, "relocating_shards" : 0, "initializing_shards" : 0, "unassigned_shards" : 0, "delayed_unassigned_shards" : 0, "number_of_pending_tasks" : 0, "number_of_in_flight_fetch" : 0, "task_max_waiting_in_queue_millis" : 0, "active_shards_percent_as_number" : 100.0 } ``` 其中,`status`字段表示集群的健康状态,其他字段的含义和命令行方式相同。 ### Elasticsearch集群分片 Elasticsearch 集群中的数据被分成多个分片(shard),每个分片是一个独立的Lucene索引。分片可以在集群中的不同节点上分布,以提高搜索和写入性能。分片有两种类型:主分片(primary shard)和副本分片(replica shard)。 主分片是每个文档的主要存储位置,每个主分片都有一个唯一的标识符,并且只能在一个节点上存在。当一个文档被索引时,它被路由到一个主分片,然后被写入该分片的Lucene索引。 副本分片是主分片的拷贝,它们可以在不同的节点上存在。副本分片的数量可以在索引创建时指定,它们可以提高搜索性能和可用性。当一个主分片不可用时,副本分片可以被用来提供搜索结果。副本分片也可以用来平衡负载,因为它们可以被用来处理读取请求。 在 Elasticsearch 集群中,**分片的数量和副本的数量可以通过索引的设置进行配置。**通常,主分片的数量应该小于或等于集群中的节点数,以确保每个节点都有主分片。副本分片的数量应该根据集群的负载和可用性需求进行配置。 当索引创建完成的时候,主分片的数量就固定了,但是复制分片的数量可以随时调整。 ### Elasticsearch故障转移 集群的 **master 节点会监控集群中的所有节点的状态**,一旦发现有节点宕机,就会立即将宕机的节点分片的数据迁移到其他节点上,以此来保证数据安全,这个流程叫故障转移。与此同时剩余节点中会重新选举主节点,当原来的主节点恢复正常时,原来迁移到其他节点上面的分片会被迁移到恢复的节点上,但此时原来的主节点不再是主节点(哥不再是当年的哥)。 #### 总结 Elasticsearch 故障转移的实现主要依赖于以下两个机制: * 分片复制机制:Elasticsearch将索引分为多个分片,每个分片都有多个副本,分布在不同的节点上。当一个节点发生故障时,其他节点上的副本可以接管该分片的工作,保证数据的可用性。 * 主从复制机制:Elasticsearch集群中的每个分片都有一个主节点和多个从节点。当主节点宕机时,从节点会自动选举一个新的主节点,以继续处理该分片的请求。 在实际应用中,为了进一步提高Elasticsearch集群的可用性和稳定性,可以采用以下措施: * 配置多个节点:将Elasticsearch集群部署在多个节点上,以分散风险,避免单点故障。 * 监控节点状态:使用监控工具对Elasticsearch节点进行实时监控,及时发现并处理故障。 * 自动化运维:使用自动化运维工具对Elasticsearch集群进行管理和维护,减少人为操作的错误和风险。 * 定期备份数据:定期备份Elasticsearch集群中的数据,以防止数据丢失和损坏,保证数据的可恢复性。 > 来源: > https://blog.csdn.net/qq_53847859/article/details/130278452 > https://blog.csdn.net/qq_44732146/article/details/120723894 ------------- ## (九):ElasticSearch 集群规划与运维经验总结 ### ES 集群容量及索引规划 #### 集群规模评估 ###### 评估什么 * 计算资源的CPU和内存 * 存储资源的类型及容量 * 节点数量 ###### 根据什么评估 * 业务场景:日志分析、指标监控、网站搜索 * 查询及写入QPS * 索引数据总量 ###### 集群规模评估准则 * 32C64G单节点配置通常可承载5W次/s的写入; * 写入量和数据量较大时,优先选择32C64G的节点配置; * 1T的数据量预计需消耗2-4GB的内存空间; * 实际存储空间通常为原始数据量2.8倍(1副本) * 搜索场景优先选择大内存节点配置 #### 索引配置评估 ###### 评估什么 * 怎么划分索引 * 索引的分片数如何设置 ###### 根据什么评估 * 业务场景:日志分析、指标监控、网站搜索 * 单日新增的数据量 ###### 索引配置评估准则 * 单个分片大小控制在30-50GB * 集群总分片数量控制在3w以内 * 1GB的内存空间支持20-30个分片为佳 * 一个节点建议不超过1000个分片 * 索引分片数量建议和节点数量保持一致 * 集群规模较大时建议设置专用主节点 * 专用主节点配置建议在8C16G以上 * 如果是时序数据,建议结合ILM索引生命周期管理 ### ES 集群写入性能优化 * 写入性能优化 * 写入数据不指定doc_id,让 ES 自动生成 * 使用自定义 routing 功能,尽量将请求转发到较少的分片 * 对于规模较大的集群,建议提前创建好索引,且使用固定的 Index mapping * 对于数据实时性要求不高的场景,可以将索引的 refresh_interval 设置为 30s * 对于追求写入效率的场景,可以将正在写入的索引设置为单副本,写入完成后打开副本 * 使用 bulk 接口批量写入数据,每次 bulk 数据量大小控制在 10M 左右 * 尽量选择 SSD 磁盘类型,并且可选择挂载多块云硬盘(云上目前最大支持 3块盘) ### ES 集群运维经验总结 #### 常见分片未分配原因总结 ###### 磁盘满了: ``` the node is above the high watermark cluster setting [cluster.routing.allocation.disk.watermark.high=95%], using more disk space than the maximum allowed [95.0%], actual free: [4.055101177689788%] ``` 解决方法:扩容磁盘或者删除数据 ###### 分配文档数超过最大值限制: ``` failure IllegalArgumentException[number of documents in the index cannot exceed 2147483519 ``` 解决方法:向新索引中写入数据,并合理设置分片大小 ###### 主分片所在节点掉线 ``` cannot allocate because a previous copy of the primary shard existed but can no longer be found on the nodes in the cluster ``` 解决方法:找到节点掉线原因并重新启动节点加入集群,等待分片恢复。 ###### 索引属性与节点属性不匹配 ``` node does not match index setting [index.routing.allocation.require] filters [temperature:“warm”,_id:“comdNq4ZSd2Y6ycB9Oubsg”] ``` 解决方法:重新设置索引的属性,和节点保持一致,若要修改节点属性,则需要重启节点 ###### 节点长时间掉线后再次加入集群,导致引入脏数据 ``` cannot allocate because all found copies of the shard are either stale or corrupt ``` 解决方法:使用reroute API: ![lmueskjp.png](http://flt-pan.58heshihu.com/blog/typecho/lmueskjp.png) ###### 未分配的分片太多,导致达到了分片恢复的最大阈值,其他分片需要排队等待 ``` reached the limit of incoming shard recoveries [2], cluster setting [cluster.routing.allocation.node_concurrent_incoming_recoveries=2] (can also be set via [cluster.routing.allocation.node_concurrent_recoveries]) ``` 解决方法:使用cluster/settings调大分片恢复的并发度和速度。 ![lmueu44d.png](http://flt-pan.58heshihu.com/blog/typecho/lmueu44d.png) ### 集群状态 shard unassigned 排查 事情起因很简单,同事对于我写的一个索引报了如下问题。出于学习目的排查下。 ![lmueuf29.png](http://flt-pan.58heshihu.com/blog/typecho/lmueuf29.png) 常见的ES集群有`三种状态`,如下: * Green:主/副分片都已经分配好且可用;集群处于最健康的状态100%可用; * Yellow:主分片可用,但是副分片不可用。这种情况ES集群所有的主分片都是已经分配好了的,但是至少有一个副本是未分配的。这种情况下数据也是完整的;但是集群的高可用性会被弱化。 * Red:存在不可用的主分片。此时只是部分数据可以查询,已经影响到了整体的读写,需要重点关注。这种情况ES集群至少一个主分片(以及它的全部副本)都缺失。 #### 查看集群状态 如下图所示分别为green和red的样子。 ``` GET /_cluster/health ``` ![lmueunzy.png](http://flt-pan.58heshihu.com/blog/typecho/lmueunzy.png) 对于上述red的情况。需要重点关注unassigned_shards没有正常分配的分片。 #### 找到异常索引 方法一:查看所有索引状况,如下就是有问题的。右侧查找 red 关键词。 ``` GET /_cat/indices ``` ![lmueuvru.png](http://flt-pan.58heshihu.com/blog/typecho/lmueuvru.png) 方法二:直接查看unassigned的shard。 查找 unassigned 关键词。 ``` GET /_cat/shards ``` ![lmuev44u.png](http://flt-pan.58heshihu.com/blog/typecho/lmuev44u.png) #### 查看不分配原因 使用 `Cluster Allocation Explain API` ,返回集群为什么不分配分片的详细原因。 ``` GET /_cluster/allocation/explain?pretty curl -X GET "http://xxx.io:48888/_cluster/allocation/explain?pretty" ``` 常见的 unassigned 原因,上一小节也具体描述过了。 > 来源: > https://blog.csdn.net/mijichui2153/article/details/125374880 > https://blog.csdn.net/shen2308/article/details/108548347 ------------- ## (十):ElasticSearch 分片/副本与数据操作流程 ### 前言 一台服务器上无法存储大量数据,ES把一个index里面的数据分成多个shard分布式的存储在多个服务器上(对大的索引分片),拆成多个,分不到不同的节点上)。ES就是通过shard来解决节点的容量上限问题的,通过主分片可以将数据分布到集群内的所有节点上。主分片数是在索引创建时指定的,一般不允许修改,除非Reindex。一个索引中的数据保存在多个分片中(默认为一个)相当于水平分表。一个分片表示一个Lucene的实例,它本身就是一个完整的搜索引擎。我们的文档被存储和索引到分片内,这些对应用程序是透明的,即应用程序直接与索引交互而不是分片。 首先看一下一个ES集群大概的组织形式图。非常直观!!!!! ![lmuex1ef.png](http://flt-pan.58heshihu.com/blog/typecho/lmuex1ef.png) 由上图可以看到shard可以分为`主分片`(primary shard)和`副分片`(replaca shard)。 图示为三个节点。其中任何一个都是有可能故障或者宕机的,此时其承载的shard的数据就会丢失;通过设置一个或者多个replica shard就可以在发生故障的时候提供备份服务。一方面可以保证数据不丢失,另一方面还可以提升操作的吞吐量和性能。 ### 解读 shard 我们可以通过shards查看分片的具体信息。命令会列出详细的列出哪些节点包含哪些分片。也会告诉你这个分片的主/副信息、每个分片的文档数和这些文档在磁盘上占用的字节数。同时也会告诉你这些节点位于那个ip。 ``` #查看所有分片的情况 GET /_cat/shards?v #指定查看某个索引的分片的情况 GET /_cat/shards/es_qidian_flow_online_v2_202202?v ``` ``` GET /_cat/shards/es_qidian_flow_online_v2_202202?pretty&v ``` 注:`GET /_cat/shards?v ` 这里"v"而不是"?v",如上这里是url链接参数的语法而已。他的的作用就是列出表头(还是要比较好,不熟悉的情况至少知道每个字段的含义)。如下为一个实例。 下图 为消息记录搜索功能对应的一个索引的具体分片信息,结合前面的示意图进行解读。 ![lmueyezt.png](http://flt-pan.58heshihu.com/blog/typecho/lmueyezt.png) ①可以看到node节点号和ip是完全能对应上的;也可以看到对于消息记录搜索这个case总共有三个节点和腾讯云上能看到的配置相同。 ②shard列为分片号,分别为0~8;针对其中任一分片例如4,可以看到其有一个主分片(p)和一个副分片(r)。其中每一个分片有点像是mongodb的1主1从的一份副本集,不过在具体组织上还是有区别的。即mongodb任一个主or从其实都是对应一个mongod实例;但是对于这里的es而言总共只有三个node,`2*9=18`个主副分片都是分布在这三个node上,只不过在组织的时候尽量让主副分片不落在同一个node上就可以了。和前面的示意图也完全一致。 ③docs可以看到每个分片的文档数,可以留意到同一分片的主副节点的文档数是完全一致的。这从侧面也说明了主副节点之间"副本集"的关系(一份数据冗余存储)。 ④store可以看到磁盘存储空间的占用情况。 ⑤增减节点(node)的时候shard会自动在nodes中负载均衡;其实从上面截图中我们也可以看到三个节点承载的分片数是差不多的。 ⑥primary shard的数量在建立索引的时候设置,后续不能修改。replica shard数量应该是可以修改的。默认情况会分别创建5个primary shard和5个replica shard。关于截图这个case的9个分片就是创建索引的时候指定的,如下模板和索引中都能看到相关设置。 ![lmuez10e.png](http://flt-pan.58heshihu.com/blog/typecho/lmuez10e.png) 注:当然硬要修改分片数也是可以的,es也提供了相应的api来进行reindex。不过这个代价有点高,要重建整个索引。 思考:其实之所以代价高是es的路由机制决定的,分片数变化后就routing不到原来的数据了。当然我们可以进行更深层次的思考,尤其是和mongodb这种可以任意扩充分片的数据库进行对比。会发现本质原因对于ES来说shard就是承载数据的最小单位,写入的时候也完全是按照shard的数量进行的路由;而mongodb最小承载单位是chunk,chunk不是固定在某个shard一成不变,而是在balancer的控制下动态地游走于各个shard之间。 ⑦primary shard不能和自己的replica shard放到同一个节点上(否则该节点挂了后这个分片的数据就彻底丢失了,起不到容错作用),但是可以和其他shard的replica节点放到一个节点上。 ⑧每个doc肯定只存在于某一个primary shard以及其对应的replica shard中,不可能存在于多个primary shard上。 ⑨replica的容错。举个例子master节点(node)宕机后,其上存在的primary shard也一并异常。这个时候会选举产生一个新的master,它会将异常primary shard对应的replica shard提升为primary shard。重启宕机node,master copy备份的数据到重启后的node,然后还会同步宕机所落后的数据,并将这个shard降级为replica。注:关于node下面会讲到。 注:统计数据量的时候副本集只能算一份数据,不要重复统计了哦!(大概8亿/月) ### ES 集群的各节点(client/master/data) 前面第⑨点提到了发生宕机时master会参与恢复,这里简要的介绍下master。其实除了master ES集群中的节点根据其被配置后所实际执行的功能可以大致分为 client/master/data三种。 查看节点信息(谁是master节点),如下图有 "*" 号的就是 master 节点。 ``` GET _cat/nodes?v ``` ![lmuezfgu.png](http://flt-pan.58heshihu.com/blog/typecho/lmuezfgu.png) 前面所说的配置无非就是主节点和数据节点的配置,如下: ``` node.master: true node.data: false ``` #### 主节点(master node) ``` elasticsearch.yml node.master: true node.data: false 主要功能:维护元数据,管理集群节点状态;不负责数据写入和查询。 配置要点:内存可以相对小一些,但是机器一定要稳定,最好是独占的机器。 ``` 当node.master被设置为true的时候就为主节点。理想情况下这个master节点就干一些管理性质的工作,比如维护索引元数据(创建删除索引)、负责切换primary shard和replica shard身份等。主节点的稳定性非常重要。默认情况集群中任何一个node.master没有被设为false的节点都有可能被选为主节点,为了让集群更加稳定分离主节点和数据节点是一个比较好的选择。 要是master节点宕机了,那么会自动重新选举一个节点为master。 如果非master节点宕机了,会有master节点让宕机节点上的primary shard的身份转移到其他机器上的replica shard。等到宕机机器重启恢复后,master节点会控制将缺失的replica shard分片补充过去,同步后续修改数据之类的,让集群恢复到有容错保障的状态。 稳定的主节点对集群的健康是非常重要的。默认情况下任何一个集群中的节点都有可能被选为主节点。索引数据和搜索查询等操作会占用大量的CPU、内存、IO资源,为了确保一个集群的稳定,分离主节点和数据节点是一个比较好的选择。我们可以通过配置指定一个节点应该是数据节点还是master节点。 #### 客户端节点(client node) ``` elasticsearch.yml node.master: false node.data: false 主要功能:负责任务分发和结果汇聚,分担数据节点压力。 配置要点:大内存,最好是独占的机器 ``` 当node.master/data都被设置为false的时候即为客户端节点。此时该节点只能处理路由请求、处理搜索、分片索引操作等。独立的客户端节点在大型集群中是非常有用的,它协调主节点和数据节点。 #### 数据节点(data node) ``` elasticsearch.yml node.master: false node.data: true 主要功能:负责数据的写入与查询,压力大。 配置要点:大内存,最好是独占的机器。 ``` node.master未设置为false、data被设置为true即为数据节点。数据节点的作用就是实际承载数据的节点,也是执行增删改查、聚合操作的节点。数据节点对cpu、内存、io要求较高。注意当资源不够用的时候需要向集群中添加新的节点。 #### 混合节点(mixed node) 不建议 ``` elasticsearch.yml node.master: true node.data: true ``` ### ES 的扩容 ES的扩容就是新增节点(node),然后在将所有的shard在节点之间进行一个重新的分配(负载均衡)就可以了。 ### ES 集群数据写入拉取流程 #### ES数据写入流程 * 1)客户端选择一个node发送请求过去,这个node就是coordinating node(协调节点); * 2)coordinating node,对document进行路由,将请求转发给对应的node(有primary shard); * 3)实际的node上的primary shard处理请求,然后将数据同步到replica node; * 4)coordinating node,如果发现primary node和所有replica node都搞定之后,就返回响应结果给客户端。 #### ES数据拉取流程 * 1)客户端发送请求到任意一个node,成为coordinate node; * 2)coordinate node对document进行路由,将请求转发到对应的node,此时会使用round-robin随机轮询算法,在primary shard以及其所有replica中随机选择一个,让读请求负载均衡; * 3)接收请求的node返回document给coordinate node; * 4)coordinate node返回document给客户端。 ``` 1.写入document时,每个document会自动分配一个全局唯一的id即doc id,同时也是根据doc id进行hash路由到对应的primary shard上。也可以手动指定doc id,比如用订单id,用户id。 2.读取document时,你可以通过doc id来查询,然后会根据doc id进行hash,判断出来当时把doc id分配到了哪个shard上面去,从那个shard去查询 ``` ### ES同mongodb分片集群比较 MongoDB就是一个个的节点先组成副本集,一个个的副本集在mongos/configserver的组织下成为一个完整的分片集群;其逻辑组织(主副关系)形式和物理实体(ip)组织形式是完全一样的。ES更像是实现约定好固定的shard数量(主副),这些shard尽量均匀的分布在N个节点(node)上;其逻辑组织形式(主副关系)和物理实体(ip)组织形式是完全独立的。 最大的不同就是ES各个节点的角色更加平等。对于ES而言,无论是写入还是查询客户端的请求可以作用与任意一个node。这个node就是协调节点,它的角色有点像是mongos了;负责路由、聚合等。其实从这个实现我们知道ES的每一个node都是要维护一个routing table的。 比对mongodb和ES后发现,两者对于一个副本集在命令上存在差异,但意思是差不多的。ES把主、副称为primary shard和replica shard,但是mongodb里面称为primary节点和secondary节点。简单理解节点可以认为为一个ip实例(一台机器);对于ES来说一个节点通常包括多个shard,所以意思还是那个意思但是你不能用节点。但是对于mongodb来说一个mongod就是一个实例,所以直接称为节点就好了。 对于写入操作。mongos的角色可能由任一个node承担,但是真正往某个shard写数据的时候其实还是优先作用与primary shard然后在同步到replica shard,这一点和mongodb差不多;只不过mongodb的一个副本集里面叫主从节点罢了。 对于查询操作。mongos的角色可能由任一个node承担,但是具体进行查询的时候则是在对应分片的primary和replica中进行轮询;而mongodb则是提供了不同的选项。 下面也贴一个mongodb分片集的示意图,方便对比。 ![lmuf0hjc.png](http://flt-pan.58heshihu.com/blog/typecho/lmuf0hjc.png) > 来源: > https://blog.csdn.net/mijichui2153/article/details/124987714 ------------- ## (十一):ElasticSearch 数据备份与迁移 Elasticsearch 默认配置是数据持久化的,就是 ES 会定时地把缓存数据刷新到硬盘,从而达到数据持久化地效果。在生产环境中,ES 的数据持久化是必须的,防止出现断电时数据的丢失。固然,除了数据持久化外,咱们也是得作到数据备份的,防止出现数据损坏时没法恢复数据的状况。 ### 备份方案 #### 离线方案 * Snapshot * Reindex * Logstash * ElasticSearch-dump * ElasticSearch-Exporter #### 增量备份方案 * logstash ### 备份数据 #### 配置文件elasticsearch.yml 在配置文件`config/elasticsearch.yml` 中添加一行数据,设置ES备份的快照数据存储路径。如果没有此目录则须要自行建立。配置好后,须要重启ES。 ``` path.repo: ["/usr/local/elasticsearch/snapshot"] ``` #### 建立仓库 其实就是在ES库中建立一个备份存储的目的仓库,这里以仓库名称为 backup 为例,有以下两种方式。 * 在linux服务器上执行如下命令。 ``` curl -H "Content-Type: application/json" -XPUT -u elastic:xxx http://ES的IP:9200/_snapshot/backup -d '{"type": "fs","settings": {"location": "/usr/local/elasticsearch/snapshot"}}' ``` * 在kibana的Dev Tools开发工具中调用接口。 ``` PUT _snapshot/backup { "type": "fs", "settings": { "location": "data_bk", "compress": true, "max_snapshot_bytes_per_sec" : "50mb", "max_restore_bytes_per_sec" : "50mb" } } ``` 调用参数说明: ``` compress #是否压缩,默认为是。 max_snapshot_bytes_per_sec #每一个节点快照速率。默认40mb/s。 max_restore_bytes_per_sec #节点恢复速率。默认40mb/s。 ``` 返回结果以下,则说明建立成功。 ``` { "acknowledged": true } ``` #### 删除备份数据 在备份数据以前,最好是先根据备份数据的名称删除原来已经备份好的数据。相同名称的备份数据是不能重复备份的。 这里以备份数据的名称为 bk_20190926 为例,后面的执行都以此为例,有以下两种方式。 * 在linux服务器上执行如下命令。 ``` curl -XDELETE http://ES的ip:端口/_snapshot/backup/bk_20190926 ``` * 在kibana的Dev Tools开发工具中调用接口。 ``` DELETE _snapshot/backup/bk_20190926 ``` 返回结果以下,则说明删除成功。 ``` { "acknowledged": true } ``` #### 开始备份数据 备份数据一样是与删除数据同样,直接调用ES的接口实现的,有以下两种方式。 * 在linux服务器上执行如下命令。 ``` curl -XPUT http://ES的ip:端口/_snapshot/backup/bk_20190926?wait_for_completion=true ``` * 在kibana的Dev Tools开发工具中调用接口。 ``` PUT _snapshot/backup/bk_20190926?wait_for_completion=true ``` 返回结果以下,则说明已经备份成功。 ``` { "snapshot": { "snapshot": "bk_20190926", "uuid": "K4fze5eGSvOwot_xWtz0Hw", "version_id": 6050399, "version": "6.5.3", "indices": [ "first_index" ], "include_global_state": true, "state": "SUCCESS", "start_time": "2019-09-27T05:36:39.398Z", "start_time_in_millis": 1569562599398, "end_time": "2019-09-27T05:36:39.723Z", "end_time_in_millis": 1569562599723, "duration_in_millis": 325, "failures": [], "shards": { "total": 5, "failed": 0, "successful": 5 } } } ``` 同时,能够在ES所在的服务器的目录`/usr/local/elasticsearch/snapshot/data_bk`下查看到增长了不少文件,这些就是备份数据所需的文件。 #### 查看备份数据 备份完数据后,直接在服务器上能够看到这些备份的文件,可是这些文件并非一眼就能看出你备份了哪些数据的,此时你能够经过调用ES的接口来查看你备份了哪些数据。一样有两种方式调用。 * 在linux服务器上执行如下命令。 ``` curl -XGET http://ES的ip:端口/_snapshot/backup/_all ``` * 在kibana的Dev Tools开发工具中调用接口。 ``` GET _snapshot/backup/_all ``` 返回结果以下,你备份了多少快照均可以在这里看到,snapshots列表的最后一个元素就是你最近备份的快照。 ``` { "snapshots": [ { "snapshot": "bk_20190926", "uuid": "K4fze5eGSvOwot_xWtz0Hw", "version_id": 6050399, "version": "6.5.3", "indices": [ "first_index" ], "include_global_state": true, "state": "SUCCESS", "start_time": "2019-09-27T05:36:39.398Z", "start_time_in_millis": 1569562599398, "end_time": "2019-09-27T05:36:39.723Z", "end_time_in_millis": 1569562599723, "duration_in_millis": 325, "failures": [], "shards": { "total": 5, "failed": 0, "successful": 5 } } ] } ``` ### 恢复数据 数据备份好了,若是真的出现了不可逆的数据损坏状况,此时就能够进行数据恢复了。 #### 备份data文件夹 data文件夹其实就是当前ES的数据存储地,防止恢复数据出现异常,先把ES目录下面的data目录备份一下。 ``` tar -cvf data-20190626.tar.gz data ``` #### 清空数据 恢复数据以前,先把当前ES的数据清空掉。有以下两种方式。 (1)在linux服务器上执行如下命令。 ``` curl -XDELETE http://ES的ip:端口/_all ``` (2)在kibana的Dev Tools开发工具中调用接口。 ``` DELETE _all ``` 返回结果以下,则说明清空数据成功。 ``` { "acknowledged": true } ``` #### 恢复数据 恢复数据一样有以下两种方式操做。 (1)在linux服务器上执行如下命令。 ``` curl -XPOST http://ES的ip:端口/_snapshot/backup/bk_20190926/_restore ``` (2)在kibana的Dev Tools开发工具中调用接口。 ``` POST _snapshot/backup/bk_20190926/_restore ``` 返回结果以下,则说明恢复数据成功。 ``` { "accepted": true } ``` 至此,ES的数据备份和恢复就介绍完啦! ### 总结 这里只是讲解了手动的操做ES的数据备份和恢复,在程序里面咱们同样能够经过调用ES的接口来进行数据备份和恢复,例如经过java程序来定时天天进行ES地数据备份,而后删除昨天或前天的备份数据,只保留一份或两份备份数据,以此来节约磁盘空间。 ### ElasticSearch 数据迁移 #### ES数据迁移有三种方式 * Rolling upgrades回滚 * snapshot快照 * elasticdump方式 ![lmuf42zg.png](http://flt-pan.58heshihu.com/blog/typecho/lmuf42zg.png) #### 迁移需考虑的问题 * 版本问题,从低版本到高版本数据的迁移 * 多租户的适配问题 * 多次或者分批迁移数据 * 数据在迁移时候富化 * FieldMapping 和数据信息分离 #### Rolling upgrades滚动升级 ###### 方法说明 该方法更好的使用在跨版本ES集群迁移中,它允许 ES集群一次升级一个节点,因此在升级期间不会中断服务。不支持在升级持续时间之后在同一集群中运行多个版本的 ES,因为无法将分片从升级的节点复制到运行旧版本的节点。所以在升级前需要对当前使用版本进行备份,以便在升级出现异常时进行回滚。 同时在升级过程中优先选择data节点,在data节点升级完成后,在对集群中master节点进行升级。 支持滚动升级准则: * 同一主要版本的次要版本之间 * v 从 5.6 到 6.8 * v 从 6.8 到 7.17.5 * v 从 7.17.0 到 7.17.5 的任何版本 * 从 6.7 或更早版本直接升级到 7.17.5 需要 完全重启集群。 在做滚动升级时需要保证ElasticSearch间的集群节点通讯,所以要保证安全认证同步。 ###### 具体步骤 ###### 升级前准备 在开始将集群升级到版本 7.17.5 之前,您应该执行以下操作: * 1、检查弃用日志以查看您是否正在使用任何弃用的功能并相应地更新您的代码。 * 2、查看重大更改并对版本 7.17.5 的代码和配置进行任何必要的更改。 * 3、如果您使用任何插件,请确保每个插件都有一个与 Elasticsearch 版本 7.17.5 兼容的版本。 * 4、在升级生产集群之前,在隔离环境中测试升级。 * 5、通过拍摄快照备份您的数据!(或对当前集群所有节点进行数据和安装包进行全量备份) ###### 升级集群 禁用自动分片功能(若在升级过程中不考虑IO性能瓶颈,可以忽略),关闭一个数据节点时,分配过程会等待。 `index.unassigned.node_left.delayed_timeout`(默认为一分钟),然后才开始将该节点上的分片复制到集群中的其他节点,这可能涉及大量 I/O。由于节点很快将重新启动,因此此 I/O 是不必要的。您可以通过在关闭数据节点之前禁用副本分配来避免争分夺秒 : ``` PUT _cluster/settings{ “persistent”: { “cluster.routing.allocation.enable”: “primaries” }} ``` * 停止不必要的索引并执行同步刷新(可选的) ``` POST _flush/synced ``` * 暂停要升级节点与集群间其他节点进行数据通讯,避免有新的数据产生(可选的) ``` POST _ml/set_upgrade_mode?enabled=true ``` 关闭当前节点,在当前服务器中升级该节点ElasticSearch版本,其中ElasticSeaarch参考原节点进行配置。 * 升级ElasticSearch使用插件 使用elasticsearch-plugin脚本安装每个已安装的 Elasticsearch 插件的升级版本。升级节点时必须升级所有插件。 ``` 语法:$ES_HOME bin/elasticsearch-plugin install XXXX ``` * 启动升级的节点 ``` 语法:$ES_HOME bin/nohup ./elasticsearch & ``` * 重新启用分片分配(若为禁用自动分片功能,无需执行此步骤) ``` PUT _cluster/settings{ “persistent”: { “cluster.routing.allocation.enable”: null }} ``` * 等待节点恢复 ``` GET _cat/health?v=true ``` ###### 注意 在滚动升级期间,分配给运行新版本的节点的主分片不能将其副本分配给使用旧版本的节点。新版本可能具有旧版本无法理解的不同数据格式。 如果无法将副本分片分配给另一个节点(集群中只有一个升级节点),则副本分片保持未分配状态,状态保持不变yellow。 在这种情况下,一旦没有初始化或重新定位分片,您就可以继续(检查init和relo列)。一但另一个节点升级,就可以分配副本并且状态将更改为green。 * 重复任务 当节点恢复并且集群稳定后,对每个需要更新的节点重复这些步骤。 * 重新启动节点与集群间其他节点进行数据通讯(若已经暂停该功能,若未暂停,忽略此操作) ``` POST _ml/set_upgrade_mode?enabled=false ``` ###### 注意 在滚动升级期间,集群继续正常运行。但是,在升级集群中的所有节点之前,任何新功能都会被禁用或以向后兼容的模式运行。一旦升级完成并且所有节点都在运行新版本,新功能就会开始运行。一但发生这种情况,就无法返回以向后兼容模式运行。运行先前版本的节点将不允许加入完全更新的集群。 如果升级过程中出现网络故障,将所有剩余的旧节点与集群隔离开来,您必须使旧节点脱机并升级它们以使其能够加入集群。 如果您在升级过程中同时停止一半或更多符合主节点条件的节点,则集群将不可用,这意味着升级不再是滚动升级。如果发生这种情况,您应该升级并重新启动所有已停止的符合主节点资格的节点,以允许集群再次形成,就像执行全集群重启升级一样。可能还需要升级所有剩余的旧节点,然后它们才能在重新形成后加入集群。 #### snapshot快照 ###### 首先创建快照仓库 注意:对于快照仓库需要每个节点都对其有访问权限,所以在实际使用中需要使用nfs挂载。 * 使用Psotman方式创建仓库 ``` Postman:PUT http://192.168.115.130:9200/_snapshot/my_repository{ “type”: “fs”, “settings”: { “location”: “/home/elastic/my_repo_floder”, “compress”: true, “max_restore_bytes_per_sec”: “50mb”, “max_snapshot_bytes_per_sec”: “50mb” } } ``` 说明:my_repository为镜像仓库名称,Location 为镜像路径。 * 使用Curl方式创建仓库 ``` curl -XPUT ‘http://192.168.115.130:9200/_snapshot/my_repository’ -H ‘content-Type:application/json’ -d ‘{ “type”: “fs”, “settings”: { “location”: “/home/elastic/my_repo_floder”, “compress”: ****true****, “max_restore_bytes_per_sec”: “50mb”, “max_snapshot_bytes_per_sec”: “50mb” } }’ ``` ###### 备份索引(全量) * 使用Psotman方式备份 ``` Postman:PUT http://192.168.115.130:9200/_snapshot/my_repository/snapshot_1?wait_for_completion=true ``` * 使用Curl方式备份 ``` curl -XPUT ‘http://192.168.115.130:9200/_snapshot/my_repository/snapshot_1?wait_for_completion=true’ ``` 日志显示 completed with state SUCCESS * 查看备份对应索引信息 ``` Postman:GET http://192.168.115.130:9200/_snapshot/my_hdfs_repository/snapshot_1#snapshot_1是备份文件名称 ``` #### elasticdump方式 Elasticdump工具是依赖于npm进行安装的,离线安装elasticdump步骤如下: ``` #第一步下载node安装包 wget https://nodejs.org/dist/v16.14.0/node-v16.14.0-linux-x64.tar.xz #第二步 在一台外网服务器安装node 解压:tar xvf node-v16.14.0-linux-x64.tar.xz 建立软链接 ln -s ~/node-v16.14.0-linux-x64/bin/node /usr/bin/node ln -s ~/node-v16.14.0-linux-x64/bin/npm /usr/bin/npm 确认安装成功 node -v npm -v #第三步安装npm-pack-all npm install -g npm-pack-all #第四步安装elasticdump npm install elasticdump -g #第五步 打包elasticdump 进入到elasticdump安装目录 cd node-v16.3.0-linux-x64/lib/node_modules/elasticdump/ 执行 npm-pack-all 当前目录生成 elasticdump-6.82.0.tgz #第六步 将node安装包和 elasticdump安装报复制到离线安装的服务器 node-v16.14.0-linux-x64.tar.xz elasticdump-6.82.0.tgz #第七步 按照第二步安装node 和npm #第八步 安装elasticdump npm install elasticdump-6.82.0.tgz #第九步建立软连接 ls ~/node_modules/elasticdump/bin/elasticdump /usr/bin/elasticdump #第十步确认安装成功 elasticdump --help ``` ``` #导出分词器 [root@localhost ~]# elasticdump --input=http://ip:9200/my_index --output=http://127.0.0.1:9200/my_index --type=analyzer #导出映射mapping [root@localhost ~]# elasticdump --input=http://ip:9200/ --output=http://127.0.0.1:9200/ --all=true --type=mapping #导出全部数据 [root@localhost ~]# elasticdump --input=http://ip:9200/ --output=http://127.0.0.1:9200/ --all=true --type=data #如果集群配置了x-pack认证 [root@localhost ~]# elasticdump --input=http://user:password@ip:9200/ --output=http://user:password@127.0.0.1:9200/ --all=true --type=data ``` #### 迁移注意事项 ES数据迁移有两种情况 * 一、ES版本不做变更,只是数据进行迁移; * 二、ES版本升级,同时数据迁移至新版本ES中 上述三种方法均能完成ES的数据迁移,在实际操作时,请根据实际生产环境进行选择,优先选择Rolling upgrades滚动升级,同时需要注意一下几点: * ES版本发生变化,需要关注JAVA版本是否要随之变化,ES7版本时开始内嵌JAVA版本为17版本,由原1.8版本升级到17版本,jdk跨度较大,对API的调用挑战性较强。需要经过大量测试,必要时需要对代码进行重构。 * ES存储数据类型发生变化,ES6版本中为自定义手动创建的,但是在ES7中只有一种数据类型为“doc”; * 当迁移数据量较大时,数据迁移花费时间较长,建议在业务平滑起或者晚上进行; > 来源: > https://blog.csdn.net/m0_67401270/article/details/126362511 > https://www.modb.pro/db/561895 ------------- ## (十二):ElasticSearch 常用 Curl 命令实践 Elasticsearch 常用命令 curl ,这是日常工作中常用的命令,也是非常实用的命令,所以,今天单独拉出来和大家一同学习一下。 ### 前言 测试环境: ``` Centos7.2 64位 jdk1.8.0_91 elasticsearch-2.2.0 ``` ### CURL 命令 * 简单认为是可以在命令行下访问url的一个工具 * curl是利用URL语法在命令行方式下工作的开源文件传输工具,使用curl可以简单实现常见的get/post请求。 * -x 指定http请求的方法(HEAD GET POST PUT DELETE) * -d 指定要传输的数据 ### CURL建立索引库 PUT/POST都可以: ``` [hadoop@h153 ~]$ curl -XPUT 'http://192.168.205.153:9200/index_name/' {"acknowledged":true} ``` ### CURL创建索引 ``` [hadoop@h153 ~]$ curl -XPOST http://192.168.205.153:9200/hui/employee/1 -d '{undefined "first_name" : "John", "last_name" : "Smith", "age" : 25, "about" : "I love to go rock climbing", "interests": [ "sports", "music" ] }' {"_index":"hui","_type":"employee","_id":"1","_version":1,"_shards":{"total":2,"successful":1,"failed":0},"created":true} ``` #### 使用文件的方式创建: ``` [hadoop@h153 ~]$ vi qiang.json {undefined "first_name" : "John", "last_name" : "Smith", "age" : 25, "about" : "I love to go rock climbing", "interests": [ "sports", "music" ] } [hadoop@h153 ~]$ curl -XPOST '192.168.205.153:9200/qiang' -d @qiang.json {"acknowledged":true} ``` PUT和POST用法:PUT是幂等方法,POST不是。所以PUT用于更新、POST用于新增比较合适。 * PUT,DELETE操作是幂等的。所谓幂等是指不管进行多少次操作,结果都一样。比如我用PUT修改一篇文章,然后在做同样的操作,每次操作后的结果并没有不同,DELETE也是一样。 * POST操作不是幂等的,比如常见的POST重复加载问题:当我们多次发出同样的POST请求后,其结果是创建出了若干的资源。 * 还有一点需要注意的就是,创建操作可以使用POST,也可以使用PUT,区别在于POST是作用在一个集合资源之上的(/articles),而PUT操作是作用在一个具体资源之上的(/articles/123),比如说很多资源使用数据库自增主键作为标识信息,而创建的资源的标识信息到底是什么只能由服务端提供,这个时候就必须使用POST。 ###### 创建索引注意事项: 索引库名称必须要全部小写,不能以下划线开头,也不能包含逗号。如果没有明确指定索引数据的ID,那么es会自动生成一个随机的ID,需要使用POST参数 ``` [hadoop@h153 ~]$ curl -XPOST http://192.168.205.153:9200/hui/emp/ -d '{"first_name" : "John"}' {"_index":"hui","_type":"emp","_id":"AV8MoiLdq8PZVDlk6J74","_version":1,"_shards":{"total":2,"successful":1,"failed":0},"created":true} ``` 如果想要确定我们创建的都是全新的内容: * 1:使用自增ID * 2:在url后面添加参数 ``` [hadoop@h153 ~]$ curl -XPUT http://192.168.205.153:9200/hui/emp/2?op_type=create -d '{"name":"zs","age":25}' {"_index":"hui","_type":"emp","_id":"2","_version":1,"_shards":{"total":2,"successful":1,"failed":0},"created":true} ``` ``` [hadoop@h153 ~]$ curl -XPUT http://192.168.205.153:9200/hui/emp/2/_create -d '{"name":"laoxiao","age":25}' {"error":{"root_cause":[{"type":"document_already_exists_exception","reason":"[emp][2]: document already exists","shard":"2","index":"hui"}],"type":"document_already_exists_exception","reason":"[emp][2]: document already exists","shard":"2","index":"hui"},"status":409} # 注:如果存在同名文件,Elasticsearch将会返回一个409Conflict的HTTP反馈码 ``` ### GET查询索引 #### 根据员工id查询 ``` [hadoop@h153 ~]$ curl -XGET http://192.168.205.153:9200/hui/employee/1?pretty { "_index" : "hui", "_type" : "employee", "_id" : "1", "_version" : 1, "found" : true, "_source" : { "first_name" : "John", "last_name" : "Smith", "age" : 25, "about" : "I love to go rock climbing", "interests" : [ "sports", "music" ] } } [hadoop@h153 ~]$ curl -XGET http://192.168.205.153:9200/hui/emp/2?pretty { "_index" : "hui", "_type" : "emp", "_id" : "2", "_version" : 1, "found" : true, "_source" : { "name" : "zs", "age" : 25 } } ``` * 在任意的查询字符串中添加pretty参数,es可以得到易于识别的json结果。 * curl后添加-i 参数,这样你就能得到反馈头文件。 ``` [hadoop@h153 ~]$ curl -i 'http://192.168.205.153:9200/hui/emp/1?pretty' HTTP/1.1 404 Not Found Content-Type: application/json; charset=UTF-8 Content-Length: 76 { "_index" : "hui", "_type" : "emp", "_id" : "1", "found" : false } ``` #### 检索文档中的一部分,如果只需要显示指定字段 ``` [hadoop@h153 ~]$ curl -XGET http://192.168.205.153:9200/hui/employee/1?_source=name,age {"_index":"hui","_type":"employee","_id":"1","_version":1,"found":true,"_source":{"age":25}} ``` #### 如果只需要source的数据 ``` [hadoop@h153 ~]$ curl -XGET http://192.168.205.153:9200/hui/employee/1?_source {"_index":"hui","_type":"employee","_id":"1","_version":1,"found":true,"_source":{undefined "first_name" : "John", "last_name" : "Smith", "age" : 25, "about" : "I love to go rock climbing", "interests": [ "sports", "music" ] }} ``` #### 查询所有 你可以再返回的hits中发现我们录入的文档。搜索会默认返回最前的10个数值: ``` [hadoop@h153 ~]$ curl -XGET http://192.168.205.153:9200/hui/employee/_search {"took":21,"timed_out":false,"_shards":{"total":5,"successful":5,"failed":0},"hits":{"total":1,"max_score":1.0,"hits":[{"_index":"hui","_type":"employee","_id":"1","_score":1.0,"_source":{undefined "first_name" : "John", "last_name" : "Smith", "age" : 25, "about" : "I love to go rock climbing", "interests": [ "sports", "music" ] }}]}} ``` #### 根据条件进行查询 ``` [hadoop@h153 ~]$ curl -XGET http://192.168.205.153:9200/hui/_search?q=last_name:Smith {"took":26,"timed_out":false,"_shards":{"total":5,"successful":5,"failed":0},"hits":{"total":1,"max_score":0.30685282,"hits":[{"_index":"hui","_type":"employee","_id":"1","_score":0.30685282,"_source":{ "first_name" : "John", "last_name" : "Smith", "age" : 25, "about" : "I love to go rock climbing", "interests": [ "sports", "music" ] }}]}} ``` 不根据条件查询: ``` [hadoop@h153 ~]$ curl -XGET http://192.168.205.153:9200/hui/_search {"took":5,"timed_out":false,"_shards":{"total":5,"successful":5,"failed":0},"hits":{"total":3,"max_score":1.0,"hits":[{"_index":"hui","_type":"emp","_id":"AV8MoiLdq8PZVDlk6J74","_score":1.0,"_source":{"first_name" : "John"}},{"_index":"hui","_type":"emp","_id":"2","_score":1.0,"_source":{"name":"zs","age":25}},{"_index":"hui","_type":"employee","_id":"1","_score":1.0,"_source":{ "first_name" : "John", "last_name" : "Smith", "age" : 25, "about" : "I love to go rock climbing", "interests": [ "sports", "music" ] }}]}} ``` ### DSL查询 Domain Specific Language(领域特定语言),这里只给出了最简单的例子,还可以写的更复杂,比如还可以添加过滤等复杂的条件: ``` [hadoop@h153 ~]$ curl -XGET http://192.168.205.153:9200/hui/employee/_search -d '{"query":{"match":{"last_name":"Smith"}}}' {"took":5,"timed_out":false,"_shards":{"total":5,"successful":5,"failed":0},"hits":{"total":1,"max_score":0.30685282,"hits":[{"_index":"hui","_type":"employee","_id":"1","_score":0.30685282,"_source":{ "first_name" : "John", "last_name" : "Smith", "age" : 25, "about" : "I love to go rock climbing", "interests": [ "sports", "music" ] }}]}} ``` ### MGET查询 使用mget API获取多个文档,先再创建一个索引:,再进行查询。`curl -XPOST http://192.168.205.153:9200/website/blog/2 -d '{"first_name":"John" , "last_name":"Smith"}'` ``` [hadoop@h153 ~]$ curl -XGET http://192.168.205.153:9200/_mget?pretty -d '{"docs":[{"_index":"hui","_type":"emp","_id":2,"_source":"name"},{"_index":"website","_type":"blog","_id":2}]}' # 返回结果 { "docs" : [ { "_index" : "hui", "_type" : "emp", "_id" : "2", "_version" : 1, "found" : true, "_source" : { "name" : "zs" } }, { "_index" : "website", "_type" : "blog", "_id" : "2", "_version" : 1, "found" : true, "_source" : { "first_name" : "John", "last_name" : "Smith" } } ] } ``` 如果你需要的文档在同一个_index或者同一个中,你就可以在URL中指定一个默认的或者`_type``/_index``/_index/_type` ``` curl -XGET http://192.168.205.153:9200/hui/_mget?pretty -d '{"docs":[{"_type":"employee","_id":1},{"_type":"emp","_id":2}]}' ``` 如果所有的文档拥有相同的 以及,直接在请求中添加ids的数组即可`_index``_type` ``` curl -XGET http://192.168.205.153:9200/hui/emp/_mget?pretty -d '{"ids":["1","2"]}' ``` ### 统计es的索引数量 ``` [hadoop@h153 ~]$ curl -XGET 'http://192.168.205.153:9200/_cat/count' 1508265400 02:36:40 44542 ``` 当然我们如果想统计某些特定索引下文档数量也是可以的。例如我们想要hui下的索引数量: ``` [hadoop@h153 ~]$ curl -XGET 'http://192.168.205.153:9200/_cat/count/hui' 1508265602 02:40:02 0 ``` ###### 查看所有的索引信息 ``` [hadoop@h153 ~]$ curl -XGET 'http://192.168.205.153:9200/_cat/indices?pretty' green open test 5 1 0 0 1.5kb 795b green open qiang 5 0 0 0 795b 795b green open hui 5 1 4 0 41.6kb 20.8kb ``` 当然如果我们想要查看固定类型的索引信息是否存在: ``` [hadoop@h153 ~]$ curl -XGET 'http://192.168.205.153:9200/_cat/indices/hui?pretty' green open hui 5 1 4 0 41.6kb 20.8kb ``` 下面我们介绍一个如何进行强制段合并的命令: ``` [hadoop@h153 ~]$ curl -XPOST 'http://192.168.205.153:9200/hui/_forcemerge?max_num_segments=1' {"_shards":{"total":5,"successful":5,"failed":0}} ``` ### HEAD使用 如果只想检查一下文档是否存在,你可以使用HEAD来替代 GET方法,这样就只会返回HTTP头文件: ``` [hadoop@h153 ~]$ curl -i -XHEAD http://192.168.205.153:9200/hui/emp/1 HTTP/1.1 404 Not Found Content-Type: text/plain; charset=UTF-8 Content-Length: 0 ``` ### Elasticsearch的更新 ES可以使用PUT或者POST对文档进行更新,如果指定ID的文档已经存在,则执行更新操作。注意:执行更新操作的时候 * ES首先将旧的文档标记为删除状态 * 然后添加新的文档 * 旧的文档不会立即消失,但是你也无法访问 * ES会在你继续添加更多数据的时候在后台清理已经标记为删除状态的文档 局部更新,可以添加新字段或者更新已有字段(必须使用POST): ``` [hadoop@h153 ~]$ curl -XPOST http://192.168.205.153:9200/hui/emp/2/_update -d '{"doc":{"city":"beijing","car":"BMW"}}' {"_index":"hui","_type":"emp","_id":"2","_version":2,"_shards":{"total":2,"successful":1,"failed":0}} ``` ### Elasticsearch的删除 ``` [hadoop@h153 ~]$ curl -XDELETE http://192.168.205.153:9200/hui/emp/2/ {"found":true,"_index":"hui","_type":"emp","_id":"2","_version":3,"_shards":{"total":2,"successful":1,"failed":0}} ``` 注:如果文档存在,found属性值为true,属性的值+1,这个就是内部管理的一部分,它保证了我们在多个节点间的不同操作的顺序都被正确标记了。`_version` ### Elasticsearch的批量操作bulk 与mget类似,bulk API可以帮助我们同时执行多个请求,格式: ``` – action:index/create/update/delete – metadata:_index,_type,_id – request body:_source(删除操作不需要) { action: { metadata }}\n { request body }\n { action: { metadata }}\n { request body }\n ``` 使用时注意,不能直接在json字符串中添加\\n字符,应该按回车`curl -XPOST -d` ``` [hadoop@h153 ~]$ curl -XPOST '192.168.205.153:9200/_bulk?pretty' -H 'Content-Type: application/json' -d' > { "delete": { "_index": "hui", "_type": "employee", "_id": "1" }} > { "create": { "_index": "website", "_type": "blog", "_id": "123" }} > { "title": "My first blog post" } > { "index": { "_index": "website", "_type": "blog" }} > { "title": "My second blog post" } > { "update": { "_index": "website", "_type": "blog", "_id": "123", "_retry_on_conflict" : 3} } > { "doc" : {"title" : "My updated blog post"} } > ' { "took" : 197, "errors" : false, "items" : [ { "delete" : { "_index" : "hui", "_type" : "employee", "_id" : "1", "_version" : 2, "_shards" : { "total" : 2, "successful" : 1, "failed" : 0 }, "status" : 200, "found" : true } }, { "create" : { "_index" : "website", "_type" : "blog", "_id" : "123", "_version" : 1, "_shards" : { "total" : 2, "successful" : 1, "failed" : 0 }, "status" : 201 } }, { "create" : { "_index" : "website", "_type" : "blog", "_id" : "AV8XEEpF4TG7AylMbq5H", "_version" : 1, "_shards" : { "total" : 2, "successful" : 1, "failed" : 0 }, "status" : 201 } }, { "update" : { "_index" : "website", "_type" : "blog", "_id" : "123", "_version" : 2, "_shards" : { "total" : 2, "successful" : 1, "failed" : 0 }, "status" : 200 } } ] } ``` create和index的区别:如果数据存在,使用create操作失败,会提示文档已经存在,使用index则可以成功执行。 ###### 使用文件的方式 ``` vi requests curl -XPOST/PUT localhost:9200/_bulk --data-binary @request ``` bulk一次最大处理多少数据量: * bulk会把将要处理的数据载入内存中,所以数据量是有限制的 * 最佳的数据量不是一个确定的数值,它取决于你的硬件,你的文档大小以及复杂性,你的索引以及搜索的负载 * 一般建议是1000-5000个文档,如果你的文档很大,可以适当减少队列,大小建议是5-15MB,默认不能超过100M,可以在es的配置文件中修改这个值`http.max_content_length: 100mb` ### Elasticsearch的版本控制 普通关系型数据库使用的是(悲观并发控制(PCC)):当我们在读取一个数据前先锁定这一行,然后确保只有读取到数据的这个线程可以修改这一行数据。 ES使用的是(乐观并发控制(OCC)):ES不会阻止某一数据的访问,然而,如果基础数据在我们读取和写入的间隔中发生了变化,更新就会失败,这时候就由程序来决定如何处理这个冲突。它可以重新读取新数据来进行更新,又或者将这一情况直接反馈给用户。 ES如何实现版本控制(使用es内部版本号):首先得到需要修改的文档,获取版本( _version )号: ``` [hadoop@h153 ~]$ curl -XGET http://192.168.205.153:9200/hui/emp/2 {"_index":"hui","_type":"emp","_id":"2","_version":2,"found":true,"_source":{"name":"zs","age":25}} ``` 在执行更新操作的时候把版本号传过去: ``` [hadoop@h153 ~]$ curl -XPUT http://192.168.205.153:9200/hui/emp/2?version=3 -d '{"name":"zs","age":25}' {"error":{"root_cause":[{"type":"version_conflict_engine_exception","reason":"[emp][2]: version conflict, current [2], provided [3]","shard":"2","index":"hui"}],"type":"version_conflict_engine_exception","reason":"[emp][2]: version conflict, current [2], provided [3]","shard":"2","index":"hui"},"status":409} [hadoop@h153 ~]$ curl -XPUT http://192.168.205.153:9200/hui/emp/2?version=2 -d '{"name":"zs","age":25}'(覆盖) {"_index":"hui","_type":"emp","_id":"2","_version":3,"_shards":{"total":2,"successful":1,"failed":0},"created":false} [hadoop@h153 ~]$ curl -XPOST http://192.168.205.153:9200/hui/emp/2/_update?version=3 -d '{"doc":{"city":"beijing","car":"BMW"}}'(部分更新) {"_index":"hui","_type":"emp","_id":"2","_version":4,"_shards":{"total":2,"successful":1,"failed":0}} ``` 注:如果传递的版本号和待更新的文档的版本号不一致,则会更新失败。 #### ES如何实现版本控制(使用外部版本号) 如果你的数据库已经存在了版本号,或者是可以代表版本的时间戳。这时就可以在es的查询url后面添加来使用这些号码。 注意:版本号码必须要是大于0小于9223372036854775807(Java中long的最大正值)的整数。`version_type=external` es在处理外部版本号的时候,它不再检查是否与请求中指定的数值是否相等,而是检查当前的是否比指定的数值小,如果小,则请求成功。example:`·_version·``·_version·` ``` [hadoop@h153 ~]$ curl -XPUT 'http://192.168.205.153:9200/hui/emp/2?version=10&version_type=external' -d '{"name":"laoxiao"}' {"_index":"hui","_type":"emp","_id":"2","_version":10,"_shards":{"total":2,"successful":1,"failed":0},"created":false} ``` 注意:此处url前后的引号不能省略,否则执行的时候会报错。 ### Elasticsearch的插件 站点插件(以网页形式展现): * BigDesk Plugin (作者 Luká? Vl?ek) * 简介:监控es状态的插件,推荐! * Elasticsearch Head Plugin (作者 Ben Birch) * 简介:很方便对es进行各种操作的客户端。 * Paramedic Plugin (作者 Karel Mina?ík) * 简介:es监控插件 * SegmentSpy Plugin (作者 Zachary Tong) * 简介:查看es索引segment状态的插件 * Inquisitor Plugin (作者 Zachary Tong) * 简介:这个插件主要用来调试你的查询。 这个主要提供的是节点的实时状态监控,包括jvm的情况, linux的情况,elasticsearch的情况: * 安装bin/plugin install lukas-vlcek/bigdesk * 删除bin/plugin remove bigdesk 登录网页查看: ``` http://192.168.205.153:9200/_plugin/bigdesk/ ``` 安装head插件: ``` bin/plugin -install mobz/elasticsearch-head ``` 登录网页查看: ``` http://192.168.205.153:9200/_plugin/head/ ``` (支持谷歌浏览器,不支持360浏览器) 可以通过该命令查看安装了哪些插件: ``` [hadoop@h153 ~]$ ./elasticsearch-2.2.0/bin/plugin list Installed plugins in /home/hadoop/elasticsearch-2.2.0/plugins: - ik - head ``` ### 健康状态查询 命令行查看:`curl http://localhost:9200/_cluster/health?pretty` ``` [hadoop@h153 ~]$ curl '192.168.205.153:9200/_cluster/health?pretty' { "cluster_name" : "my-application", "status" : "yellow", "timed_out" : false, "number_of_nodes" : 1, "number_of_data_nodes" : 1, "active_primary_shards" : 5, "active_shards" : 5, "relocating_shards" : 0, "initializing_shards" : 0, "unassigned_shards" : 5, "delayed_unassigned_shards" : 0, "number_of_pending_tasks" : 0, "number_of_in_flight_fetch" : 0, "task_max_waiting_in_queue_millis" : 0, "active_shards_percent_as_number" : 50.0 } ``` ### 其他命令 ``` [hadoop@localhost elasticsearch-2.2.0]$ curl -XGET http://192.168.205.142:9200/ { "name" : "node-1", "cluster_name" : "my-application", "version" : { "number" : "2.2.0", "build_hash" : "8ff36d139e16f8720f2947ef62c8167a888992fe", "build_timestamp" : "2016-01-27T13:32:39Z", "build_snapshot" : false, "lucene_version" : "5.4.1" }, "tagline" : "You Know, for Search" } [hadoop@localhost elasticsearch-2.2.0]$ curl -XGET 192.168.205.142:9200/_cat/health?v epoch timestamp cluster status node.total node.data shards pri relo init unassign pending_tasks max_task_wait_time active_shards_percent 1489745233 18:07:13 my-application green 3 3 6 3 0 0 0 0 - 100.0% ``` > 来源: > https://blog.csdn.net/m0_37739193/article/details/78228876 ------------- ## (十三):ElasticSearch 可视化管理工具 本文主要介绍几款比较常见的可视工具,供大家自行选择,工具没有好坏之分,只有适合之说,所以,选择合适自己的才是最好的。 工欲善其事,必先利其器。Elasticsearch 和我们的数据库是一样的都需要客户端才可以看到相关数据。 ### 推荐的五种客户端 1.Elasticsearch-Head , Elasticsearch-Head 插件在5.x版本之后已不再维护,界面比较老旧。 2.cerebro 据传该插件不支持ES中5.x以上版本。 3.kinaba 功能强大,但操作复杂,以后可以考虑。 4.Dejavu 也是一个 Elasticsearch 的 Web UI 工具,其 UI界面更符合当下主流的前端页面风格,因此使用起来很方便。但是网上可借鉴的文档较少,我也没有细查。 5.ElasticHD 不依赖ES的插件安装,更便捷;导航栏直接填写对应的ES IP和端口就可以操作Es了。 ### Dejavu 下载、安装、使用 github地址:https://github.com/appbaseio/dejavu/ ###### Docker安装 ``` docker run -p 1358:1358 -d appbaseio/dejavu ``` 启动访问:http://localhost:1358/ ###### 使用效果 这个数据预览页面非常直观,索引/类型/文档 看得一清二楚 ![lmufev1k.png](http://flt-pan.58heshihu.com/blog/typecho/lmufev1k.png) 查询功能 ![lmufeyy2.png](http://flt-pan.58heshihu.com/blog/typecho/lmufeyy2.png) ![lmuff5co.png](http://flt-pan.58heshihu.com/blog/typecho/lmuff5co.png) ### elasticsearch-head 下载、安装、使用 #### Windows ###### 配置Node环境 Head插件是采用HTML编写的,它的运行需要Node.js环境。npm在安装Nodejs时顺带已经安装成功了。 ###### 安装Grunt Grunt是一个基于命令的Javascript工程命令行构建工具。 使用npm安装Grunt的安装命令如下: ``` npm install -g grunt-cli ``` 测试是否安装成功 ``` grunt -version ``` ###### 下载Head插件源码 https://github.com/mobz/elasticsearch-head 到elasticsearch-head-5.0.0 目录下,安装依赖: ``` npm install ``` ###### 修改Elastic search配置 编辑 `elasticsearch-x.x.x/config/elasticsearch.yml`,加入如下配置: ``` http.cors.enabled: true http.cors.allow-origin: "*" ``` 作用是开启HTTP对外提供服务,使 Head插件能够访问Elasticsearch集群,修改完成之后需重启 Elasticsearch。 ###### 修改Head插件配置文件 打开`elasticsearch-head-master/Gruntfile.js`,找到下面connect属性,修改hostname的值为Elasticsearch的访问IP:(默认没有hostname这一项,此时hostname值实际为localhost,所以本处不配置也可以) ``` connect: { server: { options: { hostname: 'localhost', port: 9100, base: '.', keepalive: true } } } ``` ###### 启动Head插件 切换到elasticsearch-head-master/目录下,运行启动命令: ``` grunt server ``` 启动结果如下: ![lmuffs4m.png](http://flt-pan.58heshihu.com/blog/typecho/lmuffs4m.png) 建议将命令写为批处理: ``` cd /d D:\dev\ES\elasticsearch-head-5.0.0 grunt server @cmd /k Docker docker run -d -p 9100:9100 docker.io/mobz/elasticsearch-head:5 ``` ###### 使用 访问 http://localhost:9100 使用效果 ![lmufg3ll.png](http://flt-pan.58heshihu.com/blog/typecho/lmufg3ll.png) ![lmufg6p0.png](http://flt-pan.58heshihu.com/blog/typecho/lmufg6p0.png) ### ElasticHD 下载、安装、使用 目前支持如下功能: ``` ES 实时搜索; ES DashBoard 数据可视化; ES Index Template (在线修改、查看、上传); SQL Converts to DSL; ES 基本查询文档 ``` 不要下载源码,要下载可执行程序:https://github.com/360EntSecGroup-Skylar/ElasticHD/releases/ ![lmufgfx0.png](http://flt-pan.58heshihu.com/blog/typecho/lmufgfx0.png) 在bin下启动bat即可,如果不行,就需要使用cmd启动。 命令: ``` cd D:\Eshome\esHD (这里替换掉你下载解压后的文件夹目录) ElasticHD -p 127.0.0.1:9800 ``` ![lmufglhb.png](http://flt-pan.58heshihu.com/blog/typecho/lmufglhb.png) 如果你觉得每次都这样启动麻烦,可以用个记事本写下来,然后把记事本后缀名改成.bat ,这样就双击启动了。 然后,我们浏览器访问下(如果你启动的服务想要别的电脑访问,就不要使用127.0.0.1 ,要使用局域网IP或者外网的固定IP)。 ![lmufgx1q.png](http://flt-pan.58heshihu.com/blog/typecho/lmufgx1q.png) ![lmufh0mb.png](http://flt-pan.58heshihu.com/blog/typecho/lmufh0mb.png) > 参考来源: > https://blog.csdn.net/feiying0canglang/article/details/126347285 > https://blog.csdn.net/zth_killer/article/details/122744578 ------------- ## (十四):ElasticSearch 性能优化详解 Elasticsearch 作为一个开箱即用的产品,在生产环境上线之后,我们其实不一定能确保其的性能和稳定性。如何根据实际情况提高服务的性能,其实有很多技巧。这章我们分享从实战经验中总结出来的 elasticsearch 性能优化,主要从硬件配置优化、索引优化设置、查询方面优化、数据结构优化、集群架构优化等方面讲解。 ### 硬件配置优化 升级硬件设备配置一直都是提高服务能力最快速有效的手段,在系统层面能够影响应用性能的一般包括三个因素:`CPU`、`内存`和 `IO`,可以从这三方面进行 ES 的性能优化工作。 #### CPU 配置 一般说来,CPU 繁忙的原因有以下几个: * 线程中有无限空循环、无阻塞、正则匹配或者单纯的计算; * 发生了频繁的 GC; * 多线程的上下文切换; 大多数 Elasticsearch 部署往往对 CPU 要求不高。因此,相对其它资源,具体配置多少个(CPU)不是那么关键。你应该选择具有多个内核的现代处理器,常见的集群使用 2 到 8 个核的机器。如果你要在更快的 CPUs 和更多的核数之间选择,选择更多的核数更好。多个内核提供的额外并发远胜过稍微快一点点的时钟频率。 #### 内存配置 如果有一种资源是最先被耗尽的,它可能是内存。排序和聚合都很耗内存,所以有足够的堆空间来应付它们是很重要的。即使堆空间是比较小的时候,也能为操作系统文件缓存提供额外的内存。因为 Lucene 使用的许多数据结构是基于磁盘的格式,Elasticsearch 利用操作系统缓存能产生很大效果。 64 GB 内存的机器是非常理想的,但是 32 GB 和 16 GB 机器也是很常见的。少于8 GB 会适得其反(你最终需要很多很多的小机器),大于 64 GB 的机器也会有问题。 由于 ES 构建基于 lucene,而 lucene 设计强大之处在于 lucene 能够很好的利用操作系统内存来缓存索引数据,以提供快速的查询性能。lucene 的索引文件 segements 是存储在单文件中的,并且不可变,对于 OS 来说,能够很友好地将索引文件保持在 cache 中,以便快速访问;因此,我们很有必要将一半的物理内存留给 lucene;另一半的物理内存留给 ES(JVM heap)。 ###### 内存分配 当机器内存小于 64G 时,遵循通用的原则,50% 给 ES,50% 留给 lucene。 当机器内存大于 64G 时,遵循以下原则: * 如果主要的使用场景是全文检索,那么建议给 ES Heap 分配 4~32G 的内存即可;其它内存留给操作系统,供 lucene 使用(segments cache),以提供更快的查询性能。 * 如果主要的使用场景是聚合或排序,并且大多数是 numerics,dates,geo\_points 以及 not\_analyzed 的字符类型,建议分配给 ES Heap 分配 4~32G 的内存即可,其它内存留给操作系统,供 lucene 使用,提供快速的基于文档的聚类、排序性能。 * 如果使用场景是聚合或排序,并且都是基于 analyzed 字符数据,这时需要更多的 heap size,建议机器上运行多 ES 实例,每个实例保持不超过 50% 的 ES heap 设置(但不超过 32 G,堆内存设置 32 G 以下时,JVM 使用对象指标压缩技巧节省空间),50% 以上留给 lucene。 ###### 禁止 swap 禁止 swap,一旦允许内存与磁盘的交换,会引起致命的性能问题。可以通过在 elasticsearch.yml 中 `bootstrap.memory_lock: true`,以保持 JVM 锁定内存,保证 ES 的性能。 ###### GC 设置 老的版本中推荐默认设置为:`Concurrent-Mark and Sweep(CMS)`,给的理由是当时G1 还有很多 BUG。 原因是:已知JDK 8附带的HotSpot JVM的早期版本存在一些问题,当启用G1GC收集器时,这些问题可能导致索引损坏。受影响的版本早于JDK 8u40随附的HotSpot版本。 实际上如果你使用的JDK8较高版本,或者JDK9+,我推荐你使用G1 GC;因为我们目前的项目使用的就是G1 GC,运行效果良好,对Heap大对象优化尤为明显。修改jvm.options文件,将下面几行: ``` -XX:+UseConcMarkSweepGC -XX:CMSInitiatingOccupancyFraction=75 -XX:+UseCMSInitiatingOccupancyOnly ``` 更改为 ``` -XX:+UseG1GC -XX:MaxGCPauseMillis=50 ``` 其中 `-XX:MaxGCPauseMillis`是控制预期的最高GC时长,默认值为200ms,如果线上业务特性对于GC停顿非常敏感,可以适当设置低一些。但是 这个值如果设置过小,可能会带来比较高的cpu消耗。 G1对于集群正常运作的情况下减轻G1停顿对服务时延的影响还是很有效的,但是如果是你描述的GC导致集群卡死,那么很有可能换G1也无法根本上解决问题。通常都是集群的数据模型或者Query需要优化。 #### 磁盘 硬盘对所有的集群都很重要,对大量写入的集群更是加倍重要(例如那些存储日志数据的)。硬盘是服务器上最慢的子系统,这意味着那些写入量很大的集群很容易让硬盘饱和,使得它成为集群的瓶颈。 在经济压力能承受的范围下,尽量使用固态硬盘(SSD)。固态硬盘相比于任何旋转介质(机械硬盘,磁带等),无论随机写还是顺序写,都会对 IO 有较大的提升。 * 1.如果你正在使用 SSDs,确保你的系统 I/O 调度程序是配置正确的。当你向硬盘写数据,I/O 调度程序决定何时把数据实际发送到硬盘。大多数默认 `*nix`发行版下的调度程序都叫做 cfq(完全公平队列)。 * 2.调度程序分配时间片到每个进程。并且优化这些到硬盘的众多队列的传递。但它是为旋转介质优化的:机械硬盘的固有特性意味着它写入数据到基于物理布局的硬盘会更高效。 * 3.这对 SSD 来说是低效的,尽管这里没有涉及到机械硬盘。但是,deadline 或者 noop 应该被使用。deadline 调度程序基于写入等待时间进行优化,noop 只是一个简单的 FIFO 队列。 这个简单的更改可以带来显著的影响。仅仅是使用正确的调度程序,我们看到了 500 倍的写入能力提升。 如果你使用旋转介质(如机械硬盘),尝试获取尽可能快的硬盘(高性能服务器硬盘,15k RPM 驱动器)。 使用 RAID0 是提高硬盘速度的有效途径,对机械硬盘和 SSD 来说都是如此。没有必要使用镜像或其它 RAID 变体,因为 Elasticsearch 在自身层面通过副本,已经提供了备份的功能,所以不需要利用磁盘的备份功能,同时如果使用磁盘备份功能的话,对写入速度有较大的影响。 最后,避免使用网络附加存储(NAS)。人们常声称他们的 NAS 解决方案比本地驱动器更快更可靠。除却这些声称,我们从没看到 NAS 能配得上它的大肆宣传。NAS 常常很慢,显露出更大的延时和更宽的平均延时方差,而且它是单点故障的。 ### 索引优化设置 索引优化主要是在 Elasticsearch 的插入层面优化,Elasticsearch 本身索引速度其实还是蛮快的,具体数据,我们可以参考官方的 benchmark 数据。我们可以根据不同的需求,针对索引优化。 #### 批量提交 当有大量数据提交的时候,建议采用批量提交(Bulk 操作);此外使用 bulk 请求时,每个请求不超过几十M,因为太大会导致内存使用过大。 比如在做 ELK 过程中,Logstash indexer 提交数据到 Elasticsearch 中,`batch size` 就可以作为一个优化功能点。但是优化 size 大小需要根据文档大小和服务器性能而定。 像 Logstash 中提交文档大小超过 20MB,Logstash 会将一个批量请求切分为多个批量请求。 如果在提交过程中,遇到 EsRejectedExecutionException 异常的话,则说明集群的索引性能已经达到极限了。这种情况,要么提高服务器集群的资源,要么根据业务规则,减少数据收集速度,比如只收集 Warn、Error 级别以上的日志。 #### 增加 Refresh 时间间隔 为了提高索引性能,Elasticsearch 在写入数据的时候,采用延迟写入的策略,即数据先写到内存中,当超过默认1秒(index.refresh_interval)会进行一次写入操作,就是将内存中 segment 数据刷新到磁盘中,此时我们才能将数据搜索出来,所以这就是为什么 Elasticsearch 提供的是近实时搜索功能,而不是实时搜索功能。 如果我们的系统对数据延迟要求不高的话,我们可以通过延长 refresh 时间间隔,可以有效地减少 segment 合并压力,提高索引速度。比如在做全链路跟踪的过程中,我们就将 `index.refresh_interval` 设置为30s,减少 refresh 次数。再如,在进行全量索引时,可以将 refresh 次数临时关闭,即 `index.refresh_interval`设置为`-1`,数据导入成功后再打开到正常模式,比如30s。 在加载大量数据时候可以暂时不用 refresh 和 repliccas,`index.refresh_interval` 设置为`-1`,`index.number_of_replicas` 设置为`0`。 相关原理,请参考:ES 原理之索引文档流程详解 #### 修改 index\_buffer\_size 的设置 索引缓冲的设置可以控制多少内存分配给索引进程。这是一个全局配置,会应用于一个节点上所有不同的分片上。 ``` indices.memory.index_buffer_size: 10% indices.memory.min_index_buffer_size: 48mb ``` `indices.memory.index_buffer_size` 接受一个百分比或者一个表示字节大小的值。默认是10%,意味着分配给节点的总内存的10%用来做索引缓冲的大小。这个数值被分到不同的分片(shards)上。如果设置的是百分比,还可以设置 `min_index_buffer_size` (默认 48mb)和 `max_index_buffer_size`(默认没有上限)。 #### 修改 translog 相关的设置 一是控制数据从内存到硬盘的操作频率,以减少硬盘 IO。可将 `sync_interval` 的时间设置大一些。默认为`5s`。 ``` index.translog.sync_interval: 5s ``` 也可以控制 tranlog 数据块的大小,达到 threshold 大小时,才会 flush 到 lucene 索引文件。默认为512m。 ``` index.translog.flush_threshold_size: 512mb ``` translog我们在 ES原理之索引文档流程详解 也有介绍。 ###### 注意_id字段的使用 `_id` 字段的使用,应尽可能避免自定义 `_id`,以避免针对 ID 的版本管理;建议使用 ES 的默认 ID 生成策略或使用数字类型 ID 做为主键。 ###### 注意\_all字段及\_source字段的使用 `_all` 字段及` _source` 字段的使用,应该注意场景和需要,`_all` 字段包含了所有的索引字段,方便做全文检索,如果无此需求,可以禁用;`_source `存储了原始的 document 内容,如果没有获取原始文档数据的需求,可通过设置 `includes`、`excludes` 属性来定义放入 `_source` 的字段。 #### 合理的配置使用 index 属性 合理的配置使用 index 属性,analyzed 和 `not_analyzed`,根据业务需求来控制字段是否分词或不分词。只有 groupby 需求的字段,配置时就设置成 `not_analyzed`,以提高查询或聚类的效率。 #### 减少副本数量 Elasticsearch 默认副本数量为3个,虽然这样会提高集群的可用性,增加搜索的并发数,但是同时也会影响写入索引的效率。 在索引过程中,需要把更新的文档发到副本节点上,等副本节点生效后在进行返回结束。使用 Elasticsearch 做业务搜索的时候,建议副本数目还是设置为3个,但是像内部 ELK 日志系统、分布式跟踪系统中,完全可以将副本数目设置为1个。 ### 查询方面优化 Elasticsearch 作为业务搜索的近实时查询时,查询效率的优化显得尤为重要。 #### 路由优化 当我们查询文档的时候,Elasticsearch 如何知道一个文档应该存放到哪个分片中呢?它其实是通过下面这个公式来计算出来的。 ``` shard = hash(routing) % number_of_primary_shards ``` routing 默认值是文档的 id,也可以采用自定义值,比如用户 ID。 ###### 不带 routing 查询 在查询的时候因为不知道要查询的数据具体在哪个分片上,所以整个过程分为2个步骤: * 分发:请求到达协调节点后,协调节点将查询请求分发到每个分片上。 * 聚合:协调节点搜集到每个分片上查询结果,再将查询的结果进行排序,之后给用户返回结果。 ###### 带 routing 查询 查询的时候,可以直接根据 routing 信息定位到某个分配查询,不需要查询所有的分配,经过协调节点排序。 向上面自定义的用户查询,如果 routing 设置为 userid 的话,就可以直接查询出数据来,效率提升很多。 #### Filter VS Query 尽可能使用过滤器上下文(Filter)替代查询上下文(Query) * Query:此文档与此查询子句的匹配程度如何? * Filter:此文档和查询子句匹配吗? Elasticsearch 针对 Filter 查询只需要回答「是」或者「否」,不需要像 Query 查询一样计算相关性分数,同时Filter结果可以缓存。 #### 深度翻页 在使用 Elasticsearch 过程中,应尽量避免大翻页的出现。 正常翻页查询都是从 from 开始 size 条数据,这样就需要在每个分片中查询打分排名在前面的 from+size 条数据。协同节点收集每个分配的前 from+size 条数据。协同节点一共会受到 `N*(from+size)` 条数据,然后进行排序,再将其中 from 到 from+size 条数据返回出去。如果 from 或者 size 很大的话,导致参加排序的数量会同步扩大很多,最终会导致 CPU 资源消耗增大。 可以通过使用 Elasticsearch scroll 和 scroll-scan 高效滚动的方式来解决这样的问题。 也可以结合实际业务特点,文档 id 大小如果和文档创建时间是一致有序的,可以以文档 id 作为分页的偏移量,并将其作为分页查询的一个条件。 #### 脚本(script)合理使用 我们知道脚本使用主要有 3 种形式,内联动态编译方式、`_script` 索引库中存储和文件脚本存储的形式;一般脚本的使用场景是粗排,尽量用第二种方式先将脚本存储在 `_script` 索引库中,起到提前编译,然后通过引用脚本 id,并结合 params 参数使用,即可以达到模型(逻辑)和数据进行了分离,同时又便于脚本模块的扩展与维护。 #### Cache的设置及使用 * `QueryCache`: ES查询的时候,使用filter查询会使用query cache, 如果业务场景中的过滤查询比较多,建议将querycache设置大一些,以提高查询速度。 `indices.queries.cache.size:10%`(默认),可设置成百分比,也可设置成具体值,如256mb。 当然也可以禁用查询缓存(默认是开启), 通过`index.queries.cache.enabled:false`设置。 * `FieldDataCache`: 在聚类或排序时,field data cache会使用频繁,因此,设置字段数据缓存的大小,在聚类或排序场景较多的情形下很有必要,可通过indices.fielddata.cache.size:30% 或具体值10GB来设置。但是如果场景或数据变更比较频繁,设置cache并不是好的做法,因为缓存加载的开销也是特别大的。 * `ShardRequestCache`: 查询请求发起后,每个分片会将结果返回给协调节点(Coordinating Node), 由协调节点将结果整合。如果有需求,可以设置开启; 通过设置index.requests.cache.enable: true来开启。不过,shard request cache只缓存hits.total, aggregations, suggestions类型的数据,并不会缓存hits的内容。也可以通过设置indices.requests.cache.size: 1%(默认)来控制缓存空间大小。 #### 更多查询优化经验 * query\_string 或 multi\_match的查询字段越多, 查询越慢。可以在mapping阶段,利用copy\_to属性将多字段的值索引到一个新字段,multi\_match时,用新的字段查询。 * 日期字段的查询, 尤其是用now 的查询实际上是不存在缓存的,因此, 可以从业务的角度来考虑是否一定要用now, 毕竟利用query cache 是能够大大提高查询效率的。 * 查询结果集的大小不能随意设置成大得离谱的值, 如query.setSize不能设置成 Integer.MAX_VALUE, 因为ES内部需要建立一个数据结构来放指定大小的结果集数据。 * 避免层级过深的聚合查询, 层级过深的aggregation , 会导致内存、CPU消耗,建议在服务层通过程序来组装业务,也可以通过pipeline的方式来优化。 * 复用预索引数据方式来提高AGG性能: 如通过 terms aggregations 替代 range aggregations, 如要根据年龄来分组,分组目标是: 少年(14岁以下) 青年(14-28) 中年(29-50) 老年(51以上), 可以在索引的时候设置一个age\_group字段,预先将数据进行分类。从而不用按age来做range aggregations, 通过age\_group字段就可以了。 #### 通过开启慢查询配置定位慢查询 不论是数据库还是搜索引擎,对于问题的排查,开启慢查询日志是十分必要的,ES 开启慢查询的方式有多种,但是最常用的是调用模板 API 进行全局设置: ``` PUT /_template/{TEMPLATE_NAME} { "template":"{INDEX_PATTERN}", "settings" : { "index.indexing.slowlog.level": "INFO", "index.indexing.slowlog.threshold.index.warn": "10s", "index.indexing.slowlog.threshold.index.info": "5s", "index.indexing.slowlog.threshold.index.debug": "2s", "index.indexing.slowlog.threshold.index.trace": "500ms", "index.indexing.slowlog.source": "1000", "index.search.slowlog.level": "INFO", "index.search.slowlog.threshold.query.warn": "10s", "index.search.slowlog.threshold.query.info": "5s", "index.search.slowlog.threshold.query.debug": "2s", "index.search.slowlog.threshold.query.trace": "500ms", "index.search.slowlog.threshold.fetch.warn": "1s", "index.search.slowlog.threshold.fetch.info": "800ms", "index.search.slowlog.threshold.fetch.debug": "500ms", "index.search.slowlog.threshold.fetch.trace": "200ms" }, "version" : 1 } PUT {INDEX_PAATERN}/_settings { "index.indexing.slowlog.level": "INFO", "index.indexing.slowlog.threshold.index.warn": "10s", "index.indexing.slowlog.threshold.index.info": "5s", "index.indexing.slowlog.threshold.index.debug": "2s", "index.indexing.slowlog.threshold.index.trace": "500ms", "index.indexing.slowlog.source": "1000", "index.search.slowlog.level": "INFO", "index.search.slowlog.threshold.query.warn": "10s", "index.search.slowlog.threshold.query.info": "5s", "index.search.slowlog.threshold.query.debug": "2s", "index.search.slowlog.threshold.query.trace": "500ms", "index.search.slowlog.threshold.fetch.warn": "1s", "index.search.slowlog.threshold.fetch.info": "800ms", "index.search.slowlog.threshold.fetch.debug": "500ms", "index.search.slowlog.threshold.fetch.trace": "200ms" } ``` 这样,在日志目录下的慢查询日志就会有输出记录必要的信息了。 ``` {CLUSTER_NAME}_index_indexing_slowlog.log {CLUSTER_NAME}_index_search_slowlog.log ``` ### 数据结构优化 基于 Elasticsearch 的使用场景,文档数据结构尽量和使用场景进行结合,去掉没用及不合理的数据。 #### 尽量减少不需要的字段 如果 Elasticsearch 用于业务搜索服务,一些不需要用于搜索的字段最好不存到 ES 中,这样即节省空间,同时在相同的数据量下,也能提高搜索性能。 避免使用动态值作字段,动态递增的 mapping,会导致集群崩溃;同样,也需要控制字段的数量,业务中不使用的字段,就不要索引。控制索引的字段数量、mapping 深度、索引字段的类型,对于 ES 的性能优化是重中之重。 以下是 ES 关于字段数、mapping 深度的一些默认设置: ``` index.mapping.nested_objects.limit: 10000 index.mapping.total_fields.limit: 1000 index.mapping.depth.limit: 20 ``` #### Nested Object vs Parent/Child 尽量避免使用 nested 或 parent/child 的字段,能不用就不用;nested query 慢,parent/child query 更慢,比 nested query 慢上百倍;因此能在 mapping 设计阶段搞定的(大宽表设计或采用比较 smart 的数据结构),就不要用父子关系的 mapping。 如果一定要使用 nested fields,保证 nested fields 字段不能过多,目前 ES 默认限制是 50。因为针对 1 个 document,每一个 nested field,都会生成一个独立的 document,这将使 doc 数量剧增,影响查询效率,尤其是 JOIN 的效率。 ``` index.mapping.nested_fields.limit: 50 ``` ![lmufk35t.png](http://flt-pan.58heshihu.com/blog/typecho/lmufk35t.png) #### 选择静态映射,非必需时,禁止动态映射 尽量避免使用动态映射,这样有可能会导致集群崩溃,此外,动态映射有可能会带来不可控制的数据类型,进而有可能导致在查询端出现相关异常,影响业务。 此外,Elasticsearch 作为搜索引擎时,主要承载 query 的匹配和排序的功能,那数据的存储类型基于这两种功能的用途分为两类,一是需要匹配的字段,用来建立倒排索引对 query 匹配用,另一类字段是用做粗排用到的特征字段,如 ctr、点击数、评论数等等。 ###### document 模型设计 对于 MySQL,我们经常有一些复杂的关联查询。在 es 里该怎么玩儿,es 里面的复杂的关联查询尽量别用,一旦用了性能一般都不太好。 最好是先在 Java 系统里就完成关联,将关联好的数据直接写入 es 中。搜索的时候,就不需要利用 es 的搜索语法来完成 join 之类的关联搜索了。 document 模型设计是非常重要的,很多操作,不要在搜索的时候才想去执行各种复杂的乱七八糟的操作。es 能支持的操作就那么多,不要考虑用 es 做一些它不好操作的事情。如果真的有那种操作,尽量在 document 模型设计的时候,写入的时候就完成。另外对于一些太复杂的操作,比如 join/nested/parent-child 搜索都要尽量避免,性能都很差的。 ### 集群架构设计 合理的部署 Elasticsearch 有助于提高服务的整体可用性。 #### 主节点、数据节点和协调节点分离 Elasticsearch 集群在架构拓朴时,采用主节点、数据节点和负载均衡节点分离的架构,在 5.x 版本以后,又可将数据节点再细分为“Hot-Warm”的架构模式。 Elasticsearch 的配置文件中有 2 个参数,node.master 和 node.data。这两个参数搭配使用时,能够帮助提供服务器性能。 ###### 主(master)节点 配置 node.master:true 和 node.data:false,该 node 服务器只作为一个主节点,但不存储任何索引数据。我们推荐每个集群运行3 个专用的 `master 节点来提供最好的弹性。使用时,你还需要将 discovery.zen.minimum_master_nodes setting` 参数设置为 `2`,以免出现脑裂(split-brain)的情况。用 3 个专用的 master 节点,专门负责处理集群的管理以及加强状态的整体稳定性。因为这 3 个 master 节点不包含数据也不会实际参与搜索以及索引操作,在 JVM 上它们不用做相同的事,例如繁重的索引或者耗时,资源耗费很大的搜索。因此不太可能会因为垃圾回收而导致停顿。因此,master 节点的 CPU,内存以及磁盘配置可以比 data 节点少很多的。 ###### 数据(data)节点 配置 `node.master:false` 和 `node.data:true`,该 node 服务器只作为一个数据节点,只用于存储索引数据,使该 node 服务器功能单一,只用于数据存储和数据查询,降低其资源消耗率。 在 Elasticsearch 5.x 版本之后,data 节点又可再细分为“Hot-Warm”架构,即分为热节点(hot node)和暖节点(warm node)。 hot 节点: hot 节点主要是索引节点(写节点),同时会保存近期的一些频繁被查询的索引。由于进行索引非常耗费 CPU 和 IO,即属于 IO 和 CPU 密集型操作,建议使用 SSD 的磁盘类型,保持良好的写性能;我们推荐部署最小化的 3 个 hot 节点来保证高可用性。根据近期需要收集以及查询的数据量,可以增加服务器数量来获得想要的性能。 将节点设置为 hot 类型需要 elasticsearch.yml 如下配置: ``` node.attr.box_type: hot ``` 如果是针对指定的 index 操作,可以通过 settings 设置 `index.routing.allocation.require.box_type: hot` 将索引写入 hot 节点。 warm 节点: 这种类型的节点是为了处理大量的,而且不经常访问的只读索引而设计的。由于这些索引是只读的,warm 节点倾向于挂载大量磁盘(普通磁盘)来替代 SSD。内存、CPU 的配置跟 hot 节点保持一致即可;节点数量一般也是大于等于 3 个。 将节点设置为 warm 类型需要 elasticsearch.yml 如下配置: ``` node.attr.box_type: warm ``` 同时,也可以在 elasticsearch.yml 中设置 `index.codec:best_compression` 保证 warm 节点的压缩配置。 当索引不再被频繁查询时,可通过 i`ndex.routing.allocation.require.box_type:warm`,将索引标记为 warm,从而保证索引不写入 hot 节点,以便将 SSD 磁盘资源用在刀刃上。一旦设置这个属性,ES 会自动将索引合并到 warm 节点。 协调(coordinating)节点 协调节点用于做分布式里的协调,将各分片或节点返回的数据整合后返回。该节点不会被选作主节点,也不会存储任何索引数据。该服务器主要用于查询负载均衡。在查询的时候,通常会涉及到从多个 node 服务器上查询数据,并将请求分发到多个指定的 node 服务器,并对各个 node 服务器返回的结果进行一个汇总处理,最终返回给客户端。在 ES 集群中,所有的节点都有可能是协调节点,但是,可以通过设置 node.master、node.data、node.ingest 都为 false 来设置专门的协调节点。需要较好的 CPU 和较高的内存。 * node.master:false和node.data:true,该node服务器只作为一个数据节点,只用于存储索引数据,使该node服务器功能单一,只用于数据存储和数据查询,降低其资源消耗率。 * node.master:true和node.data:false,该node服务器只作为一个主节点,但不存储任何索引数据,该node服务器将使用自身空闲的资源,来协调各种创建索引请求或者查询请求,并将这些请求合理分发到相关的node服务器上。 * node.master:false和node.data:false,该node服务器即不会被选作主节点,也不会存储任何索引数据。该服务器主要用于查询负载均衡。在查询的时候,通常会涉及到从多个node服务器上查询数据,并将请求分发到多个指定的node服务器,并对各个node服务器返回的结果进行一个汇总处理,最终返回给客户端。 #### 关闭 data 节点服务器中的 http 功能 针对 Elasticsearch 集群中的所有数据节点,不用开启 http 服务。将其中的配置参数这样设置,http.enabled:false,同时也不要安装 head, bigdesk, marvel 等监控插件,这样保证 data 节点服务器只需处理创建/更新/删除/查询索引数据等操作。 http 功能可以在非数据节点服务器上开启,上述相关的监控插件也安装到这些服务器上,用于监控 Elasticsearch 集群状态等数据信息。这样做一来出于数据安全考虑,二来出于服务性能考虑。 #### 一台服务器上最好只部署一个 node 一台物理服务器上可以启动多个 node 服务器节点(通过设置不同的启动 port),但一台服务器上的 CPU、内存、硬盘等资源毕竟有限,从服务器性能考虑,不建议一台服务器上启动多个 node 节点。 #### 集群分片设置 ES 一旦创建好索引后,就无法调整分片的设置,而在 ES 中,一个分片实际上对应一个 lucene 索引,而 lucene 索引的读写会占用很多的系统资源,因此,分片数不能设置过大;所以,在创建索引时,合理配置分片数是非常重要的。一般来说,我们遵循一些原则: * 控制每个分片占用的硬盘容量不超过 ES 的最大 JVM 的堆空间设置(一般设置不超过 32 G,参考上面的 JVM 内存设置原则),因此,如果索引的总容量在 500 G 左右,那分片大小在 16 个左右即可;当然,最好同时考虑原则 2。 * 考虑一下 node 数量,一般一个节点有时候就是一台物理机,如果分片数过多,大大超过了节点数,很可能会导致一个节点上存在多个分片,一旦该节点故障,即使保持了 1 个以上的副本,同样有可能会导致数据丢失,集群无法恢复。所以,一般都设置分片数不超过节点数的 3 倍。 > 链接: > https://pdai.tech/md/db/nosql-es/elasticsearch-y-peformance.html ------------- ## (十五):ElasticSearch 性能监控 ### 我们要监控哪些Elasticsearch metric ![lmufnux1.png](http://flt-pan.58heshihu.com/blog/typecho/lmufnux1.png) Elasticsearch 提供了大量的 Metric,可以帮助您检测到问题的迹象,在遇到节点不可用、out-of-memory、long garbage collection times 的时候采取相应措施。 一些关键的检测如下: * Search and indexing performance(搜索、索引性能) * Memory and garbage collection * Host-level system and network metrics * Cluster health and node availability * Resource saturation(饱和) and errors 这里提供了一个`metric 搜集和监控的框架 Monitoring 101 series`,所有这些指标都可以通过 Elasticsearch 的 API 以及 Elasticsearch 的 Marvel 和 Datadog 等通用监控工具访问。 #### 搜索性能指标 搜索请求是Elasticsearch中的两个主要请求类型之一,另一个是索引请求。 这些请求有时类似于传统数据库系统中的读写请求。 Elasticsearch提供与搜索过程的两个主要阶段(查询和获取)相对应的度量。 下图显示了从开始到结束的搜索请求的路径。 step1.客户端向Node 2 发送搜索请求 ![lmufp3ma.png](http://flt-pan.58heshihu.com/blog/typecho/lmufp3ma.png) step2.Node 2(此时客串协调角色)将查询请求发送到索引中的每一个分片的副本 ![lmufqbjx.png](http://flt-pan.58heshihu.com/blog/typecho/lmufqbjx.png) step3. 每个分片(Lucene实例,迷你搜素引擎)在本地执行查询,然后将结果交给Node 2。Node 2 sorts and compiles them into a global priority queue. ![lmufqgio.png](http://flt-pan.58heshihu.com/blog/typecho/lmufqgio.png) step4. Node 2发现需要获取哪些文档,并向相关的分片发送多个GET请求。 ![lmufrb4r.png](http://flt-pan.58heshihu.com/blog/typecho/lmufrb4r.png) step5. 每个分片loads documents然后将他们返回给Node 2 ![lmufrf01.png](http://flt-pan.58heshihu.com/blog/typecho/lmufrf01.png) step6. Node 2将搜索结果交付给客户端 ![lmufrikn.png](http://flt-pan.58heshihu.com/blog/typecho/lmufrikn.png) 节点处理时,由谁分发,就由谁交付。 如果您使用Elasticsearch主要用于搜索,或者如果搜索是面向客户的功能。您应该监视查询延迟和设定阈值。 监控关于查询和提取的相关指标很重要,可以帮助您确定搜索随时间的变化。 例如,您可能希望跟踪查询请求的尖峰和长期增长,以便您可以做好准备。 ![lmufrs03.png](http://flt-pan.58heshihu.com/blog/typecho/lmufrs03.png) 搜索性能指标的要点: * Query load: 监控当前正在进行的查询数量可以让您了解群集在任何特定时刻处理的请求数量。您可能还想监视搜索线程池队列的大小,稍后我们将在本文中进一步解释。 * Query latency: 虽然Elasticsearch没有明确提供此度量标准,但监控工具可以帮助您使用可用的指标来计算平均查询延迟,方法是以定期查询总查询次数和总经过时间。 如果延迟超过阈值,则设置警报,如果触发,请查找潜在的资源瓶颈,或调查是否需要优化查询。 * Fetch latency: 搜索过程的第二部分,即提取阶段通常比查询阶段要少得多的时间。如果您注意到这一指标不断增加,可能是磁盘性能不好、highlighting影响、requesting too many results的原因。 #### 索引性能指标 索引请求类似于传统数据库系统中的写入请求,如果es的写入工作量很重,那么监控和分析您能够如何有效地使用新数据更新索引非常重要。在了解指标之前,让我们来探索Elasticsearch更新索引的过程,在新数据被添加进索引、更新或删除已有数据,索引中的每个shard都有两个过程:refresh 和 flush Index fresh 新索引的文档不能立马被搜索的。 首先,它们被写入一个内存中的缓冲区(in-memory buffer),等待下一次索引刷新,默认情况下每秒一次。刷新是以in-memory buffer为基础创建in-memory segment的过程(The refresh process creates a new in-memory segment from the contents of the in-memory buffer )。这样索引进的文档才能是可被搜索的,创建完segment后,清空buffer 如下图: ![lmufs7qf.png](http://flt-pan.58heshihu.com/blog/typecho/lmufs7qf.png) ###### A special segment on segments 索引由shards构成,shard又由很多segments组成,The core data structure from Lucene, a segment is essentially a change set for the index. 这些segments在每次刷新的时候被创建,随后会在后台进行合并,以确保资源的高效利用(每个segment都要占file handles、memory、CPU) segments 是mini的倒排索引,这些倒排索引映射了terms到documents。每当搜索索引的时候,每个主副shards都必须被遍历。更深一步说shards上的每个segment会被依次搜索。 segment是不可变的,因此updating a document 意味着如下: * writing the information to a new segment during the refresh process * marking the old information as deleted 当多个outdated segment合并后才会被删除。(意思是不单个删除,合并后一起删)。 ###### Index flush 在新索引的document添加到in-memory buffer的同时,它们也会被附加到分片的translog(a persistent, write-ahead transaction log of operations)中。每隔30分钟,或者每当translog达到最大大小(默认情况下为512MB)时,将触发flush 。在flush 期间,在in-memory buffer上的documents会被refreshed(存到新的segments上),所有内存中的segments都提交到磁盘,并且translog被清空。 translog有助于防止节点发生故障时的数据丢失。It is designed to help a shard recover operations that may otherwise have been lost between flushes. 这个translog每5秒将操作信息(索引,删除,更新或批量请求(以先到者为准))固化到磁盘上。 ![lmufsh4a.png](http://flt-pan.58heshihu.com/blog/typecho/lmufsh4a.png) Elasticsearch提供了许多指标,可用于评估索引性能并优化更新索引的方式。 ![lmufsp5b.png](http://flt-pan.58heshihu.com/blog/typecho/lmufsp5b.png) ###### 索引性能指标的要点: Indexing latency: Elasticsearch不会直接公开此特定指标,但是监控工具可以帮助您从可用的`index_total`和`index_time_in_millis`指标计算平均索引延迟。 如果您注意到延迟增加,您可能会一次尝试索引太多的文档(Elasticsearch的文档建议从5到15兆字节的批量索引大小开始,并缓慢增加)。如果您计划索引大量文档,并且不需要立即可用于搜索。则可以通过减少刷新频率来优化。索引设置API使您能够暂时禁用刷间隔。 ``` curl -XPUT <nameofhost>:9200/<name_of_index>/_settings -d '{ "index" : { "refresh_interval" : "-1" } }' ``` 完成索引后,您可以恢复为默认值`“1s”</name_of_index>` Flush latency: 在flush完成之前,数据不会被固化到磁盘中。因此追踪flush latency很有用。比如我们看到这个指标稳步增长,表明磁盘性能不好。这个问题将最终导致无法向索引添加新的数据。 可以尝试降低`index.translog.flush_threshold_size`。这个设置决定translog的最大值(在flush被触发前)。 #### 内存使用和GC指标 在运行Elasticsearch时,内存是您要密切监控的关键资源之一。 Elasticsearch和Lucene以两种方式利用节点上的所有可用RAM:JVM heap和文件系统缓存。 Elasticsearch运行在Java虚拟机(JVM)中,这意味着JVM垃圾回收的持续时间和频率将成为其他重要的监控领域。 ###### JVM heap: A Goldilocks tale Elasticsearch强调了JVM堆大小的重要性,这是“正确的” - 不要将其设置太大或太小,原因如下所述。 一般来说,Elasticsearch的经验法则是将少于50%的可用RAM分配给JVM堆,而不会超过32 GB。 您分配给Elasticsearch的堆内存越少,Lucene就可以使用更多的RAM,这很大程度上依赖于文件系统缓存来快速提供请求。 但是,您也不想将堆大小设置得太小,因为应用程序面临来自频繁GC的不间断暂停,可能会遇到内存不足错误或吞吐量降低的问题 Elasticsearch的默认安装设置了1 GB的JVM heap大小,对于大多数用例来说,太小了。 您可以将所需的heap大小导出为环境变量并重新启动Elasticsearch: ``` export ES_HEAP_SIZE=10g ``` 如上我们设置了es heap大小为10G,通过如下命令进行校验: ``` curl -XGET http://:9200/_cat/nodes?h=heap.max ``` ###### Garbage collection Elasticsearch依靠垃圾收集过程来释放heap memory。因为垃圾收集使用资源(为了释放资源!),您应该注意其频率和持续时间,以查看是否需要调整heap大小。设置过大的heap会导致GC时间过长,这些长时间的停顿会让集群错误的认为该节点已经脱离。 ![lmuft53j.png](http://flt-pan.58heshihu.com/blog/typecho/lmuft53j.png) ###### JVM指标的要点 ![lmuftbor.png](http://flt-pan.58heshihu.com/blog/typecho/lmuftbor.png) * JVM heap in use: 当JVM heap 使用率达到75%时,es启动GC。如上图所示,可以监控node的JVM heap,并且设置一个警报,确认哪个节点是否一直超过%85。如果一直超过,则表明垃圾的收集已经跟不上垃圾的产生。此时可以通过增加heap(需要满足建议法则不超过32G),或者通过增加节点来扩展集群,分散压力。 * JVM heap used vs. JVM heap committed: 与commit的内存(保证可用的数量)相比,了解当前正在使用多少JVM heap的情况可能会有所帮助。heap memory的图一般是个锯齿图,在垃圾收集的时候heap上升,当收集完成后heap下降。如果这个锯齿图向上偏移,说明垃圾的收集速度低于rate of object creation,这可能会导致GC时间放缓,最终OutOfMemoryErrors。 * Garbage collection duration and frequency: Both young- and old-generation garbage collectors undergo “stop the world” phases, as the JVM halts execution of the program to collect dead objects。在此期间节点cannot complete any task。主节点每30秒会去检查其他节点的状态,如果任何节点的垃圾回收时间超过30秒,则会导致主节点任务该节点脱离集群。 * Memory usage: 如上所述,es非常会利用除了分配给JVM heap的任何RAM。像Kafka一样,es被设计为依赖操作系统的文件系统缓存来快速可靠地提供请求。 许多变量决定了Elasticsearch是否成功读取文件系统缓存,如果segment file最近由es写入到磁盘,它已经in the cache。然而如果节点被关闭并重新启动,首次查询某个segment的时候,数据很可能是必须从磁盘中读取,这是确保您的群集保持稳定并且节点不会崩溃的重要原因之一。 总的来说,监控节点上的内存使用情况非常重要,并且尽可能多给es分配RAM,so it can leverage the speed of the file system cache without running out of space。 #### es主机的网络和系统 ![lmuftkya.png](http://flt-pan.58heshihu.com/blog/typecho/lmuftkya.png) 虽然Elasticsearch通过API提供了许多特定于应用程序的指标,但您也应该从每个节点收集和监视几个主机级别的指标。 Host指标要点: * Disk space: 如果数据很多,这个指标很关键。如果disk space 过小,讲不能插入或更新任何内容,并且节点会挂掉。可以使用Curator这样的工具来删除特定的索引以保持disk的可用性。 如果不让删除索引,另外的办法是添加磁盘、添加节点。请记住analyzed field占用磁盘的空间远远高于non-analyzed fields。 * I/O utilization: 由于创建,查询和合并segment,Elasticsearch会对磁盘进行大量写入和读取,于具有不断遇到大量I / O活动的节点的写入繁重的集群,Elasticsearch建议使用SSD来提升性能。 ![lmufts9y.png](http://flt-pan.58heshihu.com/blog/typecho/lmufts9y.png) * CPU utilization: 在每个节点类型的热图(如上所示)中可视化CPU使用情况可能会有所帮助。 例如,您可以创建三个不同的图表来表示集群中的每组节点(例如,数据节点,主节点,客户端节点), 如果看到CPU使用率的增加,这通常是由于搜索量大或索引工作负载引起的。 如果需要,可以添加更多节点来重新分配负载。 * Network bytes sent/received: 节点之间的通讯是集群平衡的关键。因此需要监控network来确保集群的health以及对集群的需求(例如,segment在节点之间进行复制或重新平衡)。 Elasticsearch提供有关集群通信的指标,但也可以查看发送和接收的字节数,以查看network接收的流量。 * Open file descriptors: 文件描述符用于节点到节点的通信,客户端连接和文件操作。如果这个number达到了系统的最大值,则只有在旧的连接和文件操作关闭之后才能进行新的连接和文件操作。 如果超过80%的可用文件描述符被使用,您可能需要增加系统的最大文件描述符数量。大多数Linux系统每个进程只允许1024个文件描述符。 在生产中使用Elasticsearch时,您应该将操作系统文件描述符计数重新设置为更大,如64,000。 * HTTP connections: ![lmufucr4.png](http://flt-pan.58heshihu.com/blog/typecho/lmufucr4.png) 可以用任何语言发送请求,但Java将使用RESTful API通过HTTP与Elasticsearch进行通信。 如果打开的HTTP连接总数不断增加,可能表示您的HTTP客户端没有正确建立持久连接。 重新建立连接会在您的请求响应时间内添加额外的毫秒甚至秒。 确保您的客户端配置正确,以避免对性能造成负面影响,或使用已正确配置HTTP连接的官方Elasticsearch客户端。 #### 集群健康和节点可用性 ![lmufuqvy.png](http://flt-pan.58heshihu.com/blog/typecho/lmufuqvy.png) ###### 指标要点 * Cluster status: 如果集群状态为黄色,则至少有一个副本分片未分配或丢失。 搜索结果仍将完成,但如果更多的分片消失,您可能会丢失数据。 红色的群集状态表示至少有一个主分片丢失,并且您缺少数据,这意味着搜索将返回部分结果。 您也将被阻止索引到该分片。 Consider setting up an alert to trigger if status has been yellow for more than 5 min or if the status has been red for the past minute. * Initializing and unassigned shards: 当首次创建索引或者重启节点,其分片将在转换到“started”或“unassigned”状态之前暂时处于“initializing”状态,此时主节点正在尝试将分片分配到集群中的数据节点。 如果您看到分片仍处于初始化或未分配状态太长时间,则可能是您的集群不稳定的警告信号。 #### 资源saturation and errors es节点使用线程池来管理线程如何消耗内存和CPU。 由于线程池设置是根据处理器数量自动配置的,所以调整它们通常没有意义。However, it’s a good idea to keep an eye on queues and rejections to find out if your nodes aren’t able to keep up; 如果无法跟上,您可能需要添加更多节点来处理所有并发请求。Fielddata和过滤器缓存使用是另一个要监视的地方,as evictions may point to inefficient queries or signs of memory pressure。 ###### Thread pool queues and rejections 每个节点维护许多类型的线程池; 您要监视的确切位置将取决于您对es的具体用途,一般来说,监控的最重要的是搜索,索引,merge和bulk,它们与请求类型(搜索,索引,合并和批量操作)相对应。 线程池队列的大小反应了当前等待的请求数。 队列允许节点跟踪并最终服务这些请求,而不是丢弃它们。 一旦超过线程池的maximum queue size,Thread pool rejections就会发生。 ![lmufuzoi.png](http://flt-pan.58heshihu.com/blog/typecho/lmufuzoi.png) 指标要点: * Thread pool queues: 大队列不理想,因为它们耗尽资源,并且如果节点关闭,还会增加丢失请求的风险。如果你看到线程池rejected稳步增加,你可能希望尝试减慢请求速率(如果可能),增加节点上的处理器数量或增加群集中的节点数量。 如下面的截图所示,查询负载峰值与搜索线程池队列大小的峰值相关,as the node attempts to keep up with rate of query requests。 ![lmufv6b4.png](http://flt-pan.58heshihu.com/blog/typecho/lmufv6b4.png) * Bulk rejections and bulk queues: 批量操作是一次发送许多请求的更有效的方式。 通常,如果要执行许多操作(创建索引或添加,更新或删除文档),则应尝试以批量操作发送请求,而不是发送许多单独的请求。 bulk rejections 通常与在一个批量请求中尝试索引太多文档有关。根据Elasticsearch的文档,批量rejections并不是很需要担心的事。However, you should try implementing a linear or exponential backoff strategy to efficiently deal with bulk rejections。 * Cache usage metrics: 每个查询请求都会发送到索引中的每个分片的每个segment中,Elasticsearch caches queries on a per-segment basis to speed up response time。另一方面,如果您的缓存过多地堆积了这些heap,那么它们可能会减慢速度,而不是加快速度! 在es中,文档中的每个字段可以以两种形式存储:exact value 和 full text。 例如,假设你有一个索引,它包含一个名为location的type。每个type的文档有个字段叫city。which is stored as an analyzed string。你索引了两个文档,一个的city字段为“St. Louis”,另一个的city字段为“St. Paul”。在倒排索引中存储时将变成小写并忽略掉标点符号,如下表 ![lmufvfz5.png](http://flt-pan.58heshihu.com/blog/typecho/lmufvfz5.png) 分词的好处是你可以搜索st。结果会搜到两个。如果将city字段保存为exact value,那只能搜“St. Louis”, 或者 “St. Paul”。 Elasticsearch使用两种主要类型的缓存来更快地响应搜索请求:fielddata和filter。 * Fielddata cache: fielddata cache 在字段排序或者聚合时使用。 a process that basically has to uninvert the inverted index to create an array of every field value per field, in document order. For example, if we wanted to find a list of unique terms in any document that contained the term “st” from the example above, we would: 1.扫描倒排索引查看哪些文档(documents)包含这个term(在本例中为Doc1和Doc2) 。 2.对1中的每个步骤,通过索引中的每个term 从文档中来收集tokens,创建如下结构。 ![lmufvo86.png](http://flt-pan.58heshihu.com/blog/typecho/lmufvo86.png) 3.现在反向索引被再反向,从doc中compile 独立的tokens(st, louis, and paul)。compile这样的fielddata可能会消耗大量堆内存。特别是大量的documents和terms的情况下。 所有字段值都将加载到内存中。对于1.3之前的版本,fielddata缓存大小是无限制的。 从1.3版开始,Elasticsearch添加了一个fielddata断路器,如果查询尝试加载需要超过60%的堆的fielddata,则会触发。 * Filter cache: 过滤缓存也使用JVM堆。 在2.0之前的版本中,Elasticsearch自动缓存过滤的查询,最大值为堆的10%,并且将最近最少使用的数据逐出。 从版本2.0开始,Elasticsearch会根据频率和段大小自动开始优化其过滤器缓存(缓存仅发生在索引中少于10,000个文档的段或小于总文档的3%)。 因此,过滤器缓存指标仅适用于使用2.0之前版本的Elasticsearch用户。 例如,过滤器查询可以仅返回年份字段中的值在2000-2005范围内的文档。 在首次执行过滤器查询时,Elasticsearch将创建一个与其相匹配的文档的位组(如果文档匹配则为1,否则为0)。 使用相同过滤器后续执行查询将重用此信息。 无论何时添加或更新新的文档,也会更新bitset。 如果您在2.0之前使用的是Elasticsearch版本,那么您应该关注过滤器缓存以及驱逐指标(更多关于以下内容)。 ![lmufwzvr.png](http://flt-pan.58heshihu.com/blog/typecho/lmufwzvr.png) * Fielddata cache evictions: 理想情况下,我们需要限制fielddata evictions的数量,因为他们很吃I/O。如果你看到很多evictions并且你又不能增加内存。es建议限制fielddata cache的大小为20%的heap size。这些是可以在elasticsearch.yml中进行配置的。当fielddata cache达到20%的heap size时,es将驱逐最近最少使用的fielddata,然后允许您将新的fielddata加载到缓存中。 es还建议使用doc values,因为它们与fielddata的用途相同。由于它们存储在磁盘上,它们不依赖于JVM heap。尽管doc values不能被用来分析字符串, they do save fielddata usage when aggregating or sorting on other types of fields。在2.0版本后,doc values会在文档被index的时候自动创建,which has reduced fielddata/heap usage for many users。 * Filter cache evictions: 如前所述,filter cache eviction 指标只有在es2.0之前的版本可用。每个segment都维护自己的filter cache eviction。因为eviction在大的segment上操作成本较高,没有的明确的方法来评估eviction。但是如果你发现eviction很频繁,表明你并没有很好地利用filter,此时你需要重新创建filter,即使放弃原有的缓存,你也可能需要调整查询方式(用bool query 而不是 and/or/not filter)。 * Pending tasks: ![lmufxl1d.png](http://flt-pan.58heshihu.com/blog/typecho/lmufxl1d.png) pending task只能由主节点来进行处理,这些任务包括创建索引并将shards分配给节点。任务分优先次序。如果任务的产生比处理速度更快,将会产生堆积。待处理任务的数量是您的群集运行平稳的良好指标,如果您的主节点非常忙,并且未完成的任务数量不会减少,我们需要仔细检查原因。 * Unsuccessful GET requests: GET请求比正常的搜索请求更简单 - 它根据其ID来检索文档。 get-by-ID请求不成功意味着找不到文档 ### 如何收集 ElasticSearch 的指标 #### ElasticSearch 指标收集工具 * 集群健康状态和各种性能 API * 有表格数据的_cat API * 开源的监控工具 (ElasticHQ, Kopf, Marvel) ### ElasticSearch 的 RESTFull API + JSON 默认情况下,集群对外开启了 9200 端口,用于整个集群的管理操作、索引的增删改查,以及整体集群、节点、索引的状态信息,通常需要关注如下几个对外 API。 * Node Stats API: 节点状态 API * Cluster Stats API: 集群状态 API * Index Stats API: 索引状态 API * Cluster Health API: 集群健康状态 API * Pending Tasks API: 阻塞任务状态 API 如下表列出了一些常见的指标以及对应的 API 接口。 ![lmufyc0f.png](http://flt-pan.58heshihu.com/blog/typecho/lmufyc0f.png) #### Node Stats API Node 状态接口是一个功能强大的工具,能提供除集群运行状况和挂起任务外几乎全部的性能指标。 注意: 对于节点状态 API 来讲,最重要的就是 indices 。在 ElasticSearch 的所有对外接口参数中,pretty 的 URI 参数标识以 json 格式进行输出,否则将输出的是字符串。 ``` # 查看集群全部节点的指标 $ curl "localhost:9200/_nodes/stats" # 输出的3级指标 { "_nodes":{ "total":6, "successful":6, "failed":0 }, "cluster_name":"prod-one-id", "nodes":{ "T3bjsBQUSeu0bstT7m8LCA":{ "timestamp":1604802841283, "name":"iZbp11gqesu0zk5sqrgwu4Z", "transport_address":"172.16.71.231:9300", "host":"172.16.71.231", "ip":"172.16.71.231:9300", "roles":Array[3], "indices":Object{...}, "os":Object{...}, "process":Object{...}, "jvm":Object{...}, "thread_pool":Object{...}, "fs":Object{...}, "transport":Object{...}, "http":Object{...}, "breakers":Object{...}, "script":Object{...}, "discovery":Object{...}, "ingest":Object{...}, "adaptive_selection":Object{...} }, "93HMUUReSYeQEaNTfNUWCQ":Object{...}, "_6TNUy4nSZ-jxumgiroqlg":Object{...}, "cEEZcNJGS0mSgppe82SZ9Q":Object{...}, "utKipUwYQpi9ac4Q7sI53g":Object{...}, "SE7IppNARjugsLSnPhil9g":Object{...} } } ``` 在集群规模比较大时,整个 node 的状态数据会比较多,此时可以指定 id,address,name 或者节点的其他属性来查看指定节点的状态信息。 ``` # 可以指定节点id,ip,name $ curl -s "http://localhost:9200/_nodes/T3bjsBQUSeu0bstT7m8LCA/stats" $ curl -s "http://localhost:9200/_nodes/172.16.71.231/stats" $ curl -s "http://localhost:9200/_nodes/iZbp11gqesu0zk5sqrgwu4Z/stats" ``` 当然,有时候,我们依然觉得,单个节点的指标比较多,我们对某些指标项目进行过滤。 ``` # 查看某个节点的指定指标 $ curl -s "http://localhost:9200/_nodes/172.16.71.231/stats/jvm,os " ``` #### Cluster Stats API 集群指标接口提供了集群范围内的信息,因此,它基本上是集群中每个节点的所有统计数据相加。虽然提供的数据不够详细,但是对于快速了解集群状态是非常有用的。 集群级别比较重要的几个指标: * status: 集群状态 (green|red|yellow) * nodes: 集群的整体节点统计信息 (/_nodes/stats 的求和,指标和指标项会比较精简:fs,jvm,os,process ) * indices: 集群的索引整体状况 ``` # 查看集群整体状况 $ curl -s "localhost:9200/_cluster/stats" # 输出的三级指标 { "_nodes":{ "failed":0, "successful":6, "total":6 }, "cluster_name":"prod-one-id", "indices":{ "completion":Object{...}, "count":5, "docs":Object{...}, "fielddata":Object{...}, "query_cache":Object{...}, "segments":Object{...}, "shards":Object{...}, "store":Object{...} }, "nodes":{ "count":Object{...}, "fs":Object{...}, "jvm":Object{...}, "network_types":Object{...}, "os":Object{...}, "plugins":Array[0], "process":Object{...}, "versions":Array[1] }, "status":"green", "timestamp":1604806385128 } ``` #### Index Stats API Index 状态接口可以反映一个指定索引的状态信息。 使用该接口可以快速查看索引的分片状态,主分片的各个操作详情统计,以及单个索引的详情统计,indices 下具体索引的详情信息 (indexing,get,search,merges,refresh,flush) ``` # 查看指定索引的状态信息 # .elastichq 为索引名称 $ curl -s localhost:9200/.elastichq/_stats # 输出的三级指标 { "_shards":{ "total":10, "successful":10, "failed":0 }, "_all":{ "primaries":Object{...}, "total":Object{...} }, "indices":{ ".elastichq":{ "primaries":Object{...}, "total":Object{...} } } } ``` #### Cluster Health HTTP API 在所有对外接口中,提供集群级别的运行态数据外,还提供了集群健康状态的接口。该接口可以公开整个集群运行状况的关键信息。 ``` $ curl localhost:9200/_cluster/health # 集群健康状态 { "cluster_name":"prod-one-id", "status":"green", "timed_out":false, "number_of_nodes":6, "number_of_data_nodes":6, "active_primary_shards":13, "active_shards":31, "relocating_shards":0, "initializing_shards":0, "unassigned_shards":0, "delayed_unassigned_shards":0, "number_of_pending_tasks":0, "number_of_in_flight_fetch":0, "task_max_waiting_in_queue_millis":0, "active_shards_percent_as_number":100 } ``` #### Pending Tasks API 待处理任务 API 是一种快速查看群集中待处理任务的快速方法。需要注意的是,pending task 是只有主节点才能执行的任务,比如创建新索引或者重建集群的分片。 如果主节点无法跟上这些请求的速度,则挂起的任务将开始排队。 ``` $ curl localhost:9200/_cluster/pending_tasks {"tasks":[]} ``` 正常情况下,将返回空的待处理任务。否则,您将收到关于每个未决任务的优先级、它在队列中等待了多长时间以及它代表了什么动作的信息。 ``` { "tasks" : [ { "insert_order" : 13612, "priority" : "URGENT", "source" : "delete-index [old_index]", "executing" : true, "time_in_queue_millis" : 26, "time_in_queue" : "26ms" }, { "insert_order" : 13613, "priority" : "URGENT", "source" : "shard-started ([new_index][0], node[iNTLLuV0R_eYdGGDhBkMbQ], [P], v[1], s[INITIALIZING], a[id=8IFnF0A5SMmKQ1F6Ot-VyA], unassigned_info[[reason=INDEX_CREATED], at[2016-07-28T19:46:57.102Z]]), reason [after recovery from store]", "executing" : false, "time_in_queue_millis" : 23, "time_in_queue" : "23ms" }, { "insert_order" : 13614, "priority" : "URGENT", "source" : "shard-started ([new_index][0], node[iNTLLuV0R_eYdGGDhBkMbQ], [P], v[1], s[INITIALIZING], a[id=8IFnF0A5SMmKQ1F6Ot-VyA], unassigned_info[[reason=INDEX_CREATED], at[2016-07-28T19:46:57.102Z]]), reason [master {master-node-1}{iNTLLuV0R_eYdGGDhBkMbQ}{127.0.0.1}{127.0.0.1:9300} marked shard as initializing, but shard state is [POST_RECOVERY], mark shard as started]", "executing" : false, "time_in_queue_millis" : 20, "time_in_queue" : "20ms" } ] } ``` ### cat API CAT 接口也提供了一些查看相同指标的可选方案,类似于 UNIX 系统中的 cat 命令。 ``` $ curl http://localhost:9200/_cat =^.^= /_cat/allocation /_cat/shards /_cat/shards/{index} /_cat/master /_cat/nodes /_cat/tasks /_cat/indices /_cat/indices/{index} /_cat/segments /_cat/segments/{index} /_cat/count /_cat/count/{index} /_cat/recovery /_cat/recovery/{index} /_cat/health /_cat/pending_tasks /_cat/aliases /_cat/aliases/{alias} /_cat/thread_pool /_cat/thread_pool/{thread_pools} /_cat/plugins /_cat/fielddata /_cat/fielddata/{fields} /_cat/nodeattrs /_cat/repositories /_cat/snapshots/{repository} /_cat/templates ``` 比如,我们可以使用 `curl localhost:9200/_cat/nodes?help` 来查看 node api 相关的指标和描述,进而采用这些描述来查询具体的指标项。 如果我们只想查看节点的堆内存使用率、合并数量 (merges) 以及段数量 (segments),可以采用如下方式来查看: ``` # 指定查看每个节点的堆内存使用率,段数量和合并数量 $ curl "http://localhost:9200/_cat/nodes?h=http,heapPercent,segmentsCount,mergesTotal" 172.16.71.231:9200 56 99 108182 172.16.71.229:9200 31 95 122551 172.16.71.232:9200 50 66 73871 172.16.71.230:9200 41 63 76470 172.16.71.234:9200 32 64 93256 172.16.71.233:9200 14 90 136450 ``` 注意: 上述输出相当于是 Node Stats API 中的` jvm.mem.heap_used_percent,segments.count,merges.total`,整个 CAT 接口是一个可以快速获取集群,节点,索引以及分片的状态数据,并且能够以可读的方式展示出来。 ### 已实现的开源工具 虽然整个 ES 对外的接口已经能够提供很好的接口来描述瞬时的指标,但是通常情况下,我们有很多节点需要进行持续的监控,而接口的 JSON 格式又不便于我们进行解析和分析,很难快速识别到问题节点并及时发现问题趋势。 为了更加有效的监控 ElasticSearch,我们通常需要一些工具来定期采集 API 的指标数据,然后聚合指标结果来反应当前集群的整体状态。而在开源社区中,也产生了很多这种类似的工具系统。 #### Elastic HQ ElasticHQ 是一个可座位托管方案,插件化下载的开源监控工具。它能够提供你的集群,节点,索引,以及一些相关的查询和映射的指标。 ElasticHQ 会自动对指标进行颜色编码,以突出潜在的问题。 ###### 插件化安装: ``` $ ${ES_HOME}/bin/elasticsearch-plugin install royrusso/elasticsearch-HQ ``` 安装完成后,可以访问 http://localhost:9200/_plugin/hq/ 来访问当前集群的监控信息。 ###### 使用 Docker 进行托管方式安装: ``` $ docker run -itd -p 8081:5000 -v /opt/data/elastichq:/src/db --restart=always --name elastichq elastichq/elasticsearch-hq ``` 接下来,就可以访问主机的 8081 端口来查看 ElasticHQ 的监控管理了,需要注意的是,此时需要添加集群地址。 ![lmufz1hl.png](http://flt-pan.58heshihu.com/blog/typecho/lmufz1hl.png) #### 其他监控插件 开源领域也有其他插件,比如 kopf 和 Cerebro 前者比较老,且现在不再更新了,而后者是一个比较全面的监控工具,且支持 LDAP 工具登录。 Cerebro:https://github.com/lmenezes/cerebro/ ``` # ldap 的配置信息 $ cat env-ldap # Set it to ldap to activate ldap authorization AUTH_TYPE=ldap # Your ldap url LDAP_URL=ldap://exammple.com:389 LDAP_BASE_DN=OU=users,DC=example,DC=com # Usually method should be "simple" otherwise, set it to the SASL mechanisms LDAP_METHOD=simple # user-template executes a string.format() operation where # username is passed in first, followed by base-dn. Some examples # - %s => leave user untouched # - %s@domain.com => append "@domain.com" to username # - uid=%s,%s => usual case of OpenLDAP LDAP_USER_TEMPLATE=%s@example.com # User identifier that can perform searches LDAP_BIND_DN=admin@example.com LDAP_BIND_PWD=adminpass # Group membership settings (optional) # If left unset LDAP_BASE_DN will be used # LDAP_GROUP_BASE_DN=OU=users,DC=example,DC=com # Attribute that represent the user, for example uid or mail # LDAP_USER_ATTR=mail # If left unset LDAP_USER_TEMPLATE will be used # LDAP_USER_ATTR_TEMPLATE=%s # Filter that tests membership of the group. If this property is empty then there is no group membership check # AD example => memberOf=CN=mygroup,ou=ouofthegroup,DC=domain,DC=com # OpenLDAP example => CN=mygroup # LDAP_GROUP=memberOf=memberOf=CN=mygroup,ou=ouofthegroup,DC=domain,DC=com ``` ``` $ docker run -p 9000:9000 --env-file env-ldap lmenezes/cerebro ``` 登录首页 ![lmufzgbi.png](http://flt-pan.58heshihu.com/blog/typecho/lmufzgbi.png) 集群概况以及索引信息 ![lmufzro1.png](http://flt-pan.58heshihu.com/blog/typecho/lmufzro1.png) 节点状态 ![lmufzys9.png](http://flt-pan.58heshihu.com/blog/typecho/lmufzys9.png) > 来源: > https://www.datadoghq.com/blog/monitor-elasticsearch-performance-metrics/ > https://www.datadoghq.com/blog/monitor-elasticsearch-performance-metrics/ 最后修改:2023 年 09 月 23 日 © 允许规范转载 赞 如果觉得我的文章对你有用,请随意赞赏