├── Automating Failure Testing Research at Internet Scale.md ├── README.md ├── anna.md ├── faster.md ├── images └── netflix │ ├── 2018-08-20 at 6.46 PM.png │ ├── 2018-08-20 at 6.47 PM.png │ └── 2018-08-20 at 6.49 PM.png ├── lsm-tree-1.md ├── lsm-tree-2.md ├── lsm-tree-3.md ├── polarfs.md ├── surf.md └── 架构师.md /Automating Failure Testing Research at Internet Scale.md: -------------------------------------------------------------------------------- 1 | # Automating Failure Testing Research at Internet Scale 2 | 3 | 这篇文章主要讲了Netflix公司在混沌工程上的一项研究和实践成果. Netflix中有一群非常著名的猴子(chaos monkey), 他们随机注入错误来帮助系统发现潜在的可用性问题. 关于如何注入错误, 在Netflix使用了两种方法: 4 | 5 | 1. 随机搜索. 这种方法虽然简单, 但是效果不高, 很可能无法发现深层次问题. 如果一个系统有100个子服务, 那么就意味着2的100次方的搜索空间. 6 | 2. 程序员指定规则. 在Netflix每个服务都有对应的owenr, 借用他们的领域知识, 可以制定出复杂的规则来生成深层次故障注入. 但是这种方法无法扩展, 需要每个服务owner的专业领域知识介入才行. 7 | 8 | 这篇文章反其道而行之. 假设我们已经掌握了整体系统的所有信息, 当获取到正确的结果之后, 我们反问正确的结果是怎么获取到的, 言外之意就是如何获取不到正确的结果? 通过不断的追问, 我们可以揭示出各种不同的冗余路径来获取正确的结果. 这些路径就是我们可以考虑注入故障的地方, 叫做lineage-driven fault injection(LDFI). 9 | 10 | ![](https://github.com/elithnever/paperreading/blob/master/images/netflix/2018-08-20%20at%206.46%20PM.png) 11 | 12 | ## Lineage-driven Fault Injection 13 | ![](https://github.com/elithnever/paperreading/blob/master/images/netflix/2018-08-20%20at%206.47%20PM.png) 14 | 15 | 以这个图所示的服务为例, 所有的调用路径一共有2的5次方, 也就是32条, 通过每次调用产生的正确或者错误结果, 来求解这个系统的关键路径. 文章中使用Boolean编码来形式化整个过程. 以下图为例说明: 16 | 17 | ![](https://github.com/elithnever/paperreading/blob/master/images/netflix/2018-08-20%20at%206.49%20PM.png) 18 | 1. 每行代表产生正确结果的必要条件 19 | 2. 行与行之间代表可以产生结果的并行条件. 也就是说单独运行每行都可以产生正确的结果. 20 | 21 | 所以经过化简之后, RepA与RepB和Bcast1与Bcast2就是关键路径, 故障注入应该发生在他们之间, 但是不能同时发生. 实际运行过程中, 如果注入故障之后, 不能得到正确的结果, 那么就说明系统的容错性有问题了. 22 | 23 | ## 总结 24 | 这篇论文是针对故障注入更高级的运用了, 基于callgraph信息可以实现更精细化的故障注入能力, 从而让系统更加健壮. 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 整理读过的论文以及心得体会 2 | -------------------------------------------------------------------------------- /anna.md: -------------------------------------------------------------------------------- 1 | # 从Anna看Berkeley RISELab在无协调分布式系统的研究历程 2 | 3 | 前些日子Berkeley RISE实验室发布的Anna论文引起了业界的热烈讨论, 其卓越的性能让大家惊叹不已. 不过论文完全读不懂, 原来Anna是RISE实验室近10年在无协调分布式系统领域研究成果的集大成者, 很多背景论文在国内讨论的并不多, 而且中文资料也非常少, 所以不得不顺藤摸瓜粗读了十来篇相关论文, 整理了一篇关于无协调分布式一致性研究成果的文章. 有些内容我也理解不深刻, 如有错误, 欢迎同行批评指正. 4 | 5 | ## 适合分布式系统的声明式编程语言 6 | ### Prolog和Datalog 7 | Prolog是一种声明式逻辑编程语言, 是Prolog的子集. Prolog是一门历史非常悠久的语言了, 在SQL流行之前, Prolog和SQL是并列的地位, 后来SQL逐步取得了垄断性地位. 因为Datalog是Prolog的子集, 所以Prolog更不容易理解一些. 咱们先看容易理解的Datalog, 我们看一个维基百科的例子. 8 | 9 | ![image](http://note.youdao.com/yws/public/resource/e772e5a10c18b98b33880d107b8b89e4/3A8C6E60DD4E4B8987A298197218D82D?ynotemdtimestamp=1539347537645) 10 | 11 | 上面两行代码说明了两个事实, bill是mary的父母, mary是john的父母. 12 | 13 | ![iamge](http://note.youdao.com/yws/public/resource/e772e5a10c18b98b33880d107b8b89e4/9BB1DE3447E945A7B4194B34686D6EC2?ynotemdtimestamp=1539347537645) 14 | 15 | 上面两行定义了两个规则: 16 | - 如果X是Y的父母, 则X是Y的祖先 17 | - 如果X是Z的父母, Z是Y的祖先, 则X是Y的祖先 18 | 其中:-符号表示蕴含, 也就是说:-符号右边如果都为真, 那么:-左边就为真. 那这个东西怎么和数据库挂上边的呢? 我们看下面的查询语句: 19 | 20 | ![iamge](http://note.youdao.com/yws/public/resource/e772e5a10c18b98b33880d107b8b89e4/576531B418D54A24899BB7A4ABB87E4D?ynotemdtimestamp=1539347537645) 21 | 22 | 这行代码表示返回所有的X, 满足bill是X祖先的条件, 其实就是返回bill所有的子孙, 这样其实就表达了一种查询条件了. 事实上, 支持Datalog的数据库还不少, 大家可以看维基百科中的列表. 23 | 24 | Datalog的语法就是有一组类似上述的语句构成的, 其中:-叫做蕴含符号, 蕴含符号左边叫做规则头, 蕴含符号右边叫做规则体. 规则体由若干个子句组成, 各个子句之间是and关系. 子句又成为一阶谓词逻辑, 由谓词(其实就是关系), 个体和量词组成. 比如parent(X, Y)中parent是量词, X和Y是个体. 以大写字母开头的标识符叫做变量, 以小写字母开头的标识符叫做常量. 如果一个子句不包括蕴含符号, 那么规则就变成了事实. 比如parent(bill, mary)就说明了bill是mary父母的事实. 25 | 26 | 那么这个与数据库查询有什么关系呢? 我们把上述场景替换成数据库的语义看看效果. parent这个谓词看做数据库中一张名字叫parent的表, (bill, mary)和(mary, john)看做两个tuple, 并且是parent表中的两行, 那么每个规则就可以表示一个查询条件了, 其中逗号用来表达join. 在Designing Data-Intensive Application一书的第二章, 讲述了Datalog的基本概念和例子, 可以帮助大家理解. 27 | 28 | ![image](http://note.youdao.com/yws/public/resource/e772e5a10c18b98b33880d107b8b89e4/5420ABEB4A594323B387BCC76592CD98?ynotemdtimestamp=1539347537645) 29 | 30 | 上面这幅图显示了datalog和SQL的转换关系, 这样对比着看就更容易理解了. 31 | 32 | ### Overlog 33 | Overlog语言是Datalog的扩展, 最初设计出来是为了简化基于overlay网络的编程, 文章(Implementing Declarative Overlays)发表于2005年. Overlog不是一个纯粹的声明式语言, 为了方便表达消息的存储和传递, 在Datalog的基础上, 引入了新的语法元素. Overlog引入了位置表达式的概念, 用@X表示节点X, 从而就可以表达网络交互了, 如下图的语句就表示从A节点移动payload到B节点: 34 | 35 | ![image](http://note.youdao.com/yws/public/resource/e772e5a10c18b98b33880d107b8b89e4/0C091B05EED54158BA468FA07E7CAE45?ynotemdtimestamp=1539347537645) 36 | 37 | 铺垫了这么晦涩的背景知识, 到底有什么用呢? 和分布式系统有啥关系呢? 在论文I Do Declare: Consensus in a Logic Language中, 作者给出了基于Overlog语言编写的2PC过程和Paxos过程, 终于和分布式系统沾上边了. 下图就是论文里说明的2PC协议的核心代码, 大家可以体会下是不是比用C++/Java这类编程语言实现要简单的多呢. 完整的paxos实现在论文中都有详细的论述, 大家感兴趣可以读论文, 我觉得我们理解了使用声明式语言来实现分布式系统编程这一核心思想就达到目的了, 毕竟这样小众的语言离我们日常工作还是有点遥远, 太学术化了. 38 | 39 | ![image](http://note.youdao.com/yws/public/resource/e772e5a10c18b98b33880d107b8b89e4/E4E8AF0F3C924D528FEF779AEC6BA3A7?ynotemdtimestamp=1539347537645) 40 | 41 | ### BOOM项目 42 | BOOM是Berkeley Orders Of Magnitude的缩写, BOOM代表一系列研究项目, 目标是利用声明式编程语言来简化分布式系统的开发. 在BOOM Analytics: Exploring Data-Centric, Declarative Programming for the Cloud论文中, 作者使用Overlog构建了一个接口兼容的HDFS系统, 性能和原生的HDFS可比, 然后在高可用, 可扩展等方面, 进行了扩展, 从而证明BOOM项目的价值. 43 | 44 | ## Bloom编程语言和dedelus 45 | 46 | Overlog虽然可以表达消息存储和传递, 但是无法表达时间和空间概念, 为此Dedalus: Datalog in Time and Space里提出了Overlog的加强版dedalus. 下面这条语句就是dedalus的典型. 47 | 48 | ![image](http://note.youdao.com/yws/public/resource/e772e5a10c18b98b33880d107b8b89e4/AF4B0F69D7BC40D890213794AB0609A3?ynotemdtimestamp=1539347537645) 49 | 50 | BOOM项目的成功继续激励Berkeley的研究人员向声明式编程语言方向前进. 虽然BOOM可以实现复杂程度如HDFS的分布式系统, 但是没有考虑分布式一致性的问题. 我们都知道, 在一个大型分布式系统中, 因为网络消息乱序或者网络延迟等问题, 完美的数据一致性是非常昂贵的, 最终一致性往往就可以满足应用场景的需求了. 那怎么保证程序是最终一致性呢? 于是研究人员提出了CALM: consistency as logical monotonicity原则, 简单点说, 就是如果程序满足单调性, 那么就不需要任何协调机制来实现最终一致性了, 对于非单调程序段, 在协调机制保护条件下, 也仍然能实现最终一致性了. 于是在这个思路下, 研究人员设计了Bloom声明式语言, 并且可以自动检测非单调的代码段, 提示开发人员引入协调机制. 51 | 52 | 那么什么是单调程序呢? 简单理解就是增加程序的input只能增加程序的output, 程序最终的输出不会因为输入乱序而改变, 比如SQL里的查询语句. 如果是聚合操作或者否定操作, 那么就不满足单调的特性了, 必须等待所有输入按照逻辑顺序接收到之后, 才能计算并输出结果. 单调程序是非常容易分布式化的, 他们可以通过流式算法来实现, 并且可以容忍消息乱序和delay. 相反, 对于非单调程序, 就拿最简单的累加操作来说, 则必须等待所有input接收到之后才能输出结果. 而等待的学术化解释其实就是协调, 协调可以包括序列号, 计数器, paxos算法等. 在分布式系统设计中, 如果我们能控制需要协调的代码段最小, 那往往就意味着高性能了, 这就是Bloom出现的驱动力了. 53 | 54 | Bloom和Overlog相比是一个纯粹的声明式编程语言, 在dedalus的基础上设计出来, 并且论文中给出了基于Ruby的原型实现Bud(Bloom Under Development). 有了datalog和overlog的基础, 理解Bloom也就不那么费劲了. 下图给出了Bloom的核心元素. 更详细的语法说明大家还是看论文吧, 我们重点还是说原理. 55 | 56 | ![image](http://note.youdao.com/yws/public/resource/e772e5a10c18b98b33880d107b8b89e4/C9922A3B43954CDDB75F81B355F57825?ynotemdtimestamp=1539347537645) 57 | 58 | 那么这和分布式存储好像还是没有扯上关系, 下面重点来了: 59 | 60 | ![image](http://note.youdao.com/yws/public/resource/e772e5a10c18b98b33880d107b8b89e4/1306D5F58EAF458CB68405090E100254?ynotemdtimestamp=1539348759862) 61 | 62 | 这幅图里使用bloom定义了通用KV存储系统的协议或者说接口, 那么基于这个接口, 就可以实现更复杂的KV存储系统了. 论文里给出了单节点KV存储系统和多节点KV存储系统的代码. 下图是单节点KV存储系统的实现代码: 63 | 64 | ![image](http://note.youdao.com/yws/public/resource/e772e5a10c18b98b33880d107b8b89e4/4377A549CE504779BA687BFD2C9FCD8F?ynotemdtimestamp=1539347537645) 65 | 66 | 通过bloom的分析软件我们进一步发现, 代码的9-11行打破了单调条件, 以上就是bloom的核心思想和应用场景. 论文中还分析了一个更复杂的购物车的例子, 并且bloom还提供了可视化功能, 方便可视化单调性分析过程. 经过这么多背景知识的学习, 总算是和分布式存储系统沾边了. 后来研究人员还设计了BloomUnit测试框架, 帮助对基于bloom实现的分布式系统进行测试, 让bloom可以更接地气了. 67 | 68 | ## Coordination Avoidance in Database Systems 69 | 如果我们把数据库看做一个整体, 数据库由多个版本的状态组成, 数据库状态的改变只能由写事务触发, 那么在保证应用层语义正确性(论文里叫invariants, 也可以理解为ACID的C)的前提下, 如果两个事务可以并行执行, 那么就认为这两个事务无须协调, 否则就认为两个事务需要协调. 协调的方法可以包括锁, 2PC, 阻塞等待等等方法. 不难想象, 如果两个事务需要协调, 那么性能必然受影响, 所以在保证invariants的情况下, 尽可能减少协调, 就意味着可以提升性能. 70 | 71 | 为此将上述过程建立数学模型. 我们将数据库看做一系列状态的改变(有点类似状态机), 事务运行在一些相互无关的状态快照上. 当写事务提交时, 写事务更改状态快照, 这些状态快照merge之后, 形成最新的状态. 论文 Coordination Avoidance in Database Systems 就根据上述模型, 给出了最优化的方法. 72 | 73 | ## Consistency Analysis in Bloom: a CALM and Collected Approach 74 | 我们知道协调往往是导致分布式数据库性能下降的原因, 而分布式数据库之所以需要协调本质上是为了满足一致性要求. Highly Available Transactions: Virtues and Limitations论文里对数据一致性和事务一致性进行了严格的定义和分类, 同时指出很多应用其实对严格的一致性并没有那么强的需求. 这就给我们提供了优化的空间了. Consistency Analysis in Bloom: a CALM and Collected Approach 这篇论文给出了一致性和逻辑单调性之间的关系, 只要代码满足逻辑单调性, 那么就一定能够满足最终一致性. 所以基于Bloom形式化语言, 并且开发了分析程序, 通过检测违反单调性的代码来指出违反最终一致性的地方, 指导开发人员增加协调机制的代码来保证一致性. 75 | 76 | ## Conflict-free Replicated Data Types(CRDT) 77 | 为了解决一致性和协调性之间的矛盾, CRDT采用了和CALM看上去出发点不同, 但是最终却非常类似的思路, 所以我们先说说CRDT. CRDT首先对数据复制过程进行建模, 论文里给出了两种建模方法, 并且证明两种方法的效果一致.这里简单介绍其中的一种, 叫做State-based object. 每个object表示成一个元组(S, s0, q, u, m). S代表object的状态集合, 进程pi中的数据副本状态, 记作si, 并且si属于集合S. object的初始状态记作s0. q, u, m分别代表3个函数, q代表query, u代表update, m代表merge. q和u比较好理解, 当数据副本接收到其他副本发过来的更新请求时, 调用m函数. 进程之间可以保证所有更新请求, 最终都会发送给所有副本. 在某些副本的操作序列会被编号, 从1开始, 编号逐步递增. 如果对一个object的所有查询请求都返回相同的结果, 则说明两个状态等价. 如果数据副本都按照相同的顺序接收请求并update本地副本, 那么所有副本一定是最终一致的. 但是这就要求请求的复制必须有序, 不得不引入协调机制来实现. 如果每个副本都可以提供读写请求(减少协调机制的引入), 并且最终所有副本都能够达成一致(假设所有消息都可以发送给所有副本, 允许乱序和不定期的时延), 那就是最理想的效果了. 78 | 79 | 能满足上述理想条件的数据结构或者说数据对象, 就叫做CRDT. 那么有没有这样的对象呢? 最直观的例子就是计数器, 显然满足上述理想效果. 论文中进行了大量的数学证明, 如果merge和update的组合能够满足交换律, 结合律和幂等率, 则满足CRDT的条件. 如果update无法满足三率, 则可以考虑让update附带元信息, 比如每次update操作都带有时间戳, 在merge时对本地副本的时间戳和远程副本的时间戳进行比较, 取最新结果, 就可以满足CRDT条件. 特殊情况, 如果update就可以满足三率, 那么merge只需要回放update即可. 让元信息满足条件的方式是update操作满足单调性, 也称为偏序关系. 论文里给出了一个相对通用的满足三律的merge函数, 叫做最小上界(Lease Upper Bound, 简称LUB). 给定一个偏序集合, 如果对于任意两个元素都存在LUB, 那么数学里叫做semilattice, 如果既存在最小上界, 也存在最大下届, 那么就叫做lattice. 显然通过LUB定义的偏序集合自然就是一个semilattice了. 论文中给出了满足CRDT的例子, 包括向量, 集合, 图等. 目前采用CRDT的系统主要是Riak KV. CRDT的论文公式比较多, 理解起来比较费劲, [CRDT——解决最终一致问题的利器](https://yq.aliyun.com/articles/635629) 这篇文章更通俗易懂些. 另外需要说明的是, CRDT不仅仅适用于多个进程, 也适用于多线程, 可以更充分的利用多核. 80 | 81 | ## Logic and Lattices for Distributed Programming 82 | CRDT比较好的满足了最终一致性并且无协调性, 但是设计一个复杂的满足CRDT的数据结构还是比较复杂的(特别是有删除操作的时候或者需要操作多个数据项的时候), 并且不容易证明和测试其正确性. 所以在CALM和Bloom的基础上, 设计了BloomL, 对Bloom进行了扩展, 除了bloom中的集合类型之外(集合类型因为只增加不减少, 所以天然满足单调性, 这就是为什么bloom只定义了集合类型的原因), 定义了叫做lattice的类型, 并且除了内嵌的merge函数之外, 还可以支持自定义的merge函数. 因为lattice存在merge函数, 所以只要merge函数满足三率, 那么就可以像集合类型一样, 仍然满足单调性. 有了BloomL对lattice的支持, 论文中基于BloomL和vector clock构建了一个通用的分布式key-value存储系统, 并且满足最终一致性, 代码非常简洁, 足以证明BloomL的强大威力. 83 | 84 | ## Anna: A KVS For Any Scale 85 | 介绍了这么多铺垫内容, 我们终于可以开始看看Anna的设计思想了. 分布式系统的关键在于可扩展, 不仅仅是机器可扩展, 随着cpu核数的不断增加, 也包括在多核之间可扩展. 所以Anna的核心设计目标就是可以扩展到任意规模: 不仅仅在单机多核上性能出众, 也可以扩展到大规模集群上. 同时考虑到不同应用对一致性的需求不同, Anna还可以支持多种一致性级别. 86 | 87 | ![image](http://note.youdao.com/yws/public/resource/e772e5a10c18b98b33880d107b8b89e4/14A35EB86FCE4F548A0065EE8330A69A?ynotemdtimestamp=1539347537645) 88 | 89 | 上图是Anna的系统架构, 每个线程都有自己的私有内存来存储数据副本, 线程数不超过CPU核数, 数据副本之间通过消息队列异步的同步消息. 数据的分布采用一致性hash技术, 由client proxy维护数据以及数据副本的分布. 数据的副本数可以最小到key的粒度, 这样可以非常容易应对局部热点情况. 为了保证最终一致性, 每个线程私有的内存数据结构满足lattice的性质即可. 从架构上来看, Anna坚决的避免了由于共享内存而引入的额外协调机制, 同时因为每个线程都是私有内存, 所以对CPU cache的利用也更友好. 还有一点好处是, 这种架构让Anna可以非常容易的扩展到多机架构上, 每个线程的处理逻辑可以说完全相同. 当然为了减少同一台机器情况下的消息传递开销, 在同一台机器上传递消息的时候并没有传递消息体, 而是把消息体放在共享内存里, 只传递消息在共享内存的索引. 论文里给出了2个典型的例子来介绍如何通过基本的内嵌lattice组合来实现不同的一致性级别, 一种方法是基于vector clock机制组合基础的lattice结构, 另一种方法是利用client proxy的内存缓存来解决不同一致性级别的数据可见性问题. 下图就是用lattice组合和vector clock机制来实现因果一致性的方法. 其他一致性级别的实现方法可以阅读原文. 90 | 91 | ![image](http://note.youdao.com/yws/public/resource/e772e5a10c18b98b33880d107b8b89e4/AC1A44E140734E2DBC76C38EFCE4E7C1?ynotemdtimestamp=1539347537645) 92 | 93 | 论文中和一些优秀的KV程序库以及Redis/Casandra进行了性能对比, 给出的实验数据也非常有冲击力, 数据可以参见下面的图, 详细测试数据的分析就不翻译了, 大家还是阅读原文吧. 94 | 95 | ![image](http://note.youdao.com/yws/public/resource/e772e5a10c18b98b33880d107b8b89e4/484209AFF3C54F03B08F14ECEFE7DEE4?ynotemdtimestamp=1539347537645) 96 | ![image](http://note.youdao.com/yws/public/resource/e772e5a10c18b98b33880d107b8b89e4/4ACEAEAD92CC48508485D925AFAC51B8?ynotemdtimestamp=1539348759862) 97 | 98 | ## 总结 99 | 经过数十篇论文之后, 终于基本理解了Anna的核心设计思想了. Anna利用具备lattice属性的数据结构来构建KVS的思想真得让我脑洞大开, 而且仅仅用2000行C++代码就完成了核心功能的实现, 让我们再次感受到了成熟理论的威力. 在工程方面, Anna没什么特别让人想不到的技术点, 架构也非常简单并且容易理解. 不过距离生成环境可用还需要大量的工程工作, 让我们期待Berkeley RISELab能否像孵化Spark一样, 产出另一个工业级产品吧. 100 | 101 | ## 参考文献 102 | - https://en.wikipedia.org/wiki/Datalog 103 | - Implementing Declarative Overlays: 论文中定义了Overlog语言, 并且基于overlog实现了overlay网络编程 104 | - Dedalus: Datalog in Time and Space: 提出了dedalus. 105 | - The Declarative Imperative: 针对dedalus进一步加强, 丰富了语言的表达能力 106 | - BOOM Analytics: Exploring Data-Centric, Declarative Programming for the Cloud 107 | - Consistency Analysis in Bloom: a CALM and Collected Approach: 明确了一致性和单调性的关系(CALM原则), 设计了bloom声明式编程语言, 可以分析不满足CALM原则的代码 108 | - BloomUnit: Declarative Testing for Distributed Programs: 针对bloom语言的测试框架 109 | - Coordination Avoidance in Database Systems: 介绍了分布式数据库的协调性的概念和优化方法 110 | - Highly Available Transactions: Virtues and Limitations: 总结了哪些隔离级别可以实现HAT以及实现HAT的方法 111 | - Conflict-free Replicated Data Types(CRDT): 介绍了CRDT的理论和实践 112 | - A comprehensive study of Convergent and Commutative Replicated Data Types: CRDT的techreport 113 | - Logic and Lattices for Distributed Programming: 介绍了格理论和偏序模型 114 | -------------------------------------------------------------------------------- /faster.md: -------------------------------------------------------------------------------- 1 | # Faster: A Concurrent Key-Value Store with In-Place Updates 2 | 这篇论文发表于SIGMOD 2018, 论文介绍了一种支持高并发高性能的key-value存储系统的设计思路, 测试结果声称可以达到每秒1.6亿次操作的性能, 效果非常惊人. 自论文发表以来, 一直对这篇论文的原理比较好奇, 趁着春节前后相对的空挡期, 仔细读了两遍论文, 对核心设计原理进行了总结. 3 | 4 | ## 系统架构 5 | 6 | ![image](http://note.youdao.com/yws/public/resource/dbfa5865fb7aa9f2608724e2d9e45e63/9B50326C0E194514AB3C1EA8C2CC1637?ynotemdtimestamp=1549966707483) 7 | 8 | 先从系统架构上整体看一下Faster的设计逻辑. 首先, Faster有一个基于hash表的内存index结构, 然后index指向一条一条record, 每条record离存放的就是key-value数据. 这些record由3类allocator分配, 分别是in-memory allocator, append-log allocator和hybird-log allocator. 整个Faster尽可能采用无锁设计, 减少锁互斥带来的开销, 提供了Read, Upsert, Read-Modify-Write, Delete这4种编程接口. 从整体架构上看, Faster核心就是一个无锁hash index加上一组不同的allocator, 没什么秘密而言, 不过声称每秒1.6亿次op的hash表, 充分体现了faster的工程能力, 下面详细看看每个部分. 9 | 10 | ## Epoch Protection Framework 11 | 为了减少锁竞争, 提升多核环境下的可扩展性, Faster基于一种叫做Epoch的设计框架. Epoch的设计原理如下: 12 | - 维护一个全局的原子计数器E, 叫做current epoch, E可以被任何线程增加 13 | - 每个线程拥有一个thread local的E的副本, 用Et表示. 每个线程周期性的更新Et. 所有的Et保存在一个共享的table中, 每个线程一个cacheline大小 14 | - 对于epoch c来说, 如果每个线程的Et都大于c, 那么我们认为c是安全的. 注意如果epoch c是安全的, 那么所有小于c的epoch也一定是安全的. 15 | - 同时维护一个全局计数器Es, 记录当前最大的安全epoch. Es的初始值通过扫描epoch table计算出来, 并且任何线程更新自己的Et的时候, 也会更新Es. 16 | - 当线程增加E的时候(比如从c增加到c+1), 可以注册一个回调函数, 该回调函数在c是安全的时候被调用. 所有的对保存在一个叫做drain-list的地方, drain-list基于数组实现, 当Es更新的时候, drain-list扫描出来可以触发的action来执行. 我们使用CAS机制来保证每个action只执行一次. 17 | 18 | 这些就是Epoch Protection Framework的核心设计思想, 当epoch变为安全的时候, 也就意味着没有任何thread在引用他了, 这时候对应的数据就可以安全释放了. 下面看看几个主要的编程接口: 19 | - Acquire. 在epoch table中, 为当前线程增加一个epoch entry, 并且设置Et=E. 20 | - Refresh. 根据E更新Et和Es, 并且触发在drain-list中的callback. 21 | - BumpEpoch(action). 给E加1, 从c到c+1, 并且将加入到drain-list中. 22 | - Release. 在epoch中释放epoch entry 23 | 24 | 举个例子, 考虑shared_ptr的实现方式. 标准的shared_ptr的实现方式是引用计数, 当引用计数为0时释放内存. 如果换成Epoch方法, 则基本流程如下: 25 | - 当前线程调用acquire. 26 | - 当需要释放内存时, 调用BumpEpoch(action), action就是释放内存的操作. 27 | - 所有线程定期调用refresh. 当epoch安全时, 调用对应的action. 28 | 29 | ## The Faster Hash Index 30 | hash index是faster的关键组件之一, faster里实现了一个支持并发, latch-free, 可扩展, 可以resize的hash index. hash index由一个cache-aligned数组组成, 每个bucket为64byte, 下图是hash bucket的结构. 每个hash bucket由7个8 byte的entry和一个8 byte的pointer entry组成, 每个overflow bucket仍然保持cache-align特性, 并且由内存alloctor提供内存空间. 每个entry由tag(15 bit)和address(48bit)组成, 最高位如果是0, 表示这是一个空的slot. tag用来降低hash碰撞, 很多64bit的机器不需要使用64bit的地址, 比如intel是48bit. 关于hash index的操作, 需要特别注意的是当entry不存在的时候, 不能直接使用CAS来插入entry, 论文中给出了一个例子, 解法就是利用最高位的tentative bit, 实现两阶段insert来解决问题, 详细算法可以参考论文. 同时hash index基于Epoch机制和多个状态实现了相对lower cost的resize操作, 详细的算法在论文附录B中描述了. 31 | 32 | ![image](http://note.youdao.com/yws/public/resource/dbfa5865fb7aa9f2608724e2d9e45e63/CC36503376FF4B53BF85E18AF5FE6FE6?ynotemdtimestamp=1549966707483) 33 | 34 | 虽然hash index在论文中描述篇幅不多, 不过实现的如此高效, 我觉得还是非常值得学习的, 大家可以对着源码一起阅读和理解. 35 | 36 | ## In-memory Allocator 37 | 有了hash index, 再结合一个简单的memory allocator, 比如jemalloc, 就可以形成简单的in-memory key value store了. 不过hash index只能保证自己是多线程安全的, in-memory allocator还需要保证自己也是多线程安全的, 下图就是一个多线程竞争产生的潜在问题, 论文中通过two-phase insert配合tentative bit解决了. 算法类似于乐观锁的机制, 论文里有详细描述, 这里就不仔细说了. 38 | 39 | ![image](http://note.youdao.com/yws/public/resource/dbfa5865fb7aa9f2608724e2d9e45e63/A8E9503AB52B448AA60BBF8261913E0A?ynotemdtimestamp=1549966707483) 40 | 41 | ## Log Allocator 42 | 只有in-memory allocator还是显得比较单薄, 为此论文中借鉴类似log-structured tree的思路, 设计了一个log allocator来实现数据持久化功能. 数据持久化之后, hash index记录的地址就不再是内存的物理地址而是磁盘的逻辑地址了. log allocator的核心设计思想和lsm-tree非常类似, 如下图所示, 内存中维护一个大的循环队列, 所有insert和update操作都会在循环队列中append, 循环队列划分成一个一个的page frame, 方便数据持久化. 随着队列的head offset和tail offset的变化, 队列中的数据被异步的刷新到磁盘中, 这里仍然采用了epoch framework来保证多线程安全. 同样和lsm-tree类似, 删除操作仅仅是插入了一个墓碑, 配合GC机制来实现空闲磁盘空间的回收利用. 43 | 44 | ![image](http://note.youdao.com/yws/public/resource/dbfa5865fb7aa9f2608724e2d9e45e63/BD91790946374D55A8750CE3A7F1E0FA?ynotemdtimestamp=1549966707483) 45 | 46 | ## HybridLog 47 | log allocator的缺点就是lsm-tree的缺点, 存在写放大问题, 对于update密集型负载来说, 不是最优化的方案. 于是论文又提出了HybridLog的设计思路对纯log allocator进行优化. 优化思路也比较直观, 和lsm-tree的设计思路类似, 对内存中的循环队列, 划分成mutable和immutable两大部分, mutable部分的update操作不在写log了, 而是直接进行原地的update. immutable部分和原来的逻辑类似, 无需修改. 不过论文中有很多细节需要考虑, 包括如何保证单调性, 如何recovery以及多线程安全等. 48 | 49 | ![image](http://note.youdao.com/yws/public/resource/dbfa5865fb7aa9f2608724e2d9e45e63/457E54FEAB52457E83EA28E4E8A03B4C?ynotemdtimestamp=1549966707483) 50 | 51 | ## 总结 52 | 我认为Faster论文中比较值得借鉴的是高速hash index和epoch framework的设计思路, 虽然不需要多么高深的理论, 但是工程复杂度还是挺高的, 如此高效的设计值得学习. 其他部分我个人认为不太实用, 工程实现的复杂度比较高, 需要考虑很多corner case, 不如复用已有成熟的存储引擎比较好, 特别是核心思路和lsm-tree非常接近, 当然很多细节可能需要性能调优. 53 | 54 | ## 参考 55 | - https://youjiali1995.github.io/storage/faster/ 56 | -------------------------------------------------------------------------------- /images/netflix/2018-08-20 at 6.46 PM.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elithnever/paperreading/e91cdb305407781839cf667cee56b7a3a0259b80/images/netflix/2018-08-20 at 6.46 PM.png -------------------------------------------------------------------------------- /images/netflix/2018-08-20 at 6.47 PM.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elithnever/paperreading/e91cdb305407781839cf667cee56b7a3a0259b80/images/netflix/2018-08-20 at 6.47 PM.png -------------------------------------------------------------------------------- /images/netflix/2018-08-20 at 6.49 PM.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elithnever/paperreading/e91cdb305407781839cf667cee56b7a3a0259b80/images/netflix/2018-08-20 at 6.49 PM.png -------------------------------------------------------------------------------- /lsm-tree-1.md: -------------------------------------------------------------------------------- 1 | # LSM-tree存储引擎的优化研究成果总结(1) 2 | 基于LSM-tree的存储引擎自从Google发表了bigtable论文之后, 就变得异常火热起来, 工业界和学术界先后做了大量的研究和优化. 为此, 我计划把比较知名的论文整理下, 整体学习和思考下业界的研究思路和成果. 3 | 4 | ## LSM-tree存储引擎建模 5 | 6 | ![image](http://note.youdao.com/yws/public/resource/79570ffa03dcce127a39aaaa7419c180/A2FC734594864994AAB0822F6E8C3625?ynotemdtimestamp=1541669980860) 7 | 8 | 本文假设读者对LSM-tree存储模型有一个基本的理解. 为了分析LSM-tree存储引擎的性能开销, 需要先进行数学建模. 在LSM-tree中, 内存有M(buffer), M(filter)和M(pointers)组成, 磁盘上分成多个Level, 每个Level有若干个sorted runs组成, Level与Level之间, 容量相差T倍. LSM-tree模型存在两种典型的merge策略, 叫做tiered和leveled. 二者的区别如下图所示: 9 | 10 | ![image](http://note.youdao.com/yws/public/resource/79570ffa03dcce127a39aaaa7419c180/E6ECC0E47D684464B4F56FDE356B662F?ynotemdtimestamp=1541669980860) 11 | 12 | tiered允许每个level最多有T个runs, 每次merge的时候, 都是从选择L层所有的runs, 然后merge成一个大的runs放在L+1层. 而leveled则每次从L层和L+1层选择runs的一部分, 生成L+1层runs的子集, 也就是说, leveled每个Level只允许最多一个run存在(L0层除外). 这样对于一个LSM-tree来说, 包含的参数就包括M(buffer), M(filter), T, 总的entry数量N, entry的平均大小E和Block的大小B以及merge策略. 针对这些输入参数, 需要优化的目标就是update cost, point lookup cost, range lookup cost了. 那么如何衡量呢? 很明显的一个方法就是用IO次数, 因为不管存储介质是机械硬盘还是固态硬盘, LSM-tree的存储模型瓶颈往往在于IO次数. 那么建模的目标就变成了在不同参数条件下如何计算各种条件的IO次数了. 但是对于查询来说, 需要考虑bloom filter的影响, 而bloom filter是一个概率模型, 分析概率模型的方法一般是取最坏/最好/平均情况, 这里我们选择最坏情况, 因为在false positive情况下, 会发生无效IO, 无效IO对于zero-result lookup还是很伤的. 有了目标, 计算各种cost相对来说就比较容易了, 下图给出了各种情况下的计算结果. 13 | 14 | ![image](http://note.youdao.com/yws/public/resource/79570ffa03dcce127a39aaaa7419c180/694E37C78D7E401FBF5992458DF9D748?ynotemdtimestamp=1541669980860) 15 | 16 | 论文里给出了解释, 其中e^(-M(filters)/N)表示bloom filter的FPR. 对于tiered来说, 因为每层最多T个runs, 所以point lookup cost就是O(L * T * FPR). 而对于leveled来说, 每层最多一个run, 所以point lookup cost就是O(L). update的开销主要来自于同一个entry的merge次数. 对于tiered来说, 每个entry在每个level只会merge一次, 同时每次merge最小的粒度都是block, 所以平摊到每个entry的update cost就是O(L/B). 而对于leveled来说, 每个entry在每个level平均拷贝O(T)次, 所以update cost就是O(L * T / B). 注意T的区间是 2 <= T < Tlim, 其中Tlim=N * E / M(buffer). 不难想象, 当T=2时, 对于tiered来说, 模型退化成log. 当T趋于Tlim时, 对于leveled来说, 模型退化成sorted array. 17 | 18 | ![image](http://note.youdao.com/yws/public/resource/79570ffa03dcce127a39aaaa7419c180/73F56079BFB44FF58975EC3E3BBEDD2D?ynotemdtimestamp=1541669980860) 19 | 20 | 当把模型的参数画在坐标系中的时候, 如上图所示, 就构成了整个LSM-tree模型的设计空间了. 再次感受到了数学的魅力, 有了这个数学模型, 那么就有了明确的优化空间了, 而且各种优化手段也不会超越这个设计范畴了. 更详细的数学建模过程大家可以参考monkey和dostoevsky的论文, 二者对比着看会更清楚些. 21 | 22 | ## Monkey: Optimal Navigable Key-Value Store 23 | 上面介绍的数学模型为优化LSM-tree提供了设计的空间, 主要从三个方面考虑: 24 | 1. 如何调整M(filter)的大小来减少FPR 25 | 2. 如何分配M(buffer)和M(filter)的大小 26 | 3. 如何调整T和选择merge策略, 这往往和workload有密切关系 27 | 28 | 目前现有的LSM-tree的存储引擎针对上述设置往往是静态的, 为此作者提出了Monkey的设计思路, 希望可以快速准确的在上面的设计参数中进行选择和调节, 从而实现在给定memory和workload的情况下, 让lookup cost和update cost达到最佳平衡点. 核心思路上, Monkey主要的贡献在于: 设计可调节的开关, 通过调整M(filter)最小化lookup cost, 性能预测, 根据历史数据自动化参数调整, 下面重点总结下Monkey的核心思路. 29 | 30 | ### 1. 最小化查询开销 31 | 最坏情况下的查询开销取决于FPR, 所以平均最坏情况下的查询开销定义参见公式3. 对于leveled来说, 每个level只有一个run, 所以R等于每个level的FPR加和. 对于tiering来说, 每个level最多T-1个run, 所以要乘以T-1. 根据FPR的定义, 不难计算出总的M(filter)的大小, 参见公式4. 32 | 33 | ![image](http://note.youdao.com/yws/public/resource/79570ffa03dcce127a39aaaa7419c180/B0416B9B475741EBB7805FDCCCF307DB?ynotemdtimestamp=1541669980860) 34 | 35 | ![image](http://note.youdao.com/yws/public/resource/79570ffa03dcce127a39aaaa7419c180/7D899DC566C04354927D9005FCDEBF73?ynotemdtimestamp=1541669980860) 36 | 37 | 有了公式3和公式4, 现在的问题变成了在R一定的情况下, 如何让M(filter)最少呢? 在论文的附录B中, 作者给出了数学证明, 我们直接看论文中的结论, 参见公式5和公式6. 简单点说就是在R给定的情况下, 如果按照指数递增的方式逐步提升FPR, 可以实现M(filter)最少. 直觉上来说, level越大, 包含的entry数量越多, 分配的bits越少, 才能让filter占用的总内存尽可能少. 以leveldb默认的pi=1%, L=7来说, 代入公式5计算可以节省大约60%的内存, 相当于层次越高, bits越少. 38 | 39 | ![image](http://note.youdao.com/yws/public/resource/79570ffa03dcce127a39aaaa7419c180/BDCA6CB73A2A49A596F6AFF22F6A3D33?ynotemdtimestamp=1541669980860) 40 | 41 | ![image](http://note.youdao.com/yws/public/resource/79570ffa03dcce127a39aaaa7419c180/FEDC648B154A4035972AA9B32C26F78A?ynotemdtimestamp=1541669980860) 42 | 43 | ### 2. 预测查询开销 44 | 对于zero-result lookup cost R来说, 结合公式4, 5和6, 可以推导出公式7和8. 公式8给出了M(filter)的变化对R的影响, 一般情况下, 在内存允许的条件下, M(filter)至少需要M(threshold)大小. 不过这只是在查询不到数据的情况, 对于最坏能查询到数据的情况也比较简单, 肯定是数据处于最后一层的情况, 相应的查询开销V=R-P(L)+1. 45 | 46 | ![image](http://note.youdao.com/yws/public/resource/79570ffa03dcce127a39aaaa7419c180/B1B4E8636AEA46129AEDEE1C8ACCB924?ynotemdtimestamp=1541669980860) 47 | 48 | ![image](http://note.youdao.com/yws/public/resource/79570ffa03dcce127a39aaaa7419c180/9B41F36D55C04B82B9EB6BAE92F00316?ynotemdtimestamp=1541669980860) 49 | 50 | ### 3. 写入开销和区间查询开销建模 51 | 思路仍然和上面类似, 构造一个最坏的写入模式, 也就是每个entry经过N个写请求之后, 最多更新一次, 这样这个entry一定会下沉到最后一层, 这中间的merge操作不会删除这个entry. 这样每个level的平均merge次数分别是(T-1)/T和(T-1)/2, 对应tiering和levelding. 由于每个entry一定会从L1下沉到最后一层, 所以总的次数乘以L, 又由于每次merge最小移动B个entry, IO次数还需要除以B, 所以得到公式10. 另外由于读和写操作的开销不一样, 所以增加一个因子代表读和写的开销比例. 52 | 53 | ![image](http://note.youdao.com/yws/public/resource/79570ffa03dcce127a39aaaa7419c180/084D6354A85E4D6FA23999C1BFAE2676?ynotemdtimestamp=1541669980860) 54 | 55 | 对于区间扫描来说, 最坏情况下, 每个level都存在数据, 所以每个level的每个run都需要一次IO, 那么leveling模式就是L次, tiering模式就是L*(T-1)次. 假设s代表每个run的平均扫描页面个数, 那么写入区间查询的开销就是公式11. 56 | 57 | ![image](http://note.youdao.com/yws/public/resource/79570ffa03dcce127a39aaaa7419c180/4BA3E56792754AB2A19D58784AF8EAF0?ynotemdtimestamp=1541669980860) 58 | 59 | ### 4. 可扩展性和可调节性 60 | 61 | ![image](http://note.youdao.com/yws/public/resource/79570ffa03dcce127a39aaaa7419c180/57CFE3FCE4F84CF18AC7506F55033039?ynotemdtimestamp=1541669980860) 62 | 63 | 根据上面介绍的公式7, 公式8和公式10, 在上面的表格中显示了monkey和主流lsm-tree存储系统的各纬度开销差异. 论文中对上面表格中的性能差异进行了分析, 主要观点有: 64 | - M(filter) > M(threshod), 查询的复杂度是O(Rfiltered), 否则复杂度是O(Runfiltered). 65 | - 当T=2的时候, 每个entry平均分配的bloomfilter的bits数大概是1.44, 通常leveldb配置的是10bits, 远远大于1.44. 所以主流系统的主要查询复杂度是O(Rfiltered) 66 | - R是一个与层数L无关的函数, 评估开销的时候不需要考虑L的影响. 67 | - R与entry size无关, 因为bloomfilter只与entry的数量有关, 与entry的大小无关. 而且与整体的buffer size也无关, 这样也无须平衡总的内存和bloomfilter的内存占用了. 当然这里是最坏情况下的查询开销(也就是数据不存在的情况), 对于数据存在的情况, 如果数据能缓存在内存里, 肯定能提升查询性能了. 68 | 69 | 论文还给出了调节吞吐, 参数T, merge策略, 内存的方法以及实验数据, 详细内容请大家阅读论文, 论文附录中还有公式的详细推导, 不需要很复杂的数学知识, 大家看的时候也不用有畏惧的心态. 论文的实验中全部关闭了block cache, 在附录里补充了开启block cache的测试结果, 仍然符合上述规律. Monkey这篇论文创造性的用数学知识推导出了一个非常容易实现的做法, 就达到了内存, 查询延迟, 吞吐的提升, 真的是非常神奇的工作, 向作者致敬. 70 | 71 | ## dostoevsky: Better Space-Time Trade-Offs for LSM-Tree Based Key-Value Stores 72 | dostoevsky在monkey建立的数学模型基础上, 进行了进一步的优化. dostoevsky主要的启发来源于最后一层entry数量最多, 也就贡献了最多的lookup cost和update cost, 所以如果能有效减少最后一层的cost就能降低整体的cost. 73 | 74 | ### 1. 空间放大 75 | Monkey论文没有对空间放大进行建模, 空间放大建模比较容易. 我们知道, 1到L-1层包含了大约N/T个entry, L层包含N(T-1)/T个entry. 对于leveling来说, 最坏情况就是1到L-1层的entry全部被更新了, 那么L层就会包含N/T个无效的entry, 空间放大就是O(1/T), 如果T=10, 那么空间放大大约是10%. 对于tiering来说, 最坏情况是1到L-1层的entry全部更新, 所以L层的每个run都是无效数据, 最坏情况下空间放大就是O(T). 76 | 77 | ### 2. range lookup cost 78 | dostoevsky对long range lookup进行了定义, 如果扫描的block数量大于2 * Lmax, 就认为是long, 否则就是short. range lookup是无法使用bloomfilter的, 所以可以认为, shot range lookup每个run都需要一次IO, 则leveling就是O(L), tiering就是O(L * T). 对于long range lookup来说, 开销和空间放大成正比了, 对于leveling来说就是O(s/B), tiering则是O(T * s / B). 79 | 80 | ### 3. Lazy Leveling 81 | 82 | ![image](http://note.youdao.com/yws/public/resource/79570ffa03dcce127a39aaaa7419c180/A9C0C2F9C6DF4A519A7D34EFB3D0D901?ynotemdtimestamp=1541669980860) 83 | 84 | 从上面的性能分析对比上来看, point lookup cost和long range lookup cost的大头来自于最后一层, 因为最后一层包含的entry数量最多. 而update cost来说, 每个level的cost是一样的, 所以dostoevsky的核心思想就是尽量优化1到L-1层的merge操作, 减少这些层的update cost, 理论上对于lookup cost的影响也非常小, 可以保持持平. 所以lazy leveling的做法就是1到L-1层采用tiering模式, L层采用leveling模式. 这样L层最多包括1个run, 1到L-1层每层最多包括T-1个run. 和monkey论文的方法类似, dostoevsky也同样对lazy leveling进行数学建模, 然后分析相应的开销, 分析结果如下图所示. 从图中可以看到, lazy leveling模式很好的在leveling和tiering中进行了性能折中. 需要注意一点的是, 每层的FPR设置方法和monkey类似, 也是等比变化的. 85 | 86 | ![image](http://note.youdao.com/yws/public/resource/79570ffa03dcce127a39aaaa7419c180/1C91C71A50044D109F1547F7B9D7B256?ynotemdtimestamp=1541669980860) 87 | 88 | ### 4. Fluid LSM-tree 89 | 有了lazy leveling思路之后, 将上述模式通用化, 就引出了Fluid LSM-tree模式. L层最多Z个runs, 其他层最多K个runs, 那么可以发现: 90 | - Z=1, K=1, 就是leveling模式 91 | - Z=T-1, K=T-1, 就是tiering模式 92 | - Z=1, K=T-1就是lazy leveling模式 93 | 94 | ![image](http://note.youdao.com/yws/public/resource/79570ffa03dcce127a39aaaa7419c180/3FC85105E95F47DEACBE696B992697A8?ynotemdtimestamp=1541669980860) 95 | 96 | 不过至于如何设置K和Z, 那就取决于workload了, 没什么更好的办法, 不停的实验和调节吧. 97 | -------------------------------------------------------------------------------- /lsm-tree-2.md: -------------------------------------------------------------------------------- 1 | # LSM-tree存储引擎的优化研究成果总结(2) -- 关于索引空间的优化 2 | 3 | 上一篇文章主要是从数学模型作为切入点, 对lsm-tree做了各种优化, 那么这篇文章则重点选择了几篇关于内存的优化文章. 我们都明白, 如果能够尽可能多的把索引放在内存中, 那么对于提升性能无疑是巨大的帮助, 所以这块也是lsm-tree存储引擎的研究热点. 4 | 5 | ## SlimDB: A SpaceEfficient Key-Value Storage Engine For Semi-Sorted Data 6 | 这篇论文来自2017年的VLDB会议. 我们知道, 传统的LSM-tree存储引擎, 存在写放大和空间放大问题, 因此LSM-tree的研究往往在读放大, 写放大和空间放大之间进行折中. 这篇论文观察到有很多workload, 他们的key存在所谓的semi-sorted特点, 具体的来说就是很多key存在共同的前缀, 比如推荐系统的特征存储, 文件系统的元数据存储, 图数据库存储等. 针对semi-sorted特点, 这篇论文提出了更优的索引和filter设计方法. 核心是提出了两个新技术: three-level block index和multi-level cuckoo filter. 在介绍他们之前, 先科普下cuckoo hash和cuckoo filter. 7 | 8 | ### cuckoo hash和cuckoo filter 9 | bloom filter相信大家都比较熟悉了, cuckoo filter的作用和bloom filter类似, 但是相比于bloom filter可以提供更低的False Positive Rate. cuckoo filter的原理耗子哥写过一篇非常详细的中文材料, 大家可以从coolshell上找到. 我简单总结下核心思想. 首先说说cuckoo hash, cuckoo hash采用两个hash表存储数据, 设置两个hash函数, 对于每个key, 如果第一个hash表有空闲位置, 就插入到第一个hash表, 否则就插入到第二个hash表. 如果第二个hash表也被占用了, 那么需要把这个数据踢走, 反复上述过程, 直到踢的次数达到一个上限, 就认为hash表已经满了, 需要rehash了. cuckoo的名字也是由此而来, 因为cuckoo(布谷鸟)有一个恶习, 就是他自己不筑巢, 把蛋生在其他鸟类的鸟巢里, 但是他的蛋会先孵化出来, 从而挤占掉其他鸟. 虽然看上去cuckoo hash需要很多次数据踢出, 但是平均的插入开销却是O(1)的, 并且cuckoo hash的满载率可以达到80%以上, 所以是一个性价比很高的hash算法. 用cuckoo hash就可以直接起到filter的作用, 但是如果因为会涉及key的替换, 所以必须存储完整的key来计算hash值, 但是存储全部的key还是比较占用内存空间的. 在论文Cuckoo Filter: Practically Better Than Bloom中, 给出了partial-key cuckoo hashing算法, 为了节省存储空间, 只存储key的fingerprint值, 不存储原始的key来节省存储空间. 但是因为不存储完整的key, 那么就必须找到一种方法二次计算hash值. 为此partial-key cuckoo hash算法设计了一个很巧妙的hash函数, 下图是插入算法, 查找和删除算法参见论文. 论文中对cuckoo filter的存储空间和FRP之间的关系进行了数学建模, 每个bucket的slot数量越多, hash table的负载越高, fingerprint的bit越多, FPR越小. 当bucket size=4 的时候, 装载率可以到95%, 在相同FPR条件下, bloom filter的存储空间是cuckoo filter的1.44倍. 10 | 11 | ![image](http://note.youdao.com/yws/public/resource/79570ffa03dcce127a39aaaa7419c180/73DD3FF483C4440ABDE56FAE54006414?ynotemdtimestamp=1544784896470) 12 | 13 | ### Entropy-Encoded Trie data structure 14 | 针对已经排序好的数据, 通常的做法是使用二分查找来查询具体的key. 进一步来说, 因为key有序, 所以可以用更高效的编码方式对key进行编码来构建一个索引, 加速查找. trie树, 也叫做前缀树, 可以作为这样的数据结构来使用, 如果key-value都是固定长度的, 那么可以针对key的最短unique前缀来构建索引, 而无须完整的key. 下图上半部分就是一个trie树的例子. 然而使用树形结构在持久化是非常消耗存储空间的, 因为有大量的指针, 所以需要对树形结构进行相应的编码. 编码的基本思路是递归: Repr(T) = |L| Repr(L) Repr(R), 其中|L|代表左子树叶子节点的个数详细方法参加论文: SILT: A Memory-Efficient, High-Performance Key-Value Store. 基本上编码完之后的效果就是下图下半部分所示. 论文中还给出了进一步的优化方法. 15 | 16 | ![image](http://note.youdao.com/yws/public/resource/79570ffa03dcce127a39aaaa7419c180/F3D34CCC35B24124ADEBEB91FC1D6C15?ynotemdtimestamp=1544784896470) 17 | 18 | 但是ECT仅仅对固定长度的key和value有效, 如果key和value长度不固定, 那么需要进行一定的变种, 一种方法是存储把(key, value)替换成(offset, paitial of key value), 然后其余的部分, 存储在单独的地方, 但是这种方法需要多引入一次IO操作. 如果value足够小, 那么可以和key存储到一起, 减少二次IO的开销. 使用ECT编码, 论文中给出平均每个entry占用0.4 bytes, 但是没有找到证明过程. 19 | 20 | ### three-level block index 21 | 利用three-level block index技术, 替换掉rocksdb原生的block index之后, 可以优化index的存储空间到平均每个key 0.7bits. 下面来看看three-level block index的核心设计. 在leveldb里, 每个SSTable的最后都有一个index block, 存储每个data block的last key, 这样每次查找都需要基于index block做一次二分查找来定位到具体的数据. 在作者观测到的典型的workload中, 每个entry的大小不超过256 bytes, 如果block size是4KB的话, 每个block最多16个entry(4KB/256=16), index block里存储的是full key, 平均大小为16 bytes, 所以平均每个entry的index需要16B/16=8bits. 和leveldb不同, leveldb存储的key是全部有序的, 但是semi-sorted data只要求key的前缀有序, 所以这就让我们可以使用ECT编码来更高效的进行index的压缩, ECT平均每个entry使用0.4 bytes. 在semi-sorted data环境下, ECT可以达到平均每个key只存储2.5 bits的效果, 论文中进一步优化到了1.9 bits. 22 | 23 | 但是ECT是为了index hash table设计的, 对于semi-sorted key来说, 单纯使用ECT不够高效, 所以这里设计了Three-level index. Three-level index的原理如下图所示: 24 | - 将每个block的first key和last key组成一个数组, 这个数组的前缀进行ECT编码, 重复的前缀会删除, 这就是第一个level 25 | - 第二个level存储第一个数组中的下标, 这样当给定一个key查询时, 就能筛选出一组可能存在这个key得SSTable 26 | - 为了进一步缩小查找范围, 每个block last key的后缀组成一个数组, 共享公共前缀的suffix仍然采用ECT编码, 这样就可以采用二分查找的方式定位具体的block了. 当然如果为了加速查找, 不使用ECT编码也可以. 27 | 28 | 因为ECT必须针对key-value定长的情况, 所以这里使用所有key的最长公共前缀进行ECT编码的, 但是不知道对于所有的key的后缀是否需要补齐长度, 论文里没有写, 我估计是需要的. 29 | 30 | ![image](http://note.youdao.com/yws/public/resource/79570ffa03dcce127a39aaaa7419c180/62E4E141FC3B4AD58C25CE30CD9DDC58?ynotemdtimestamp=1544784896470) 31 | 32 | 简单分析下three-level block index的开销, 对于第一层, 每个block使用first和last两个key, 使用ECT之后, 平均每个key 2.5 bit, 所以每个block需要5 bit. 第二层存储的是个数, 可以使用差分编码, 平均每个block也是2.5 bit. 第三层仍然是ECT编码, 每个block一个key, 那么平均每个block占用2.5 bit. 所以平均每个block需要占用10 bit(5 + 2.5 +2.5). 如果每个block平均存储16个entry, 那么每个entry相当于10/16=0.7 bit, 远远小于leveldb的8 bit. 33 | 34 | ### multi-level cuckoo filter 35 | 因为cuckoo filter中存储的是key的fingerprint, 所以存在一定冲突的可能, 而冲突就会产生False Positive的读请求. 为了降低在冲突情况下的读延迟, 引入了main table和secondary table. main table存储fingerprint和level, 这样可以快速定位key所属于的level. secondary table中存储了main table中冲突的entry的full key和对应的level, 基本逻辑如下图所示. 36 | 37 | ![image](http://note.youdao.com/yws/public/resource/79570ffa03dcce127a39aaaa7419c180/144FFF95721847A4A53CA3EDF58AFFF2?ynotemdtimestamp=1544784896470) 38 | 39 | 下面这幅图显示了SlimDB和原生levelDB的性能对比, 可以看出来SlimDB的内存节省还是挺明显的, 详细的测试数据大家看论文吧. 40 | 41 | ![image](http://note.youdao.com/yws/public/resource/79570ffa03dcce127a39aaaa7419c180/12C49C541BCF4A51AE7EDF08A384F94B?ynotemdtimestamp=1544784896470) 42 | 43 | ## LSM-trie: An LSM-tree-based Ultra-Large Key-Value Store for Small Data 44 | 既然提到了trie树, 就一起来看看这篇论文吧. 这篇论文发表自2015年的ATC(USENIX Annual Technical Conferene), 核心出发点就是希望用trie树结构来精简索引的大小, 来降低大量小key-value数据而造成的内存膨胀问题. 但是为了使用trie树, 就必须采用类似sha-1的hash算法, 将原始的key计算出来的hash值来替换掉原始的key, 因为sha-1 hash之后, key是定长的而且公共前缀更多了, trie树的优势才能充分发挥出来. 下图所示的结构就是一种所谓SSTable-trie的结构, 和SSTable相比就是把hashkey存在levelDB里了. 但是这还不够高效, 毕竟还是存在索引的, 将原始key转换成hashkey之后, 就无法保证key有序的特性了, 所以论文干脆引入了HTable的思路, 这就是LSM-trie. 简单点说, 就是在给item分配block的时候, 使用hash而不是基于hashkey排序了, 不过使用hash就容易导致不同的block之间数据不均衡, 所以还需要对特别不均衡的block进行一定程度的修正, 这里面细节比较多. 同时还需要引入一种新的bloomfilter算法, 这块大家感兴趣就去看论文吧. 45 | 46 | ![image](http://note.youdao.com/yws/public/resource/79570ffa03dcce127a39aaaa7419c180/36E95C08EA5240D9853DE78B79F6D22E?ynotemdtimestamp=1544784896470) 47 | 48 | ## Optimizing Space Amplification in RocksDB 49 | 这篇论文比较简单, 论文的核心内容是基于rocksdb研发了MySQL的存储引擎, 在facebook的workload下, 替换InnoDB之后大约降低了50%的存储空间. 论文中介绍了RocksDB使用的一些空间压缩技术, 主要包括: 50 | - 为不同的层设置不同的压缩算法. 比如最后一层包含90%的数据, 但是被读取的概率相比其他层要小一些, 所以最后一层设置更高压缩率的算法, 相对来说性价比更高, 而低层因为数据量少, 访问频率可能更高, 可以采用相对低压缩率的算法, 节省压缩和解压缩的CPU开销, 提高访问性能. 51 | - 动态条件每个level的size. 我们知道传统的leveldb实现中, 每个level的size是固定配置的, 但是在最坏情况下, 最后一层可能不是上一层的10倍, 因为最后一层可能数据不满, 这时候空间放大就不是1.1倍了. RocksDB引入了动态调整每个level size的技术, 仍然保证上下两层的size在固定的倍数, 比如10, 这样可以保证空间用于是1.1倍. 52 | - Key的前缀压缩, sequence ID的垃圾回收, 基于词典的压缩算法等. 53 | - 前缀bloomfilter. 对于scan来说, 传统的bloomfilter就起不到作用了, 然而很多scan请求都是基于共同前缀的, 比如如果key是(userid, timestamp), 那么userid很可能会成为前缀. rocksdb开发了prefix bloom filter, 由用户传入一个prefix extractor, 来实现prefix bloom filter, 在facebook的workload上可以降低64%的读放大. 54 | 55 | 论文的实验环节介绍了很多在facebook workload下的实验数据和参数, 来证明使用RocksDB替换innodb之后的效果, 详细数据可以参考论文原文. 56 | 57 | ## Accordion: Better Memory Organization for LSM Key-Value Stores 58 | 这篇论文发表于2018年的VLDB, 论文提出了优化内存分配和使用的方法. 这篇论文主要针对HBase做了内存方面的优化, 核心思路是一方面在memtable中提前做compact, 并且将immemtable的index进行flatten, 从而节省内存空间, 这一点和tera的优化思路类似, 另一方面用Java的堆外内存技术, 将内存组织成相对较大的数据块, 提高内存管理的效率, 降低GC的影响. 因为Java有复杂的内存管理和GC机制, 所以单纯的在内存里进行compact不一定能提升性能, 因为compact过程可能会free大量小对象, 对GC和内存管理会带来负担. 所以HBase 2.0提供了2个策略, 一个叫做basic策略, 在内存里不进行数据删除, 只对索引进行flatten, 另一个种策略叫做Eager, 非常激进的进行数据删除, 来应对数据频繁变化的workload场景. 这两种策略相对死板, 所以Accordion提出了一种adaptive的策略. Accordion设计了2是个参数, 一个是t, 随着内存的增加而增加, 一个是u, 记录unique的key的比例, 通过给t和u设置阈值, 来决定是否进行数据删除. 59 | 60 | 整体上这篇论文比较偏工程化, 而且很大一部分是结合Java内存管理机制进行优化的, Java堆外内存优化这部分细节可以详见论文. 61 | -------------------------------------------------------------------------------- /lsm-tree-3.md: -------------------------------------------------------------------------------- 1 | # LSM-tree存储引擎的优化研究成果总结(3) -- 架构的优化 2 | ## Scaling Concurrent Log-Structured Data Stores 3 | 4 | ![image](http://note.youdao.com/yws/public/resource/79570ffa03dcce127a39aaaa7419c180/6BF0395B9FD14BF19C150C8229E687FA?ynotemdtimestamp=1546605107269) 5 | 6 | 如上图所示, LSM-DS的模型可以抽象成上图的形式, 任何数据的读写请求, 都会涉及Pd, Pm和P'm这三个指针, 同时后台的compact任务也需要访问和修改这3个指针. 那么这样一来, 这三个指针就必须进行一些同步的操作来保证正确性, 这篇论文的核心就是提供了一组算法来最大化的降低锁竞争和提升并发度, 进而提升性能. 上图中抽象的模型和三个指针的含义非常容易理解, 大家看图中的描述文字吧, 就不多解释了. 7 | 8 | 为了实现高并发, 论文设计了两个钩子函数, 分别是beforeMerge和afterMerge, 在compact(或者说merge)之前和之后调用. compact过程(或者说merge过程)结束之后, 返回一个新的指针指向disk的component, 并且作为参数传递给afterMerge函数. 如果内存中的memtable是多线程安全的, 那么get请求无须加锁, 因为即使在get操作过程中, 这3个指针发生了变化, 那么也不影响正确性, 最坏情况是有些component被访问了两次. 但是put操作就需要精心设计了, 防止put数据到无效的内存component上. 为此论文引入了读写锁来对读写操作进行同步控制. 基本的算法如下图所示: 9 | 10 | ![image](http://note.youdao.com/yws/public/resource/79570ffa03dcce127a39aaaa7419c180/75D8F320D9764BBAA28D060913315352?ynotemdtimestamp=1546605107269) 11 | 12 | 上面的算法没有考虑snapshot的功能, 类似levelDB, 我们可以用时间戳来实现snapshot功能, 但是引入snapshot功能之后, 算法需要考虑更多关于snapshot的细节, 优化之后的算法如下图: 13 | 14 | ![image](http://note.youdao.com/yws/public/resource/79570ffa03dcce127a39aaaa7419c180/44AAD29310594C89ABC2736D1064665C?ynotemdtimestamp=1546605107269) 15 | 16 | - 需要一个额外的active表来记录所有被snapshot的timestamp 17 | - put的流程没有太大的变化, 仅仅是在插入memtable之后, 多了一个getTs()的函数调用, 返回合适的ts, 并且把ts从active表中删除 18 | - 多了一个GetSnap函数, 论文中详细解释了如果只选择当前时间直接作为snapshot timestamp的问题, 本质上就是因为get或者put操作需要持续一段时间, 所以算法进行了优化来解决这个问题. 方法就是维护active表, 当然active表尽可能lockfree来做到nonblocking. getSnap会选择比所有active表里timestamp都小的一个作为timestamp. 考虑到getSnap也支持并发操作, 需要仔细更新snapTime变量, 为此引入了CAS操作. 19 | - getTs操作会选择大于snapTime的最小timestamp返回 20 | 21 | snapshot的功能让原本简单的流程复杂化了, 论文中还给出了read-modify-write的算法, 详细算法大家阅读论文吧. 在实现上, cLSM基于levelDB修改了代码. levelDB使用了一个全局mutex来保护临界区, 因为只有单个写线程, 所以没有引入类似active表这样复杂的机制. cLSM使用上述算法支持了原生levelDB的所有接口, 尽可能的消除了代码中需要block的地方. 由于cLSM消除了单个写线程的限制, 所以log中的数据可能会出现无序现象, 不过由于每个item都关联了timestamp, 所以按照时间来recover日志也非常容易. 22 | 23 | 总结下, 论文通过巧妙的设计并发控制算法, 最大限度的减少了代码的临界区, 提升了读写并发度. 不过我认为支持并发写对性能提升帮助不大, 因为不管是SSD还是机器硬盘, 顺序写的性能都要高于并发写. 对于读性能提升来说, 从论文中给出的数据来看效果不错, 提升了一倍以上, 不过这只限于当数据读请求的瓶颈到达CPU的时候, 因为绝大部分瓶颈都在存储设备上而不是CPU. 所以整体上看论文的研究成果实用性不是特别强, 仅仅对少量情况有性能提升的效果和优势, 另外这篇论文的正确性缺乏论证, 如果论文能够提供一份机遇TLA+的形式化证明, 可能就更完善了. 24 | 25 | ## PebblesDB: Building Key-Value Stores using Fragmented Log-Structured Merge Trees 26 | 这篇论文发表在2017年的SOSP上, 读完之后发现论文的思路和dostoevsky有异曲同工之妙. 我们知道LSM的模型主要的写放大在于compact, 特别是对于类似levelDB这种leveling compact style来说, 需要多次读写level i和level i+1的数据, 因此会引入更多的IO操作. 为此RocksDB引入了tiering compact style, 仅仅在同一个level上进行compact, 不引入层与层之间的compact. 但是这种策略对读请求来说却非常不友好, 因为每次读需求多次IO. 27 | 28 | PebblesDB引入了所谓的Fragmented Log-Structured Merge Trees结构, 思路来源于skip list. 其实现在看来, 和tiering compact style也非常类似, 核心思路如下图所示. 在每个level中, 设置若干个guard, 同一个level上这些guard包含的key range是不能交叉的. 每个sstable根据key range来决定属于某个guard. 同一个guard内, compact的逻辑就非常简单, 仅仅进行merge形成一个新的sstable下沉到下一层就ok了. 这样每个sstable在每一个level内最多只会merge一次了, 可以减少大量的读写IO. 不过因为最后一层的sstable不能下沉了, 所以最后一层就必须进行rewrite了, 这样可以减少最后一层key range交叉的sstable数量. 同时倒数第二层也有可能进行rewrite, 比如当最后一层对应的guard数据满了的时候. 当然论文中还包括不少细节, 比如如何选择guard的key range, 如何并行compact, 如何添加或者删除guard, 如何条件参数, 分析算法复杂度等等. 相信如果看完dostoevsky这篇论文之后, 会理解的更为透彻, 因为dostoevsky像是PebblesDB的加强版. 29 | 30 | ![image](http://note.youdao.com/yws/public/resource/79570ffa03dcce127a39aaaa7419c180/09DD50AFD2564A6F86C7C87E97BBBEEA?ynotemdtimestamp=1546605107269) 31 | 32 | ## WiscKey: Separating Keys from Values in SSD-Conscious Storage 33 | 这篇论文可以说是提出了一个非常开脑洞的想法, 而且想法还非常容易理解. 那就是既然LSM-tree模型虽然实用, 但是存在非常严重的读写放大问题, 而产生读写放大的根源就是需要不停的compact. 那如果把value独立存储到一个叫做vLog的文件里, 使用lsm-tree只存储索引的话, 就可以极大的减少lsm-tree后台compact的开销了. 不过想法虽然简单, 却仍然有不少问题需要考虑, 这篇论文针对这些问题给出了相应的解法, 下面针对每个问题, 总结下对应的解决方法. 34 | - 并行读优化. key value分开之后, value数据无序了, 这样无疑会增加scan的开销, 把顺序IO转变成随机IO了. 针对SSD, 论文中对顺序读和随机读进行了测试, 发现当一次读的数据块大于一定程度(论文中测试的数据是64KB)时, 并发随机读和单线程顺序读的读吞吐基本上接近了. 由此很自然的想法就是用并发读(32线程)来优化, 并且针对scan迭代器的Next()和Prev()接口, 进行预读优化. 35 | - GC优化. value保存在独立的vLog里, 不能和LSM-tree一起GC了, 所以需要单独设计GC机制. 简单的方法就是首先扫描LSM-tree的index, 所有在vLog中但是不在LSM-tree中的数据视为无效, 但是这种方式显然太重了. WiscKey提供了一种轻量级的方法. 在vLog里保存完整的key value数据, 并且使用一组head和tail指针来记录有效数据的区间. 在GC过程中, 从tail开始读取一组数据, 然后和lsm-tree的index进行比较. 如果数据有效, 则append到head位置, 然后更新tail指针. tail和head指针非常重要, 也需要持久化到lsm-tree中. 需要注意的是, 当value被append到vLog之后, 需要调用fsync()同步数据. GC过程无疑会产生文件的空洞, 为此可以使用fallocate()函数来高效的删除空洞. 36 | - Crash Consistency. LSM-tree可以保证插入的key value是原子的, 并且及时crash也仍然能够恢复. 但是引入了vLog之后, 灾难恢复的过程就会变得复杂一些. WiscKey的灾难恢复过程使用了现代文件系统的一个特性, 那就是如果数据X在写入过程中crash了, 那么X以后的数据一定不会成功写到vLog文件里. 为此在数据恢复的过程中, 需要验证LSM-tree的index指向的数据和vLog里的数据是否匹配, 如果不匹配则认为数据丢失了, 从LSM-tree中删除相应的index. 37 | - LSM-tree log优化. LSM-tree中通常包含一个log文件来实现数据持久化, 考虑到vLog中已经包括了完整的key value对了, 因此LSM-tree的log理论上就可以优化掉. 一个简单的想法就是启动后扫描整个vLog肯定能恢复完整的数据. 但是这种方法显然效率太低了, 为此在LSM-tree中记录vLog的head指针, 这样启动的时候就只需要从head开始扫描了. 38 | 39 | WiscKey这篇论文虽然思路不难, 但是却影响很大, 目前很多LSM-tree的存储引擎都在朝着这个方向优化. 但是仔细思考会发现GC过程实际需要考虑的问题比论文中还要多, 一个特别需要考虑的问题就是GC和真是写请求之间的并发控制问题. 假如GC和真实写请求在更新同一个key, 那就必须在GC的时候hold住真实写请求, 防止真实写请求的index被GC所覆盖. 但是这样block住写请求, 又会降低并发, 所以需要一种更好的解决方法了. 40 | -------------------------------------------------------------------------------- /polarfs.md: -------------------------------------------------------------------------------- 1 | # PolarFS: An Ultralow Latency and Failure Resilient Distributed File System for Shared Storage Cloud Database 2 | 3 | PolarFS是阿里云基于NVMe SSD和RDMA网络构建的具备低延迟的访问的新一代分布式文件系统. 这篇论文发表于2018年VLDB上. 本文对论文的要点和自身的理解进行记录, 起到帮助大家快速了解论文的作用. 4 | 5 | ## 背景 6 | 现代存储系统的研究开始密切关注新硬件对于存储系统的影响, PolarFS就是构建在新一代硬件上的分布式文件系统. 作为新一代SSD, NVMe SSD可以提供500K的iops, 平均延迟100us左右, 最新的3D XPoint SSD甚至可以将延迟降低到10us左右. 特别是最近Intel发布的SPDK开发套件, 可以bypass掉内核从而降低内核层面的软件开销. RDMA提供了低延迟的网络通信接口, 远远低于TCP/IP协议栈的网络接口. 应用程序通过API操作RDMA的三个队列, 分别是发送队列, 接收队列和完成队列. 发送和接收队列负责进行数据传输, 完成队列用来轮询数据传输完成的事件. 为了减少线程间切换的开销, PolarFS轮询完成队列. 7 | 8 | ## 架构说明 9 | 10 | ![image](http://note.youdao.com/yws/public/resource/4daef1dc09efae12290cb8e2d4b3209e/5A3FF9599E9947DA98FD0B1B26585953?ynotemdtimestamp=1536742335888) 11 | 12 | 如上图所示, PolarFS由4个部分组成, 分别是libpfs, PolarSwitch, ChunkServer和PolarCtrl. 从逻辑上说, PolarFS架构分成两大部分, 上层是文件系统层, 由libpfs和PolarCtrl共同完成文件系统层的抽象功能, 其中libpfs主要负责缓存元数据和向存储层发送IO请求, PolarCtrl主要负责处理元数据的读写请求(注: 这里说的元数据仅仅是与chunk server有关的元数据而不是文件系统层面的元数据). 下层是数据存储层, 由PolarSwitch, ChunkServer和PolarCtrl组成. 其中PolarSwitch负责缓存元数据, 并且转发IO请求, ChunkServer基于raft协议处理数据读写请求, PolarCtrl负责维护ChunkServer的状态和处理元数据的读写请求. 下面我们分别看看每个部分的详细功能. 13 | 14 | ###     1.libpfs 15 | libpfs以一个程序库的形式存在, 提供posix兼容的API接口, 同时缓存所有的元数据, 将posix接口的文件读写请求, 转化为下层的IO存储请求, IO存储请求由(volumeid, offset, size)这个三元组组成. volume在PolarFS中是一个逻辑概念, 类似于Linux LVM中的逻辑卷. 物理上volume由一组chunk组成, volume的大小可以从10GB到100TB之间. chunk是ChunkServer的数据管理单元, 也是raft复制组的最小单元, 每个chunk的大小为10GB. 在每个chunk内部, 划分成一系列定长的block, 每个block的大小为64KB. block是数据读写的最小粒度. 在使用文件系统之前, 需要调用pfs_mount接口, pfs_mount从PolarCtrl加载所有的元数据, 在内存中构建文件系统层元数据的数据结构(注: 论文中没有说元数据的大小, 能否cache所有元数据). 元数据结构如下图所示: 16 | 17 | ![image](http://note.youdao.com/yws/public/resource/4daef1dc09efae12290cb8e2d4b3209e/EDCB5182E1C24132BD8948F436DADA46?ynotemdtimestamp=1536742335888) 18 | 19 | 文件系统层的元数据主要包括3个部分, 分别是dir entry, inode和block tag. 这3部分和普通的本地文件系统类似, 比较容易理解. 根据这些元数据, libpfs就可以把posix接口的读写请求, 转化成ChunkServer层的读写请求了(volumeid, offset, size). 所有的元数据都用metaobject来表示和持久化, 为了修改或者更新一个或者多个metaobject, 需要将metabobject的修改封装成一个事务来执行. 为此PolarFS将事务的写请求保存在journal文件里, journal文件逻辑是一个循环队列, 同时为了确保多个进程安全的并发读写journal, PolarFS使用Disk Paxos算法实现了分布式抢锁的逻辑. (注: 从上面的图上推测, metaobject最终也是要持久化到chunk上的, 只不过为了保证原子性, 在每个libpfs进程里使用journal文件实现类似WAL的效果. 同时为了实现并发控制, 基于DiskPaxos实现了分布式锁.) Disk Paxos算法是paxos算法的一个变种, paxos算法加锁每个节点只能读写自己的磁盘或者内存, 使用消息传递的方式交换信息. 而Disk Paxos算法假设每个节点可以读写所有节点的磁盘, 从而将节点之前的消息传递改成读写本地和其他节点的磁盘数据, 这个算法对节点进程是否存活没有要求, 只要能成功写入多数派磁盘就可以保证信息一致, 适用于类似SAN这种共享存储的场景, 详细的算法大家可以搜索原始论文(笔者猜测是因为PolarFS基于RDMA通信, 延迟和可靠性和SAN类似, 所以采用了Disk Paxos算法, 不过没想明白为什么只是用于抢锁而不是也一起传输journal, journal是如何同步到每个libpfs进程的? 如果有同学有想法, 感谢回复我. 有一种可能是journal是存储在共享存储上, 这样方便所有的libpfs节点同步元数据的更新). 上面的图也说明了元数据的更新过程: 20 |     1. Node 1分配块201至文件316后, 请求互斥锁并获得锁 21 |     2. Node 1开始记录事务到journal中, 最后的写入项标记为pending. 当所有的项记录之后(注: 这里不太确定是不是说所有libpfs进程都收到记录之后), pending tail标记成journal的有效tail. 22 |     3. Node 1将元数据持久化到superblock(注: 应该是chunkserver中的block), 更新内存. 于此同时, node 2尝试获取互斥锁, node 2会失败重试. 23 |     4. Node 2在Node 1释放互斥锁之后抢锁成功, 读取journal的数据, 更新内存中的元数据. 24 |     5. Node 3抢锁成功之后, 读取journal更新本地内存. 25 | 26 | ###     2. PolarSwitch 27 | PolarSwitch是一个daemon进程, 相当于一个代理, 部署在每个libpfs的节点上. PloarSwtich接收到libpfs的请求后, 根据请求的信息, 转发给一个或者多个chunk server. PolarSwitch会混存元数据信息, 并且和PolarCtrl同步. 为了提升libpfs和PolarSwitch之间数据传递的性能, libpfs和PolarSwitch之间使用共享内存, 组织成一个环形缓存. 28 | 29 | ###     3. ChunkServer 30 | ChunkServer是数据的持久化层, 为了减少线程同步和资源争抢的开销, 每块NVMe SSD盘部署一个ChunkServer进程. 每个ChunkServer都有一个Write Ahead Log, 每个写请求都会先写入WAL, 然后在更新到block里, 为了降低延迟, PolarSwtich使用3D Xpoint SSD来保存WAL. ChunkServer之间使用ParallelRaft协议来实现数据副本的复制. 当ChunkServer之间负载不均匀或者局部故障时, chunk可以在不同的ChunkServer之间移动. 31 | 32 | ###     4. PolarCtrl 33 | PolarCtrl是PolarFS的总控模块, 主要职责包括: 34 | - 追踪ChunkServer的列表和存活性, 对ChunkServer进行负载均衡 35 | - 维护volume的状态和chunk位置信息 36 | - 创建volume, 给ChunkServer分配chunk 37 | - 向PolarSwitch同步元数据 38 | - 收集chunk的延迟和iops 39 | - 周期性的发起副本内和副本间的CRC数据校验 40 | 41 | ## IO执行流程 42 | ![image](http://note.youdao.com/yws/public/resource/4daef1dc09efae12290cb8e2d4b3209e/A1D9F7DDA1F847DD81B73B3313FF2639?ynotemdtimestamp=1536742335888) 43 | 44 | 了解了上述每个部分之后, 我们看看整体的IO执行流程. 详细的IO流程在论文里写的比较清楚了, 大家可以参考论文的说明. 为了提升IO的性能, PolarFS没有使用linux的文件系统, 而是使用SPDK读写NVMe SSD, 这样通过bypass内核的方式来提升IO性能, 基本上已经成为NVMe SSD使用的主流方式了. 45 | 46 | ## ParallelRaft 47 | 数据一致性协议基本上就是paxos和raft了, 但是众所周知, raft协议不允许日志出现空洞, 也就是说无论是leader还是follower, 都必须按顺序提交或者确认, 所以并发的写请求无形中就被串行化了, 这也是为什么很多数据库采用paxos而不是raft的原因. PolarFS设计了ParallelRaft协议来解决这个问题. ParallelRaft的核心思路是对于没有重叠的写请求, 可以允许出现空洞从而并行提交, 不会影响正确性, 但是对于有重叠的写请求, 则必须严格按照顺序提交. 判断是否重叠的方法是给每个log entry增加LBA信息, LBA包含之前还所有还没有提交的写请求的区间, 从而提供了判断写请求是否重叠的方法. 在工程实践上, 可以设置一个参数N, 作为允许的最大空洞的log entry个数, 这样可以保证空洞不会被无限放大, 减少LBA的维护成本. 在PolarFS的RDMA网络环境下, N设置成2就足够了. 48 | 49 | 在选举Leader的时候, Raft协议要求新当选的Leader必须包括最新的term和最长的log entry, 但是和Raft不同, 新当选的Leader可能存在空洞, 所以ParallelRaft需要一个merge过程来弥补空洞数据. 在merge过程中, leader处于leader candidate状态, 论文中详细的对需要考虑的各种边界情况进行了描述, 详细过程可以参考原文. 对于follow的数据复制过程, PolarFS同样业设计了fast-catch-up机制和streaming-catch-up机制来追赶leader的数据, 思路和一般的raft算法类似. 下图是raft算法和ParallelRaft的性能对比数据, 从数据上看吞吐提升还是挺明显的. 50 | 51 | ![image](http://note.youdao.com/yws/public/resource/4daef1dc09efae12290cb8e2d4b3209e/BF1E8E74E33B4FDA9E9E5B9ECA4ACDC7?ynotemdtimestamp=1536742335888) 52 | 53 | ## 性能测试 54 | 测试环境由6个存储节点和1个客户端节点组成, 节点之前使用RDMA通信, 使用fio产出测试数据. 55 | 56 | ![image](http://note.youdao.com/yws/public/resource/4daef1dc09efae12290cb8e2d4b3209e/1ABE0EF2519D4F19B654E3458DDDD63C?ynotemdtimestamp=1536742335888) 57 | 58 | 从论文上的数据来看, IO的延迟已经比较接近本地文件系统了, 而且比CephFS要好不少. 59 | 60 | ![image](http://note.youdao.com/yws/public/resource/4daef1dc09efae12290cb8e2d4b3209e/644CA18ACE5A46F2B78DC1EE2EC652BA?ynotemdtimestamp=1536742335888) 61 | 62 | 吞吐上看也远远好于CephFS, 和本地文件系统的差距也不大. 论文中还有阿里云RDS, PolarDB on PolarFS和PolarDB on ext4的性能对比数据, 详细信息可以参考论文. 63 | 64 | ## 设计抉择和经验教训 65 | - 中心化还是去中心化. PolarFS的chunk server层采用了中心化的设计思想, 上层文件系统层采用了去中心化的设计思想, 可以说是中心化和去中心化的折中. 66 | - 从底向上的snapshot. 快照是数据库的普遍需求, PolarFS实现了所谓的disk outage consistency snapshot机制, 并且制作快照期间不会影响用户的读写请求. ChunkServer基于Copy-On-Write机制实现了快照功能, 论文中写的不是很详细, 细节不太明白. 67 | - 外部服务和内部可靠性. 新节点的加入或者chunk的迁移, 需要同步大量数据, 这往往需要消耗较多资源. 为了减少对外服务的影响, PolarFS将每个chunk切分成128KB的数据块, 进而将数据同步任务切分成很多子任务. 这些小的子任务执行的快, 可预期性好, 更有利于ChunkServer进行调度. 其他类似的后台维护工作, 也采用类似的方法解决. 68 | 69 | ## 总结和思考 70 | PolarFS结合新型硬件设计了更符合现代硬件的分布式存储系统, 其设计思路和工程实践都有很高的参考价值. 不过需要注意的是, PolarFS的应用场景不是为了替换类似GFS/HDFS这种大规模分布式文件系统的, 更多的是为了满足分布式NewSQL数据库而设计的, 从支持最大的volume是100TB就可见一斑. 71 | 读完论文之后, 还有一些疑问, 记录在最后, 欢迎大家和笔者讨论. 72 | - 去中心化的文件系统层. 当节点较多时, 同步文件系统元数据可能开销较大, 这里面可能需要考虑比较多的工程问题. 73 | - 文件系统元数据的更新和同步机制比较复杂, 不太确定journal文件是存储在每个libpfs节点上, 还是存储在小型高性能共享存储上(比如SAN)? 如果不是存储在共享存储上, 那journal数据的同步机制是什么? 为什么不用一致性协议来实现数据复制? 74 | - 多个libpfs为什么使用disk paxos协议进行抢锁, 选择disk paxos协议的初衷是什么? 75 | - 既然ParallelRaft主要是为了解决日志空洞问题, 那为什么不直接使用Paxos协议? 76 | 77 | ## 参考文献 78 | - PolarFS: An Ultralow Latency and Failure Resilient Distributed File System for Shared Storage Cloud Database 79 | - [面向云数据库,超低延迟文件系统PolarFS诞生了](https://mp.weixin.qq.com/s/4s7lDKlQjV1mUoVv558Y7Q) 80 | -------------------------------------------------------------------------------- /surf.md: -------------------------------------------------------------------------------- 1 | # 读后感 SuRF: Practical Range Query Filtering with Fast Succinct Tries 2 | 3 | SuRF这篇论文是2018年SIGMOD唯一一篇best paper, 论文的核心思想是实现了一种叫做FST(Fast Succinct Trie)的数据结构, 既可以享受Succinct数据结构的高压缩特性, 还可以实现快速的point查询和range查询. FST本质上是一种高度优化之后的Trie树, 其实可以实现静态词典的数据结构. 论文中使用FST替换掉了rocksdb的bloomfilter, 在相同存储空间的情况下获得了查询性能的提升. 4 | 5 | ## Trie树 6 | ![image](http://note.youdao.com/yws/public/resource/fbcfe09e73906ae17ea9279fe69a7e4d/AF7BBDFFED774B76BDE7CCC902C9FA64?ynotemdtimestamp=1535524658253) 7 | 8 | 上图是维基百科中介绍的Trie树的例子. Trie树又称前缀树或者字典树, 是一种可以保存静态kv数据的数据结构. Trie树包括以下几个特点: 9 | 1. 一个节点的所有子孙节点具有相同的前缀 10 | 2. 从根节点到叶子节点可以唯一表示一个健 11 | 3. 可以实现基于前缀的模糊查询 12 | 4. 根节点对应空字符串 13 | 14 | ## Level-Ordered Unary Degree Sequence(LOUDS) 15 | 16 | ![image](http://note.youdao.com/yws/public/resource/fbcfe09e73906ae17ea9279fe69a7e4d/15C129E9C4B44FFEA41C9715F3BC09E6?ynotemdtimestamp=1535524348126) 17 | 18 | 对于一个树来说, 基于succinct的思路可以让树的存储空间接近信息论的下界. 上图将一个树的每个节点进行编码, 节点的编号按照层数生成. 编码规则就是对于一个节点来说, 将孩子节点标记为1, 最后标记为0. 比如对于节点3来说, 其编码就是1110. 按照节点编号的顺序, 生成一个bit序列从而完成整个树结构的编码(不包含value). 19 | 20 | 为了能够访问这棵树, 给定一个bit序列(起始位置是0), 定义四个基本操作: 21 | * rank1(i): 返回[0, i)位置区间内, 1的个数 22 | * rank0(i): 返回[0, i)位置区间内, 0的个数 23 | * select1(i): 返回第i个1的位置(整个bit序列) 24 | * select0(i): 返回第i个0的位置(整个bit序列) 25 | 26 | 为了计算方便, 在root节点之上, 增加一个新的root节点, 然后基于下面三个公式来访问整个树: 27 | 28 | * first-child(i) = select0(rank1(i)) + 1 29 | * parent(i) = select1(rank0(i)) 30 | * next-sibling(i) = i + 1 31 | 32 | 其中first-child(i), parent(i), next-sibling(i)都表示位置为i的节点对应的第一个子节点, 父节点和兄弟节点的位置. 大家可以使用上述公式计算下图中描述的树结构是否正确. 关于succinct tree的编码方式, 论文里写的比较简单, 论文的引文34给出了更详细的论述, 这篇论文是Jacobson在1989年发表的, 更详细的内容大家还是查阅论文, 里面有更多关于子节点的操作方法. 33 | 34 | ## Fast Succinct Trie 35 | 基于LOUDS编码方式, FST对LOUDS进行了进一步压缩, 下图介绍了基本的压缩方法: 36 | 37 | ![image](http://note.youdao.com/yws/public/resource/fbcfe09e73906ae17ea9279fe69a7e4d/5F2F376364BD4898A2A74DCFC322F0DB?ynotemdtimestamp=1535524348126) 38 | 39 | FST将LOUDS分成了两层, 上层节点数量少, 使用LOUDS-Dense编码方式, 下层节点数多, 使用LOUDS-Sparse编码方式. 40 | 41 | 1. LOUDS-Dense 42 | 43 | 我们先来看看LOUDS-Dense的编码方式. 假设每个节点最多有256个子节点, 那么在LOUDS-Dense编码方式中, 每个节点使用3个256个bit的bitmap来保存信息. 这3个bitmap分别是: 44 | * D-Labels: 将子节点的label变化置位 45 | * D-HasChild: 标记对应的子节点是否是叶子节点还是中间节点 46 | * D-IsPrefixKey: 标记当前前缀是否是有效的key 47 | 我们仍然可以使用select&rank操作来访问对应的tree节点. 48 | 49 | 2. LOUDS-Sparse 50 | 51 | LOUDS-Sparse使用3个bit序列来对trie树进行编码, 在整个bit序列中, 每个节点的长度相同, 这三个bit序列分别是: 52 | * S-Labels: 记录每个节点的label编号, key节点用0xFF标记, 按照树的层数按顺序记录(如果最多有256个子节点, 则每个节点占用4个byte) 53 | * S-HasChild: 记录每个节点是否含有子节点, 有的话标记为1, 每个节点使用一个bit 54 | * S-LOUDS: 记录每个节点是否是第一个节点, 每个节点使用一个bit 55 | 仍然可以使用rank&select操作来访问整个trie树. 56 | 57 | trie树经过LOUDS-DS编码之后, 可以高效支持下面3个操作: 58 | * ExtractKeySearch(key): 如果key存在, 返回value 59 | * LowerBound(key): 返回一个迭代器, 迭代器指向第一个大于等于key的位置 60 | * MoveToNext(iter): 移动迭代器指向下一个key-value 61 | * 62 | 63 | 3. LOUDS-DS的空间复杂度分析 64 | 65 | 给定一个含有n个节点的trie树, S-labes需要使用8n个bits, S-HasChild和S-LOUDS一共使用2n个bits, 所以LOUDS-Sparse使用10n个bits. LOUDS-Dense的空间与Sparse和Dense的分界线有关, 通常情况下, Dense占用的空间要远远小于Sparse部分. 这样整个LOUDS-DS编码的Trie树接近10n个bits, 理论证明最少的编码数量大约是9.44n个bits, 接近理论的下限了. 66 | 67 | ## Succinct Range Filters 68 | 69 | 虽然FST已经尽可能的使用最少的存储空间了, 但是我们仍然希望减少存储空间的占用, 进而让整个索引全部放在内存里, 为此引入了4种不同的Trie树的裁剪方式. 70 | 71 | ![image](http://note.youdao.com/yws/public/resource/fbcfe09e73906ae17ea9279fe69a7e4d/3C7E9E50369849D3A73610E2133C6B9F?ynotemdtimestamp=1535524348126) 72 | 73 | 1. Basic SuRF 74 | 75 | FST是一个完整的索引结构, 可以存储全部的索引数据, 这种情况下是100%精确的. Basic SuRF的思想就是只存储key的前缀, 实际上就是砍掉树的部分叶子节点. 我们使用FPR(false positive rate)来衡量效果, 具体的FPR与key的分布有关, 论文中给出了Basic SuRF的FPR的上限. 76 | 77 | 2. SuRF with Hashed Key Suffixes 78 | 79 | 为了降低FPR, 在Basic SuRF的基础上, 对key进行hash计算之后, 将hash值的n个bits存储到value中, 查询的时候还原回来完整的key. 这种方法可以降低FPR, 论文中有计算公式, 但是这种方法对range query没什么帮助. 80 | 81 | 3. SuRF with Real Key Suffixes 82 | 83 | 和SuRF with Hashed Key Suffixes不同, SuRF-Real存储n个bits的真实key, 这样point查询和range查询都可以获益, 但是在point查询下, FPR比SuRF-Hash要高. 84 | 85 | 4. SuRF with Mixed Key Suffixes 86 | 87 | 为了享受Hash和Real两种方式的优点, Mix模式就是将两种方式混合使用, 混合的比例可以根据数据分布进行调节来获得最好的效果. 88 | 89 | ## 性能测试 90 | 91 | 1. FST和基于指针的索引结构性能对比 92 | 93 | ![image](http://note.youdao.com/yws/public/resource/fbcfe09e73906ae17ea9279fe69a7e4d/F5C0B876BAB84C3C8C0444EB704AAFA6?ynotemdtimestamp=1535525810393) 94 | 95 | 论文中使用了两组key的数据进行性能对比测试. 一组是由YCSB输出的64bit的整数, 另一组是由字符串组成的电子邮件地址, 其中整数的key有50M个, 电子邮件地址组成的key有25M个. 然后使用FST分别和B+tree, ART(Adaptive Radix Tree), C-ART进行比较, 因为latency和memory实际上是两个trade-off, 所以上面的对比图中定一个了一个关于latency和memory的代价函数, 图中对比的是代价函数. 96 | 97 | 2. FST和其他succinct结构的性能对比 98 | ![image](http://note.youdao.com/yws/public/resource/fbcfe09e73906ae17ea9279fe69a7e4d/20A4CE6CCFA54721A3CFF3F308A408C4?ynotemdtimestamp=1535525810393) 99 | 100 | 第二组实验对FST和其他几种succinct数据结构进行了对比, 可以看出来无论是memory使用还是latency FST都是最优的. 101 | 102 | 3. SuRF和bloomfilter的性能对比 103 | 104 | ![image](http://note.youdao.com/yws/public/resource/fbcfe09e73906ae17ea9279fe69a7e4d/68BF960CC814437288EEE3154B2D4181?ynotemdtimestamp=1535525810393) 105 | 106 | 这幅图对比了SuRF不同模式和bloomfilter的FPR对比, 一般情况下, 在pointquery下, SuRF比bloomfilter还是要差一些. 对于email这组测试数据, range query的FPR比较高(20%~30%之间了). 107 | 108 | ![image](http://note.youdao.com/yws/public/resource/fbcfe09e73906ae17ea9279fe69a7e4d/57608D08022845FBBEDA7B9ADB54937B?ynotemdtimestamp=1535525810393) 109 | 110 | 这幅图对比了SuRF和bloomfilter的吞吐, 吞吐实际上指的是查询速度, 大家可以从这里大概评估出SuRF的吞吐数量级. 111 | 112 | ## 应用场景 113 | 试想如果我们把rocksdb的所有key都复制一份存储在SuRF中的话(不存储value), 那么SuRF起的作用不就和bloomfilter一样了么, 同时还可以支持range query了. 为此论文将SuRF应用在了Rocsdb中, 替换了bloomfilter, 并且进行了对比测试(占用的空间和bloomfiler相同). 测试程序运行在普通的SSD上, 下图是性能对比数据: 114 | 115 | ![image](http://note.youdao.com/yws/public/resource/fbcfe09e73906ae17ea9279fe69a7e4d/BD0D0D24ED964972821A72E3AD0AA599?ynotemdtimestamp=1535524348126) 116 | 117 | 从性能数据上看, 对于point query, SuRF的效果比bloomfilter相比还是差一些, 但是在range query下, 效果比bloomfilter要好很多了, IO减少的次数还是非常明显的. 118 | 119 | ## 代码 120 | SuRF的代码和rocksdb的集成代码已经在github上[开源](https://github.com/efficient/SuRF), 大家可以进一步了解代码. 作者代码封装的也比较工整, 读起来也比较顺畅. 121 | 122 | ## 总结 123 | 为了便于理解SuRF, 作者设计了一个[demo website](https://www.rangefilter.io/), 配合demo会更容易理解. 读完这篇论文之后, 最大的感受是之前的数据结构白学了! 在1989年就提出的LOUDS编码方法, 竟然完全不知道, 事实上, LOUDS已经在MIT的高级数据结构课程里了([youtube上有公开课视频](https://www.youtube.com/watch?reload=9&v=3Y2weLDiUWw/)). 作者在LOUDS的基础上设计了FST, 并且进行了相应的工程优化最终形成了SuRF, 无论是思路上还是效果上都非常出众, 能获得best paper还是很有道理的. 论文中的测试数据表明, 在和bloomfilter存储空间相同的条件下, point query的性能还是有所下降, 不过bloom filter本身占用的空间不大, 在我们的生产环境中, bloomfilter都是常驻内存的, 所以我觉得可以适当提升SuRF的空间占用来弥补point query的性能下降. 124 | 125 | ## 参考文章 126 | - [SuRF: 基于Fast Succinct Tries的Range Query Filter](https://blog.intzero.net/algorithm/database/SuRF.html) 127 | - [SuRF: 一个优化的 Fast Succinct Tries](https://www.jianshu.com/p/b3529729ee94) 128 | -------------------------------------------------------------------------------- /架构师.md: -------------------------------------------------------------------------------- 1 | # 架构师能力模型 2 | 架构师在很多人眼中是一个非常高大上的职业, 就像武侠小说中的绝世高手一样, 关键时刻可以起到扭转乾坤的作用, 是团队中的灵魂人物. 回想我自己做一线架构师的过程中, 也没有经历过比较系统的培训, 都是摸着石头过河. 近期在培养架构师的过程中, 促使我一直在思考, 一个合格的架构师到底应该具备哪些能力? 对希望成长为架构师的同学, 或者在承担架构师职责的同学, 需要提供哪些方面的指导和帮助, 才能让他逐步成长为合格的架构师呢? 下面我结合自己的经验, 总结了我认为对架构师来说非常重要的十项能力, 希望给那些努力成长为架构师的同学提供一点点帮助. 3 | 4 | ## 研发流程的持续改进 5 | 架构师不是单兵作战, 凭借个人英雄主义是无法做成大事的. 架构师一定是指挥一个团队来共同完成既定目标, 或者一个复杂项目. 在软件研发领域, 决定团队研发效率的核心在于研发流程的优化. 现阶段互联网公司大多采用敏捷研发流程, 这其中主要包括: 6 | * 需求卡片的状态流转. 尽可能依靠工具实现状态的自动化流转, 减少人为操作的情况. 7 | * 开发工具的选择. 比如web ide, 代码审查工具, 项目管理工具等. 8 | * 代码的开发和审查(包括单测代码和测试流水线代码). 开发功能代码的同时, 必须同时提交单测代码和测试流水线代码, 保证新增代码的基本功能被覆盖. 尽量不要分开开发, 保证测试代码的质量. 9 | * 测试流水线的建设和持续优化. 测试流水线需要在测试覆盖率和运行时间方面进行平衡, 测试覆盖率越大势必会增加测试时间, 如果每次提交pr, 都需要跑很长时间的测试流水线, 那么无疑会降低研发效率. 这时候可以采用测试case分级的方法, 设计ci pileline, daily pipeline, perf pipeline等多条测试流水线, 并且以不同的周期运行. 10 | * 持续发布和持续部署. 敏捷开发模式核心就是希望通过小步快跑的方式优化传统瀑布模型的阶段性开发模式, 让每次迭代尽可能快速的得到效果反馈, 从而可以针对反馈更快速的进行软件迭代. 这就要求我们一定要有持续发布和部署的能力, 可以采用灰度发布, A/B test等模式. 11 | 12 | 架构师需要对研发流程的每个环节保持着敏锐的嗅觉, 可以及时发现其中的问题, 并提出有效的优化方法. 我们经常讨论架构师要不要写代码的问题, 在我看来, 不管架构师是否动手写代码, 一定要对代码保持敏感. 保持敏感的方法就是对研发流程保持足够的把控, 参与代码审查, 持续的优化研发流程. 做职称评审的时候, 对于不做code review的架构师我一直是保持怀疑态度的, 我始终认为, 连代码都不进行review的架构师根本没办法真正指导一个项目或者一个技术方向, 至少一线架构师如此. 13 | 14 | ## 归纳抽象和技术泛化能力 15 | 架构设计很多情况下我理解就是将共性和差异化的东西分离出来, 共性的部分抽象成独立的接口, 功能模块或者组件, 差异化的部分分别形成其他代码模块. 那如何识别或者分析出共性的部分, 我认为主要就是依靠架构师的归纳, 抽象和技术泛化能力. 这需要架构师对问题进行反复深入的思考和对比, 透过现象探究事务的本质, 需要架构师拥有举一反三的能力. 锻炼这样的能力, 我觉得可以从日常编写代码中体会和训练. 比如把握好代码设计的SOLID原则, 在需要的时候对代码或者架构进行局部重构, 参考写的更好的代码或者架构设计等. 另外在日常工作中及时进行总结和复盘也非常有必要, 不要一味地低头走路, 在每完成一些阶段性工作之后, 对自己的工作过程和成果进行总结, 发现其中的优点和缺点, 避免走弯路. 16 | 17 | ## 业务和需求的分析和理解能力 18 | 架构的核心是为了业务服务的, 很多架构师觉得自己高高在上, 看不起业务研发同学, 这可不是什么好的想法. 架构就是要让业务更快速的发展, 所以架构师一定要接地气. 所谓的接地气, 我认为就是要加强对业务的深入理解, 能够预测业务的发展趋势, 提前在业务需要的技术方向进行适当的布局. 同时对业务提出的需求, 要多问多思考需求背后的本质是什么, 来帮助我们识别并解决业务真正的痛点. 对业务的理解也意味着架构师不会设计出天马行空不切实际的架构, 可以让架构师的架构设计更快的落地, 也让架构师能够更为顺畅的和业务研发同学进行沟通和交流. 所以架构师切忌不可脱离业务, 要时刻保持对业务的一定程度的理解能力. 19 | 20 | ## 技术折中和持续改进能力 21 | 很多架构师或者研发工程师都有所谓的代码或者架构洁癖, 动不动就想重构或者重写, 并自认为是一种非常好的品质. 这确实在某种程度上体现了工程师的主动性和匠心精神, 但是绝对要把握好时机. 我相信任何一家公司或者任何一个代码模块, 都有所谓的技术债务或者实现的不那么好的代码和架构, 希望短时间内彻底解决是不现实的. 架构师需要能够识别那些真正的技术债务, 并且要在适当的时机进行适当的重构来解决问题. 技术债务的识别能力是架构师抽象能力和业务理解能力等多方面能力的体现. 适当的时机指的是架构师能够在一定程度上预测未来的发展趋势, 同时结合当前的研发任务, 让架构朝着可以逐步迭代演化的方式来进化到理想目标. 迭代式的发展可以有效降低技术风险, 也可以让架构师更能充分的把握理想目标. 这个过程要注意避免两个极端. 一个极端是架构师动辄进行大规模的重构, 一个项目耗时数人年或者更长时间上线. 这种情况不仅项目风险极大, 上线之后非常容易回滚, 而且也大大减少了试错的机会. 可以说是不成功便成仁. 另一个极端是架构师仅仅就是新功能的建筑师, 不断的添砖加瓦, 只关注新功能的设计和研发节奏, 而不做任何架构优化工作. 这样时间久了之后, 项目就会越发难以维护, 周边类似项目不胜枚举. 当然上述两种情形毕竟只是极端情况, 更多的时候还是考察架构师对时机的把握了. 22 | 23 | ## 技术广度和深度 24 | 这一项能力都比较容易理解了. 架构师毕竟仍然是工程师, 而且大都是从一线研发工程师逐步成长和积累起来的, 在某一技术领域或者技术方向通常都有较为深入的理解和积累. 这里我想说的是, 不管是一线研发同学还是架构师, 至少应该在1~2个技术领域有着深入理解的基础上, 再同时涉猎技术广度. 如果缺乏对技术基础知识或者某个技术方向的深入理解, 那想继续在技术广度上拓展就非常困难了. 就像那些所谓的武林高手, 通常都有深厚的内功根基, 学习其他武学招数就会特别快一样. 技术广度通常也成为技术视野, 计算机技术发展特别迅速, 即使在BAT或者Google/Facebook等世界顶级科技公司, 也切忌固步自封, 要多了解多同类问题的架构设计和解决方案, 取其精华弃其槽粕. 平时多了解多对比相关技术, 在遇到类似问题的时候, 就可以多一些参考信息, 少走一些弯路. 25 | 26 | ## 持续学习的能力 27 | 计算机技术发展速度非常快, 持续学习能力对于计算机工程师来说都非常重要, 特别是架构师还要求开阔技术视野. 持续学习能力与其说是一种能力, 更多的还是一种习惯的养成. 大家可以自己回想一下, 每天读多少文章, 每周或者每个月读几本书, 平时对于读到的文章或者书籍有没有记录笔记等. 处于信息爆炸的时代, 我们可以接触到的信息也越来越多, 持续学习能力还要注意信息质量, 注意把握信息的核心内容, 对信息区分精读和粗读. 这里我觉得一些付费内容往往质量较高, 正所谓一份价钱一分货, 为知识付费投资自己还是挺划算的. 28 | 29 | ## 技术影响力 30 | 架构师一定意义上也是某些领域的技术专家, 打造个人技术品牌, 树立技术影响力对于架构师来说就更为必要了. 现阶段技术社区非常活跃, 架构师可以通过开源项目, 技术论坛, 开设技术课程, 发表学术论文, 在技术类大会上发表演讲等多种途径来提升个人的技术影响力. 可以先从公司内部做起, 平时指导一线工程师的过程中, 注意积累素材, 积累到一定程度之后, 可以开设一些技术类课程, 然后可以到一些技术会议上进行更大范围的技术演讲等. 参与开源社区也是一种比较好的途径, 同时还可以接触相关技术圈子, 大家交流技术互通有无, 其乐融融. 31 | 32 | ## 沟通表达能力 33 | 作为架构师, 日常工作的沟通或者工作汇报必不可少. 能否将问题和解决方案表达清楚, 对待不同的听众, 能否区别的进行沟通都对架构师沟通能力的考察. 我在日常工作中参与过多次的职称评审, 也参与过无数次的技术评审和工作汇报等会议, 我发现很多架构师的通病就是平时不注意培养沟通表达能力. 有的同学一上来就讲解决方案, 很多同学对问题的背景都还不清楚呢, 大家自然对解决方案一头雾水. 有的同学对解决方案的非关键细节花费了大量的时间进行描述, 丝毫没有全局视角或者整体的介绍, 让大家听的是云里雾里. 有的同学在工作汇报时, 对技术方案进行了全方位阐述, 而忽略了对最终结果的介绍, 那大家可以想想老板是什么感受. 有的同学在和其他团队合作的沟通中, 强势的要求对方积极配合, 而丝毫没有替对方考虑的意思, 那这样的沟通成功率可想而之了. 34 | 35 | 类似的例子不胜枚举, 那么如何培养沟通能力呢? 我认为首先要能够站在听众的角度思考问题, 明白听众真正想要的是什么. 比如做工作汇报的时候, 老板更多的想知道事情的结果, 或者项目的计划, 对技术细节往往不那么关心. 做技术评审的时候, 评审专家关注整体的架构设计和技术难点的可行性, 对非关键细节就不需要过多阐述. 希望和其他团队合作的话, 尽量从双赢的角度, 先描述对方的收益, 然后在表达对方需要投入的工作, 这样往往能够取得对方的支持. 36 | 37 | 当然沟通表达能力的基础上是归纳抽象和逻辑思维能力, 如果没有良好的逻辑, 那么其他一切沟通技巧都是徒劳的. 另外强调一个沟通表达的礼貌问题, 那就是在发表意见之前, 注意倾听对方的话语, 切忌打断其他人的讲话. 随意打断别人的讲话, 不仅仅沟通低效, 而且还十分不礼貌, 沟通过程中如果遇到经常打断别人讲话的架构师, 那基本上可以敬而远之了. 38 | 39 | ## 技术管理能力 40 | 架构师不是做完架构设计之后就可以高枕无忧了, 架构师往往要带领整个研发团队完成架构的落地. 这就要求架构师即使不是经理角色, 也要具备一定的技术管理能力, 从而带着整个团队一起完成工作. 管理工作的核心就是管人, 管事和管钱. 管人就意味着需要和人打交道, 理解团队成员的特点, 建立良好的信任关系. 管事就意味着管理项目和技术的落地, 包括项目计划的拆解, 项目执行进度的追踪, 项目技术难点的攻克等等. 管钱就意味着需要考虑成本的因素, 考虑投入产出比, 考虑激励因素等. 管理是一门学问, 非常复杂, 我写在这里是希望架构师不能一味的钻研业务和技术, 也需要学一些管理方面的知识. 41 | 42 | ## 坚持正确的价值观 43 | 写在最后我觉得不仅仅是架构师, 一个成功的人, 往往都需要具备正确的价值观, 这也是我们常说的德才兼备. 我这里不是想让大家喝鸡汤, 而是我觉得任何所谓的能力都可以有意识的构建起来, 但是一个人的价值观等因素, 却是很容易被大家忽视. 比如遇到问题, 能不能首先反思自己的问题, 进行自我批评, 而不是仅仅觉得都是外界的问题. 比如遇到困难或者逆境, 能不能有坚定的信念和勇气. 比如待人接物, 能不能坚持诚信的原则, 能不能信守承诺. 比如面对挑战和压力, 能不能有所担当, 不甩锅不逃避. 比如面对误解, 能不能坚持原则, 能不能内心坚强. 诸如此类情况, 还有很多, 我理解这都是构建正确价值观的一些因素. 这也就是为什么我们常说先学做人, 在学做事的原因吧. 44 | 45 | --------------------------------------------------------------------------------