目录

LSM Tree

WireTiger引擎基本数据结构

wiredtiger引擎定义的磁盘数据结构

插入多条数据后


作为了一个从事了7-8年开发经验的老coder,之前接触的数据库都是关系型数据库;从最初接触到的SQL-Server,再到后面的Oracle,然后就是MySQL。在No SQL大潮形成之后,一直也没好好去研究下这些数据库,知道最近的一个项目中用到了mongoDB,然后在一个客户处出现了一次数据损坏,好不容易修复之后才想起没有好好了解过这个数据库的原理,是时候好好来了解一下了。

我是从一次数据损坏才萌发的这个想法,所以了解mongodb原理的入口就选择了从数据库存储,也就是这一篇想讲的内容。

为了了解更详细的内容,还下载了wiredtiger的源码下来配合这一起了解。所以这个过程会很漫长,非常的漫长。

LSM Tree

mongodb的存储引擎WiredTiger的是基于LSM Tree的。所以先来看一下什么是LSM Tree

首先来说一说这个LSM Tree。LSM-Tree是一套算法,而不是某个数据结构,这套算法定义了三件事情:

  1. 定义内存数据结构

  2. 定义磁盘数据结构

  3. 定义读写过程,或者说是这两种数据结构的转换过程。

很多数据库存储引擎都是基于B+树来设计的,那么LSM Tree是解决什么问题呢?LSM Tree的初衷是基于一个前提:磁盘批量的顺序写要远比随机写性能高出很多,无论是机械磁盘还是SSD。围绕这一原理进行设计和优化,以此让写性能达到最优,正如我们普通的Log的写入方式,这种结构的写入,全部都是以Append的模式追加,不存在删除和修改。当然有得就有舍,这种结构虽然大大提升了数据的写入能力,却是以牺牲部分读取性能为代价,故此这种结构通常适合于写多读少的场景。

磁盘在随机读写和顺序读写下的性能对比图。如果是顺序读取的话,两者的性能差距差了几个数量级,所以LSM Tree设计的一大重点就是,数据库持久化之后的磁盘文件必须要是顺序读写的,记住这个前提。

LSM Tree的整个逻辑结构如图所示:

先不看内存结构,先看一下磁盘结构。LSM tree 持久化到硬盘上之后的结构称为 Sorted Strings Table (SSTable)。顾名思义,SSTable 保存了排序后的数据(实际上是按照 key 排序的 key-value 对)。每个 SSTable 可以包含多个存储数据的文件,称为 segment,每个 segment 内部都是有序的,但不同 segment 之间没有顺序关系。一个 segment 一旦生成便不再修改(immutable)(这也是重要的前提)。

如上所述,SSTable是一种拥有持久化,有序且不可变的的键值存储结构,它的key和value都是任意的字节数组,并且了提供了按指定key查找和指定范围的key区间迭代遍历的功能。SSTable内部包含了一系列可配置大小的Block块,典型的大小是64KB,关于这些Block块的index存储在SSTable的尾部,用于帮助快速查找特定的Block。当一个SSTable被打开的时候,index会被加载到内存,然后根据key在内存index里面进行一个二分查找,查到该key对应的磁盘的offset之后,然后去磁盘把响应的块数据读取出来。当然如果内存足够大的话,可以直接把SSTable直接通过MMap的技术映射到内存中,从而提供更快的查找。

LSM-Tree写数据的基本过程如下:

  1. 当收到一个写请求时,会先把该条数据记录在WAL Log里面,用作故障恢复。

  2. 当写完WAL Log后,会把该条数据写入内存的SSTable里面(删除是墓碑标记,更新是新记录一条的数据),也称Memtable。注意为了维持有序性在内存里面可以采用红黑树或者跳跃表(mongodb这里是不是就是用的B+树来管理?基于page的管理)相关的数据结构。

  3. 当Memtable超过一定的大小后,会在内存里面冻结,变成不可变的Memtable,同时为了不阻塞写操作需要新生成一个Memtable继续提供服务。

  4. 把内存里面不可变的Memtable给dump到到硬盘上的SSTable层中,此步骤也称为Minor Compaction,这里需要注意在L0层的SSTable是没有进行合并的,所以这里的key range在多个SSTable中可能会出现重叠,在层数大于0层之后的SSTable,不存在重叠key。

  5. 当每层的磁盘上的SSTable的体积超过一定的大小或者个数,也会周期的进行合并。此步骤也称为Major Compaction,这个阶段会真正 的清除掉被标记删除掉的数据以及多版本数据的合并,避免浪费空间,注意由于SSTable都是有序的,我们可以直接采用merge sort进行高效合并。

