postgres-x2是一個基于pgsql、面向otlp的分布式資料庫,采用了shared-nothing的架構,目标是針對oltp\olap應用能做到可擴充的系統。源碼在github上:https://github.com/postgres-x2/postgres-x2 。
最近在針對 postgres-x2做壓力測試。測試是在一台dell r510上進行的,該伺服器上有4顆x5650和64g記憶體,另外是兩塊老式的ssd,型号不詳,最大寫入速度100m左右。
測試的版本是直接從github上拉下來的,直接編譯安裝。在這台伺服器上安裝了gtm、coordinator、datanode各一個,gtm和coordinator的資料目錄都在一個ssd上,datanode的資料目錄在另外的一個ssd盤上。以最大化利用磁盤資源。之是以在一台伺服器上進行測試,主要是想模拟一個網絡帶寬不受限的環境,也是為了簡化問題。
coordinator的幾個重要配置如下:
conf代碼
1. max_connections = 1024
2. shared_buffers = 2048mb # shared_buffer沒有必要設定過大
3. checkpoint_segments = 64
4. checkpoint_timeout = 20min # 讓checkpoint間隔盡量大
5. logging_collector = on
6. autovacuum = off
7. min_pool_size = 100 # 連接配接池初始大小
8. max_pool_size = 1024
datanode的配置如下
1. max_connections = 1024
2. shared_buffers = 20480mb
3. vacuum_cost_delay = 10
4. vacuum_cost_limit = 10000
5. checkpoint_segments = 256
6. checkpoint_timeout = 10min
7. logging_collector = on
8. autovacuum_vacuum_cost_delay = 5ms
測試的主要方法是使用pgbench生成scale為1000的資料集合,大概有16g,主要的測試方法就是先執行checkpoint,将資料塊刷回磁盤,以減小checkpoint的影響,然後執行下面的指令:
bash代碼
1. pgbench -c n -j n -t 60
n是連接配接數,這個操作會模拟并發的n個客戶連續通路資料庫60秒,每個客戶需要在60秒内不停執行一個有着5條sql的事務:
1. begin;
2. update pgbench_tellers set tbalance = tbalance + $1 where tid = $2;
3. update pgbench_branches set bbalance = bbalance + $1 where bid = $2;
4. select abalance from pgbench_accounts where aid = $1;
5. insert into pgbench_history (tid, bid, aid, delta, mtime) values ($1, $2, $3, $4, current_timestamp);
6. update pgbench_accounts set abalance = abalance + $1 where aid = $2;
7. end;
測試的時候,n從32增大到768,取tps。結果如下:

