Monthly Archives: June 2008

Berkeley DB 启用事务下的应用架构设计

当构建基于事务的数据存储应用时,需要考虑相关的应用启动和系统以及应用的失效。 数据库环境下的恢复是一个单线程的过程。一个要进行恢复的进程或者线程必须在任何其它的thread of control在开始操作数据库环境之前完成。 进行恢复首先标记任何存在的数据库环境为失效然后删除这环境,使得运行在数据库中的thread of control检测到失效并向应用返回。 在删除一个可能在被任何一个其它的线程所使用数据库环境中(作为恢复的一部分)的问题是Berkeley DB library所使用的互斥量类型。当使用test and set互斥量的时候,在此类互斥量上等待的threads of control可以在环境被标识为失效时很快注意到失效并返回给应用。如果使用的是blocking mutexes,当环境失效时,潜在的互斥量实现不会在持有mutex的thread of control死后解除mutex等待者的阻塞。等待这样的互斥量的线程可能会永远挂起。 多个thread of control意图同步恢复数据库环境是没有意义的,因为最后运行的一个将删除先于它运行的环境。但是可以让一个单独的thread of control先运行做恢复然后删除数据库环境,随后系统启动一批进程,每个将创建数据库环境并使用。 有3种常用的设计方式,基于你使用的是单进程或者相关的进程组,或者是不相关的进程。 1、使用单一的进程(这个进程可能是也可能不是多线程)。 当应用启动时,它运行恢复然后打开数据库。应用随后可以根据它们的选择创建threads of control。它们可以共享使用那个打开的DB_ENV和DB句柄,或者创建它们自己的。此种架构是最简单的,线程的创建可以序列化。 2、第二个方式是使用一组相关进程(它们可能是多线程或者不是多线程的)。 这要求它们能序列化地进行数据库环境恢复。 这个架构要求监控threads of control。如果任何tread of control退出而有未关闭的DB句柄,应用可以调用DB_ENV->failchk方式来检测丢失的互斥量和锁决定应用是否能继续。如果应用没有调用DB_ENV->failchk或者这个方法返回数据库环境已没法可用,应用必须认为有一个系统失效,要进行恢复创建一个新的环境。一旦这些活动完成。其它的threads of cotrol可以继续(所有存在的句柄被丢弃)或者重启。 对这组相关的进程构建的最简单的方式是首先创建一个单一的watcher进程(通常是一个脚本),它在系统首次启动时运行,运行恢复创建实际要工作的进程或者线程。初始线程除了等待它启动的thread of control结束外无其它责任,保证它们不会异常退出。如果一个thread of … Continue reading

Posted in Berkeley DB | Tagged | Leave a comment

事务上的锁:2阶段锁

Berkeley DB使用一个叫做2阶段锁的锁协议(2PL)。这是传统的基于锁的事务所用的协议。 在一个2阶段的锁系统中,事务被分为2个不同的阶段。在第一个阶段中,事务只获取锁;在第二个阶段中,事务只释放锁。一旦一个事务释放了一个锁,他将不能获取任何其它的锁。锁在事务获取后整个期间都被持有直到提交或者取消。在Berkeley DB锁的释放通过DB_TXN->abort或者DB_TXN->commit。唯一的例外是当我们使用锁耦合去遍历一个数据结构时。如果锁只为遍历所持有,那么在事务提交或者取消之前释放锁是安全的。 对于应用而言,2PL意味着长期运行的事务将持有较长时间的锁。当设计应用时,锁竞争应该被考虑,为了减少死锁的可能性,下面的准则是有帮助的: 当访问多个数据库时,设计所有的事务使得他们以相同的顺序访问数据库文件; 如果可能,最后访问你最热门的资源,这可以使得它们的锁被持有的时间尽可能短; 使用嵌套事务来保护可能死锁的事务。

Posted in Berkeley DB | Tagged | Leave a comment

Berkeley DB 死锁的调试

