1 ES简介
Elasticsearch:基于Apache Lucene并使用Java开发的分布式开源搜索和分析引擎。是 Elastic Stack 的核心,它集中存储您的数据。
Elastic Stack:包括 Elasticsearch、Logstash 、 Kibana 和Beats (也称为 ELK Stack)。能够安全可靠地获取任何来源、任何格式的数据,然后实时地对数据进行搜索、分析和可视化。
ES是一个分布式、可扩展的、近实时的数据搜索、分析与存储引擎。支持全文搜索、结构化搜索、半结构化搜索、数据分析、地理位置和对象间关联关系搜索等功能。其底层基于Lucene,但Lucene比较复杂,面向普通应用开发者而言,易用性不是很好,同时对于目前的主流分布式架构支持也不好,所以就诞生了ES。ES使用Java编写,它的内部使用Lucene做索引与搜索,隐藏了Lucene的复杂性,面向开发者暴露了一套即使是不同编程语言也基本一致的API和client,方便大家将搜索功能快速植入到日常应用中。
2 应用场景
- 数据量特别大,又需要各种条件查询,同时近实时的返回查询结果数据的情况:如果采用传统RDBMS,在大数据量情况下,需要分库分表,当查询条件过于复杂,或者涉及到跨库联合查询时,传统RDBMS解决方案就很复杂,或者成本代价过高
- 数据结构化特征不够良好,或者数据schema无法提前预定等情况:在一些实际业务中,某些需求不确定,导致传统RDBMS的数据库表结构很难确定,当需求稍微变化时,可能就会导致表结构的大变化,从而使得业务代码也不得不跟着变化,变化成本过高
- 大量数据写入频繁,但更新较少,查询较多的情况:这种情况下,也可以考虑一些常见的NoSQL方案,但相比大部分业界流行的NoSQL,ES在易用性、性能、复杂查询、异构语言兼容性等方面等显得更好
- 需要全文搜索功能、相似度搜索与比较、模糊匹配、地理位置聚合、搜索内容多语言支持与分词支持等:这些都是ES拿手的基本特性功能
- 存储数据容量变化范围大,目前数据量很小,但随着业务发展,后期数据量可能会爆炸性增长:ES能胜任上百个服务节点的扩展,并可以支持PB级别的结构化或者非结构化数据,水平扩容方便快捷
- 海量日志数据的记录与分析:ELK玩法
3 基本概念
3.1 概念概览
|-------------------|----------------------|
| Elasticsearch | 关系型数据库 |
| 索引(index) | 数据库(Database) |
| 文档类型(type) | 表(Table) |
| 文档(document) | 一行数据(Row) |
| 字段(field) | 一列数据(Column) |
| 映射(mapping) | 数据库的组织和结构(Schema) |
| Query DSL | SQL |
| GET http:// | select * from table |
| PUT http:// | update table |
- RDBMS中的数据库(DataBase),等价于ES中的索引(Index)。
- RDBMS中一个数据库下面有 N 张表(Table),等价于1个索引 Index 下面有 N 多类型(Type)。
- RDBMS中一个数据库表(Table)下的数据由多行(Row)多列(column,属性)组成,等价于1个 Type 由多个文档(Document)和多 Field 组成。
- RDBMS中定义表结构、设定字段类型等价于 ES 中的 Mapping。例如,在一个关系型数据库里面,Schema 定义了表、每个表的字段,还有表和字段之间的关系。与之对应的,在 ES 中,Mapping 定义索引下的 Type 的字段处理规则,即索引如何建立、索引类型、是否保存原始索引 JSON 文档、是否压缩原始 JSON 文档、是否需要分词处理、如何进行分词处理等。
- RDBMS中的增 insert、删 delete、改 update、查 search 操作等价于 ES 中的增 PUT/POST、删 Delete、改 _update、查 GET。其中的修改指定条件的更新 update 等价于 ES 中的 update_by_query,指定条件的删除等价于 ES 中的 delete_by_query。
- RDBMS中的 group by、avg、sum 等函数类似于 ES 中的 Aggregations 的部分特性。
- RDBMS中的去重 distinct 类似 ES 中的 cardinality 操作。
- RDBMS中的数据迁移等价于 ES 中的 reindex 操作。
3.2 具体释意
集群(cluster)
一个Elasticsearch集群由一个或多个ES节点组成,并提供集群内所有节点的联合索引和搜索能力(所有节点共同存储数据)。一个集群被命名为唯一的名字(默认为elasticsearch),集群名称非常重要,因为节点需要通过集群的名称加入集群。
请确保在不同的环境使用不同的集群名称,否则会导致节点添加到错误的集群中。
节点(node)
一个节点是集群中的一个服务器,用来存储数据并参与集群的索引和搜索。和集群类似,节点由一个名称来标识,默认情况下,该名称是在节点启动时分配给节点的随机通用唯一标识符(UUID)。您也可以自定义任意节点的名称,节点名称对于管理工作很重要,因为通过节点名称可以确定网络中的哪些服务器对应于Elasticsearch集群中的哪些节点。
一个节点可以被添加到指定名称的集群中。默认情况下,每个节点会被设置加入到名称为elasticsearch的集群中,这意味着,如果在您在网络中启动了某些节点(假设这些节点可以发现彼此),它们会自动形成并加入名称为elasticsearch的集群中。
一个集群可以拥有任意多的节点。此外,如果在您的网络中没有运行任何Elasticsearch节点,此时启动一个节点会创建一个名称为elasticsearch的单节点集群。
索引(index)
一个索引是一个拥有一些相似特征的文档的集合(相当于关系型数据库中的一个数据库)。例如,您可以拥有一个客户数据的索引,一个商品目录的索引,以及一个订单数据的索引。一个索引通常使用一个名称(所有字母必须小写)来标识,当针对这个索引的文档执行索引、搜索、更新和删除操作的时候,这个名称被用来指向索引。
类型(type)
一个类型通常是一个索引的一个逻辑分类或分区,允许在一个索引下存储不同类型的文档(相当于关系型数据库中的一张表),例如用户类型、博客类型等。目前已经不支持在一个索引下创建多个类型,并且类型概念已经在后续版本中删除,详情请参见Elasticsearch官方文档。
文档(document)
一个文档是可以被索引的基本信息单元(相当于关系型数据库中的一行数据)。例如,您可以为一个客户创建一个文档,或者为一个商品创建一个文档。文档可以用JSON格式来表示。在一个索引中,您可以存储任意多的文档,且文档必须被索引。
字段(field)
组成文档的最小单位。相当于关系型数据库中的一列数据。
映射(mapping)
用来定义一个文档以及其所包含的字段如何被存储和索引,例如在mapping中定义字段的名称和类型,以及所使用的分词器。相当于关系型数据库中的Schema。
分片(shards)
代表索引分片,Elasticsearch可以把一个完整的索引分成多个分片,这样的好处是可以把一个大的索引拆分成多个,分布到不同的节点上,构成分布式搜索。分片的数量只能在索引创建前指定,并且索引创建后不能更改。
一个分片可以是主分片或副本分片。Elasticsearch 7.0以下版本默认为一个索引创建5个主分片,并分别为每个主分片创建1个副本分片,7.0及以上版本默认创建1个主分片和1个副本分片。两者区别如下:
|----------|---------------------|-------------------|--------------------------------------------------------------------------------------------------------------------|
| 分片类型 | 支持处理的请求 | 数量是否可修改 | 其他说明 |
| 主分片 | 支持处理查询和索引请求。 | 在创建索引时设定,设定后不可更改。 | 索引内任意一个文档都存储在一个主分片中,所以主分片的数量和大小决定着索引能够保存的最大数据量。 |
| 副本分片 | 支持处理查询请求,不支持处理索引请求。 | 可在任何时候添加或删除副本分片。 | 副本分片对搜索性能非常重要,主要体现在以下两个方面: * 提高系统的容错性,当某个节点某个分片损坏或丢失时可以从副本中恢复。 * 提高Elasticsearch的查询效率,Elasticsearch会自动对搜索请求进行负载均衡。 |
注意 主分片不是越多越好,因为主分片越多,Elasticsearch性能开销也会越大。
recovery
代表数据恢复或数据重新分布,Elasticsearch在有节点加入或退出时会根据机器的负载对索引分片进行重新分配,挂掉的节点重新启动时也会进行数据恢复。
gateway
代表Elasticsearch索引快照的存储方式,Elasticsearch默认优先将索引存放到内存中,当内存满时再将这些索引持久化存储至本地硬盘。gateway对索引快照进行存储,当这个Elasticsearch集群关闭再重新启动时就会从gateway中读取索引备份数据。Elasticsearch支持多种类型的gateway,有本地文件系统(默认)、分布式文件系统、Hadoop的HDFS和阿里云的OSS云存储服务。
discovery.zen
代表Elasticsearch的自动发现节点机制,Elasticsearch是一个基于p2p的系统,它先通过广播寻找存在的节点,再通过多播协议进行节点之间的通信,同时也支持点对点的交互。
Transport
Transport代表Elasticsearch内部节点或集群与客户端的交互方式,默认使用TCP协议进行交互。同时,通过插件的方式集成,也支持使用HTTP协议(JSON格式)、thrift、servlet、memcached、zeroMQ等传输协议进行交互。
4 ES分布式设计
4.1 节点类型
|-----------------|----------------------------------------------------------------------------------------------------------------------------------------|
| master | 负责保存和更新集群的一些元数据信息,之后同步到所有节点,所以每个节点都需要保存全量的元数据信息: 1. 集群的配置信息 2. 集群的节点信息 3. 模板template设置 4. 索引以及对应的设置、mapping、分词器和别名 5. 索引关联到的分片以及分配到的节点 |
| datanode | 负责实际的数据存储和查询 |
| coordinator | 1. 路由索引请求 2. 聚合搜索结果集 3. 分发批量索引请求 |
| ingestor | 类似于logstash,对输入数据进行处理和转换 |
ES集群的高可用性不需要依赖于其他外部组件,例如Zookeeper、HDFS等,master节点的主备依赖于ES内部自带的选举算法,通过副本分片的方式实现了数据的备份的同时,也提高了并发查询的能力。
一般一个物理节点的缺省配置是将master和datanode两属性集为一体。对于3-5个节点的小集群来讲,通常让所有节点存储数据(datanode)和具有获得主节点(master)的资格。
专用协调节点(也称为client节点或路由节点)从数据节点中消除了聚合/查询的请求解析和最终阶段,随着集群写入以及查询负载的增大,可以通过协调节点减轻数据节点的压力,可以让数据节点更多专注于数据的写入以及查询。
4.2 Master选举机制
ES master节点选举不需要依赖于ZK之类的外部服务,而是实现了一套自己的选举策略:
选举时机
集群启动:后台启动线程去ping集群中的节点,按照上述策略从具有master资格的节点中选举出master
现有的master离开集群:后台一直有一个线程定时ping master节点,超过一定次数没有ping成功之后,重新进行master的选举
选举策略
两张列表。master列表用来记录现在的master节点,存储0或1个节点信息。候选列表,用来存储有资格成为master但不是master的节点。
如果集群中存在master,认可该master加入集群。如果集群中不存在master,从具有master资格的节点中选id最小的节点作为master。
具体的选举流程如下:
4.3 避免脑裂
脑裂问题是采用master-slave模式的分布式集群普遍需要关注的问题,脑裂一旦出现,会导致集群的状态出现不一致,导致数据错误甚至丢失。
ES避免脑裂的策略:过半原则,可以在ES的集群配置中添加一下配置,避免脑裂的发生:
##选举时需要的节点连接数,N为具有master资格的节点数量
discovery.zen.minimum_master_nodes=N/2+1
对于网络波动比较大的集群来说,适当增加ping的间隔时间和ping的次数,一定程度上可以增加集群的稳定性,例如:
##一个节点多久ping一次,默认1s
discovery.zen.fd.ping_interval: 5s
##等待ping返回时间,默认30s
discovery.zen.fd.ping_timeout: 30s
##ping超时重试次数,默认3次
discovery.zen.fd.ping_retries: 5
4.4 集群状态
Green
所有主分片和备份分片都准备就绪,分配成功, 即使有一台机器挂了(假设一台机器实例),数据都不会丢失(但是会变成yellow状态)。
Yellow
所有主分片准备就绪,但至少一个主分片(假设是A)对应的备份分片没有就绪,此时集群处于告警状态,意味着高可用和容灾能力下降.如果刚好A所在的机器挂了,并且你只设置了一个备份(且已处于未就绪状态), 那么A的数据就会丢失(查询不完整),此时集群将变成Red状态。
Red
至少有一个主分片没有就绪(直接原因是找不到对应的备份分片成为新的主分片),此时查询的结果会出现数据丢失(不完整)。
5 ES存储设计
5.1 存储架构设计
分片(shard)
- 一个索引将数据对应分不到多个分片。每一个主分片对应一个数据相同的副分片,用来保证容灾。
- 创建索引时需要制定分片数量,分片会均匀的分布到集群的机器中。当扩展分片数量是需要利用API重建索引。
- 一个分片只能运行在一个机器上,因此集群扩容没有意义,需要对应进行分片扩容
- 每个分片都相当于一个独立的lucene引擎,太多的分片意味着集群中需要管理的元数据信息增多。会导致master承压过大、小文件会增多、内存以及文件句柄的占用量会增大、查询速度也会变慢。
- 每个分片是独立的,对于一个Search Request的行为,每个分片都会执行这个Request。每个分片都是一个Lucene Index,所以一个分片只能存放 Integer.MAX_VALUE - 128 = 2,147,483,519 个documents。
数据副本(replica)
- 数据副本是分片的副本。
- 副本可以保证高可以用,作用有:分片挂掉可以数据副本同步;增加查询能力;但写入时会增加集群压力。
- 数据首先写入分片,成功后发送到副本分片,完成写入后返回客户端信息。
- 主分片和备分片不会出现在同一个节点上(防止单点故障)
- 默认情况下一个index将会创建5个分片,每个分片创建一个备份(即1 index = 5 primary + 5 replica = 10个分片)
存储架构总结
- 容灾:primary分片丢失,replica分片就会被顶上去成为新的主分片,同时根据这个新的主分片创建新的replica,集群数据不会丢失。
- 提高查询性能:replica和primary分片的数据是相同的,所以对于一个query既可以查主分片也可以查备分片,在合适的范围内多个replica性能会更优(但同时资源占用情况[cpu/disk/heap]等也会提高)。
- 对于一个索引,除非重建索引否则不能调整分片的数目(主分片数, number_of_shards),但可以随时调整replica数(number_of_replicas)。
5.2 写入设计
|------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 状态 | 描述 | 详情 |
| 初始状态 | 一个Lucene索引包含一个提交点和三个段 ES基于Lucene, ES引入了按段搜索概念,每一段(segment)本身都是一个倒排索引, 但索引在Lucene中除表示所有段的集合外, 还增加了提交点(commit point)的概念: 一个列出了所有已知段的文件。 | |
| 追加日志 | 新的文档被添加到内存缓冲区并且被追加到了事务日志 一个文档被索引之后,就会被添加到内存缓冲区(in-memory buffer),并且被写入translog文件中,该文件持久化到磁盘,保证服务器宕机的时候数据不会丢失,由于顺序写磁盘,速度也会很快,但是此时文档还不可被搜索。写入方式有两种: * 同步写入:每次写入请求执行的时候,translog在fsync到磁盘之后,才会给客户端返回成功。 * 异步写入:写入请求缓存在内存中,每经过固定时间之后才会fsync到磁盘,在写入量很大,同时对于数据的完整性要求又不是非常严格的情况下,可以开启异步写入。 | 内存缓冲区和translog具有相同的数据内容。 |
| Refresh | 新的文档可被搜索 经过固定的时间(默认1s),或者手动触发之后,将内存缓冲区中的数据构建索引生成segment,写入文件系统缓冲区,此时文档即可被搜索到了。 refresh将完成下面几个动作: * 这些在内存缓冲区的文档被写入到一个新的段中(即下图中的灰色圆柱体),但还没有提交到磁盘。 * 这个段被打开,使其可被搜索。 * 内存缓冲区被清空,但translog中还保存着这些数据。 | |
| 积累文档 | 随着新数据的不断进入,更多的文档被添加到in-memory buffer和追加到translog中。 随着数据进入,in-memory buffer中的数据会被定期生成segment,但Translog中会持续记录数据。 | |
| Commit/Flush | 超过固定的时间,或者translog文件过大之后,触发flush操作: * 所有在in-memory buffer中的文档都被写入一个新的segment。 * in-memory buffer被清空。 * 一个commit point被写入硬盘。 * 文件系统缓存通过fsync被刷新(flush)。 * 老的translog被删除。 | |
| Merge | 由于每次refresh的时候,都会在文件系统缓冲区中生成一个segment,后续flush触发的时候持久化到磁盘。所以,随着数据的写入,尤其是refresh的时间设置的很短的时候,磁盘中会生成越来越多的segment。segment数目太多会带来较大的负担, 每一个segment都会消耗文件句柄、内存和cpu运行周期,更重要的是,每个搜索请求都必须轮流检查每个segment,所以segment越多,搜索也就越慢。因此,需要对这些segment进行merge操作, merge的过程大致描述如下: * 磁盘上两个小segment:A和B,内存中又生成了一个小segment:C * A,B被读取到内存中,与内存中的C进行merge,生成了新的更大的segment:D * 触发commit操作,D被fsync到磁盘 * 创建新的commit point,删除A和B,新增D * 删除磁盘中的A和B | |
从上面的写入流程可知,所有的segment一但生成之后,其数据是不可变的,如果要改变segment的数据,则是采用将旧的segment销毁,重新生成一个新的segment的方式来进行的。segment的不可变性有如下好处:
- 不需要锁。如果你从来不更新索引,你就不需要担心多进程同时修改数据的问题。
- 一旦索引被读入内核的文件系统缓存,便会留在哪里,由于其不变性。只要文件系统缓存中还有足够的空间,那么大部分读请求会直接请求内存,而不会命中磁盘。这提供了很大的性能提升。
- 其它缓存(像filter缓存),在索引的生命周期内始终有效。它们不需要在每次数据改变时被重建,因为数据不会变化。
- 写入单个大的倒排索引允许数据被压缩,减少磁盘 I/O 和 需要被缓存到内存的索引的使用量。
5.3 删除设计
磁盘上的每个segment都有一个.del文件与它相关联。当发送删除请求时,该文档未被真正物理删除,而是在.del文件中标记为已删除。此文档可能仍然能被搜索到,但会从结果中过滤掉。当segment合并时,在.del文件中标记为已删除的文档不会被包括在新的segment中,在merge的时候才会真正物理删除被删除的文档。
5.4 更新设计
创建新文档时,ES将为该文档分配一个版本号。对文档的每次更改都会产生一个新的版本号。当执行更新时,旧版本在.del文件中被标记为已删除,并且新版本在新的segment中写入索引。旧版本可能仍然与搜索查询匹配,但是从结果中将其过滤掉。
6 ES查询设计
ES最关键的一个特性就是提供了强大的索引能力。ES索引的精髓:"一切设计都是为了提高搜索的性能"。从另一方面讲,为了提高搜索的性能,难免会牺牲某些其他方面,比如插入/更新等。
ES的设计中引入了倒排索引,这种索引比关系型数据库的B-Tree索引更快。
6.1 B-Tree/B+Tree索引
二叉树查找效率是logN,同时插入新的节点不必移动全部节点,所以用树型结构存储索引,能同时兼顾插入和查询的性能。因此在这个基础上,再结合磁盘的读取特性(顺序读/随机读),传统关系型数据库采用了B-Tree/B+Tree这样的数据结构:
B-Tree 和 B+Tree的区别如下:
- 有n棵子树的结点中含有n个关键字; (而B树是n棵子树有n-1个关键字)
- 所有的叶子结点中包含了全部关键字的信息,及指向含有这些关键字记录的指针,且叶子结点本身依关键字的大小自小而大的顺序链接。(而B树的叶子节点并没有包括全部需要查找的信息)
- 所有的非终端结点可以看成是索引部分,结点中仅含有其子树根结点中最大(或最小)关键字。 (而B 树的非终节点也包含需要查找的有效信息)
为了提高查询的效率,减少磁盘读取次数,将多个值作为一个数组通过连续区间存放,一次读取多个数据,同时也降低树的高度。
6.2 倒排索引(Inverted Index)
倒排索引也找反向索引。正向索引是通过key找value,反向索引则是通过value找key。
其中,几个基本概念:
- **Term(单词):**一段文本经过分析器分析以后就会输出一串单词,这一个一个的就叫做Term
- **Term Dictionary(单词字典):**顾名思义,它里面维护的是Term,可以理解为Term的集合
- **Term Index(单词索引):**为了更快的找到某个单词,我们为单词建立索引
- **Posting List(倒排列表):**倒排列表记录了出现过某个单词的所有文档的文档列表及单词在该文档中出现的位置信息,每条记录称为一个倒排项(Posting)。根据倒排列表,即可获知哪些文档包含某个单词。
下面用一个表举个例子:
假设有个user索引,它有四个字段:分别是name,gender,age,address。其跟关系型数据库表结构如下:
ES建立的索引大致如下:
- gender字段的倒排索引
- address字段的倒排索引-ES建立倒排索引会进行分词操作:
结合以上实例再来理解倒排索引的设计:
- Term:例如性别中的1、2。
- Posting list:例如性别中的[1,3],其是一个数组,存储符合某个Term的文档ID。
- Term dictionary:当Term集合很多时,会进行遍历查询速度特别慢(N次)。因此Term dictionary会对Term集合进行排序,排序后的数据进行二分查找,比全遍历更快地找出目标的Term(logN次)。
- Term index:磁盘的随机读操作非常昂贵(一次random access大概需要10ms的时间)。所以尽量少的读磁盘,有必要把一些数据缓存到内存里。但是整个term dictionary本身又太大了,无法完整地放到内存里。于是就有了term index(如下图所示),term index有点像一本字典的大的章节表。term index不会存储全部的term dictionary,而是仅存储一些前缀。
- 倒排索引是per field的,一个字段有一个自己的倒排索引
Term index补充说明。由于Term dictionary是一个已经排好序的结构,因此Term index不会存储全部的Term,只会存储部分。通过Term index可以快速定位到term dictionary的某个offset,然后从这个位置再往后顺序查找。再加上一些压缩技术(搜索 Lucene Finite State Transducers) term index 的尺寸可以只有所有term的尺寸的几十分之一,使得用内存缓存整个term index变成可能。
6.3 ES查询快的原因
ES在提高查询速度的同时,牺牲了删除和更新性能。
- Mysql只有term dictionary这一层,是以b-tree的方式存储在磁盘上的。检索一个term需要若干次的random access的磁盘操作(当然,mysql采用b+tree的方式,也尽量减少了磁盘的随机访问,此处只是比较两者的基本原理之差)。
- 而Lucene在term dictionary的基础上添加了term index来加速检索,term index以树的形式缓存在内存中。从term index查到对应的term dictionary的block位置之后,再去磁盘上找term,大大减少了磁盘的random access次数。
- 额外值得一提的是:term index在内存中是以FST(finite state transducers)的形式保存的,其特点是非常节省内存。Term dictionary在磁盘上是以分block的方式保存的,一个block内部利用公共前缀压缩,比如都是Ab开头的单词就可以把Ab省去。这样term dictionary可以比b-tree更节约磁盘空间。