为什么 "SELECT * FROM table" 并非一个读操作?(文章翻译)

为什么 "SELECT * FROM table" 并非一个读操作?(文章翻译)

文一

2024-09-15 发布52 浏览 · 0 点赞 · 0 收藏

我们期待越来越多的朋友们参与到“内核一周一审”之中,在一个又一个内核细节的积累之中,提升自己的“功力”!

本篇文章来自于 https://www.cybertec-postgresql.com/en/why-select-from-table-is-not-a-read/ ,非常感谢 Cybertec 的工作人员。


人们总是认为,SELECT 操作总是一种“读”操作。这种看法,似乎显然与明了,但是从理智的角度上面来看,它真的正确吗?难道 SELECT 语句真的只是读操作吗?难道我们真的能够仅仅从语句类别的归属(译者注:即 SELECT, INSERT, UPDATE, DELETE 所对应的 “增删查改”)上,就能够实现完全的负载均衡吗?

实践中的经验指出,并非如此。事实在于,一些从表面上看完全是读操作的内容,实际上并不只展开了读取数据的操作 --- 而这显然影响深远,它也将是我们将要在后面文章中所讨论的内容。

负载均衡:我们如何让其工作于错误的方式之上?

人们总是尝试着在假定查询可以通过某些类型的中间件展开处理这一基础上,实现对于查询的负载均衡。不过,这种做法注定是要失败的。简单来说:不要构建那种神奇地决定要在中间件中做些什么的软件。

img

为什么我们这么阐述呢?请查看如下的语句:

SELECT * FROM tab;

但是当我们执行下面的语句的时候,请猜想会发生些什么?

test=# CREATE TABLE a (id int);
CREATE TABLE
test=# CREATE FUNCTION x(int) 
    RETURNS int AS ' INSERT INTO a VALUES ($1) RETURNING *' 
LANGUAGE 'sql';
CREATE FUNCTION

在这个案例之中,我定义了一张简单的数据表,同时构建了一个函数,它将会向数据表中插入一些简单的数据。但是当我们加入如下所示的视图的时候,请看将会发生些怎么样的事情?

# 译者补充:在 PostgreSQL 中,视图基于规则系统(Rule System)实现,因此它实际上就是一组被打包好的 SQL 查询语句
test=# CREATE VIEW tab AS SELECT * FROM a, x(10) AS y;
CREATE VIEW

它将会带来一些改变,因为我们访问视图非常容易:

test=# SELECT * FROM tab;
 id | y 
----+---
(0 rows)

如果我们再次尝试调用之,我们将会看到那些在之前的查询之中的一些残留情况:

test=# SELECT * FROM tab;
 id | y  
----+----
 10 | 10
(1 row)

自数据应用程序,乃至于中间件中,我们依旧在讨论一个简单的 SELECT 查询语句。请问这其中的问题究竟在哪里?好的,正如你所见:许多的事情会趋向于错误的结果,因为我们发送给数据库后端交付执行的字符串中,并没有涵盖所有中间件做出决策所需要的有关信息(即我们在前文中所讨论的读与写)。

将读操作发送给二级节点,并把写操作发送给主节点?

现在,请让我们将目光聚焦于我们这里希望解决的问题:我们希望使用我们的读后备节点(这个节点我们已经经由流复制进行连接)来做负载均衡的工作。为了实现这项工作,我们就必须要指导,什么是读,什么是写。但是谁会有这些信息呢?只有一个组件了解这方面情况:数据应用程序。为了实现负载均衡,应用程序就必须清楚数据往哪里去。

请查看下面的案例:


test=# START TRANSACTION;
START TRANSACTION
test=*# SELECT 1;
 ?column? 
----------
        1
(1 row)
 
test=*# SELECT 2;
 ?column? 
----------
        2
(1 row)
 
test=*# INSERT ...  

我们开启了一个假定我们仅仅进行读操作的事务。不过,在事务执行期间,其结果(应用程序逻辑视角看)我们实际上是有着写入一些数据的需要的。问题就在于:我们应该对这个打开的事务做什么?我们有可能在尽可能快的时间内,将其转移到主事务节点吗?

  • 摧毁了事务的独立性
  • 失去了连接的诸多设置
  • 销毁了打开的游标(cursor)
  • 种种其它

这甚至没有其它的途径:如果你需要实现负载均衡,你至少需要两条数据库的连接。你的数据应用程序必须清楚,什么是读,什么是写。这是一个无法回避的问题。

img

读写均衡与流复制

在这里,我们必须强调另外的一件事情:假定我们的数据应用程序足够聪明。它非常清楚什么时候展开数据写入工作,同时什么时候展开数据读取工作。那么我们最终就可以简单地自远程的备节点读取数据并且展开负载均衡工作吗?在 PostgreSQL 中,存在着一种名为 “同步流复制” 的机制,绝大多数的人们假定同步流复制(synchronous_commit = on)选项打开的时候,立即查看在复制节点中的数据就已经足够了。但这种观点并不契合实际。那么,让我们对我们所作的事情,做一个简单的回顾,如何?在默认的情况下麦年,同步流的复制意味着,数据已经被写入到至少两台机器上面(等价于“写入到了 wal 日志中”)。对于确保数据在复制节点的可见性(= SELECT)而言,这种做法并不足够。为什么这种情况会发生呢?这就涉及到 “复制冲突”(replication conflict),请阅读下面的两篇文章,用以了解这方面的情况:

这两篇文章的核心观点在于,读数据的操作可以组织 wal 经由复制节点生效(它同样在我们的博客中被描述)。实际在于,wal 已经在复制节点里面,被写入到磁盘之中,因此它并不能够确保你可以看到数据已经被改变,除非 synchronous_commit 被调整为 "remote_write",这当然包含了一些超出本文讨论范围的影响。

负载均衡:结论

我们的结论如下:如何你的目标在于负载均衡,请停止编写有关的那些试图找出如何实现负载均衡或者用什么去实现负载均衡的中间件。这种做法可能会适得其反,除非你已经完全清白地意识到你正在做什么,同时你可以清晰的意识到什么将会破坏整体的工作,而什么不会。在事务独立性是一个问题的场景下面,那么提出你自己的负载均衡的解决方案的尝试,极大可能将会失败。我们将这发布出来作为一个警告,因为我们最近已经看到太多的人在这个事情上面失败,所以我们计划编写这篇短小的文章,用以尽可能地阻止更多类似于这种情况的灾难发生。