2026/4/9 12:03:07
网站建设
项目流程
oa办公系统怎么使用,重庆主城优化网站建设,国内网站是cn还是com,个人域名网站本文字数#xff1a;7186#xff1b;估计阅读时间#xff1a;18 分钟 作者#xff1a;David Wheeler 本文在公众号【ClickHouseInc】首发 原文标题#xff1a;深入理解 PostgreSQL GUC 的 “extra” 数据
在开发 pg_clickhouse 的过程中#xff0c;我实现了一个 Postgre…本文字数7186估计阅读时间18 分钟作者David Wheeler本文在公众号【ClickHouseInc】首发原文标题深入理解 PostgreSQL GUC 的 “extra” 数据在开发 pg_clickhouse 的过程中我实现了一个 PostgreSQL 设置也称为 “GUC”它可以接收一组键/值对并将这些设置作为会话参数附加到发送至 ClickHouse 的每一条查询中。在 v0.1.0 版本中https://github.com/ClickHouse/pg_clickhouse/releases/tag/v0.1.0这些设置以字符串形式保存并且会在每次查询时重新解析。为了降低这部分开销我们希望在设置 GUC 时就将其解析为键/值数据结构。本文将深入探讨这一优化在 v0.1.1 版本中的实现过程https://github.com/ClickHouse/pg_clickhouse/releases/tag/v0.1.1包括多次尝试的过程以及最终采用的解决方案。希望这些经验能对其他 PostgreSQL 扩展开发者有所帮助。如果你只是想使用 pg_clickhouse 从 PostgreSQL 查询 ClickHouse而不关心这些 C 代码和内部实现细节可以直接阅读相关教程。挑战我的目标是避免在每次查询时解析 pg_clickhouse pg_clickhouse.session_settings GUC 中的键/值对而是在赋值阶段就完成解析并将结果存入一个独立的变量中。实现这一点并不容易因为 GUC API 对 extra 数据的内存分配方式有着相当严格的要求。我经过多次尝试最终才找到一个既可行又正确的实现方案。下面是 pg_clickhouse.session_settings 的定义方式DefineCustomStringVariable( pg_clickhouse.session_settings, Sets the default ClickHouse session settings., NULL, ch_session_settings, join_use_nulls 1, group_by_use_nulls 1, final 1, PGC_USERSET, 0, chfdw_check_settings_guc, chfdw_settings_assign_hook, NULL );参数确实不少下面是其公共声明extern void DefineCustomStringVariable( const char *name, const char *short_desc, const char *long_desc, char **valueAddr, const char *bootValue, GucContext context, int flags, GucStringCheckHook check_hook, GucStringAssignHook assign_hook, GucShowHook show_hook ) pg_attribute_nonnull(1, 4);参数说明如下nameGUC 的名称通过 SET 命令使用。扩展定义的 GUC 通常需要带有前缀和点号因此这里使用的是 pg_clickhouse.session_settings。short_descGUC 的简要说明。long_descGUC 的详细说明。valueAddr指向用于存储 GUC 值的变量的指针。pg_clickhouse 的这个 GUC 使用的是一个全局的 char * 变量。bootValue扩展加载时该 GUC 的默认值。context用于决定谁可以在什么阶段设置该 GUC。这里我们希望允许所有用户设置 pg_clickhouse.session_settings不过该选项还支持多种不同的限制方式。flags用于控制行为、格式化方式以及解析逻辑的一组 GUC 标志位掩码。check_hook用于校验新值的回调函数同时也可以在此阶段设置需要传递给 assign hook 的 extra 数据。在我们的实现中该回调指向 chfdw_check_settings_guc如果新值无法解析为合法的键/值列表就会抛出错误。assign_hook用于将 GUC 值赋给 valueAddr 以外变量的回调函数它可以使用 check hook 中设置的 extra 数据。show_hook用于在显示 GUC 值时重新格式化输出的回调函数。一些 GUC 会利用它对值进行规范化处理例如时区设置。通过 check_hook 和 assign_hook 这两个参数我们得以完成所需的 extra 数据解析流程。整体思路是在 check hook 中将 extra 指向一个解析后的数据结构然后将该指针传递给 assign hook由后者负责将其赋值给相应的变量。第一次尝试在第一次尝试中我创建了一个指向键/值对列表的指针设想可以在 check hook 中让 extra 指向这个结构然后在 assign hook 中将其赋值。思路大致如下static bool chfdw_check_settings_guc(char **newval, void **extra, GucSource source) { if (*newval NULL || *newval[0] \0) return true; kv_list * settings parse_and_malloc_kv_list(*newval); if (!settings) return false; *extra settings; return true; }需要注意的是parse_and_malloc_kv_list() 使用 malloc() 为它构建的数据结构分配内存包括列表本身、列表中的每一项以及每个键和值字符串。之所以选择 malloc()是因为我们打算把结果存储在一个全局变量中因此它不能被 GUC 的内存上下文自动回收。接着在 assign hook 中static void chfdw_settings_assign_hook(const char *newval, void *extra) { if (ch_session_settings_list) kv_list_free(ch_session_settings_list); ch_session_settings_list (kv_list *) extra; }如果当前已经存在一个设置列表就先将其释放然后把 extra 中的新列表赋值过来。看起来似乎非常简单对吧但遗憾的是我始终没能让这个赋值逻辑正确运行这也暴露出我对内存分配、指针以及多级指针等问题理解得还不够深入。第二次尝试于是我尝试换一种思路直接在 check hook 中完成赋值彻底移除 assign hook。大致实现如下static bool chfdw_check_settings_guc(char **newval, void **extra, GucSource source) { if (*newval NULL || *newval[0] \0) return true; kv_list * pairs parse_and_malloc_kv_list(*newval); if (!pairs) return false; if (ch_session_settings_list) kv_list_free(ch_session_settings_list); ch_session_settings_list pairs; return true; }我在 Postgres Discord 上询问了这种做法Tom Lane 很快给出了回复你绝对不能在 check hook 中修改任何会话状态。check hook 被调用时我们只是用来推测某个操作例如一个拟议中的 ALTER DATABASE SET 命令是否可行并不能保证这个设置一定会被真正应用。与此同时我还发现 RESET 操作也无法正常工作因为在执行重置时并不会调用 check hook。这个方案也就宣告失败了。第三次尝试接下来为了绕开上述问题我改成在两个阶段都进行解析。此时check hook 被简化为static bool chfdw_check_settings_guc(char **newval, void **extra, GucSource source) { if (*newval NULL || *newval[0] \0) return true; kv_list * settings parse_and_malloc_kv_list(*newval); if (!settings) return false kv_list_free(ch_session_settings_list); /* No errors, return true. */ return true; }而 assign hook 则变成了static void chfdw_settings_assign_hook(const char *newval, void *extra) { if (ch_session_settings_list) kv_list_free(ch_session_settings_list); PG_TRY(); { ch_session_settings_list parse_and_malloc_kv_list(newval); } PG_CATCH(); { ereport(LOG, (errcode(ERRCODE_FDW_ERROR), errmsg(unexpected error parsing \%s\, newval))); } PG_END_TRY(); }这样一来parse_and_malloc_kv_list() 会在两个 hook 中各执行一次。考虑到这已经避免了在每条查询中重复解析设置这种双重解析在性能上是可以接受的。这个方案最终形成了 pg_clickhouse#95。不过我最终还是没有采用它一方面是因为它的内存管理逻辑比最终方案更加复杂另一方面则是因为 Tom Lane 在 Discord 上的一句随口评论让我更加犹豫我不推荐在 assign hook 中进行新的内存分配。通常认为 assign hook 应当是不会失败的。这让我意识到即便真正抛出一个逃逸出 PG_TRY() 的错误的概率很低这种潜在风险也足以让该方案显得不够理想。插曲Extra 的正确用法回到第一次尝试当时 parse_and_malloc_kv_list() 会对构建出的数据结构中的每一部分分别调用 malloc()kv_list一个指向键/值对列表的指针、列表中的每个键/值对以及键和值字符串本身。在 pg_clickhouse#95 的 kv_list.c 中这种内存分配模式仍然存在。但这种做法使得 extra 的使用几乎变得不可能——至少已经超出了我当时的 C 语言能力范围。随后Tom 向我讲解了 extra 的正确使用方式你应该使用 guc_malloc而且 extra 数据必须是一个单独的 malloc 内存块而不能是由多个独立分配组成的列表。对于 GUC 的 extra 数据有一个通用原则一旦 check_hook 将其返回后续在不再需要时就由 guc.c 负责释放它。之所以要求是单一内存块是因为 guc.c 并不了解该数据结构的其他组成部分。这种方式可以正确地使用 extra 数据在 check hook 中不进行任何其他赋值操作而在 assign hook 中也只负责赋值从而消除了出错的风险。剩下的唯一问题就是如何通过一次 guc_malloc() 来分配并构建一个包含键/值对列表的数据结构。第四次尝试在 pg_clickhouse#94 中提交的最终方案参考了 PostgreSQL 的 datetime.c 中 ConvertTimeZoneAbbrevs() 的实现思路先计算所有键/值对所需的总内存大小然后一次性分配一整块连续内存。对应的结构体定义如下typedef struct kv_list { int length; char data[]; } kv_list;这里使用了一个可变数组 data[]。它作为键和值的存储起点构造函数会从该地址开始依次写入以空字符结尾的字符串形式的键和值列表。严格来说它并不是一个传统意义上的 C 结构体。那么这块内存究竟是如何分配的呢构造函数会先像下面这样累加所有键和值字符串所需的内存大小kv_list * list guc_malloc(ERROR, offsetof(kv_list, data) summed_size);在计算时它会先取 kv_list 结构体中 data 字段之前的大小再加上刚才统计得到的键和值字符串所需的总内存。完成 kv_list 的分配之后代码会再次遍历所有键/值对并从 data 字段对应的位置开始将它们一个接一个顺序写入这块内存中。由于直接遍历这些键/值对并不直观最终的 kv_list API 还额外提供了一个迭代器结构用来简化遍历过程。for (kv_iter iter new_kv_iter(settings); !kv_iter_done(iter); kv_iter_next(iter)) { printf(%s %s\n, iter.name, iter.value); }这样一来在代码中真正需要处理这些设置的地方——也就是为 binary 和 http 查询准备参数时——就可以保持实现上的简洁清晰。在完成上述改造并确保所有数据都存放在一个通过 guc_malloc() 分配的内存块中之后pg_clickhouse#94 终于能够按照预期正确地使用 check hook 和 assign hook 了static bool chfdw_check_settings_guc(char **newval, void **extra, GucSource source) { if (*newval NULL || *newval[0] \0) return true; kv_list * settings parse_and_guc_malloc_kv_list(*newval); if (!settings) return false; *extra settings; return true; }这一实现与第一次尝试几乎完全一致唯一的区别在于parse_and_guc_malloc_kv_list() 使用 guc_malloc() 将所有设置一次性分配为一个连续的内存块并将其赋给 extra。这样一来assign hook 的实现就不再存在出错的隐患static void chfdw_settings_assign_hook(const char *newval, void *extra) { ch_session_settings_list (kv_list *) extra; }它不再负责释放 ch_session_settings_list 之前的值而是完全交由 GUC API 在合适的时机进行清理。还有更多可以学习的内容这个方案让我非常满意它最大程度地减少了设置值的内存开销把内存管理的责任交给了 GUC API提供了一个易于使用的设置遍历接口并且以正确的方式使用了 check hook 和 assign hook。除此之外我也在这个过程中深入理解了 GUC API以及如何在 C 语言中突破严格的结构体式内存分配限制。尽管如此我仍然保留了一份 pg_clickhouse#95 的本地副本等到自己对 C 语言中的指针、指向指针的指针甚至指向指针的指针的指针理解得更加透彻之后再回头研究。我觉得理论上或许存在一种方式可以让 extra 指向某块内存中的某个位置使得 GUC API 只移除这个指针而不是释放它所指向的内存。这样就可以在 check hook 中通过 malloc() 分配数据再在 assign hook 中完成赋值类似于下面这样的逻辑在 check hook 中kv_list * settings parse_and_malloc_kv_list(*newval); if (!settings) return false; /* Allocate just the memory needed to point to a kv_list. */ extra guc_malloc(ERROR, sizeof(kv_list *)); extra settings; return true;而 assign hook 则可能类似于ch_session_settings_list (kv_list *) *extra;不过我目前还没有找到实现这一点的正确“咒语”正如前面所说的那样。倒也不是说我真的会合并这样一种方案——它同样会让 RESET 无法正常工作因为 GUC 假定自己持有完整的 extra 值。但这个问题本身对我而言很有挑战性它促使我不断学习、掌握更高级的 C 编程技巧。也许有一天我还能学会如何更高效地“滥用”这些技巧。 征稿启示面向社区长期正文文章内容包括但不限于关于 ClickHouse 的技术研究、项目实践和创新做法等。建议行文风格干货输出图文并茂。质量合格的文章将会发布在本公众号优秀者也有机会推荐到 ClickHouse 官网。请将文章稿件的 WORD 版本发邮件至Tracy.Wangclickhouse.com