[{"content":"前言 LSM-tree和B-tree作为两种不同的数据存储索引结构，在不同的情况下有着各自的优势。在上学期浅薄学习了数据库相关知识后，我打算进阶一下，不止局限于SQL语法的增删改查，更多地面向底层逻辑的知识拓展。本文简单地讲解了关于LSM-tree和B-tree的知识，也算我个人的一个学习总结。之后也许大概可能会有一篇实操性的文章吧，不确定。 WARNING！ 由于笔者的文学功底和能力水平极其平庸，本文可能会不可避免地出现:\n对错参杂地理解 外星人地语言表达 无意义的加粗加黑 请谅解\nRefence 在学习过程，参考了以下书籍和网站\n《Designing Data-Intensive Applications》中文译版\n数据结构 C语言第三版 人民邮电出版社\nrocksDB文档\nlevelDB文档\n初级的驱动数据库的数据结构 世界上最简单的数据库 它非常简单，就是一个文本文件，然后每一行包含用户所输入的键值对，键值对之间会用逗号隔开。它只需要两串bash命令就可以实现。\n#!/bin/bash db_set () { echo \u0026#34;$1,$2\u0026#34; \u0026gt;\u0026gt; database } db_get () { grep \u0026#34;^$1,\u0026#34; database | sed -e \u0026#34;s/^$1,//\u0026#34; | tail -n 1 } 每次添加或者是更改数据，都只是在文件的末尾新添一条记录，也就是说如果你是进行的更改操作，旧的值是不会被删除的。这样的模式类似于日志（log），一个仅追加的文件。\n日志 引用《Designing Data-Intensive Applications》来解释日志（log）。 “这个词通常指应用日志：即应用程序输出的描述正在发生的事情的文本。”\n本文在更普遍的意义下使用日志这一词：一个仅追加的记录序列。它可能压根就不是给人类看的，它可以使用二进制格式，并仅能由其他程序读取。\n虽然这样数据库在写入时十分高效，但是我们可以很容易发现这个数据库有显然的不足之处：\n每次查找都会从头查找，如果存储的数据量过大会造成查找时间过长。 它的数据量也许大概可以“无限”增加，造成硬盘空间没了。 这时我们便引用一个数据结构来高效地查找数据库中特定键的值：索引（index）。\n索引 index的大致思想是通过保存一些额外的数据来作为一个指引路标来帮助用户来找到想要的数据. 本质是一个从主数据衍生的额外的数据，它能够有效的缩短查询所需要的时间，但是它可能会导致写入时的性能很难超过简单的追加写入文件。这是一个值得程序员们进行权衡的地方。\n键值数据的索引 接下来介绍一种常见的索引：键值数据的索引。\n但是首先我们要插播一个知识点：散列映射（hash map） 和 散列表（hash table）\n哈希表/散列表：一个有限连续的地址空间，用以存储按散列函数/散列映射计算得到相应散列地址的数据记录。————《数据结构》第三版\n我的理解是它通过一个映射函数，也就是散列函数配合码值得到了一个存储地址。这个存储地址就是哈希表数组的下标比如存入 Key = \u0026ldquo;Apple\u0026rdquo;，Value = \u0026ldquo;Red\u0026rdquo;。我假设通过某个散列函数得到了一个结果3.然后我就将\u0026quot;Red\u0026quot;这个value存储到数组的3号位置。 tip：有时会出现冲突情况，这里不做详细解释。 对于查找数据，既是重新进行一遍运算，直接定位，便可以做到不遍历全部数据。\n键值存储与在大多数编程语言中可以找到的 字典（dictionary） 类型非常相似，通常字典都是用 散列映射（hash map） 或 散列表（hash table） 实现的。\n既然可以用散列映射来表示内存中的数据结构，那我们也可以尝试用它来索引硬盘上的数据:假设我们的数据存储仅仅只是一个追加写入的文件，那最简单的策略就是：得到一个散列映射（散列函数），每一个键都能得到对应的存储地址一般情况下能。然后将指向值的偏移量！！！存储进去，不是值。当我们想要更新或者新增时，也只是重复以上步骤。它仍然没有做到删除旧的数据，但是它极大地提升了大数据量时的查询速率。\n以上方法看上去十分的简单，但是它是一个现实可行的方法。在现实中Bitcask（Riak中默认的存储引擎）就是这样做的。 Bitcask将键放在内存中，值放在硬盘中，值可以使用比内存更多的空间，而且只需要通过知道偏移量，进行一次硬盘查找来查询。像 Bitcask 这样的存储引擎非常适合每个键的值经常更新的情况。\n简单的优化 但是我们目前所有的前提都是只进行追加写入，硬盘空间迟早会被用完。一种好的解决方法：将日志分为几个特定大小的段 当日志的长度达到我们所规定的长度时，停止写入当前的段。在另一个新的段中进行写入。 我们便可以对那些已经达到长度的段进行压缩,他会保留键值的最新更新，而且还会对多个段进行合并。那些旧的键值就被删去了。 每个段现在都有自己的内存散列表，将键映射到文件偏移量。为了找到一个键的值，我们首先检查最近的段的散列映射；如果键不存在，我们就检查第二个最近的段，依此类推。\n合并过程将保持段的数量足够小，所以查找过程不需要检查太多的散列映射。\n在整个想法实现过程中会有几个问题：\n文件格式。 删除记录。 崩溃恢复。 部分写入记录 并发控制。 追加和分段合并都是顺序写入操作，通常比随机写入快得多，尤其是在磁性机械硬盘上。 在面对范围查询是效率较低，而且散列表必须要能放在内存中，否则一切都是白谈。\nSSTables和LSM-tree SSTbales 之前的追加写入，都是直接在尾部新增数据，对键值对的顺序并没有一个排序。现在我们要进行一个改变即键值对在文件中的序列要按照键排序。这个变化，便形成了一种新的格式：排序字符串表（Sorted String Table）。\n和之前的没有排序的情况相比：\nSSTbale在合并时，更加地高效。它只需要并排读取多个段，查看每个段的第一个键，将排序最小的键安置到合并后的段。不断重复此步骤，这样也能保证合并后的段是按键排序的。这时就有人要问了：主包主包，如果在几个段中出现相同的键怎么办呢？这时不同段的\u0026quot;冻结时间\u0026quot;就起到了至关重要的作用，假设我们总是依次合并相邻的段，晚\u0026quot;冻结\u0026quot;的段的键总是比先\u0026quot;冻结\u0026quot;的段的键更\u0026quot;新\u0026quot;。所以，我们可以保留晚\u0026quot;冻结\u0026quot;也就是更近的一个段的值，并且删除丢弃掉旧段中的值。 SSTable不用保存所有键的索引。因为SSTable中总是按键来进行排序的。举个栗子：假如我们正在内存中寻找键77，虽然我们不知道这个键在段文件中的切确偏移量。但是我们却知道比如70和80的偏移。因为按键排序所以77必须出现在这两者之间。这代表着，我们可以从70的偏移位置查询，直到找到77或者扫到80（这代表该段中没有这个键）也就意味着我们不再需要散列映射，只需要进行稀疏索引。 由于读取请求无论如何都需要扫描所请求范围内的多个键值对，因此可以将这些记录分组为块（block），并在将其写入硬盘之前对其进行压缩 疏内存索引中的每个条目都指向压缩块的开始处。除了节省硬盘空间之外，压缩还可以减少对 I/O 带宽的使用。 构建和维护SSTables 这一段直接叙述《Designing Data-Intensive Applications》中的过程，因为它写的非常浅显易懂 我们可以让我们的存储引擎以如下方式工作：\n有新写入时，将其添加到内存中的平衡树数据结构（例如红黑树）来实现按键序排列。这个内存树有时被称为 内存表（memtable）。 当内存表大于某个阈值（通常为几兆字节）时，将其作为 SSTable 文件写入硬盘。这可以高效地完成，因为树已经维护了按键排序的键值对。新的 SSTable 文件将成为数据库中最新的段。当该 SSTable 被写入硬盘时，新的写入可以在一个新的内存表实例上继续进行。 收到读取请求时，首先尝试在内存表中找到对应的键，如果没有就在最近的硬盘段中寻找，如果还没有就在下一个较旧的段中继续寻找，以此类推。 时不时地，在后台运行一个合并和压缩过程，以合并段文件并将已覆盖或已删除的值丢弃掉。这个方案效果很好。它只会遇到一个问题：如果数据库崩溃，则最近的写入（在内存表中，但尚未写入硬盘）将丢失。为了避免这个问题，我们可以在硬盘上保存一个单独的日志，每个写入都会立即被追加到这个日志上，就像在前面的章节中所描述的那样。这个日志没有按排序顺序但这并不重要，因为它的唯一目的是在崩溃后恢复内存表。每当内存表写出到 SSTable 时，相应的日志都可以被丢弃。 LSM-tree 基于前文的1）只增不改。2）SSTable顺序排序。3）后台合并。的原理的存储引擎就是所谓LSM存储引擎。而所谓LSM-tree是基于前文的日志结构文件系统来构建的。 这里要注意LSM存储引擎和LSM-tree的区别 1）LSM-tree仅是一个抽象的数据结构概念 2）LSM存储引擎是一个具体的软件模块是基于LSM-tree设计出来的具体“实物” 现代的levelDB和RocksDB都是非常纯正的LSM存储结构。 和前文一样，任何的存储引擎在实践中都面临着优化问题。比如面临一个极端问题：查找一个不存在的键，这时LSM-tree可能很慢。 这时为了优化，便出现一个概念布隆过滤器（Bloom filters）\n布隆过滤器：这里我们仅简单涉及布隆过滤器的概论和一小部分原理，关于它的具体数学结论，准确率不做详细追究，感兴趣的读者可以查看《leveldb-handbook》 此文。 布隆过滤器是一种空间效率极高的随机数据结构。利用位数组很简洁地表示一个集合，并能判断一个元素是否属于这个集合 但是它偶尔会出错，可能把不属于这个集合的元素误认为属于这个集合。 具体原理：bloom过滤器底层是一个位数组，初始时每一位都是0\n当插入值x后，分别利用k个哈希函数（图中为3）利用x的值进行散列，并将散列得到的值与bloom过滤器的容量进行取余，将取余结果所代表的那一位值置为1。\n一次查找过程与一次插入过程类似，同样利用k个哈希函数对所需要查找的值进行散列，只有散列得到的每一个位的值均为1，才表示该值“有可能”真正存在；反之若有任意一位的值为0，则表示该值一定不存在。例如y1一定不存在；而y2可能存在。\nB-tree 前文所讲的索引类型目前并不算是最常见的索引类型，目前最广泛的索引结构与其相当不同，我们叫它B-tree。像 SSTables 一样，B 树保持按键排序的键值对，这允许高效的键值查找和范围查询，但这也就是所有的相似之处了。\n前文的日志结构索引将数据库分解为等大的段，通常会有几mb，而且总是顺序写入段，但是，而且总是顺序写入段，但是 B-tree则是将数据库分解成固定大小的块通常只有4kb，并且一次只能读取或写入一个页面。\n每个页面都可以使用地址或位置来标识，这允许一个页面引用另一个页面 —— 类似于指针，但在硬盘而不是在内存中。 下图讲解了如何利用B-tree来快速查找一个键： 这里有一个注意点：当我们想添加一个键时，我们找到了其范围能包含这个键的页面，但如果这个页面没有空间来容纳了，这是就需要B-tree的生长，将其继续分为两个半满页面。 如下图所示： B-tree的写操作和LSM-tree截然不同，它是用新数据来覆盖掉旧的数据，而LSM-tree仅是追加写入，然后最后删除旧数据，并不修改已有的内容。\n在机械硬盘上，我们要等待磁头移动到正确的位置，磁盘转过去才会进行覆写。而在固态硬盘上由于 SSD 必须一次擦除和重写相当大的存储芯片块，所以会发生更复杂的事情\n问题优化 有时可能会出现一种情况，在进行上文提到的页面分裂时，整个分裂过程没有完成就发生比如断电之类的事情，这时可能会出现父页面仍指向我们需要分裂的那个页面，但是我们的页面早已分裂，新分裂的两个页面，没有被指向，从而出现问题。这时一个额外的数据结构便挺身而出：预写式日志-WAL这是一个仅追加的文件，每个 B 树的修改在其能被应用到树本身的页面之前都必须先写入到该文件。当数据库在崩溃后恢复时，这个日志将被用来使 B 树恢复到一致的状态。\n如果多个线程要同时访问 B 树，则需要仔细的并发控制 —— 否则线程可能会看到树处于不一致的状态。这通常是通过使用 锁存器（latches，轻量级锁）保护树的数据结构来完成。 这里简单讲一下锁存器：在某个动作完成之前，把相关的路径全部“闩住”，保证别人看到的树永远是结构完整的。直到完成了，再进行修改。\nB-tree现在也存在许多优化版本，这里不做详细介绍，读者可以下去自行查询。如B+树。\nLSM-tree和B-tree的比较 在这之前我们还要介绍一下读放大，写放大，空间放大\n读放大：为了读取一条数据，数据库实际从磁盘读取的数据页数（或字节数）。举个栗子：我想在数据库里查询一个姓名，为了找到它，从磁盘读了8个不同的数据块，读放大就是8. 写放大：实际写入磁盘的数据量与应用程序请求写入的数据量之比。举个栗子：你想存 1KB 的数据，但数据库最终往硬盘写了 10KB，写放大就是 10 空间放大：数据库占用的磁盘空间与实际数据量之比。举个栗子：例子：你的实际数据只有 1GB，但数据库文件占了 2GB 磁盘空间，空间放大就是 2。\nB-tree：优先优化了 R（读），牺牲了 U（写）和 M（空间）。\nLSM-tree：优先优化了 U（写）和 M（空间），牺牲了 R（读）。\n按照经验来说，LSM-tree的写入速度优于B-tree，但是B-tree的读取速度又明显优于LSM-tree。 LSM-tree通常能够比 B-tree支持更高的写入吞吐量，部分原因是它们有时具有较低的写放大；部分是因为它们顺序地写入紧凑的 SSTable 文件而不是必须覆写树中的几个页面。\nLSM-tree能比B-tree被压缩的更好，因为B-tree存储引擎会由于一些页面的空间未被使用而造成所谓的“浪费”；但是LSM-tree不会，而且它还会定期删去旧数据进行合并等。\n不过LSM-tree也存在一些问题：压缩过程可能影响正在进行的读写操作。就是因为硬盘的资源有限，有时可能出现因为写入量过大导致前面的段还没有压缩，合并完，导致未合并的段越来越多从而使得磁盘空间用完等问题。\nB-tree的一个优点是每个键只存在于索引中的一个位置，使得其在想要提供强大的事务语义的数据库中很有吸引力。\n尾记 只涉及了一些皮毛，接下来会继续深入，包括实操啥的，这篇文章算是纯概念了。\n","permalink":"https://linlixun-lin.github.io/posts/lsm-tree%E5%92%8Cb-tree/","summary":"\u003ch2 id=\"前言\"\u003e前言\u003c/h2\u003e\n\u003cp\u003eLSM-tree和B-tree作为两种不同的数据存储索引结构，在不同的情况下有着各自的优势。在上学期浅薄学习了数据库相关知识后，我打算进阶一下，不止局限于SQL语法的增删改查，更多地面向底层逻辑的知识拓展。本文简单地讲解了关于LSM-tree和B-tree的知识，也算我个人的一个学习总结。之后也许大概可能会有一篇实操性的文章吧，不确定。\nWARNING！\n由于笔者的文学功底和能力水平极其平庸，本文可能会不可避免地出现:\u003c/p\u003e","title":"LSM-tree和B-tree的学习总结"},{"content":"前言 一开始还没打算做这个博客，这个学习资料原来准备在平板上自己手写的，但是后面觉得在平板上又重新手写一便写过的知识点很累且很蠢，所以我打算整成一篇博客，正好壮大一下我的博客。\n文章中所有知识点均来自本人在听了王道考研课程，看了《计算机操作系统》第四版 后的理解和原文。\n这个我估计会一周一更吧，打算边学边写的。\n一.计算机系统概述 首先了解一下计算机系统的层次结构\n最底层：裸机（仅包含CPU，内存等硬件的设备）\n之后：操作系统（比如windows，mac等） 再接着：应用程序和用户。 一般而言，是应用程序与操作系统紧密贴合，但是特殊情况下，比如用户接口，用户也可以直接和操作系统联系。\n操作系统的基本概念 阐述操作系统的概念：\n控制和管理整个计算机系统的硬件与软件资源，合理地组织，调度计算机的工作与资源的分配，进而为用户和其他软件提供方便接口与环境的程序集合。操作系统是计算机系统中最基本的\n总结三点要点：\n系统资源的管理者 向上层提供方便易用的服务 最接近硬件的一层软件 操作系统的功能和目标 一. 系统资源的管理者\n处理机管理 存储器管理 文件管理 设备管理 之后的篇章会详细学习，这里只列名字了\n二. 向上层提供方便易用的服务\n硬件只能听懂二进制的指令，但是操作系统提供了更加方便的交互接口\n命令接口\n命令接口分为两类：联机命令接口（交互式命令接口），我的理解是及时反馈，即用户输入一条命令，设备就能完成指定的功能，这也强调了交互性。 脱机命令接口（批处理命令接口），我的理解是批量反馈，就类似于写程序代码，将整体全部写完后再运行，用户将一大堆指令汇集在一起提交，然后设备集中处理实现。 程序接口\n程序接口由一组系统调用（也称作广义指令）组成。用户通过在程序中使用这些系统调用来请求操作系统提供服务。普通用户不能直接地使用程序接口，只能通过程序代码来间接使用。 命令接口和程序接口合在一起统称为用户接口。\n三. 对计算机资源的扩充\n对硬件机器的拓展，让各类硬件相互配合协调。\n操作系统的特征 操作系统的基本特征包括以下四个：并发，共享，虚拟，异步 其中并发和共享是最基本的两个特征，二者互为存在条件。\n并发\n指两个或者多个事件在同一时间间隔内发生，宏观上看它们是同时发生的，比如我一天内干了很多事情，一天这个概念我们暂且看作宏观，微观上看它们其实是隔开发生的，比如细化到一天的每个小时，我做的事情都是在不同时间段完成的。\n注意和并行区别开来，并行就是在同一时间内同时进行两件或者多个事情。\n一个重要考点：单核CPU在同一时刻只能执行一个程序，各个程序能够并发执行。多核CPU能够在同一时刻实现并行。\n共享\n共享即资源共享，指系统中的资源可供内存中多个并发执行的进程共同使用。\n共享可分为两类：互斥共享方式和同时访问方式。\n互斥共享方式：简单来说，一段时间内只允许一个进程访问该资源，比如打印机的使用，假如有两个人想同时使用同一个打印机，那就无法做到，只能两个人分时间段来进行打印。\n同时访问方式：和互斥共享方式相反，该方式允许一个时间段内资源可被多个进程同时访问，比如磁盘设备。\n虚拟\n这个东西很难理解，是指将一个物理上的实体变为若干逻辑上的对应物，就比如通过时分复用技术，让多道程序并发执行，来分时使用一个处理器从而达到“多个处理器”的效果。还有空分复用技术等。\n异步 由于资源有限，进程的进行总是走走停停的，比如当一个进程进行时，突然有一个更高优先级的进程插队，或者当前进程的时间片到了，导致这个进程不得不停止下来。这种情况导致进程以“不可预知的速度向前推进”，这就是异步。\n操作系统发展历程 手工操作阶段：在这个阶段，操作系统还没有出现，用户在计算机上的所有工作，都需要人工来进行干预，比如利用纸条打孔来装入程序，结果的输出也是反映在纸条上。两个显著的缺点：1. 一个用户独占了机器，导致计算机资源的利用率极其低下。2.CPU等待手工操作，CPU的利用并不充分。 批处理阶段：操作系统开始出现，引入了脱机输入和输出。分为两类：1. 单道批处理系统：将一系列的作业脱机输入进磁带，并在系统上配置上监督程序,但是这一系列的作业仍然是按顺序依次进行的，内存中始终只有一个作业。主要特征：自动性，顺序性，，单道性。仍然有较大问题，只能串行。2. 多道批处理系统：可以实现并行，用户提交的作业先在外存上，排成一个队列，然后调度程序再在队列中选取一系列作业调入内存中，让他们在管理程序的控制下并发，穿插的进行，使得各种资源都能得到有效的利用，极大地提高了效率。 分时操作系统：分时技术：将处理器地运行时间分割成若干个时间片，按照时间片轮流将处理器分配给每个作业。若在单个时间片内，该作业不能完成，则暂时停止该作业的进行，转而进行下一个作业，直到下一个属于该作业的时间片的到来。特点 多个用户通过终端同时操作共享一台主机。但是用户之间无法相互感知到对方.1）同时性 2）交互性 3）独立性 4）及时性。 缺点 不能优先反馈相应一些紧急任务。 实时操作系统：为了能在某个时间限制内能够及时的完成某些紧急的任务，而不会存在像分时操作系统一样，导致需要时间片排队。1）硬实时系统：绝对保证，让某个特定的动作在规定的时间内完成，不得超过。2）软实时系统：能偶尔接受违反时间规定，且不会引起永久性的重大损害。 后续还有网络操作系统，分布式操作系统等这里不多介绍。 操作系统的运行环境 CPU通常执行两种不同性质的程序模式：操作系统内核程序和用户自编程序（应用程序）；通常来说两种模式的作用是不同的，前者是后者的管理者；内核程序能执行一些特权指令，但是应用程序通常为了考虑安全性不能执行这些特权指令。\n特权指令和非特权指令\n特权指令：不允许用户直接使用的指令，如I/O指令，关中断指令，内存清零指令等\n非特权指令：允许用户直接使用的指令，不能直接访问系统中的软硬件资源，仅能访问用户的地址空间。\n处理器的运行模式\nCPU的运行模式分为两类：用户态和内核态（核心态），当CPU处于内核态时：既可以执行特权指令，也可以执行非特权指令。 当CPU通过切换到用户态这个特权指令之后。CPU处于用户态，就只能执行非特权指令。应用程序向系统请求服务时通过使用访管指令。访管指令在用户态执行，自然也是一种非特权指令。\n内核：内核时操作系统中最重要最核心的部分。多个内核程序组成了操作系统内核。\n中断和异常 前面提到过内核态和用户态两种工作状态，我们需要考虑如何在这两种状态之间进行转换。因为用户态下有时是需要内核态下的功能的，但是系统并不允许在用户态下实现内核态的功能，这时就需要一个方法来进行形态的转换————中断或者异常。\n在发生中断或者异常时，处于用户态的CPU会直接跳转为内核态（通过硬件实现） “中断”可以实现计算机资源的高效利用，通过将未使用资源却在占用资源的程序中断，从而释放资源。 中断是操作系统让系统内核夺回CPU使用权的唯一途径\n中断分为两类：\n内中断，也被称为异常。是指来自CPU执行指令内部的事件。例如 程序的非法操作码 地址越界 运算溢出 虚存系统的缺页 陷入指令（非特权指令）等等 异常并不能被屏蔽掉，一旦出现了，就必须立即进行处理。 外中断，也简称中断。指来自CPU执行指令外部的事件。常用于信息的输入和输出。例如I/O中断（用来表示输入/输出的资源使用完成），时钟中断（用于表示时间片已经用完等）。 中断和异常又分别分为几类：\n中断又分为可屏蔽中断和不可屏蔽中断。 3. 可屏蔽中断是指通过INTR线发出的中断请求，可以通过修改屏蔽字来实现多重中断。 4. 不可屏蔽中断是指通过NMI线发出的中断请求。通常是紧急的硬件故障，比如电源掉电。\n异常可分为故障，自陷和终止\n故障一般是指令执行引起的异常，如非法操作码，运算溢出等。 自陷是一种“事先安排好”的异常事件，一般用在用户态尝试调用操作系统内核程序，如条件陷阱指令。 终止是指出现了使得CPU无法继续执行的硬件故障，如控制器出错，存储器检验错等。 ","permalink":"https://linlixun-lin.github.io/posts/os/%E6%96%B0%E6%96%87%E4%BB%B61/","summary":"\u003ch2 id=\"前言\"\u003e前言\u003c/h2\u003e\n\u003cp\u003e一开始还没打算做这个博客，这个学习资料原来准备在平板上自己手写的，但是后面觉得在平板上又重新手写一便写过的知识点很累且很蠢，所以我打算整成一篇博客，正好壮大一下我的博客。\u003c/p\u003e","title":"OS学习总结"},{"content":"关于本人： 就读中国西南某211高校，大二地理信息科学学生。\n缓慢摸索计算机科学中\n","permalink":"https://linlixun-lin.github.io/about/","summary":"了解一下林里寻","title":"关于我"}]