db_stat -Co工具能检测和打印未解决的死锁。下面是一个常见的输出: Locks grouped by object Locker Mode Count Status ----------- Object ---------- 1 READ 1 HELD a.db handle 0 80000004 WRITE 1 HELD a.db page 3 在这个例子中,我们打开了一个数据库存储了一个单一的key/data对。因为我们打开了一个数据库句柄,在那个句柄上有一个读锁。这个句柄上的锁是标记未handle的读锁。我们在数据库调试时通常可以忽略句柄锁。 要注意的是锁持有者的ID是32位的无符号整数,被分为2个名字空间。如果这个ID的高位被置1(即大于等于80000000的ID ),是跟事务有关的锁持有者ID。高位没有被置1的是没有跟事务关联的锁持有者。事务关联的锁持有者ID和事务是1对1的对应。 在存储新的key/data对的时候持有一个数据库页上的写锁。页锁被标为page,打开在页号3上,如果我们在存另外一个key/data到数据库中,我们可能看到下面的输出: Locks grouped by object Locker Mode Count Status ----------- … Continue reading

Posted in Berkeley DB | Tagged | Leave a comment

Berkeley DB 使用定时器的死锁检测

锁和事务超时能作为常规死锁检测的代替或者附加手段。如果锁超时被设置,锁调用中的锁请求将在锁的超时时间过后返回DB_LOCK_NOTGRANTED,也就是说,锁请求被阻塞和等待超过了指定的时间。如果事务超时被设置,锁调用中的锁请求将在锁调用检测到事务的活动时间超过指定的超时时间之后返回DB_LOCK_NOTGRANTD. 如果锁或者事务的超时时间被设置,数据库操作将在锁超时过期或者事务活动时间超过指定的超时时间之后返回DB_LOCK_DEADLOCK。应用如果要区分真正的死锁和超时可以使用DB_TIME_NOTGRANTED配置标识,它使得数据库操作在超时时返回DB_LOCK_NOTGRANTED. 由于锁和事务超时只在锁请求首次被阻塞或者当死锁检测进行时才被检测,超时的精度取决于死锁检测的频度。事务如果没有阻塞于锁请求则将继续运行,即使超过事务的活动时间;如果依赖超时进行检测,则需要一个单独的死锁检测线程或者进程进行死锁检测;否则,如果没有新的锁请求被阻塞,则一个悬而未决的超时将永不被触发。 如果数据库环境的死锁检测器配置为使用DB_LOCK_EXPIRE标识,超时只是打破死锁的唯一机制。如果死锁检测器配置为使用一个不同的选项,则常规的死锁检测将被进行,如果指定了超时,锁请求和事务的超时同样有效。 锁和事务的超时可以在环境范围内指定。锁的超时也可以在每个锁请求时进行指定(DB_ENV->lock_vec)。锁和事务的超时也可以在每个事务上进行指定,对于每个锁和事务上指定的超时将覆盖环境级别的超时。

Posted in Berkeley DB | Tagged | Leave a comment

Berkeley DB 锁的粒度

除了Queue访问方式以外,Berkeley DB使用的访问方式都是页级锁。 在Btree访问方式中,Berkeley DB使用一种称为锁耦合(Lock Coupling)的技术来提高并发性。对于Btree的遍历要求读一个页,搜索那个页决定再搜索哪个页,然后在下个页上重复进行类似操作。一旦一个页被搜索,对此类操作将不再访问,除非要求进行页的分裂。为了在树中提高并发性,一旦下一个被读取或者搜索的页被决定,那个页将被锁住,原先的页锁将被原子释放(也就是此过程说不会将控制权交予锁管理器)。当页分裂变得必要时,写锁重新获得。 因为Recno访问方式基于Btree,对于读操作也使用锁耦合(lock coupling),实际上,因为Recno访问方式必须在内部页上维持一个记录总数,在写操作中不能锁耦合。取而代之的是,在每次更新操作过程中在所有的内部页上维持写锁。因此在写操作过程中不能有很高的并发。 Queue访问方式只使用短期的页级锁。也就是说,一个页级锁在请求下一个页级锁之前释放。记录锁被用于事务隔离。这为写操作提供了高并发性。使用了一个元数据页来跟踪队列的头部和尾部,在其它的锁或者IO操作中这个页将从不被锁。 Hash访问方式没有这样遍历的问题,因为使用的是动态hash技术,它必须在计算hash函数时总是引用元数据。这个元数据页存放于数据库的一个特殊的页中。在每次操作时必须对这个页上读锁。当需要分配新页时写需要写锁,发生在下列3种情况下: 一个hash桶满需要分裂; 一个key或者data太大不能存放于一个通常的页上; 对于一个固定的key,重复项太多,不得不挪动到附加页上。 当遍历一个key的重复数据项时,这个key上的锁也作为key上所有重复项的锁。因此。2个冲突的threads of control 不能同时访问同样的重复项。

