引子
本文致力于ElasticSearch理论体系构建,从基本概念和术语讲起,具体阐述了倒排索引和TransLog,接着讲了ElasticSearch的增删改查的流程和原理,最后讲了讲集群的选举和脑裂问题。
前言
大碗宽面-Kafka一本道万事通,上一篇文章被评论区的老哥们夸了,夸我写的很有体系,哈哈,真是过赞了。我在看文章时,特别讨厌那种拆散的知识点,比如那什么一二三四五的连续剧博客看得真是让人难受。
本文承接上篇Kafka文章的风格,同样会拆分为理论和实战两块。Elasticsearch作为数据库有一些微调,场景题我会放到实战篇,单独说理论的话,实在是太抽象,得结合配置和实践进行讲解,因此并不收录在理论篇,请大家期待下篇啦。
Elasticsearch基本概念和术语讲解
简要介绍一下Elasticsearch?
Elasticsearch 是一个分布式、RESTful 风格的搜索和数据分析引擎
- 分布式架构:Elasticsearch采用分布式架构,可以在多台服务器上运行,实现数据的分片和复制,确保高可用性和容错性。
- 实时性:Elasticsearch以毫秒级的速度返回查询结果,使其成为实时应用和实时分析的理想选择。
- 强大的搜索能力:Elasticsearch基于Lucene构建,提供全文搜索和复杂查询功能,支持词条匹配、短语匹配、模糊搜索和近似搜索等。
- 易用性:它具有简单的RESTful API,使得与Elasticsearch的交互变得简单直观。
- 多种数据类型支持:Elasticsearch支持多种数据类型的索引和搜索,包括文本、数值、地理位置等。
- 自动化负载均衡:集群中的节点能够自动分配和重新分配数据,保证集群负载均衡。
- 强大的数据聚合功能:Elasticsearch提供聚合功能,可以对大量数据进行分组、统计和分析。
- 实时监控和管理:Elasticsearch提供了Kibana工具,用于实时监控集群状态和性能,并且可以用于数据可视化和管理。
- 可扩展性:由于其分布式架构,Elasticsearch能够轻松地水平扩展,以适应不断增长的数据需求。
ElasticSearch中的集群、节点、索引、文档、类型是什么?
- 索引(Index):索引是一组具有相似结构的文档的集合,每个文档都包含了一些字段和对应的值。它类似于关系型数据库中的表。索引用于加速数据的搜索、过滤和聚合操作。
- 文档(Document):文档是索引中的基本数据单元,它是一条JSON格式的记录,可以包含各种类型的数据。每个文档在索引中都有一个唯一的标识,称为文档ID。
- 类型(Type,已在ES 7.x中弃用):在早期的Elasticsearch版本中,一个索引可以包含多个类型,每个类型表示不同的数据结构。但是从Elasticsearch 7.x开始,类型被弃用,一个索引只能包含一个类型。
- 分片(Shard):索引可以被分成多个分片,每个分片是一个独立的数据单元,包含部分索引数据。分片允许数据分布在集群的多个节点上,提高了数据的并行性和可伸缩性。
- 副本(Replica):每个分片可以有零个或多个副本,提供了数据的冗余备份,以提高数据的可用性和故障恢复能力。
- 节点(Node):相当于一个Elasticsearch实例。多个节点组成了一个分布式的Elasticsearch集群。
- 集群(Cluster):集群由多个节点组成,共同协作来存储和处理数据。它提供了高可用性、负载均衡和分布式计算能力。
- 映射(Mapping):映射定义了索引中每个字段的数据类型和属性,类似于数据库中表的结构定义。映射决定了如何对文档的字段建立索引和进行搜索。
- 分析器(Analyzer):分析器是一个用于将文本数据分解为词条(terms)的处理器,它由字符过滤器、分词器和词条过滤器组成。分析器用于在建立索引和搜索时对文本进行标准化和处理。
ElasticSearch的倒排索引是什么
传统的我们的检索是通过文章,逐个遍历找到对应关键词的位置。倒排索引,是通过分词策略,形成了词和文章的映射关系表(比如应用中文分词器IK Analyzer,可以将“我是中国人”分词为我,是,中国,人,这些词汇聚为词典),这种词典+映射表即为倒排索引。
有了倒排索引,就能实现 O(1)时间复杂度的效率检索文章了,极大的提高了检索效率,倒排索引相对于传统索引明显会消耗更多的空间,典型的空间换时间策略。倒排索引的底层实现是基于:FST(Finite State Transducer)数据结构。
lucene 从 4+版本后开始大量使用的数据结构是 FST。FST 有两个优点:
- 空间占用小。通过对词典中单词前缀和后缀的重复利用,压缩了存储空间;
- 查询速度快。O(len(str))的查询时间复杂度。
倒排索引的特点:
- 倒排索引中的所有词项对应一个或多个文档;
- 倒排索引中的词项根据字典顺序升序排列
- 分词器的选择会影响搜索的精度,比如中国用中来搜需要选用合适的分词器才能搜到
Elasticsearch 支持哪些类型的查询?
- Match Query:match查询用于根据指定字段中的文本内容进行搜索。它会将搜索词进行分词处理,并匹配文档中包含任何一个分词的数据。
- Term Query:term查询用于精确匹配字段中的值,不会对搜索词进行分词处理。适用于关键字字段。
- Range Query:range查询用于匹配字段中落在指定范围内的值,如日期范围、数字范围等。
- Bool Query:bool查询用于组合多个查询条件,支持must(AND)、should(OR)、must_not(NOT)等逻辑操作。
- Wildcard Query:wildcard查询用通配符匹配字段中的内容。支持通配符*(匹配任意字符序列)和?(匹配单个字符)。
- Fuzzy Query:fuzzy查询用于模糊匹配,处理拼写错误或近似词汇。它可以根据编辑距离找到与搜索词相似的文档。
- Prefix Query:prefix查询用于匹配以指定前缀开头的字段内容。
- Terms Query:terms查询用于匹配多个精确值的字段内容。
- Nested Query:nested查询用于在嵌套对象中查询数据。这允许你在嵌套的字段中搜索并返回父文档。
- Match Phrase Query:match_phrase查询用于匹配完整短语,要求搜索词在文档中以相同顺序和紧密程度出现。
- Parent-Child Query:has_child和has_parent查询用于在父子关系的文档中查询数据。可以根据子文档搜索相关联的父文档或反之亦然。
- Geo Queries:geo_shape和geo_distance等查询用于地理位置查询。geo_shape允许你搜索包含特定地理形状的文档,而geo_distance用于搜索特定距离内的文档。
- Script Query:script查询用于执行自定义脚本查询,允许你以JavaScript、Painless等脚本语言编写自定义逻辑。
Elasticsearch的主要可用字段数据类型?
ES索引mapping举例 https://www.elastic.co/guide/en/elasticsearch/reference/7.9/mapping-types.html
index 影响字段的索引情况。
- true:字段会被索引,则可以用来进行搜索。默认值就是true
- false:字段不会被索引,不能用来搜索
index的默认值就是true,也就是说你不进行任何配置,所有字段都会被索引。但是有些字段是我们不希望被索引的,比如企业的logo图片地址,就需要手动设置index为false。
**store **是否将数据进行独立存储。
原始的文本会存储在 _source 里面,默认情况下其他提取出来的字段都不是独立存储的,是从_source 里面提取出来的。当然你也可以独立的存储某个字段,只要设置store:true即可,获取独立存储的字段要比从_source中解析快得多,但是也会占用更多的空间,所以要根据实际业务需求来设置,默认为false。
analyzer 指定分词器
一般我们处理中文会选择ik分词器 ik_max_word ik_smart
type:类型,可以是text、long、short、date、integer、object等
- Text类型:
- 用于全文搜索的文本字段。
- 会进行分词处理,将文本拆分成单词进行索引和搜索。
- 不支持排序和聚合操作。
- Keyword类型:
- 用于精确匹配的关键字字段。
- 不会进行分词处理,将整个字段值作为一个索引项。
- 支持排序和聚合操作。
- Numeric类型:
- 包括整数和浮点数类型。
- 支持范围查询、排序和聚合操作。
- 可以设置精度和格式化选项。
- Date类型:
- 用于日期和时间字段。
- 支持日期范围查询、排序和聚合操作。
- 可以设置日期格式。
- Boolean类型:
- 用于存储布尔值(true或false)。
- 支持布尔运算、排序和聚合操作。
- Object类型:
- 用于存储嵌套的JSON对象。
- 允许在一个字段中嵌套另一个对象。
- 可以在查询中使用点号语法访问嵌套对象的属性。
- Array类型:
- 用于存储数组或多值字段。
- 可以存储多个值,每个值可以是不同的数据类型。
- Geo类型:
- 用于存储地理位置信息。
- 支持地理位置查询和距离计算。
- IP类型:用于存储IPv4或IPv6地址。
- Binary类型:用于存储二进制数据。
ElasticSearch的translog作用及原理
作用
- durability(持久性):在数据写入索引之前先写入translog,即使节点故障也不会丢失数据。
- write throughput(写吞吐量):写入translog不需要fsync即可返回,大幅提高吞吐量。
- search recency(查询实时性):搜索可以直接从translog读取最新的数据。
原理和特点
- 每个分片都有一个translog,存储格式为JSON。
- 写入一个文档时,会同时写入内存buffer和translog。
- buffer定时(refresh,默认1s)刷入FlileSystem Cache,满时会刷新到磁盘segment文件,这时清空已写入segment的translog记录。
- translog根据大小自动轮转(flush,默认5s),保证单个文件不过大。当 translog 文件的大小达到一定阈值后,会关闭这个文件,打开一个新的 translog 文件继续记录操作。
- 后台线程会不断检查translog,老的 translog 文件在后台线程会被异步清理,对已持久化的记录进行清理。
- 最大保留最近N小时的translog,避免其过多占用空间。
- 重启时,会先重放translog再打开索引,保证不丢数据。
- 索引读取文档时,会先查找内存 buffer,然后查找最新的 translog 文件。
综上,translog通过写入优化、可恢复性和实时搜索能力,是Elasticsearch实现高性能写入和搜索的关键机制。它与buffer/segment结合,实现了Elasticsearch的近实时索引。
针对最后一条解释下
当Elasticsearch的一个索引接收到读取文档的请求时,它的处理流程如下:
- 首先会在内存buffer中查找文档。internal buffer中保存了最近接收到还未持久化到磁盘的文档。
- 如果buffer中没有找到,则会在translog(事务日志)文件中查找。translog中保存了所有还未持久化的最近更新、删除等操作记录。
- 如果translog中还是没有找到,则会遍历各个磁盘segment文件,逐个进行查找。segment是已经持久化到磁盘的只读索引文件。
- 最后,如果在所有segment中仍然没有找到文档,则返回文档不存在。
所以Elasticsearch的文档读取是优先查找内存buffer和translog的,以保证可以读取到近乎实时的文档数据。只有buffer和translog中没有,才会查找磁盘segments。这种设计支持了Elasticsearch的实时搜索能力。而buffer和translog的数据也会通过定期刷新机制及时持久化到磁盘,以保证数据不会丢失。
详细描述一下 Elasticsearch 文档的读写查删
写入文档的过程
- 用户向集群某节点发送请求。(如果没有指定路由/协调节点,请求的节点扮演路由节点的角色)
- 节点 1 接受到请求后,成为协调节点。根据路由和文档ID,计算分片ID(shard = hash(_routing) % (num_of_primary_shards)),假设该文档属于分片0。分片0的主分片位于节点3,因此请求会被转到节点3。
- 节点 3 在主分片上执行写操作,如果成功,则将请求并行转发到节点 1和节点 2 的副本分片上,等待结果返回。所有的副本分片都报告成功,
- 节点 3 将向协调节点(节点 1)报告成功,节点 1 向请求客户端报告写入成功。
shard = hash(_routing) % (num_of_primary_shards)公式说明:_routing是一个可以配置的变量,默认为文档ID。num_of_primary_shards是索引创建时指定的主分片数量。shard是取余后得到的结果分片。该公式也解释了主分片不可变的原因之一,如果更改分片数,计算的结果会错误,就找不到文档了。
分片数是否可变
逆向思考一下,分片数如果随意改变会带来什么问题:
- 数据分布和路由:分片数决定了索引数据在集群中的分布方式。每个文档都会被分配到一个特定的分片中,该分片是通过文档的路由计算得出的。如果允许随意变更分片数,会导致文档的路由发生变化,可能导致数据的重新分布,从而影响性能和查询效率。
- 数据重分布:如果允许变更分片数,那么当索引分片数增加或减少时,集群需要执行数据的重分布操作,将文档从一个分片移动到另一个分片。这涉及到数据的物理移动和重新路由,可能会导致集群的负载增加,影响性能。
- 性能和可伸缩性:索引的分片数直接影响到Elasticsearch集群的性能和可伸缩性。不合理地频繁变更分片数可能导致性能下降,影响查询和写入的吞吐量。因此,分片数的设置需要在索引创建时慎重考虑,根据数据量、查询模式和集群规模等因素进行规划。
虽然规则上不可改变,但是我们可以逃课解决。举个栗子,使用别名进行操作,掩盖真实的索引名,那么我们可以创建一个新的改变了分片数的索引,将别名指向这个新的索引,就变相完成了分片数的变更。详细操作参见我之前的实战文章,ElasticSearch不停机重建索引引申来的优化与思考–5091阅读27点赞43收藏
写数据底层原理
- 当分片所在的节点接收到来自协调节点的请求后,会将请求写入到 MemoryBuffer(应用缓存,使用Java堆内存的一部分),然后定时(默认是每隔 1 秒)写入到 Filesystem Cache(系统缓冲区),这个从 MemeryBuffer 到 Filesystem Cache 的过程就叫做 refresh;
- 当然在某些情况下,存在 Memery Buffer 和 Filesystem Cache 的数据可能会丢失,ES 是通过 translog 的机制来保证数据的可靠性的。其实现机制是接收到请求后,同时也会写入到 translog 中,当 Filesystem cache 中的数据写入到磁盘中时,才会清除掉,这个过程叫做 flush;
- 在 flush 过程中,内存中的缓冲将被清除,内容被写入一个新段,段的 fsync将创建一个新的提交点(commit),并将内容刷新到磁盘,旧的 translog 将被删除并开始一个新的 translog。(flush 触发的时机是定时触发(默认 30 分钟)或者 translog 变得太大(默认为 512M)时)
总结一下,数据先写入内存 buffer,然后每隔 1s(默认),将数据 refresh 到 os cache(操作系统缓存),到了 os cache 数据就能被搜索到(所以我们才说 es 从写入到能被搜索到,中间有 1s 的延迟)。每隔 5s(默认),将数据写入 translog 文件(这样如果机器宕机,内存数据全没,最多会有 5s 的数据丢失),translog 大到一定程度,或者默认每隔 30mins,会触发 commit 操作,将缓冲区的数据都 flush 到 segment file 磁盘文件中。
读取(搜索)文档
PS:写请求是写入 primary shard,然后同步给所有的 replica shard;读请求可以从 primary shard 或 replica shard 读取,采用的是随机轮询算法。
query 阶段的目的:定位到位置,但不取。
- 用户向集群某节点发送请求,未指定的话当前节点成为协调节点,向各个分片发起请求。
- 每个分片在本地进行查询,结果返回到本地有序的优先队列中。
- 每个分片返回各自优先队列中所有文档的ID和排序值给协调节点,它合并这些值到自己的优先队列中来产生一个全局排序后的结果列表
fetch 阶段的目的:取数据。
- 协调节点辨别出哪些文档需要被取回并向相关的分片提交多个GET请求。
- 每个分片根据请求的文档ID查询详细信息返回给协调节点。
- 一旦所有的文档都被取回了,协调节点返回结果给客户端。
删除更新底层原理
- 删除文档–当删除一个文档时,Elasticsearch不会立即删除,而是先将该文档标记为删除。Elasticsearch会新增一个.del文件,记录下该文档的删除操作。当索引刷新时,.del文件会和段文件进行合并,被标记的删除文档会被更新的段文件排除。即文档逻辑上被删除,但物理上实际内容会在段文件中保留,直到段合并才会被移除。
- 更新文档–Elasticsearch中的文档是不可变的,但可以使用新增+删除的方式来实现文档的更新。当更新一个文档时,Elasticsearch会先新增该文档的新版本,新版本的文档会在段文件中增加。接着Elasticsearch会再在.del文件中标记旧版本文档的删除。 refresh后,新文档可见而旧文档不可见,实现文档更新。
- 异常恢复–删除和更新的操作都需要记录在事务日志中,以保证节点故障时能恢复操作。
结论:Elasticsearch的文档删除和更新并不是真正的物理删除和修改。它使用新的索引、标记删除和事务日志的方式来实现对文档的更新。这样可以保持每个段文件的不变性以实现高效搜索。
Elasticsearch集群相关
Elasticsearch是如何实现master选举的?
前置条件:
1、只有是候选主节点(master:true)的节点才能成为主节点。
2、最小主节点数(min_master_nodes)的目的是防止脑裂。
Elasticsearch 的选主是 ZenDiscovery 模块负责的,主要包含 Ping(节点之间通过这个RPC来发现彼此)和 Unicast(单播模块包含一个主机列表以控制哪些节点需要 ping 通)这两部分;获取主节点的核心入口为 findMaster,选择主节点成功返回对应 Master,否则返回 null。
选举流程大致描述如下:
第一步:确认候选主节点数达标,elasticsearch.yml 设置的值discovery.zen.minimum_master_nodes;
第二步:对所有候选主节点根据节点ID字典排序(id为String类型),每次选举每个节点都把自己所知道节点ID排一次序,然后选出第一个(第0位,最小)节点,暂且认为它是master节点。
第三步:如果对某个节点的投票数达到一定的值(候选主节点数n/2+1)并且该节点自己也选举自己,那这个节点就是master。否则重新选举一直到满足上述条件。
如何解决Elasticsearch集群的脑裂问题
脑裂问题打个比方就是,因为网络、高负载、GC时间过长等原因导致节点失去响应。假设分为A和B两个多节点集合,互相联系不上,那么就会重选举,生成A和B两个集群都可以读写,产生数据不一致的问题
- 主节点不要设置节点读写,只设置node.master:true,设置node.data:false
- 将节点响应超时discovery.zen.ping_timeout设置长一些(默认是3秒),避免误判。
- 最重要的是设置discovery.zen.minimum_master_nodes,指的是集群中至少有多少个 master 候选节点存在才能组成集群。官方推荐配置是 (N/2)+1,N 是 Master 候选节点总数量。
这样配置以后,假设集群从网络故障中恢复,若干个数据节点与 (N/2)+1 个 Master 候选节点组成了一个集群,剩余数据节点与剩余 master 候选节点尝试组成集群,由于剩余 Master 候选节点数为 N-((N/2)+1) = (N/2)-1 < (N/2)+1,所以他们不能成为一个集群,他们必须去继续寻找其他节点来组成集群,从而从根本上解决了脑裂问题。这个配置也是几乎所有 HA 系统都会提供的基础配置。