Pgsrcstructure

From PostgreSQL wiki
Jump to navigationJump to search

PostgreSQL及其代码的结构


本文将对PostgreSQL全体的结构以及源码的结构概要进行说明。解析的版本是PostgreSQL9.1.x系列的版本,其他版本有可能会和本文说明的内容有些差别。

这篇文章将以笔者05年在技术评论社《WEB+DB PRESS》第26期发表的文章为基础,根据PostgreSQL9.1的实际情况加以修改而成。

PostgreSQL的使用形态

PostgreSQL采用C/S(客户机/服务器)模式结构。应用层通过INET或者Unix Socket利用既定的协议与数据库服务器进行通信。

另外,还有一种‘Standalone Backend’使用的方式, 虽然通过这种方式也可以启动服务器,但是一般只在数据库的初始化(PostgreSQL的cluster的初始化,相当于其他数据库的instance的初始化)、紧急维护的时候使用,所以简单来说可以认为PostgreSQL是使用C/S的形式进行访问的。

PostgreSQL把客户端称为前端(Frontend),把服务器端成为后端(Backend), 后端有复数个进程构成,这个在后面会进行说明。

前端和后端通信的协议在PostgreSQL的官方文档中的《前端和后端的通信协议》一章中有详细的说明。简单来说,大体的工作模式是:前端向后端发送查询的SQL文,然后后端通过复数个报文把结果返回给前端。

由于需要进行连接的初始化、错误等各种各样处理,PostgreSQL的协议的处理也是相当复杂,如果要自己从头实现这些协议的处理的话,还是相当麻烦的,所以PostgreSQL本身提供了C语言写的libpq这样一个协议处理库,利用这个库可以比较轻松地和后端进行通信。PostgreSQL的话除了C以外,还支持Perl和PHP等其他语言,这些语言在内部也调用了libpq.

也有不使用libpq而直接与PostgreSQL通信的库。比较具有代表性的是Java, PostgreSQL的JDBC驱动是不依赖于libpq直接与PostgreSQL通信的.

另外后端的话,比较核心的是进行数据库处理的数据库引擎(Database Engine)。 数据库引擎可以对用户所编写的函数进行解析和处理,用户如果能够利用好这个功能的话,可以柔软地扩展PostgreSQL的功能。 比较经常使用的是存储过程(PostgreSQL中称为用户自定义函数),PostgreSQL支持的用户定义函数的语言如下:

语言	   对应的自定义函数
C	   C函数
SQL	   SQL 函数
类似Oracle的PL/SQL的语言	PL/pgSQL
Perl	   PL/Perl
Python	    PL/Python

PostgreSQL的话,用户可以自定义语言处理引擎。各种服务器脚本语言的解析引擎,以第三方的形式存在,主要的处理语言有Ruby、Java以及PHP等。

PostgreSQL的结构

这里的话,再详细看看PostgreSQL的结构。 后端由几个进程构成。
Src-arch.jpg

Potgres(常驻进程)

管理后端的常驻进程,也称为’postmaster’。其默认监听UNIX Domain Socket和TCP/IP(Windows等,一部分的平台只监听tcp/ip)的5432端口,等待来自前端的的连接处理。监听的端口号可以在PostgreSQL的设置文件postgresql.conf里面可以改。

一旦有前端连接过来,postgres会通过fork(2)生成子进程。没有Fork(2)的windows平台的话,则利用createProcess()生成新的进程。这种情形的话,和fork(2)不同的是,父进程的数据不会被继承过来,所以需要利用共享内存把父进程的数据继承过来。

Postgres(子进程)

子进程根据pg_hba.conf定义的安全策略来判断是否允许进行连接,根据策略,会拒绝某些特定的IP及网络,或者也可以只允许某些特定的用户或者对某些数据库进行连接。

