PostgreSQL源碼結構

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的結構。 後端由幾個進程構成。 

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的動作。(原文)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章