轉至:http://blog.jqian.net/post/berkeley-db.html
數據存儲
Berkeley DB的數據存儲可以抽象爲一張表,其中第一列是key,剩餘的n-1列(fields)是value。
BDB訪問數據庫的方式,或者套用MySQL數據庫的說法是存儲引擎,有四種:
- Btree 數據保存在平衡樹裏,key和value都可以是任意類型,並且可以有duplicated keys
- Hash 數據保存在散列表裏,key和value都可以是任意類型,並且可以有duplicated keys
- Queue 數據以固定長度的record保存在隊列裏,key是一個邏輯序號。這種訪問方式可以快速在隊列尾插入數據,然後從隊列頭讀取數據。它的優點在於可以提供record級別的鎖機制,當需要併發訪問隊列的時候,可以提供很好性能。
- Recno 這種訪問方式類似於Queue,但它可以提供變長的record。
BDB的數據容量是256TB,單個的key或value可以保存4GB數據。
BDB是爲併發訪問設計的,thread-safe,且良好的支持多進程訪問。
少量或者中量數據都建議使用BTREE,尤其併發的場景下,BTREE支持 lock coupling 技術,可以提升併發性能。
BDB組成
Berkeley DB內含多個獨立的子系統:
- Locking subsystem
- Logging subsystem
- Memory Pool subsystem
- Transaction subsystem
一般使用的時候,這些子系統都被整合在DB environment裏,但它們也單獨拿出來,配合BDB之外的數據結構使用。
所謂DB Environment就是一個目錄,其中保存着Locking、Logging、Memory Pool等子系統的信息,不同的thread可以打開同一個目錄讀寫DB environment,BDB通過這種方式實現多進程/線程共享數據庫。
【注意】多進程共享一個環境時,必須要使用 DB_SYSTEM_MEM
,否則無法正常初始化環境。
關於DB environment的設置很多,一般沒必要全部在代碼裏設置,也可以使用名爲 DB_CONFIG 的配置文件來設置,該文件默認位於環境目錄。
Concurrent Data Store (CDS)
CDS適用於多讀單寫的應用場景,當使用CDS的時候,僅需要 DB_INIT_MPOOL | DB_INIT_CDB
這兩個子系統,不應該啓用任何其他子系統,比如DB_INIT_LOCK
、DB_INIT_TXN
、DB_RECOVER
等。
由於CDS並不啓動lock子系統,所以使用CDS無需檢查deadlock,但下面的幾種情況會導致線程永遠阻塞:
- 混用DB handle和cursor(此時同一thread會有兩個locker競爭)。
- 當打開一個write cursor的時候,在同一個線程裏有其他的cursor開啓。
- 不檢查BDB的錯誤碼(當一個cursor錯誤返回時,必須關閉這個cursor)。
其實CDS和DS的唯一區別就在於,當要寫db的時候,應該使用DB_WRITECURSOR創建一個write cursor。當這樣的write cursor 存在的時候,其他試圖創建 write cursor 的線程將被阻塞,直到該 write cursor被關閉。當write cursor存在的時候,read cursor不會被阻塞;但是,所有實際的寫操作,包括直接調用DB->put()或者DB->del()都將被阻塞,直到所有的read cursor關閉,纔會真正的寫入db。這就是multiple-reader/single-writer的具體工作機制。
CDS中的注意事項
如果使用secondary database,意味着會在同一個cursor下操作兩個db,此時如果用CDS,也許必須設置DB_CDB_ALLDB,但這會嚴重影響性能。
所謂 DB_CDB_ALLDB
是一個非常粗粒度的鎖,CDS的鎖基於API-layer,默認per-database,但如果設置了DB_CDB_ALL
,則是per-environment,這意味着:
- 整個DB environment下只能有一個write cursor。
- 當寫db的時候,整個DB environment下任何read cursor不可以打開。
讀寫CDS簡單的做法是能用DB handle的地方直接使用DB handle,沒有必要使用CURSOR handle,因爲你用DB->put()或者DB->del()來修改數據庫時,它內部也是調用了CURSOR handle。當然,如果你要使用CURSOR遍歷數據庫時,用於寫的CURSOR必須設置DB_WRITECURSOR來創建:
DB->cursor(db, NULL, &dbc, DB_WRITECURSOR);
直接調用DB->put()
或者DB->del()
,或者先使用DB_WRITECURSOR
創建CURSOR handle,最終都進入__db_cursor()
函數,設置db_lockmode_t mode = DB_LOCK_IWRITE
,然後用該mode
加鎖。但需要注意的是,不能在同一thread下混用DB和CURSOR handle,因爲每個CURSOR會分配一個LOCKER,而DB
handle也會分配一個LOCKER,兩者可能導致self-deadlock。
如果在read lock或者write lock過程中,程序崩潰,這可能導致lock遺留在env中無法釋放(可以用db_stat -CA
觀察到),這種情況下該environment已經損壞,只能刪除該environment(刪除掉__db.001之類的文件即可),重新創建。
Transactional Data Store (TDS)
TDS是使用BDB的終極方式,它適用於多讀多寫,並且支持Recoveriablity等任何你能想到的常見數據庫特性,或者不如說,只有當你確定需要這些特性的時候,你才應該使用BDB;如果你僅僅想要一個單純的KV系統,那也許BDB並不適合你。
一般來說,創建TDS Environment的flag如下:
DB_CREATE | DB_INIT_MPOOL | DB_INIT_LOCK | DB_INIT_LOG | DB_INIT_TXN
TDS的任何DB相關的操作都必須是事務性的,包括打開db時,都需要先創建txn:
DB_TXN* txn; int ret = env->txn_begin(env, NULL, &txn, 0); ret = db->open(db, txn, "test.db", NULL, DB_BTREE, DB_CREATE, 0); // 如果使用secondary database, 則associate()調用也需要包含在txn裏 ret = db->get(db, txn, &key, &val, 0); ret = db->put(db, txn, &key, &val, 0); if(ret) txn->abort(txn); else txn->commit(txn, 0);
如果僅僅有讀操作,其實可以無需調用commit,直接abort即可。
如果使用 DB_AUTO_COMMIT 打開db,則關於db handle的操作,不需要額外指定txn參數,此時使用了BDB的autocommit特性。
Write Ahead Logging
WAL是很多事務性數據庫使用的技術,即在數據實際寫入到數據庫文件之前,先記錄log,一旦log被寫入到log文件,即認爲該事務完成(並不會等待數據實際寫入到數據庫文件)。
這是因爲log的寫入始終是順序寫到文件末尾的,這比實際數據寫入數據庫文件(隨機寫入文件)要快2個數量級。
清理無用log的辦法:
- 使用命令
db_archive -d
- 調用
ENV->set_flags
設置DB_LOG_AUTOREMOVE
Deadlock
使用TDS時,死鎖原則上無法避免:
- 兩個進程互相等待一塊被對方鎖住的資源則會發生死鎖
- 甚至單一進程內試圖獲取一個已經被不同locker獲取過的lock,也會發生死鎖
採用BTREE/HASH訪問方式下,併發操作時,無法避免死鎖,因爲page splits隨時可能發生,見圖:
死鎖檢測(原理是遍歷wait-for圖,發現環;如果有環出現,則打破它):
- 同步檢測 DB_ENV->set_lk_detect(),在每個阻塞的鎖上檢測,好處是立即發現,壞處是cpu佔用略高(insignificant)
- 異步檢測 DN_ENV->lock_detect() 或者 db_deadlock,需要額外發起一個進程或線程,壞處是隻有當運行該命令時才能檢測,好處是cpu佔用低
一般解決死鎖的辦法:同步檢測 + 異步檢測 + 設置鎖超時
當environment沒有被損壞時,可以使用 db_stat -Cl
查看死鎖情況。
Degree isolation
degree 2 isolation 保證事務讀到已經COMMIT的數據,但是在該讀事務結束之前,其他事務可以修改該記錄。degree 2 isolation適用於長時間的讀取事務,比如遍歷數據庫等。
使用辦法:使用
DB_TXN->txn_begin(),
DB->get(),
DBC->get() 等函數時,設置參數 DB_READ_COMMITTED
。
區別於degree 3 isolation,後者保證在一個讀事務內,無論讀取多少遍,都可以讀到同樣的記錄。但這會拒絕該記錄的任何寫事務。所謂degree 1 isolation則更進一步,可以讀取未COMMIT的數據,建議謹慎使用,容易導致數據不一致。
性能調優和參數設置
lock table size
lock table的大小依賴於以下三個參數:
- lock最大數量:ENV->set_lk_max_locks() 同時可以請求的鎖的最大值,比如同時2進程併發,要鎖11個對象,則需要2x11個鎖。
- locker最大數量:ENV->set_lk_max_lockers() 同時發起鎖請求的最大值,比如同時2進程併發,則最多2個locker。
- lock object最大數量:ENV->set_lk_max_objects() 同時需要鎖住的object的最大值,比如同時2進程併發,如果5層BTREE,則需要鎖住2x5=10個對象,此外再加上單獨的DB handle。
實際上面的計算得到的最大值還要double,因爲如果開啓deadlock檢測,對每個locker來說BDB會新增一個dd locker,用於檢測死鎖。
timeout
可以分別設置鎖和事務的超時:
- ENV->set_timeout() 設置鎖和事務的默認超時
- DB_TXN->set_timeout() 單獨設置事務的超時
cachesize
使用db_stat查看cache命中情況:
$ db_stat -h var -m 125MB 8KB Total cache size 1410M Requested pages found in the cache (99%) 14 Requested pages not found in the cache
建議根據程序設置合理的cachesize,儘量保證所有數據都可以被cache命中。