Postgres会接受前端过来的查询,然后对数据库进行检索,最好把结果返回,有时也会对数据库进行更新。更新的数据同时还会记录在事务日志里面(PostgreSQL称为WAL日志),这个主要是当停电的时候,服务器当机,重新启动的时候进行恢复处理的时候使用的。另外,把日志归档保存起来,可在需要进行恢复的时候使用。在PostgreSQL 9.0以后,通过把WAL日志传送其他的postgreSQL,可以实时得进行数据库复制,这就是所谓的‘数据库复制’功能。

其他的进程

Postgres之外还有一些辅助的进程。这些进程都是由常驻postgres启动的进程。

Writer process

Writer process在适当的时间点把共享内存上的缓存写往磁盘。通过这个进程,可以防止在检查点的时候(checkpoint),大量的往磁盘写而导致性能恶化,使得服务器可以保持比较稳定的性能。Background writer起来以后就一直常驻内存,但是并非一直在工作,它会在工作一段时间后进行休眠,休眠的时间间隔通过postgresql.conf里面的参数bgwriter_delay设置,默认是200微秒。

这个进程的另外一个重要的功能是定期执行检查点(checkpoint)。

检查点的时候,会把共享内存上的缓存内容往数据库文件写,使得内存和文件的状态一致。通过这样,可以在系统崩溃的时候可以缩短从WAL恢复的时间,另外也可以防止WAL无限的增长。 可以通过postgresql.conf的checkpoint_segments、checkpoint_timeout指定执行检查点的时间间隔。

WAL writer process

WAL writer process把共享内存上的WAL缓存在适当的时间点往磁盘写,通过这样,可以减轻后端进程在写自己的WAL缓存时的压力,提高性能。另外,非同步提交设为true的时候,可以保证在一定的时间间隔内,把WAL缓存上的内容写入WAL日志文件。

Archive process

Archive process把WAL日志转移到归档日志里。如果保存了基础备份以及归档日志,即使实在磁盘完全损坏的时候,也可以回复数据库到最新的状态。

stats collector process

统计信息的收集进程。收集好统计表的访问次数,磁盘的访问次数等信息。收集到的信息除了能被autovaccum利用,还可以给其他数据库管理员作为数据库管理的参考信息。

Logger process

把postgresql的活动状态写到日志信息文件(并非事务日志),在指定的时间间隔里面,对日志文件进行rotate.

Autovacuum启动进程

autovacuum launcher process是依赖于postmaster间接启动vacuum进程。而其自身是不直接启动自动vacuum进程的。通过这样可以提高系统的可靠性。

自动vacuum进程

autovacuum worker process进程实际执行vacuum的任务。有时候会同时启动多个vacuum进程。

wal sender / wal receiver

wal sender 进程和wal receiver进程是实现postgresql复制(streaming replication)的进程。Wal sender进程通过网络传送WAL日志,而其他PostgreSQL实例的wal receiver进程则接收相应的日志。Wal receiver进程的宿主PostgreSQL(也称为Standby)接受到WAL日志后,在自身的数据库上还原,生成一个和发送端的PostgreSQL(也称为Master)完全一样的数据库。

后端的处理流程

下面看看数据库引擎postgres子进程的处理概要。为了简单起见下面的说明中,把backend process简称为backend。Backend的main函数是PostgresMain (tcop/postgres.c)。

  1. 接收前端发送过来的查询(SQL文)
  2. SQL文是单纯的文字,电脑是认识不了的,所以要转换成比较容易处理的内部形式构文树parser tree,这个处理的称为构文解析。构文解析的模块称为parser.这个阶段只能够使用文字字面上得来的信息,所以只要没语法错误之类的错误,即使是select不存在的表也不会报错。这个阶段的构文树被称为raw parse tree. 构文处理的入口在raw_parser (parser/parser.c)。
  3. 构文树解析完以后,会转换为查询树(Query tree)。这个时候,会访问数据库,检查表是否存在,如果存在的话,则把表名转换为OID。这个处理称为分析处理(Analyze), 进行分析处理的模块是analyzer。 另外,PostgreSQL的代码里面提到构文树parser tree的时候,更多的时候是指查询树Query tree。分析处理的模块的入口在parse_analyze (parser/analyze.c)
  4. PostgreSQL还通过查询语句的重写实现视图(view)和规则(rule), 所以需要的时候,在这个阶段会对查询语句进行重写。这个处理称为重写(rewrite),重写的入口在QueryRewrite (rewrite/rewriteHandler.c)。
  5. 通过解析查询树,可以实际生成计划树。生成查询树的处理称为‘执行计划处理’,最关键是要生成估计能在最短的时间内完成的计划树(plan tree)。这个步骤称为’查询优化’(不叫query optimize, 而是optimize), 而完成这个处理的模块称为查询优化器(不叫query optimizer,而是optimizer, 或者称为planner)。执行计划处理的入口在standard_planner (optimizer/plan/planner.c)。
  6. 按照执行计划里面的步骤可以完成查询要达到的目的。运行执行计划树里面步骤的处理称为执行处理‘execute’, 完成这个处理的模块称为执行器‘Executor’, 执行器的入口地址为,ExecutorRun (executor/execMain.c)
  7. 执行结果返回给前端。
  8. 返回到步骤一重复执行。

