开发者常见问题解答
参与
如何参与 PostgreSQL 开发?
下载代码并四处看看。参见 下载源代码树。
订阅并阅读 pgsql-hackers 邮件列表(通常称为“黑客”)。这是项目主要贡献者和核心成员讨论开发的地方。
如何下载/更新当前源代码树?
有几种方法可以获取源代码树。偶尔的开发者可以从 ftp://ftp.postgresql.org/pub/snapshot/ 获取最新的源代码树快照。
经常的开发者可能想要利用我们源代码管理系统的匿名访问权限。源代码树目前托管在 git 中。有关如何从 git 获取源代码的详细信息,请参见 文档 和 使用 Git。
开发代码需要什么开发环境?
PostgreSQL 主要使用 C 编程语言开发。源代码针对大多数流行的 Unix 平台和 Windows 环境(XP、Windows 2000 及更高版本)。
大多数开发者运行类似 Unix 的操作系统,并使用 GCC、GNU Make、GDB、Autoconf 等开源工具链。如果您以前贡献过开源软件,您可能熟悉这些工具。在 Windows 上使用此工具链的开发者使用 MinGW,尽管目前大多数 Windows 上的开发都是使用 Microsoft Visual Studio 2005(版本 8)开发环境和相关工具进行的。
在 安装说明 中可以找到构建 PostgreSQL 所需软件的完整列表。
经常重建源代码的开发者通常将 --enable-depend 标志传递给 configure。结果是,如果您对 C 头文件进行修改,所有依赖于该文件的代码也会被重建。
src/Makefile.custom 可用于设置环境变量,如 CUSTOM_COPT,这些变量用于每次编译。
如何参与 PostgreSQL 网站开发?
PostgreSQL 网站开发在 pgsql-www 邮件列表 上进行讨论,并由 基础设施团队 组织。postgresql.org 网站的源代码存储在一个 Git 仓库 中。
开发工具和帮助
源代码是如何组织的?
如果您将浏览器指向 后端流程图,您将看到几段描述数据流、后端组件流程图以及共享内存区域的描述。您可以点击任何流程图框以查看说明。然后,如果您点击目录名称,您将被带到源目录,以浏览其背后的实际源代码。我们还在某些源代码目录中提供了一些 README 文件来描述模块的功能。当您进入目录时,浏览器也会显示这些文件。
有哪些信息可以用来学习 PostgreSQL 内部原理?
- PostgreSQL 内部原理概述 https://postgresql.ac.cn/docs/devel/static/overview.html
- 编码 https://postgresql.ac.cn/developer/coding/
- 黑客攻击 PostgreSQL 入门 - 附带大量代码审查! https://www.cse.iitb.ac.in/infolab/Data/Courses/CS631/PostgreSQL-Resources/hacking_intro.pdf
- 黑客攻击 PostgreSQL 入门 http://www.neilconway.org/talks/hacking/
- Postgres 内部原理演示文稿 http://momjian.us/main/presentations/internals.html
- PostgreSQL 的内部原理 http://www.interdb.jp/pg/
- PostgreSQL 源代码分析(日语) http://ikubo.x0.com/PostgreSQL/pg_source.htm
- PostgreSQL 备忘录(日语) http://www.nminoru.jp/~nminoru/postgresql/
有哪些工具可以用来学习/检查 PostgreSQL 磁盘格式?
- contrib/pageinspect
- pg_hexedit - 十六进制编辑器工具包
- pg-internals-explorer - 用于探索磁盘格式的 ncurses 界面(未维护)
开发人员可以使用哪些工具?
首先,src/tools 目录中的所有文件都是为开发人员设计的。
RELEASE_CHANGES changes we have to make for each release ccsym find standard defines made by your compiler copyright fixes copyright notices
entab converts spaces to tabs, used by pgindent find_static finds functions that could be made static find_typedef finds typedefs in the source code find_badmacros finds macros that use braces incorrectly fsync a script to provide information about the cost of cache syncing system calls make_ctags make vi 'tags' file in each directory make_diff make *.orig and diffs of source make_etags make emacs 'etags' files make_keywords make comparison of our keywords and SQL'92 make_mkid make mkid ID files git_changelog used to generate a list of changes for each release pginclude scripts for adding/removing include files pgindent indents source files pgtest a semi-automated build system thread a thread testing script
在 src/include/catalog 中
unused_oids a script that finds unused OIDs for use in system catalogs duplicate_oids finds duplicate OIDs in system catalog definitions
tools/backend 已在上面的问答中进行了描述。
其次,你确实应该使用一个可以处理标签的编辑器,这样你就可以标记一个函数调用以查看函数定义,然后在该函数内部进行标记以查看更低级别的函数,然后退出两次以返回到原始函数。大多数编辑器通过标签或 etags 文件支持此功能。
第三,你需要从 ftp://ftp.gnu.org/gnu/idutils/ 获取 id-utils。
通过运行 tools/make_mkid,可以创建源符号的存档,可以快速查询。
一些开发人员使用 cscope,可以在 http://cscope.sf.net/ 找到。其他人使用 glimpse。
tools/make_diff 包含用于创建可以应用于发行版的补丁差异文件的工具。这会生成更易于阅读的差异。
pgindent 用于修复源代码样式以符合我们的标准,通常在每个开发周期结束时运行;有关我们样式的更多信息,请参见 此问题。
pginclude 包含用于将必要的 #include 添加到包含文件以及删除不必要的 #include 的脚本。
添加内置对象(例如类型或函数)时,需要为其分配 OID。我们的约定是,所有手动分配的 OID 都是 1-9999 范围内的不同值。(从机制上讲,它们在单个系统目录中是唯一的,但为了清晰起见,我们要求它们在整个系统中是唯一的。)src/include/catalog 中有一个名为 unused_oids 的脚本,它显示当前未使用的 OID。要分配新的 OID,请根据 unused_oids 选择一个空闲的 OID。该脚本将向你推荐一个范围,如下所示
Patches should use a more-or-less consecutive range of OIDs. Best practice is to start with a random choice in the range 8000-9999. Suggested random unused OID: 9209 (46 consecutive OID(s) available starting here)
通常最好接受它的建议。另请参见 duplicate_oids 脚本,它会在你犯错时发出警告。
PostgreSQL 源代码使用什么格式样式?
我们的标准格式是 BSD 样式,每级代码缩进一个制表符,每个制表符为四列。你需要将你的编辑器或文件查看器设置为将制表符显示为四个空格。
最新源代码的 src/tools/editors 目录 包含可用于 emacs、vim 和兼容编辑器的示例设置,以帮助保持 PostgreSQL 编码标准。
Vim 用户还将在文章 为 Postgres 开发配置 vim 中找到使用 vim 进行 postgres 开发的提示
对于 less 或 more,请指定 -x4
以获得正确的缩进。
我们使用pgindent美化程序来格式化代码以提高布局一致性。pgindent 至少在每个开发周期中都会对所有源文件运行一次,许多开发人员在提交或提交补丁之前都会运行修改过的代码通过 pgindent。请参见 src/tools/pgindent 源代码目录。
pgindent 的一项功能是重新排列注释中的文本。虽然这通常很有用,但它可能会破坏精心制作的手动布局。需要特定换行的注释块应写为块注释,其中注释以 /*------ 开头。此类注释不会以任何方式重新格式化。从第 1 列开始的注释块(例如文件或函数标题注释)也不会重新排列。
另请参见文档中的 格式部分。此主题 讨论了我们对变量和函数名的命名。
如果你想知道我们为什么要这样做,这篇文章 描述了一致的编码风格的价值。
是否有系统目录的图表可用?
是的,我们至少有一个 v8.3 的图表 (SVG 版本),以及 几个 v10 的图表。
哪些书籍适合开发人员?
有五本好书
- 数据库系统导论,作者 C.J. Date,Addison,Wesley
- SQL 标准指南,作者 C.J. Date 等,Addison,Wesley
- 数据库系统基础,作者 Elmasri 和 Navathe
- 事务处理,作者 Jim Gray 和 Andreas Reuter,Morgan Kaufmann
- 事务信息系统,作者 Gerhard Weikum 和 Gottfried Vossen,Morgan Kaufmann
configure 是什么?
configure 和 configure.in 文件是 GNU autoconf 包的一部分。Configure 允许我们测试操作系统的各种功能,并设置可以在 C 程序和 Makefile 中测试的变量。Autoconf 安装在 PostgreSQL 主服务器上。要将选项添加到 configure,请编辑 configure.in,然后运行 autoconf 生成 configure。
当用户运行 configure 时,它会测试各种操作系统功能,将它们存储在 config.status 和 config.cache 中,并修改 *.in 文件列表。例如,如果存在 Makefile.in,configure 会生成一个 Makefile,其中包含 configure 找到的所有 @var@ 参数的替换。
当你需要编辑文件时,请确保不要浪费时间修改 configure 生成的文件。编辑 *.in 文件,然后重新运行 configure 以重新创建所需文件。如果你从顶层源代码目录运行 make distclean,configure 生成的所有文件都会被删除,因此你只看到源代码分发中包含的文件。
如何添加新的端口?
需要修改许多地方才能添加新的端口。首先,从 src/template 目录开始。为你的操作系统添加适当的条目。另外,使用 src/config.guess 将你的操作系统添加到 src/template/.similar。你不应该完全匹配操作系统版本。configure 测试将查找确切的操作系统版本号,如果找不到,则查找没有版本号的匹配项。编辑 src/configure.in 以添加你的新操作系统。(参见上面的 configure 项目。)你需要运行 autoconf 或者修补 src/configure。
然后,检查 src/include/port 并添加你的新操作系统文件,其中包含适当的值。希望 src/include/storage/s_lock.h 中已经存在针对你 CPU 的锁定代码。还有一个 src/makefiles 目录用于特定于端口的 Makefile 处理。如果你需要针对你的操作系统的特殊文件,还有一个 backend/port 目录。
为什么不使用原始设备、异步 I/O、<插入你喜欢的炫酷功能>?
总有一种诱惑,想要尽快使用最新的操作系统功能。我们抵制这种诱惑。
首先,我们支持 15 个以上的操作系统,因此任何新功能都必须经过充分验证,我们才会考虑使用它。其次,大多数新的炫酷功能并不能提供显著的改进。第三,它们通常有一些缺点,例如可靠性下降或需要额外的代码。因此,我们不会急于使用新功能,而是等待功能稳定下来,然后要求进行测试以证明可以实现可衡量的改进。
例如,线程尚未用于代替后端的多个进程,因为
- 从历史上看,线程支持不完善且存在错误。
- 一个后端中的错误可能会破坏其他后端(如果它们是单个进程中的线程)
- 使用线程进行的加速与剩余的后端启动时间相比微不足道。
- 后端代码会更加复杂。
- 终止后端进程允许操作系统干净利落地快速释放所有资源,防止内存和文件描述符泄漏,并使后端关闭更便宜、更快
- 调试线程程序比调试工作进程要困难得多,而核心转储的用处也小得多
- 共享只读可执行映射和使用 shared_buffers 意味着进程(像线程一样)非常节省内存
- 进程的定期创建和销毁有助于防止内存碎片,而内存碎片在长时间运行的进程中可能难以管理
(单个后端进程是否应该使用多个线程来利用多个核心执行单个查询是一个单独的问题,这里不予讨论)。
因此,我们并不是对新功能一无所知。只是我们对采用新功能持谨慎态度。TODO 列表中通常包含指向讨论的链接,这些讨论展示了我们在这些领域中的推理。
即使是一些现代平台也存在与广泛使用的功能相关的令人惊讶的问题。例如,Linux 的 AIO 层不提供可靠的异步方式来执行 fsync() 并获得完成通知。
如何管理分支?
有关如何处理分支和回滚的信息,请参见 使用回滚分支 和 使用 Git 提交。
我在哪里可以获得 SQL 标准的副本?
你应该从 ISO 或 ISO 成员(如 ANSI)处购买它们。搜索 ISO/ANSI 9075。ANSI 的报价更便宜,但这两个组织提供的文档内容相同。
由于购买标准的官方副本非常昂贵,因此大多数开发人员依靠互联网上提供的各种草案版本。其中一些是
- SQL:2003 http://www.wiscorp.com/sql_2003_standard.zip
- SQL:2011(初步)http://www.wiscorp.com/sql20nn.zip
- 似乎不存在 SQL:2016 的免费副本。(如果你知道的话,请在这里添加链接。)
PostgreSQL 文档包含有关 PostgreSQL 和 SQL 兼容性 的信息。
关于 SQL 标准的一些其他网页是
请注意,要成为 PostgreSQL 开发的有用贡献者,不需要访问 SQL 标准副本。解释标准很困难,需要多年的经验。由于标准对索引等许多有用功能保持沉默,因此在标准范围之外有很多开发活动。
另请参阅 SQL 标准,以获取有关获取标准和参与其开发的更多信息。
PostgreSQL 中是否存在已知的 SQL 标准偏差?
当然。我们把它们列在 这里。
在哪里可以获得技术支持?
许多新接触代码的人所持有的技术问题已在 pgsql-hackers 邮件列表中得到解答,该列表的档案可在 http://archives.postgresql.org/pgsql-hackers/ 找到。
如果您找不到有关特定问题的讨论或信息,请随时在列表中提出问题。
主要贡献者还会在 IRC 的 #postgresql 频道(irc.freenode.net)中回答技术问题,包括有关新功能开发的问题。
开发流程
选择要处理的项目后该怎么办?
向 pgsql-hackers 发送一封电子邮件,提出您想做的事情的建议(假设您的贡献不琐碎)。不建议孤立地工作,因为经验表明,通常存在一些不明显的需求,如果事先没有达成一致,就会导致浪费时间和精力。在邮件中,讨论您计划使用的内部实现方法以及任何对用户可见的更改(新语法等)。对于复杂的补丁,在开始工作之前获得社区反馈非常重要。如果不这样做,您的补丁可能会被拒绝。如果您的工作由公司赞助,请阅读 本文,获取有关提高效率的技巧。
我们维护待审核补丁队列,通过一个定制的 CommitFest 网页应用,地址是 https://commitfest.postgresql.org。
如何测试我的更改?
基本系统测试
测试代码最简单的方法是确保它可以针对最新版本的代码进行构建,并且不会生成编译器警告。
建议您将 --enable-cassert 传递给 configure。这将开启源代码中的断言,这通常会使错误更容易发现,因为它们会导致数据损坏或段错误。这通常使调试更容易。
然后,通过 psql 进行运行时测试。
运行时环境
为了测试您修改后的 PostgreSQL 版本,最好将 PostgreSQL 安装到本地目录(例如您的主目录)中,以避免与系统范围内的安装冲突。使用 --prefix= 选项配置以指定安装位置;--with-pgport 指定非标准默认端口也很有用。要运行此实例,您需要确保使用正确的二进制文件;根据您的操作系统,需要设置 PATH 和 LD_LIBRARY_PATH(在大多数 Linux/类 Unix 系统上)等环境变量。设置 PGDATA 也很有用。
为了避免手动设置此环境,您可能想使用 Greg Smith 的 peg 脚本,或 脚本,这些脚本在构建场中使用。
回归测试套件
下一步是针对现有回归测试套件测试您的更改。为此,在源代码树的根目录中执行“make check”。如果任何测试失败,请调查原因。
回归测试和控制程序位于src/test/regress.
控制程序是pg_regress,但您通常通过 make 而不是直接运行它。
您可能发现使用PG_REGRESS_DIFF_OPTS=-ud make check获取统一的差异,而不是默认的上下文差异,这将pg_regress产生。
如果您有意更改了现有行为,此更改可能会导致回归测试失败,但不会出现任何实际回归。如果是这样,您也应该修补回归测试套件。
要更改 PostgreSQL 为特定回归测试执行运行的选项,可以使用PGOPTIONS环境变量,例如
PGOPTIONS="-c log_error_verbosity=verbose -c log_min_messages=debug2" make check
隔离测试
对于并发问题,PostgreSQL 在src/test/isolation中包含一个“隔离测试器”。此工具支持多个连接,如果您试图重现与并发相关的错误或测试新功能,它将很有用。
Valgrind
要使用 Valgrind,请编辑src/include/pg_config_manual.h设置#define USE_VALGRIND,然后在 Valgrind 下运行 postmaster,并使用提供的抑制。
参见 Valgrind。
其他运行时测试
一些开发人员使用 perf(来自 Linux 内核)、gprof(与 GNU binutils 套件一起提供)、ftrace、dtrace 和 oprofile (http://oprofile.sourceforge.net/) 等工具进行分析和其他相关工具。
单元测试、静态分析、模型检查…怎么样?
已经围绕其他测试框架进行了很多讨论,一些开发人员正在探索这些想法。
请记住,Makefile 没有为包含文件提供适当的依赖关系。您必须执行 make clean,然后执行另一个 make。如果您使用的是 GCC,则可以使用 configure 的 --enable-depend 选项让编译器自动计算依赖关系。
我已经开发了一个补丁,接下来该怎么办?
您需要将补丁提交到 [email protected]。为了帮助确保您的补丁得到及时审查和提交,请尝试遵循 提交补丁 中的指南。
提交补丁后会发生什么?
它将由项目的其他贡献者进行审查,并将被接受或退回以供进一步处理。该流程在 提交补丁 中有更详细的说明。
我如何帮助审查补丁?
如果您想通过审查 CommitFest 队列中的补丁来做出贡献,我们非常欢迎您这样做。请阅读 审查补丁 中的指南以获取更多信息。
我需要签署版权转让协议吗?
不需要,贡献者保留他们的版权(正如大多数欧洲国家的情况一样)。他们只是把自己看作是 Postgres 全球开发组的一部分。(实际上不可能将版权转让给 PGDG,因为它不是一个法律实体)。这与 Linux 内核和许多其他开源项目的运作方式相同。
我可以适当地添加我自己的版权声明吗?
不,请不要。我们希望法律信息简明扼要。此外,我们听说这可能会给企业用户带来问题。
PostgreSQL 许可证本身是否要求保持版权声明完整?
是的,确实如此。它确实如此,因为 PostgreSQL 全球开发组涵盖了所有版权持有者。另外请注意,美国法律不要求任何版权声明即可获得版权,就像大多数欧洲法律一样。
技术问题
如何从后端代码有效地访问系统目录中的信息?
首先,您需要找到感兴趣的元组(行)。有两种方法。第一种是使用 SearchSysCache() 和相关函数,通过使用目录上的预定义索引查询系统目录。这是访问系统表的首选方法,因为对缓存的第一次调用会加载所需的行,并且以后的请求可以返回结果而无需访问基本表。可在 src/backend/utils/cache/syscache.c 中找到可用缓存的列表。src/backend/utils/cache/lsyscache.c 包含许多特定于列的缓存查找函数。
返回的行是堆行的缓存所有版本。因此,您不能修改或删除 SearchSysCache() 返回的元组。您应该做的是在完成使用后使用 ReleaseSysCache() 释放它;这会通知缓存,如果需要,它可以丢弃该元组。如果您忽略调用 ReleaseSysCache(),则缓存条目将在事务结束之前保持锁定状态,这在开发过程中是可以接受的,但对于可发布的代码来说是不可接受的。
如果您不能使用系统缓存,则需要使用所有后端共享的缓冲区缓存直接从堆表中检索数据。后端会自动处理将行加载到缓冲区缓存中。为此,请使用 heap_open() 打开表。然后,您可以使用 heap_beginscan() 启动表扫描,然后使用 heap_getnext() 并持续执行,只要 HeapTupleIsValid() 返回 true。然后执行 heap_endscan()。可以将键分配给扫描。不使用索引,因此所有行都将与键进行比较,并且只返回有效的行。
您还可以使用 heap_fetch() 通过块号/偏移量获取行。虽然扫描会自动锁定/解锁缓冲区缓存中的行,但使用 heap_fetch(),您必须传递一个 Buffer 指针,并在完成后使用 ReleaseBuffer() 释放它。
一旦您拥有了行,您就可以通过简单地访问 HeapTuple 结构条目来获取所有元组共有的数据,如 t_self 和 t_oid。如果您需要特定于表的列,您应该获取 HeapTuple 指针,并使用 GETSTRUCT() 宏访问元组的特定于表的起始位置。然后将指针强制转换为类型,例如,如果您访问 pg_proc 表,则将其强制转换为 Form_pg_proc 指针,或者如果您访问 pg_type 表,则将其强制转换为 Form_pg_type。然后,您可以使用结构指针访问元组的字段
((Form_pg_class) GETSTRUCT(tuple))->relnatts
但是请注意,这仅适用于固定宽度且永不为空的列,并且仅当所有之前的列也同样固定宽度且永不为空时。否则,该列的位置是可变的,您必须使用 heap_getattr() 或相关函数从元组中提取它。
此外,避免直接将数据存储到结构字段中作为更改活动元组的方法。最好的方法是使用 heap_modifytuple() 并将您的原始元组以及您要更改的值传递给它。它返回一个 palloc'ed 元组,您将其传递给 heap_update()。您可以通过将元组的 t_self 传递给 heap_delete() 来删除元组。您也使用 t_self 进行 heap_update()。请记住,元组可以是系统缓存副本,这些副本在您调用 ReleaseSysCache() 后可能会消失,或者直接从磁盘缓冲区读取,在您调用 heap_getnext()、heap_endscan 或 ReleaseBuffer() 时,在 heap_fetch() 的情况下会消失。或者它可能是一个 palloc'ed 元组,您必须在完成时 pfree()。
为什么表、列、类型、函数、视图名称有时被引用为 Name 或 NameData,有时被引用为 char *?
表、列、类型、函数和视图名称存储在系统表中,这些表在类型为 Name 的列中。Name 是一个固定长度的、以 null 结尾的类型,包含 NAMEDATALEN 字节。(NAMEDATALEN 的默认值为 64 字节。)
typedef struct nameData { char data[NAMEDATALEN]; } NameData; typedef NameData *Name;
通过用户查询进入后端的表、列、类型、函数和视图名称存储为可变长度的、以 null 结尾的字符字符串。
许多函数都使用这两种类型的名称调用,例如 heap_open()。因为 Name 类型是以 null 结尾的,所以将其传递给需要 char * 的函数是安全的。因为有很多情况是磁盘上的名称(Name)与用户提供的名称(char *)进行比较,所以有很多情况是 Name 和 char * 交互使用。
为什么我们使用 Node 和 List 来构建数据结构?
我们这样做是因为这允许以灵活的方式在后端内部一致地传递数据。每个节点都有一个 NodeTag,它指定节点内部数据的类型。列表是作为前向链表链接在一起的一组节点。列表元素的顺序可能很重要,也可能不重要,具体取决于特定列表的用法。
以下是 List 操作命令的一些示例
- lfirst(i)
- lfirst_int(i)
- lfirst_oid(i)
- 返回列表单元格 i 的数据(分别为指针、整数或 OID)。
- lnext(i)
- 返回 i 之后的下一个列表单元格。
- foreach(i, list)
- 遍历列表,将每个列表单元格分配给 i。
需要注意的是,i 是一个 ListCell *,而不是 List 单元格中的数据。您需要使用 lfirst 变体之一来获取单元格的数据。
以下是一个典型的代码片段,它遍历一个包含 Var * 单元格的 List,并处理每个单元格
List *list; ListCell *i; ... foreach(i, list) { Var *var = (Var *) lfirst(i); ... /* process var here */ }
- lcons(node, list)
- 将节点添加到列表的前面,或者如果列表是 NIL,则创建一个包含节点的新列表。
- lappend(list, node)
- 将节点添加到列表的末尾。
- list_concat(list1, list2)
- 将 list2 连接到 list1 的末尾。
- list_length(list)
- 返回列表的长度。
- list_nth(list, i)
- 返回列表中第 i 个元素,从零开始计数。
- lcons_int, ...
- 这些函数有整数版本:lcons_int、lappend_int 等。还有 OID 列表版本:lcons_oid、lappend_oid 等。
您可以在 gdb 中轻松地打印节点。首先,在您使用 gdb print 命令时禁用输出截断
(gdb) set print elements 0
您可以使用接下来的两个命令来打印 List、Node 和结构内容,而不是以 gdb 格式打印值,这是一种更容易理解的详细格式。列表会展开成节点,节点会详细打印出来。第一个以简短格式打印,第二个以长格式打印
(gdb) call print(any_pointer) (gdb) call pprint(any_pointer)
输出显示在服务器日志文件中,或者如果您直接运行没有 postmaster 的后端,则显示在您的屏幕上。
我刚刚向结构中添加了一个字段。我还应该做什么?
解析器、重写器、优化器和执行器中传递的结构需要相当多的支持。大多数结构在 src/backend/nodes 中都有支持例程,用于创建、复制、读取和输出这些结构——特别是,大多数节点类型需要在 copyfuncs.c 和 equalfuncs.c 文件中进行支持,有些还需要在 outfuncs.c 中进行支持,并且可能需要在 readfuncs.c 中进行支持。确保您在这些文件中添加对新字段的支持。找到结构可能需要代码的任何其他地方——搜索结构的现有字段的引用是一个好方法。mkid 对此很有帮助(参见可用工具)。
为什么我们使用 palloc() 和 pfree() 来分配内存?
palloc() 和 pfree() 用于代替 malloc() 和 free(),因为我们发现更容易在查询完成时自动释放所有分配的内存。这确保了所有分配的内存都会被释放,即使我们丢失了分配内存的位置。存在内存可以分配到的特殊非查询上下文。这些会影响后端何时释放分配的内存。
您可以转储有关这些内存上下文的信息,这在查找泄漏时很有用。参见#检查后端内存使用情况。
什么是 ereport()?
ereport() 用于将消息发送到前端,并可以选择终止正在处理的当前查询。参见此处,了解有关如何使用它的更多详细信息。
什么是 CommandCounterIncrement()?
通常情况下,语句无法看到它们修改的行。这使得 UPDATE foo SET x = x + 1 可以正常工作。
但是,在某些情况下,事务需要查看事务先前部分中受影响的行。这是通过使用命令计数器来实现的。递增计数器允许事务被分成多个部分,这样每个部分都可以看到先前部分修改的行。CommandCounterIncrement() 递增命令计数器,创建事务的新部分。
我需要对查询解析进行一些更改。你能简要地解释一下解析器文件吗?
解析器文件位于 'src/backend/parser' 目录中。
scan.l 定义了词法分析器,即将字符串(包含 SQL 语句)拆分为令牌流的算法。令牌通常是一个单词(即不包含空格,但以空格分隔),但也可以是整个单引号或双引号字符串,例如。词法分析器基本上是通过正则表达式定义的,正则表达式描述了不同的令牌类型。
gram.y 使用词法分析器生成的令牌作为基本构建块来定义 SQL 语句的语法(句法结构)。语法使用 BNF 表示法定义。BNF 类似于正则表达式,但它是在令牌级别上工作的,而不是在字符级别上。此外,模式(在 BNF 中称为规则或产生式)被命名,并且可能是递归的,即使用自身作为子模式。
实际的词法分析器是由一个名为 flex 的工具从 scan.l 生成的。您可以在https://westes.github.io/flex/manual/index.html找到手册
实际的解析器是由一个名为 bison 的工具从 gram.y 生成的。您可以在http://www.gnu.org/s/bison/找到手册。
但是请注意,如果您以前从未使用过 flex 或 bison,那么您将面临相当陡峭的学习曲线。
我遇到了移位/归约冲突,我不知道如何处理
如何查看查询计划或解析后的查询?
通常需要检查解析后的查询或查询计划的结构。PostgreSQL 将这些存储为分层树,它可以用自定义格式打印出来。
函数pprint用于将这些树转储到后端的 stderr,您可以在日志中捕获它。您通常通过在运行查询之前将像 gdb 或 MSVC 这样的调试器附加到目标后端,然后在解析器/重写器/优化器/执行器中您想要查看查询状态的位置设置一个断点,来调用此函数。当断点触发时,只需运行
call pprint(theQueryVariable)
其中 queryVariable 是任何Node*类型,它pprint了解。通常您会在一个Query*上调用它,但也很常见的是转储查询的各种子部分,例如目标列表等。
此功能与 gdb 或 MSVC 跟踪点结合使用非常有用。
有哪些调试功能可用?
编译时
首先,如果您正在开发新的 C 代码,则应该始终在使用--enable-cassert和--enable-debug选项配置的构建中工作。启用断言会打开许多健全性检查选项。启用调试符号支持使用调试器(例如 gdb)来跟踪行为异常的代码。在gcc上编译时,额外的 cflags-ggdb -Og -g3 -fno-omit-frame-pointer也很有用,因为它们插入了大量的调试信息细节。您可以将它们传递给configure,使用类似以下内容:
./configure --enable-cassert --enable-debug CFLAGS="-ggdb -Og -g3 -fno-omit-frame-pointer"
使用-O0而不是-Og将禁用大多数编译器优化,包括内联,但-Og与通常的优化器标志(如-O2或-Os)的性能几乎一样好,同时提供了更多调试信息。您会看到更少的<value optimised out>变量,更少混乱和难以跟踪的执行重新排序等,但性能仍然相当可用。-ggdb -g3告诉gcc在生成的二进制文件中也包含最大数量的调试信息,包括宏定义等。
-fno-omit-frame-pointer在使用perf等跟踪和分析工具时很有用,因为帧指针允许这些工具捕获调用堆栈,而不仅仅是堆栈上的顶部函数。
运行时
postgres 服务器有一个-d选项,允许记录详细的信息(elog 或 ereport DEBUGn 打印输出)。-d 选项接受一个数字,指定调试级别。请注意,高调试级别值会生成大型日志文件。此选项在通过pg_ctl启动服务器时不可用,但您可以使用-o log_min_messages=debug4或类似内容代替。
在添加打印语句进行调试时,请记住logging_collector = on必须在您的 postgresql.conf 中设置(默认值为off),才能将 stdout/stderr 捕获并记录到文件中。考虑使用以下任一方法:elog()或fprintf(stderr, "Log\n")而不是printf("Log\n")因为通常 stdout 是完全缓冲的,而 stderr 只是行缓冲的。如果您打印到 stdout,则需要经常使用fflush来使输出与错误/日志消息同步(通过 stderr 传输)。
gdb
如果 postmaster 正在运行,在一个窗口中启动 psql,然后使用以下命令查找 psql 使用的 postgres 进程的 PID:SELECT pg_backend_pid(). 使用调试器附加到 postgres PID -gdb -p 1234或者,在运行的 gdb 中,attach 1234. 您可能还会发现 gdblive 脚本 很有用。 您可以在调试器中设置断点,然后从 psql 会话中发出查询。
如果您想查找生成错误或日志消息的位置,请在以下位置设置断点:errfinish. 这将捕获所有已启用的日志级别的elog和ereport调用,因此可能会被触发很多次。如果您只对 ERROR/FATAL/PANIC 感兴趣,请使用 gdb 条件断点 用于errordata[errordata_stack_depth].elevel >= 20, 或者在 PANIC、FATAL 和 ERROR 的 case 中设置源代码行断点errfinish. 请注意,并非所有错误都会经过errfinish; 特别是,权限检查是单独抛出的。如果您的断点没有触发,git grep查找错误文本并查看它从何处抛出。
如果您正在调试在会话启动期间发生的事件,您可以设置PGOPTIONS="-W n", 然后启动 psql。 这将导致启动延迟 n 秒,以便您可以使用调试器附加到进程,设置适当的断点,然后继续执行启动序列。
您有时可以通过查看以下内容来找出调试的目标进程:pg_stat_activity, 日志,pg_locks, pg_stat_replication等。
工具
有一些有用的 gdb 宏和 Python 脚本集可以帮助进行 PostgreSQL 调试,例如
您也可以 从内部调用 PostgreSQL 函数,如 pprintgdb检查数据结构。
所有这些工具和技术都适用于 gdb 包装器,例如 Eclipse CDT 独立图形调试器.
核心转储
如果很难预测哪个进程会出现问题,但您可以可靠地使它崩溃(也许是通过添加适当的Assert(...)并使用--enable-cassert) 您可以调试核心转储。 在 Linux 上,您需要确保/proc/sys/kernel/core_pattern具有合理的价值,例如core.%e.%p.SIG%s.%t并且,在您从其启动 PostgreSQL 的 shell 中,运行
ulimit -c unlimited
除非您使用的是大型shared_buffers您可能还想将核心转储(以及gdb的gcore) 设置为包含共享内存,使用
echo 127 > /proc/self/coredump_filter
核心转储将输出到 PostgreSQL 数据目录,除非您的内核的core_pattern另有说明。
rr 记录和重放调试器
PostgreSQL 13 可以使用 rr 调试记录器 进行调试。 您可以将 rr 视为使用可重放的程序执行“记录”的 GDB 的强大框架。 请参阅 使用 rr 调试 Postgres 的指南 以获取更多详细信息。
独立后端
如果 postmaster 未运行,您实际上可以从命令行运行 postgres 后端,并直接键入您的 SQL 语句。 但是,这几乎总是错误的做法,因为使用环境不如 psql 友好(例如没有命令历史记录),并且没有机会研究并发行为。 如果您破坏了 initdb,您可能需要使用此方法,但除此之外,它没有可取之处。
我破坏了 initdb,如何调试它?
有时,补丁会导致initdb失败。 这些很少出现在initdb本身;更常见的是,失败发生在postgres后端启动时initdb执行一些设置工作。
如果其中之一崩溃或触发断言,则附加gdb到initdb本身不会有什么帮助。initdb本身没有崩溃,所以gdb不会断开。
您需要做的是运行initdb在gdb下,在fork上设置断点,然后继续执行。 当您触发断点时,finish该函数。gdb将报告已创建子进程,但这不是您想要的,它是启动真实进程的 shellpostgres实例。
在initdb暂停时,使用ps查找它启动的postgres实例。pstree -p对此很有用。 找到它后,使用gdb会话附加到它gdb -p $the_postgres_pid. 在这一点上,您可以安全地分离gdb从initdb并调试正在失败的postgres实例。
另请参阅 创建集群时跟踪问题
性能分析,CPU 使用率
有许多用于分析 PostgreSQL 的选项,但现在最流行的选项之一是perf, Linux 内核分析工具。 请参阅 使用 perf 进行分析.
perf非常强大,不限于 CPU 分析;它也是一个有用的跟踪工具。
您还可以使用启用了分析功能的 PostgreSQL 进行编译,以查看哪些函数正在占用执行时间。 使用--enable-profiling是设置此项的推荐方法。 来自服务器进程的分析文件将被存放到pgsql/data目录中。 来自客户端(如psql的分析文件将放在客户端的当前目录中。
通常不应该使用--enable-cassert或任何用户定义的-O标志,例如-Og / -O0在研究性能问题时。 cassert 启用的检查并不总是便宜,因此它们会扭曲您的分析数据。 编译器优化对于确保您分析的是实际运行的相同内容非常重要。
--enable-debug在使用gcc进行分析时是可以的;对于其他编译器,应避免使用它。
perf是现代 Linux 系统上比--enable-profiling更不具侵入性的替代方案。
检查后端内存使用情况
PostgreSQL 的palloc是一个分层内存分配器,它包装了平台分配器。 请参阅 #为什么使用 palloc() 和 pfree() 来分配内存?.
使用palloc分配的内存被分配到一个内存上下文,该上下文是位于TopMemoryContext的层次结构的一部分。 每个上下文都有一个名称。
您可以使用以下命令转储有关内存上下文及其子级的统计信息:MemoryContextStats(MemoryContext*)函数。 在最常见的用法中,即
gdb -p $the_backend_pid (gdb) p MemoryContextStats(TopMemoryContext)
输出将写入 stderr。
这可能出现在主服务器日志文件中、初始化系统用于 PostgreSQL 的日志收集器启动之前的辅助日志中、journald 中,或者如果您直接运行后端而没有 postmaster,则出现在您的屏幕上。
从 v14 开始,您可以使用视图 pg_backend_memory_contexts 或函数 pg_log_backend_memory_contexts 来访问相同的信息,而无需使用 gdb。
gdb/MSVC 跟踪点
有时您希望跟踪执行并捕获信息,而无需每次遇到断点时都切换到 gdb。
MSVC 和gdb都为此提供了跟踪点。 它们比perf提供的跟踪点强大得多 - 唯一的权衡是它们更具侵入性,需要调试器。 对于 gdb,请参阅 gdb 跟踪点。 您可以使用调试器跟踪点来执行一些操作,例如每次遇到跟踪点时触发内存上下文转储,或打印查询解析树等等。
对于一些更简单的用例,一种可行的替代方法是现在使用perf捕获函数调用、局部变量等等。 请参阅 使用 perf 进行分析.
为什么我的变量充满了 0x7f 字节?
在调试器或崩溃转储中,您可能会看到充满了 0x7f 字节的内存 - 0x7f7f 字,0x7f7f7f7f7f 长整型等等。
这是因为定义了CLOBBER_FREED_MEMORY的构建将在内存本身或其包含的内存上下文被释放时覆盖内存。 这并不一定与显式pfree相关 - 它可能作为MemoryContextReset或类似操作的结果发生,可能发生在您通过调用palloc隐式分配给当前内存上下文的内存上,或者通过调用另一个函数间接分配的内存上。
CLOBBER_FREED_MEMORY通过传递--enable-cassert.
启用。 请参阅src/backend/utils/mmgr/aset.c了解更多信息。
如何阻止 gdb 不断被 SIGUSR1 中断?
PostgreSQL 使用 SIGUSR1 为后端设置闩锁,用于 SetLatch / WaitLatch / WaitLatchOrSocket 等等。
默认情况下,gdb 会中断 SIGUSR1,这使得调试变得困难。
只需
handle SIGUSR1 noprint pass
即可使它静默地将 SIGUSR1 传递给程序,而不会暂停。 或者像这样启动它:
gdb -ex 'handle SIGUSR1 nostop'
如何附加 gdb 并在后台工作进程/辅助进程中设置断点?
如果您试图调试 autovacuum、一些任意后台工作进程等等,那么当您想要时很难让 gdb 附加。 特别是如果进程很短暂。
这里一个方便的技巧是注入一个无限循环,它会打印 PID 直到您附加 gdb 并更改循环变量以允许调试继续。 例如,如果您在调用要调试的函数之前添加以下内容
/* You may need to #include "miscadmin.h" and <unistd.h> */ bool continue_sleep = true; do { sleep(1); elog(LOG, "zzzzz %d", MyProcPid); } while (continue_sleep); func_to_debug()
您可以搜索日志中的“zzzz”直到它出现,附加到感兴趣的 PID,设置断点,然后继续执行。
$ gdb -p $the-pid (gdb) break func_to_debug (gdb) p continue_sleep=0 (gdb) cont
请注意,在 PostgreSQL 后端中使用sleep是一种不好的做法;使用WaitLatch并设置超时。 这对于调试来说是可以的。
另一个选择是让 PostgreSQL 使用postgres -W <seconds>选项在启动时延迟所有进程,但这在您调试复杂 bgworker 组中的问题或仅在长时间运行后发生的事件时效果很差。