Site icon 时鹏亮的Blog

Elasticsearch 从来就不是数据库

以下内容转载自:James Blackwood-Sewell Elasticsearch Was Never a Database

Elasticsearch 从来就不是一个数据库。它是作为一个基于 Apache Lucene (一个功能强大的全文搜索库) 的搜索引擎 API 而构建的,但并非作为一个记录系统。即使是 Elastic 自己的指导长期以来也建议你的真相来源应该存在于其他地方,Elasticsearch 只是作为一个次要索引。然而,在过去的十年里,许多团队试图将搜索引擎扩展成为他们的主要数据库,通常会得到意想不到的结果。

我们说的 “数据库” 是什么意思?

需要说明的是,当我们在这个语境中提到数据库时,我们指的是一个可以用作 OLTP 事务性工作负载主要数据存储的系统:这是你的应用程序真实存在的地方。想想 Postgres (连续三年被投票选为最受欢迎的数据库)、MySQL, 甚至 Oracle。

我们是如何走到这一步的?

故事通常从一个简单的需求开始:搜索。一个团队已经在使用 Postgres 或 MySQL 存储他们的应用程序数据,但内置的文本搜索功能无法扩展。Elasticsearch 看起来是完美的解决方案;它快速、灵活,而且容易启动。

起初,它只是一个索引。文档存在于数据库中,而副本存在于 Elastic 中进行搜索。但随着时间推移,这条线开始变得模糊。如果文档已经存在于 Elasticsearch 中,为什么还要费心将它们写入数据库呢?保持两个存储同步的过程是栈中最脆弱的部分,所以为什么不摆脱它呢?现在,搜索索引也是数据库。记录系统已经悄悄地发生了变化。

问题就从这里开始。数据库不仅仅是存储 JSON、文本文档和一些元数据的地方。它是权威的真相来源,是保护应用程序数据安全的仲裁者。这个角色承载着期望:原子事务、可预测的更新、安全演变模式的能力、丰富的查询 (允许你提出超出检索范围的问题) 以及故障时的可靠性。Elasticsearch 并非为解决这一系列问题而构建。作为索引,它很出色,但作为数据库,它很脆弱。

从未发生过的交易

第一个裂缝出现在一致性方面。在关系数据库中,事务确保相关写操作同时成功或失败。如果你插入一个订单并减少库存,这两个操作是原子性的。要么两者都发生,要么两者都不发生。

Elasticsearch 无法在单个文档之外提供这种保证。写操作会独立成功,并且可能出现故障。如果逻辑组中的一个操作失败,你只需要执行一半的操作。起初,团队会添加重试或协调任务,试图弥补差距。但这时 Elasticsearch 就不再像数据库那样运行了。一个记录系统永远不应该让不一致性随着时间的推移悄然潜入。

你可以在读取方面看到同样的问题。Elasticsearch 实际上有两种类型的读取:GET by ID 和 SEARCH。GET 总是返回文档的最新确认版本,反映了数据库的工作方式 (尽管在故障情况下可能会发生脏读取)。然而,SEARCH 只查看 Lucene 段,这些段是异步刷新的。这意味着最近确认的写入可能要等到下次刷新才会出现。

数据库通过事务边界和隔离级别来解决这些问题。Elasticsearch 没有这些,因为它不需要这些来成为一个有效的搜索引擎。

需要重新索引的模式迁移

然后应用程序发生了变化。一个曾经是整数的字段现在需要小数位。一个文本字段被重新命名。在 Postgres 或 MySQL 中,这将是一个简单的 ALTER TABLE。在 Elasticsearch 中,索引映射一旦设置就不可变,因此有时唯一的选择是创建一个包含更新映射的新索引,并将每个文档都传输到其中。

当 Elasticsearch 位于另一个数据库的下游时,这是痛苦的 (完全的网络传输), 但是安全的,你可以从真实的信息源重放。但是当 Elasticsearch 是唯一的存储时,模式迁移需要将整个记录系统迁移到一个新的结构中,在负载下,没有安全网 (除了恢复)。本应是常规的模式更改可能变成一个高风险的操作。

没有连接的查询

一旦 Elasticsearch 成为主要存储,开发人员自然想要的不仅仅是搜索。他们想要对数据提出问题。这就是你开始遇到另一堵墙的地方。

Elasticsearch 基于 JSON 的查询 DSL 对于全文查询和聚合功能强大,但对于关系工作负载却有限。用 Elastic 自己的话说,它 “支持复杂的搜索、过滤和聚合”, 但如果你想超越这些,就会发现其中的缺陷。你期望从记录系统 (如基本连接) 中获得的特性要么缺失,要么只得到部分支持。

