BerkeleyDB(버클리 디비, BDB)는 1992년경 UCB에서 개발한 임베디드 DB다. 유닉스 프로그램에서 보통 DB라 하면 이 버클리 DB를 말할정도로 대중적인 임베디드 데이터베이스이다. 1996년 SleepyCat 이라는 법인을 설립해 소스를 관리했으며, SleepyCat은 2006년 2월 오라클에 합병되었다. 사용 하기도 쉽고, 성능도 좋은데다, 버그 없이 잘 구현되어있어 널리 사용되고 있다. 비슷한 임베디드 DB로는 SQLite가 있다. SQLite에 대해서는 윈디하나의 솔라나라: SQLite를 참고하자.
BerkeleyDB 이전에는 켄 톰슨이 개발한 1979년 배포한 DBM 이나 DBM을 개선한 NDBM이 사용되고 있었으며 BerkeleyDB 는 NDBM 의 대체품정도로 인식되었다고 한다. 요즘은 NDBM 보다 GDBM이 주로 사용된다. DBM에 대한 설명은 윈디하나의 솔라나라: DBM, NDBM, GDBM(작성중)을 읽어보자.
이후 BerkeleyDB 를 개선한 WiredTiger가 개발되었고, WiredTiger는 2014년 12월 MongoDB에 인수되어 MongoDB 3.2 이후부터 저장엔진으로 사용되고 있다.
DB라고는 하지만, db-5.1 까지만 해도 SQL과 같은 쿼리 언어를 지원하지 않았고, 테이블 개념도 없었다가 Oracle Berkely DB 11g Release 2 버전부터 SQL과 SQLite API 를 지원하게 되었다. 또한 락이나 트랜잭션을 지원하며 Join나 Replication, In-Memory 데이터베이스 기능도 지원한다. BerkeleyDB에 SQL 분석기를 붙이고 인덱스 구성 가능하게 만든게 초창기의 MySQL 이기도 하다.
BerkeleyDB의 장점중 하나는 이식성이다. C/C++/C#/STL/TCL/JAVA등의 다양한 언어와 다양한 운영체제에서 임베드 되어 사용할 수 있다. C/C++를 지원하기 때문이 이 기반의 다른 언어들, Ruby나 Python, PHP등의 언어도 BerkeleyDB API를 제공하고 있다. Java 언어를 위한 BerkeleyDB Java와 XML-DB지원을 위한 BerkeleyDB XML도 나와있다.
이 문서에서는 BerkeleyDB의 설치와 간단한 프로그램 예제 그리고 BerkeleyDB를 사용하다 발생하는 문제 해결 방법을 다룬다.
db_checkpoint: 데이터 베이스 체크포인트및 모니터에 사용되는 데몬. 프로그램 내부에서 쓰레드를 생성해 txn_checkpoint 를 호출하는 방법 대신 사용할 수 있다.
db_convert: 데이터 베이스 파일의 엔디안을 변경할 수 있는 툴
db_deadlock: 대드락이 감지되었을 때, 락을 제거하기 위해 사용하는 데몬
db_dump: 데이터 베이스 파일을 db_load에서 사용할 수 있는 텍스트 형식의 파일로 변환
db_hotbackup: hot backup 또는 hot failover 스냅샷을 생성
db_load: db_dump 로 덤프된 파일을 이용해 데이터 베이스 생성
db_log_verify: 로그 파일 검증
db_printlog: 데이터베이스 로그 파일을 읽을 수 있도록 변환
db_recover: 트랜잭션의 비정상 종료로 인해 깨진 데이터베이스 파일을 복구. 스냅샷이나 아카이브가 있을때 -c옵션을 붙여 실행하면 쉽게 복구된다.
db_replicate: 트랜잭션 환경에서 Replicatio/HA 서비스를 제공해주는 데몬
db_stat: 데이터베이스 파일의 통계를 표시
db_tuner: 이진트리 데이터베이스를 분석하고 압축
db_upgrade: 데이터 베이스 파일을 업그레이드
db_verify: 데이터 베이스 파일 무결성 검사
dbsql: libdb_sql 의 CLI
sqlite3: SQLite 3 클라이언트
아래의 유틸리티 사용 예제에서는, berkeleydb.db 파일을 예로 들었는데, 이 파일은 문서 하단의 berkeleydb.c 를 컴파일 한 후 실행시키면 생성된다. (BerkeleyDB는 샘플 데이터 베이스를 제공하지 않는다)
db_stat
데이터 파일의 상태를 보여준다.
root@wl ~ # /usr/local/db/bin/db_stat -d berkeleydb.db
Thu Aug 26 00:00:00 2010 Local time
53162 Btree magic number
9 Btree version number
Little-endian Byte order
Flags
2 Minimum keys per-page
16384 Underlying database page size
4079 Overflow key/data size
1 Number of levels in the tree
3 Number of unique keys in the tree
3 Number of data items in the tree
0 Number of tree internal pages
0 Number of bytes free in tree internal pages (0% ff)
1 Number of tree leaf pages
16298 Number of bytes free in tree leaf pages (0% ff)
0 Number of tree duplicate pages
0 Number of bytes free in tree duplicate pages (0% ff)
0 Number of tree overflow pages
0 Number of bytes free in tree overflow pages (0% ff)
0 Number of empty pages
0 Number of pages on the free list
db_verify
데이터 파일을 검증할 수 있는 유틸리티다.
root@wl ~ # /usr/local/db/bin/db_verify berkeleydb.db
db_verify: Page 1: btree leaf page has incorrect level 221
db_verify: Page 1: bad HOFFSET 16605, appears to be 16384
db_verify: Page 1: item order check unsafe: skipping
db_verify: berkeleydb.db: DB_VERIFY_BAD: Database verification failed
db_archive, db_checkpoint, db_recover
주) 테스트 못해봄 ^^
root@wl ~ # /usr/local/db/bin/db_recover
root@wl ~ # ls -alh
-rw-r--r-- 1 root root 32K 8월 26일 00:00 berkeleydb.db
-rw-r----- 1 root root 10M 8월 26일 00:00 log.0000000001
db_convert
DB 파일의 엔디안(endianness)을 변경할 수 있는 툴이다. 자세한 설명은 생략한다.
접근 방식(Access Method): BerkeleyDB는 데이터 베이스 액세스에 4가지 메쏘드를 제공한다. 각각은 다음과 같다. 접근 방식이라고 번역했지마 사실상 '저장 방식'이라고 하는 것이 이해하기 쉬울 것이다.
BTree: 데이터는 B-Tree구조로 정렬되어 저장된다. 복합 데이터도 키가 될 수 있다.
Hash: 데이터는 확장된 선형 해시 테이블로 저장된다. 복합 데이터도 키가 될 수 있다.
Queue: 데이터는 고정 크기의 레코드로 된 큐에 저장된다. 이 형식은 큐의 마지막에 빠르게 삽입하기 위해 개발되었다. 동시성이 큰 어플리케이션에 적용하면 좋다. 논리 레코드 번호가 키가 된다.
Recno: 데이터는 가변 크기의 레코드에 저장된다. Queue의 단점을 보완했지만, 가변인만큼 부하가 있다. Queue처럼 논리 레코드 번호가 키가 된다.
접근 방식의 선택
적은 양의 데이터로 인해 메모리에 적재된 경우, BTree와 Hash의 성능 차이는 없지만, 많은 데이터로 인해 디스크에 저장된 경우에는 차이를 보인다. 대부분의 경우 BTree여도 상관 없겠지만, 매우 큰 데이터 셋인 경우 Hash도 고려해봐야 한다.
병렬 작업이 많은 경우 Queue나 Recno가 적합하다.
berkeleydb.c
(6,222 바이트)
/*
BerkeleyDB Sample Code
WindyHana's Solanara: BerkeleyDB http://www.solanara.net/solanara/berkeleydb
cc -R/usr/local/db/lib -L/usr/local/db/lib -ldb -I/usr/local/db/include -o berkeleydb berkeleydb.c
*/
#include <sys/types.h>
#include <sys/stat.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "db.h"
#define DBFILE "berkeleydb.db"
#define ENV_DIRECTORY "./"
DB_ENV* dbenv;
DB* dbp;
//DB_MPOOL_STAT* dbmpoolstat;
int putData(char * key, char * val) {
DBT dbkey, dbval;
memset(&dbkey, 0, sizeof(dbkey));
memset(&dbval, 0, sizeof(dbval));
dbkey.data = key;
dbkey.size = sizeof(key);
dbval.data = val;
dbval.size = sizeof(val);
return dbp->put(dbp, NULL, &dbkey, &dbval, 0);
}
int genSampleData() {
putData("apple", "red");
putData("banana", "yellow");
putData("grape", "purple");
}
void env_open(DB_ENV **dbenvp) {
DB_ENV *dbenv;
int flags;
int ret;
/* Create the environment handle. */
if ((ret = db_env_create(&dbenv, 0)) != 0) {
fprintf(stderr, "txnapp: db_env_create: %s\n", db_strerror(ret));
exit (1);
}
/* Set up error handling. */
dbenv->set_errpfx(dbenv, "데이터베이스 정보/오류");
/* Do deadlock detection internally. */
if ((ret = dbenv->set_lk_detect(dbenv, DB_LOCK_DEFAULT)) != 0) {
dbenv->err(dbenv, ret, "set_lk_detect: DB_LOCK_DEFAULT");
exit (1);
}
/* * Open a transactional environment: * create if it doesn't exist * free-threaded handle * run recovery * read/write owner only */
flags = DB_CREATE | DB_INIT_LOCK | DB_INIT_LOG | DB_INIT_MPOOL | DB_INIT_TXN | DB_RECOVER | DB_THREAD;
if ((ret = dbenv->open(dbenv, ENV_DIRECTORY, flags, S_IRUSR | S_IWUSR)) != 0) {
dbenv->err(dbenv, ret, "dbenv->open: %s", ENV_DIRECTORY);
exit (1);
}
*dbenvp = dbenv;
}
void * checkpoint_thread(void *arg) {
DB_ENV *dbenv;
int ret;
pthread_t ptid;
dbenv = arg;
printf("체크포인트 쓰레드(%lu) ", (u_long)pthread_self());
/* Checkpoint once a minute. */
for (;; sleep(60)) if ((ret = dbenv->txn_checkpoint(dbenv, 0, 0, 0)) != 0) {
dbenv->err(dbenv, ret, "checkpoint thread");
exit (1);
}
}
/* 필요 없는 로그 파일 삭제 */
void * logfile_thread(void *arg) {
DB_ENV *dbenv;
int ret;
char **begin, **list;
dbenv = arg;
printf("로그 삭제 쓰레드(%lu) ", (u_long)pthread_self());
/* Check once every 5 minutes. */
for (;; sleep(300)) {
/* Get the list of log files. */
if ((ret = dbenv->log_archive(dbenv, &list, DB_ARCH_ABS)) != 0) {
dbenv->err(dbenv, ret, "DB_ENV->log_archive");
exit (1);
}
/* Remove the log files. */
if (list != NULL) {
for (begin = list; *list != NULL; ++list) {
if ((ret = remove(*list)) != 0) {
dbenv->err(dbenv, ret, "remove %s", *list);
exit (1);
}
}
free (begin);
}
}
}
/* 사용하는 로그 파일 보임 */
void log_archlist(DB_ENV *dbenv)
{
int ret;
char **begin, **list;
/* Get the list of database files. */
if ((ret = dbenv->log_archive(dbenv, &list, DB_ARCH_ABS | DB_ARCH_DATA)) != 0) {
dbenv->err(dbenv, ret, "DB_ENV->log_archive: DB_ARCH_DATA");
exit (1);
}
if (list != NULL) {
for (begin = list; *list != NULL; ++list) printf("database file: %s\n", *list);
free (begin);
}
/* Get the list of log files. */
if ((ret = dbenv->log_archive(dbenv, &list, DB_ARCH_ABS | DB_ARCH_LOG)) != 0) {
dbenv->err(dbenv, ret, "DB_ENV->log_archive: DB_ARCH_LOG");
exit (1);
}
if (list != NULL) {
for (begin = list; *list != NULL; ++list) printf("데이터베이스 로그파일: %s\n", *list);
free (begin);
}
}
int main() {
DBT key, val;
int ret;
pthread_t ptid;
printf("데이터베이스 환경 오픈\n");
env_open(&dbenv);
printf("데이터베이스 체크 포인트 쓰레드 시작\n");
if ((ret = pthread_create(&ptid, NULL, checkpoint_thread, (void *)dbenv)) != 0) {
fprintf(stderr, "checkpoint: failed spawning checkpoint thread: %s\n", db_strerror(ret));
exit (1);
}
fflush(stdout); printf("데이터베이스 로그 삭제 쓰레드 시작\n");
if ((ret = pthread_create(&ptid, NULL, logfile_thread, (void *)dbenv)) != 0) {
fprintf(stderr, "txnapp: failed spawning log file removal thread: %s\n", db_strerror(ret));
exit (1);
}
printf("데이터베이스 생성 (버전: %s)\n", DB_VERSION_STRING);
if ((ret = db_create(&dbp, dbenv, 0)) != 0) {
fprintf(stderr, "db_create: %s\n", db_strerror(ret));
exit(1);
}
printf("데이터 베이스 오픈(%s): ", DBFILE);
if ((ret = dbp->open(dbp, NULL, DBFILE, NULL, DB_BTREE, DB_CREATE, 0664)) != 0) {
printf("Fail - "); fflush(stdout); dbp->err(dbp, ret, "db->open(%s)", DBFILE);
goto done;
}
printf("성공\n");
memset(&key, 0, sizeof(key));
memset(&val, 0, sizeof(val));
key.data = "key1";
key.size = sizeof("key1");
val.data = "val1";
val.size = sizeof("val1");
printf("데이터 넣기 (%s, %s): ", key.data, val.data);
if ((ret = dbp->put(dbp, NULL, &key, &val, 0)) != 0) {
printf("Fail - "); fflush(stdout); dbp->err(dbp, ret, "db->put");
goto done;
}
printf("성공\n");
memset(&val, 0, sizeof(val));
printf("데이터 얻기 (%s): ", key.data);
key.flags = DB_DBT_REALLOC;
val.flags = DB_DBT_REALLOC;
if ((ret = dbp->get(dbp, NULL, &key, &val, 0)) != 0) {
printf("Fail - "); fflush(stdout); dbp->err(dbp, ret, "db->get");
goto done;
}
printf("성공 - %s\n", val.data);
printf("데이터 삭제 (키: %s): ", key.data);
if ((ret = dbp->del(dbp, NULL, &key, 0)) != 0) {
printf("Fail - "); fflush(stdout); dbp->err(dbp, ret, "DB->del");
goto done;
}
printf("성공\n");
printf("삭제된 데이터 얻기(실패해야함): ", key.data);
if ((ret = dbp->get(dbp, NULL, &key, &val, 0)) != 0) {
printf("Fail - "); fflush(stdout); dbp->err(dbp, ret, "DB->get");
goto done;
}
printf("성공\n"); // Cannot be reached.
done:
genSampleData();
printf("샘플 데이터 삽입됨.\n");
log_archlist(dbenv);
if (dbp != NULL) { ret = dbp->close(dbp, 0); }
if (dbenv != NULL) { ret = dbenv->close(dbenv, 0); }
printf("데이터 베이스 닫음.\n");
exit(ret);
}
# ./berkeleydb
데이터베이스 환경 오픈
데이터베이스 체크 포인트 쓰레드 시작
데이터베이스 로그 삭제 쓰레드 시작
데이터베이스 생성 (버전: Berkeley DB 6.2.32: (April 5, 2017))
로그 삭제 쓰레드(3) 데이터 베이스 오픈(berkeleydb.db): 체크포인트 쓰레드(2) 성공
데이터 넣기 (key1, val1): 성공
데이터 얻기 (key1): 성공 - val1
데이터 삭제 (키: key1): 성공
삭제된 데이터 얻기(실패해야함): Fail - 데이터베이스 정보/오류: DB->get: BDB0073 DB_NOTFOUND: No matching key/data pair found
샘플 데이터 삽입됨.
데이터베이스 로그파일: /root/./log.0000000001
데이터 베이스 닫음.
# ls -al
# ll
...
-rw------- 1 windy staff 184K 9월 26일 14:57 __db.001
-rw------- 1 windy staff 32K 9월 26일 14:57 __db.002
-rw------- 1 windy staff 48K 9월 26일 14:57 __db.003
-rwxr-xr-x 1 windy staff 18K 9월 26일 14:55 berkeleydb*
-rw-r--r-- 1 windy staff 6.1K 2014년 9월 15일 berkeleydb.c
-rw-r--r-- 1 windy staff 32K 9월 26일 14:57 berkeleydb.db
-rw------- 1 windy staff 10M 9월 26일 14:57 log.0000000001
SQLite
SQLite 에 대한 대체 라이브러리(Drop-in Replacement)를 제공한다. (BerkeleyDB에서 SQL을 구현할때 SQLite 와 호환되도록 만든것으로 알고 있다) 이를 이용해 SQLite 로 작성된 프로그램은 소스 수정 없이 BerkeleyDB를 사용하도록 할 수 있다. 예제 소스코드는 윈디하나의 솔라나라: SQLite - C 예제를 수정없이 사용했다.
windy@wl ~ $ wget http://www.solanara.net/contents/includes/sqlite_init.sql
windy@wl ~ $ /usr/local/db/bin/sqlite3 userdb.sqlite ".read sqlite_init.sql" 1)
SQLite 3.18.2
0|0|0|SEARCH TABLE authinfo AS a USING COVERING INDEX sqlite_autoindex_authinfo_1 (userid=?)
0|1|1|SEARCH TABLE userinfo AS u USING INTEGER PRIMARY KEY (rowid=?)
root|루트|010-111-1111|1.0|2019-05-16 01:59:28
windy|윈디하나|010-000-0000|1.0|2019-05-16 01:59:28
2019-05-16 01:59:28|2
Error: near line 49: no such table: dbstat
windy@wl ~ $ wget http://www.solanara.net/contents/includes/sqlite.c
windy@wl ~ $ cc -R/usr/local/db/lib -L/usr/local/db/lib -lsqlite3 -lmd5 -m64 -lnsl -lsocket -I/usr/local/db/include -o sqlite sqlite.c 2)
windy@wl ~ $ ./sqlite
SQLite 3.18.2
SQLite Lib 3.18.2
SQLITE_LIMIT_LENGTH = 1000000000
SQLITE_LIMIT_COLUMN = 2000
SQLITE_LIMIT_EXPR_DEPTH = 1000
SQLITE_LIMIT_COMPOUND_SELECT = 500
SQLITE_LIMIT_VDBE_OP = 250000000
SQLITE_LIMIT_FUNCTION_ARG = 127
SQLITE_LIMIT_ATTACHED = 10
SQLITE_LIMIT_LIKE_PATTERN_LENGTH = 50000
SQLITE_LIMIT_VARIABLE_NUMBER = 999
SQLITE_LIMIT_TRIGGER_DEPTH = 1000
SQL: select md5('한글') "한글(UTF-8)MD5해시"
한글(UTF-8)MD5해시
1 52b8c54ab4ea672ee6cdfdfef0a31db4
.......SQL: select first(username) fun from userinfo
fun
1 루트
.SQL: select * from userlist
userid username tel1 grade rdate
1 root 루트 010-111-1111 1.0 2019-05-16 01:59:28
.2 windy 윈디하나 010-000-0000 1.0 2019-05-16 01:59:28
1) BerkeleyDB에 설치된 SQLite 라이브러리가 아닌 시스템에 설치된 SQLite 라이브러리가 로드된 경우 아래와 같이 SQLite header and source version mismatch 메시지가 나온다
# /usr/local/db/bin/sqlite3
SQLite header and source version mismatch
2019-04-16 19:49:53 884b4b7e502b4e991677b53971277adfaf0a04a284f8e483e2553d0f83156b50
2017-06-17 09:59:36 036ebf729e4b21035d7f4f8e35a6f705e6bf99887889e2dc14ebf2242e7930dd
이런 경우 LD_LIBRARY_PATH에 정의되어있는 라이브러리 검색 디렉토리 순서를, 시스템에 설치되어있는 SQLite 라이브러리 디렉토리 보다 BerkeleyDB 에 설치된 SQLite 라이브러리 경로를 우선하게 조절하면 해결할 수 있다. 2) SQLite 는 소켓 라이브러리를 필요로 하지 않지만, BerlekenyDB 는 기본적으로 소켓 라이브러리를 필요로 한다. 소스는 수정 안해도 되지만, 빌드시 소켓 라이브러리 부분은 추가해야 한다. 3) SQLite 헤더는 3.18.2 이지만 SQLite Lib 는 시스템에 설치된 라이브러리 버전이 표시될 수 있다. 따라서 헤더 버전과 라이브러리 버전이 다를 수 있다. 윈디하나의 솔라나라: SQLite - 설치대로 설치하고 실행해보면 달리 나올 것이다.