PostgreSQL的源码

现在基本上理解了PostgreSQL的大体的结构,我们再来看看PostgreSQL代码的结构。 PostgreSQL初期的时候,大概只有20万行左右的代码,现在已经发展到100万行了。这个量来说,没有指导读起来是极为难理解的,这里把大概的代码结构说明一下,让大家对源码的结构有个理解。

第一级目录结构

进入PostgreSQL的源码目录后,第一级的结构如下表所示。在这一级里,通过执行如下命令configure;make;make install可以立即进行简单的安装,实际上从PostgreSQL源码安装是极为简单的。

文件目录	说明
COPYRIGHT	版权信息
GUNMakefile	第一级目录的 Makefile
GUNMakefile.in	Makefile 的雏形
HISTORY        修改历史
INSTALL        安装方法简要说明
Makefile	Makefile模版
README	        简单说明
aclocal.m4	config 用的文件的一部分
config/	config 用的文件的目录
configure	configure 文件
configure.in	configure 文件的雏形
contrib/	contribution 程序
doc/	        文档目录
src/	        源代码目录

PostgreSQL 的src下面有。

文件目录	说明
DEVELOPERS	        面向开发人员的注视
Makefile	        Makefile 
Makefile.global	make 的设定值(从configure生成的)
Makefile.global.in	Configure使用的Makefile.global的雏形
Makefile.port	        平台相关的make的设定值,实际是一个到makefile/Makefile的连接. (从configure生成的)
Makefile.shlib	        共享库用的Makefile
backend/	        后端的源码目录
bcc32.mak	        Win32 ポート用の Makefile (Borland C++ 用)
bin/	                psql 等 UNIX命令的代码
include/	        头文件
interfaces/	        前端相关的库的代码
makefiles/	        平台相关的make 的设置值
nls-global.mk	        信息目录用的Makefile文件的规则
pl/	                存储过程语言的代码
port/	                平台移植相关的代码
template/	        平台相关的设置值
test/	                各种测试脚本
timezone/	        时区相关代码
tools/	                各自开发工具和文档
tutorial/	        教程
win32.mak	        Win32 ポート用の Makefile (Visual C++ 用) 


这里比较核心的是backend,bin,interface这几个目录。Backend是对应于后端,bin和interface对应于前端。

bin里面有pgsql,initdb,pg_dump等各种工具的代码。interface里面有PostgreSQL的C语言的库libpq,另外可以在C里嵌入SQL的ECPG命令的相关代码。

Backend目录的结构如下:

