解剖 Elasticsearch 集群 - 之一
本篇文章是一系列涵盖 Elasticsearch 底层架构和原型示例的其中一篇。在本篇文章中,我们会讨论底层的存储模型以及 CRUD(创建、读取、更新和删除)操作在 Elasticsearch 中是如何工作的。
全文搜索(Full-text search)
- 例如,找到与搜索词最为相关的维基百科文章。
聚合(Aggregations)
- 例如,对搜索词在广告网络中进行直方图可视化展示。
空间地理位置 API(Geospatial API)
- 例如,拼车平台匹配最近的车主与乘客。
因为 Elasticsearch 在行业中非常流行,本篇文章会分享它的存储模型以及 CRUD 操作是如何工作的。
当我们谈到一个分布式系统是如何工作的时候,通常会有以下画面:
浮于表面的是 API 而在它之下的是真正的引擎,也就是所有奇妙发生的地方。本篇文章会关注表面之下的部分。会主要介绍:
- 它是一个主从架构还是一个无主架构?
- 存储模型是怎样的?
- 写是如何工作的?
- 读是如何工作的?
- 搜索结果的相关性如何?
在我们深入理解这些概念之前,让我们熟悉一些基本术语。
混淆 Elasticsearch 的索引和 Lucene 的索引以及其他的术语...
一个 Elasticsearch 索引(index)是一个逻辑空间用来组织数据(像数据库)。一个 Elasticsearch 索引有一个或多个分片(shard 默认是 5 个)。一个分片是一个 Lucene 的索引,它是数据真正存储的地方,也是搜索引擎本身。每个分片可以有零个或多个副本(默认是 1)。一个 Elasticsearch 索引也有 “类型(types)”(像数据库里的表一样)它使我们可以对索引里的数据进行分区。在 Elasticsearch 索引中给定 “类型(types)” 的所有文档都具有相同的属性(像表模式)。
类比关系型数据库的术语
- Elasticsearch 索引 ~ 数据库
- 类型 ~ 表
- 映射 ~ 模式
NOTE: 以上仅作类比用,它们并不等价。推荐阅读这篇文章来帮助决定何时选择索引或类型来存储数据。
现在我们熟悉了 Elasticsearch 世界中的术语,再来看看不同节点所扮演的角色。
节点的类型
一个 Elasticsearch 是一个节点,一组节点组成一个集群。Elasticsearch 集群中的节点有三种配置方式:
主节点(Master Node)
-
它控制 Elasticsearch 集群,负责所有集群范围的操作如创建和删除索引,跟踪集群的各个节点并为节点指定分片。主节点在同一时间只保持集群的一个状态,然后将状态广播其他所有节点并得到其他节点的确认响应。
-
一个节点可以通过设置 elasticsearch.yml 文件中的 node.master 属性 true(默认值)将其配置为主节点。
-
对于大规模生产环境的集群,推荐配置仅用来控制集群状态而不处理任何用户请求的主节点。
数据节点(Data Node)
- 它保持数据和倒排索引。默认情况下,每个节点都是被配置成数据节点的,属性 node.data 在 elasticsearch.yml 文件中设置为 true 。如果想要有专门的主节点,需要将 master.data 属性设置为 false 。
客户端节点(Client Node)
- 如果将 node.master 和 node.data 设置成 false ,节点被配置成为客户端节点,它在集群中扮演负载均衡器将请求路由至不同节点。
在 Elasticsearch 集群中作为客户端连接的节点叫做协调节点。协调节点将客户端请求路由至集群中相应的分片。对于读请求,协调节点每次都会选择一个不同的分片来应对请求从而达到负载均衡的目的。
在介绍 CRUD 请求被发送到协调节点并传送到集群之前,我们先看看 Elasticsearch 内部的数据存储是如何支持低延迟的全文搜索的。
存储模型
Elasticsearch 使用 Apache Lucene ,一个用 Java 语言开发的全文搜索库,它的作者是 Doug Cutting(Apache Hadoop 的创建人),它内部使用了一种被称为倒排索引的数据结构来支持低延时的搜索结果。文档是 Elasticsearch 里的数据单元,倒排索引是根据对文档里的词进行 tokenizing 处理生成的,创建所有唯一词的有序列表以及词语再文档中的所处位置信息。
它和书背后的索引很像,包括书里面所有的唯一词以及可以找到该词的页。当我们说一个文档被索引了,我们指的是倒排索引。让我们看看以下两个文档的倒排索引是什么样子:
- Doc 1: Insight Data Engineering Fellows Program
- Doc 2: Insight Data Science Fellows Program
如果我们想要找到包含 “insight” 的文档,我们可以扫描倒排索引(里面的词语已排序),找到词 “insight” 然后返回包括该词的文档 ID ,这里是 Doc 1 和 Doc 2 。
为了提升搜索能力(例如,大小写都能获得相同的结果),文档首先被分析,然后被索引。分析包括两个过程:
- 将句子语汇单元化为单独的词
- 将词语归一化成标准形式
默认情况下,Elasticsearch 使用标准分析器,使用
- 标准分词器(Standard tokenizer)在词语边界对词语进行拆分
- 小写分词过滤器(Lowercase token filter)将词转换成小写形式
还有很多其他的分析器,可以在官方文档中找到它们。
为了提供相关的搜索结果,每个查询会使用索引时相同的分析器。
NOTE: 标准分析器会用 stop token 过滤器,不过默认它是禁用的。
在清楚了倒排索引的概念后,让我们看看 CRUD 操作。
结构写操作
创建 - (C)reate
当向协调节点发送请求,索引一个新文档时,会有以下一系列操作:
-
在 Elasticsearch 集群中的所有节点都包含元信息,知道哪个分片处于哪个节点。协调节点根据文档 ID 将文档路由至合适的分片。Elasticsearch 用 murmur3 哈希函数对文档 ID 进行哈希计算,然后根据索引内的主分片数进行模运算,以此决定文档索引的分片。
shard = hash(document_id) % (num_of_primary_shards)
-
当节点接收到来自协调节点的请求后,请求被写入 translog (后续会介绍 translog 的相关内容)然后文档会被存入内存缓冲区。如果请求在主节点请求成功,请求会并行发送到备份分片。只有所有主备分片上的 translog 同步后(fsync'ed),客户端才会接收到请求成功的应答。
-
内存缓冲会定期刷新(默认时间是 1 秒),内容会被写入到文件系统缓存里的新段(segment)中。这个段还没有被同步(fsync'ed),但是,段是开放的而且内容也可供搜索。
-
translog 会每隔 30 分钟(或过大时)被清空,文件系统缓存同时也会进行同步操作(fsync'ed)。在 Elasticsearch 中,这个过程被称为 flush 。在 flush 过程中,in-memory 缓冲会被清除,内容同时被写到新段(segment)中。一个新的提交点发生在段同步并写入磁盘时。旧的 translog 会被删除,并开始于一个全新的。
更新和删除 - (U)pdate and (D)elete
Delete 和 Update 操作也是写操作。但 Elasticsearch 中的文档是不可变的,所以不能被删除或修改。那么一个文档是如何删除或更新的呢?
磁盘上的每个段都有一个 .del 文件关联它。当 delete 请求发送时,文档并不是真正被删除了,而是在 .del 文件中被标记为了已删除状态。这个文档仍然会匹配搜索查询,但是会在结果中被过滤掉。当段合并后(我们会在后续文章中介绍段合并),在 .del 文件中被标记为已删除的文档不会包括在新合并的段中。
现在,我们来看看更新的工作方式。当新文档创建后,Elasticsearch 会为文档指定版本号。文档的每次改变都会生成一个新的版本号。当更新完成后,旧版本会在 .del 文件中标记为已删除,新版本会被索引到新的段中。旧版本仍然会在搜索查询中被匹配到,但是会在结果中被过滤掉。
在文档索引/更新后,我们会处理搜索请求。让我们看看搜索是如何在 Elasticsearch 中执行的。
解剖读 - (R)ead
读操作包括两个部分:
- 查询阶段(Query Phase)
- 读取阶段(Fetch Phase)
让我们看看每个阶段的工作方式。
查询阶段(Query Phase)
在这个阶段中,协调节点会将搜索请求路由到索引内的所有分片(主分片或备分片)。分片会各自独立完成搜索,根据相关度评分对结果排序创建一个优先队列(在后续文章中会介绍相关度评分)。所有分片都会返回匹配的文档 ID 以及相关度评分到协调节点。协调节点会创建一个优先队列对全局结果进行排序。匹配文档的数目可以很多,在默认状态下,每个分片都会发送前 10 个 结果到协调节点,协调节点会创建优先队列对来自所有分片的结果进行排序并返回前 10 的结果。
读取阶段(Fetch Phase)
在协调节点对所有结果进行排序并创建一个有序的文档列表后,它会向所有分片发出请求获取原始文档。所有分片都会补充文档内容,并将它们返回到协调节点。
下图展现了读请求和数据流向。
就像之前提到的,搜索结果是根据相关度进行排序的。让我们看看相关度是如何定义的。
搜索的相关度
相关度是由 Elasticsearch 为搜索结果中每个文档的评分所决定的。默认评分算法是 tf/idf(term frequency/inverse document frequency)。词频(term frequency)度量词项在文档中出现的次数(越高频 = 越相关),逆向文档频率(inverse document frequency)度量词项在整个索引出现的次数作为索引内所有文档的总百分比(越高频 = 越不相关)。最终的评分是 tf-idf 评分与其他如词近似度(短语查询 - phrase queries)、词项相似度(模糊查询 - fuzzy queries)等因子结合后的结果。
接下来是什么?
CRUD 操作是由一些内部数据结构和技术支持的,它们对于理解 Elasticsearch 的工作方式十分重要。在接下的文章中,会涵盖以下概念以及使用 Elasticsearch 时的一些陷阱:
- Elasticsearch 里的脑裂问题以及如何避免
- 事务日志(Transaction log)
- Lucene 段(Lucene segments)
- 为什么搜索中深度分页是十分危险的?
- 计算搜索相关度的难度以及权衡
- 并发控制(Concurrency control)
- 为什么 Elasticsearch 是准实时的?
- 如何保证读写的一致?
参考
参考来源:
Anatomy of an Elasticsearch Cluster: Part I