zoukankan      html  css  js  c++  java
  • 实现键值对存储(三):Kyoto Cabinet 和LevelDB的架构比較分析

    译自  Emmanuel Goossaert (CodeCapsule.com)


    在本文中,我将会逐组件地把Kyoto Cabinet 和 LevelDB的架构过一遍。目标和本系列第二部分讲的差点儿相同,通过分析现有键值对存储的架构来思考我应该怎样建立我自己键值对存储的架构。本文将包含:

    1. 本架构分析的意图和方法
    2. 键值对存储组件概览
    3. Kyoto Cabinet 和LevelDB在结构和概念上的分析
      3.1 用Doxygen建立代码地图
      3.2 总体架构
      3.3 接口
      3.4 參数化
      3.5 字符串
      3.6 错误管理
      3.7 内存管理
      3.8 数据存储
    4. 代码审查
      4.1 声明和定义的组织
      4.2 命名
      4.3 代码反复
    5. 參考文献


    1. 本架构分析的意图和方法

    我以前想过是应该写两篇独立的文章,一篇写LevelDB还有一篇写Kyoto Cabinet,还是应该写一篇综合的文章。我相信软件架构是一门非常须要决策的技艺,就如同建筑师须要考虑并选择每一个部分的设计一样。方案不能孤立的评估,而应该与其它方案之间进行权衡。软件系统架构的分析仅仅能依据其背景在评价,并与其它架构比較。因此我将把键值对存储中遇到的主要组件过一遍,并比較现有键值对系统的方案。我将会为Kyoto Cabinet 和 LevelDB使用我自己的分析,但其它项目我会使用现有的分析。这里是我选用的其它人的分析:

    - BerkeleyDB, Chapter 4 in The Architecture of Open Source Applications, by Margo Seltzer and Keith Bostic (Seltzer being one of the two original authors of BerkeleyDB) [1]
    - Memcached for dummies, by Tinou Bao [2]
    - Memcached Internals [3]
    - MongoDB Architecture, by Ricky Ho [4]
    - Couchbase Architecture, by Ricky Ho [5]
    - The Architecture of SQLite [6]
    - Redis Documentation [7]

    2. 键值对存储组件概述

    虽然键值对存储的内部架构有非常大不同,但总有相似的组件。以下列出了大部分键值对存储中遇到的主要组件及其功能的简述。

    接口:键值对存储暴露给用户的一组方法和类,使用户能够与之互动。也叫做API。键值对存储的最小API包含Get(),、Put() 和Delete()方法。

    參数系统:选项设置并传递给整个系统的其它组件。

    数据存储:接口是用来訪问内存中数据(也就是键和值)的。假设数据必须维护在持久性存储器中,比如硬盘或闪存,那么可能会出现同步性问题和并发性问题。

    数据结构:用算法和方法来组织数据,并同意高效的存储的检索。通常使用哈希表或者B+树。LevelDB中则是日志结构合并树。数据结构的选择基于数据的内部结构和底层数据存储方案。

    内存管理:系统中用来管理内存的算法和技术。内存相当重要,假设数据存储用错误的内存管理技术来訪问,会极大地影响性能。

    遍历:对数据库中全部键和值进行枚举和顺序訪问的方法。解决方式大多是迭代器和游标。

    字符串:数据结构是用来訪问字符串的。把字符串单独拿出来说也许看起来有些过分具体了,但对于键值对存储来说,大量的时间都用来传递和处理字符串,STL的std::string可能不是最佳方案。

    锁管理:全部关系到并发訪问(带有信号灯和相互排斥的)内存区锁的机制,以及当数据存储是文件系统时的文件锁。同一时候处理关于多线程的问题。

    错误管理:用来拦截和处理系统中遇到的错误的技术。

    日志:记录系统中发生的事件的机制。

    事务管理:可以确保全部操作正常运行的一系列操作的机制,而且在出现错误时,确保没有操作被运行且数据库也没有更改。

    压缩:用来压缩数据的算法

    比較器:用来比較两个键是否同样的方法。

    校验和:用了測试并确保数据的完整性。

    快照:快照提供其创建时所有数据库的仅仅读镜像。

    分区:也被称为分片,其包含将整套数据分配到多个数据存储中,可能是网络中的多个节点。

    数据备份:为了防止系统或者硬件错误,确保持久性,一些键值对存储同意数据(或者数据分区)有数个同一时候维护的拷贝,最好是在多个节点上。

    測试框架:用来測试系统的框架,包含单元測试和总体測试。

    3. Kyoto Cabinet和LevelDB结构和概念的分析

    下述关于LevelDB和Kyoto Cabinet的分析将集中在下列组件:參数系统、数据存储、字符串和错误管理。关于接口、数据结构、内存管理、日志和測试框架这些组件将包括在IKVS系列之后的文章中。至于其它的组件,我眼下不打算讲。其它系统,比如关系型数据库,有其它的诸如命令处理器、请求处理器、以及计划/优化器之类的组件,但它们已经超出了IKVS系列的内容。

    在我開始分析之前,请注意我觉得Kyoto Cabinet 和 LevelDB是非常出色的软件部分,我也非常尊敬它们的作者。即便我说了关于他们的设计的坏话,要记得的是他们的代码仍然非常出色,而我并没有像他们那样的才华。这就是说,下边的文章是我对于Kyoto Cabinet 和 LevelDB代码的一点意见。

    3.1 用Doxygen建立代码图

    为了理解Kyoto Cabinet 和LevelDB的架构,我须要挖掘它们的代码。可是我也用Doxygen,一个用来浏览应用模块结构和类的很强大的工具。 Doxygen是一个适用于多个编程语言的文档系统,它能够直接从源码中创建报告文档或者HTML站点格式的文档。然而Doxygen相同能够用在没有凝视的代码中,并创建基于系统组织方式(文件、命名空间、类和方法)的接口。

    你能够从官网上获得Doxygen [8]。在你机器上安装好Doxygen之后,仅仅须要打开shell界面,到包括全部你须要分析的源码的文件夹下。然后输入例如以下命令就可以创建默认设置文件。

    1
    $ doxygen -g

    这将创建一个叫“Doxygen”的文件。打开这个文件,确认下述全部设置都设置为“yes”:EXTRACT_ALL, EXTRACT_PRIVATE, RECURSIVE, HAVE_DOT, CALL_GRAPH, CALLER_GRAPH。这些选项会保证从代码中抽取全部对象,包含子文件夹,并创建调用图。全部可用设置的描写叙述能够在Doxygen的在线文档中找到[9]。仅仅须要输入以下的命令就可以用已选好的设置来创建文档。

    1
    $ doxygen Doxygen

    文档将在“html”目录中创建,你能够用不论什么web浏览器打开“index.html”文件来訪问文档。你能够浏览代码,查看类之间的继承关系,并通过图来查看每一个方法由其他哪个方法调用。

    3.2 总体架构

    图3.1和3.1各自是Kyoto Cabinet v1.2.76 和LevelDB 1.7.0的架构。类以UML类图标准表示。组件以圆角矩形表示,黑箭头表示其他实体调用了这个实体。从A到B的黑箭头表示A使用或者訪问了B的元素。

    这些图示表示的功能架构和结构架构基本同样。以图3.1为例,非常多组件出如今HashDB类内部,因其这些组件的代码被定义为HashDB类的一部分。

    根据内部组件的组织方式来比較,LevelDB是大赢家。原因是Kyoto Cabinet中,遍历、參数设置、内存管理和错误管理的组件都作为内核/接口组件的一部分,如图3.1所看到的。这使得这些组件和内核之间形成了强耦合,并局限了系统的模块化和功能扩展性。与之相反,LevelDB是以一种很模块化的方法建立的,仅仅有内存管理才是内核组件的一部分。

     

    图3.1


    图3.2

     

    3.3 接口

    Kyoto Cabinet 的HashDB类暴露出来至少50个方法,与之相比的是LevelDB的DBImpl类仅仅有15个方法(当中4个还是測试用的)。这是Kyoto Cabinet的Core/Interface组件强耦合的直接结果。

    API设计将会在将来的IKVS系列中具体讨论。

    3.4 參数设置

    在Kyoto Cabine中,參数是通过调用HashDB类的方法来调节的。有15个以“tune_”开头的方法来完毕这个工作。

    在LevelDB中,參数被定义在特定的对象中。“Options”对象中是通用參数,“ReadOptions”和“WriteOptions”中是Get()和Put()分别须要的參数,如图3.2中所看到的。种子解耦提供了比較好的选项的扩展性,而不必像Kyoto Cabinet中调用Core中乱七八糟的公共接口。

    3.5 字符串

    在键值对存储中,随时都有大量的字符串处理。字符串被迭代、哈希、压缩、传递和返回。因此,巧妙的实现字符串类相当重要,每一个对象节省一点,在大规模的运用上将会在全局造成引人注目的影响。

    LevelDB使用一个特殊的类,称为“Slice” [10]。一个Slice包括一个字节数组以及数组的长度。这能够在O(1)的时间内获取字符串的长度,而不是std::string所需的O(n)而不是对C的字符串调用strlen()时所需的O(n)。独立保存字符串长度也能够同意保存字符‘’,这表示键和值能够是真正的字节数组而非由null终结的字符串。最后且最重要的是,Slice处理拷贝是通过创建一个浅拷贝,而非深拷贝。这表示它仅仅简单地拷贝字节数组的指针,而不像std::string那样拷贝所有的字节数组。这避免了拷贝有可能出现的很大的键或值。

    像LevelDB一样,Redis使用他自己的数据结构来处理字符串。其目标相同是避免取字符串长度的时候避免使用O(n)操作[11]

    Kyoto Cabinet使用std::string作为字符串对象。

    我的意见是,一个字符串类的实现适应于键值对存储的需求是很必要的。假设可以避免,为什么要花费时间来拷贝字符串并分配内存呢?

    3.6 错误管理

    在我看过的键值对存储的全部C++源码中,我没有见过一个将异常作为全局的错误管理系统使用。在Kyoto Cabinet中,kcthread.cc文件里的线程组件使用了异常,但我觉得这个选择与其说是通用架构倒不如说是仅仅是在处理线程而已。异常十分危急,并应该尽可能的避免。

    BerkeleyDB有非常好的C风格的方法来处理错误。错误信息和代码集中在一个文件里。全部返回错误代码的函数都有一个叫“ret”的整型本地变量,这个变量将会在处理过程中赋值并在最后返回。这样的方法贯穿在全部的文件和模块中:相当优雅和标准化的错误管理。在一些函数中使用了向前跳转的goto语句——一种在如Linux内核那样的纯C系统中广泛使用的技巧[12]。尽管这样的方法十分简洁和干净,但C风格的错误管理方法不太适合C++应用。

    Kyoto Cabinet中,错误对象存储在每一个诸如HashDB的数据库对象中。在数据库类中,各个方法在出现错误的时候调用set_error()来设置错误对象,然后以非常符合C风格的返回true或者false。不会像BerkeleyDB那样在方法末尾返回本地变量,返回语句出如今错误出现的地方。

    LevelDB全然不使用异常,而是使用一个叫做Status的类。这个类有错误值和错误信息。每一个方法都返回这个对象,这样错误状态既能够就地处理也能够传递给调用栈中更高的其它方法。这个Status类错误码存储在字符串中,也是一种很的聪明的实现。我对于这样的设计方法的理解是,在大部分时间里,方法将会返回一个“OK”的状态(Status)对象,以表示没有出现不论什么错误。这样,错误信息字符串是NULL,而这个Status对象的处理是相当轻量的。假设Status对象添加一个属性来保存错误码,那么即便在“OK”状态的Status对象中仍须要给这个属性赋值,这即表示在每次调用方法的时候都要用很多其它的空间。全部的组件都使用这个Status类,而且不是必需像Kyoto Cabinet那样总要调用一个方法,如图 3.1 and 3.2所看到的。

    错误管理的全部方案都在上文中讲过了,我个人比較推荐LevelDB使用的方案。这个方法避免使用了异常,也不是一个我看来相当局限的单纯的C风格的错误管理,而且其避免了像Kyoto Cabinet那样与核心组件不论什么不必要的耦合。

    3.7 内存管理

    Kyoto Cabinet 和LevelDB都在内核组件中定义了内存管理。对于Kyoto Cabinet,内存管理一来能够跟踪数据库文件里临近的空块,二来当数据项保存的时候能够选择足够大小的块。而文件本身仅仅是用mmap()函数映射出来的内存空间。另外MongoDB也使用内存映射文件[13]

    而LevelDB使用的是一个日志结构合并树,其不像保存在硬盘上的哈希表那样文件里有未使用的空间。内存空间管理也包含一旦日志文件大小超过某值后,压缩这些文件的功能[14]

    其他如Redis之类的键值对存储,用malloc()来分配内存——在Redis的样例中,内存分配算法不是操作系统提供的dlmalloc或者ptmalloc3,而是jemalloc[15] 。

    3.8 数据存储

    Kyoto Cabinet, LevelDB, BerkeleyDB, MongoDB 和Redis使用文件系统来存储数据。与之相反Memcached 则是在内存中保存数据。

    4. 代码审查

    本节是对Kyoto Cabinet 和LevelDB的一个简单的代码审查。这个代码审查并不全面,并仅仅包括了我在阅读源码时认为比較出色的元素。

    4.1  声明和定义的组织

    假设代码都像LevelDB那样正常的组织,声明都在.h头文件里,而定义都在.cc文件里。但我在Kyoto Cabinet中发现了一些令人震惊的事情。实际上,非常多类中.cc文件并没有包括不论什么定义,而方法都直接在.h文件里定义。在其它文件里,一些方法在.h中定义还有一些在.cc文件里定义。尽管我理解这样做的背后可能有一些原因,但我仍觉得在C++应用中不遵守这些惯例根本是错误的。之所以说是错的是由于一来它让我像那样吃惊,二来我必须在两种不同的文件里找定义。

    4.2 命名

    首先,Kyoto Cabinet相对于Tokyo Cabinet.有了显著的改进。总体架构和命名规则都大幅改进了。虽然如此,我仍然发现Kyoto Cabinet中的非常多名字都非常晦涩,譬如属性和方法叫做embcomp、trhard、fmtver()、fpow()。这让人认为C++代码中混进了一些C代码。还有一方面,LevelDB中的命名相当清晰,除了诸如mem、imm和in的一些暂时变量。但这些不清晰的password相当微量而代码可读性相当强。

    4.3 代码反复

    我在Kyoto Cabinet中确实看到了一些代码反复。这些用来文件碎片整理的代码至少反复了3次,而全部须要分为Unix和Windows两个版本号的方法都显示出大量的反复。我没有在LevelDB看到明显的代码反复,我相信应该也有一些,但须要挖掘的更深才干找到。这证明LevelDB的代码反复问题确实比Kyoto Cabinet要小。

    5. 參考文献

    [1] http://www.aosabook.org/en/bdb.html
    [2] http://work.tinou.com/2011/04/memcached-for-dummies.html
    [3] http://code.google.com/p/memcached/wiki/NewUserInternals
    [4] http://horicky.blogspot.com/2012/04/mongodb-architecture.html
    [5] http://horicky.blogspot.com/2012/07/couchbase-architecture.html
    [6] http://www.sqlite.org/arch.html
    [7] http://redis.io/documentation
    [8] http:://doxygen.org
    [9] http://www.stack.nl/~dimitri/doxygen/config.html
    [10] http://leveldb.googlecode.com/svn/trunk/doc/index.html
    [11] http://redis.io/topics/internals-sds
    [12] http://news.ycombinator.com/item?id=3883310
    [13] http://www.briancarpio.com/2012/05/03/mongodb-memory-management/
    [14] http://leveldb.googlecode.com/svn/trunk/doc/impl.html
    [15] http://oldblog.antirez.com/post/everything-about-redis-24.html


  • 相关阅读:
    2018.5.27 OraclePLSQL编程 if-else练习和循环结构练习
    桶排序
    从函数中返回引用
    函数返回值
    参数传递模式
    计数排序(不基于比较的排序算法)
    快排序
    堆用作优先队列
    堆排序
    合并算法
  • 原文地址:https://www.cnblogs.com/mengfanrong/p/4082356.html
Copyright © 2011-2022 走看看