考虑以下 SQL 查询:

-- What are the top ten products by average review rating, -- only considering products with at least 50 reviews SELECT p.id, p.name, AVG(r.rating) AS avg_rating FROM products p JOIN reviews r ON r.product_id = p.id GROUP BY p.id, p.name HAVING COUNT(r.id) >= 50 ORDER BY avg_rating DESC LIMIT 10;

在 Postgres 中,这是常规操作。在 Elasticsearch 中,你的选择很笨拙:将评论反规范化到每个产品文档中 (在每个新的评论中重写产品), 将评论作为子代嵌入产品中,或者分别查询两个索引并在应用程序代码中将结果拼接在一起。

Elastic 一直在努力解决这个差距。最新的 ES|QL 引入了一个类似的特性,称为查找连接,而 Elastic SQL 提供了一种更为人所熟悉的语法 (没有连接)。但这些仍然受 Lucene 的底层索引模型的约束。除此之外,开发人员现在面临着一个令人困惑的重叠查询语法网络 (目前为:查询 DSL、ES|QL、SQL、EQL、KQL), 每种语法都适用于不同的使用案例,并具有不同的优缺点。

这是一种进步,但不能与关系数据库相提并论。

可靠性可能会短暂下降

最终,每个系统都会崩溃。索引和数据库之间的区别在于它们如何恢复。数据库使用预写或重做日志来确保一旦事务被提交,其所有更改都是持久的,并且在崩溃后能够清晰地重放。

在正常操作下,Elasticsearch 的持久性也达到了其设计初衷:单个文档写入。Translog 确保已认可的文档在主分片上同步,能够在崩溃中存活,并且可以在恢复时重放。但是,正如我们在事务中所看到的,这种持久性不会超出单个文档的范围。没有事务边界来保证相关写入同时存活或失败 (因为这个概念根本不存在)。失败可能会导致操作半途而废,恢复也不会像数据库那样回滚操作。

当 Elasticsearch 是一个叠加在数据库之上的索引时,这种假设是可以接受的。然而,如果它是你唯一的存储,事务持久性的差距就会变成正确性的差距。中断不仅会减慢搜索速度,还会使你的记录系统面临风险。

损害稳定性的操作

大规模运行 Elasticsearch 引入了另一种现实检查。数据库应该是稳定的基础:你运行它们,监控它们,并相信它们会保护你的数据安全。Elasticsearch 被设计成具有不同的优先级:弹性。分片可以移动,集群可以扩展和收缩,数据可以重新索引或重新平衡。这种弹性非常强大,但分布式系统伴随着操作权衡。分片可能会失去平衡,JVM 堆需要仔细调优,重新索引会消耗集群容量,滚动升级可能会导致流量停滞。

Elastic 添加了工具来缓解这些挑战,许多团队确实成功地运行了大型集群。但基准期望是不同的。关系数据库的设计是为了稳定性和正确性,因为它假设它将是你的真相来源。Elasticsearch “优化了速度和相关性”, 同时作为一个记录系统运行意味着承受比数据库更多的作业风险。

误用的代价

Elasticsearch 的操作已经很复杂,资源占用很大。当你试图将其作为主数据库时,这两方面的成本都被放大了。在单个系统上运行感觉像是一种简化,但这往往会使事情变得更加困难,因为你有两个不同的优化目标。

事务缺口、脆弱迁移、有限查询、复杂操作和变通方法都堆积如山。你非但没有降低复杂性,反而将其集中在最脆弱的地方。结果比你最初的解决方案还要糟糕:工程努力增加,运营成本提高,而且仍然没有得到你期望从真相来源获得的任何保证。

那么 Elasticsearch 的前景如何呢?

老实说,这让它回到了它应该回到的地方,也是它开始的地方:一个搜索引擎。Elasticsearch (以及它下面的 Apache Lucene) 是一项令人难以置信的成就,为各地的开发人员带来了世界级的搜索。只要你不试图将它用作一个记录系统,它就能完全满足你的需求。

然而,即使 “正确” 使用,最困难的部分通常不是搜索本身,而是它周围的一切。ETL 管道、同步作业和摄取层很快就成为堆栈中最脆弱的部分。

这就是 ParadeDB 的用武之地。将其作为主数据库运行,在一个系统中结合 OLTP 和全文搜索,或者保留现有的 Postgres 数据库,并通过将其部署为逻辑跟随器来消除 ETL。

如果你想要具有正确性、简单性和世界级性能的开源搜索,可以从 ParadeDB 开始。


尊重他人劳动成果。转载请务必附上原文链接,我将感激不尽。


与《Elasticsearch 从来就不是数据库》相关的博文:

Exit mobile version