Apr3

Berkeley DB 由浅入深【转自架构师杨建】

Author: 杨建  Click: 22051   Date: 2010.04.03 @ 15:39:22 pm Category: 数据库
在网上看到不少介绍Berkeley DB的文章,几乎所有的中文文章都是介绍完入门就再也没了。大都是个概括。最近做这个,所以想系统的由浅入深的介绍一下。不清楚的地方可以和我讨论,或参照官方网站sleepycat上的文档。我用的是最新版本 db-4.4.16.NC.tar.gz,这个包中含有详细的英文文档。
为什么要使用Berkeley DB,它适合什么场合应用?
Berkeley DB并不适合所有的应用,因为简单,专一所以高效。
嵌入式数据库,的“嵌入”是指它内嵌在程序中,而不是说他只应用在嵌入式系统上。它的特点很适合应用于嵌入式系统上。当然在我们的pc机集群或大型服务器上,也可以灵活的配置,完成更艰巨的任务。
它适合于管理海量的,简单的数据。Google用Berkeley DB HA (High Availability) 来管理他们的帐户信息. Motorola在他的无线产品中用Berkeley DB跟踪移动单元。hp,microsoft,Sun Microsystems...等也都是它的大客户。它不能完全取代关系数据库,但在某些方面,它却有他们望尘莫及的高效性。
性能测试,在如下的配置上:
Linux – SuSE Linux 9.1 running on an AMD Athlon 64 processor 3200+ at 1GHz system with 1GB of RAM。
每秒钟,单条记录读操作 1,002,200次。单条记录写操作 766,034次。 用bulk APIs能进行读操作 13,501,800次。当然这些都是发生在内存中的操作,因为bdb使用了cache。  性能测试具体数据可以参考官方网站的Performance Metrics & Benchmarks: Berkeley DB。
Berkeley DB简介
Berkeley DB可以说是一个专为程序员准备的数据库。我的文章中只针对c程序员介绍的。它还支持C++、Java、Perl、Tcl、Python和PHP等。原理和接口都差不多。它的安装很简单。
cd  build_unix
../dist/configure
make
make install
这几步就ok了,其实也就是把头文件和编译好的db库放到指特定位置。甚至可以不用make install,直接在编译你的程序时用-I -L -ldb 指定头文件和连接库的位置。可以完全把它当作一个函数库来用。由db库透明的来完成对数据的管理。无论是系统中的多个进程,或者是相同进程中的多个线程,都可以在同一时间调用访问数据库的函数。而底层的数据加锁、事务日志和存储管理等都在Berkeley DB函数库中实现。他不像传统的数据库那样有client和server,还专门跑几个进程。所以应用程序不需要事先同数据库服务建立起网络连接,而是通过内嵌在程序中的Berkeley DB函数库来完成对数据的保存、查询、修改和删除等操作。
Berkeley DB函数库本身虽然只有300KB左右,但却能够用来管理多达256TB的数据,并且在许多方面的性能还能够同商业级的数据库系统相抗衡。就拿对数据的并发操作来说,Berkeley DB能够很轻松地应付几千个用户同时访问同一个数据库的情况。因而,在资源受限的嵌入式系统上进行数据库管理,Berkeley DB也是一个不错的选择。
Berkeley DB为何高效?
Berkeley DB作为一种嵌入式数据库系统在许多方面有着独特的优势。首先,由于其应用程序和数据库管理系统运行在相同的进程空间当中,进行数据操作时可以避免繁琐的进程间通信包括建立socket连接等,因此耗费在通信上的开销自然也就降低到了极低程度。其次,Berkeley DB使用简单的函数调用接口来完成所有的数据库操作,而不是在数据库系统中经常用到的SQL语言。这样就避免了对结构化查询语言进行解析和处理所需的开销。
基本概念
关键字/数据 是Berkeley DB用来进行数据库管理的基础。每个 Key/Data 对构成一条记录。而整个数据库实际上就是由许多这样的结构单元所构成的。通过使用这种方式,开发人员在使用Berkeley DB提供的API来访问数据库时,只需提供关键字就能够访问到相应的数据。当然也可以也可以提供 Key 和部分Data来查询符合条件的相近数据。
一个例子来完成入门
使用过rdb的人相信都能看的懂下面的例子。简要的说一下下面持续完成的功能。作为一个简单的例子environment部分可以不是必要的,我把它的用法也一起加了进来。创建一个environment指明要把数据库文件创建到哪个目录下面。创建数据库,打开数据库,写一个记录进去,然后读出记录,然后将写入的记录删除,然后关闭environment和数据库。会了这些基本操作,你就可以使用bdb完成简单的应用了。
....................................................................