随着連接配接數的增加,tps幾乎是線性降低。datanode資料目錄所在的磁盤使用率也基本上從最高的60%降低到了20%,而整體的cpu使用率不怎麼飙升,隻是gtm看起來還比較忙。
感覺gtm比較有問題,于是祭出了gprof來做性能分析,結果啥都沒有,google一下,發現是gtm屏蔽了sigprof信号,打上我的這個patch就ok了:
c代碼
1. diff --git a/src/gtm/libpq/pqsignal.c b/src/gtm/libpq/pqsignal.c
2. index e3f482c..594540f 100644
3. --- a/src/gtm/libpq/pqsignal.c
4. +++ b/src/gtm/libpq/pqsignal.c
5. @@ -119,6 +119,10 @@ pqinitmask(void)
6. sigdelset(&blocksig, sigcont);
7. sigdelset(&authblocksig, sigcont);
8. #endif
9. +#ifdef sigprof
10. + sigdelset(&blocksig, sigprof);
11. + sigdelset(&authblocksig, sigprof);
12. +#endif
記得使用--enable-profiling選項來重新生成makefile 或是直接編輯makefile加上 -pg選項,然後重新編譯一下gtm。拿pgbench運作一段時間之後,停掉gtm,在gtm的目錄下會有一個gmon.out,執行:
1. gprof -b /usr/local/pgx2/bin/gtm gmon.out > gtm.out
在生成的gtm.out中,可以看到有兩個函數幾乎占用了cpu的大部分時間:
gmon代碼
1. each sample counts as 0.01 seconds.
2. % cumulative self self total
3. time seconds seconds calls ms/call ms/call name
4. 31.41 49.90 49.90 8256962 0.01 0.01 gtm_gxidtohandle
5. 15.00 73.72 23.82 3791614 0.01 0.01 gtm_gettransactionsnapshot
6. 6.33 83.77 10.05 4305476 0.00 0.00 gtm_list_delete
7. 5.07 91.83 8.06 790 10.20 181.29 gtm_threadmain
8. 3.09 96.75 4.92 25006706 0.00 0.00 allocsetalloc
9. 2.06 100.02 3.27 752039341 0.00 0.00 globaltransactionidprecedes
10. 1.78 102.85 2.83 30641196 0.00 0.00 elog_start
11. 1.66 105.49 2.64 11157165 0.00 0.00 pq_recvbuf
12. 1.62 108.07 2.58 13648719 0.00 0.00 internal_flush
在花了半天看了這兩個耗時的函數,大概有點眉目:
1,當coordinator上啟動一個事務時,回去gtm申請一個一個事務id (xid) 和存放事務相關資訊的gtm_transactioninfo資料結構,并把這個資料結構的指針放入一個全局的連結清單gtmtransactions.gt_open_transactions中,gtm将事務id傳回給coordinator
2,事務執行時,會将xid發給gtm去擷取快照。gtm會首先調用gtm_gxidtohandle函數去獲得對應的gtm_transactioninfo資料結構的指針,gtm_gxidtohandle函數會周遊全局連結清單gtmtransactions.gt_open_transactions來擷取。然後将該gtm_transactioninfo資料結構的指針傳給gtm_gettransactionsnapshot函數來獲得快照:周遊gtmtransactions.gt_open_transactions中的每個元素,擷取全局最小的xmin,和活躍的事務id(小于最近送出事務的最大id),放入快照并傳回給coordinator。
3,事務結束時,coordinator将xid傳回給gtm,gtm根據xid查找對應的gtm_transactioninfo資料結構,将其回收,并删除gtmtransactions.gt_open_transactions中的對應item。
簡單看來,這兩個耗時的函數都是o(n)級别的複雜度,n 是gtmtransactions.gt_open_transactions的長度,也就是目前正在進行的事務數。是以,随着連接配接數的增加,coordinator和datanode内部鎖競争的加劇,會導緻事務逐漸的積壓起來,讓gtmtransactions.gt_open_transactions長度變得越來越長,是以堵住了很多擷取事務快照的事務。最終就是剛才描述的情形:事務等待gtm,磁盤使用率急劇降低。
是以,從直覺出發,其實可以直接用開放式hash表來優化gtm_gxidtohandle函數,key是事務id,value是對應的gtm_transactioninfo的指針,将這個的函數的操作複雜度降低到o(1)。但是,發現效果并不好:因為gtm_gettransactionsnapshot函數依然還是要去周遊所有的目前事務擷取最小事務xmin和快照。
在經過一個禮拜的調整之後,直接采用教科書的方法:使用一個avl樹來存儲gtm_transactioninfo的指針,比較大小的方法就是對比其中的gxid;另外一個使用avl樹來存最小的xmin。簡單來說就是分别以xmin和事務id為主鍵建立兩個類似btree的索引,以滿足查找需求。最終去掉了gtmtransactions.gt_open_transactions,将這兩個耗時的函數的複雜度降低到了o(log(n)),n是目前系統内的事務。
采用前面的測試方法,來擷取tps。和前面的對比圖如下:
可以看到優化之後的gtm響應時間基本維持在o(log(n)),進而使得tps在連接配接數增大時,tps沒有像目前版本下降得這麼劇烈(678個連接配接時,tps是目前版本的幾乎5倍),datanode的資料目錄所在磁盤利用使用率機會基本維持在65%~60%之間。這樣看來,基本上能改善gtm的擴充能力。
當然,這個優化并非是完美的,因為在執行簡單事務并且連接配接數不多的情況下,tps和原有的版本幾乎相同,但是,一旦事務變得複雜,在gtm中停留的時間增大,或是連接配接數增大後同時執行的事務數量多了之後,優化之後的gtm相對現有的gtm會穩定很多。
最後,為了驗證工作,選了三台相同配置的r510,配置和前面所述相同。一台上裝gtm,其他兩台上配置了coordinator和datanode各一個(配置也和前面相同)。
生成了scale為1000的資料,32g,使用前面的測試方法,但是分别執行在一張大表上的簡單主鍵查詢:
1. \setrandom aid 1 :1000
2. select abalance from pgbench_accounts where aid = :aid;
和基于主鍵的更新
sql代碼
1. \setrandom aid 1 10000000
2. \setrandom delta -5000 5000
3. update pgbench_accounts set abalance = abalance + :delta where aid = :aid;
在兩台coordinator同時上執行。
最終, 簡單select操作的總qps最大值是45000(每個coordinator上的qps是22500),簡單update操作的總 tps最大值是 35000(每個coordinator上最大的qps是17500)。碰巧的是獲得最大值時,每台coordinator上執行pgbench的連接配接數都是64。而随着連接配接數增大到一定程度,優化之後的gtm會比目前的gtm 結果高50%以上。
從測試看來,在64個連接配接數的情況,增大一個伺服器(部署上datanode和coordinator),可以增加17500的tps,網絡流量增加20mb/s。是以如果真的想達到10w的tps,預計需要10台左右這樣的伺服器用來部署coordinator和datanode。
但是我們需要解決兩個問題:一方面,我們需要子網的交換機來提供200mb/s以上的帶寬連接配接各個伺服器,這是首先必須解決的問題,當然也是比較容易解決的問題;另外一方面,對于gtm來說,我們必須采用類似的優化來增大gtm的可擴充能力,因為如果每個coordinator上使用64個連接配接,那麼對于10台的叢集來說,系統内操作的連接配接數就是640了,如果還采用目前的gtm,tps qps會急劇下降,這是根本沒法做到的。
<b>本文來自雲栖社群合作夥伴“dbgeek”</b>