LSM的基本内容说完了,接下来看看wiredtiger引擎是怎么实现这个LSM Tree结构的。

WireTiger引擎基本数据结构

wiredtiger引擎定义的磁盘数据结构

mongodb在磁盘上的目录里面有一个db的目录。然后以数据库的名称为文件夹,文件夹下又分为两个目录:collection和index。每个collection会形成一个物理文件:XXX.wt。

db

--db_name1

--db_name2

----collection

------xxx.wt

----index

------xxx.wt

我们在mongodb中创建一张表后,不创建任何数据,这个wt文件就是4KB大小,也就是4096字节。可以使用文本编辑工具打开,看到只有前12个字节是存在数值的,后面的统一为0.

db.createCollection("demo_collection");

41d8 0100 0100 0000 d808 23b7 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000

...

插入一条数据之后:

db.demo_collection.insert({"name":"zoulei"})

我们再来看这个文件里面发生了什么。

  1. 这个文件的大小变成了16KB,这条数据肯定是没有12KB的,这个我理解应该是引擎分配的原则,以16KB为最小的分配单位。

  2. 参考网上的数据结构如下:

a. 数据存储以extent为单位。

b. 每个extent包含了一个page header,一个block header,然后就是具体的数据。

c. extent里面具体的数据就是以key-value的形式保存,每个cell或者value都是包含一个cell的元数据,再加上真实的数据

d. 如果真实数据比较大的话,会以一个指针链接的方式指向其他的extent。暂时不讨论这种情况。

