Elasticsearch 是最近两年异军突起的一个兼有搜索引擎和NoSQL数据库功能的开源系统,基于Java/Lucene构建。最近研究了一下,感觉 Elasticsearch 的架构以及其开源的生态构建都有许多可借鉴之处,所以整理成文章分享下。本文的代码以及架构分析主要基于 Elasticsearch 2.X 最新稳定版。
Elasticsearch 看名字就能大概了解下它是一个弹性的搜索引擎。首先弹性隐含的意思是分布式,单机系统是没法弹起来的,然后加上灵活的伸缩机制,就是这里的 Elastic 包含的意思。它的搜索存储功能主要是 Lucene 提供的,Lucene 相当于其存储引擎,它在之上封装了索引,查询,以及分布式相关的接口。
Elasticsearch 中的几个概念
-
集群(Cluster)一组拥有共同的 cluster name 的节点。
-
节点(Node) 集群中的一个 Elasticearch 实例。
-
索引(Index) 相当于关系数据库中的database概念,一个集群中可以包含多个索引。这个是个逻辑概念。
-
主分片(Primary shard) 索引的子集,索引可以切分成多个分片,分布到不同的集群节点上。分片对应的是 Lucene 中的索引。
-
副本分片(Replica shard)每个主分片可以有一个或者多个副本。
-
类型(Type)相当于数据库中的table概念,mapping是针对 Type 的。同一个索引里可以包含多个 Type。
-
Mapping 相当于数据库中的schema,用来约束字段的类型,不过 Elasticsearch 的 mapping 可以自动根据数据创建。
-
文档(Document) 相当于数据库中的row。
-
字段(Field)相当于数据库中的column。
-
分配(Allocation) 将分片分配给某个节点的过程,包括分配主分片或者副本。如果是副本,还包含从主分片复制数据的过程。
Elasticsearch 是一个分布式的搜索和分析引擎,基于 Apache Lucene 构建,广泛用于全文搜索、日志分析、数据可视化等场景。以下是 Elasticsearch 的架构和源码概览:
架构概览
Cluster(集群):
Elasticsearch 是一个分布式系统,由多个节点组成一个集群。集群中的节点通过协作,分发和处理数据,实现高可用性和扩展性。
每个集群都有一个唯一的名字,节点可以自动发现并加入集群。
Node(节点):
节点是 Elasticsearch 的基本工作单元,运行在集群中的每个实例即为一个节点。
节点可以分为多种角色,如主节点(Master Node)、数据节点(Data Node)、协调节点(Coordinator Node)等,分别负责不同的任务。
Index(索引):
索引是数据的逻辑分组,类似于关系型数据库中的数据库。每个索引由多个分片(shard)组成。
索引的数据存储在分片中,分片是数据的最小存储单元。
Shard(分片):
分片是索引数据的物理存储单元,每个分片本质上是一个独立的 Lucene 实例。
为了实现数据的高可用性和负载均衡,每个分片可以有一个或多个副本(Replica Shard)。
Document(文档):
文档是 Elasticsearch 存储和管理的基本单位,类似于关系型数据库中的行。
文档使用 JSON 格式表示,包含多个字段和值。
核心模块
Discovery Module:
负责节点间的通信和集群的管理,包括节点的加入、离开和选举主节点。
Cluster Service:
管理集群的元数据,如索引的状态、节点的分布、分片的分配等。
Transport Module:
处理节点间的数据传输,支持 HTTP 和 TCP 协议。
Indices Service:
负责索引的创建、删除、管理和查询。
Search Module:
处理用户的查询请求,包含查询解析、优化、执行等步骤。
Gateway:
负责持久化集群的元数据和索引数据,以确保在节点重启后数据不丢失。
源码概览
Elasticsearch 的源码分布在多个模块中,主要包括:
Core:
包含核心功能的实现,如索引管理、分片管理、集群管理等。
Server:
包含 Elasticsearch 的服务器端实现,包括 HTTP 服务、节点间通信等。
Client:
提供了多种客户端 API,如 Java Client、REST Client 等,用于与 Elasticsearch 进行交互。
Plugins:
Elasticsearch 的插件机制允许用户扩展功能,如分析插件、安全插件等。
源码解析建议
调试和阅读源码: 了解 Lucene 是基础,因为 Elasticsearch 的很多底层实现都依赖于 Lucene。建议从
IndicesService
、TransportService
等模块入手,了解索引管理和节点通信的原理。集群管理: 深入阅读
ClusterService
,它是 Elasticsearch 的核心模块,管理整个集群的状态和分片的分配。如果你有兴趣深入了解某个特定模块或功能,可以从相应的源码模块入手,并结合官方文档和社区资源进行学习。
分布式以及 Elastic
分布式系统要解决的第一个问题就是节点之间互相发现以及选主的机制。如果使用了 Zookeeper/Etcd 这样的成熟的服务发现工具,这两个问题都一并解决了。但 Elasticsearch 并没有依赖这样的工具,带来的好处是部署服务的成本和复杂度降低了,不用预先依赖一个服务发现的集群,缺点当然是将复杂度带入了 Elasticsearch 内部。
你提到的非常关键的一点正是 Elasticsearch 在分布式系统设计中的一个核心挑战:节点发现和选主机制。Elasticsearch 选择不依赖外部的服务发现工具,而是在内部实现了这些机制,以降低部署成本和复杂性,但同时增加了系统内部的复杂度。让我们更深入地探讨这个问题。
1. 节点发现机制
Elasticsearch 通过自带的Zen Discovery模块实现节点的自动发现。Zen Discovery 是一个插件,负责集群中节点的自动发现、主节点的选举,以及节点间的心跳和通信。
节点发现的过程:
广播发现(Multicast):在早期版本中,Elasticsearch 使用广播发现的方式,通过局域网内的多播来发现节点。但是这种方法在跨网络段或云环境中表现不佳,因此在现代版本中被弃用。
单播发现(Unicast):现在,Elasticsearch 主要使用单播发现机制。通过配置集群中的种子节点(seed nodes)的 IP 地址列表,新的节点可以通过单播请求发现其他节点,并加入集群。这些种子节点通常配置在
elasticsearch.yml
文件中。2. 主节点选举机制
Elasticsearch 在主节点选举中采用了基于 Zen Discovery 的一致性算法。在这个过程中,Elasticsearch 使用了一种类似于 Raft 或 Paxos 的算法来选举出主节点(Master Node)。主节点负责集群状态管理,包括分片的分配、索引的创建与删除等。
选举的过程:
预选阶段:当集群中的节点发现当前主节点不可用或离线时,会进入预选阶段。各节点会向其他节点发送投票请求,尝试获得足够的支持成为候选者。
选举阶段:在收到足够的投票后,节点会向集群宣布自己成为主节点。如果多个节点宣布成为主节点,则通过进一步的通信和比对来确认最终的主节点。
仲裁节点(Quorum Node):为了防止分区脑裂,Elasticsearch 要求集群中有一个仲裁节点数(通常是
(n/2) + 1
个节点,n 为集群中的节点数)来确保集群的一致性。这意味着即使集群的一部分节点发生故障,只要有超过半数的节点正常运行,集群就可以正常工作。3. 内部复杂性与外部工具的对比
正如你所提到的,Elasticsearch 没有依赖 Zookeeper 或 Etcd 这样的外部服务发现和一致性管理工具,而是选择了将这些功能集成到自己的内部模块中。这种设计带来了以下几个方面的影响:
好处:
部署简单:用户不需要为 Elasticsearch 额外部署和维护一个服务发现和一致性管理的集群,降低了运维成本和部署复杂度。
一体化解决方案:所有的功能都内置在 Elasticsearch 中,不依赖外部服务,增强了系统的自包含性。
挑战:
内部复杂性增加:为了实现这些分布式系统的核心功能,Elasticsearch 需要在其内部管理节点的发现、通信、一致性和选举逻辑。这增加了代码的复杂度和维护成本。
灵活性和扩展性:外部工具如 Zookeeper 和 Etcd 专注于一致性和服务发现,经过了广泛的测试和优化,可能在极端情况下的表现更稳定。而 Elasticsearch 内置的方案可能在一些特殊场景下表现欠佳,需要更多的调整和调优。
总结
Elasticsearch 通过内部实现节点发现和选主机制,既减少了外部依赖,也在内部增加了复杂性。这种设计权衡在实际应用中展现出其独特的优势,但也要求对系统的运维和调优有更深入的理解。如果你对这些机制的源码或细节有更深入的兴趣,建议直接研究
Zen Discovery
模块的源码和相关文档,这将帮助你更好地理解 Elasticsearch 的分布式架构设计。
服务发现以及选主 ZenDiscovery
-
节点启动后先ping(这里的ping是 Elasticsearch 的一个RPC命令。如果 discovery.zen.ping.unicast.hosts 有设置,则ping设置中的host,否则尝试ping localhost 的几个端口, Elasticsearch 支持同一个主机启动多个节点)
-
Ping的response会包含该节点的基本信息以及该节点认为的master节点。
-
选举开始,先从各节点认为的master中选,规则很简单,按照id的字典序排序,取第一个。
-
如果各节点都没有认为的master,则从所有节点中选择,规则同上。这里有个限制条件就是 discovery.zen.minimum_master_nodes,如果节点数达不到最小值的限制,则循环上述过程,直到节点数足够可以开始选举。
-
最后选举结果是肯定能选举出一个master,如果只有一个local节点那就选出的是自己。
-
如果当前节点是master,则开始等待节点数达到 minimum_master_nodes,然后提供服务。
-
如果当前节点不是master,则尝试加入master。
Elasticsearch 将以上服务发现以及选主的流程叫做 ZenDiscovery 。由于它支持任意数目的集群(1-N),所以不能像 Zookeeper/Etcd 那样限制节点必须是奇数,也就无法用投票的机制来选主,而是通过一个规则,只要所有的节点都遵循同样的规则,得到的信息都是对等的,选出来的主节点肯定是一致的。但分布式系统的问题就出在信息不对等的情况,这时候很容易出现脑裂(Split-Brain)的问题,大多数解决方案就是设置一个quorum值,要求可用节点必须大于quorum(一般是超过半数节点),才能对外提供服务。而 Elasticsearch 中,这个quorum的配置就是 discovery.zen.minimum_master_nodes 。 说到这里要吐槽下 Elasticsearch 的方法和变量命名,它的方法和配置中的master指的是master的候选节点,也就是说可能成为master的节点,并不是表示当前的master,我就被它的一个 isMasterNode 方法坑了,开始一直没能理解它的选举规则。
弹性伸缩 Elastic
Elasticsearch 的弹性体现在两个方面:
-
服务发现机制让节点很容易加入和退出。
-
丰富的设置以及allocation API。
Elasticsearch 节点启动的时候只需要配置discovery.zen.ping.unicast.hosts,这里不需要列举集群中所有的节点,只要知道其中一个即可。当然为了避免重启集群时正好配置的节点挂掉,最好多配置几个节点。节点退出时只需要调用 API 将该节点从集群中排除 (Shard Allocation Filtering),系统会自动迁移该节点上的数据,然后关闭该节点即可。当然最好也将不可用的已知节点从其他节点的配置中去除,避免下次启动时出错。
分片(Shard)以及副本(Replica) 分布式存储系统为了解决单机容量以及容灾的问题,都需要有分片以及副本机制。Elasticsearch 没有采用节点级别的主从复制,而是基于分片。它当前还未提供分片切分(shard-splitting)的机制,只能创建索引的时候静态设置。
(elasticsearch 官方博客的图片)
比如上图所示,开始设置为5个分片,在单个节点上,后来扩容到5个节点,每个节点有一个分片。如果继续扩容,是不能自动切分进行数据迁移的。官方文档的说法是分片切分成本和重新索引的成本差不多,所以建议干脆通过接口重新索引。
Elasticsearch 的分片默认是基于id 哈希的,id可以用户指定,也可以自动生成。但这个可以通过参数(routing)或者在mapping配置中修改。当前版本默认的哈希算法是MurmurHash3。
Elasticsearch 禁止同一个分片的主分片和副本分片在同一个节点上,所以如果是一个节点的集群是不能有副本的。
恢复以及容灾
分布式系统的一个要求就是要保证高可用。前面描述的退出流程是节点主动退出的场景,但如果是故障导致节点挂掉,Elasticsearch 就会主动allocation。但如果节点丢失后立刻allocation,稍后节点恢复又立刻加入,会造成浪费。Elasticsearch的恢复流程大致如下:
-
集群中的某个节点丢失网络连接
-
master提升该节点上的所有主分片的在其他节点上的副本为主分片
-
cluster集群状态变为 yellow ,因为副本数不够
-
等待一个超时设置的时间,如果丢失节点回来就可以立即恢复(默认为1分钟,通过 index.unassigned.node_left.delayed_timeout 设置)。如果该分片已经有写入,则通过translog进行增量同步数据。
-
否则将副本分配给其他节点,开始同步数据。
但如果该节点上的分片没有副本,则无法恢复,集群状态会变为red,表示可能要丢失该分片的数据了。
分布式集群的另外一个问题就是集群整个重启后可能导致不预期的分片重新分配(部分节点没有启动完成的时候,集群以为节点丢失),浪费带宽。所以 Elasticsearch 通过以下静态配置(不能通过API修改)控制整个流程,以10个节点的集群为例:
-
gateway.recover_after_nodes: 8
-
gateway.expected_nodes: 10
-
gateway.recover_after_time: 5m
比如10个节点的集群,按照上面的规则配置,当集群重启后,首先系统等待 minimum_master_nodes(6)个节点加入才会选出master, recovery操作是在 master节点上进行的,由于我们设置了 recover_after_nodes(8),系统会继续等待到8个节点加入, 才开始进行recovery。当开始recovery的时候,如果发现集群中的节点数小于expected_nodes,也就是还有部分节点未加入,于是开始recover_after_time 倒计时(如果节点数达到expected_nodes则立刻进行 recovery),5分钟后,如果剩余的节点依然没有加入,则会进行数据recovery。
搜索引擎 Search
Elasticsearch 除了支持 Lucene 本身的检索功能外,在之上做了一些扩展。
-
脚本支持
Elasticsearch 默认支持groovy脚本,扩展了 Lucene 的评分机制,可以很容易的支持复杂的自定义评分算法。它默认只支持通过sandbox方式实现的脚本语言(如lucene expression,mustache),groovy必须明确设置后才能开启。Groovy的安全机制是通过java.security.AccessControlContext设置了一个class白名单来控制权限的,1.x版本的时候是自己做的一个白名单过滤器,但限制策略有漏洞,导致一个远程代码执行漏洞。 -
默认会生成一个 _all 字段,将所有其他字段的值拼接在一起。这样搜索时可以不指定字段,并且方便实现跨字段的检索。
-
Suggester Elasticsearch 通过扩展的索引机制,可以实现像google那样的自动完成suggestion以及搜索词语错误纠正的suggestion。
NoSQL 数据库
Elasticsearch 可以作为数据库使用,主要依赖于它的以下特性:
-
默认在索引中保存原始数据,并可获取。这个主要依赖 Lucene 的store功能。
-
实现了translog,提供了实时的数据读取能力以及完备的数据持久化能力(在服务器异常挂掉的情况下依然不会丢数据)。Lucene 因为有 IndexWriter buffer, 如果进程异常挂掉,buffer中的数据是会丢失的。所以 Elasticsearch 通过translog来确保不丢数据。同时通过id直接读取文档的时候,Elasticsearch 会先尝试从translog中读取,之后才从索引中读取。也就是说,即便是buffer中的数据尚未刷新到索引,依然能提供实时的数据读取能力。Elasticsearch 的translog 默认是每次写请求完成后统一fsync一次,同时有个定时任务检测(默认5秒钟一次)。如果业务场景需要更大的写吞吐量,可以调整translog相关的配置进行优化。
-
dynamic-mapping 以及 schema-free
Elasticsearch 的dynamic-mapping相当于根据用户提交的数据,动态检测字段类型,自动给数据库表建立表结构,也可以动态增加字段,所以它叫做schema-free,而不是schema-less。这种方式的好处是用户能一定程度享受schema-less的好处,不用提前建立表结构,同时因为实际上是有schema的,可以做查询上的优化,检索效率要比纯schema-less的数据库高许多。但缺点就是已经创建的索引不能变更数据类型(Elasticsearch 写入数据的时候如果类型不匹配会自动尝试做类型转换,如果失败就会报错,比如数字类型的字段写入字符串”123”是可以的,但写入”abc”就不可以。),要损失一定的自由度。另外 Elasticsearch 提供的index-template功能方便用户动态创建索引的时候预先设定索引的相关参数以及type mapping,比如按天创建日志库,template可以设置为对 log-* 的索引都生效。
这两个功能我建议新的数据库都可以借鉴下。
-
丰富的QueryDSL功能
Elasticsearch 的query语法基本上和sql对等的,除了join查询,以及嵌套临时表查询不能支持。不过 Elasticsearch 支持嵌套对象以及parent外部引用查询,所以一定程度上可以解决关联查询的需求。另外group by这种查询可以通过其aggregation实现。Elasticsearch 提供的aggregation能力非常强大,其生态圈里的 Kibana 主要就是依赖aggregation来实现数据分析以及可视化的。
你提到的这些特性确实是 Elasticsearch 能够作为数据库使用的核心原因,涵盖了从数据存储、数据持久化、动态映射到强大的查询能力等多个方面。让我详细地解读这些特性,并探讨它们如何让 Elasticsearch 成为一个有效的数据库选项。
1. 原始数据存储与读取
Lucene 的存储功能:
Elasticsearch 基于 Lucene,每个索引都包含倒排索引、存储字段(stored fields)、文档值(doc values)、词典(term dictionary)等多个数据结构。倒排索引用于高效的全文搜索,而存储字段允许 Elasticsearch 保存原始的文档数据,使其能够在查询时直接返回原始文档。
这意味着 Elasticsearch 不仅仅是一个搜索引擎,还能持久化存储原始数据,支持直接查询和读取。
2. Translog 机制
Translog 的角色:
Lucene 本身不具备事务日志(translog)功能,这会导致在某些情况下(如进程崩溃时)数据丢失。为了弥补这一点,Elasticsearch 实现了 translog,作为一个附加的日志机制,记录所有即将写入的操作。
实时读取:当写入操作发生时,数据首先被写入到 translog,然后被写入到 Lucene 索引中。当通过 ID 直接读取文档时,Elasticsearch 首先检查 translog,如果数据在其中,便直接从 translog 返回数据,否则从索引中获取。这确保了即使索引更新延迟,用户也能获得最新的数据。
持久化与 fsync:默认情况下,Elasticsearch 在每次写请求完成后将 translog 刷新到磁盘(fsync),确保在系统崩溃时数据不会丢失。用户可以根据具体的性能需求调整刷新频率,以在数据安全性和写入性能之间找到平衡。
3. Dynamic Mapping 以及 Schema-free 特性
Dynamic Mapping:
当用户第一次插入数据时,Elasticsearch 会自动推断字段的类型,并在索引中动态创建相应的映射。这种机制称为 Dynamic Mapping。它可以使开发者免去提前定义索引结构的工作,让数据的存储变得更加灵活。
Schema-free(而非 Schema-less):虽然被称为“Schema-free”,但实际上,Elasticsearch 在底层是有 schema(映射)的。它自动生成和维护这些 schema,确保查询时能够进行优化。相比于完全 schema-less 的数据库,这种方式既保留了一定的灵活性,也保证了查询效率。
限制与转换:一旦字段类型被确定,后续写入的数据必须与该类型一致。Elasticsearch 会自动尝试将输入数据转换为预期的类型(如将字符串“123”转换为整数),但如果转换失败(如尝试将“abc”转换为整数),则会抛出错误。这种行为保证了数据的一致性和类型安全。
Index Templates:
Elasticsearch 提供了 Index Template 功能,使得在动态创建索引时,可以预定义索引的映射和设置。例如,用户可以定义一个模板,适用于所有名称以
log-*
开头的索引。这样,自动创建的新索引将继承模板中的配置,简化了索引管理。日志管理:在需要按天创建日志索引的场景下,Index Template 非常有用。它确保每个新创建的日志索引都具有一致的结构和配置。
4. 丰富的 Query DSL 功能
Query DSL(Domain Specific Language):
Elasticsearch 的查询 DSL 提供了非常丰富的查询功能,几乎可以实现 SQL 查询的大部分功能。它支持复杂的布尔查询、范围查询、全文检索等。
Join 查询的限制:与传统的关系数据库不同,Elasticsearch 不直接支持表与表之间的 Join 操作。这是因为它的底层数据结构更适合处理独立的文档集合而非关联数据。然而,通过使用嵌套对象和父子关系,可以在一定程度上实现类似 Join 的查询需求。
Aggregation 功能:
Elasticsearch 的 Aggregation 功能类似于 SQL 中的
GROUP BY
和聚合函数,但功能更强大。它可以用于执行复杂的统计分析、分组、嵌套聚合等操作。数据分析与可视化:Aggregation 功能使 Elasticsearch 特别适合用作数据分析平台,尤其是在结合 Kibana 这样的工具时。Kibana 利用 Aggregation API 实现了强大的数据可视化功能,用户可以轻松地对海量数据进行统计分析和展示。
总结
Elasticsearch 作为数据库使用,依赖于其以下几个关键特性:
原始数据的持久化存储:通过 Lucene 的存储功能,Elasticsearch 能够存储和返回原始数据,不仅限于搜索。
实时数据读取与持久化:通过 translog 机制,Elasticsearch 实现了近实时的数据读取和可靠的持久化,确保数据在异常情况下也不会丢失。
灵活的动态映射与 schema-free 设计:Dynamic Mapping 提供了高度的灵活性,同时仍保留 schema 优化查询。
强大的查询和聚合能力:丰富的 Query DSL 和 Aggregation 功能,使得 Elasticsearch 能够应对复杂的查询和数据分析需求。
这些特性使得 Elasticsearch 在特定场景下能够作为一个强大的数据库,特别是在需要高效搜索、实时分析、灵活数据模型的应用中表现出色。
Elasticsearch 提供了丰富的查询和聚合能力,使其成为一个强大的数据分析和搜索平台。以下是关于Elasticsearch的查询和聚合能力的相关信息:
Query DSL
Elasticsearch 的 Query DSL(查询领域特定语言)允许用户以 JSON 格式定义复杂的查询。它类似于 SQL 查询语言,但专为 Elasticsearch 的特性设计。Query DSL 支持各种查询类型,包括全文搜索、结构化搜索、地理位置搜索等。用户可以通过 Query DSL 构建精确匹配、范围查询、布尔逻辑查询等,以满足不同的搜索需求。
Aggregation 功能
Elasticsearch 的 Aggregation 功能允许用户对搜索结果进行分组、统计和计算,从而获得数据的分布情况和趋势。聚合类型包括度量聚合(如计算平均值、总和、最小值、最大值等)、桶聚合(如按字段值分组、按时间间隔分组等)、管道聚合(对聚合结果进行二次处理)等。这些功能使得 Elasticsearch 能够进行复杂的数据分析,如电商平台销售分析、社交媒体用户行为分析等。
嵌套聚合
Elasticsearch 支持聚合的嵌套,即在一个聚合内部再嵌套另一个聚合。这种特性使得用户可以执行更细粒度的数据分析,例如,首先按产品类别分组,然后在每个类别内部计算价格的平均值。嵌套聚合提供了强大的数据分析能力,使用户能够从数据中提取出更深层次的洞察。
脚本支持
Elasticsearch 允许在查询和聚合中使用脚本,这为用户提供了更大的灵活性。脚本可以用于执行动态计算,但需要注意的是,脚本可能会影响性能,并且可能存在安全风险。因此,在使用脚本时,应谨慎考虑其影响,并尽量使用沙盒环境中的脚本语言。
实时搜索和分析
Elasticsearch 的实时搜索功能使其能够立即处理和分析新数据,提供近实时的查询和分析结果。这对于需要实时监控和响应的应用程序来说非常关键。
通过这些强大的查询和聚合能力,Elasticsearch 不仅能够作为搜索引擎使用,还能够作为数据分析平台,满足各种复杂的数据分析需求。
在 Elasticsearch 中,"schema 优化查询" 指的是通过预定义的数据结构(即 schema)来优化搜索和查询性能。尽管 Elasticsearch 支持动态映射(dynamic mapping),允许在没有预先定义 schema 的情况下自动推断字段类型,但在某些情况下,预定义 schema 可以带来以下好处:
1. 性能优化:
- 预定义 schema 可以让 Elasticsearch 更好地了解数据的布局和结构,从而优化索引和搜索的性能。
- 例如,通过指定字段类型,Elasticsearch 可以使用更高效的数据存储和检索方法。
2. 数据一致性:
- 预定义 schema 可以确保数据的一致性和准确性,避免因为动态映射导致的类型不匹配或错误。
- 例如,如果你知道某个字段应该是整数类型,但在动态映射中它被错误地推断为字符串类型,这可能会导致查询错误或性能下降。
3. 查询优化:
- 预定义 schema 可以帮助 Elasticsearch 更好地优化查询计划,从而提高查询速度。
- 例如,Elasticsearch 可以利用 schema 中的信息来选择最有效的查询算法和数据结构。
4. 文档和版本控制:
- 预定义 schema 可以作为文档的一部分,帮助团队成员理解数据的结构和要求。
- 通过版本控制 schema,可以更容易地管理和跟踪数据结构的变化。
5. 集成和工具支持:
- 许多工具和库依赖于预定义的 schema 来生成代码、验证数据或提供其他功能。
- 预定义 schema 可以简化这些工具的集成和使用。
总之,"schema 优化查询" 指的是通过预定义的数据结构来提高 Elasticsearch 的查询性能、数据一致性和整体效率。尽管动态映射提供了灵活性,但在某些情况下,预定义 schema 可以带来显著的好处。
确实,Elasticsearch 作为一个强大的分布式搜索和分析引擎,也可以作为数据库使用,这主要得益于它的一些关键特性:
1. 原始数据的持久化存储:
- Elasticsearch 使用 Lucene 库来存储数据,这使得它能够高效地处理大量数据,并提供快速的搜索和分析能力。
- 数据以文档的形式存储在索引中,每个文档可以包含多个字段,这些字段可以是各种数据类型。
2. 实时数据读取与持久化:
- Elasticsearch 通过 translog(事务日志)机制来确保数据的持久性和一致性。每次对数据的修改都会被记录在 translog 中,即使在系统崩溃的情况下,也可以通过重放 translog 来恢复数据。
- Elasticsearch 还支持近实时的搜索,这意味着新写入的数据通常在几秒钟内就可以被搜索到。
3. 灵活的动态映射与 schema-free 设计:
- Dynamic Mapping 允许 Elasticsearch 在没有预先定义 schema 的情况下自动推断字段类型。这对于快速迭代和适应不断变化的数据结构非常有用。
- 尽管 Dynamic Mapping 提供了灵活性,但 Elasticsearch 也支持预定义的 schema,这有助于优化查询性能和数据一致性。
除此之外,Elasticsearch 还具有以下特性,使其适合作为数据库使用:
- 分布式架构:Elasticsearch 是一个分布式系统,可以自动将数据分片并分布在多个节点上,从而提供高可用性和可扩展性。
- 强大的搜索功能:Elasticsearch 提供了丰富的搜索功能,包括全文搜索、结构化搜索、地理位置搜索等。
- 聚合和分析:Elasticsearch 支持复杂的聚合查询,可以用于数据分析和可视化。
- RESTful API:Elasticsearch 提供了基于 RESTful 的 API,使得与其他系统的集成变得简单。
总的来说,Elasticsearch 的这些特性使其成为一个强大的全文搜索引擎和数据库,适用于需要快速搜索、分析和存储大量数据的场景。
Elasticsearch 作为一个分布式搜索和分析引擎,虽然最初的设计目的是为了全文搜索,但由于其强大的查询功能、数据处理能力和分布式架构,它可以在特定场景下被用作数据库。下面我详细说明为什么 Elasticsearch 可以作为数据库使用:
1. 文档存储模型
JSON 文档:Elasticsearch 使用 JSON 格式来存储数据,每个文档包含多个字段和值。这使得 Elasticsearch 非常适合存储复杂的嵌套数据结构,类似于 NoSQL 数据库。
灵活的 Schema:虽然 Elasticsearch 支持动态映射(即自动为新字段创建映射),但用户也可以手动定义索引的映射(类似于关系数据库中的表结构)。这种灵活性允许根据需要调整数据模式。
2. 分布式存储与扩展性
分片和副本机制:Elasticsearch 的索引被划分为多个分片(primary shards),每个分片可以有一个或多个副本(replica shards)。这种设计确保了数据的高可用性和扩展性,支持在多个节点上分布式存储和查询数据。
水平扩展:Elasticsearch 可以通过增加节点来扩展存储和计算能力,适应大规模数据存储的需求,这与许多 NoSQL 数据库类似。
3. 强大的查询能力
全文搜索:Elasticsearch 内置了强大的全文搜索功能,支持复杂的查询、分词、权重计算等。这使得它特别适合需要全文搜索的应用场景,如日志管理、文档搜索等。
复杂查询语法:Elasticsearch 支持丰富的查询 DSL(Domain Specific Language),可以执行布尔查询、聚合分析、地理查询、模糊查询等。这些功能使得 Elasticsearch 能够处理比传统数据库更复杂的查询需求。
聚合分析:Elasticsearch 提供的聚合功能类似于 SQL 中的 GROUP BY,可以快速计算数据的统计信息,如计数、求和、平均值等,这使得它在实时分析场景中表现出色。
4. 实时性
近实时(Near Real-Time, NRT):Elasticsearch 以近实时的方式处理数据,数据一旦被索引,几乎立即可以被搜索到。这对于需要快速查询和分析最新数据的应用场景非常有用,如监控系统和实时数据分析。
5. 冗余与高可用性
副本机制:Elasticsearch 的副本机制不仅提供了数据冗余,还能提高查询性能。通过将查询分布到多个副本上,系统能够更快地响应请求。
灾难恢复:由于分布式架构,Elasticsearch 能够在节点故障时自动恢复,确保数据的高可用性和系统的容错性。
6. 数据持久化
持久化存储:尽管 Elasticsearch 最初是为搜索设计的,但它确实将数据持久化存储在磁盘上,并支持定期快照(snapshot)以备份数据。这些特性使得它能够像传统数据库一样持久保存数据。
7. 与数据库的差异与挑战
尽管 Elasticsearch 可以在很多场景下替代传统数据库,但它与关系数据库和一些 NoSQL 数据库相比,也有一些限制和挑战:
事务支持:Elasticsearch 并不支持像关系数据库那样的 ACID 事务,尽管它支持写操作的原子性,但在分布式环境下实现的强一致性事务是一个挑战。因此,在需要强事务性的应用场景下,Elasticsearch 可能不适合。
更新操作:在 Elasticsearch 中,文档更新是通过删除旧文档并插入新文档来实现的。这意味着频繁更新可能会导致性能下降和磁盘空间浪费。
聚合性能:虽然 Elasticsearch 的聚合功能非常强大,但在处理非常大的数据集时,性能可能会受限于内存和计算能力。
存储成本:由于 Elasticsearch 需要为搜索建立倒排索引,这会增加存储空间的使用。因此,在需要大量存储但不需要全文搜索的场景中,存储成本可能较高。
总结
Elasticsearch 作为数据库使用有很多独特的优势,特别是在需要高效搜索、实时分析和大规模分布式存储的场景中。然而,它也有自己的限制,特别是在事务性需求和存储效率方面。因此,在考虑将 Elasticsearch 作为数据库使用时,必须根据具体的应用场景进行权衡。如果你的应用场景适合 Elasticsearch 的强项,它将是一个非常强大且灵活的数据库选择。
系统架构
Elasticsearch 的依赖注入用的是guice,网络使用netty,提供http rest和RPC两种协议。
Elasticsearch 之所以用guice,而不是用spring做依赖注入,关键的一个原因是guice可以帮它很容易的实现模块化,通过代码进行模块组装,可以很精确的控制依赖注入的管理范围。比如 Elasticsearch 给每个shard单独生成一个injector,可以将该shard相关的配置以及组件注入进去,降低编码和状态管理的复杂度,同时删除shard的时候也方便回收相关对象。这方面有兴趣使用guice的可以借鉴。
ClusterState
前面我们分析了 Elasticsearch 的服务发现以及选举机制,它是内部自己实现的。服务发现工具做的事情其实就是跨服务器的状态同步,多个节点修改同一个数据对象,需要有一种机制将这个数据对象同步到所有的节点。Elasticsearch 的ClusterState 就是这样一个数据对象,保存了集群的状态,索引/分片的路由表,节点列表,元数据等,还包含一个ClusterBlocks,相当于分布式锁,用于实现分布式的任务同步。
主节点上有个单独的进程处理 ClusterState 的变更操作,每次变更会更新版本号。变更后会通过PRC接口同步到其他节点。主节知道其他节点的ClusterState 的当前版本,发送变更的时候会做diff,实现增量更新。
Rest 和 RPC
Elasticsearch 的rest请求的传递流程如上图(这里对实际流程做了简化):
-
用户发起http请求,Elasticsearch 的9200端口接受请求后,传递给对应的RestAction。
-
RestAction做的事情很简单,将rest请求转换为RPC的TransportRequest,然后调用NodeClient,相当于用客户端的方式请求RPC服务,只不过transport层会对本节点的请求特殊处理。
这样做的好处是将http和RPC两层隔离,增加部署的灵活性。部署的时候既可以同时开启RPC和http服务,也可以用client模式部署一组服务专门提供http rest服务,另外一组只开启RPC服务,专门做data节点,便于分担压力。
Elasticsearch 的RPC的序列化机制使用了 Lucene 的压缩数据类型,支持vint这样的变长数字类型,省略了字段名,用流式方式按顺序写入字段的值。每个需要传输的对象都需要实现:
void writeTo(StreamOutput out)
T readFrom(StreamInput in)
两个方法。虽然这样实现开发成本略高,增删字段也不太灵活,但对 Elasticsearch 这样的数据库系统来说,不用考虑跨语言,增删字段肯定要考虑兼容性,这样做效率最高。所以 Elasticsearch 的RPC接口只有java client可以直接请求,其他语言的客户端都走的是rest接口。
网络层
Elasticsearch 的网络层抽象很值得借鉴。它抽象出一个 Transport 层,同时兼有client和server功能,server端接收其他节点的连接,client维持和其他节点的连接,承担了节点之间请求转发的功能。Elasticsearch 为了避免传输流量比较大的操作堵塞连接,所以会按照优先级创建多个连接,称为channel。
-
recovery: 2个channel专门用做恢复数据。如果为了避免恢复数据时将带宽占满,还可以设置恢复数据时的网络传输速度。
-
bulk: 3个channel用来传输批量请求等基本比较低的请求。
-
regular: 6个channel用来传输通用正常的请求,中等级别。
-
state: 1个channel保留给集群状态相关的操作,比如集群状态变更的传输,高级别。
-
ping: 1个channel专门用来ping,进行故障检测。
(3个节点的集群连接示意,来源 Elasticsearch 官方博客)
每个节点默认都会创建13个到其他节点的连接,并且节点之间是互相连接的,每增加一个节点,该节点会到每个节点创建13个连接,而其他每个节点也会创建13个连回来的连接。
线程池
由于java不支持绿色线程(fiber/coroutine),我前面的《并发之痛》那篇文章也分析了线程池的问题,线程池里保留多少线程合适?如何避免慢的任务占用线程池,导致其他比较快的任务也得不到执行?很多应用系统里,为了避免这种情况,会随手创建线程池,最后导致系统里充塞了大的量的线程池,浪费资源。而 Elasticsearch 的解决方案是分优先级的线程池。它默认创建了10多个线程池,按照不同的优先级以及不同的操作进行划分。然后提供了4种类型的线程池,不同的线程池使用不同的类型:
-
CACHED 最小为0,无上限,无队列(SynchronousQueue,没有缓冲buffer),有存活时间检测的线程池。通用的,希望能尽可能支撑的任务。
-
DIRECT 直接在调用者的线程里执行,其实这不算一种线程池方案,主要是为了代码逻辑上的统一而创造的一种线程类型。
-
FIXED 固定大小的线程池,带有缓冲队列。用于计算和IO的耗时波动较小的操作。
-
SCALING 有最小值,最大值的伸缩线程池,队列是基于LinkedTransferQueue 改造的实现,和java内置的Executors生成的伸缩线程池的区别是优先增加线程,增加到最大值后才会使用队列,和java内置的线程池规则相反。用于计算和IO耗时都不太稳定,需要限制系统承载最大任务上限的操作。
这种解决方案虽然要求每个用到线程池的地方都需要评估下执行成本以及应该用什么样的线程池,但好处是限制了线程池的泛滥,也缓解了不同类型的任务互相之间的影响。
脑洞时间
以后每篇分析架构的文章,我都最后会提几个和该系统相关的改进或者扩展的想法,称为脑洞时间,作为一种锻炼。不过只提供想法,不深入分析可行性以及实现。
-
支持shard-spliting
这个被人吐糟了好长时间,官方就是不愿意提供。我简单构想了下,感觉实现这个应该也不复杂。一种实现方式是按照传统的数据库sharding机制,1分2,2分4,4分8等,主要扩展点在数据迁移以及routing的机制上。但这种方式没办法实现1分3,3分5,这样的sharding。另外一个办法就是基于当前官方推荐的重建索引的机制,只是对外封装成resharding的接口,先给旧索引创建别名,客户端通过别名访问索引,然后设定新索引的sharding数目,后台创建新的索引,倒数据,等数据追上的时候,切换别名,进行完整性检查,这样整个resharding的机制可以自动化了。 -
支持mapreduce
认为Elasticsearch 可以借鉴 Mongo 的轻量mapreduce机制,这样可以支持更丰富的聚合查询。 -
支持语音以及图片检索
当前做语音和图片识别的库或者服务的开发者可以提供一个 Elasticsearch 插件,把语音以及图片转换成文本进行索引查询,应用场景应该也不少。 -
用ForkJoinPool来替代 Elasticsearch 当前的线程池方案
ForkJoinPool加上java8的CompletableFuture,一定程度上可以模拟coroutine效果,再加上最新版本的netty内部已经默认用了ForkJoinPool,Elasticsearch 这种任务有需要拆子任务的场景,很适合使用ForkJoinPool。