berkeley db--進階特性分析

轉至: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_LOCKDB_INIT_TXNDB_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的具體工作機制。

參考:Berkeley DB 產品對比

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隨時可能發生,見圖:

Btree deadlock

死鎖檢測(原理是遍歷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

可以分別設置鎖和事務的超時:

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命中。

發佈了43 篇原創文章 · 獲贊 138 · 訪問量 70萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章