目录文件	        说明
Makefile	makefile
access/	各种存储访问方法(在各个子目录下) common(共同函数)、gin (Generalized Inverted Index通用逆向索引)
gist (Generalized Search Tree通用索引)、 hash (哈希索引)、heap (heap的访问方法)、
index (通用索引函数)、 nbtree (Btree函数)、transam (事务处理) bootstrap/ 数据库的初始化处理(initdb的时候) catalog/ 系统目录 commands/ SELECT/INSERT/UPDATE/DELETE以为的SQL文的处理 executor/ 执行器(访问的执行) foreign/ FDW(Foreign Data Wrapper)处理 lib/ 共同函数 libpq/ 前端/后端通信处理 main/ postgres的主函数 nodes/ 构文树节点相关的处理函数 optimizer/ 优化器 parser/ SQL构文解析器 port/ 平台相关的代码 postmaster/ postmaster的主函数 (常驻postgres) replication/ streaming replication regex/ 正则处理 rewrite/ 规则及视图相关的重写处理 snowball/ 全文检索相关(语干处理) storage/ 共享内存、磁盘上的存储、缓存等全部一次/二次记录管理(以下的目录)buffer/(缓存管理)、 file/(文件)、
freespace/(Fee Space Map管理) ipc/(进程间通信)、large_object /(大对象的访问函数)、
lmgr/(锁管理)、page/(页面访问相关函数)、 smgr/(存储管理器) tcop/ postgres (数据库引擎的进程)的主要部分 tsearch/ 全文检索 utils/ 各种模块(以下目录) adt/(嵌入的数据类型)、cache/(缓存管理)、 error/(错误处理)、fmgr/(函数管理)、
hash/(hash函数)、 init/(数据库初始化、postgres的初期处理)、 mb/(多字节文字处理)、
misc/(其他)、mmgr/(内存的管理函数)、 resowner/(查询处理中的数据(buffer pin及表锁)的管理)、
sort/(排序处理)、time/(事务的 MVCC 管理)


backend等的代码的头文件包含在include里面。其组织虽然与backend的目录结构类似,但是并非完全相同,基本上来说下一级的子目录不再设下一级目录。例如backend的目录下面有utils这个目录,而util下面还有adt这个子目录,但是include里面省略了这个目录,变成了扁平的结构。

access/
bootstrap/
c.h
catalog/
commands/
dynloader.h
executor/
fmgr.h
foreign/
funcapi.h
getaddrinfo.h
getopt_long.h
lib/
libpq/
mb/
miscadmin.h
nodes/
optimizer/
parser/
pg_config.h
pg_config.h.in
pg_config.h.win32
pg_config_manual.h
pg_config_os.h
pg_trace.h
pgstat.h
pgtime.h
port/
port.h
portability/
postgres.h
postgres_ext.h
postgres_fe.h
postmaster/
regex/
rewrite/
rusagestub.h
snowball/
stamp-h
storage/
tcop/
tsearch/
utils/
windowapi.h

代码的阅读方法

用调试器追踪代码

PostgreSQL那样的庞大系统,用眼睛来追踪源码并不容易。这里推荐用gdb这样的实际调试器来追踪代码的执行流程。可能有些人畏惧调试器,但是如果只是简单追踪代码的执行流的话,还是很简单的。

但是多少还是要做一些准备的,PostgreSQL在编译的时候一定要把调试开关打开。通常在编译的时候configure的时候加上--enable-debug的选项,然后可能的话可以编辑src/Makefile.global这个文件

CFLAGS = -O2 -Wall -Wmissing-prototypes -Wpointer-arith \
-Wdeclaration-after-statement -Wendif-labels -Wformat-security \
-fno-strict-aliasing -fwrapv

上面的行的"-O2"选项删除,然后加上"-g"

CFLAGS = -g -Wall -Wmissing-prototypes -Wpointer-arith \
-Wdeclaration-after-statement -Wendif-labels -Wformat-security \
-fno-strict-aliasing -fwrapv

"-O2"是编译器的优化选项,如果打开了,代码的执行顺序会改变,使得追踪起代码来比较困难,所以要去除。当然这样的话,编译后的可执行文件会比较大,而且会比较慢,生产环境不太合适。大家需要理解这个操作仅仅是在学习的时候而设置的。

实际使用gdb试试

下面实际使用gdb来看看比较简单点的select文。

select 1;

select文执行后,至executor的其中一个函数ExecSelect停止,然后我们调查一下实际调用了那些函数。

