暂无图片
暂无图片
2
暂无图片
暂无图片
暂无图片

聊聊Postgres 的HOOK 机制

1308

Image.png
大家好,今天和大家聊聊Postgres 的HOOK 机制以及Extension。

上一篇 《Postgres extension 的新春祝福(2024 版)》 https://www.modb.pro/db/1755155378081976320 我们用最简单的方式制作了自己的第一个extension :Happy2024!

今天我们站在方法论的角度来看看PG的extension 和 hook程序。

我们先来看看HOOK (钩子程序): 什么是钩子程序?

来自权威的维基百科:https://en.wikipedia.org/wiki/Hooking

Image.png
维基百科中提到了设计模式中的模板方法模式(template method design pattern): 这个设计模式对于国内基数众多的JAVA程序员是再熟悉不过了。

父类FrameworkClass定义好了方法的名称,方法的步骤以及顺序:
templatedMethod() including:
stepOne();
stepTwo();
stepThree();

子类applicationClaaOne和applicationClassTwo 分别实现了自己的stepTwo()方法的override。

Image.png

我们具体来看一下postgres 中hook的实现机制:

PG 通过全局指针函数(global pointer),来判断PG中预制的各种HOOK是否为空, 如果不为空,则指向用户自定义的HOOK函数(注册用户的自定义 hook)。

如果参数shared_preload_library 中包含多个插件,并且存在多个插件都修改同一个HOOK的情况,那么会把previous hook 保存记录下来, 如果 previous hook 不为空,

会在自己开发中的HOOK中调用previous hook, 从而避免了后面的HOOK覆盖掉之前HOOK的逻辑,类似于 HOOK 链的设计。

当PG启动的时候,会调用方法__PG__init()来加载shared library 的 *.so 文件:

当PG关系实例的时候, 会调用方法__PG___finit()来关闭掉shared library 中加载的插件。

PG 预制HOOK的种类:

General Hooks
Security Hooks
Function Manager Hooks
Planner Hooks
Executor Hooks
PL/pgsql Hooks

开发详情可以参考github: https://github.com/taminomara/psql-hooks/blob/master/Detailed.md#security-hooks

我们尝试用colin debug 一下PG 15.3 的源代码,观察一下hook加载的过程:

我们修改postgres.conf,中的参数shared_preload_libraries,启动的时候加载2个插件:pg_stat_statements和auto_explain

shared_preload_libraries='pg_stat_statements,auto_explain'

我们把断点打在源码: src/backend/postmaster/postmaster.c 的函数: process_shared_preload_libraries();

Image.png

我们debug模式启动实例: 启动程序指到 erc/backend/postgres 指定参数 -D 数据库目录

Image.png

点击debug按钮之后,可以看到程序来到了我们的断点:
Image.png

我们进入函数load_libaries 继续打断点:

Image.png

我们可以看到 libraries 是值 是我们的GUC 的shared_preload_libraries值:是一个字符串形式。

Image.png

接下来会把字符串按照逗号拆分放入 elemlist, 进行循环加载调用函数 load_file.

Image.png

我们进去核心函数load_file, 观察一下动态链接文件是如何加载的:

我们看到 internal_load_library 这个函数是实际的加载链接文件的入口:

Image.png

函数internal_load_library注解是:
加载指定的动态链接文件,返回指针pg_dl的文件句柄。
动态库名称和指定的动态文件命名成是完全一致的。
当前还没有动态卸载库文件的功能,我们可能会在未来会添加此功能这样可以方便我们安全的从hook函数的指针中卸载hook程序,清除GUC参数,或者解决当前一些不安全的隐患。

/* * Load the specified dynamic-link library file, unless it already is * loaded. Return the pg_dl* handle for the file. * * Note: libname is expected to be an exact name for the library file. * * NB: There is presently no way to unload a dynamically loaded file. We might * add one someday if we can convince ourselves we have safe protocols for un- * hooking from hook function pointers, releasing custom GUC variables, and * perhaps other things that are definitely unsafe currently. */ static void * internal_load_library(const char *libname)

扫描一下文件,查看是否已经加载过

/* * Scan the list of loaded FILES to see if the file has been loaded. */ for (file_scanner = file_list; file_scanner != NULL && strcmp(libname, file_scanner->filename) != 0; file_scanner = file_scanner->next)

调用底层 操作系统OS 函数 dlopen 打开动态链接文件

Image.png

通过dlsym 调用动态加载文件中的 PG_MODULE_MAGIC 判断插件程序的兼容性。
如果存在不兼容的情况,则会调用dlclose(file_scanner->handle); 卸载动态库

Image.png

验证完插件的兼容性之后,会调用插件中的初始化函数 _PG_init(void)

Image.png

在pg_stat_statements.c 文件中, 初始化函数 _PG_init(void) 完成了:

  1. EnableQueryId 设置SQL 的query ID
    2.DefineCustomIntVariable,DefineCustomEnumVariable,DefineCustomBoolVariable 设置用户自己定义的GUC变量
    3.install hook – 加载钩子函数
/* * Install hooks. */ prev_shmem_request_hook = shmem_request_hook; shmem_request_hook = pgss_shmem_request; prev_shmem_startup_hook = shmem_startup_hook; shmem_startup_hook = pgss_shmem_startup; prev_post_parse_analyze_hook = post_parse_analyze_hook; post_parse_analyze_hook = pgss_post_parse_analyze; prev_planner_hook = planner_hook; planner_hook = pgss_planner; prev_ExecutorStart = ExecutorStart_hook; ExecutorStart_hook = pgss_ExecutorStart; prev_ExecutorRun = ExecutorRun_hook; ExecutorRun_hook = pgss_ExecutorRun; prev_ExecutorFinish = ExecutorFinish_hook; ExecutorFinish_hook = pgss_ExecutorFinish; prev_ExecutorEnd = ExecutorEnd_hook; ExecutorEnd_hook = pgss_ExecutorEnd; prev_ProcessUtility = ProcessUtility_hook; ProcessUtility_hook = pgss_ProcessUtility;

我们可以看到在HOOK的实现上,如果预制的HOOK已经被加载,则是 优先执行之前的PREVIOUS hook.

这样才能保证插件彼此时间不存在互相覆盖的现象。

Image.png

整个加载extension 流程图如下:

Image.png

Have a fun 🙂 !

References:

https://github.com/taminomara/psql-hooks/blob/master/Detailed.md#security-hooks
https://en.wikipedia.org/wiki/Hooking

「喜欢这篇文章,您的关注和赞赏是给作者最好的鼓励」
关注作者
【版权声明】本文为墨天轮用户原创内容,转载时必须标注文章的来源(墨天轮),文章链接,文章作者等基本信息,否则作者和墨天轮有权追究责任。如果您发现墨天轮中有涉嫌抄袭或者侵权的内容,欢迎发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论