原文作者:
原文地址:
https://dzone.com/articles/hooks-the-secret-feature-powering-the-postgres-ecosystem
Postgres Hooks 是自定义 Postgres 工作方式的强大方法。从流行的项目中获取示例,我们探索如何使用它们进行构建。
开发人员说 Postgres 是“可扩展的”是什么意思?他们指的是可以改变数据库核心功能的低级 API。在这篇文章中,我们将探索一个秘密的 - 意思是未记录的- 称为钩子的功能,它允许开发人员不仅可以向 Postgres 添加功能,还可以修改执行查询和解析数据的方式。
Postgres 生态系统中一些最受欢迎的项目使用 Hooks,包括 Timescale、pg_stat_statements 和 Supabase。为了让您了解可以构建的内容,我们将了解每个项目如何使用特定的钩子。在这篇文章的最后,我们提供了一个 Makefile、一些 C 代码和编译说明,以便您开始自定义 Postgres。
什么是钩子?
钩子是 Postgres 中基于事件的函数。我们可以基于事件/触发器创建一个钩子,数据库会调用它。Hooks 被许多库和项目广泛使用,但它们都具有相同的基本结构。
结构
Postgres 钩子首先包含 Postgres 标头并提供一些数据库引擎知道的标准包装器
C
include "postgres.h"
// Hooks Code
static void custom_hook()
// All hooks are called from this function
void _PG_init(void);
// Hooks logic end in this function
void _PG_fini(void);初始化
Hooks 需要放在contribPostgres 源代码路径文件夹中。您可以检查此处提供的源路径以了解 Postgres 已附带的扩展。
挂钩代码到位后,您可以从源代码构建它或单独发送附件。将钩子作为扩展超出了这篇博客文章的范围,但这就是这里介绍的所有 3 个项目如何使用它们。一旦插件到位,您就可以加载插件LOAD 'plugin_name'来测试它。
钩子的类型
可以在 C 编程语言中调用的 6 个类别中大约有 30 个钩子。Github 上的psql-hooks 项目是所有功能的非官方文档。
- General Hooks - 与一般 Postgres 功能一起使用的 Hooks。
- 安全挂钩 - 具有特定安全功能的挂钩,例如密码和用户创建。
- 函数挂钩 - 在函数执行期间工作。
- Planner Hooks - 在计划阶段拦截。例如,Timescale 使用
planner_hook来更改查询执行计划。 - Executor Hooks - 在执行阶段拦截。例如,pg_stat_statements 使用
ExecutorRun_hook来检查正在运行的查询,而 Supbase 使用ProcessUtility_hook来实现基于角色的安全性。 - PL/PG SQL Hooks - 使用 PL/PGSql 函数时很方便。
大多数文档都带有示例,因此让我们回顾一下 Timescale、pg_stat_statements 和 Supabase 是如何使用钩子的。为了获得更多乐趣,请阅读下面链接的 C 文件,从他们的开发人员那里获得关于如何使用这些低级 API 的明智建议。
时间刻度中的挂钩
TimescaleDB “通过在 PostgreSQL 的查询计划器、数据和存储模型以及执行引擎中添加挂钩”来扩展 Postgres 。这为我们提供了关于它们插入数据库架构的哪些部分的提示。特别是,Timescale 以块的形式处理时间序列数据,即存储一组连续的日期、时间、事件或其他时间序列数据的内存位。第二个核心特性是连续聚合,其中块随着新数据的进来而更新。最后,Timescale 有一些非常有趣的调度特性,同样在 Postgres 中使用钩子直接实现。现在我们知道存在哪些类型的钩子,我们可以检查 Timescale 的开源代码库,了解它们是如何用于创建上述功能的。
查询计划和执行引擎
Timescale 最重要的钩子可能是当用户或应用程序向其发送 SQL 语句时Postgres 生成planner_hook的查询计划。在我们看到他们如何使用该函数来修改典型的查询计划以包括块并确保正确查询 HyperTable。一旦计划者完成 Timescale 使用他们在 Postgres 操作一开始就初始化的查询对查询进行进一步的转换。src/planner.ctimescaledb_plannerpost_parse_analyze_hooksrc/loader/loader.c
同样,在 中,一旦生成查询计划src/process_utility.c,该钩子用于修改 Postgres 的执行引擎。ProcessUtility该文件包含超过 4,000 行 C 语言,可确保正确执行 ALTER TABLE 等语句,并利用 Timescale 独特的压缩功能实现最大效果。
家政
除了计划和执行之外,还有另外两个有趣的钩子可以帮助 Timescale 确保顺利更新并测试它们的扩展。shmem_startup_hook在初始化时触发,用于确保工作人员在更新期间拥有正确版本的扩展、调度正确发生以及其他任务。所有这些功能都离不开测试,因此emit_log_hook用于在src/bgw/log.c自动化测试工具中注入日志。
pg_stat_statements 中的钩子
Citus Data 称 pg_stat_statements是最有用的 postgres 扩展。它是Postgres 性能监控的最佳工具之一,可以轻松开启。pg_stat_statements 大量使用 、 the ExecutorStart_hook、ExecutorRun_hooktheExecutorFinish_hook和 the ,ExecutorEnd_hook因为它对有关查询运行时的信息感兴趣。这是一个示例pg_stat_statements.c,我们从中跟踪查询的总经过时间。
C
/*
* ExecutorStart hook: start up tracking if needed
*/
static void
pgss_ExecutorStart(QueryDesc *queryDesc, int eflags)
{
if (prev_ExecutorStart)
prev_ExecutorStart(queryDesc, eflags);
else
standard_ExecutorStart(queryDesc, eflags);
/*
* If query has queryId zero, don't track it. This prevents double
* counting of optimizable statements that are directly contained in
* utility statements.
*/
if (pgss_enabled(exec_nested_level) && queryDesc->plannedstmt->queryId != UINT64CONST(0))
{
/*
* Set up to track total elapsed time in ExecutorRun. Make sure the
* space is allocated in the per-query context so it will go away at
* ExecutorEnd.
*/
if (queryDesc->totaltime == NULL)
{
MemoryContext oldcxt;
oldcxt = MemoryContextSwitchTo(queryDesc->estate->es_query_cxt);
queryDesc->totaltime = InstrAlloc(1, INSTRUMENT_ALL, false);
MemoryContextSwitchTo(oldcxt);
}
}
}Supabase 中的钩子
我们可以详细介绍 Supabase(在 Postgres 中实现的开源 Firebase)如何使用钩子,但他们自己在一篇关于保护 Postgres 中的保留角色的优秀博客文章中这样做了。可以说他们只使用一个钩子,ProcessUtility_hook劫持(以一种很好的方式)在每个 SQL 语句运行之前命中的全局指针。
Supabase 使用 ProcessUtility 跳转到supautils.c,一个检查正在执行语句的角色是否被允许这样做的库。这有助于他们将 supbase.com 上基于 Web 的身份验证与 Postgres 中的数据库角色相匹配,并提供非常流畅的身份验证体验。除了阅读 pg_stat_statements C 代码之外,他们的博客文章supautils.c也是实现中等复杂挂钩的最佳参考。
一个完整的例子
让我们看一下在此处的 Github 存储库中找到的完整示例。注意:以下代码的许可证可在此处获得。
C
//Initialise hook variable
static ClientAuthentication_hook_type original_client_auth_hook = NULL;
//Hook method
static void auth_delay_checks(Port *port, int status)
{
if (original_client_auth_hook)
original_client_auth_hook(port, status);
if (status != STATUS_OK)
{
pg_usleep(1000000L);
}
}
//The custom hook method is called in the global init
void _PG_init(void)
{
original_client_auth_hook = ClientAuthentication_hook;
ClientAuthentication_hook = auth_delay_checks;
}
//Unlink once the work is done
void _PG_fini(void)
{
ClientAuthentication_hook = original_client_auth_hook;
}上面这段代码是钩子的基本演示。当身份验证错误时,它会在抛出错误之前引入 1 秒的睡眠。此代码可用作暴力破解密码的威慑(这只是一个示例,而不是推荐的安全功能/策略)。
Postgres 的其他基于事件的架构
还有其他架构可以从 Postgres 中收集事件。使用 Debezium 的 Apache Kafka 是一种流行的方法。Postgres 还具有事务提交日志,可以在服务器之间监视和复制以实现冗余或备份。
结论
Hooks 是一个非常先进的概念。一个典型的数据库用户永远不会在他们的日常工作中使用它,但是了解它们是如何工作的很有趣,因为你永远不知道它们什么时候会派上用场。此外,一些库已经在使用它们,因为它们需要挂钩各种指标。
您可以用来查看挂钩是否适合您的项目的粗略策略涉及考虑以下几点:
- 它是基于事件的用例吗?
- 它不能在应用程序端完成,只能在数据库端完成。那样可以么?
- 我们可以不使用 CDC(变更数据捕获)工具来解决这个问题吗?
- 你有关于
C编程语言的背景和关于 Postgres 内部的必要知识吗? - 您是否有能力将 Postgres 打包为自定义安装?云安装和服务(如 RDS/CloudSQL)将不具备此能力。
如果这些问题已经得到解答,那么钩子就是一个很好的用例。




