从Greenplum中独特的临时表实现说起
前言
这两天一直在和临时表切磋武艺,说得更准确点儿,是与 Greenplum 中的临时表打交道,PostgreSQL 中的临时表我们已经十分熟悉了,其优势在于不会记录 WAL,可以充当一些临时结果集等,但是坏处就是滥用会造成系统表膨胀,比如 pg_attribute,除此之外,还有哪些危害?Greenplum 中的临时表又有什么骨骼惊奇的地方?
临时表
首先看下 PostgreSQL 中的临时表,由于是临时表,因此只能会话内可见,其他会话是无法访问的,即使你手动指定模式名也不行。
postgres=# select * from pg_temp_3.t2;
ERROR: cannot access temporary tables of other sessions
另外,针对临时表的内存参数是 temp_buffers,此参数是按需分配,主打一个节俭,没有用到的时候仅消耗 64KB,一个 BUFFER 描述符的大小。
Sets the maximum amount of memory used for temporary buffers within each database session. These are session-local buffers used only for access to temporary tables.
值得注意的是,temp_buffers 一经设置之后,是无法像 work_mem 此类参数一样动态调整的,其原因也不难理解
postgres=# create temp table t1(id int);
CREATE TABLE
postgres=# insert into t1 values(generate_series(1,1000));
INSERT 0 1000
postgres=# show temp_buffers ;
temp_buffers
--------------
10MB
(1 row)postgres=# set temp_buffers to '32MB';
ERROR: invalid value for parameter "temp_buffers": 4096
DETAIL: "temp_buffers" cannot be changed after any temporary tables have been accessed in the session.
postgres=# set temp_buffers to '10MB';
SET
postgres=# set temp_buffers to '20MB';
ERROR: invalid value for parameter "temp_buffers": 2560
DETAIL: "temp_buffers" cannot be changed after any temporary tables have been accessed in the session.
其次,由于临时表在会话结束之后便消失了,因此,系统表内死元组就会导致膨胀。这个危害基本人人都能想到,其实还有一个更为深层次的性能问题,让我们先卖个关子,先看下浓眉大眼的 Greenplum 有什么不同。
Greenplum 中的临时表
千万不要认为 Greenplum 中的临时表和 PostgreSQL 没有区别,首先,Greenplum 中的临时表是存放在共享内存里的!没错,shared buffers。
以最新的 Greenplum 代码为例:
/* ---------------------------------------------------------------------
* DropRelFileNodesAllBuffers
*
* This function removes from the buffer pool all the pages of all
* forks of the specified relations. It's equivalent to calling
* DropRelFileNodeBuffers once per fork per relation with
* firstDelBlock = 0.
* --------------------------------------------------------------------
*/
void
DropRelFileNodesAllBuffers(RelFileNodeBackend *rnodes, int nnodes)
{
int i,
n = 0;
RelFileNode *nodes;
bool use_bsearch; if (nnodes == 0)
return;
nodes = palloc(sizeof(RelFileNode) * nnodes); /* non-local relations */
/* Temp tables use shared buffers in Greenplum */
/* If it's a local relation, it's localbuf.c's problem. */
for (i = 0; i < nnodes; i++)
{
#if 0
if (RelFileNodeBackendIsTemp(rnodes[i]))
{
if (rnodes[i].backend == MyBackendId)
DropRelFileNodeAllLocalBuffers(rnodes[i].node);
}
else
#endif
nodes[n++] = rnodes[i].node;
}
/*
* If there are no non-local relations, then we're done. Release the
* memory and return.
*/
if (n == 0)
{
pfree(nodes);
return;
}
注释很明显
Temp tables use shared buffers in Greenplum.
那么这样设计之后,不难想象,Greenplum 中的临时表,并不是真正意义上的临时表,其他会话是可以访问的。
mydb=# create temp table test(id int) distributed by(id);
CREATE TABLE
mydb=# insert into test values(1) ;
INSERT 0 1
mydb=# \d
List of relations
Schema | Name | Type | Owner | Storage
----------------+------+-------+---------+---------
pg_temp_781774 | test | table | mxadmin | heap
(1 row)mydb=# select pg_backend_pid();
pg_backend_pid
----------------
182745
(1 row)
新开一个会话
mydb=# select pg_backend_pid();
pg_backend_pid
----------------
233951
(1 row)mydb=# select * from pg_temp_781774.test;
id
----
1
(1 row)
DBA表示惊呆了!😳那么这样和普通表又有何异呢?网上搜了一圈,找到这样一个回答:
Temp tables in Greenplum use shared buffers (and not local buffers as upstream PostgreSQL). It's designed this way in Greenplum because Greenplum can create many processes for a session (called slices) to execute the query. And each of these processes belonging to the same session should have access to the temp table data. Hence, temp tables are logically accessible from other sessions as well as they are pretty much similar to regular tables.
Though important thing to note is, the life-cycle of temp table is still bonded by the session it's created in similar to PostgreSQL. Hence, when the session which created the table exits the temp table will be deleted along with it.
Greenplum 中的临时表使用共享缓冲区 (而不是上游 PostgreSQL 那样的本地缓冲区)。Greenplum 中是这样设计的,因为 Greenplum 可以为一个会话创建许多进程 (称为切片slice) 来执行查询。并且属于同一会话的每个进程都应该有权访问临时表数据。因此,临时表在逻辑上可以从其他会话访问,并且它们与常规表非常相似。尽管需要注意的重要一点是,临时表的生命周期仍然由它创建的会话绑定,类似于 PostgreSQL。因此,当创建该表的会话退出时,临时表将随之删除
这段注释已经比较清晰了,为了提高查询执行并行度和效率,Greenplum 把一个完整的分布式查询计划从下到上分成多个 Slice,每个 Slice 负责计划的一部分。划分 slice 的边界为 Motion,每遇到 Motion 则一刀将 Motion 切成发送方和接收方,得到两颗子树。每个 slice 由一个 QE 进程处理。
因此,Greenplum 基于共享内存实现的临时表也就不足为奇了。
潜在危害
既然知道了这个独特的实现之后,回想一下我之前写的文章👉🏻从一个罕见案例聊聊我对社区的看法,简而言之,就是 shared_buffers 参数的大小会直接影响到 DROP/TRUNCATE 的性能,因为代码目前需要遍历整个 shared_buffers,同理,对于临时表也是如此,直接受到 temp_buffers 的影响。
那么既然 Greenplum 是基于共享内存实现的 (代码中同样是遍历),那么遇到性能问题的时候,就千万不要去傻乎乎的调整 temp_buffers 了。不过,问了一下三水,也问了一些其他老鸟,Greenplum 中一般不咋调整 shared buffers,毕竟更多是面向 AP 场景。
For AO/CO tables, the performance gain will be even more pronounced. This is because unlike heap tables, the blocks read from disk are not buffered in shared buffers – so blocks saved directly translate to block reads saved from disk.
此处写了一段简单的 JAVA
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;public class PostgresSessionExample {
private static final String URL = "jdbc:postgresql://localhost:5433/postgres";
private static final String USER = "postgres";
private static final String PASSWORD = "123";
public static void main(String[] args) {
ExecutorService executor = Executors.newFixedThreadPool(50);
for (int i = 0; i < 50; i++) {
executor.submit(PostgresSessionExample::runDbOperations);
}
executor.shutdown();
}
private static void runDbOperations() {
try (Connection conn = DriverManager.getConnection(URL, USER, PASSWORD)) {
conn.setAutoCommit(false); // 关闭自动提交
try (Statement stmt = conn.createStatement()) {
for (int i = 1; i <= 30; i++) {
String tableName = "temp_table_" + i;
// 创建临时表
stmt.execute("CREATE TEMP TABLE " + tableName + " (id SERIAL PRIMARY KEY, data TEXT)");
// 插入数据
stmt.executeUpdate("INSERT INTO " + tableName + " (data) VALUES ('Sample data')");
// 清空表
stmt.execute("TRUNCATE TABLE " + tableName);
}
conn.commit();
} catch (SQLException e) {
conn.rollback();
e.printStackTrace();
}
} catch (SQLException e) {
e.printStackTrace();
}
}
}
不断建临时表,插入并清空,当并发上来了之后,就会看到大量的 LWLock——lock_manager,此现象是基于 PostgreSQL 12,和 Greenplum7 内核保持一致。
wait_event | wait_event_type | count
---------------------+-----------------+-------
| | 3
buffer_content | LWLock | 10
BgWriterMain | Activity | 1
wal_insert | LWLock | 4
WALInitWrite | IO | 1
AutoVacuumMain | Activity | 1
ClientRead | Client | 8
lock_manager | LWLock | 26
LogicalLauncherMain | Activity | 1
WalWriterMain | Activity | 1
(10 rows)postgres=# select count(*) from pg_locks ;
count
-------
13394
(1 row)
小结
其实不仅是 Greenplum7,想必其他基于 PostgreSQL 的分布式数据库,临时表实现也应该是基于共享内存的,因为涉及到多个节点,多个进程的协同,如果是进程内可见,那么就无法进行通信了。
这也再次证明,分布式数据库较单机要复杂太多,分布式不是银弹,选择了分布式,势必会阉割许多特性。
参考
https://github.com/greenplum-db/gpdb
https://stackoverflow.com/questions/70567865/scope-of-temporary-table-in-greenplum
https://www.postgresql.org/docs/current/runtime-config-resource.html
推荐阅读
Feel free to contact me
微信公众号:PostgreSQL学徒 Github:https://github.com/xiongcccc 微信:_xiongcc 知乎:xiongcc 墨天轮:https://www.modb.pro/u/39588