2026/4/18 10:21:27
网站建设
项目流程
佛山哪家网站建设比较好,网站建站公司排行,做网站的广告词,公司的公关本文字数#xff1a;7060#xff1b;估计阅读时间#xff1a;18 分钟 作者#xff1a;David Wheeler 本文在公众号【ClickHouseInc】首发 在开发 pg_clickhouse 的过程中#xff08;https://pgxn.org/dist/pg_clickhouse/#xff09;#xff0c;我设计了一个 PostgreSQL…本文字数7060估计阅读时间18 分钟作者David Wheeler本文在公众号【ClickHouseInc】首发在开发 pg_clickhouse 的过程中https://pgxn.org/dist/pg_clickhouse/我设计了一个 PostgreSQL 的配置项即 “GUC”https://github.com/postgres/postgres/blob/master/src/backend/utils/misc/README它接受一组键值对并在每条发送到 ClickHouse 的查询中将这些键值对作为会话设置自动附加。在 v0.1.0 版本中https://github.com/ClickHouse/pg_clickhouse/releases/tag/v0.1.0这些设置以字符串形式保存并在每次查询时重新解析。为了降低这种重复解析带来的性能开销我们希望能在设置 GUC 时就完成解析将其转为键值对结构进行存储。这篇文章深入记录了这一功能在 v0.1.1 中的实现过程包括曾尝试的方案与最终落地的实现细节希望对其他 PostgreSQL 扩展开发者有所帮助。如果你只是想尝试如何通过 pg_clickhouse 从 PostgreSQL 查询 ClickHouse不想被这些 C 语言和底层实现细节困扰可以直接参考教程。问题与挑战我们的目标是避免在每条查询中重复解析 pg_clickhouse.session_settings GUC 中的键值对内容而是在用户设置该 GUC 时预先完成解析并将结果赋值给一个单独的变量。但由于 GUC API 对于处理额外数据extra data时的内存分配方式有严格要求这一过程我尝试了几次才找到既可行又正确的做法。以下是 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 );参数很多以下是其公开定义https://github.com/postgres/postgres/blob/bfe5c4b/src/include/utils/guc.h#L393-L402extern 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_desc简要描述该 GUC。long_desc详细描述该 GUC。valueAddr用于存储值的变量指针。pg_clickhouse 的 GUC 使用一个全局的 char * 类型变量。bootValue扩展加载时的默认值。context定义了哪些用户、在何种场景下可以设置该 GUC。我们希望允许所有用户设置 pg_clickhouse.session_settings但还有其他控制选项。flags一组 GUC 行为标志位位掩码用于控制值的格式化和解析行为。check_hook值验证回调函数可在校验同时设置额外数据供 assign hook 使用。我们的实现中使用 chfdw_check_settings_guc如果新值无法解析为合法的键值对列表该函数会抛出错误。assign_hook将 GUC 的值赋给非 valueAddr 变量的回调函数可使用 check_hook 设置的额外数据。show_hook将 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 中修改任何会话状态。这个钩子函数调用的场景往往只是为了‘预判’一个设置操作例如 ALTER DATABASE SET是否有效并不能保证后续一定会应用这个值。我还发现在执行 RESET 命令时check hook 根本不会被触发。这意味着该方法连 RESET 都无法支持显然不能继续采用。第三次尝试于是我采取了“双重解析”的策略来规避上述问题在 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()。虽然是重复解析但相比于在每条查询中都重复解析一次 session_settings这种做法的代价还是可以接受的。这个方案后来被整理为 pg_clickhouse#95。不过我最终还是放弃了这一方案原因主要有两个一是它的内存管理逻辑比最终采用的方案更复杂二是 Tom Lane 在 Discord 上的随口一评让我产生了顾虑我不建议在 assign hook 中进行新的内存分配。这个钩子函数应当设计成不会失败。这让我意识到虽然在 assign hook 中因分配失败而触发异常的概率非常小但一旦真的发生会绕过 PG_TRY() 异常处理机制从而让这种方案的稳定性无法保证至少不是理想的选择。插曲正确使用 Extra 数据回顾我第一次的尝试当时 parse_and_malloc_kv_list() 会使用 malloc() 为构建的整个结构体中的各个部分分别分配内存包括整个键值对列表指针 kv_list、其中每一项以及每个 key 和 value 字符串本身。这种分配逻辑至今仍保留在 pg_clickhouse#95 的 kv_list.c 文件中。但正是这种方式使得我无法正确使用 extra —— 或者说这已经超出了我当时对 C 语言的掌握。Tom 及时指出了正确的做法你应该使用 guc_malloc而且 extra 数据必须作为一个单独的内存块进行分配而不能是多个分散的分配。GUC 的设计原则是一旦 check_hook 把 extra 数据传回后续就由 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[]它实际上是一个写入所有 key 和 value 字符串的内存起点。构造函数会按顺序把这些以 null 结尾的字符串写入该区域。这一结构其实并不是标准的 C 结构体。那么内存是怎么分配的呢构造函数会遍历所有键值对计算出各个字符串所需的总空间kv_list * list guc_malloc(ERROR, offsetof(kv_list, data) summed_size);在分配内存时会先算出 kv_list 结构体本身不包括 data的大小再加上 key 和 value 字符串的总和。完成分配后代码会再次遍历每个键值对将它们逐个写入从 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 和 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 系统自动在适当时间进行释放。还有很多值得学习的内容这个方案让我感到满意它有效地减少了内存使用将释放逻辑交由 GUC 处理为遍历设置项提供了简洁接口并正确地应用了 check 与 assign hook 的职责划分。而且我从中学到了很多关于 GUC API 的细节以及如何在 C 中绕开传统结构体分配限制实现灵活的内存控制。尽管如此我仍然保留了一份 pg_clickhouse#95 的本地副本作为继续学习 C 指针机制的练习素材 —— 尤其是关于指针、指针的指针甚至指针的指针的指针。我相信理论上应该可以让 extra 指向某块内存地址而 GUC 只移除这个指针而不释放它指向的内存。这样就可以在 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