Posted in Berkeley DB | Tagged | Leave a comment

Berkeley DB 隔离的级别

事务能以不同的级别来进行隔离。序列化提供了最大的隔离,同时意味着在事务的生存时间,任何时间都只有一个thread of control能读一个数据项,它将不会由前一个数据发生改变(假定这个thread of control不改变它)。缺省情况下,BDB迫使事务包装下的数据库操作序列化,这被称为级别3隔离。 很多应用并不需要在事务中包装所有的读取,因此可能的情况下,事务保护下使用序列化读取应该尽量避免,因为会因为会引起性能问题。比如,一个序列化读游标读取数据库中的每个key /data对,将会对数据库中的绝大多数页上读锁因此会阻塞数据库上的写操作直到事务提交或者取消。实际上,如果有更新操作存在于应用中,读操作仍然必须使用锁,必须准备重复执行任何可能返回DB_LOCK_DEADLOCK的操作(比如关闭和打开游标)。需要重复读的应用需要能重复访问一个数据项而能确切知道它并没有在它操作过程中被其它事务改变过。 快照隔离(snapshot isolation)也能保证重复读取,但是通过使用MVCC(multiversion concurrency control)避免读锁。这使得更新操作更加昂贵,因为它们不得不为新版本分配空间和保存快照,但是能减少读锁增大吞吐量。 一个事务可能只要求游标稳定性。它能保证游标看见的提交的数据只要被游标定位就不会改变,但是可能在读事务完成之前改变。这是2级隔离。一旦游标移动,先前引用的数据可能会改变。 Berkeley DB也支持读未提交的数据,也就说,读操作可能请求的数据已经被修改但是仍然没有被另外事务提交。这被称为1级隔离。这使得当另外一个事务在请求的数据上持有写锁时读操作不会被阻塞;缺点是读操作可能会返回被写操作取消的数据,这些数据可能被持有写锁的事务取消。 快照隔离 当配置环境使用快照隔离时,应用可能会花去更多的cache。如果cache在老的页被丢弃之前变满,这些页会被写到磁盘上的临时"freezer"文件,可能会导致额外的IO。为了避免产生此类freezing的缓存,可以通过DB_ENV->log_archive进行检查点来进行估计。cache的量大概是剩下日志的2倍。 环境应该被配置足够多的事务。 利用快照隔离好处最简单的方法:对于有更新操作的事务使用读写锁,在只读事务或者游标上使用DB_TXN_SNAPSHOT。这应该最小化快照隔离被阻塞的可能性。 如果应用更新的事务读取了大量的数据项并只更新了一小部分,也能在快照隔离时运行一些更新来提高吞吐量。

Posted in Berkeley DB | 1 Comment

Berkeley DB 事务的提交

为了完全理解当你提交事务时发生了什么,你必须首先理解BDB中的日志系统在做什么。日志引起所有的数据库写操作通过日志而进行标识,在缺省状况下,这些日志备份在磁盘上。在系统或者应用失效时这些日志将被用来还原你的数据库,通过日志,DB维护数据的一致性。 DB使用write-ahead日志。这意味着在实际的数据库改变之前信息被写到日志中。这意味着在事务保护下所提交之前的写行为将被注释在日志中,实际上,数据库在内存中维护日志。如果你备份日志在磁盘上,日志信息最终会写到磁盘上,但是在事务正在进行时日志数据可能在内存中。 当你提交一个事务时,下面的事情将发生: 提交的记录被写到日志中。这意味着事务进行的改变是永久的。缺省状况下,这个写操作同步到磁盘,因此被提交的记录将会被最早到达日志文件。 任何内存中的日志信息同步到磁盘。注意这个要求可以改变,取决于你所进行的提交类型。你也可以使用非持久性的事务,或者你使用的是完全在内存中的日志系统。 事务所持有的锁将被释放,这意味着其他事务或者线程的读操作能看到这些被修改的数据而不必求助于使用 非提交 读来达到。 要注意的是提交一个事务并不要求你内存中修改的数据写到磁盘上的数据库文件中。脏的数据库页写到磁盘有一些原因,事务提交并不是其中的一个原因。一个脏的数据库页在如下几种情况被写到磁盘中: 检查点:在检查点进行时,所有的脏页被写到磁盘中,同时一个检查点记录被写到日志中。 cache满:如果内存中的cache满了,则脏页可能被写到磁盘来释放一些空间给其他的页使用。如果脏页被写到数据库文件中,则描述这些脏页的日志记录早于数据库页被写到磁盘