根据上面的结构,我翻了源代码,可以说明这种情况:

  • 源代码:

    第一部分是Page_Header:

    参考源代码(btmem.h):

    struct __wt_page_header {
    ​
    •    /*
    ​
    •     \* The record number of the first record of the page is stored on disk so we can figure out
    ​
    •     \* where the column-store leaf page fits into the key space during salvage.
    ​
    •     */
    ​
    •    uint64_t recno; /* 00-07: column-store starting recno */
    ​
    ​
    ​
    •    /*
    ​
    •     \* We maintain page write-generations in the non-transactional case as that's how salvage can
    ​
    •     \* determine the most recent page between pages overlapping the same key range.
    ​
    •     */
    ​
    •    uint64_t write_gen; /* 08-15: write generation */
    ​
    ​
    ​
    •    /*
    ​
    •     \* The page's in-memory size isn't rounded or aligned, it's the actual number of bytes the
    ​
    •     \* disk-image consumes when instantiated in memory.
    ​
    •     */
    ​
    •    uint32_t mem_size; /* 16-19: in-memory page size */
    ​
    ​
    ​
    •    union {
    ​
    •        uint32_t entries; /* 20-23: number of cells on page */
    ​
    •        uint32_t datalen; /* 20-23: overflow data length */
    ​
    •    } u;
    ​
    ​
    ​
    •    uint8_t type; /* 24: page type */
    ​
    ​
    ​
    /*
    ​\* No automatic generation: flag values cannot change, they're written to disk.
    ​*/
    ​
    \#define WT_PAGE_COMPRESSED 0x01u   /* Page is compressed on disk */
    ​
    \#define WT_PAGE_EMPTY_V_ALL 0x02u  /* Page has all zero-length values */
    ​
    \#define WT_PAGE_EMPTY_V_NONE 0x04u /* Page has no zero-length values */
    ​
    \#define WT_PAGE_ENCRYPTED 0x08u    /* Page is encrypted on disk */
    ​
    \#define WT_PAGE_UNUSED 0x10u       /* Historic lookaside store page updates, no longer used */
    ​
    •    uint8_t flags;                 /* 25: flags */
    ​
    ​
    ​
    •    /* A byte of padding, positioned to be added to the flags. */
    ​
    •    uint8_t unused; /* 26: unused padding */
    ​
    ​
    ​
    \#define WT_PAGE_VERSION_ORIG 0 /* Original version */
    ​
    \#define WT_PAGE_VERSION_TS 1   /* Timestamps added */
    ​
    •    uint8_t version;           /* 27: version */
    ​
    };

    第二部分是是Block_Header,根据源代码(block.h):

    struct __wt_block_header {
    ​
    •    uint32_t disk_size; /* 00-03: on-disk page size */
    ​
    •    uint32_t checksum; /* 04-07: checksum */
    ​
    •    #define WT_BLOCK_DATA_CKSUM 0x1u /* Block data is part of the checksum */
    ​
    •    uint8_t flags;               /* 08: flags */
    ​
    •    uint8_t unused[3]; /* 09-11: unused padding */
    ​
    };
  • 数据文件结构

    把文件下下来打开一看,从第4097个字节开始:

    0000 0000 0000 0000 0100 0000 0000 0000 5200 0000 0200 0000 0704 0000 0010 0000 48ad 8b28 0100 0000 0581 9f27 0000 0007 5f69 6400 6176 41cf 81e4 2737 74c7 64d1 026e 616d 6500 0700 0000 7a6f 756c 6569

    0000 0000 0000 0000 0100 0000 0000 0000

    根据上图的结构来解析:

    • page_header,共占据28个字节,也就是0000 0000 0000 0000 0100 0000 0000 0000 5200 0000 0200 0000 0704 0000这一部分内容:

      有兴趣的可以一一去比对和了解里面的逻辑。

      • recno,8个字节: 0000 0000 0000 0000

      • write_gen,8个字节: 0100 0000 0000 0000

      • mem_size,4个字节: 5200 0000

      • u, 联合结构(union),4个字节:0200 0000

      • type,一个字节:07

      • flags,一个字节:04

      • unused,保留字节,一个字节:00

      • version,一个字节:00

    • block_header,共占据12字节,也就是: 0010 0000 48ad 8b28 0100 0000这一部分内容。

      有兴趣的可以一一去比对和了解里面的逻辑。

      • disk_size,4个字节:0010 0000

      • checksum,校验码,4个字节:48ad 8b28

      • flags,一个字节:01

      • unused,保留字节,3个字节:00 0000

    • extent_data部分。

      我们看一下我们插入的真实数据在数据文件中的内容:0581 9f27 0000 0007 5f69 6400 6176 41cf 81e4 2737 74c7 64d1 026e 616d 6500 0700 0000 7a6f 756c 6569 0000(需要加上这两个0字节)

  • 根据上面的图,每行数据都是有一个cell的结构的,在源码中找一下cell的结构:

    • struct __wt_cell {

      /*
      ​\* Maximum of 71 bytes:
      ​\*  1: cell descriptor byte
      ​\*  1: prefix compression count
      ​\*  1: secondary descriptor byte
      ​\* 36: 4 timestamps   (uint64_t encoding, max 9 bytes)
      ​\* 18: 2 transaction IDs  (uint64_t encoding, max 9 bytes)
      ​\*  9: associated 64-bit value (uint64_t encoding, max 9 bytes)
      ​\*  5: data length   (uint32_t encoding, max 5 bytes)
      ​*
      ​\* This calculation is extremely pessimistic: the prefix compression
      ​\* count and 64V value overlap, and the validity window, 64V value
      ​\* and data length are all optional in some cases.
      ​*/
      ​
      uint8_t __chunk[1 + 1 + 1 + 7 * WT_INTPACK64_MAXSIZE + WT_INTPACK32_MAXSIZE];

      };

      我是先搞明白后面的东西,所以分析在这个里面,只用到了前面的3个字节:也就是

      0581 9f

      至于具体代表什么含义,在这个文件中起到什么作用,暂时没搞明白。

    • 在wiredtiger引擎中,每个键值对中都是使用bson结构来储存。bson的存储结构是长这个样子的:<unsigned totalSize> {<byte BSONType><cstring FieldName><Data>}* EOO,这个在源码的mongo/bson/bsonobj.h文件中。

      在源码中还可以找到bsontype的说明:

      enum BSONType {

      /** smaller than all other types */

      MinKey = -1,

      /** end of object */

      EOO = 0,

      /** double precision floating point value */

      NumberDouble = 1,

      /** character string, stored in utf8 */

      String = 2,

      /** an embedded object */

      Object = 3,

      /** an embedded array */

      Array = 4,

      /** binary data */

      BinData = 5,

      /** (Deprecated) Undefined type */

      Undefined = 6,

      /** ObjectId */

      jstOID = 7,

      /** boolean type */

      Bool = 8,

      /** date type */

      Date = 9,

      /** null type */

      jstNULL = 10,

      /** regular expression, a pattern with options */

      RegEx = 11,

      /** (Deprecated) */

      DBRef = 12,

      /** code type */

      Code = 13,

      /** (Deprecated) a programming language (e.g., Python) symbol */

      Symbol = 14,

      /** (Deprecated) javascript code that can execute on the database server, with SavedContext */

      CodeWScope = 15,

      /** 32 bit signed integer */

      NumberInt = 16,

      /** Two 32 bit signed integers */

      bsonTimestamp = 17,

      /** 64 bit integer */

      NumberLong = 18,

      /** 128 bit decimal */

      NumberDecimal = 19,

      /** max type that is not MaxKey */

      JSTypeMax = 19,

      /** larger than all other types */

      MaxKey = 127

      };

    • mongodb会给每一行增加一个key="id"的唯一键值,所以这一条数据中的第一个键值对就是id:XXXXX。而在wiredtiger中,这个XXXX是ObjectId类型。看一下源码中的描述:

      /**

      * Object ID type.

      * BSON objects typically have an _id field for the object id. This field should be the first

      * member of the object when present. The OID class is a special type that is a 12 byte id which

      * is likely to be unique to the system. You may also use other types for _id's.

      * When _id field is missing from a BSON object, on an insert the database may insert one

      * automatically in certain circumstances.

      *

      * The BSON ObjectID is a 12-byte value consisting of a 4-byte timestamp (seconds since epoch),

      * in the highest order 4 bytes followed by a 5 byte value unique to this machine AND process,

      * followed by a 3 byte counter.

      *

      * 4 byte timestamp 5 byte process unique 3 byte counter

      * |<----------------->|<---------------------->|<------------->

      * OID layout: [----|----|----|----|----|----|----|----|----|----|----|----]

      * 0 4 8 12

      *

      * The timestamp is a big endian 4 byte signed-integer.

      *

      * The process unique is an arbitrary sequence of 5 bytes. There are no endianness concerns

      * since it is never interpreted as a multi-byte value.

      *

      * The counter is a big endian 3 byte unsigned integer.

      *

      * Note: The timestamp and counter are big endian (in contrast to the rest of BSON) because

      * we use memcmp to order OIDs, and we want to ensure an increasing order.

      *

      * Warning: You MUST call OID::justForked() after a fork(). This ensures that each process will

      * generate unique OIDs.

      */

      在我们的例子中,这个bson结构包含了两个键值对:

      我们根据bson的结构来看一下:

      <unsigned totalSize>{<byte BSONType><cstring FieldName><Data>}* EOO

      • _id:6176 41cf 81e4 2737 74c7 64d1

      • name:zoulei

      • 27 0000 00是四字节的长度,27是低位,16进制换算成10进制之后是39,加上长度自己正好是39个字节。

      • 07 5f69 6400 6176 41cf 81e4 2737 74c7 64d1中的07就是BSONType,对应上面的正好是jstOID。5f69 6400是FieldName (_id),后面的就是Data (5f69 6400 6176 41cf 81e4 2737 74c7 64d1就是这一部分,对应了上面的12个字节。)。

      • 026e 616d 6500 0700 0000 7a6f 756c 6569中的02也是BSONType,对应上面的正好是String(有字符串结束符)。6e 616d 6500是FieldName(name),7a6f 756c 6569 00(zoulei)是Data。

      • 在我们的真实数据7a6f 756c 6569 00之前,还有一小段内容:0700 0000。这部分是因为数据部分是一个字符传,wiredtiger在这部分加入了一个字符串长度来描述,作为数据一起放在这个地方。

      • 最后的一个00字节就是EOO。

