hook是一系列的回调函数,在插件中可以注册这些回调函数来为系统的各个处理环节插入逻辑代码用来实现想要的功能。
这样就可以在不修改内核代码的情况下,通过插件为内核增加功能。pg_stat_statements插件就通过这些hook函数获得sql执行及统计信息。
hook函数在实现上比较简单,它们是一系列的全局函数指针,数据库在适当环节会判断对应的hook是否被设置了,如果已经设置,则会调用这个函数。
以post_parse_analyze_hook为例,这个hook会在系统构造好查询树后调用,插件可以在系统对查询树做任何处理之前对查询树进行二次加工。
/** src/backend/parser/analyze.c */
/* 函数指针类型定义 */
typedef
void (*post_parse_analyze_hook_type) (ParseState *pstate,
Query *query);
/* 全局函数指针的定义,在插件中可以对它直接赋值 */
post_parse_analyze_hook_type post_parse_analyze_hook = NULL;
Query *parse_analyze(...)
{
Query *query;
/* ... */
/* 构造查询树 */
query = transformTopLevelStmt(...);
/* 调用hook */
if (post_parse_analyze_hook)
(*post_parse_analyze_hook) (...);
/* ... */
return query;
}
在系统加载插件后,会先调用_PG_init函数对插件进行初始化,在插件被卸载前会调用_PG_fini函数进行反初始化,
所以注册hook回调的方法是在插件中实现这个函数,在函数内部注册hook回调。 :
void _PG_init(void)
{
/* 注册hook(直接覆写全局函数指针) */
post_parse_analyze_hook = analyze_hook_impl;
}
/* hook实现函数 */
static void analyze_hook_impl(...)
{
/* hook逻辑 */
}
从上面看到postgreSQL的hook设计非常简单粗暴。但是这个在多插件的情况下是有问题的,
系统的hook函数指针只有一个,但是插件是有多个的,假如多个插件都需要注册这个hook怎么办?
:
/* 插件A(先初始化) */
post_parse_analyze_hook = analyze_hook_impl_a;
/* 插件B(后初始化) */
post_parse_analyze_hook = analyze_hook_impl_b;
/* 插件A的hook被覆盖了!!!!! */
postgreSQL把解决这个问题的责任留给了每一位的插件开发者,也就是说每个插件都要处理这个事情。
方法是把初始化前的hook指针保存起来,在hook调用后,hook内部也调用以下保存好的原始hook函数
/* 声明一个指针,用来保存别人的hook函数 */
static
post_parse_analyze_hook_type prev_post_parse_analyze_hook = NULL;
void _PG_init(void)
{
/* 把原先的hook函数保存起来 */
prev_post_parse_analyze_hook = post_parse_analyze_hook;
/* 注册自己的hook函数 */
post_parse_analyze_hook = analyze_hook_impl;
}
/* hook实现函数 */
static void analyze_hook_impl(...)
{
/* 调用一下别人的hook函数 */
if (prev_post_parse_analyze_hook)
(*prev_post_parse_analyze_hook) (...);
/* 自己的hook逻辑 */
}
void _PG_fini(void)
{
/* 自己的模块被卸载后,要恢复下现场 */
post_parse_analyze_hook = prev_post_parse_analyze_hook;
}
还有一种和前面略有不同的hook函数,例如计划器中的planner_hook,在内核中是这么使用的:
:
/* src/backend/optimizer/plan/planner.c */
PlannedStmt *planner(Query *query,...)
{
PlannedStmt *result;
if (planner_hook)
result = (*planner_hook) (query, ...);
else
result = standard_planner(query, ...);
return result;
}
这里要注意的是,内核计划器的入口函数是planner,而实现代码却在standard_planner函数中,但问题在于,
只要注册了planner_hook函数,就不会再调用standard_planner函数了。
那standard_planner还要不要调用呢?
当然要调的,除非在插件中实现了一个完整的计划器。postgreSQL把这个调用的责任也留给了插件开发者,
在hook函数内部不能忘记调用standard_planner。 :
PlannedStmt *planner_hook_impl(Query *query, ...)
{
PlannedStmt *result;
/* 自己的hook逻辑 */
/* 调用一下内核的计划器 */
result = standard_planner(query,...);
/* 自己的hook逻辑 */
return result;
}
这种hook设计有个好处,就是在一个hook函数中可以同时对计划器执行前,和执行后两个时间点进行处理。
换句话说,也就是插件的开发者可以自己决定是要在计划器调用前增加逻辑,还是计划器调用后增加逻辑,或者前后都加。
最后,结合前面处理多插件兼容的解决方法,这类hook完整的实现是这样的:
/* 声明一个指针,用来保存别人的hook函数 */
static
planner_hook_type prev_planner_hook = NULL;
void _PG_init(void)
{
/* 把原先的hook函数保存起来 */
prev_planner_hook = planner_hook;
/* 注册自己的hook函数 */
planner_hook = planner_hook_impl;
}
/* hook实现函数 */
static PlannedStmt * planner_hook_impl(...)
{
PlannedStmt *result;
/* 自己的hook逻辑 */
if (prev_planner_hook)
/* 调用一下别人的hook函数
* 这个时候调用standard_planner的重任就落在别的插件上了
*/
result = (*prev_planner_hook) (...);
else
/* 没有别的插件了,我们自己调用standard_planner */
result = standard_planner(...);
/* 自己的hook逻辑 */
}
void _PG_fini(void)
{
/* 自己的模块被卸载后,要恢复下现场 */
planner_hook = prev_planner_hook;
}
从上面分析可以看到,postgreSQL提供hook功能机制比较简单,把许多责任留给了hook使用者,
这会给postgreSQL的插件带来的兼容风险,系统加载多个插件后,只要有一个插件没按这个规矩来,就可能会相互影响。
我们再来看下planner_hook的实现 :
/* hook实现函数 */
static PlannedStmt * planner_hook_impl(...)
{
PlannedStmt *result;
/* 位置1 */
if (prev_planner_hook)
result = (*prev_planner_hook) (...);
else
result = standard_planner(...);
/* 位置2 */
}
注意看上面的 位置1 和 位置2
,这两个地方的代码在运行时间上有着微妙的差异,
如果多个插件有依赖的情况下,这两个位置的代码不能随意调换,例如三个插件都注册了这个hook,则其执行顺序是这样的:
插件的排序和创建插件的顺序是一致的,shared_preload_libraries的插件和其配置顺序一致,
后创建的插件在上图中是排在前面,也就意味着后创建插件的 位置1
代码会在前面所有插件调用前就先调用, 而 位置2
的代码会在前面所有插件执行完成后调用。
从上面分析看来,这个hook本质上和java中常见的拦截器或者切面编程的模式是一致的。
下面看一个实际应用的例子,PostgreSQL的queryid是在pg_stat_statements插件中的post_parse_analyze_hook中计算的,
如果我们想要自己的插件想尽快拿到queryid,那么依据上面的分析,我们需要确保以下两点:
- 确保在pg_stat_statements之后创建(如果需在shared_preload_libraries中配置,则必须配置在pg_stat_statements后面)
- 注册post_parse_analyze_hook然后在 位置2 中获取queryid
static void
post_parse_analyze_hook_impl(ParseState *pstate, Query *query)
{
/*
* 位置1:
* 这里pg_stat_statements插件还没调用,
* queryid还没有计算出来
*/
if (prev_post_parse_analyze_hook)
prev_post_parse_analyze_hook(pstate, query);
/*
* 位置2:
* 这里pg_stat_statements插件已经调用过了,
* 可以拿到queryid
*/
}