Posted in Berkeley DB | Tagged | Leave a comment

Berkeley DB 的可恢复性

BDB的恢复基于 write-ahead日志,这意味着当对一个数据库页做改动时,关于此改变的描述被写到日志文件中,这个日志文件中的描述保证在数据库中受影响的页写到可靠的存储之前被写到可靠的存储介质上。这通常是日志系统提供持久性和回滚的基础特性。 如果应用或者系统崩溃,日志在恢复过程中被查阅。任何日志中描述的已经提交事务的改变而从未写到数据库的将会被作为恢复的一部分写到实际的数据库,未提交的且写到实际数据库的将被取消。 当考虑可恢复性时,有2个接口需要考虑: 1、BDB和文件系统/OS。 2、文件系统/OS和潜在的可靠存储硬件。 BDB使用OS接口和潜在的文件系统来进行文件的读写,这意味着当潜在的文件系统失效时BDB会失效。接口的要求很简单,比如BDB用于刷新数据到磁盘的系统调用,必须保证在它将结果返回到BDB之前将所有必须的信息写到可靠的存储介质上。 除此以外,BDB潜在使用OS和硬件之间的接口,这个要求描述起来不简单。 首先,必须考虑的是BDB的页大小,BDB使用应用指定的页大小进行写操作,它假定页级写操作是原子性的,这意味着如果OS的IO的块大小和页大小不同,将增加数据库崩溃的可能。 另外,如果你拷贝数据库文件(不管你是不是在进行热备),同样需要关注页大小,copy命令必须以数据库页大小为单位原子化运行,中间不能穿插写操作,copy程序必须读取页大小的整数倍,一般情况下这并不是问题。某些Solaris上的cp可能有问题。 第三,必须考虑潜在的存储硬件的影响。 如果储存数据库的磁盘爆炸,你能进行通常的灾难恢复,因为它仅仅要求一个数据库的快照加上已经归档的日志文件。如果你的数据库环境和数据库在丢失日志文件之后仍然可用,你能导出你的数据库,但是这可能是一个不一致的快照,因为事务的部分改变没有提交但是出现在导出之中,此时你可将导出的数据库和灾难恢复之后的进行对比。

Posted in Berkeley DB | Leave a comment

Berkeley DB 恢复过程

在应用或者系统失效以后,有2个可能的方法来做数据库恢复: 1、不需要恢复性,所有的数据库能从0开始重新创建。尽管这些应用可能仍然需要事务保护,恢复通常包括删除数据库环境,然后重启应用。 2、在系统或者应用失效后需要恢复信息。在这种情况下,需要使用db_recover工具或者调用带DB_ECOVER或者DB_RECOVER_FATAL标志的DB_ENV->open方法。 在恢复过程中,所有被取消或者未完成的事务被撤销,所有提交的改变将重做,在恢复完成之前数据库应用不能重启。 有2个类型的恢复过程:通常的和灾难性的。 如果当前的数据库和日志文件都存在于稳定的文件系统上,且是可访问的,则通常的恢复是足够的,运行db_recover或者在DB_ENV->open中指定DB_RECOVER标志。实际上,通常的恢复绝不包括使用数据库环境的热备。比如,你不能热备数据库和日志文件,还原热备然后运行通常恢复————你必须在使用热备时运行灾难恢复。 如果数据库或者日志文件被损坏,或者通常的恢复失效,灾难恢复将是必须的。比如,灾难恢复包括磁盘驱动器物理损坏,潜在的文件系统损坏,操作系统通常的文件检查操作不能将文件系统调整为一致。此类很难侦测,通常的做法是当通常的恢复失效时或者在数据库应用过程中校验码出错使用灾难恢复。运行灾难恢复时,做下面的步骤: 1、还原最近的数据库和日志快照。 2、如果从上次快照以来的日志文件存在,则将其还原到恢复的目录。 3、运行db_recover,指定-c选项,或者调用DB_ENV->open指定DB_RECOVER_FATAL标志,灾难恢复将检查日志和数据库文件,将数据库带到一个最近未损坏的日志文件所指示的时间所处的时刻。要注意的是只有那个时刻之前被提交的数据出现在数据库中  