插入多条数据后

这是插入一条数据的情况,如果插入了两到三条数据,结果我想象中的结果不太一样,等说完内存数据结构之后再一起说。先把图放出来:

  • 1024字节处:

  • 4096字节处:

里面有重复的内容,相当于一条数据写了多次,这就是上面提到了,wiredtiger是顺序存储的,而不是随机读写某一个磁盘文件的某个位置!!后续再详细说说这里面的机制。因为这个涉及到wiredtiger的内存数据结构和读写机制。

mongo存储引擎那些事(一):硬盘数据结构相关推荐

  1. MySQL的存储引擎InnoDB,B+Tree数据结构索引的实现原理图(聚簇索引/聚集索引)

    1.表数据文件本身就是按B+Tree组织的一个索引结构文件 2.InnoDB的B+Tree的索引数据结构中,表数据和索引数据合并在一起,即叶子节点包含了完整的数据记录,这样的索引叫聚簇索引.

  2. bootstrap-table真实交互数据_mysql存储引擎InnoDB详解,从底层看清InnoDB数据结构

    InnoDB一个支持事务安全的存储引擎,同时也是mysql的默认存储引擎.本文主要从数据结构的角度,详细介绍InnoDB行记录格式和数据页的实现原理,从底层看清InnoDB存储引擎. InnoDB简介 ...

  3. 关于数据存储引擎结构,没有比这篇更详细的

    摘要:常见存储算法结构涵盖:哈希存储,B .B+.B*树存储,LSM树存储引擎,R树,倒排索引,矩阵存储,对象与块,图结构存储等等. 介绍 在存储系统的设计中,存储引擎属于底层数据结构,直接决定了存储 ...

  4. mysql format row_MySQL之InnoDB存储引擎:Row Format行格式

    MySQL下用的比较多.比较广的存储引擎就属InnoDB.这里我们来介绍下InnoDB存储引擎下数据记录的存储格式--Row Format行格式 基本操作 在MySQL中,所谓Row Format行格 ...

  5. ClickHouse和他的朋友们(5)存储引擎技术进化与MergeTree

    21 世纪的第二个 10 年,虎哥已经在存储引擎一线奋战近 10 年,由于强大的兴趣驱动,这么多年来几乎不放过 arXiv 上与存储相关的每一篇 paper.尤其是看到带有 draft 的 paper ...

  6. 思考:固态硬盘的普及,是否影响到了存储引擎的设计?

    思考 1:固态硬盘的普及,是否影响到了存储引擎的设计? Reference: Let's Talk About Storage & Recovery Methods for Non-Volat ...

  7. Mysql面试题,sql优化,存储引擎,数据结构,基础知识等

    目录 一.相关知识 什么是MySQL?SQL是什么? SQL的生命周期? 什么是超键.候选键.主键.外键? 数据库有哪几个范式,谈谈理解? MySQL的binlog有有几种录入格式?分别有什么区别? ...

  8. Mysql使用大全(MySQL架构与存储引擎 、事务 、业务设计 、索引 、数据结构 、执行计划 、数值类型)

    这是一篇mysql大全,学习完这篇文章,相信在日常业务和面试完全不在问题,下面我们来一一介绍 MySQL架构与存储引擎 全局变量和会话变量 要想显式指定是否设置全局或会话变量,使用GLOBAL或SES ...

  9. 数据结构与索引-- mysql InnoDB存储引擎索引

    索引与算法 索引是我们在应用开发过程中程序数据可开发的一个重要助力.也是一个重要的研究方向,索引太多,应用的性能可能受到影响,如果索引太少,对查询性能又会有制约.我们需要找到一个合适的平衡点,这个对性 ...