#include
#include
#include
#include
//only this head should include for use bdb.
#include   
#define DATABASE "yangjian.db"

int main()
{
        DB_ENV *myEnv;
        DB *dbp;
        DBT key, data;
        int ret,t_ret;
        u_int32_t env_flags;
        //........... Create an environment object and initialize it for error reporting
        ret = db_env_create(&myEnv, 0);
        if (ret != 0)
        {
                fprintf(stderr, "Error creating env handle: %s\n", db_strerror(ret));
                return -1;
        }
        //........If the environment does not exist create it. Initialize the in-memory cache.
        env_flags = DB_CREATE | DB_INIT_MPOOL;
        //........Open the environment.
        ret = myEnv->open(myEnv,"/home/yangbin1/yangjian/my/db/testevn",env_flags,0);
        if (ret != 0)
        {
                fprintf(stderr, "Environment open failed: %s", db_strerror(ret));
                return -1;
        }
        if ((ret = db_create(&dbp, myEnv, 0)) != 0)
        {
                fprintf(stderr, "db_create: %s\n", db_strerror(ret));
                exit (1);
        }

        if ((ret = dbp->open(dbp, NULL, DATABASE, NULL, DB_BTREE, DB_CREATE, 0664)) != 0)
        {
                dbp->err(dbp, ret, "%s", DATABASE);
                exit (1);
        }
        memset(&key, 0, sizeof(key));
        memset(&data, 0, sizeof(data)); key.data = "sport";
        key.size = sizeof("sport");
        data.data = "football";
        data.size = sizeof("football");
/*
        //......put data
        if ((ret = dbp->put(dbp, NULL, &key, &data, 0)) == 0)
        {
                printf("db: %s: key stored.\n", (char *)key.data);
        }
         else
        {
                dbp->err(dbp, ret, "DB->put");
        }
*/

        //........put data NOOVERWRITE
        if ((ret = dbp->put(dbp, NULL, &key, &data, DB_NOOVERWRITE)) == 0)
        printf("db: %s: key stored.\n", (char *)key.data);
        else dbp->err(dbp, ret, "DB->put");

        //.......get data
        if ((ret = dbp->get(dbp, NULL, &key, &data, 0)) == 0)
        printf("db: %s: key retrieved: data was %s.\n", (char *)key.data, (char *)data.data);
        else
        dbp->err(dbp, ret, "DB->get");

        //......del data
        if((ret = dbp->del(dbp, NULL, &key, 0)) == 0)
        printf("db: %s: key was deleted.\n", (char *)key.data);
        else
        dbp->err(dbp, ret, "DB->del");

        //.........close, only when the db successful closed,the data can real write to the disk.
        //if ((t_ret = dbp->close(dbp, 0)) != 0 && ret == 0)
        //ret = t_ret;
        //exit(ret);
        if (dbp != NULL)
        dbp->close(dbp, 0);
        //.........close evn
        //........When you are done with an environment, you must close it.
        //........Before you close an environment, make sure you close any opened databases
        if (myEnv != NULL)
        myEnv->close(myEnv, 0);

        return 0;
}


Hash or Btree?

Hash 和 Btree方法应该被用于当逻辑记录号不是用来做主键对数据访问的情况。(如果逻辑记录号是一个secondary key,用来对数据进行访问,Btree方法是一个可能的选择,因为它支持通过一个键和一个记录号来同时的访问。)

Btrees中的键是按一定的秩序来存放的。Btrees应该被用于那些keys存在某种关系的时候。例如用时间做keys,当现在访问8AM时间戳的时候,可能下一个就访问9AM时间戳。也就是在排列顺序中附近的(near)。再比如,用names做keys,我们也许要访问那些有相同last name的,Btrees仍然是一个不错的选择。