Posted in Berkeley DB | 1 Comment

Berkeley DB 选择一个页大小

潜在的页大小可以通过DB->set_pagesize方法设定,最小是512字节,最大64K,必须是2的整数次方,如果你没有设定,那么系统将根据潜在的文件系统IO块大小设定。 当选择一个页大小的时候有几点考虑:overflow记录大小,锁问题,IO效率,恢复性。 首先,页大小设定了溢出(overflow)记录的大小。溢出记录是key或者data项太大而不能存放于一个单个的数据库页中,因此存放于溢出页中,溢出页是哪些存在于正常的数据库结构之外的页。由于这个原因,对于溢出记录,修改或者获取溢出记录有明显的性能损害。如果页太小,使得不得不创建大量的溢出页,能严重影响性能。 第二,在Btree,Hash和Recno访问方式中,最细的锁是页级锁,选择一个太大的页将会引起进程或者线程等待更久,因为其他的thread of control正在修改同样页上的数据。 第三,页大小设定了数据库对于操作系统的IO粒度,Berkeley DB使用页级单位字节来进行磁盘读写的操作系统调度。对于许多操作系统,有一个内部的块大小,也被用来作为OS向磁盘进行IO的粒度。一般来说,对于Berkeley DB写入和OS写入同样大小的块会有较高效率。 选择一个数据库页小于文件系统块大小引起操作系统对于Berkeley DB的页进行额外的拼接或者操作,可能会影响性能。当页尺寸小于文件系统块大小的时候,当一个Berkeley DB要写的页没有在OS缓存中找到的时候,OS不得不从磁盘上读取一个块,将此页拷贝到要读的块中,然后将这个块写到磁盘上,而不是简单将这个页写到磁盘上。另外,由于OS会读比每次Berkeley DB请求一页时所严格要求的更多的数据到它的缓存中,OS的缓存可能在浪费内存。 相反,选择一个页大小大于文件系统的块大小可能引起OS读取比要求更多的数据。在某些系统上,连续读文件系统块可能引起OS开始进行预先读。如果请求一个单一的数据库页暗示要读足够的文件系统块来满足OS进行预先读的准则,OS不得不进行比需求更多的IO。 第四,当使用事务的时候,页大小影响数据库错误的恢复。

Posted in Berkeley DB | Leave a comment

Berkeley DB 事务存储下的锁协定

当一个Berkeley DB数据库被打开时,DB句柄被赋予一个唯一的锁持有者ID。除非指定了事务,在所有的调用中,这个ID将会被用来标识这个锁持有者。为了锁住一个文件、文件中的页、文件中的记录,我们必须创建一个唯一的ID用来标识这个对象,以供锁管理器调用。通常情况下,这个ID是一个28字节的值,由唯一的文件ID,页号或者记录号,对象类型拼接而成。 在事务保护的环境中,数据库创建和删除都是可恢复的和单线程的,通过对整个环境使用一个唯一的锁来完成,这个锁在任何创建或者删除操作之前必须获得。这个要被环境锁住的对象是一个4字节的整数0。 If applications are using the lock subsystem directly while they are also using locking via the access methods, they must take care not to inadvertently lock objects that happen to be equal to the unique file IDs … Continue reading

Posted in Berkeley DB | Tagged , | Leave a comment