首先以PostgreSQL的超级用户登录。我的环境是使用t-ishii这个用户安装PostgreSQL的,通常一般使用postgres这个用户,大家在阅读的时候替换一下即可。

然后,用psql和数据库进行连接,连接的状态可以通过ps命令调查。

$ ps x

3714 ?        Ss     0:00 postgres: t-ishii test [local] idle                   

可以看到上面的进程。这个就是后端的进程。这个是后端的进程,还有其他大量用户的PostgreSQL的连接也显示出来,比较难看清楚,所以还是准备好测试的环境来进行测试比较好。

启动gdb后,附加到ps里显示的进程号码。

$ gdb postgres 3714
GNU gdb (GDB) 7.2
Copyright (C) 2010 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later 
<http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.  Type "show copying"
and "show warranty" for details.
This GDB was configured as "x86_64-vine-linux".
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>...
Reading symbols from /usr/local/pgsql/bin/postgres...done.
Attaching to program: /usr/local/pgsql/bin/postgres, process 3714
Reading symbols from /lib64/libdl.so.2...done.
Loaded symbols for /lib64/libdl.so.2
Reading symbols from /lib64/libm.so.6...done.
Loaded symbols for /lib64/libm.so.6
Reading symbols from /lib64/libc.so.6...done.
Loaded symbols for /lib64/libc.so.6
Reading symbols from /lib64/ld-linux-x86-64.so.2...done.
Loaded symbols for /lib64/ld-linux-x86-64.so.2
Reading symbols from /lib64/libnss_files.so.2...done.
Loaded symbols for /lib64/libnss_files.so.2
0x00007fad266f82e2 in __libc_recv (fd=<value optimized out>, buf=0xbe9900,
n=8192, flags=<value optimized out>)
    at ../sysdeps/unix/sysv/linux/x86_64/recv.c:30
30	../sysdeps/unix/sysv/linux/x86_64/recv.c: 
	in ../sysdeps/unix/sysv/linux/x86_64/recv.c
(gdb) 

(gdb) 是gdb的命令行。在这个状态下,可以接受gdb的命令,如果输入b命令的话,在ExecResult可以设置断点。

(gdb) b ExecResult
Breakpoint 1, ExecResult (node=0xd13eb0) at nodeResult.c:75
(gdb) 

psql启动以后从终端执行select 1,输入以后,后端就会执行该命令。这个时候,postgres进程已经暂停,所以psql会动不了。要继续执行的话,可在gdb里执行"c"命令。执行了以后,就会在ExecResult 处停止。

Continuing.

Breakpoint 1, ExecResult (node=0xd13eb0) at nodeResult.c:75
75		econtext = node->ps.ps_ExprContext;
(gdb) 

到ExecSelect为止的函数的调用路径可以用bt的命令显示出来。

(gdb) bt
#0  ExecResult (node=0xd13eb0) at nodeResult.c:75
#1  0x00000000005b92a4 in ExecProcNode (node=0xd13eb0) at execProcnode.c:367
#2  0x00000000005b71bb in ExecutePlan (estate=0xd13da0, planstate=0xd13eb0,
    operation=CMD_SELECT, sendTuples=1 '\001', numberTuples=0, 
    direction=ForwardScanDirection, dest=0xcf9938) at execMain.c:1439
#3  0x00000000005b5835 in standard_ExecutorRun (queryDesc=0xc62820, 
    direction=ForwardScanDirection, count=0) at execMain.c:313
#4  0x00000000005b5729 in ExecutorRun (queryDesc=0xc62820, 
    direction=ForwardScanDirection, count=0) at execMain.c:261
#5  0x00000000006d2f79 in PortalRunSelect (portal=0xc60810, 
    forward=1 '\001', count=0, dest=0xcf9938) at pquery.c:943
#6  0x00000000006d2c4e in PortalRun (portal=0xc60810, 
    count=9223372036854775807, isTopLevel=1 '\001', dest=0xcf9938, 
    altdest=0xcf9938, completionTag=0x7fffa4b0eeb0 "") at pquery.c:787
#7  0x00000000006cd135 in exec_simple_query 
    (query_string=0xcf8420 "select 1;") at postgres.c:1018