在小的数据设置上,Hash 和 Btree在性能表现上没什么差别。在那儿,所有的,或大部分数据设置被放在了cache里面。

尽管如此,当一个一数据设置足够大的时候,会有一些重要的数据页再也装不进cache里了。这种情况下,我们上面讨论的btree在性能表现上就很重要了。
例如,因为在hash中没有排列顺序中附近的机制。所以,cache在Btree中通常比Hash中更有效。Btree方法将产生更少的I/O调用。

尽管如此,当一个数据设置更大的时候,hash访问方法能赢过btree方法。原因是btree比hash数据库包含了更多的元数据页。
数据设置可以变的非常大,以至于元数据开始支配整个cache。如果这种事情发生,Btree将不得不对每次请求都进行一次I/O操作。Cache中几乎没有地方再放置那些真正的数据页了,失去了cache的意义。而因为hash有很少的元数据,可以它的cache照样可以用来放置那些数据页,起到cahche的作用。

当一个数据更更大的时候,以至于每个随机请求,hash和btree几乎都要进行一次I/O操作的时候。在这中情况下,实际上hash只要遍历少树几个内部页(internal pages)就差不多能找到,所以这也是hash在性能上的一个优势。
应用程序对数据的访问式样也极大的影响这些行为。例如,延着光标往下遍历的话,每次I/O操作到cache中的数据,将满足接下来的很多数据请求。

如果数据设置只是比cache大一点,我们还是条件使用Btree,如果你实在有太大的数据设置,hash也许会更好一些。db_stat公用程序是一个有用的工具,用来监视,你的cache表现的怎么样。
总结:
其实到这你应该能看出来,btree是在数据不是很大的时候是很优秀的,在更大的时候,由于元数据占用太多cache的原因,导致性能下降,落后与hash了,而不是说hash能超过它。所以能在元数据占用cache不是太多以前,也就是你的cache足够大,使用btree只最好的选择。当然,如果每次访问的数据都是随机的没有什么次序,也不是near的,那用btree也没什么优势了。

针对我们的应用我只讨论了 Hash or Btree?。Queue or Recno?我就不再讨论了。
选择一个页的大小:
太大了会产生很多不必要的i/o,而且影响并发性,因为Btree, Hash and Recn都是对页上锁。太小了会使用溢出页,大量使用溢出页会严重影响性能。所以一般
页的大小都选择和文件系统的I/O块,大小相等。
选择一个cache大小:
要设置的足够大,至少能满足一次操作的数据。如果你的cache设的太小,每个新页将要强迫换出least-recently-used page。
Berkeley DB将要重新读一次树的root page对于每次数据库请求。当然cache也不是越大越好,当cache大小增长到一个特定的点时,再增加就不会对性能有什么提高了。当到达这个点时,两件事情发生了。Cache足够大以至于,几乎所有的请求都不用再访问磁盘了就能从cache中得到信息。或则是你的应用程序做一些确实很随机的访问,因此再增加cache对于下一个请求也不会有什么性能上的提高了。第二种情况发生的概率很小,因为几乎所有的应用,都显示了一些,请求的相关联性。
如果cache设定的超过了操作系统的能力,将会使用交换分区,频繁换入换出,会很影响性能。


觉得有必要先把DBT结构放在这。方便后面看。
typedef struct {
 void *data;
 u_int32_t size;
 u_int32_t ulen;
 u_int32_t dlen;
 u_int32_t doff;
 u_int32_t flags;
} DBT;
1. 数据对齐
Berkeley DB没有为以DBT为参数的,返回的data/key对,或回调函数的字节对齐提供任何保证。
应用程序有责任对齐任何需要对齐的。DB_DBT_MALLOC, DB_DBT_REALLOC 和 DB_DBT_USERMEM标志可能被用来对齐存储在内存中的返回项。