最新文章

  1. jQuery版本的网页开关灯、jQuery版本网页开关灯的另一种写法
  2. 浅析低功耗广域网及在智慧城市中的应用
  3. 开源文化依旧熠熠生辉 —— 在openEuler社区,有技术、有idea,你就是主角
  4. Qt总结之二十:加载字体库
  5. (转)Servlet
  6. 如何在 iPhone 和 iPad 上快速找到合适照片?
  7. 解读IBM存储虚拟化的两大法宝
  8. AVOD、SVOD、TVOD、PVOD:揭秘视频点播商业模式
  9. 惠普触控板使用指南_hp触摸板(hp笔记本触摸板怎么开)
  10. 半年总结——思想的转变
  11. 免费的云数据库平台Planetscale
  12. python毕业设计 基于django框架企业公司网站系统毕业设计设计与实现
  13. 文献阅读2019-Computer-aided diagnosis system for breast ultrasound images using deep learning
  14. 360极速浏览器用ie8模式打开网页(360浏览器同理)
  15. springboot properties
  16. LINUX--创建新用户为新用户设置权限
  17. 文件操作之特殊文件操作
  18. dockers移盘挂载
  19. python学习第八天
  20. 见缝插针小游戏制作详细步骤

热门文章

  1. hsrp和vrrp的区别
  2. 页面渲染之回流与重绘讲解
  3. 北京信息科技大学计算机考研资料汇总
  4. Android滑动锁屏
  5. AB PLC使用BOOTP/DCHPSvevr2.3给模块分配IP地址步骤
  6. linux crond
  7. OSPF IP FRR 快速重路由
  8. display属性最详解
  9. 关于Java中的WeakReference
  10. Java.JFrame