#8  0x00000000006d1144 in PostgresMain (argc=2, argv=0xc42da0, 
    username=0xc42c40 "t-ishii") at postgres.c:3926
#9  0x0000000000683ced in BackendRun (port=0xc65600) at postmaster.c:3600
#10 0x00000000006833dc in BackendStartup (port=0xc65600) at postmaster.c:3285
#11 0x0000000000680759 in ServerLoop () at postmaster.c:1454
#12 0x000000000067ff4d in PostmasterMain (argc=3, argv=0xc40e00) 
   at postmaster.c:1115
#13 0x00000000005f7a39 in main (argc=3, argv=0xc40e00) at main.c:199
(gdb) 

说明一下看的方法,发起调用的函数在下面,被调用的函数在上面。也就是ExecProcNode调用了ExecResult,ExecutePlan调用了ExecProcNode,ExecutorRun调用了ExecProcNode,这样的形式来写。特别是中间的第7行。

#7  0x00000000006cd135 in exec_simple_query 
    (query_string=0xcf8420 "select 1;") at postgres.c:1018

这样可以清楚看到在处理SELECT文。:)仔细看gdb的输出,可以发现这些细节。

gdb是源码调试器,所以可以看到和源代码的对应关系。例如list命令可以看到现在执行的行附近的代码。

(gdb) list
70     TupleTableSlot *resultSlot;
71     PlanState  *outerPlan;
72     ExprContext *econtext;
73     ExprDoneCond isDone;
74 
75     econtext = node->ps.ps_ExprContext;
76 
77     /*
78      * check constant qualifications like (2 > 1), if not already done
79      */

利用up命令可以往上面的函数移动。下面用list命令,可以确认实际调用ExecSelect 的地方。

(gdb) up
#1  0x00000000005b92a4 in ExecProcNode (node=0xd13eb0) at execProcnode.c:367
367             result = ExecResult((ResultState *) node);
(gdb) list
362     {
363             /*
364              * control nodes
365              */
366         case T_ResultState:
367             result = ExecResult((ResultState *) node);
368             break;
369 
370         case T_ModifyTableState:
371             result = ExecModifyTable((ModifyTableState *) node);

利用down可以往下面的函数移动。利用up和down的组合,可以调查函数的调用关系。

要退出gdb的话可以用quit。

(gdb) quit

Inferior 1 [process 3714] will be detached.

Quit anyway? (y or n) y
Detaching from program: /usr/local/pgsql/bin/postgres, process 3714

到了这里gdb就结束了,但是后端进程并不会终止。

使用tag来跳转到相应的函数定义文件

我们已经使用了gdb来调查postgreSQL的运行,另外用gdb的list来追踪源码的话还是相当辛苦的,一般来说用emacs等编辑器一起调查和浏览代码,可以在边调试边查看代码。

当然,在gdb模式下也可以使用。这个时候,例如如果想看看'exec_simple_query'的定义的话,使用emacs的tags命令可以立刻跳转到函数定义的地方。要使用tags的话,需要生产tags文件,PostgreSQL的话,带有生产tags文件的脚本。

$ cd /usr/local/src/postgresql-9.1.1/src
$ tools/make_etags (使用emacs的场合)
$ tools/make_tags (使用vi的场合)

这样就可以拉。 然后在emacs中,在exec_simple_query 处执行'ESC-.'(按了ESC键后输入逗号.),即可打开光标所在文字所在的exec_simple_query函数的定义文件。

总结

要完全理解PostgreSQL的话,通过调查源代码还是比较有效果的。要理解代码的话,可以按照目的自己追加必要的功能,改变一些功能的行为,大家可以最大限度的的享受开源带来的好处。这次为了让大家能够理解PostgreSQL的源代码,说明了PostgreSQL 9.1的全体结构,还有说明了代码树。然后还使用了调试器来追踪PostgreSQL的动作,接下来,看看PostgreSQL更详细的结构。

下次将继续调查parser的结构。

(2011年11月15日公開)