2. 在bulk中取回数据
当从数据库中取回大量记录的时候,那些方法调用经常影响性能。Berkeley DB提供bulk取数据接口,它能有效的提高一些应用持续的性能要使用bulk,必须先为DB->get或DBcursor->c_get指定一个buffer。这个在c api中的实现是通过设置DBT结构的data和ulen域还有flag域被设为DB_DBT_USERMEM来引用应用程序的buffer。DB_MULTIPLE或DB_MULTIPLE_KEY 需要指定给DB->get或 DBcursor->c_get方法, 以使多条记录被返回到指定的buffer中。这两个标志的区别请看手册。
下面函数只看红色标出部分就可以了。示范如何使用bulk。
...................................................................................
int rec_display(DB *dbp)
{
 DBC *dbcp;
 DBT key, data;
 size_t retklen, retdlen;
 char *retkey, *retdata;
 int ret, t_ret;
 void *p;
 memset(&key, 0, sizeof(key));
 memset(&data, 0, sizeof(data));
 /* Review the database in 5MB chunks. */
#define BUFFER_LENGTH (5 * 1024 * 1024)
 if ((data.data = malloc(BUFFER_LENGTH)) == NULL)
  return (errno);
 data.ulen = BUFFER_LENGTH;
 data.flags = DB_DBT_USERMEM;
 /* Acquire a cursor for the database. */
 if ((ret = dbp->cursor(dbp, NULL, &dbcp, 0)) != 0) {
  dbp->err(dbp, ret, "DB->cursor");
  free(data.data);
  return (ret);
 }
 for (;;) {
  /*
   * Acquire the next set of key/data pairs.  This code does
   * not handle single key/data pairs that won't fit in a
   * BUFFER_LENGTH size buffer, instead returning DB_BUFFER_SMALL
   * to our caller.
   */
  if ((ret = dbcp->c_get(dbcp,
      &key, &data, DB_MULTIPLE_KEY | DB_NEXT)) != 0) {
   if (ret != DB_NOTFOUND)
    dbp->err(dbp, ret, "DBcursor->c_get");
   break;
  }
  for (DB_MULTIPLE_INIT(p, &data);;) {
   DB_MULTIPLE_KEY_NEXT(p,
       &data, retkey, retklen, retdata, retdlen);
   if (p == NULL)
    break;
   printf("key: %.*s, data: %.*s\n",
       (int)retklen, retkey, (int)retdlen, retdata);
  }
 }
 if ((t_ret = dbcp->c_close(dbcp)) != 0) {
  dbp->err(dbp, ret, "DBcursor->close");
  if (ret == 0)
   ret = t_ret;
 }
 free(data.data);
 return (ret);
}
................................................................................................
3. 记录的部分的存储和取回
在Berkeley DB的访问方法中,可以只存储或取回数据项的某一部分。这个通过设置DBT结构的DB_DBT_PARTIAL 标志来实现。
同时还要设置DBT的其他几个值。
doff 数据开始处
dlen 数据长度
例如,如果数据项是ABCDEFGHIJKL, doff的值为3是指从字节D开始。dlen为4,是指随后的4个字节DEFG。
取回记录:
当从一个数据库中取回一个数据项时,从doff位置开始的dlen字节,被返回。如果被指定的那些字节不存在,其他存在的字节将被返回。
存储记录:
下面的例子初始化数据项字节长度都是20: ABCDEFGHIJ0123456789
1,
size = 20
doff = 0
dlen = 20
data = abcdefghijabcdefghij
Result: The 20 bytes at offset 0 are replaced by the 20 bytes of data;
that is, the entire record is replaced.
ABCDEFGHIJ0123456789 -> abcdefghijabcdefghij
2,
size = 10
doff = 2
dlen = 15
data = abcdefghij
Result: The 15 bytes at offset 2 are replaced by the 10 bytes of data.
ABCDEFGHIJ0123456789 -> ABabcdefghij789
2,
size = 10
doff = 25
dlen = 0
data = abcdefghij
Result: The 0 bytes at offset 25 are replaced by the 10 bytes of data;
that is, 10 bytes are inserted into the record past the end of the
current data (\0 represents a nul byte).
ABCDEFGHIJ0123456789 -> ABCDEFGHIJ0123456789\0\0\0\0\0abcdefghij