├── HPC ├── gemini │ ├── Gemini A Computation-Centric Distributed.pdf │ └── notes.md ├── mlsys_roc │ ├── mlsys_roc.pdf │ └── notes.md ├── neugraph │ ├── NeuGraph_ATC_2019.pdf │ └── notes.md ├── nts │ └── notes.md └── pregel │ ├── 1807167.1807184.pdf │ └── notes.md ├── README.md ├── db ├── ARIES │ ├── aries.pdf │ └── aries_note.md ├── ARIES:IM │ ├── ARIES:IM.pdf │ └── notes.md ├── A_Comparison_of_Adaptive_Radix_Trees_and_Hash_Tables │ ├── alverez-icde2015.pdf │ └── notes.md ├── A_Critique_of_Snapshot_Isolation │ └── 2168836.2168853.pdf ├── Access_Path_Selection_in_Main_Memory_Optimized_Data_Systems_Should_I_Scan_or_Should_I_Probe │ ├── kester-sigmod17.pdf │ └── notes.md ├── Amazon-Aurora-Design-Considerations-for-High-Throughput-Cloud-Native-Relational-Databases │ └── aurora.pdf ├── An_Analysis_of_Concurrency_Control_Protocols_for_in-Memory_Databases_with_CCBench │ ├── notes.md │ └── p3531-tanabe.pdf ├── An_Empirical_Evaluation_of_In_Memory_Multi_Version_Concurrency_Control │ ├── notes.md │ └── wu-vldb2017.pdf ├── An_Evaluation_of_Concurrency_Control_with_One_Thousand_Cores │ ├── notes.md │ └── p209-yu.pdf ├── CockroachDB_The_Resilient_Geo_Distributed_SQL_Database │ ├── 3318464.3386134.pdf │ └── notes.md ├── Dont_Hold_My_Data_Hostage_A_Case_For_Client_Protocol_Redeesign │ ├── notes.md │ └── p1022-muehleisen.pdf ├── ERMIA_Fast_Memory_Optimized_Database_System_for_Heterogeneous_Workloads │ ├── ermia.pdf │ └── notes.md ├── Efficiently_Compiling_Efficient_Query_Plans_for_Modern_Hardware │ ├── notes.md │ └── p539-neumann.pdf ├── High_Performance_Concurrency_Control_Mechanisms_for_Main_Memory_Databases │ ├── notes.md │ └── p298-larson.pdf ├── Integrating_Compression_and_Execution_in_Column-Oriented_Database_Systems │ ├── abadi-sigmod2006.pdf │ └── notes.md ├── LLAMA_A_Cache_Storage_Subsystem_for_Modern_Hardware │ ├── llama-vldb2013.pdf │ └── notes.md ├── Magma-A-High-Data-Density-Storage-Engine-Used-in-Couchbase │ └── magma.pdf ├── Morsel_Driven_Parallelism_A_NUMA_Aware_Query_Evaluation_Framework_for_the_Many_Core_Age │ ├── notes.md │ └── p743-leis.pdf ├── Opportuinities_for_Optimism_in_Contented_Main_Memory_Multicore_Transactions │ ├── notes.md │ └── p629-huang.pdf ├── Serializable_Snapshot_Isolation_in_PostgreSQL │ ├── notes.md │ └── p1850_danrkports_vldb2012.pdf ├── Socrates-The-New-SQL-Server-in-the-Cloud │ └── socrates.pdf ├── TicToc_Time_Travling_Optimisitc_Concurrency_Control │ ├── notes.md │ └── tictoc.pdf ├── architecture_of_database_system │ ├── linziyu-Architecture of a Database System(Chinese Version)-ALL.pdf │ └── notes.md ├── arkdb_a_key_value_engine_for_scalable_cloud_storage_services │ ├── ArkDB.pdf │ └── notes.md ├── bw-tree-cmu │ ├── mod342-wangA.pdf │ └── notes.md ├── bw-tree-ms │ └── bwtree-ms.pdf ├── constant_time_recovery_in_Azure_SQL_Database │ ├── notes.md │ └── p2143-antonopoulos.pdf ├── fast_serializable_mvcc │ ├── notes.md │ └── p677-neumann.pdf ├── high-performance-transactions-in-deuteronomy │ └── high-performance-transactions-in-Deuteronomy.pdf ├── htap-tutorial │ ├── htap database.pdf │ └── htap.md ├── optimal_column_layout_for_hybrid_workloads │ ├── notes.md │ └── p2393-athanassoulis.pdf ├── scalable_garbage_collection_for_in_memory_mvcc_systems │ ├── notes.md │ └── p128-bottcher.pdf └── whatgoesaround-stonebraker.pdf └── distribute ├── GentleRain_Cheap_and_Scalable_Causal_Consistency_with_Physical_Clocks ├── 2670979.2670983.pdf └── notes.md ├── bigtable ├── 68a74a85e1662fe02ff3967497f31fda7f32225c.pdf └── notes.md ├── chubby ├── chubby-osdi06.pdf └── notes.md ├── gfs ├── gfs.pdf └── notes.md ├── mapreduce ├── mapreduce.pdf └── notes.md ├── more-gfs └── notes.md ├── more-raft ├── link.txt ├── notes.md └── stanford.pdf ├── percolator ├── Percolator.pdf └── notes.md ├── raft ├── notes.md └── raft-extended.pdf ├── session_guarantee_for_weak_consistent_replicated_data ├── SessionGuaranteesPDIS.pdf └── notes.md └── time_clocks_and_ordering_of_events ├── Time-Clocks-and-the-Ordering-of-Events-in-a-Distributed-System.pdf └── notes.md /HPC/gemini/Gemini A Computation-Centric Distributed.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ysj1173886760/paper_notes/df11876635eaa0da706671f51f6edd3b42e5391c/HPC/gemini/Gemini A Computation-Centric Distributed.pdf -------------------------------------------------------------------------------- /HPC/gemini/notes.md: -------------------------------------------------------------------------------- 1 | # Motivation 2 | 3 | 虽然最先进的共享内存处理系统可以高效的处理图。但是缺乏可拓展性使得他们无法处理那些单台机器无法承载的图。而分布式解决方案虽然可以将图拓展到更大的规模。但是他们的性能和成本效率往往不是很好 4 | 5 | 一个对于前沿系统的比较 6 | ![20220316193642](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220316193642.png) 7 | 8 | 可以发现分布式系统的网络没有饱和。限制他的主要因素是计算而非通信 9 | 10 | 与共享内存系统相比,他们执行了更多的额指令,更多的内存引用,更差的局部性以及多核利用率低。这种低效性有多个来源:(1)通过hashmap来在全局和局部状态间转换vertexID,(2)维护顶点的副本,(3)在GAS中的communication-bound apply phase,(4)动态调度 11 | 12 | # Gemini Graph Processing Abstraction 13 | 14 | graph processing problem updates information stores in vertices, while edges are viewed as immutable objects. 15 | 16 | undirected edges could be replaced with a pair of directed edges. 17 | 18 | For a common graph hprocessing application, the processing is done by propagating vertex updates along the edges, until the graph state converges or a given number of iterations are completed. 19 | 20 | 正在更新的顶点叫活跃顶点,他们的出边构成了活跃边集 21 | 22 | ## Dual Update Propagation Model 23 | 24 | 图处理的时候,活动边集的可能是密集的或者稀疏的。通常由活动顶点传出边的总数决定。 25 | 26 | 比如CC(connected components)在最开始的时候活动边集是密集的,但是会随着顶点接收到其最终标签变得越来越稀疏。SSSP(单元最短路)从一个稀疏的边集开始的,当更多的顶点被其邻居激活的时候,他会变的更加密集,当算法收敛时他会再次变得稀疏 27 | 28 | 稀疏情况下,更适合用push模型(每个点通过其出边来更新对应的点),因为我们只需要遍历活动顶点传出来的边 29 | 30 | 密集的情况下,则更适合用pull模型(每个顶点的更新通过入边来收集相邻顶点的信息),因为这样可以减少通过锁或者原子操作更新顶点时的争用 31 | 32 | 在gemini系统中,graph会被partition在多个不同的节点上,信息的传递和更新是通过显示的message passing。 33 | 34 | gemini使用master mirror的概念,每一个节点被分配给一个分区,在该分区中他是master vertex,作为维护顶点状态数据的主副本。同一个顶点会有多个副本,称为mirror,每一个mirror对应的分区上都至少拥有一个他的邻居。每个master-mirror pair之间有一条双向边,但是在任意一个传播模式下只使用一条。同时gemini的mirror不存储真实数据,只是占位符。 35 | 36 | ![20220316204536](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220316204536.png) 37 | 38 | signals和slots用来表示user-defined vertex-centric function,用来描述发送和接受信息的行为 39 | 40 | figure1中是两种模式下的操作 41 | 42 | ![20220316213633](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220316213633.png) 43 | 44 | 论文中这块有点怪怪的,我没太看懂为什么复杂度降低到了vertex数量 45 | 46 | # Distributed Graph Representation 47 | 48 | 在部署gemini的时候,graph必须进行partition 49 | 50 | 比如vertex-centric,即将顶点和相关数据均匀的分配到每个节点上 51 | 52 | 或者edge-centric,即将边均匀分配,并复制对应的点 53 | 54 | ## Chunk-Based Partitioning 55 | 56 | inspiration就是现实世界中很多的graph都是具有天然的locality的,因为我们一般是用爬虫去获得这些图 57 | 58 | 所以相邻的顶点就更有可能被存储到一起 59 | 60 | 即便是输入的数据的局部性丢失了,我们也可以通过其拓扑结构来恢复局部性 61 | 62 | gemini将图划分成p个连续的顶点块(v0, v1, ....,vp - 1) 63 | 64 | 其中vi表示的是分区i拥有的顶点(master顶点) 65 | 66 | ![20220317134050](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220317134050.png) 67 | 68 | 比较反直觉的一点是出边集指的是终点在vi的边集,入边集则是起点在vi的边集 69 | 70 | ![20220317134224](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220317134224.png) 71 | 72 | figure5是一个dense mode的划分,可以看到边都是从master指向mirror的。每一个mirror都会在本地收集数据,并发送给master节点 73 | 74 | 比如2号点,在p0和p2就会分别收集来自0和4的信息,并发送给在p1的master 75 | 76 | 这种连续的分区还简化了顶点数据的表示,每个node上的内存中只分配了他拥有的那部分顶点数组。我们不需要额外的进行顶点ID转化来压缩顶点状态的空间消耗 77 | 78 | chunk-based partition可以保存顶点访问的局部性 79 | 80 | 他原文里有一句这么写` Gemini can then benefit from chunk-based partitioning when the system scales out, where random accesses could be handled more efficiently as the chunk size decreases`,我的猜测是当chunk size变小的时候,我们的random access的范围就会变小,从而增强了局部性。(但是缺点是需要更多的master-mirror进行通信了,很直观,因为信息沿边传输是必须的,当每个机器内部负责的沿边传输少了,我们自然需要更多的网络通信) 81 | 82 | ## Dual-Mode Edge Representation 83 | 84 | gemini对于入边集使用的是CSR(Compressed Sparse Row)来存储 85 | 86 | 对于出边集则是CSC(Compressed Sparse Column) 87 | 88 | ![20220317140456](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220317140456.png) 89 | 90 | 这里是对于上面figure5的一个表示 91 | 92 | idx数组记录了每一个vertex的边的分布。对于vertex[i]来说,idx[i]和idx[i+1]构成了这个节点在这个分区中的边。 93 | 94 | nbr数组表示这些边的邻居(对于入边就是source,出边就是destination) 95 | 96 | 可以看到,当提高分区数的时候,我们的nbr会减少,因为我们只存了这个分区对应的边。但是对于vertex来说,他是不变的,仍然是O(V)的 97 | 98 | 引入两种优化的策略 99 | 100 | * Bitmap Assisted Compressed Sparse Row: 引入bitmap ext作为检查,表示当前的这个节点是否有出边 101 | * Doubly Compressed Sparse Column: 对于dense mode,只存储有入边的vertex(vtx),以及他们对应的offset(off) 102 | 103 | 我好奇为什么sparse模式下不用Doubly Compressed这种方法来压缩呢?是因为push这个操作是异步的吗?如果v很大的情况下用一个hashmap也比这样存好一些吧 104 | 105 | ## Locality-Aware Chunking 106 | 107 | gemini的chunk-based partition是vertex-centric,但是由于现实世界图数据的幂律分布,会导致load balance比较差的情况 108 | 109 | 然而在chunk-based partition中,即便是我们均分edge,也会导致显著的负载不均衡。因为顶点访问的局部性在不同的分区中有着显著的差异,这是由于顶点数量的差异较大导致的。 110 | 111 | gemini使用了一种locality-aware的分区方法。每一个partition都有一个均衡的数值 α · |Vi| + |EDi|。a是一个可以配置的数。所以我们希望a * 顶点数 + 出边数尽量均衡 112 | 113 | intuition就是对于计算的复杂度,我们对于顶点和边都要考虑。E会影响我们的工作量,而V则会影响局部性 114 | 115 | Question:V的数量导致的局部性影响这么大吗? 116 | 117 | ## NUMA-Aware Sub-Partitioning 118 | 119 | gemini通过recursively apply sub-partitioning来进行NUMA-Aware的子分区 120 | 121 | 对于一个拥有s个socket的node,我们可以继续分区为s个sub-chunk。而edge则会分配到对应的socket中,通过和inter-node一样的方法。 122 | 123 | 通过sub-partition,顺序的边访问和随机的点访问都很可能是在本地memory中进行,从而提高了LLC的利用率以及加快了访存的速度。 124 | 125 | # Task Scheduling 126 | 127 | Bulk Synchronous Parallel的示例 128 | 129 | ![20220317154140](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220317154140.png) 130 | 131 | 即Computation, Communication,以及Synchronous 132 | 133 | gemini使用了BSP这种模型。对于每次迭代来说,gemini通过cyclic ring order来重叠计算和通信。在节点内部,gemini使用细粒度的work-stealing scheduler来达到动态的负载均衡 134 | 135 | ## Co-Scheduling of Computation and Communication Tasks 136 | 137 | 对于拥有c个core的节点,gemini维护一个有c个thread的OpenMP pool,用来进行edge processing 138 | 139 | 2个额外的thread用来进行inter-node message sending/receiving 140 | 141 | ![20220317162113](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220317162113.png) 142 | 143 | 每一个iteration会被划分为p个mini-step 144 | 145 | 在figure7中,是node0的例子。他会与node0, 1, 2进行communicate 146 | 147 | 在每一个mini-step中,节点都会进行本地的denseSignal,message send/receive, denseSlot 148 | 149 | 在第一个mini-step中,本地的2和3做pull updates,从节点0和1获得信息(signal)。然后创建一个batch的消息发送给node1。然后node0会等待来自node2的信息,并执行denseSlot,更新本地的节点 150 | 151 | 计算和通信重叠的意思指的是,当一个节点计算完毕传输给下一个节点的时候,他是可以开始下一步计算的。这样可以充分利用网络和CPU资源 152 | 153 | ## Fine-Grained Work-Stealing 154 | 155 | 当partition越来越小的时候,我们就很难通过调整a来保证负载均衡了 156 | 157 | 所以gemini为节点内部的任务实现了细粒度的work-stealing 158 | 159 | 最开始在socket之间进行locality-aware的划分。每一个线程每次只拿一个小chunk进行处理。每个线程都会首先完成他对应的per-core task,然后开始从其他的线程上偷mini-chunk 160 | 161 | ![20220317190832](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220317190832.png) 162 | 163 | 每个socket上有若干个core,每个core有若干个mini-chunk,当一个core执行完以后就可以尝试去其他人哪里steal task 164 | 165 | 之所以用steal-work而不是交错划分mini-chunk,是因为steal-work具有更好的局部性,并且缓解了原子加的争用 -------------------------------------------------------------------------------- /HPC/mlsys_roc/mlsys_roc.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ysj1173886760/paper_notes/df11876635eaa0da706671f51f6edd3b42e5391c/HPC/mlsys_roc/mlsys_roc.pdf -------------------------------------------------------------------------------- /HPC/mlsys_roc/notes.md: -------------------------------------------------------------------------------- 1 | 就把这篇论文当作图计算的入门论文了 2 | 3 | ![20220312191047](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220312191047.png) 4 | 5 | GNN中一个顶点的计算过程 6 | 7 | 要收集他的邻居的信息,然后aggregation,再传入到传统的DNN中做分类/回归 8 | 9 | Roc用了一个linear regression model做partition 10 | 11 | 通过dp来最小化数据传输的代价 12 | 13 | GNN对于每一个vertex学习一个vector representation,并可以用这个representation给下游任务。比如做vertex classification, graph classification, link prediction等 14 | 15 | ![20220313154620](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220313154620.png) 16 | 17 | 公式是这样的 18 | 19 | h就是在第k层的激活(就是值),第0层的值就是输入的原始数据 20 | 21 | N就是所有v的邻居。a表示的就是这一层的聚合 22 | 23 | 可以看到对于第k层的聚合,是收集了所有邻居节点在k-1层的值得到的 24 | 25 | 然后计算节点在第k层的值就是用k-1层的值加上当前这一层得到的聚合 26 | 27 | 所以可以发现,第k层的activation就会汇聚最多K跳的邻居的信息。然后就可以用来做下游任务的输入了 28 | 29 | ## related work 30 | 31 | DNN computation可以在sample,operator,attribute,parameter这几个维度上进行划分,从而实现并行执行 32 | 33 | 比如sample上划分则是data parallelism 34 | 35 | 在operator上划分就是model parallelism 36 | 37 | 对于GNN的一个特点是在attribute上进行划分,比如在一个大型的单个样本上进行分区 38 | 39 | 由于highly connected natrue of real-world graphs。计算h的时候需要我们去获得相当多的邻居的数据。所以提出了down-sampling neighbors of each vertex 40 | 41 | ![20220313155838](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220313155838.png) 42 | 43 | 其中的N表示的是采样邻居节点所生成的子集,具有有限的大小 44 | 45 | 采样的方法会导致model accuracy loss 46 | 47 | ![20220313160307](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220313160307.png) 48 | 49 | 图中列举的划分图的方法 50 | 51 | equally就是相等的划分图 52 | 53 | GraphX和Gemini则是通过最小化启发函数得到的静态分区方法,比如最小化跨越分区的边的数量 54 | 55 | 这些划分方法在数据密集型的处理中具有良好的性能,但是由于逐顶点计算负载的变化,他们在计算密集型的GNN中无法很好的工作。 56 | 57 | 动态分区方法利用了图计算中迭代的特性。他们通过测量前一次iteration的performance来进行workload rebanlance 58 | 59 | 这种方法对于GNN training会收敛到一个平衡的工作负载,但是对于GNN inference来说就不太行,因为inference对于每一个新的graph只计算一次 60 | 61 | # Roc overview 62 | 63 | ![20220313202033](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220313202033.png) 64 | 65 | 输入GNN architecture和图,通过partitioner划分子图 66 | 67 | 然后每个节点内部有一个DPMM(dynamic-programming-based memory manager)用来减少在CPU和GPU之间的data transfer 68 | 69 | graph partitioner是和GNN一起训练的,并且也可以在新图的inference时候用来partition 70 | 71 | partition以后,每个子图都发给对应的节点。使用更大的DRAM来保存全部的数据,并将GPU memory看做cache。但是在GPU和DRAM之间传输tensor还是对runtime performance有很大影响。所以Roc用DP来最小化data transfer 72 | 73 | ![20220313212317](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220313212317.png) 74 | 75 | gather就是neighborhood aggregation,后面的就是正常的DNN 76 | 77 | For each state S, we define its active tensors A(S) to be the set of tensors that were produced by the operations in S and will be consumed as inputs by the operations outside of S. Intuitively, A(S) captures all the tensors we can cache in the GPU to eliminatefuture data transfers at the stage S. 78 | 79 | ![20220313221138](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220313221138.png) 80 | 81 | 用dp来求最优传输 -------------------------------------------------------------------------------- /HPC/neugraph/NeuGraph_ATC_2019.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ysj1173886760/paper_notes/df11876635eaa0da706671f51f6edd3b42e5391c/HPC/neugraph/NeuGraph_ATC_2019.pdf -------------------------------------------------------------------------------- /HPC/neugraph/notes.md: -------------------------------------------------------------------------------- 1 | # NeuGraph Programming Abstraction 2 | 3 | GCN: 4 | 5 | 初始情况下,每个vertex都有一个feature vector 6 | 7 | 每一个顶点都收集他邻居的特征向量,然后根据边上的权重进行加和。 8 | 9 | 然后一个全连接的NN来计算新一层的特征向量 10 | 11 | 比如在推荐系统中,如果用户对某一个item进行评分,就可以在用户顶点和item顶点之间连边,评分即作为边值。然后GCN可以从graph以及用户和item的特征中学习用户和item的embeddings。最后通过这些embedding来预测缺失的user-item评分 12 | 13 | GAT和GCD主要的不同点就是GAT对于每个边都计算一个attention value,用来在通过边传输features的时候使用 14 | 15 | ![20220317202535](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220317202535.png) 16 | 17 | ## Running Example 18 | 19 | G-GCN的例子 20 | 21 | ![20220317204556](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220317204556.png) 22 | 23 | 等式2代表了EdgeNN,通过两个点的feature来计算边权 24 | 25 | 等式1代表了VertexNN,他从邻居中聚合信息,并在外层做NN操作 26 | 27 | ## SAGA-NN Model 28 | 29 | ![20220317205000](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220317205000.png) 30 | 31 | SAGA-NN模型有两个user-defined functions。ApplyEdge和ApplyVertex。用来让用户去定义在edge和vertex上的NN计算操作 32 | 33 | ApplyEdge定义了在edge上的计算,输入为`edge`和`p`,其中`p`是可学习的参数。`edge`是一个tensor的三元组[src, dest, data]。这个函数可以用来在边上应用NN模型,输出的就是中间结果。对应图中的edge output 34 | 35 | ApplyVertex函数定义了在vertex上的计算,输入是`vertex data`,聚合后的数据`accum`,以及可学习参数`p`。返回的则是应用完NN之后的新的vertex data 36 | 37 | 另外两个state,Scatter和Gather,则是用来做数据传输,并将数据准备好提供给ApplyEdge和ApplyVertex的。 38 | 39 | 同时,NeuGraph也避免将聚合操作暴露给user,而是提供了一些默认的操作。我们通过设置Gather.accumulator来改变聚合的方式 40 | 41 | * Scatter将顶点数据传输给他的出边,从而构建edge 42 | * 然后ApplyEdge阶段会根据user-defined function会开始并行的计算,并为每条边生成结果 43 | * Gather阶段会将这些结果沿着边在目标点处进行聚合 44 | * 最后ApplyVertex会执行user-defined function,根据我们聚合的结果,这个点原始的数据最后得到下一层的数据 45 | 46 | ![20220317211412](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220317211412.png) 47 | 48 | 这个则是G-GCN在SAGA-NN模型上的实现 49 | 50 | 数据流抽象使我们表达神经网络结构以及自动微分更加容易。通过well-defined stages来建模GNN,可以让我们在graph computation以及dataflow scheduling进行优化 51 | 52 | # NeuGraph System 53 | 54 | NeuGraph包含了 55 | 56 | 1. 翻译引擎,用来将SAGA-NN表示的GNN翻译成chunk-granularity的dataflow graph 57 | 2. streaming scheduler来减少在GPU以及主机之间的data movement,并且最大化communication和computation之间的overlap 58 | 3. graph propagation engine来提供各种算子 59 | 4. dataflow execution runtime(这是用来干啥的) 60 | 61 | ## Graph-Aware Dataflow Translation 62 | 63 | 由于图数据无法放入到GPU中,所以目前的DL framework不能在GPU上直接使用 64 | 65 | 2D graph partitioning 66 | 67 | 将顶点数据划分成p等份。然后将邻接矩阵放到P×P的矩阵中 68 | 69 | ![20220317220409](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220317220409.png) 70 | 71 | 其中Eij中包含了Vi到Vj的边 72 | 73 | 这样划分的时候,我们可以对每个chunk单独进行处理,同时只会涉及到原点和目标点。这样就可以放到GPU中进行计算。 74 | 75 | ![20220317221050](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220317221050.png) 76 | 77 | figure5是计算v0的过程,前向传播 78 | 79 | 对于反向传播来说,由于ApplyEdge和ApplyVertex都是由tensor组成的dataflow computations。所以我们可以通过自动微分来的到反向传播的梯度。同时还提供了`backward-Scatter`和`backward-Gather`,一个用来收集梯度,一个用来分发梯度 80 | 81 | 计算的过程不需要全局的barrier,根据依赖关系不断的调度即可 82 | 83 | 在计算的时候,我们可以使用row-oriented或者column-oriented。对于前向传播来说,如果使用row-oriented的方法,那么要存储accum的结果的节点就会很多,导致我们不能很好的复用accum(虽然我们可以复用当前的节点),而对于column-oriented的情况下,我们可以直接计算出某一个chunk的accum的值,而且好处是这个accum的值还可以继续复用,在下面的ApplyVertex stage中我们可以用这个值来更新对应的节点。所以对于前向传播来说,column-oriented的方法更好一些 84 | 85 | 而对于反向传播的情况,梯度会从终点传向起点。所以在row-oriented的情况下,我们可以复用一个chunk下起点的梯度,并且后续可以直接更新他 86 | 87 | 决定vertex chunk的数量P也是比较关键的。比如在前向传播的时候,我们使用column-oriented,那么就需要进行P次IO来加载源点的chunk。所以希望有一个更小的P。在NeuGraph中,他们选择的就是能保证每个chunk存到GPU中最小的P。论文中这里的观点是通过次数来计算,但是如果看数量的话,总共加载的数据数量都是一样的。 88 | 89 | ## Streaming Processing out of GPU Core 90 | 91 | ### Selective Scheduling 92 | 93 | 由于有的时候一个edge chunk不会用到对应的vertex chunk中所有的点,比如上面figure5中的左下角chunk,就没有用到2这个点。所以为了减少CPU到GPU的数据传输,每次在CPU的数据中应用一个filter,把用不到的顶点过滤掉 94 | 95 | 但是这种做法对于random partition的情况下效果不好,比如我们比较希望的是比较密集的边块用到了所有的顶点,稀疏的边块只用到很少一部分。但是对于随机分区的情况下边块的分布也是均匀的。所以通过locality-aware graph partition的方法,让连在同一个点的边尽量都压缩到一起。这样就可以构成较为密集的边块。这样在访问顶点的时候就会有更强的局部性 96 | 97 | 当大多数顶点都是有用的时候,这时候不去应用filter是更快的,因为filter也会引入一次额外的CPU中的内存复制。所以通过有效顶点的比例以及GPU和CPU的速度来综合考虑 98 | 99 | ### Pipeline Scheduling 100 | 101 | 通过pipeline来重叠通信和计算的开销。比如当我们计算当前的edge chunk的时候,就可以将下一个edge chunk传入 102 | 103 | 在这种情况下,更小的chunk size会有更好的overlap的能力。类比CPU中的超流水 104 | 105 | 但是之前又说过希望通过扩大chunk size来减少IO。所以为了解决这种问题引入了sub-chunk。把每个edge chunk和对应的source vertex分割成若干个sub-chunk,这样我们就可以去并行的处理这些sub-chunk。 106 | 107 | ???我怎么感觉有点奇怪,这分割成了sub-chunk不是和分割为更小的p是一样的吗? 108 | 109 | 对于不同的sub-chunk我们可能有不同的数据传输量以及计算开销。所以为了更好的pipeline,NeuGraph在开始的时候首先生成一个随机的处理顺序,然后他会不断的交换sub-chunk的处理顺序,直到处理的时间收敛。(只交换sub-chunk,可能是为了防止破坏局部性) 110 | 111 | ![20220320092309](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220320092309.png) 112 | 113 | NeuGraph在前几次iteration收集每个sub-chunk的计算以及传输时间。然后在后面的iteration中,他会模拟当前schedule order的执行时间。看figure6,系统会找到传输时间远大于计算时间的sub-chunk,以及计算时间远大于传输时间的sub-chunk,并尝试交换他们,从而找到更好的执行顺序。 114 | 115 | 这里还是感觉怪怪的。 116 | 117 | # Parallel Multi-GPU Processing 118 | 119 | 感觉可以很直观的想到,forward的时候,一个GPU处理若干个dst vertex,他们之间不会有相互的关联 120 | 121 | ![20220320101556](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220320101556.png) 122 | 123 | 通过figure7可以看到,GPU之间的连接有的也是要通过CPU的。并且他们共享着PCIe总线。当GPU0和GPU1同时搬运数据到DRAM的时候,他们的带宽是跑不满的。我们会受到上层链路的限制 124 | 125 | 为了解决这个问题,NeuGraph使用一种chain-based streaming scheduling模式。因为每一个vertex chunk都会所有的GPU使用,所以我们可以通过GPU之间的PCIe switch来转发这些vertex chunk,而不是走DRAM这条线 126 | 127 | NeuGraph认为在相同PCIe switch下的GPU是一个大的virtual GPU 128 | 129 | 传递数据的顺序就是figure7中的红线 130 | 131 | 对于每个GPU来说,他们有两种操作,一种是从DRAM中加载edge chunk,或者从DRAM或者他的上一个GPU中加载vertex chunk,第二种就是执行计算 132 | 133 | ![20220320150815](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220320150815.png) 134 | 135 | 在figure8中演示的。最开始GPU0和2加载V0,并开始计算,与此同时他们可以从DRAM中开始传输V1的数据。而对于GPU1和3来说,他们会开始从GPU0和2那里获取V0的数据。之后系统就可以这样一直pipeline下去 136 | 137 | # Graph Propagation Engine 138 | 139 | 在ApplyEdge阶段,每一个顶点都有若干个出边,每次我们都会让顶点数据去与参数矩阵做矩阵乘法,从而导致很多的冗余计算。所以把这些独立的计算放到上一层中来消除冗余 140 | 141 | 在大多数的GNN计算中,ApplyEdge只会去做一些element-wise的操作,在这种情况下我们可以去融合这些算子,让他们可以直接在GPU寄存器中存储数据,而不需要涉及到数据的复制以及中间数据的暂存。NeuGraph将SAG融合成一个操作叫做Fused-Gather。他会首先加载Scatter的输入,比如源点数据或者边,然后用GPU线程来执行in-place的更新,最后生成每个点的acc。(我的理解就是将前面的GNN处理阶段的操作fuse一下) 142 | 143 | # Implementation 144 | 145 | 在tensorflow的基础上额外提供了 146 | 147 | 1. 将vertex-centric symbolic program转化成dataflow 148 | 2. streaming scheduler 149 | 3. graph propagation engine以及优化的kernel,用来实现Gather/Scatter operator 150 | 151 | ## Dataflow Translation 152 | 153 | NeuGraph提供了和传统operator类似的GNNlayer。他会将点和边划分成chunk,并根据user program来将GNNlayer和Gather/Scatter组合起来,生成基于chunk的dataflow graph 154 | 155 | ## Streaming Scheduler 156 | 157 | scheduler会尝试优化dataflow graph。然后根据有效顶点的数量决定是否使用filter。以及profile执行时的数据,并优化plan 158 | 159 | ## Multi-GPU Execution 160 | 161 | 在NeuGraph中,不同的GPU会并发的执行(我理解是用于通信用的)算子,在每个算子中他会申请空间并与其他的设备交换这些地址,从而实现D2D的数据传输。在不同的GPU中我们还需要去在每个iteration之间同步参数,这个是通过all-reduce来实现的。(?为什么不传输到CPU中然后用PS呢) 162 | 163 | ## Graph Propagation Engine 164 | 165 | NeuGraph实现了gather,scatter,fused-gather。 166 | 167 | scatter是一个map operator,将vertex data转化成edge data 168 | 169 | gather则是一个reduce operator,用来累加edge上的数据 170 | 171 | fused-gather则是当edge计算都是element-wise的时候,对应的one-pass computation -------------------------------------------------------------------------------- /HPC/nts/notes.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | 3 | 一层GNN layer的例子 4 | 5 | ![20220323091027](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220323091027.png) 6 | 7 | ![20220323090737](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220323090737.png) 8 | 9 | dependencies cached方法 10 | 11 | 对于一个k层的GNN模型,每个结点的k跳邻居的featrue都会被提前放到一个worker中,作为cache 12 | 13 | 比较明显的例子就是图中的partition0,可以看到虽然我们会在第二层中把节点3的信息传给1,但是在第一层中3还是聚合了2的信息,而不是从另一个worker哪里拿。 14 | 15 | 也就是说对于有关联的节点,会让他们在自己的worker中进行重复的计算 16 | 17 | 有一个直观的体会就是对于k来说,k越大我们要缓存的节点也就越多,从而导致了更多的重复计算。 18 | 19 | 另一个方法则是dependencies communicated方法 20 | 21 | ![20220323091614](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220323091614.png) 22 | 23 | 每次计算的时候不是重复的在本地进行计算,而是从远端的worker哪里拉取数据。 24 | 25 | 缺点就是comminucation overhead会很大,可能导致性能的下降 26 | 27 | 比如如果依赖少的情况下(以及较大的隐藏层,这个我不太明白),使用DepCache效果会好一些。因为冗余的计算不会影响性能。 28 | 29 | 而在high network bandwidth的情况下,对于高依赖的GNN数据,DepComm的方法会更好一些,因为communication cost不会影响太多。 30 | 31 | 后来问了学长明白了,这个hidden layer指的是中间层生成的顶点的feature,小的时候通信开销就比较小 32 | 33 | ![20220323093602](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220323093602.png) 34 | 35 | 论文中主要提到的就是hybrid的方法 36 | 37 | NTS结合了DepCache和DepComm来提高性能。NTS通过给出的图数据以及硬件环境,去估计DepCache和DepComm的开销,然后选择对于顶点最优的方式进行计算 38 | 39 | 通过vertex-cur master-mirror的方式来解耦跨越worker之间的图操作(gemini) 40 | 41 | # Execution Patterns of GNN 42 | 43 | ## GNN Training 44 | 45 | ![20220323094723](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220323094723.png) 46 | 47 | 一个Epoch下的GNN训练过程 48 | 49 | 输入是图数据,layer数量,顶点featrue,顶点的标签,以及对于每个layer的模型 50 | 51 | 输出的结果则是每个layer训练出来的模型 52 | 53 | 和之前的SAGA-NN类似,对于每个边,我们根据边的权值,以及边上两个顶点的feature来输入到NN中计算这个边的feature。然后对于每个顶点,聚合他的入边的feature,和当前层他自己的feature输入到NN中得到下一层的feature 54 | 55 | 然后通过feature来预测,得到loss。反向传播的时候则完全相反。首先每个顶点把他自己的gradient传给他的入边,然后每个边再把梯度传给边上的两个点 56 | 57 | 在算法中可以看到,loss会传给最上层的点,第l层的点的梯度会传给l层的边,然后l层的边会传给l - 1层的点 58 | 59 | ## Distributes GNN Training Approaches 60 | 61 | ![20220323103148](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220323103148.png) 62 | 63 | DepCache的训练过程 64 | 65 | 首先将V分割成若干个子集,分发给若干个worker 66 | 67 | 因为我们有l层GNN,所以对于每个worker,获取他们l跳邻居的信息。包括edge以及顶点的feature 68 | 69 | 然后执行algorithm1 70 | 71 | 这里需要同步的原因是我们每一个节点在同一层中使用的参数是相同的,所以有一个隐含的共享点就是需要共享模型 72 | 73 | 更新这个共享模型我感觉会需要很大的开销 74 | 75 | ![20220323210348](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220323210348.png) 76 | 77 | 相比与DepCache,DepComm就会显得复杂一些 78 | 79 | 最开始还是先分区 80 | 81 | 然后对于每一层来说,首先拿到remote worker上的数据,然后跑algorithm1 82 | 83 | 反向传播则是会首先计算顶点的梯度。然后把这些部分梯度发送给对应的remote worker 84 | 85 | 最后同步的更新参数 86 | 87 | ### Existing GNN Systems Review 88 | 89 | 对于DepCache的系统来说,为了减少重复的计算,他们只采样一部分多跳邻居的信息来进行训练 90 | 91 | sampling通常和mini-batch梯度下降训练方法相结合。虽然会牺牲一定的准确性,但是我们可以处理大规模的图数据了 92 | 93 | 对于DepComm的系统来说,他们不会有精度下降因为他们没有冗余的计算。所以他们的目的主要是去优化worker之间的通信以及主机和GPU之间的通信。 94 | 95 | ![20220323211440](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220323211440.png) 96 | 97 | ## Performance of the Two Approaches 98 | 99 | ![20220323211813](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220323211813.png) 100 | 101 | 通过vary一些参数去观察两种方法的性能 102 | 103 | 第一个图则代表了不同的图输入的情况,比如不同的依赖情况的影响 104 | 105 | 第二个图代表了不同的隐藏层大小,影响了DepComm传输数据时的大小 106 | 107 | 第三个图则vary了集群的环境,对应不同的网络带宽以及不同的计算能力 108 | 109 | 结论就是DepCache适合少依赖,较大的hidden layer size,以及高算力的集群 110 | 111 | DepComm适合多依赖,较小的hidden layer size,以及高网络带宽的集群 112 | 113 | # Hybrid Dependency Management 114 | 115 | 通过一个cost model来量化冗余计算开销以及通信开销。 116 | 117 | ![20220324200105](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220324200105.png) 118 | 119 | 冗余计算的公式,其中V(u)表示的就是u的入点,E(u)表示的就是入边 120 | 121 | 比如第一层的1号点,他的V就是0,3,5。因为他在上一层的入点就是0,3,5 122 | 123 | 而他的E就是(0,1),(3,1),(5,1) 124 | 125 | ![20220324201729](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220324201729.png) 126 | 127 | 其中d表示的是第k层的维度。Tv和Te表示每一维的vertex tensor和edge tensor计算的代价。注意本地的顶点和边集被去除掉了 128 | 129 | 而对于DepComm来说计算开销就容易一些 130 | 131 | ![20220324205941](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220324205941.png) 132 | 133 | u节点在第l层的通信开销其实就是把l-1层的u节点传过来。Tc表示的就是每一维传输的开销。d表示的就是维度 134 | 135 | Hybrid的方法则是根据cost model,选择部分节点做DepCache,选择部分节点做DepComm 136 | 137 | 这里我截图一下原文 138 | 139 | ![20220324210359](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220324210359.png) 140 | 141 | D表示远端的依赖,R表示DepCache的集合,C表示DepComm的集合。由R和C一同构成远端的依赖 142 | 143 | 结合起来考虑GNN的计算代价就是 144 | 145 | ![20220324210517](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220324210517.png) 146 | 147 | 其中miu是一个因子,因为DepCache的情况下很多的计算依赖都是有重复的。我们应该只计算一次 148 | 149 | 我们的任务就是找到最优的R和C的集合。这个问题可以被转化成一个经典的NP hard问题,叫做0/1 linear planning problem 150 | 151 | ![20220324211047](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220324211047.png) 152 | 153 | Algorithm4是一种贪心的方法来解决这个问题。 154 | 155 | 第一行是通过在一个小型的测试图上去探测前面需要的factor Tv,Te和Tc 156 | 157 | 然后预计算出每一个节点在每一层的依赖 158 | 159 | 然后对于每一层,首先把每个节点的tr值放到优先队列里。然后不断取出最小的tr值的依赖点u。如果这个点的tr小于tc,说明用Cache的方法更好,我们把它加入到R中,并且把这个点的依赖点加入到我们已有的依赖集合Vrep中 160 | 161 | 最后剩下的没加入R的就是用DepComm的 162 | 163 | 依赖管理和图分区是正交的,所以可以使用不同的图分区策略来配合hybrid的依赖管理 164 | 165 | # NTS 166 | 167 | ![20220325085310](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220325085310.png) 168 | 169 | nts的架构,根据那些子模块也可以看到nts应用的优化 170 | 171 | lock-free的队列,任务的重叠,Ring-based通信以及基于source的sub-chunking 172 | 173 | ![20220325090236](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220325090236.png) 174 | 175 | 其他的和NeuGraph类似,主要就是GetFromDepNbr这块。这里会根据hybrid的策略来选择是从远端拉取数据还是说从本地拿缓存 176 | 177 | ![20220325090340](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220325090340.png) 178 | 179 | 这里则是一层GNN计算的过程。可以看到最后的参数更新是通过All-reduce完成的 180 | 181 | ![20220325091312](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220325091312.png) 182 | 183 | figure7中演示的是nts的DepComm的实现,通过master-mirror来实现双向通信 184 | 185 | partition0的1和0先发给partition1的结点2。反向传播的时候则是2结点发送给0和1,然后再发送给远端的master节点 186 | 187 | ![20220325093040](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220325093040.png) 188 | 189 | ring-based的scheduling 190 | 191 | 后面应该和gemini就类似了 192 | 193 | 最后这个lock-free的队列,貌似不是普通的lock-free,而是说通过目的地直接指定写入的位置,从而避免写冲突。 -------------------------------------------------------------------------------- /HPC/pregel/1807167.1807184.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ysj1173886760/paper_notes/df11876635eaa0da706671f51f6edd3b42e5391c/HPC/pregel/1807167.1807184.pdf -------------------------------------------------------------------------------- /HPC/pregel/notes.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | 3 | pregel的program model类似BSP。每一个iteration叫做一个superstep。每一个superstep,系统会在读取上一个superstep传给顶点的数据,并应用user-defined function,然后他会沿边将数据传输出去,从而让他的邻居在下一个superstep使用这些数据 4 | 5 | 这种做法和MapReduce非常像,用户给出处理每个顶点的逻辑,然后系统会将这个操作应用到大规模的数据集上,并且不会暴露出执行顺序以及superstep之间的通信细节。 6 | 7 | # Model Of Computation 8 | 9 | pregel的输入就是一个有向图,其中边和点都可以有权值。 10 | 11 | pregel的计算过程:输入图数据,然后初始化图,然后进行由global synchronizatio point分割的superstep,直到收敛 12 | 13 | 算法什么时候终止取决于vertex是否选择去halt。每个vertex最开始都是active的,当他们不再参与计算了(比如最短路里距离收敛了),他们就会vote to halt,进入inactive的状态 14 | 15 | ![20220321101002](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220321101002.png) 16 | 17 | 每个vertex的状态机表示。当其他的节点发送信息的时候,他们就会再次被激活(比如spfa中更新了节点距离,那我们就要让这个节点入队) 18 | 19 | 一个传播最值的例子 20 | 21 | ![20220321101536](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220321101536.png) 22 | 23 | 图上的算法可以被表示为一系列的MapReduce操作。但是MapReduce是函数式的,对于多个链接起来的MapReduce操作来说,我们需要去传输整个状态。而对于pregel用的这种模型,我们只需要传输messages(顶点的数据之类的东西)。从而减少网络的负担 24 | 25 | # API 26 | 27 | pregel提供了一个aggregator,每次superstep每个顶点都可以向aggregator发送一些消息。aggregator就可以聚合并统计这些信息。比如计算当前图的边数等 28 | 29 | 同时aggregator也可以用做global coordination的方法。因为每个顶点都可以访问到aggregator中的数据,所以我们可以在aggregator中通过一些谓词来判断是否满足条件。 30 | 31 | 还可以通过aggregator来实现一些高级的操作,比如分布式的优先队列。在当前superstep中,每个节点发送他们的index以及distance。然后在aggregator中统计出最小距离的顶点们,让他们在下一个superstep中继续更新。 32 | 33 | 某些算法需要去更改图的拓扑结构,比如聚类算法可能将一个cluster替换成一个vertex。或者MST(最小生成树)可能会移除很多边。这时候我们就可能出现竞争,比如两个顶点同时要求添加一个idx相同但是初始值不同的顶点。 34 | 35 | 通过partial ordering和handler来解决这个问题。在一个superstep中,我们首先删边,然后删点,然后添加点,再添加边,最后根据Compute执行mutation。这种偏序关系让我们可以解决掉很多的冲突 36 | 37 | 剩余的竞争则是通过用户指定的hanlder来处理。比如添加或者删除相同的vertex或者edge。否则的话系统可能挑选任意一个操作来执行 38 | 39 | pregel的coordination mechanism是lazy的。对于这些global mutations(我理解就是图的拓扑结构变化),直到他们真的被应用的时候才会去协调冲突,从而有助于stream processing。比如对于一个点V的修改的冲突会在V上体现出来的时候再去让V自己来协调。而不是说让一个global coordinator去协调所有的操作。 40 | 41 | # Implementation 42 | 43 | ## Basic Architecture 44 | 45 | pregel将图划分为若干个分区,每个分区是一组顶点,以及他们的出边。一个顶点具体的分区位置取决于顶点的ID,所以只通过顶点ID就可以知道其他顶点存储的位置。默认情况下分区就是hash(ID) mod N 46 | 47 | 不考虑fault的情况下,pregel的执行由若干个stages组成: 48 | 1. user program会被分发到集群并执行。其中的一份拷贝会作为master,master不会获得graph的任何部分,而是负责协调worker。worker使用集群管理系统的name service来找到master的位置,并向master发送注册信息 49 | 2. master负责决定graph有多少个分区。然后将这些分区分发给每个worker。master可以向同一个worker发送多个partition,从而可以达到load balancing,以及更好的利用并发。每一个worker都负责维护自己的那部分图,执行Compute以及管理消息的传入和传出。 50 | 3. master会把用户的输入划分并分配给user。这里输入的划分和图的划分是正交的,并且输入的划分一般是根据文件的边界划分(GFS的文件)。如果worker加载的数据属于他自己,他就会更新对应的数据结构。否则的话他就会把这些数据发送给负责这块数据的worker 51 | 4. master负责指示每个worker来执行superstep。worker会遍历他的active vertices,每一个分区用一个线程来执行Compute,并且接受到上一个superstep中的消息。为了实现communication和computation的overlap,消息的传输是异步的(我猜测就是处理一个batch的节点,然后发送这个batch节点对应的消息,与此同时处理下一个batch的节点。同时batch内部还可以用上面的combiner进行合并)。当worker完成,他就会向master发送还有多少个活跃的顶点。master会不断重复这个操作直到不再有活跃的顶点。 52 | 5. 当计算终止了以后,master可以告诉worker去保存他的那部分数据 53 | 54 | ## Fault tolerance 55 | 56 | pregel通过进行checkpointing来实现fault tolerance 57 | 58 | worker与master通过交换心跳来确保对方存活。如果worker发现收不到master的心跳,他就会终止程序。如果master收不到worker的心跳,他就会认为这个worker失效了。 59 | 60 | 当一个或者多个worker失效的时候,master就会对现有的worker进行重新分区。然后这些worker会从最近的检查点重新加载数据(因为checkpoint应该是在GFS内) 61 | 62 | worker会在superstep中log他们发送的message。当我们知道了那些partition被丢失的时候,我们就可以只从checkpoint恢复这些丢失的分区,然后对于丢失的分区根据那些被log的消息来进行重新计算。 63 | 64 | 这种方法节省了恢复时的计算资源。因为这时我们只需要重新计算和传输和丢失分区有关的数据。但是logging增加了开销。目前的机器中不会收到这些logging的overhead的影响。 65 | 66 | 但是这种方法要求用户的算法是确定性的,否则恢复的分区就会接受到来自之前的消息以及recovery的消息。这样的话可以fall back到基本的recovery方法来进行恢复 67 | 68 | ## Worker implementation 69 | 70 | worker负责维护他自己这部分的图的状态。可以被认为是vertexID到每一个vertex状态的一个映射。vertex的状态由vertex value,他的出边的链表,incoming messages的队列,以及一个flag表示active-deactive的状态 71 | 72 | worker在执行superstep的时候,他会循环每一个vertex,然后执行Compute,传入他当前的值,以及incoming message的迭代器,还有出边的迭代器 73 | 74 | 当一个worker处理他的顶点的时候,在另一个线程会同时接受来自其他worker的消息。 75 | 76 | 当Compute希望发送给其他vertex消息的时候,worker首先确认目标点是不是在远端。如果是在远端他就会把消息buffer起来等待传输。当buffer的大小到达一个阈值的时候,buffer就会被异步的刷新并发送消息。对于本地的传输,我们可以直接把message放到对应顶点的queue里 77 | 78 | 这里注意,对于当前的superstep,每个vertex有一份顶点数据和他对应出边的数据的拷贝。但是有两个vertex flag和incoming message queue的拷贝。因为两个flag标识他当前的状态,以及下一个superstep的状态。而queue一个存储当前用的消息,一个用来接受下一个superstep的消息(异步传输) 79 | 80 | 如果user提供了combiner,在消息放到传输队列的时候以及在输入队列接受到的时候,他们就会被应用。前一个减少了网络负担,后一个减少了用来存储message的空间。 81 | 82 | ## Master implementation 83 | 84 | master维护了每个worker的信息,包括worker的标识符,地址,负责的partition等 85 | 86 | 大多数master的操作,包括输入,输出,计算,保存checkpoint和恢复checkpoint都会在barrier中结束。master会给所有的worker发送相同的request,然后等待回复。如果有worker失效了,master就会进入恢复模式。如果barrier synchronizatio成功了,master就会进入到下一个阶段。 87 | 88 | ## Aggregator 89 | 90 | 每个worker都会维护一个aggregator实例的集合。在每一个superstep中,worker就会将提供给aggregator的值进行部分的聚合。在每个superstep的最后,worker会以树形来聚合不同worker的部分聚合值,从而形成全局的聚合,并在最后发送给master。master会在每一次superstep开始的时候将这些值发送给每个worker 91 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Paper Notes 2 | 3 | This repo contains the notes that i've take while i was reading the paper 4 | 5 | For DB part, mostly i was referring the reading list in CMU15-721 6 | 7 | For Distribute part, mostly i was referring the MIT6.824, but there is also some paper from other reading list. 8 | 9 | And HPC part is a quick introduction to GNN training system/Graph processing system. 10 | 11 | The reading list that i'm referring: 12 | [CMU15-721](https://15721.courses.cs.cmu.edu/spring2020/schedule.html) 13 | [MIT6.824](http://nil.csail.mit.edu/6.824/2021/schedule.html) 14 | [PingCAP](https://github.com/pingcap/awesome-database-learning) 15 | [AwesomeDistributedSystems](https://github.com/theanalyst/awesome-distributed-systems) 16 | 17 | If you have questions while reading those paper, feel free to contact and have a discussion with me. I'm interested in digging details on those papers with others. 18 | 19 | Happy Reading! -------------------------------------------------------------------------------- /db/ARIES/aries.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ysj1173886760/paper_notes/df11876635eaa0da706671f51f6edd3b42e5391c/db/ARIES/aries.pdf -------------------------------------------------------------------------------- /db/ARIES:IM/ARIES:IM.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ysj1173886760/paper_notes/df11876635eaa0da706671f51f6edd3b42e5391c/db/ARIES:IM/ARIES:IM.pdf -------------------------------------------------------------------------------- /db/ARIES:IM/notes.md: -------------------------------------------------------------------------------- 1 | # ARIES/IM 2 | 3 | ## Tree Architecture 4 | 5 | A key in a leaf page is a key-value, record-ID pair. 6 | 7 | The leaf pages alone are forward and backward chained. 8 | 9 | A high key stored in the nonleaf page for a given child page is always greater than the highest key that is actually stored in that child page. 10 | 11 | Basic Index Operations: 12 | 13 | 1. Fetch: Given a key value or a partial key value(prefix), check if it is in the index and fetch the full key. 14 | 2. Fetch Next: Having opened a range scan with a Fetch call, fetch the next key satisfying the key range specification. 15 | 3. Insert: Insert the given key(key-value, RID). For a unique index, the search logic is called to look for only the key value since duplicates must be avoided. For a nonunique index, the whole new key is provided as input for search. 16 | 4. Delete: Delete the given key. 17 | 18 | Problems: 19 | 20 | 1. 怎么生成日志 21 | 2. 怎么保证SMO在恢复后的一致性 22 | 3. 怎么执行操作以减少对其他并发访问的影响 23 | 4. 怎么保证事务rollback不会影响SMO 24 | 5. logical undo 25 | 6. 仍然是logical undo 26 | 7. 避免死锁 27 | 8. 支持不同粒度的锁 28 | 9. 处理phantom problem 29 | 10. 唯一索引的处理 30 | 11. SMO与其他操作的并发执行 31 | 32 | ARIES通过dummy CLR来保证SMO不会被回滚。i.e. 在SMO之后加一个PrevLsn在SMO之前的CLR。这样在rollback的时候就不会回滚SMO。 33 | 34 | ## Concurrency Control 35 | 36 | ![](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220918152422.png) 37 | 38 | ARIES/IM的特点: 39 | 40 | 1. treating as the lock of a key the same lock as the one on the corresponding record data in a data page 41 | 2. not acquiring commit duration locks on index pages even during SMOs 42 | 3. allowing key retrievals, inserts, and deletes to go on concurrently with SMOs. 43 | 44 | To lock a key, ARIES/IM locks the record whose record ID is present in the key. We call this `data-only locking` 45 | 46 | 其他的方法,SystemR和ARIES/KVL中会对KV上锁,DB2和SystemR会做Page Locking。这里称他们为`index-specific locking` 47 | 48 | With data-only locking, the current key is not explicitly locked by the index manager during key deletes and inserts, since the record manager would have already locked the corresponding data with a commit duration X lock during the data page operation. 49 | 50 | The explicit locking of the deleted or inserted key by the index manager is needed only if index-specific locking is being done. 51 | 52 | During fetch and fetch next calls, the index manager locks the current key, the record manager does not have to lock the corresponding record during the subsequent record retrieval from the data page. 53 | 54 | 说白了就是index specific locking会在各种操作的时候都在index manager以及record manager中上锁。而index locking则只会在insert/delete的时候在record manager中上锁,在fetch的时候在index manager中上锁。 55 | 56 | 参考一位前辈的[文章](https://blog.mwish.me/2022/04/30/ARIES-IM/),里面有一个index specific lock和data-only lock的示意图 57 | 58 | ![](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220918155314.png) 59 | 60 | 这里是innodb的示意图。index manager说的就是在index上上锁。对应就是二级索引上的gap锁。record manager在这里说的就是在primary key(cluster index)上上锁。对应了索引和数据。 61 | 62 | 插入和删除会锁住next key。来防止phantom 63 | 64 | Latching: Not more that 2 index pages are held latched simultaneously at anytime. In order to improve concurrency and to avoid deadlocks involving latches, even those latches are not held while waiting for a lock which is not immediately grantable. 65 | 66 | Latch coupling is used while traversing the tree. 67 | 68 | ### SMO 69 | 70 | SMO是bottom-up的。为了避免死锁,会先释放下面的page的锁,再获取父节点的锁。 71 | 72 | 这样读者可能看到一些不一致的状态。他通过SMO_Bit来标识当前结点正在进行SMO操作。从而让其他人等待。 73 | 74 | SMO之间的操作是串行的。Tree-level有一个SMO RwLock。 75 | 76 | > SMOs within a single index tree are serialized using an X tree latch that is specific to this index 77 | 78 | 说白了,SMO_bit的作用就是避免我们每次都需要获取SMO的的lock。所以每次只有当发现当前page上标记了smo bit的时候,才会尝试获得S lock,来等待SMO结束。 79 | 80 | ![](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220918164316.png) 81 | 82 | 阴间画质... 83 | 84 | 当child是非叶节点的时候,如果他的high key大于当前输入的key。说明即便有SMO叶不会受到影响。或者输入的key大于child中的high key且当前没有SMO。说明这次操作没有受到SMO的影响,只是我们需要拓展high key而已。那么我们会正常执行。 85 | 86 | 如果本次受到了SMO的影响,那么我们应该等待SMO结束,先释放锁,然后获取SMO lock的读锁等待他结束。根据记录的LSN找到最早的没有被影响到的page。重新下降。 87 | 88 | 叶子节点的话,即遍是受到了SMO的影响,我们也可以跟到左右邻居的page上执行操作。但其实感觉叶节点上重做一下上面的逻辑更好一些。即发现SMO的时候重新定位一下。 89 | 90 | ### Fetch 91 | 92 | ![](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220918165550.png) 93 | 94 | Fetch是单点查。如果没找到的话会找next key。 95 | 96 | 如果发现当前page上没有就会持有latch并找到下一个page。找到next key并锁住。 97 | 98 | 如果next key没找到会锁住一个特殊的锁叫EOF。 99 | 100 | 文章中描述的lock会假设锁会立刻被持有。为了避免死锁并且允许更高的并发度,当我们没能够获取这些锁的时候,会做以下几步: 101 | 102 | 1. all the latches must be released 103 | 2. the lock must be requested unconditionally 104 | 3. once the lock is granted, a verification must be performed. 105 | 106 | tree latch also should not be requested unconditionally while holding page latches. 107 | 108 | 这里的conditionally说的应该是try lock。而且注意这里说的是lock。因为lock持有时间较长。而非latch。 109 | 110 | 之所以要锁住next key是为了防止phantom 111 | 112 | 在fetch next的时候,返回给User我们就会放锁。回来的时候会重新检查page上的LSN。如果发生了变化,那么next key会通过一次Fetch来重新获取。当有not found的时候同样要获取next key的锁,来防止phantom。(所以可能获取锁的range要大于我们想要的) 113 | 114 | ### Insert 115 | 116 | ![image-20220918172945790](/Users/bytedance/Library/Application Support/typora-user-images/image-20220918172945790.png) 117 | 118 | RR通过在next key上获取X lock来防止phantom。x lock是瞬时的。同时也是为了防止有其他事务删除了相同的key,但是还没有提交,我们就需要等待。 119 | 120 | non unique的情况下因为锁住的是整个key,所以其实和unique的情况类似。 121 | 122 | ### Delete 123 | 124 | ![](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220918180927.png) 125 | 126 | 删除的时候会获取在next key上的commit duration的x lock。来防止其他事务插入或者删除这个key。同时防止phantom。 127 | 128 | 当发现删除的Key是最大或者最小的key的时候,即修改了当前page的boundary,他就会获取一下SMO lock 129 | 130 | ### SMO 131 | 132 | ![](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220918182310.png) 133 | 134 | SMO的时候先捞起page。再X lock住整个tree。 135 | 136 | 从下到上设置SMO_Bit,然后修改双向链表指针,记录log,在unlatch page。 137 | 138 | 继续向上并设置SMO_Bit。 139 | 140 | 写入dummy clr来跳过SMO的undo。 141 | 142 | 重设SMO_bit为0。 143 | 144 | 这里不同的是,如果是删除,就先删除,再SMO。如果是插入,则先SMO,再插入。直观的感觉可能是和空间有关。 145 | 146 | 上面Insert和Delete都有对SMO_bit的处理。所以这里的清空SMO bit是可选的。其他的人来了会用SMO lock互斥一下,然后帮助清理这些bit。 147 | 148 | 这里delete bit的设置要仔细考虑一下。 149 | 150 | 为什么insert可以是instant duration,而delete需要是commit duration。 151 | 152 | 因为我们需要有一个点可以让其他的事务在和我们冲突(materialize conflict?)。对于insert来说,他这个kv本身就可以让其他人看到,就算读也有tuple level的lock。而对于delete来说,如果我们把一个kv删掉了,由于他随时可能回滚,其他人就有可能看不到这个kv对了,所以需要在next key上上锁,这样后续的人就可以通过锁上的词冲突意识到这次删除。 153 | 154 | > as long as the key is in the uncommitted deleted state, we need to leave behind a strong lock on a still-existing key for other transactions to trip on and realize that there is an uncommitted delete. 155 | > 156 | > The inserted key itself serves as the trpping point, whereas for delete the tripping point has to be another key which must be guaranteed to be a stable one. 157 | 158 | ## Recovery 159 | 160 | 这一节提到了ARIES/IM的恢复。其实和原本的ARIES没有什么区别。 161 | 162 | SMO不能被undo,所以写入结束后会写一个dummy CLR来跳过对SMO的undo 163 | 164 | 这里比较关键的是说了一下上面的插入删除的算法里面一些步骤的理由。比如为什么删除操作需要一个Delete Bit。 165 | 166 | 这里提出了一个概念叫`a point of structural consistency`(POSC),代表的是树的结构是一致的。 167 | 168 | 在进行SMO的过程中会破坏掉树的结构。 169 | 170 | ![](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220924175429.png) 171 | 172 | 这里应该是在非叶节点没有用到BlinkTree的技术。执行的顺序是T3进行SMO,将P3的指针从P1移动到P2.然后T1删除了P6中的一个元素,T2在P6中插入了一个元素,然后Commit了。这个时候宕机了。那么恢复的时候我们就需要先把T1回滚掉,即把数据从P6中插回来,然后再回滚这个SMO。 173 | 174 | 但是问题在于,P6可能已经满了插入不进去了,导致我们这次回滚需要做逻辑Undo。但是由于SMO没有被回滚,导致我们没办法从root重新下降。(如果是blink tree 就可以,而且ARIES/IM用的就是类似blink tree,所以感觉是没啥问题的) 175 | 176 | 解决问题的办法就是在这种可能需要逻辑回滚的情况下,保证树的一致性,也就是POSC 177 | 178 | ARIES/IM中列出了这些情况: 179 | 180 | If an operation performed originally at time t1 needs to be undone at time t2, then during such an undo, tree traversal is performed only if page-oriented undo cannot be performed due to 181 | 182 | 1. lack of enough free space on the original page to undo a key delete, thereby necessitating a page split SMO 183 | 2. the key definitely does not belong on the original page anymore: in the undo of a key insert case, the key is not on the page anymore (caused by interleaving page split SMO) 184 | 3. it is ambigious whether the key belongs on the original page or not: undo of a key delete case - the original page is still a leaf page but the key to be put back is not bound on the page 185 | 4. the undo causes the original page to become empty, thereby necessitating a page delete SMO: undo of a key insert case - since at the time of the original insert there must have been at least one other key on the page. it means that there must have been a delete of a boundary key in the time between t1 and t2 186 | 187 | 翻译一下,undo的时候做插入导致SMO,由于中间的SMO操作导致key不在这个page上了,或者undo的时候做删除导致需要SMO 188 | 189 | 所以要undo就必须做逻辑undo(其实第四点我感觉可以先删除,再做SMO,但是这个SMO可能作用于一个不一致的树上,所以不行) 190 | 191 | 那么为了保证做逻辑undo时树的一致性,我们在执行上述操作的时候必须保证POSC 192 | 193 | 第一点就是通过delete bit来保证的。如果有其他的插入请求遇到的并发的删除请求的话,他就会先要求POSC,来保证后续对于delete的undo会看到一个一致的树。 194 | 195 | 第二点则不会有任何问题,因为这个SMO一定被回滚了,或者完成了。所以我们看到的一定是一个一致的树,只不过由于SMO导致page-oriented log失效了而已。 196 | 197 | 第三点,我们需要保证在修改boundary key的时候要满足POSC。所以在删除操作发现要修改boundary key的时候,要获取SMO lock保证POSC 198 | 199 | 第四点也不会有任何问题,因为如果这里undo需要触发SMO,中途一定出现了boundary key deletion。所以在t1到t2之间一定有POSC。 200 | 201 | 而对于算法中的SMO bit来说,是为了防止在SMO结束之前有人操作的SMO相关的page。由于SMO不是原子的,所以后续的Undo就会很难做。所以我个人感觉是为了简化undo。否则如果SMO是原子的话(MTR),那么上面的逻辑都可以省略掉,因为任何时刻树都是一致的。 -------------------------------------------------------------------------------- /db/A_Comparison_of_Adaptive_Radix_Trees_and_Hash_Tables/alverez-icde2015.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ysj1173886760/paper_notes/df11876635eaa0da706671f51f6edd3b42e5391c/db/A_Comparison_of_Adaptive_Radix_Trees_and_Hash_Tables/alverez-icde2015.pdf -------------------------------------------------------------------------------- /db/A_Comparison_of_Adaptive_Radix_Trees_and_Hash_Tables/notes.md: -------------------------------------------------------------------------------- 1 | # A Comparison of Adaptive Radix Trees and Hash Tables 2 | 3 | # Abstract 4 | 5 | 比较ART, Judy Array, 两种基于二次探测哈希的变体,三种Cuckoo Hashing的变体 6 | 7 | 结果发现ART和Judy都不能与哈希方法相比 8 | 9 | # Introduction 10 | 11 | 这里提到了这里的比较只用于integer: 12 | We only focus on keys from an integer domain. In this regard, we would like to point out that the story could change if keys were arbitrary strings of variable size 13 | 14 | 文章中提到了一个概念covering index/non-covering index(即索引内部是否存储了所有需要查询的key) 15 | 16 | 以及提到了Hashing approach中,hashing scheme和hash function都很重要(比如hash function可以决定我们的冲突等。而hash scheme决定怎么解决冲突,常用的链式方法就会导致比较差的局部性) 17 | 18 | # Radix Trees 19 | 20 | ART是一种trie树的变体。trie树的缺点就是局部性不好,并且很耗内存。因为很多的分支都是稀疏的 21 | 22 | trie树的优点: 23 | 1. 形状只取决于键的空间以及长度 24 | 2. 不需要rebalancing 25 | 3. 有序,支持高效的前缀查找 26 | 4. 允许前缀压缩(叶节点可以只存储后面的部分) 27 | 28 | Judy Array根据键的分布以及数据的基数来变化他的节点的表示。从而防止了过高的内存占用以及低局部性的问题 29 | 30 | ARTful index,也就是ART和Judy非常像,但是他没有像Judy那样设计为关联数组(比如map,这里说的应该是外层接口),而是针对主存数据库的索引设计的 31 | 32 | ## Judy 33 | 34 | 1. Judy1: A bit array that maps integer keys to true or false and hence can be used as a set 35 | 2. JudyL: An array that maps integer keys to integer values(or pointers) and hence can be used as an integer to integer map 36 | 3. JudySL: An array that maps string keys of arbitrary length to integer values(or pointers) and hence can be used as a map from byte sequences to integers 37 | 38 | 为了对比后面就关注JudyL 39 | 40 | Judy的作者发现cache miss对数据结构的影响是巨大的。我们希望避免cache-line fill(which can result in cache misses)。而Radix tree的最大缓冲行填充数取决于radix tree的高度的(每次访问都引起一次cache miss)。对于每一层来说,我们希望只访问一次cache line,并且里面保存了指针指向我们接下来要去的地方 41 | 42 | Judy使用了很多压缩技术来避免缓存未命中。大概可以分为两类,Horizontal compression和Vertical compression 43 | 44 | Horizontal Compression:这个技术是用来解决大个节点分散稀疏的问题的。Judy使用动态变化的节点大小。但是这时候节点就不能直接寻址了,我们需要通过比较才能知道节点具体在那里。Judy使用两种节点来解决这个问题 45 | 46 | ![20220426200809](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220426200809.png) 47 | 48 | linear node就是先存储了对应的键,然后再是值 49 | 50 | 而bitmap node则是把对应的位和指针交错分布。256的值域会被用8个32位的整数划分开。注意这会导致两次cache line miss 51 | 52 | Judy把节点的元信息存储在指针上,所以每个指针有16位,8位的物理指针,还有8位存储元数据 53 | 54 | Vertical Compression:当节点只有一个孩子的时候,我们可以跨越这个节点。也就是skipping levels。这时候缺失的节点的key会被存储到之前提到的元数据中,用来后续的比较。这个技术又被称为path compression 55 | 56 | 另一个技术叫immediate indexing。当后面的一系列节点没有分支的时候,我们可以直接把叶子节点的值存到上面指针的位置。从而可以快速到达叶节点。我感觉这个也是用来解决分散稀疏的问题的。 57 | 58 | ## ART 59 | 60 | ART和Judy有很多类似的地方。他也是256-radix tree。使用了(1) different node types for horizontal compression, and (2) vertical compression also via path compression and immediate indexing 61 | 62 | 而这里还有两个主要的区别。第一,ART中的元数据存储在header中。他有4种类型的节点,Node4,Node16,Node48以及未压缩的Node256。ART利用了SIMD的技术来加速查找(这个比较nb,但是还是有cache的问题)。第二,ART被设计为是数据库的索引,而Judy则是通用的关联数组(kv storage)。ART不需要保存完整的键值对,而是存储一个指向table的指针即可。所以ART主要用于non-covering index。 63 | 64 | ART中的Node4和Node16与Judy中的Linear Node相似 65 | 66 | ![20220426203149](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220426203149.png) 67 | 68 | Node48由256字节大小的数组,以及48个子节点指针组成。这里相当于只是把pointer压缩了一下。那为什么不设计类似的Node128什么的呢? 69 | 70 | Judy的bitmap节点弥补了大节点和小节点的gap,因为他是动态变化的(这么看来感觉bitmap的设计更好一些,但是bitmap插入可能需要重新分配内存?这样会导致大量的cache line miss) 71 | 72 | # Hash Tables 73 | 74 | ## Quadratic probing 75 | 76 | ![20220427101632](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220427101632.png) 77 | 78 | i表示的是第i次探测。二次探测的优势就是(1)容易实现(2)在哈希表的大小是2的整数次幂的时候,他可以保证每个slot都被使用到(不会出现空缺) 79 | 80 | 但是二次探测的缺点就是如果两个键在第一次探测出现了冲突。那么后面就会一直冲突。所以选择一个好的哈希函数是很关键的。(leveldb的那个bloom filter也会这样) 81 | 82 | 当table变大的时候,空间利用率低,但是效率高。而table小的时候效率低,但是空间利用率高 83 | 84 | ## Cuckoo Hashing 85 | 86 | 两个哈希表T0,T1,每一个都有他自己的哈希函数h0,h1。 87 | 88 | 每一个元素要么插入到T0[h0(x)],要么插入到T1[h1(x)] 89 | 90 | 当一个元素插入的时候,他首先探测T0,如果已经有元素y了,那么他会把y踢走,自己存到这个位置上。然后y就会去探测T1[h1(y)]。如果还是有其他元素了,y就会把那个人踢走,自己存到T1中。我们会不断循环,直到所有人都找到他的位置 91 | 92 | 但是这样可能会导致无限循环。比如当y把z从T1踢出来的时候,z需要从T0找位置。但是z之所以在T1就是因为T0有人占了他的位置。我们通过循环固定的次数来解决这个问题。当出现循环的时候,我们就会进行rehashing,选择两个新的哈希函数,然后把table中的所有函数都重新hash一遍。(这样的话rehash的时候会导致performance的问题) 93 | 94 | 当load factor小于50%的时候,cuckoo hashing才能有好的performance。但是我们可以增加table的数量,当有4个表的时候,load factor可以增加到96%,但是代价就是效率降低。 95 | 96 | 一个优化就是让一个slot里面不是只能存一个元素,而是让slot和cache line对齐 97 | 98 | 当table多的时候空间利用率高,但是效率会很低。table只有两个的时候效率高,但是空间利用率低 99 | 100 | cuckoo hashing的好处就是我们查询的次数与load factor无关。最多只有两次。但是缺点就是对于哈希函数很敏感。 101 | 102 | ## Hash functions 103 | 104 | Multiplicative hashing: 105 | 106 | ![20220427180519](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220427180519.png) 107 | 108 | 这个哈希函数是非常快的。我们可以在乘法之前就mod 2的w次幂,并且除法可以简单的通过右移来实现。并且他还有理论保证,两个数的冲突率是: 109 | ![20220427180727](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220427180727.png) 110 | 这个冲突率是理想的冲突率的2倍(应该算是非常小的了) 111 | 112 | 虽然Murmur hash相当robust,但是会带来巨大的performance degradation。我们认为multiplicative hashing是足够robust的,并且有很好的performance 113 | 114 | -------------------------------------------------------------------------------- /db/A_Critique_of_Snapshot_Isolation/2168836.2168853.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ysj1173886760/paper_notes/df11876635eaa0da706671f51f6edd3b42e5391c/db/A_Critique_of_Snapshot_Isolation/2168836.2168853.pdf -------------------------------------------------------------------------------- /db/Access_Path_Selection_in_Main_Memory_Optimized_Data_Systems_Should_I_Scan_or_Should_I_Probe/kester-sigmod17.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ysj1173886760/paper_notes/df11876635eaa0da706671f51f6edd3b42e5391c/db/Access_Path_Selection_in_Main_Memory_Optimized_Data_Systems_Should_I_Scan_or_Should_I_Probe/kester-sigmod17.pdf -------------------------------------------------------------------------------- /db/Amazon-Aurora-Design-Considerations-for-High-Throughput-Cloud-Native-Relational-Databases/aurora.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ysj1173886760/paper_notes/df11876635eaa0da706671f51f6edd3b42e5391c/db/Amazon-Aurora-Design-Considerations-for-High-Throughput-Cloud-Native-Relational-Databases/aurora.pdf -------------------------------------------------------------------------------- /db/An_Analysis_of_Concurrency_Control_Protocols_for_in-Memory_Databases_with_CCBench/notes.md: -------------------------------------------------------------------------------- 1 | # An Analysis of Concurrency Control Protocols for In-Memory Databases with CCBench 2 | 3 | 本文就是总结了一下之前比较经典的CC protocol,以及他们使用的优化。并将他们集成到了一个统一的CCBench中来做测试。 4 | 5 | 但是作为读者来说我们读的核心不在于他的bench,而是他所提炼出的这些cc protocol的关键点 6 | 7 | # Preliminaries 8 | 9 | 先回顾一下这几个协议 10 | 11 | ## Silo 12 | 13 | Silo的核心有两个,一个是避免读对内存造成影响(比如TO会在读的时候更新tuple的read timestamp)。另一个是通过基于Epoch的提交来实现parallel logging,以及实现pre-commit(其实这个地方严谨的说法应该是可以让事务读到pre-commit的状态)。 14 | 15 | (从OCC的角度来看,原始的OCC也不需要我们更新读集。所以Silo高性能的关键其实在于他的pre-commit) 16 | 17 | 这种不会更改内存状态的读叫做`InvisibleReads` 18 | 19 | ## TicToc 20 | 21 | tictoc通过data-driven timestamp management,根据事务的读写集合计算出一个ts。从而避免的TSO的瓶颈问题,同时获得了更大的调度空间。 22 | 23 | 然而相对于Silo来说的话,则是tictoc会更新读集,从而可能引发大量的Cache Invalidation 24 | 25 | ## MOCC 26 | 27 | MOCC我没有看过,根据论文介绍是结合了悲观和乐观的并发控制。还不太清楚和Hekaton的区别 28 | 29 | ## Cicada 30 | 31 | Cicada用本地时钟来分发时间戳,将start ts作为事务在serial order中的位置。并且不会处理写写冲突。write intent可以根据txn ts的顺序插入到版本链中 32 | 33 | Cicada应用了很多的优化。比如读的过程中可以提前abort。随着时间推进,会最终将最新的版本内联到main table中,从而避免indirection。 34 | 35 | Cicada的GC则是worker thread做协助性的GC。每次事务结束都会尝试GC。从而尽可能的减少内存的开销。 36 | 37 | AdaptiveBackOff会适应性的修改回退的时间大小,从而减少高竞争情况下事务的abort率 38 | 39 | Cicada根据他的时间戳机制,会在提交阶段写入write intent的时候,根据他的ts来排序。(他认为ts较高的元组更容易受到冲突,从而提前发现,并abort事务) 40 | 41 | ## SSN 42 | 43 | 据说是去检测dangerous structures来abort事务。不太清楚和PG的SSI的区别是什么 44 | 45 | # CCBench 46 | 47 | ![20220725202750](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220725202750.png) 48 | 49 | ![20220725202901](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220725202901.png) 50 | 51 | 这个table是CCBench提供的优化以及原文所使用的。比如Cicada的GC就可以应用到其他的MVCC系统中。 52 | 53 | ![20220725203031](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220725203031.png) 54 | 55 | 这里是对于上面优化的一个解释。其中AssertiveVersionReuse和ReadPhaseExtension是CCBench单独提出来的。 56 | 57 | # Analysis 58 | 59 | 说一下他的结论 60 | 61 | cahce line conflict,这个结论比较常见了,多核架构下同步缓存需要时间 62 | 63 | cache line replacement,当数据多的时候,memory footprint会变大,从而导致缓存命中率低 64 | 65 | 跨越socket的读写会导致L3 miss。所以InvisibleRead是一个比较关键的特性。即避免读操作写元数据,进而导致缓存失效 66 | 67 | 不能说Wait和NoWait那个更好,而是应该区分情况。当txn size变大的时候,Abort开销变大,这时候Wait会变得更好。而由于某些未知的原因,Silo的NoWait表现的比Wait更好。(并且在2PL的情况下,Wait要求对上锁有一个全序关系来防止死锁) 68 | 69 | ![20220725215302](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220725215302.png) 70 | 71 | 这个图表示的意思是Payload size变大的时候,性能可能提高。原因是较大的payload size导致read phase会变长。从而导致了并发的进入validation阶段的事务变少,进而减少了abort。(作者原文说的是read validation failures decreases,但是我感觉增长read phase不会减少failure,反而会增加,因为我们更有可能读旧数据。如果说论点是减少了由于上锁导致的abort可能还更好一点) 72 | 73 | 有关Version maintaince的结论。即便是使用了RapidGC,长事务也会很大程度上影响性能。TNM的那个Scalable GC貌似解决了这个问题,用更细粒度的GC。 74 | 75 | 文章中也提到了一个idea是可以利用Thomas写规则来减少version的数量。(但是不清楚效果,并且需要追踪ReadTS。貌似被generialized了以后叫做non-visible write。有点Immotal write的感觉?) 76 | 77 | 78 | -------------------------------------------------------------------------------- /db/An_Analysis_of_Concurrency_Control_Protocols_for_in-Memory_Databases_with_CCBench/p3531-tanabe.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ysj1173886760/paper_notes/df11876635eaa0da706671f51f6edd3b42e5391c/db/An_Analysis_of_Concurrency_Control_Protocols_for_in-Memory_Databases_with_CCBench/p3531-tanabe.pdf -------------------------------------------------------------------------------- /db/An_Empirical_Evaluation_of_In_Memory_Multi_Version_Concurrency_Control/notes.md: -------------------------------------------------------------------------------- 1 | 感觉这篇自己理解的也不是很通透,之后还需要再看。这里记一些读到的要注意的点 2 | 3 | 在多核机器上拓展MVCC不是一个容易的事情,当线程的数量变的很多的时候,同步所造成的开销会大于并发带来的加速 4 | 5 | MVCC的四个决策点:并发控制协议,版本存储,垃圾回收,索引管理 6 | 7 | MVCC主要的好处就是writer don't block reader, 以及 Read-Only txns can read a consistent snapshot without acquiring lock 8 | 9 | MVCC有个额外的好处就是如果不做垃圾回收的话,我们天然的可以得到time travel operation 10 | 11 | 在主存数据库中,MVCC的实现将一些meta-data内联到了tuple中。tuple header中有一项就是txn-id,他代表了这个tuple的latch 12 | 13 | 两个属性begin-ts和end-ts代表了这个tuple的生命期。还有一个pointer指向版本链中的上一个版本或者下一个版本 14 | 15 | 并发控制协议: 16 | 17 | Timestamp Ordering(MVTO) 18 | 19 | 在header中维护一个read-ts,表示读到这个tuple的最大的txn的id,和Timestamp Ordering的方法一样,通过timestamp来确定执行顺序 20 | 21 | 对于同样想修改同一个tuple的不同txn,先修改的人胜利,后到的txn则会abort 22 | 23 | MVTO允许txn读到一个还未提交的条目,所以为了保证可串行化,我们需要额外的数据结构来维护abort对tuple的影响。 24 | 25 | 多版本对于TO的改进其实是提供了一个consitent snapshot 26 | 27 | OCC (MVOCC) 28 | 29 | 这个就是在原本OCC的基础上加上了多版本 30 | 31 | 好处在于我们不用担心写的冲突问题,因为多版本会帮我们解决。所以validation阶段我们只需要去检验read是否有效即可。即是否读到了不该读的,或者没读到该读的tuple 32 | 33 | 2PL (MV2PL) 34 | 35 | 在tuple中内嵌了一个read-cnt,表示读锁,原本的txn-id表示写锁。这样可以通过CAS操作来实现latch-free的操作 36 | 37 | 让txn提交的时候,我们释放latch,并更新tuple的版本 38 | 39 | 但是说白了还是2PL,我们还是会有等待,所也也会出现死锁等情况。 40 | 41 | 在这里,因为我们内嵌了锁,所以如果要进行DL_DETECT的话需要额外的数据结构。因此我们使用死锁预防,比如no-wait,或者wait-die 42 | 43 | 个人的理解,MV2PL还是有等待的,所以MV的添加其实是缓解了2PL的冲突。原本所有的txn都要等待一个tuple,现在有了多个版本,冲突也就相应的减少了。 44 | 45 | 对于MVCC的read uncommitted,我们可以跟踪每个txn所读到的uncommitted data,并且只有读到的data全都commit以后才能提交当前事务 46 | 47 | 我们也可以让txn去更新一个被uncommitted txn读过的数据项。但是同样我们需要维护他们的依赖关系,并等所有依赖的txn提交之后,当前的txn才能提交 48 | 49 | 版本存储: 50 | 51 | DBMS使用tuple中的pointer属性来创建一个latch-free的链表,就是version chain 52 | 53 | 添加新版本的方法有append-only,就是在后面追加 54 | 55 | 还有就是对每个tuple维护一个单独的表,里面存储了历史版本。主版本在主表中,历史版本单独存在一张表 56 | 57 | 好处就是遍历历史版本很快,我们有顺序扫描。同时我们可以直接断开历史版本的链接,达到快速回收的目的 58 | 59 | 还有一种就是优化,我们不存储每个tuple,而是存储他们的变化,也就是delta storage 60 | 61 | 好处就是空间占用小,但是我们需要额外的计算来重构tuple 62 | 63 | 对于存储来说,之前的论文有提到过,就是可以申请 thread local的空间,而不是去访问全局的数据结构。中心化的数据结构带来的冲突会影响到效率。这样对于每个worker划分空间的做法也就相当于对数据库进行了分区 64 | 65 | 垃圾回收: 66 | 67 | 我们需要回收那些被abort txn创建的tuple,以及不再能被现在的txn看到的tuple 68 | 69 | 通过一个中心化的数据结构来跟踪tuple是否能被当前活跃的txn看到。但是这样会因为访问中心化的数据结构所导致的冲突而产生瓶颈 70 | 71 | 主存数据库通过细粒度,epoch-based的方法来跟踪这些信息 72 | 73 | 每个epoch包含了一些txn,epoch中应该是维护了版本相关的信息,以及活跃的txn,当没有活跃的txn,并且这个epoch的版本不再能被新的txn看到的时候,就可以清除这个epoch 74 | 75 | (这里我理解的不是很清楚,因为没有具体的实例) 76 | 77 | GC有tuple level的以及txn level的 78 | 79 | 对于tuple level的,我们有background vacuuming,就是后台进程来回收 80 | 81 | 还有coop的方法,就是当一个新的txn读version chain的时候,他顺便把不会再被读到的版本标记并清除 82 | 83 | txn level的就需要我们跟踪txn所更新的tuple,并且在这些tuple不再能被active txn看到的时候清除他们 84 | 85 | 索引管理: 86 | 87 | 我们有两种,逻辑指针和物理指针 88 | 89 | 对于物理指针来说,我们会在索引中指向版本链中所有的tuple。 90 | 91 | 当新的版本出现时,我们还需要将所有的辅助索引更新,添加这个新的key 92 | 93 | 对于逻辑指针来说,辅助索引指向的是主键或者一个对应tuple的唯一键,tuple-id之类的 94 | 95 | 逻辑指针指向的是版本链的表头,我们可能还需要一个额外的哈希表来映射tuple-id到具体的物理地址 96 | 97 | 但是更新的时候,我们就只需要修改这个哈希表即可,而不需要再去修改所有的辅助索引了 98 | 99 | 个人感觉就是没有实践的话,单独读论文的体验还是比较差的,我还是应该去找一些代码看,进而加深理解 -------------------------------------------------------------------------------- /db/An_Empirical_Evaluation_of_In_Memory_Multi_Version_Concurrency_Control/wu-vldb2017.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ysj1173886760/paper_notes/df11876635eaa0da706671f51f6edd3b42e5391c/db/An_Empirical_Evaluation_of_In_Memory_Multi_Version_Concurrency_Control/wu-vldb2017.pdf -------------------------------------------------------------------------------- /db/An_Evaluation_of_Concurrency_Control_with_One_Thousand_Cores/notes.md: -------------------------------------------------------------------------------- 1 | 论文的核心就是测试在多核的机器上测试不同的并发控制的方法的拓展性 2 | 3 | 主要使用了7种并发控制方法。 4 | 5 | 其中三种是基于锁的,分别是`Dl_DETECT`死锁检测,`NO_WAIT`获得锁失败时就abort,`WAIT_DIE`利用时间戳进行死锁预防。 6 | 7 | 四种基于时间戳,分别是`TIMESTAMP`通过记录在tuple上读写的时间戳实现串行化,`MVCC`创建tuple的多个版本,`OCC`完成工作后进行验证并判断是否有并发事务与当前事务冲突,`H-STORE`基于分区进行封锁,并根据时间戳分配锁 8 | 9 | 目前我们还是没有这么多核的计算机的,所以这个实验是在一个平台上模拟的多核CPU 10 | 11 | 将CPU组织成一个2D的网格结构,CPU共享L2缓存,并有独立的L1缓存。CPU之间通过network switch通信(模拟片上网络) 12 | 13 | 非一致存储模型,访存延迟会随着距离的增大而增大 14 | 15 | 实现中的优化: 16 | 17 | malloc效率很低,所以实现了一种会根据工作负载自动调整内存池大小的malloc 18 | 19 | tuple level的锁表 20 | 21 | 避免在关键路径上使用mutex,因为mutex会导致芯片之间的昂贵的消息传输。对于2PL来说,保护死锁探测器的mutex是一个瓶颈。对于T/O来说,我们也会在申请时间戳的时候用到互斥锁 22 | 23 | 优化2PL: 24 | 25 | 中心化的等待图和死锁检测算法会导致瓶颈。根据CPU核对我们用到的数据结构分区(等待图),同时使用lock-free的死锁检测 26 | 27 | 当并发度提高时,瓶颈就会出现在锁的等待上。当一个事务直到提交时才释放他获得的锁,他就会阻塞住大量其他的事务,导致出现瓶颈。 28 | 29 | 我们可以主动abort掉一些事务来减缓锁的争用。当等待锁的时间超过一个阈值的时候,我们就abort掉这个transaction 30 | 31 | 等待时间越短,abort rate越高。可以减少锁争用的问题。等待时间长则不容易abort。但是会增加锁争用的问题。所以实际中阈值的设定应取决与工作负载 32 | 33 | 优化T/O: 34 | 35 | 时间戳获取是一个比较关键的瓶颈。上面提到过使用mutex获得时间戳会导致瓶颈 36 | 37 | 一个改进的方法是使用原子加,原子加在多核的情况下效率会降低,因为这会导致维护缓存一致性上的拥塞。验证缓存副本以及写回缓存会消耗大量的总线带宽 38 | 39 | 还有一个改进的方法是使用批量的原子加,timestamp manager可以一次返回一批的time stamp来应对并发的request 40 | 41 | 还可以通过使用CPU时间加上线程ID的方法来构建独一无二的时间戳。这种方法具有较大的拓展性。在分布式数据库中,我们使用软件方法来获得时钟的同步。但是在many-core system中,使用软件方法会导致大量的开销,所以我们需要一些硬件的实现 42 | 43 | 最后还可以使用内建的硬件计数器来获得时间戳,目前没有CPU可以达成,但是在实验中他们在模拟器上实现了这一点 44 | 45 | OCC中的验证阶段,利用per-tuple的验证来将一个验证分成若干个小的操作。于是我们可以将关键路径拆分,从而缓解mutex导致的验证阶段的瓶颈 46 | 47 | 逻辑分区,允许事务直接访问远程分区的tuple,而非使用IPC来让远程分区的worker执行操作。(其实这块我不太明白,因为对H-STORE不太熟悉) 48 | 49 | 然后是具体的实验分析,这里就不一一列了,最后列一下总结 50 | 51 | DL_DETECT在低冲突的情况下可以拓展,但是会受到锁争用的限制 52 | 53 | NO_WAIT拓展性较高,但是abort rate也较高。实际中的abort造成的rollback可能会导致较高的负担 54 | 55 | WAIT_DIE受到timestamp和锁争用的限制 56 | 57 | TIMESTAMP,会因为本地复制数据导致较高的开销。timestamp限制 58 | 59 | MVCC表现较好,不会阻塞读或者写。timestamp限制 60 | 61 | OCC,复制数据导致较高开销,abort代价较高。timestamp限制 62 | 63 | H-STORE,分区工作负载下效果好。受到跨多分区的事务以及timestamp的限制 64 | 65 | 基本上所有的并发控制协议都会受到维护lock和latch的限制,MVCC虽然不会使用lock,但是每次都会生成新的record,增加内存的占用量 66 | 67 | 最后的conclusion 68 | 69 | low core的时候,2PL的算法可以较好的处理低冲突情况下的短事务 70 | 71 | T/O的算法可以更好的处理高冲突情况下的长事务 -------------------------------------------------------------------------------- /db/An_Evaluation_of_Concurrency_Control_with_One_Thousand_Cores/p209-yu.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ysj1173886760/paper_notes/df11876635eaa0da706671f51f6edd3b42e5391c/db/An_Evaluation_of_Concurrency_Control_with_One_Thousand_Cores/p209-yu.pdf -------------------------------------------------------------------------------- /db/CockroachDB_The_Resilient_Geo_Distributed_SQL_Database/3318464.3386134.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ysj1173886760/paper_notes/df11876635eaa0da706671f51f6edd3b42e5391c/db/CockroachDB_The_Resilient_Geo_Distributed_SQL_Database/3318464.3386134.pdf -------------------------------------------------------------------------------- /db/CockroachDB_The_Resilient_Geo_Distributed_SQL_Database/notes.md: -------------------------------------------------------------------------------- 1 | # CockroachDB: The Resilient Geo-Distributed SQL Database 2 | 3 | ![20220717140845](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220717140845.png) 4 | 5 | 一个真实的例子。 6 | 7 | It has the following requirements: to comply with the EU’s General Data Protection Regulation (GDPR), personal data for its European users must be domiciled within the EU 8 | 9 | Features: 10 | * Fault tolerance and high availability To provide fault tolerance, CRDB maintains at least three replicas of every partition in the database across diverse geographic zones. It maintains high availability through automatic recovery mechanisms whenever a node fails. 11 | * Geo-distributed partitioning and replica placement CRDB is horizontally scalable, automatically increasing capacity and migrating data as nodes are added. By default it uses a set of heuristics for data placement (see Section 2.2.3), but it also allows users to control, at a fine granularity, how data is partitioned across nodes and where replicas should be located. We will describe how users can use this feature for performance optimization or as part of a data domiciling strategy 12 | * High-performance transactions CRDB’s novel transaction protocol supports performant geo-distributed transactions that can span multiple partitions. It provides serializable isolation using no specialized hardware; a standard clock synchronization mechanism such as NTP is sufficient. As a result, CRDB can be run on off-the-shelf servers, including those of public and private clouds 13 | 14 | 分层 15 | SQL:做解析,optimize,并转化成读写请求 16 | Transactional KV:负责处理事务相关逻辑,保证事务的隔离性以及原子性。 17 | Distribution:为上层提供一个logical key space(类比address space)。数据可以在这个key space中被定位到。数据通过一个two-level index来定位,并且Range的位置会被缓存起来。(这里和BigTable是一样的) 18 | Replication:通过consensus-based replication实现持久性 19 | Storage:单机的KV 20 | 21 | The unit of replication in CRDB is a command, which represents a sequence of low-level edits to be made to the storage engine 22 | 23 | 为什么不固定leaseholder为raft group leader?看起来CRDB可能使用follower做leaseholder,然后在变更的时候要提交一个log来保证只有一个人可以获取lease。(目的是为了把consensus和leaseholder的逻辑解偶开吗?) 24 | 25 | ![20220717152253](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220717152253.png) 26 | 27 | 这个的算法主要是做write pipeline。跟踪每个op的依赖。非commit操作的依赖就是之前相同key上的操作。 28 | 29 | paralle commit说的就是并行的写入txn record和writes。引入了额外的一个状态叫Staging。当都成功的时候,我们可以直接给用户返回成功,然后异步的去将txn标志为committed。(1PC优化) 30 | 31 | ![20220717161902](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220717161902.png) 32 | 33 | 如果op是写操作的话,我们需要push他的ts,来解决RW-Conflict。同时我们需要保证读到的内容在推进的这段ts中没有改变。如果改变了就要提前abort。 34 | 35 | 回复给coordinator的时候只要计算了ts就可以直接回复,然后异步的复制。如果是commit的话要保证commit被持久化了再返回。 36 | 37 | 读或写的时候要拿一个latch,来防止并发的操作相互影响。比如更新ts等 38 | 39 | 写的时候先写write intent,write intent指向txn record。里面会标识txn的状态。pending, staging, committed, aborted. 40 | 41 | read遇到write intent的时候会尝试resolve。如果是pending的话就会阻塞。而对于staging的话,the reader attempts to abort the transaction by preventing one of its writes from being replicated. 42 | 43 | WR-Conflict通过等待write intent来解决。如果是一个更大的ts的write intent,我们就直接跳过 44 | 45 | RW-Conflict通过提高WriteTs来解决。 46 | 47 | WW-Conflict则和WR-Conflict类似,如果是write intent,则等待。如果是一个比较大的CommitedWrite则提高WriteTs。 -------------------------------------------------------------------------------- /db/Dont_Hold_My_Data_Hostage_A_Case_For_Client_Protocol_Redeesign/notes.md: -------------------------------------------------------------------------------- 1 | # Don't Hold My Data Hostage - A Case For Client Protocol Redesign 2 | 3 | # Abstract 4 | 5 | 我感觉之后我还是把摘要整个写一下 6 | 7 | 从数据库传输大量的数据到客户端是一个相当昂贵的操作。这个传输时间可以很容易就占有整个语句执行时间的主导部分。这对于一些外部数据分析的工具来说是一个很大的影响。在这篇论文中,我们将分析并探索将结果集序列化的设计空间。通过实现表明现有的方法都会有性能不足的情况。然后我们提出了一种列式的序列化方法。 8 | 9 | (我猜测是不是通过列式存储提高压缩率,从而减少网络负担) 10 | 11 | # Introduction 12 | 13 | Result Set Serialization(RSS)对整个系统的性能有比较大的影响 14 | 15 | ![20220524100034](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220524100034.png) 16 | 17 | 本地环回的传输时间 18 | 19 | 可以发现基本上所有的时间都是在做网络传输(我好奇linux不会对本地环回优化吗?比如引入zero-cost copy之类的) 20 | 21 | 现有的大多数分析工具都要求传入一个已经在内存中的数据表,并且即便是支持从数据库加载数据的工具,也会受到网络的限制。 22 | 23 | 有一些工作则是希望在数据库内部执行计算,从而避免数据的传输。然而这种大规模的系统级计算不容易实现以及优化 24 | 25 | main contribution为: 26 | 1. benchmark了不同的序列化方法。并解释了他们为什么是这样,以及他们设计上的缺陷。 27 | 2. 探索了序列化方法的设计空间,调查并测评了一些可以用来优化RSS的技术,并且讨论了他们的优缺点 28 | 3. 提出了column-based序列化方法 29 | 30 | # State Of The Art 31 | 32 | ![20220524102234](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220524102234.png) 33 | 34 | 客户和服务器一次交互的过程如图2 35 | 36 | 结果集的设计决定了每一步的执行时间。如果我们使用了heavy compression,那么结果集的序列化和反序列化就需要更多的时间,然而带给我们的是网络传输的优化。反之,我们可以更快的进行序列化和反序列化,但是代价就是更多的网络传输的时间。 37 | 38 | ## Overview 39 | 40 | 评测现有数据库的传输时间,隔离开每个部件分别计算。通过netcat(nc)传输一个相同的CSV文件来作为baseline 41 | 42 | baseline的作用是为了包含需要传输这些数据的时间,而不引入额外的database-specific overhead 43 | 44 | ![20220524103331](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220524103331.png) 45 | 46 | table1显示了隔离后的传输数据。没有系统可以接近baseline的速度 47 | 48 | (我在想他这个测试是不是不太公平,毕竟loopback的传输开销很低。但是看table1的size貌似也不能说明数据库就是在压缩,可能是因为额外的overhead导致的?) 49 | 50 | mongodb是文档模型,所以传输数据也传输了每一个field name,导致开销很大 51 | 52 | 文章中也说了loopback传输开销低,所以大多数的时间都是在做序列化和反序列化。但是即便如此这些文件的大小也没有小,甚至还大了 53 | 54 | ## Network Impact 55 | 56 | netem是linux的工具,可以用来模拟有限带宽以及延迟下的网络连接。测试他们传了1百万行数据 57 | 58 | 限制延迟: 59 | 延迟增加将导致传输数据的固定开销增加,因为我们需要更长的时间去传确认包。 60 | 61 | ![20220524105428](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220524105428.png) 62 | 63 | DB2和DBMS X貌似显式的发送确认包,从而导致了更多的开销。即便是没有显式发送确认,底层的TCP也会这样做,从而导致传输速度减慢(我在想如果我们可以显式的增大tcp缓冲区的长度是不是可以缓解这个问题,让他可以pipeline) 64 | 65 | 限制吞吐: 66 | 这里指的就是限制带宽 67 | 68 | 限制带宽意味着在socket上发送的字节越多,我们的性能就会越差。所以随着带宽的降低,数据压缩就越来越重要 69 | 70 | ![20220524105745](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220524105745.png) 71 | 72 | ## Result Set Serialization 73 | 74 | 这一节是有关序列化的编码 75 | 76 | ![20220524131335](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220524131335.png) 77 | 78 | 对于每一种格式,我们给出他们对table2的16进制编码 79 | 80 | ![20220524131628](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220524131628.png) 81 | 82 | postgres的方法,每一行都是一个独立的消息。每一行包含了总长度,属性数量,每个属性的长度,以及数据。 83 | 84 | 我们可以发现很多的数据都是冗余的,比如对于一个结果集来说,属性数量应该是一个定值 85 | 86 | 这解释了为什么上面postgres会传那么多的数据了。但是在另一个方面,这种方法可以让我们更快速的进行序列化和反序列化。 87 | 88 | ![20220524132108](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220524132108.png) 89 | 90 | figure6是MySQL/MariaDB的协议。每行是一个3字节的数据长度。然后是一个一字节的packet sequence number(0-256,循环)。然后是每个域的长度,以及对应的数据 91 | 92 | ![20220524132411](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220524132411.png) 93 | 94 | DBMS X的编码格式。每一行是一个header,加上所有的值以及他们的长度。并且长度是变长的。 95 | 96 | 底层DBMS X还用了固定的网络消息长度来进行批量传输,应该是一个消息若干行 97 | 98 | ![20220524132951](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220524132951.png) 99 | 100 | MonetDB。他是text-based序列化方法,所以显示出来的就是ASCII码的编码方式。 101 | 102 | 虽然这种方式很简单,但是将内部的值转化成test,再反序列化回去是比较耗时的。 103 | 104 | 值与值之间通过特殊的分割符话分开,就类似CSV一样 105 | 106 | ![20220524133412](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220524133412.png) 107 | 108 | Hive,使用的是基于Thrift的协议 109 | 110 | 编码中包含了各种元数据字节,从而允许在客户端处将数据进行反序列化 111 | 112 | 而且看这个表示也可以发现他是列存的,把每个列存在了一起。 113 | 114 | 列存的特性让元数据的存储不取决于结果集的行数。 115 | 116 | Hive在benchmark中表现的很差,这可能是因为在整数列中,每一个值都是变长的。导致我们反序列化会比较慢。 117 | 118 | (这么看的话,metadata可以让我们self-contain,但是需要额外空间。fixed-length可以加速序列化,但是会浪费空间。而variable-length虽然节省空间,但是序列化较慢) 119 | 120 | # Protocol Design Space 121 | 122 | protocol的设计空间是一个在computation和transfer的trade-off。如果computation不是问题的话,那么heavy compression会比较好。如果transfer不是问题的话,比如服务器和数据库是在一个机器上的。那么我们就可以选择以更多的数据传输来换取更小的计算代价 123 | 124 | ## Protocol Design Choices 125 | 126 | Row/Column-wise: 127 | 128 | 和存储的选择一样,我们在发送消息的时候也可以选择是按照列式进行序列化还是按照行来序列化 129 | 130 | 对于行存储的格式来说,优点就是目前的ODBC/JDBC提供的API主要针对的是row-wise access。并且数据库的客户端也是按照行来输出他们的数据的 131 | 132 | 对于列式序列化来说,优点则是压缩的比率更高。并且目前的数据分析系统在内部都是按照列来存储数据的。所以当数据以行发送过来的时候,他们会首先在内部转换成列式的存储。如果我们使用列式传输的话则不需要这个过程 133 | 134 | 列式传输的缺点就是一个列必须要等上一个列传完才能开始。如果用户希望访问一行,那么他需要缓存整个数据集 135 | 136 | 我们的选择则是一个折中。vector-based protcol。在chunk内部是列式编码,访问一行的时候只需要缓存一个chunk 137 | 138 | Chunk Size: 139 | 140 | 当以chunk的格式发送数据的时候,我们需要确定chunk的大小。 141 | 142 | chunk越大意味着服务器和客户端需要越大的缓冲区来缓存chunk 143 | 144 | 然而chunk越小意味着我们不能通过列式编码得到更好的压缩比 145 | 146 | ![20220524140920](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220524140920.png) 147 | 148 | 通过实验可以发现压缩比率收敛的很快,并且在chunk size为1mb左右的时候达到了最优性能。这意味着用户可以不需要缓存很大的chunk来提高performance 149 | 150 | Data Compression: 151 | 152 | Compression Method也是一个比较关键的点。轻量级的压缩工具比如Snappy和LZ4的目的在于快速压缩。而重量级的攻击XZ则是追求压缩比。GZIP则是在他们之间的一个均衡 153 | 154 | ![20220524141646](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220524141646.png) 155 | 156 | 可以看到即便是使用了data-agnostic compression method,列式的也比行式的压缩比更高一些。 157 | 158 | ![20220524142715](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220524142715.png) 159 | 160 | 这里是不同的带宽下进行数据传输的开销。T1000表示的就是1000MB/s 161 | 162 | 可以看到高带宽情况下轻量级的压缩方案更好。而低带宽的时候,GZIP表现的更好一些。 163 | 164 | 虽然XZ有很高的压缩比率,但是主要耗时都是在压缩的地方,导致他表现的不好。 165 | 166 | 选择那种压缩方法完全由带宽决定。所以我们使用一个启发式的方法。当数据库和用户在同一个机器的时候,我们不进行压缩。否则就使用轻量级压缩。因为现实场景中他们或者使用的是LAN,或者是高速带宽 167 | 168 | Column-Specific Compression: 169 | 170 | 除了通用的压缩方案,我们还可以单独为列进行压缩。比如RLE,或者delta-encoding 171 | 172 | 通过这些特殊的压缩方案我们可以得到更高的压缩比率以及更小的开销。整数可以使用更加快速的vectorized binpacking(我猜可能是bitmap?) 173 | 174 | 这里的实验是针对整数来测试这些特殊的压缩算法。 175 | 176 | ![20220524144559](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220524144559.png) 177 | 178 | 这里的+Sy表示的就是先用特殊算法压缩,然后再用snappy压缩整个消息 179 | 180 | 然而在ACS数据集中,他们的表现并不好。这是因为当整数的列太多的时候,我们会对每一个整数列都调用压缩算法,从而导致过多次的单列压缩。并且由于列很多,导致每个chunk上的行很少,也影响了压缩的性能。Snappy不会被影响,因为他说data-agnostic的 181 | 182 | ontime数据集中这些特定的压缩算法效果也不好,这是因为他们有较大的值,以及最值之间的差值较大。 183 | 184 | 这些特定的压缩算法对数据分布有一定的要求,并且他们需要一定数量的行才能有效(要求chunk size较大)。所以我们选择不使用这些特殊的压缩算法 185 | 186 | Data Serialization: 187 | 188 | ![20220524151912](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220524151912.png) 189 | 190 | 测试不同的序列化方法 191 | 192 | 由于protobuf是一种通用的序列化方案,他会考虑到传输时候的endian问题,从而导致序列化/反序列化开销昂贵 193 | 194 | protobuf会保存变长的int,从而减少消息的大小。 195 | 196 | 由于protobuf以非常高的开销实现了较低的压缩比,所以我们决定不使用他,而是使用自定义的序列化方案 197 | 198 | String Handling: 199 | 200 | 有三种主要的传输字符的方式: 201 | * Null-Termination 202 | * Length-Prefixing 203 | * Fixed Width 204 | 205 | Length-Prefixing需要额外的空间,但是我们可以通过变长的整数来存储字符串长度,从而节省空间。 206 | 207 | Null-Termination的好处就是只需要一个byte来分割。但是他必须读完一个字符串之后才能读下一个。 208 | 209 | Fixed-Width的优势就是如果字符串大小相同,则我们没有不必要的填充(这里我不太明白,其他的编码应该也没有,他的意思应该是额外的元信息),但是缺点就是对于varchar类型,我们可能有大量的null或者短字符串,从而导致大量的浪费 210 | 211 | ![20220524153526](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220524153526.png) 212 | 213 | table8中是传输一个固定大小的字符串,所以对fixed-width来说优势比较大 214 | 215 | ![20220524153719](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220524153719.png) 216 | 217 | 对于fixed-width来说,这里有大量不需要被传输的数据。(可以看到对于44长度的来说,虽然他的大小比较大,但是时间较快,因为我们没有额外的计算开销) 218 | 219 | 所以我们选择只有在varchar(1)的时候使用fixed-width(即便是varchar(2)使用其他方法都会更好,因为可能会有很多空串) 220 | 221 | 而对于大字符串来说,我们选择null-termination,因为他具有更好的压缩比率(因为null是相同的值所以可能对于大量空串有较好的压缩率,但是对于fixed-length来说貌似理由也成立。看表的话感觉可能的理由是varint prefix会导致更高的计算开销) -------------------------------------------------------------------------------- /db/Dont_Hold_My_Data_Hostage_A_Case_For_Client_Protocol_Redeesign/p1022-muehleisen.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ysj1173886760/paper_notes/df11876635eaa0da706671f51f6edd3b42e5391c/db/Dont_Hold_My_Data_Hostage_A_Case_For_Client_Protocol_Redeesign/p1022-muehleisen.pdf -------------------------------------------------------------------------------- /db/ERMIA_Fast_Memory_Optimized_Database_System_for_Heterogeneous_Workloads/ermia.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ysj1173886760/paper_notes/df11876635eaa0da706671f51f6edd3b42e5391c/db/ERMIA_Fast_Memory_Optimized_Database_System_for_Heterogeneous_Workloads/ermia.pdf -------------------------------------------------------------------------------- /db/ERMIA_Fast_Memory_Optimized_Database_System_for_Heterogeneous_Workloads/notes.md: -------------------------------------------------------------------------------- 1 | # ERMIA: Fast Memory-Optimized Database System for Heterogeneous Workloads 2 | 3 | Desired properties: 4 | * To provide robust and balanced concurrency control for the logical interactions over heterogeneous transactions. 5 | * To address the physical interactions between threads in a scalable way and have a lightweight recovery methodology. 6 | 7 | provide snapshot-isolation 8 | 9 | 通过SSN来保证serializability 10 | 11 | * Highlights the mismatch between existing OCC mechanisms and heterogeneous workloads, and revisits snapshot isolation with cheap serializability guarantee as a solution 12 | * Presents a system architecture to efficiently support CC schemes in a scalable way with latch-free indirection arrays, scalable centralized logging, and epoch-based resource managers. 13 | * Presents a comprehensive performance evaluation that studies the impact of CC and physical layer in various workloads, from the traditional OLTP benchmarks to heterogeneous workloads. 14 | 15 | 我们主要关注的就是第二点,他的latch-free indirection arrays, scalable centralized logging和epoch-based resource managers 16 | 17 | # Design Directions 18 | 19 | ## Scalable Centralized Logging 20 | 21 | 单点的logging容易成为bottle neck。 22 | 23 | H-Store放弃使用logging,而是通过replication来避免这个问题 24 | 25 | Silo放弃了txn的total order来避免logging bottleneck,但是缺点就是不容易实现弱隔离级别,比如SI 26 | 27 | ERMIA选择了一个在fully coordinated logging和fully uncoordinated logging的折中点 28 | 29 | ## Latch-free indirection arrays 30 | 31 | 这里的indirection是类似TupleID这样的间接层,从而避免在插入新版本的时候要更改所有对旧版本的引用 32 | 33 | 从而减少了对索引的更新。(间接的减少logging压力) 34 | 35 | ## Append-only storage 36 | 37 | 因为append only storage逻辑简单,所以可以简化代码中对corner case的处理。并且简化了IO pattern 38 | 39 | ## Epoch-based resource management 40 | 41 | 这个就是和MVCC配合的EBR 42 | 43 | # ERMIA 44 | 45 | ![20220730164635](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220730164635.png) 46 | 47 | ![20220730155913](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220730155913.png) 48 | 49 | ### Initialization 50 | 51 | 初始化阶段先将txn加入到三个Epoch-based resource manager中。分别是Log Manager,TID Manager,以及Garbage Collector 52 | 53 | 获取log buffer,TID,以及begin ts 54 | 55 | ### Forward Processing 56 | 57 | txn通过索引访问到indirection array,然后再去遍历版本链 58 | 59 | 安装新版本的时候,将自己的TID写入到版本的begin ts field中。 60 | 61 | 读者读到了具有TID的version就会根据tid查询txn context,来判断这个版本是否可见。 62 | 63 | 日志则是会写入到private log buffer中避免冲突。 64 | 65 | (和Hekaton没啥区别,如果是SI的话就从read view中查询状态就可以) 66 | 67 | ### Pre-commit 68 | 69 | 拿到一个commit ts。然后根据CC protocol来执行验证等操作 70 | 71 | 将Log buffer放到全局的log buffer中。并将状态设置为Committed 72 | 73 | ### Post-commit 74 | 75 | 遍历write set,将TID替换为Commit Ts。 76 | 77 | 最后将自己从epoch manager中拿出来。这样在并发访问的线程离开当前事务的context后,epoch manager就可以把它释放 78 | 79 | ## Log Manager 80 | 81 | txn的ts是由lsn决定的,所以其实ERMIA是把LSN的推进和ts的获取放到了一起。这样每个txn只需要修改一次lsn,就可以获得log buffer中的空间,以及commit ts 82 | 83 | ERMIA的log将物理偏移量和LSN解偶开。将日志划分为若干个segment。感觉主要的目的是为了回收日志buffer 84 | 85 | ## Epoch-based resource management 86 | 87 | Epoch manager在切换epoch的时候,那些没有切换的线程中可能有的是没有任务的线程。所以他不会声明quiscent状态,从而阻止某个Epoch关闭。 88 | 89 | ERMIA通过3种状态的Epoch来减少这种问题。当一个新的Epoch开始的时候,老的Epoch会被变成Closing状态。而再次出现新的Epoch的时候,Closing的Epoch才会被变成Closed的状态。这时候仍然停留的线程可能就是idle的线程。这时候就需要一些别的手段去处理他们。 90 | 91 | ## SSN 92 | 93 | ![20220730173638](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220730173638.png) 94 | 95 | pstamp就是读到的时间。对于写集中的元素,我们为了避免RW-conflict,就要保证提交时间在pstamp之后。 96 | 97 | 而sstamp应该是serial stamp的意思,代表了真正的串行顺序。即如果txn读到了某个版本,那么这个txn的ts就一定要小于这个版本的sstamp,否则就会导致读集被改变。 98 | 99 | 所以最终计算出来t.sstamp才是真正的serial stamp。然后验证他要大于pstamp,保证不会影响其他人的读。 100 | 101 | 这样的话其实CommitTs并不能保证是按照serial order来的。 102 | 103 | (感觉和tictoc的思路很类似,只不过tictoc会移动这里的commit ts,而非去验证他在这个范围内) -------------------------------------------------------------------------------- /db/Efficiently_Compiling_Efficient_Query_Plans_for_Modern_Hardware/notes.md: -------------------------------------------------------------------------------- 1 | # Efficiently Compiling Efficient Query Plans for Modern Hardware 2 | 3 | # Abstract 4 | 5 | 这个Abstract写的很清楚。现有的iterator model在执行的时候对locality,以及instruction prediction利用率很差。导致执行性能比不上hand-written的代码。即便是有vectorized tuple processing,只能缓解这个问题,但是性能还是不够。 6 | 7 | 本文提出了一种基于LLVM框架的将query翻译成机器码的方法。通过对于locality和instruction prediction的关注,从而获得可以和hand written code竞争的性能。 8 | 9 | (如果他没有说LLVM可能就要先去看看LLVM相关的东西) 10 | 11 | ![20220617085059](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220617085059.png) 12 | 13 | 这个是llvm框架。具体的代码通过前端编译成统一的IR,然后优化,再根据后端从IR转化成对应的机器码。 14 | 15 | 可以大概猜测我们做的就是类似JIT,根据query生成llvm ir,然后llvm ir优化后生成为对应的机器码再执行。(或者可能执行是数据库模拟的虚拟机) 16 | 17 | # Introduction 18 | 19 | 传统的iterator模型就不介绍了,核心就是若干个实现了next的算子,不断递归调用 20 | 21 | iterator model是从以前的I/O dominated的时候发展出来的,那个时候CPU开销不大。因为每次iterator的next调用都是通过virtual call实现的,从而降低了分支预测的性能。并且每一个tuple都需要一次函数调用,从而加剧了上面virtual call的performance downgrade。同时,这种模型要求维护很多信息,并且局部性不好。比如我们需要记录上一次扫描到字节流的位置等。 22 | 23 | 所以现代的系统中使用的是block-oriented,也就是一次处理一个block,多个tuple。从而均谈iterator model的开销。然而这种模型也削弱了iterator model的主要特点,也就是去pipeline data,即tuple流动的过程中不需要额外的拷贝。 24 | (这里我的理解就是由于我们需要一个block一个block的传输数据,当operator要更改数据的时候,比如用过select筛掉一些tuple,或者join得到一些新的tuple的时候,我们需要重新构建这个block,也就是需要额外的拷贝来将tuple组装成新的block传给下一个operator) 25 | 然而block的好处还有就是我们可以利用向量指令来加速执行,也就是减少instruction count 26 | 27 | ![20220617091457](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220617091457.png) 28 | 29 | figure1中可以发现,hand written code很明显性能会好很多。关系代数算子模型对于多个查询来说很有用(因为比较generialize),但是对于单个查询内部,则表现的不好。(因为这种表示法可以简便的表示任意种查询,但是对于单个查询的性能,则没有好的保证。我个人感觉他说的是iterator model,或者说是这种算子表示法的问题) 30 | 31 | 他的核心点: 32 | 1. Processing is data centric and not operator centric. Data is processed such that we can keep it in CPU registers as long as possible. Operator boundaries are blurred to achieve this goal. 33 | 2. Data is not pulled by operators but pushed towards the operators. This results in much better code and data locality 34 | 3. Queries are compiled into native machine code using the optimizing LLVM compiler framework. 35 | 36 | (核心在于是data centric,我们操纵的是data,而非实例化很多operator去pull data) 37 | 38 | (他是用的现有的llvm framework,而非集成编译技术到query engine中) 39 | 40 | # Related Work 41 | 42 | MonetDB会将所有的结果都物化,从而不需要重复调用operator function。对于OLTP system比较适用 43 | 44 | MonetDB/X100会传一批数据。也就是block oriented 45 | 46 | 一些系统会将query编译成java bytecode,从而通过jvm执行。然而他们仍然在使用iterator model(我猜测可能是通过编译来避免虚函数调用) 47 | 48 | HIQUE系统将query编译成C代码。他们为每个operator编写code template,然后生成C代码。并且他们通过物化结果来消除掉iterator model的影响,但是iterator的边界仍然十分清晰。(我猜测应该是利用编译技术来避免虚函数调用,然后通过物化结果才减少function call,以及去除其他的book keeping)。并且还有一个缺点就是生成C代码的开销比较高。 49 | 50 | 还有一些技术可以用来加速query processing。一个比较关键的就是去结合连接词谓词,比如AND,OR,来减少branching的数量。还有比如通过SIMD指令来加速评估谓词。 51 | 52 | # The Query Compiler 53 | 54 | ## Query Processing Architecture 55 | 56 | 首先定义pipeline breaker: 57 | An algebraic operator is a pipeline breaker for a given input side if it takes an incoming tuple out of the CPU registers. It is a full pipeline breaker if it materializes all incoming tuples from this side before continuing processing. 58 | 59 | 他这个定义是从寄存器角度来定义的。更加严格,也和他前面说的对应,从bottom up向上push数据可以拥有对寄存器的控制。 60 | 61 | 核心点就是我们会将数据传到内存的操作视为pipeline breaker。从而尽可能的让数据停留在寄存器中 62 | 63 | iterator model每次的function call都会导致寄存器的内容被存到栈中。而block-oriented的方法虽然减少了function call,但是一个block肯定也不能存在register中。文章中提到:any iterator-style processing paradigm that pulls data up from the input operators risks breaking the pipeline 64 | 65 | 由于在提供iterator-based view的时候,我们需要提供一个*linearized access interface*。(这里我理解就是在为operator提供这种lineraized access interface的时候,这种pull data的操作需要额外的book keeping(比如栈,迭代器指针等),从而会导致breaking pipeline) 66 | 67 | 所以我们反转data flow control的方向。我们会持续push data到consumer operator中,直到遇到一个pipeline breaker。所以data is *always pushed from one pipeline-breaker into another pipeline-breaker* 68 | 69 | ![20220617104448](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220617104448.png) 70 | 71 | 那个长的很像F的我后面就用F,是group by 72 | 73 | 从R2扫描元组,然后通过z去group by,再和R3的c去join。得到的元组再让R3.b和R1.a去join 74 | 75 | 去看一下这个过程中的data flow,我们可以发现tuple总是从一个materialized point到另一个。最上面的join,tuple会从一个materializaed state(scan of R1)移动到上面join的hash table中。 76 | 77 | 在figure3中是这些物化点 78 | 79 | ![20220617111817](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220617111817.png) 80 | 81 | 由于我们无论如何也需要这些物化点。所以我们将query编译成执行流在物化点之间的移动。在物化点内部则是pure CPU operation。 82 | 83 | 可以看到核心在于是以full pipeline breaker为粒度的执行。在代码段内部是full pipelined。并且是tight loop,对于数据以及代码的局部性利用的都很好。(因为如果让我们自己写代码,写出来的也是类似的样式) 84 | 85 | (在iterator模型中这种物化也是必须的。但是他的思路是不根据operator来划分,而是根据materialized point来划分。根据operator划分的时候,我们的视角是tuple经由一个一个的operator流动,operator是控制的核心点。而根据materialized point划分的时候,则是整体的数据从一个物化点移动到另一个物化点,我们不需要关心这个过程中经过了多少的operator。这个思路很棒要多加体会) 86 | 87 | 下面一个问题就是我们要怎么将关系代数的执行计划转化成这样的代码段了 88 | 89 | ## Compiling Algebraic Expressions 90 | 91 | 可以发现在figure4中的代码里,operator的界限是模糊的,没有了很明显的operator的概念了这里。 92 | 93 | query execution code is no longer operator centric but data centric 94 | 95 | 这里划分的点是根据物化点来的,每一段代码处理的都是被物化点所分开的pipeline。然而一个operator的逻辑很有可能被划分到不同的代码段中,从而导致代码生成的难度增高。(比如join,我们需要一块代码段来生成哈希表,另一块去probe) 96 | 97 | 还有一个难点就是比如对于二元的pipeline breaker。选择物化左边的输入和选择物化右边的输入会有很大的不同(这里我感觉就是物化的那个输入就先处理,目前不太清楚有什么不同点) 98 | 99 | 虽然会复杂,但是这是优势而非限制。iterator model用了很简单的next call,但是代价就是virtual function call以及frequent memory access(我们需要不断的访问栈去进行函数调用) 100 | 101 | 我们会看到生成代码的逻辑也含有比较清晰的结构。所有的operator都有统一的接口。但是在生成代码之后,所有的细节都会被暴露出来。 102 | 103 | (这里也是看待compile的一个观点,精妙的接口存在于编译期,然后编译器会帮我们生成高效的执行代码。而反观iterator model,他的精简也存在于execution engine中,从而导致我们没能利用query本身的结构。iterator model基本上是对plan的直接翻译,更贴合关系代数,逻辑实现简单。而query compile则是根据plan去利用他的内部结构,根据pipeline breaker划分,从而实现高效。) 104 | 105 | 每一个operator都有两个函数: 106 | * `produce()` 107 | * `consume(attributes, source)` 108 | 109 | produce的作用是让operator去生成result tuple,然后会通过调用下一个operator的consume来push到下一个operator上。 110 | 111 | ![20220617151130](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220617151130.png) 112 | 113 | figure5是一个简易的翻译映射。 114 | 115 | 比如对于figure3的plan来说。我们会首先调用最上面的join的.produce,根据figure5,join的produce是分别调用left.produce和right.produce。然后调用select的produce,select的produce会调用input的produce,即scan.produce。他会生成这样的代码:`for each tuple in relation`,然后调用parent.consume,即回到select的consume,他会根据谓词生成if语句,并调用parent.consume,回到join,这时候他会生成`materialize tuple in hash table`然后退栈回到之前的right.produce继续调用。 116 | 117 | 感觉思路上来说,produce有点像是代码生成的依赖,也就是调用他的依赖项去生成代码。而consume则是具体的生成逻辑。(一个问题是为什么不把consume的逻辑放到produce的后面呢?可能这样需要我们从parent中去跟踪tuple的格式,或者consume允许我们处理更加复杂的逻辑) 118 | 119 | # Code Generation 120 | 121 | ## Generating Machine Code 122 | 123 | 现在我们讨论了如何将plan转化成伪代码,但是实际上需要我们编译成机器码来执行。最初是通过生成C++代码,然后传给编译器,然后再通过shared library加载进来并执行。由于HyPer本身是通过C++写的,编写成C++代码的好处就是他可以访问系统中的其他代码。然而缺点就是C++编译器比较慢(在数据库角度看),编译一个复杂的query可能需要几秒中。并且C++并不能含有完整的控制权(我们只能从高层角度去编写代码,但是不能控制底层寄存器级别的操作) 124 | 125 | 所以选择了LLVM compiler framework。并可以通过LLVM的JIT compiler来直接执行。LLVM隐藏了寄存器分配的问题,提供了无限制的寄存器数量。所以我们可以假定我们为tuple的每个属性都有一个对应的寄存器。从而简化了实现(否则我们需要自己处理寄存器分配问题)。并且由于LLVM是IR,我们不用担心翻译成machine code的问题。JIT compiler会帮我们处理这些。LLVM汇编器是强类型的,可以帮助我们找到很多隐藏的bug。LLVM的优化能力很强,可以生成很快的机器码,并且编译速度只需要几毫秒。(从llvm到machine code比较快,但是从C++到IR相对比较慢) 126 | 127 | LLVM还可以让我们可以直接调用C++代码,并且C++也可以调用LLVM代码。 128 | 129 | ![20220617160841](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220617160841.png) 130 | 131 | 算子中复杂的逻辑,就是途中的齿轮,会被预编译。算子之间的连接是通过生成的LLVM代码实现的。这些LLVM chain是动态生成的。所以我们可以达到非常低的编译时间。 132 | 133 | 比如实现scan,我们需要定位数据结构,确定下一个要扫描的tuple等复杂逻辑是由C++实现的。但是tuple的处理,比如filter等是由LLVM实现的。 134 | 135 | 我们希望外部函数的调用尽可能的少,虽然将寄存器压到栈中会通常在cache内,但是当调用次数很多的时候仍然是一个不可忽视的开销。 136 | 137 | (这里是一个trade off,我们希望编译时间短,同时有好的性能。用C++实现的复杂逻辑需要引入额外的函数调用开销。好处是我们可以减少编译时间,因为复杂逻辑的编译和优化都是预先执行的。我们只需要保证我们的执行路径主要在LLVM上,就可以忽略掉复杂逻辑的开销。从而实现高性能+快速编译。) 138 | 139 | ## Complex Operators 140 | 141 | 虽然我们希望不跨越函数调用来执行query,但是这是不可能的,因为生成的代码会增长的非常快。在递归的情况下基本不可能消除函数调用。并且一些复杂的逻辑需要我们调用C++代码,比如sort,join等。我们只要防止在hot path上不要有函数调用就不会有大问题。 142 | 143 | 这里通过一个例子来演示生成的LLVM代码 144 | 145 | ![20220617163047](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220617163047.png) 146 | 147 | 可以看到生成的代码中,我们只会在少数情况下调用C++代码去分配空间或者将数据写入磁盘中。主要的逻辑,即评估谓词以及生成哈希表都是通过LLVM来执行的。 148 | 149 | (因为分配空间这种数量比较少并且复杂的操作上,如果我们也生成LLVM,那么得到的代码就会很大,并且编译时间较长。所以我们调用C++代码。而对于hash probe这种hot path上的操作,需要生成LLVM代码,从而减少function call) 150 | 151 | ## Performance Tuning 152 | 153 | 这块我理解的可能不对,他说由于生成的代码相当快,导致一些无关紧要的地方现在变成了瓶颈。比如在访问哈希表的时候,我们不是去延迟计算元组中的属性,而是提前把他们计算出来,从而隐藏计算延迟。(我猜测可能是比如我们知道之后要根据一个value访问哈希表的时候,不是在需要的时候再去计算hash value,然后再probe,而是提前拿到这个value,计算hash value。比如figure7中,我们会在probe之前取到z并哈希,然后probe。我们可以在上面提前计算。)。这块我感觉怪怪的。 154 | 155 | 还有就是有关分支预测的优化。 156 | 157 | ![20220617170315](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220617170315.png) 158 | 159 | 这段代码中while干了两件事。确定Entry是否存在,以及遍历这个链。 160 | 161 | 实际上由于哈希表基本上都是满的,所以Entry存在的概率很大,而碰撞的概率很小,所以next存在的几率不大。 162 | 163 | ![20220617170437](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220617170437.png) 164 | 165 | 所以在这样写代码的时候,cpu可以分开预测这两个分支,从而达到更好的利用率。 166 | 167 | # Advanced Parallelization Techniques 168 | 169 | 通过SIMD去处理tuple有很大的优势,只要我们可以将tuple维持在寄存器中。并且SIMD可以延迟分支(我猜测是通过mask位来避免分支,然后最后再进行branching) 170 | 171 | 我们还可以通过多核来加速处理。通过将数据分区我们可以很容易让query compile framework支持多核处理。 -------------------------------------------------------------------------------- /db/Efficiently_Compiling_Efficient_Query_Plans_for_Modern_Hardware/p539-neumann.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ysj1173886760/paper_notes/df11876635eaa0da706671f51f6edd3b42e5391c/db/Efficiently_Compiling_Efficient_Query_Plans_for_Modern_Hardware/p539-neumann.pdf -------------------------------------------------------------------------------- /db/High_Performance_Concurrency_Control_Mechanisms_for_Main_Memory_Databases/notes.md: -------------------------------------------------------------------------------- 1 | # High-Performance Concurrency Control Mechanisms for Main-Memory Databases 2 | 3 | 这次换一个记录的方式。点出思考和关键点,而不是类似翻译似的看文章 4 | 5 | single version locking works well when transaction are short and contention is low 6 | 7 | The multiversion schemes have higher overhead but are much less sensitive to hotspots and the presence of long-running transactions 8 | 9 | traditional single-version locking is “fragile”. It works well when all transactions are short and there are no hotspots but performance degrades rapidly under high contention or when the workload includes even a single long transaction. 10 | 11 | First, we propose an optimistic MVCC method designed specifically for memory resident data. Second, we redesign two locking-based concurrency control methods, one single-version and one multiversion, to fully exploit a main-memory setting 12 | 13 | 如果一个txn的读和写逻辑上发生在了同一个时间,那么他就是serializable的。 14 | 15 | SI不是serializable的,因为他的读发生在了txn的开头,而写发生在了txn的结尾。 16 | 17 | 要保证serializability,我们需要保证两点: 18 | 1. Read Stability: 19 | 说的是读的一个tuple的一个版本在事务提交之前不能发生改变。防止lost update。我们可以在读的时候上读锁。或者是在提交的时候去检查这个版本没有被更新。 20 | 2. Phantom Avoidance 21 | 也是希望保证view不变,这个的视角主要在phantom上。即相同的scan不会返回不同的结果。我们可以在table或者index上锁。或者是在提交的时候重新scan 22 | 23 | ![20220716094755](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220716094755.png) 24 | 25 | 读可以直接遍历版本链。找到覆盖当前read ts的时间段的那个版本,读即可。 26 | 27 | 写的话则是在老版本的结束时间和新版本的开始时间上写入txn id,表示获得写锁。 28 | 29 | 提交的时候则将txn id替换成commit timestamp。(用一个bit来区分这个域里的是txn id还是commit ts) 30 | 31 | ![20220716101658](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220716101658.png) 32 | 33 | 读得时候有这么几种情况: 34 | * begin和end field都是timestsamp,直接检查是否有重合即可。 35 | * begin field是txn id 36 | 37 | ![20220716102708](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220716102708.png) 38 | 39 | 应该是要去事务表里检查txn的状态。根据这个事务的状态来判断当前的动作。 40 | 41 | * end field是txn id 42 | 43 | ![20220716111531](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220716111531.png) 44 | 45 | 仍然是需要去根据事务的状态来确定 46 | 47 | 他这个TE为active的时候的判断有个比较反直觉的。就是只处理了同个事务多次更新的情况。而对于其他的事务来说,如果TE是active的话,应该可以直接读到这个版本。 48 | 49 | 更新的时候先通过CAS把txn id换到end field中。然后再把新的版本安装进来。失败的话则需要abort 50 | 51 | commit dependencies通过register and report来实现。注册依赖的时候就在被依赖的事务上面注册一个事件,当被依赖的事务提交或者abort的时候就会通知依赖他的事务。 52 | 53 | wait关系只存在于年轻事务去等老事务。所以不会出现死锁。 54 | 55 | # optimisitic transactions 56 | 57 | 原始的OCC有两种方案,backward validation和forward validation。这里使用的是forward validation 58 | 59 | 检查的时候,由于我们已经避免了WW conflict。所以只需要处理读的版本有没有被其他的事务修改。这里检查的方法就是去检查这个版本是否还对于当前事务可见。 60 | 61 | 只要txn将自己的state设为committed,其他人就可以看到他的版本了,而不需要额外的write phase 62 | 63 | 读谓词分开成两个,一个Ps是用在索引上的谓词,比如BTree上的range,或者hash index等值谓词。还有一个就是剩下的谓词(下推来的谓词) 64 | 65 | 只有当一个版本的end ts是inifinity,或者他的txn id是一个aborted的txn的时候我们才能更新。对于start ts来说他可以是一个正在进行commit的事务。这样我们可以做speculative updates 66 | 67 | 更新的写入成功后就可以将其写入到版本链中了。比如插入到clustered index中。abort也比较容易,修改start ts即可。 68 | 69 | txn的preparation phase有三个步骤。分别是: 70 | * 读检查,即检查读到的版本的可见性,以及phantoms。检查读集的版本,以及重做ScanSet中的scan。 71 | * 等待提交依赖 72 | * logging 73 | 74 | ![20220716143500](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220716143500.png) 75 | 76 | version的生命周期。 77 | 78 | RR:验证的时候只验证Read Stability。(可以防止部分的write skew) 79 | RC:用当前时间来作为ReadTime,从而保证读到最新版本。 80 | SI:不需要Validation 81 | 82 | # Pessimistic Transactions 83 | 84 | pessimistic txn会在读的时候上锁。 85 | 86 | txn维护了三个集合: 87 | * ReadSet 88 | * BucketLockSet(读到的hash bucket,应该类似谓词锁,但是resize要怎么处理?) 89 | * WriteSet 90 | 91 | 我们有两种锁。record lock和bucket lock 92 | 93 | record lock放在version上,用来保证read stability 94 | bucket lock放在has bucket上,用来防止phantom 95 | 96 | (因为prototype里实现的是hash index,我们可以使用range lock在sorted index上实现类似的谓词锁) 97 | 98 | ![20220716145935](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220716145935.png) 99 | 100 | ![20220716150153](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220716150153.png) 101 | 102 | (等待获得锁要怎么实现呢?如果内嵌这个标志的话可能需要我们忙等待?) 103 | 104 | bucket lock中有这两个 105 | ![20220716151015](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220716151015.png) 106 | 107 | ![20220716151107](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220716151107.png) 108 | 109 | 为了防止锁带来的blocking,paper中提出了一种机制叫eager updates 110 | 111 | 在获得锁的时候,我们可以直接获得这个锁,并添加一个依赖。在我们获得end ts之前,要等待所有的依赖都消失才行。这里的依赖说的是比如一个写者要获得写锁,但是这里有读者,他就会先拿到写锁,然后添加对这个version上的依赖。当所有的reader都退出后,他就可以成功获得写锁。 112 | 113 | 当一个更新事务TU尝试获取写锁的时候,他会判断如果当前的ReadLockCount大于0,他就会获取在这个version上的依赖,并等到version上的ReadLockCount为0的时候才能开始提交。 114 | 115 | 如果另一个读事务TR来了,他会判断version上是否有写锁,并且他自己是不是第一个读者,如果是的话,他就会让TU在这个version上等待。他会增加TU的WairForCnt。 116 | 117 | 当TR释放锁的时候,他会判断如果version上有写锁,并且他是最后一个读的人。他就要减少TU的WaitForCnt。TR会原子的将ReadCnt设为0,并将NoMoreReadLocks设为True,防止后续的读者继续阻塞TU,从而让TU继续执行。 118 | 119 | (TU更新完之后可以原子的将NoMoreReadLocks以及write lock设为0) 120 | 121 | bucket lock的作用不是去阻止有事务添加新的版本,而是为了防止这些新的版本对TR可见。即当TU尝试提交的时候,他必须等TS上的人释放的锁之后,才能获取end ts。从而保证了TS上的事务不会看到TU的更新 122 | 123 | 其实可以看到这些依赖就是RW依赖。我们允许了对某一个版本的并发的读写,但是就需要追踪起RW依赖,来保证提交的顺序。 124 | 125 | 在遍历版本链的时候,对于那些我们读不到的version,我们需要添加RW依赖,这些事务必须要等我们提交后才能提交。 126 | 127 | 有关deadlock,就是构建等待图去判定。 128 | 129 | 悲观和乐观的并发控制可以并存。当乐观事务尝试更新的时候,如果这里读锁,他就会增加一个RW依赖。当他插入新版本到bucket的时候,他也会在读事务上添加依赖。 130 | 131 | 这样乐观事务的写就不会影响到悲观事务的读。而对于乐观事务的读,则会在最后去验证,所以不受悲观事务的影响。 132 | 133 | 说白了就是乐观事务检测RW冲突可以兼容悲观事务,而悲观事务检测RW冲突要求上锁来保证写不会影响读,所以乐观事务的写也需要上锁。 -------------------------------------------------------------------------------- /db/High_Performance_Concurrency_Control_Mechanisms_for_Main_Memory_Databases/p298-larson.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ysj1173886760/paper_notes/df11876635eaa0da706671f51f6edd3b42e5391c/db/High_Performance_Concurrency_Control_Mechanisms_for_Main_Memory_Databases/p298-larson.pdf -------------------------------------------------------------------------------- /db/Integrating_Compression_and_Execution_in_Column-Oriented_Database_Systems/abadi-sigmod2006.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ysj1173886760/paper_notes/df11876635eaa0da706671f51f6edd3b42e5391c/db/Integrating_Compression_and_Execution_in_Column-Oriented_Database_Systems/abadi-sigmod2006.pdf -------------------------------------------------------------------------------- /db/Integrating_Compression_and_Execution_in_Column-Oriented_Database_Systems/notes.md: -------------------------------------------------------------------------------- 1 | # Integrating Compress and Execution in Column-Oriented Database Systems 2 | 3 | # Abstract 4 | 5 | 列存储的数据库可以提高相邻数据项之间的相似度,从而带来更好的压缩效果。 6 | 7 | 压缩的最好方式不仅取决于数据的属性,还取决于查询的workload(sql workload) 8 | 9 | # Introduction 10 | 11 | 列存压缩的一个优势就是我们可以在压缩的数据上直接应用算子,从而提高CPU的性能。对于RLE(游程编码)来说,比如他把数据压缩成(42,1000),表示有1000个值为42的数据项。那么我们可以快速计算出他的SUM,而从避免了后续的扫描。 12 | 13 | 主要点: 14 | * 总揽数据库的压缩算法,并说明他们是怎么被应用到列存的系统中的。并且对比专门用于列存的压缩算法 15 | * 通过实验说明这些算法的tradeoff,并且构建了一个决策树来帮助决策要怎么进行压缩 16 | * 介绍一个可以将算子直接应用在压缩数据上的执行器的架构 17 | * 说明列存中order的重要性 18 | 19 | # Related Work 20 | 21 | 前人的工作指出了将压缩算法的具体实现和数据库实现代码隔离开的关键性。通常来说我们可以通过在数据到达算子之前将他们解压缩来实现。但是某些情况下我们可以通过将算子直接应用在压缩的数据上来提高性能。本文的工作就是提出了在提供隔离性的同时,还能从这些优化中获得性能提升的一种方案。 22 | 23 | (即算子和压缩模式的解耦) 24 | 25 | (其实我比较好奇的是行存的数据库是怎么压缩的呢?RLE的可能性比较小,感觉要么是根据attribute来做dictionary compression,要么是写盘前来把数据看成未解释的字节,然后应用一些压缩算法,基于滑动窗口什么的) 26 | 27 | # C-Store Architecture 28 | 29 | C-store中一个table会被表示为若干个projections。每个projection都由若干个列组成,存储为有序的列存格式。每一列至少在一个projection中,并且一列数据可以在多个projection中存储(这么看起来他应该不会关注跨越projection之间的关系?或者是存储对应的ID来重组整个元组) 30 | 31 | 他这里说了不同的projection之间是通过join indices来关联的。join indices就是一个排列,可以把一个projection的元组映射到另一个上。(这就带来了两个问题,排列所带来的映射应该是单向的,而我们可能需要双向的映射,还有就是他只能用于两个对象,所以对于多个projection的时候,带来的复杂度是n^2的,但是好处就是我们可以很快速的找到一个projection对应另一个projection的元组,而不需要根据index来进行整个的扫描,即space/computation的tradeoff。不过这样想貌似插入的开销也很大,因为需要重构permutation) 32 | 33 | C-store实现了大多数的列版本的关系算子。他和传统的关系算子不同的有: 34 | * selection算子生成的是bitmap,然后一个特殊的mask算子可以根据bitmap和列来进行物化数据得到结果 35 | * 特殊的permute算子,通过之前说到的join index来重排序一个列(从而让他们对应) 36 | * 投影是免费的,因为直接输出列就可以,不需要改变数据(对比row-store,我们得到元组后需要根据output schema重构这个元组) 37 | * join得到的是position,而非value,后面会说为什么(我猜得到的可能是index pair,这样我们可以在推迟物化,从而根据后面的投影来进行物化?) 38 | 39 | # Compression Schemes 40 | 41 | ## Null Suppression 42 | 43 | 核心思路就是数据中连续的0或者空白会被替换为一个描述信息,用来描述这些null值的有多少,他们存在哪里。(类比存图的CSC/CSR format,就是只存储有用的信息) 44 | 45 | 本文中他们实现的思路更加的细粒度,比如一个整数是4个字节。但是可能不需要4个字节就能存储这个整数。所以他们存储了额外的信息,用2位来表示后面的整数是用多少字节存的。从而节省空间(这里应该就是把我们没用到的那些前缀的0给压缩起来,我还以为是以属性为粒度,但是这里用的是数据粒度的) 46 | 47 | ## Dictionary Encoding 48 | 49 | 将常出现的数据替换成更小的编码,一个例子就是三个颜色"green", "yellow", "red",可以被替换成0, 1, 2。从而节省空间 50 | 51 | row-store中的这种压缩方法有一个缺点,就是只能映射单个元组的一个属性。比如一个tuple中的"green"替换成了一个byte 0。然而实际上我们只需要2位就可以完整的表示颜色这个属性。但是不同的元组之间不能混合起来存,所以导致我们就必须通过一个byte来存颜色这个属性。 52 | 53 | 而column-store的一个好处就是他可以把不同的tuple的属性混合起来压缩,比如刚才的例子,我们就可以让一个byte来存4行的颜色属性 54 | 55 | 决策要怎么进行压缩的时候的一个考虑点就是我们希望字典能够fit in cache。比如我们的值域是32,用5位来压缩。如果我们用2byte来表示3个value的话,查找的时候就可以直接用这2byte去索引字典。对应的就是32 x 32 x 32种情况,也就是这么大的数组 56 | 57 | 这种压缩还有一个好处,就是我们可以很容易获得单个tuple的压缩后的数据,从而可以让我们实现在压缩数据上的计算 58 | 59 | ![20220508140917](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220508140917.png) 60 | 61 | 而且还可以延迟解压缩的过程,从而节省空间 62 | 63 | 还有一个tradeoff就是没有选择order preserving的压缩方法,因为这通常导致了变长的字典项,从而失去了拥有定长字典项的优势(快速检索单个项,以及byte alignment) 64 | 65 | ## Run-length Encoding 66 | 67 | 在row-stored的系统中,RLE只能压缩那些大的有很多空格的,或者重复的字符串,即针对单个属性 68 | 69 | 而排序的列存中则更容易进行压缩。因为我们更容易找到相同的值 70 | 71 | ## Bit-Vector Encoding 72 | 73 | 当列的值域比较小的时候,我们可以通过这种方式编码。(其实就是位图编码,对每个属性记录他在那行出现) 74 | 75 | ![20220508142257](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220508142257.png) 76 | 77 | 然后我们可以继续压缩这个位图编码,然而最近研究表示bitmap只有相当稀疏的时候性能才不会受到这个额外的压缩的影响。 78 | 79 | 然而我们只有在列的基数(意思应该是不同值的个数)小的时候才用这种方法,所以vector相对比较密集 80 | 81 | ## Heavyweight Compression Schemes 82 | 83 | Lempel-Ziv Encoding,一个常见的无损压缩算法。他使用滑动窗口动态的构建pattern table,基本思路就是他将输入的序列划分成不相交的变长的block,并且构建block的字典。当后续遇到这些block的时候就会把它替换成指向前面block的一个指针 84 | 85 | # Compressed Query Execution 86 | 87 | ## Query Executor Architecture 88 | 89 | 为每个新增的压缩算法添加两个类。一个类包装了压缩的数据,叫做compression block。一个compression block包含了一个有压缩数据的buffer 90 | 91 | ![20220508143922](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220508143922.png) 92 | 93 | 并通过table1中的API来访问数据 94 | 95 | 我们可以通过getNext来获得数据,他会解压缩下一个数据,并返回那个值,以及对应的值在正常的列存中的位置。另一个方法就是asArray,会解压缩整个缓冲区,并返回一个指向未压缩数据的指针 96 | 97 | block information是我们可以在不去解压缩数据之前就可以获得的信息。比如对于RLE来说,我们可以直接获得数据的大小,以及他们的起始和结束的位置。 98 | 99 | 对于bit vector来说,一个block里存的就是一个value以及对应的bit vector。所以我们称bit vector block为 non position-contiguous block,指的就是得到的值的位置不是连续的 100 | 101 | 对于另一个类来说,则称为DataSource operator。DataSource operator是作为query plan和storage manager的接口来使用的,并且含有一些特定的信息,比如压缩的页是怎么存储的,那些索引是有效的等。所以他可以作为一个scan operator,用来从磁盘中读取压缩的数据,并转换为Compressed block 102 | 103 | selection的谓词可以被下推到DataSources中,比如字典压缩,DataSource就可以把谓词的值也压缩起来,然后直接在压缩的数据上来应用(这么看起来DataSource也有遍历元组的能力,只不过主要还是读取数据,然后返回compressed block,compressed block就作为iterator来使用,所以一个是算子,一个是算子返回的值。相当于是一个tradeoff了,因为我们可以把压缩这个抽象放到table iterator中来做,这样不需要更改现有的执行引擎。但是我们希望算子可以应用到压缩数据中,所以就把它拉了出来做成了一个额外的算子,这样就可以得到谓词的信息,从而实现在压缩数据上的计算。不过这里有个问题,如果DataSource返回的是compressed block,然后我们需要再得到tuple,为什么不直接让他返回tuple呢?但是如果把compressed block做成抽象的iterator,那暴露给上层的东西是不是太多呢?也可能有特定实现的算子专门使用compressed block,从而把compression和正常的执行隔离开) 104 | 105 | ## Compression-Aware Optimization 106 | 107 | 他这里说了应该是把算子都修改了,所以有n个压缩算法就会有n个对应版本的算子。 108 | 109 | 虽然不会影响性能,但是会导致代码复杂度变高。并且有多个输入的算子的复杂度会变得更大。 110 | 111 | 通过优化join来演示这种问题,我们在做join的时候,可以只挑选需要做join的两个列,然后得到若干个position pair(和我上面预测的一样) 112 | 113 | ![20220508151307](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220508151307.png) 114 | 115 | ![20220508151602](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220508151602.png) 116 | 117 | 这个算子的伪代码 118 | 119 | 可以看到当压缩的方法变多的时候,我们的代码复杂度也会越来越高 120 | 121 | 所以通过compression block来抽象上面的过程。 122 | 123 | 当算子不能直接在压缩的数据上应用的时候,他就可以通过之前提到的getNext等方法来获得原始数据,正常操作。而当他可以在压缩的数据上操作的时候,他就可以利用block information来优化操作 124 | 125 | ![20220508152618](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220508152618.png) 126 | 127 | 比如这个count的例子。这里是count t,并且根据c1分组 128 | 129 | 可以看到他就可以通过IsOneValue来做优化。这样虽然RLE和bit vector是不同的压缩方案,但是在代码中不需要进行区分。 130 | 131 | (这个抽象做的很棒,识别了compression data的优化需要的必要信息,并给出了对应的接口。间接看出来做这种抽象要求我们对被抽象者,以及使用抽象接口的地方都要有很深的理解) 132 | 133 | (所以这里也能看出来,compressed block的作用是抽象压缩数据的使用,而DataSource operator则是抽象数据的读取,从而生成compressed block) 134 | 135 | 这个地方的原文很不错我贴一下:By using compressed blocks as an intermediate representation of data, operators can operate directly on compressed data whenever possible, and can degenerate to a lazy decompression scheme when this is impossible. Further, by abstracting general properties about compression techniques and having operators check these properties rather than hardcoding shielded from needing knowledge about the way data is encoded. They simply have to condition for these basic properties of the blocks of data they receive as input. 136 | 137 | ![20220508153142](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220508153142.png) 138 | 139 | 这个图是更一般化的优化技术,第一个的优化我感觉需要group by和aggregate value需要是同一个顺序的。 -------------------------------------------------------------------------------- /db/LLAMA_A_Cache_Storage_Subsystem_for_Modern_Hardware/llama-vldb2013.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ysj1173886760/paper_notes/df11876635eaa0da706671f51f6edd3b42e5391c/db/LLAMA_A_Cache_Storage_Subsystem_for_Modern_Hardware/llama-vldb2013.pdf -------------------------------------------------------------------------------- /db/LLAMA_A_Cache_Storage_Subsystem_for_Modern_Hardware/notes.md: -------------------------------------------------------------------------------- 1 | # LLAMA: A Cache/Storage Subsystem for Modern Hardware 2 | 3 | LLAMA is a subsystem designed for new hardware environments that supports an API for page-oriented access methods, providing both cache and storage management 4 | 5 | CL(Cache Layer) support data updates and managements updates via latch-free CAS atomic state changes on its mapping table. 6 | 7 | SL(Storage Layer) uses the same mapping table to cope with page location changes produced by log structuring on every page flush. 8 | 9 | 适应现代的硬件: 10 | 1. Good processor utilization and scaling with multi-core processors via latch-free techniques 11 | 2. Good performance with multi-level cache based memory systems via delta updating that reduces cache invalidations 12 | 3. Write limited storage in two senses: (1) limited performance or random writes; (2) flash write limits; via log structuring 13 | 14 | ![20220731155918](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220731155918.png) 15 | 16 | ![20220731160242](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220731160242.png) 17 | 18 | LLAMA向上提供Page的抽象 19 | 20 | 通过PID来确认Page的状态(在Secondary Storage中,还是在缓存中) 21 | 22 | 读取的时候将Page从Secondary Storage拿到缓存中 23 | 24 | intrudoction部分还说了很多让人看不太懂的东西,所以先看看后面 25 | 26 | # LLAMA Interface 27 | 28 | LLAMA提供两种形式的更新。而对于常用的CRUD来说,都通过更新来实现。 29 | 30 | ## Page Data Operations 31 | 32 | data operation用来提供对数据的修改 33 | 34 | 比如bwtree会通过Update-D来添加delta。然后在某个时间通过Update-R做consolidate 35 | 36 | 1. Update-D(PID, in-ptr, out-ptr, data) 37 | delta-update会添加一段delta,用来描述对当前page的修改。其中in-ptr表示page之前的状态。out-ptr表示page之后的状态 38 | 2. Update-R(PID, in-ptr, out-ptr, data) 39 | replacement-update会为page生成一个全新的状态。其中data参数必须是page整体的状态。 40 | 3. Read(PID, out-ptr) 41 | 读取这个page,out-ptr则是指向目标page的物理地址 42 | 43 | ## Page Management Operations 44 | 45 | 1. Flush(PID, in-ptr, out-ptr, annotation) 46 | Flush会将page拷贝到LSS(log structured store)IO buffer中。他会为page添加一个带有annotation的delta,这个delta带有Flush的标记 47 | 2. Mk-Stable(LSS address) 48 | 保证到LSS address之前的所有buffer都已经被刷入到secondary storage中 49 | 3. Hi-Stable(out-LSS address) 50 | 返回secondary storage中最大的LSS address 51 | 4. Allocate(out-PID) 52 | 申请一个新的page。返回PID。保证这次申请是被持久化的。(通过system transaction) 53 | 5. Free(PID) 54 | 释放一个page。操作仍然是被持久化的。(通过system transaction) 55 | 56 | ## System Transaction Operations 57 | 58 | system transaction用来提供持久化的原子操作的。(比如提供SMO,类似mini txn) 59 | 60 | 1. TBegin(out-TID) 61 | 开启一个事务,会将这个事务插入到ATT(active transaction table) 62 | 2. TCommit(TID) 63 | 将事务从ATT中移除。并将page状态的变化安装到mapping table中,然后刷入到LSS buffer中 64 | 3. TAbort(TID) 65 | 将page重置到事务开始的时候 66 | 67 | -------------------------------------------------------------------------------- /db/Magma-A-High-Data-Density-Storage-Engine-Used-in-Couchbase/magma.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ysj1173886760/paper_notes/df11876635eaa0da706671f51f6edd3b42e5391c/db/Magma-A-High-Data-Density-Storage-Engine-Used-in-Couchbase/magma.pdf -------------------------------------------------------------------------------- /db/Morsel_Driven_Parallelism_A_NUMA_Aware_Query_Evaluation_Framework_for_the_Many_Core_Age/notes.md: -------------------------------------------------------------------------------- 1 | # Morsel-Driven Parallelism: A NUMA-Aware Query Evaluation Framework for the Many-Core Age 2 | 3 | # Abstract 4 | 5 | 随着计算机架构的发展,在parallel query execution中出现了两个问题: 6 | * 为了利用好多核的优势,每个查询需要被均匀的分布到每个线程中 7 | * 即便是我们拥有相当准确的统计数据,也很难将负载均匀的划分开 8 | 9 | 这两个问题导致了目前的plan-driven parallelism陷入了负载均衡和上下文切换的瓶颈中 10 | 11 | 第三个问题是在many-core的架构中,内存控制器是去中心化的(可能是若干个核对应一个内存控制器),从而导致了NUMA 12 | 13 | 这篇文章提出了morsel-driven query execution framework。调度将变成细粒度NUMA-aware的运行时调度。并行度不再在plan中详细规划,而是可以在执行时动态的变化。调度器是NUMA-aware的,所以绝大多数的查询将会在本地内存中进行 14 | 15 | # Introduction 16 | 17 | 之前的火山模型中的并行执行是通过exchange算子来实现的。exchange会将元组流转发给多个线程,每个线程都会执行相同的pipeline segments。这样的实现被称为plan-driven:optimizer statically determines at query compile-time how many threads should run, instantiates one query operator plan for each thread, and connects these with exchange operators. 18 | 19 | 而morsel-driven的idea如下 20 | 21 | ![20220601091048](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220601091048.png) 22 | 23 | 一个查询会被划分为若干个segments,每个segment取一小部分的数据并执行,并最终将他们物化到pipeline breaker中。 24 | 25 | 图中的颜色则代表了NUMA-aware的特性:A thread operates on NUMA-local input and writes its result into a NUMA-local storage area. 26 | 27 | 文章中提到了他的dispatcher是pin在固定的核上的,所以不会出现由于OS移动线程导致的局部性缺失的问题(我的感觉是dispatcher的位置是无所谓的,因为他与数据无关,然而工作线程应该是pin在核上才对) 28 | 29 | morsel-wise的框架中,所有的算子在任何阶段都是可以被并行化的,这相对于火山模型来说不仅在输入输出数据是并行化的,在中间的状态也是并行化的。比如用于做join的hash table,就需要被多个核共享访问。 30 | 31 | 在火山模型的框架中,并行会被算子隐藏起来,并且我们会避免共享的状态(比如要执行hash join的时候就需要先物化哈希表,变成一个算子的工作)。并且由于隐藏了并行的细节,我们就需要在exchange operator中进行on-the-fly的数据分区,(比如round-robin),而这个工作可以通过我们的locality-aware的dispatcher来进行。对于算子并行来说,有的系统提倡使用per-operator的并行从而实现执行的灵活性,然而这需要引入额外的算子间的同步。我们认为morsel-wise的框架可以被集成到现有的系统中。我们可以修改exchange operator的实现来达到morsel-wise scheduling,并且引入数据结构的共享。 32 | 33 | (我感觉核心的思路就是将operator-thread变成了morsel-thread,这样我们的线程就不需要绑定到具体的operator,从而引发额外的同步或者灵活性较差的问题。现在每个线程只需要执行自己的morsel就可以。直观的感觉就是之前的模型就是提前搭建好了管道,然后我们往里喂数据。现在则是一堆工人在数据堆里一块一块的取。有点data-oriented的感觉,因为每一个morsel我们不需要关注他的worker是谁(当然优先local worker),只要关注执行的逻辑即可) 34 | 35 | # Morsel-Driven Execution 36 | 37 | 通过这个例子来演示: 38 | 39 | ![20220601101622](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220601101622.png) 40 | 41 | 假设R是filter之后最大的表,所以我们会用R作为probe input,并在另外两个表上构建哈希表 42 | 43 | ![20220601101742](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220601101742.png) 44 | 45 | 我们得到了3个pipeline: 46 | 1. scan,filter,并构建哈希表HT(T) 47 | 2. scan,filter,并构建哈希表HT(S) 48 | 3. scan,filter,并用结果从HT(S)中探测,然后再从HT(T)中探测,最后存储结果 49 | 50 | morsel-driven execution会被QEPobject来控制。他会把executable pipeline传给dispatcher执行。QEPobject负责跟踪数据的依赖性。比如上面的例子中,只有当前两个pipeline都执行完成的时候,最后一个pipeline才能开始。QEPobject会为每个pipeline分配临时的空间用于存储他们的结果。当pipeline结束后,这个临时的空间会被划分为等大小的morsels。这样后续的pipeline就会从新的morsel开始执行,而不会保留之前的morsel的边界(因为可能之前相同的morsel的结果不是等大的,从而可能导致负载偏斜) 51 | 52 | ![20220601105641](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220601105641.png) 53 | 54 | 对应生成的pipeline 55 | 56 | ![20220601110541](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220601110541.png) 57 | 58 | 构建哈希表的pipeline 59 | 60 | 每一个线程会操作一个morsel。基本表T存在了morsel-wise的NUMA-organized memory中。 61 | 62 | 每当一个线程处理完一个morsel的时候,他要么会被分配到另一个任务上(被dispatcher),要么会获取相同颜色的另一个morsel来作为下一个任务。 63 | 64 | 而构建哈希表的这个pipeline是由两个物理的pipeline组成的。逻辑上讲,我们是读取一个tuple,做filter,并插入到哈希表中。实际上在figure3中则是划分为两个阶段。 65 | 66 | 第一阶段主要是filter tuple,并插入到NUMA-local的存储区。 67 | 68 | 当所有的morsel都被scan并filter了以后,我们会重新扫描这些结果,并将指针插入到哈希表中。(我好奇难道不能直接插入吗) 69 | 70 | 这里说到是因为我们可以知道有多少元素会被插入到哈希表中,从而可以得到一个具体的哈希表的大小(这里我感觉是因为动态变化大小的哈希表不容易处理,并且可以均匀的把哈希表划分到每个NUMA-area中)。并且由于会有很多的线程同时插入,所以lock-free的实现是必要的 71 | 72 | ![20220601112458](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220601112458.png) 73 | 74 | 当所有的哈希表都构建完毕的时候,我们就可以开始执行probing pipeline 75 | 76 | 和之前一样,dispatcher会让本地的核去获取本地的数据,并将结果存到本地的内存中。(这里他没提到的一点,我猜测hash table的访问肯定会引入remote memory access,但是由于我们是给全局共享了哈希表,这也是必要的) 77 | 78 | 和火山模型不同的是morsel-driven的执行中,pipeline不是独立的(pipeline segment不同并且有依赖)。morsel-driven的pipeline会共享数据结构,从而需要一些同步手段。还有就是不同的pipeline segment的线程数不同,并且pipeline segment内部的线程也可以变化。 79 | 80 | (这个感觉有点像数据并行和模型并行,数据并行不需要考虑依赖和同步,数据与数据之间是独立的。而模型并行则需要考虑依赖性,并且内部也会有数据的并行。但是这篇文章的重点不在这里,而是在细粒度的执行,NUMA-aware的scheduling,以及共享的数据结构) 81 | 82 | # Dispatcher: Scheduling Parallel Pipeline Tasks 83 | 84 | dispatcher负责控制以及分发计算资源给每个pipeline。我们通过将task分配给每个worker来实现这一点。我们会为每个硬件线程(逻辑核我猜测)绑定一个对应的worker。因此查询的并行度不由创建或者终止线程来实现,而是通过将分配任务来实现。 85 | 86 | 一个task是由一个pipeline job,以及一个morsel组成的。任务的抢占只有在morsel的边界处才会出现,即执行的粒度是morsel,从而可以不需要引入额外的中断机制。 87 | 88 | 分配task给worker的主要目标为: 89 | 1. Perserving locality by assigning data morsels to cores on which the morsels are allocated 90 | 2. Full elasticity concerning the level of parallelism of a particular query 91 | 3. Load balancing requires that all cores participating in a query pipeline finish their work at the same time in order to prevent fast cores from waiting for other slow cores. 92 | 93 | ![20220601141728](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220601141728.png) 94 | 95 | dispatcher维护了一个链表,里面存储了那些依赖已经被满足的pipeline job。前面提到过QEPobject负责跟踪pipeline的依赖,所以他负责将可执行的pipeline发给dispatcher。 96 | 97 | ## Elasticity 98 | 99 | 通过dispatching jobs a morsel at a time来实现调度的elasticity。比如我们有一个长的查询Ql在运行,当出现了一个更加高优先级的查询Q+的时候,我们可以降低Ql的并行度,并优先处理Q+,当Q+执行结束后,我们就可以回来提高Ql的并行度。 100 | 101 | 每个pipeline job都会维护一个pending morsels的list。他的这个list是每个core单独维护的,Core0上存的就是core0的morsel。而不是有一个中心化的结构来保存所有的morsel。 102 | 103 | ## Implementation Overview 104 | 105 | 实现上,我们并不是在每个socket上都执行一个dispatcher thread,而是让core在需要morsel的时候去自己执行dispatcher的code。 106 | 107 | 通过lock-free的数据结构来减少争用(lock-free应该是减少等待而不是减少争用) 108 | 109 | 数据之间的依赖的处理,即QEPobject是通过一个状态机来实现的(他这里说的是passive state machine,我个人感觉就是拓扑排序的实现)。当我们的dispatcher发现pipeline job执行完的时候,他就会通过QEPobject来尝试找到新的pipeline job。和dispatcher一样,代码也是在worker上执行的。 110 | 111 | (这里说白了就是没有真的dispatcher,就是socket上有对应的pipeline job列表,以及morsel列表。然后worker自己去拿,或者通过QEPobject去放新的pipeline job。) 112 | 113 | 当某一个socket的job都完成了,他就会尝试从其他的socket去steal work,从而防止闲置的线程。 114 | 115 | 并且这个模型还可以提供很好的方法来取消查询。比如当我们事务abort,或者内存耗尽等问题出现的时候,我们可以不去让OS杀掉对应的线程,而是可以标记这个query。当worker发现这个query被取消了,他就可以不从里面获取morsel。这个方法可以让worker自己去做clean up的工作,而不需要引入额外的机制(比如RAII,杀掉线程的时候释放线程申请的资源) 116 | 117 | ## Morsel Size 118 | 119 | Morsel size不是一个很关键的因素,和Vectorwise的执行不同,我们不要求morsel可以放入缓存中,他们只是一个用于调度的任务单元而已。在选择的时候,只要保证他们足够大可以均摊调度开销就可以。 120 | 121 | 系统中的shared-datastructure,也就是dispatcher,不容易成为瓶颈。因为我们最开始会为每个线程分配他们的range,只有当他们本地的range使用完之后线程才会尝试steal其他的range,从而导致一定的争用。同时如果有多个任务同时执行的时候,这个效果会变得更小(因为我们更不容易去steal work)。并且我们可以通过增加morsel size来减少对共享数据结构的访问。只要有足够的query,增加morsel size就不会影响整体的吞吐量。 122 | 123 | # Parallel Operator Details 124 | 125 | 我们需要保证每个算子都是可以并行执行的。这一节则是讨论并行算子的实现 126 | 127 | ## Hash Join 128 | 129 | 前面已经提到过,hash join有两个阶段。第一阶段是将输入的数据物化到thread-local area中。第二阶段则是通过CAS将tuple的指针插入到哈希表中 130 | 131 | 文章提到了一些选择single-table hash join的优点。这里就不提了。 132 | 133 | ## Lock-Free Tagged Hash Table 134 | 135 | 他的hashtable有一个优化就是在指针上存了一个filter。64位中有16位的filter,以及48位的指针。 136 | 137 | ![20220601212807](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220601212807.png) 138 | 139 | 在指针上存了一个bloom filter,然后通过bloom filter可以减少cache miss 140 | 141 | (我好奇他都已经确定了哈希表的大小了,为什么还要用链式哈希,而不是用开放寻址)文章中他说因为开放寻址法需要把tuple保存到哈希表中,而链式的则保存指针就好。并且链式的缺点是缓存命中率低,也通过filter解决了。 142 | 143 | 这里有一个很有意思的点。实现的时候是通过mmap来分配哈希表的空间的。并且由于OS会进行lazy allocation,只有第一次写入才会真正的分配空间。带来两个好处,一就是我们不需要手动初始化哈希表,让OS在分配页的时候帮我们初始化即可。二就是哈希表会适应性的分布到每个NUMA-node中。因为当第一个线程写入的时候,对应的页会被分配到这个线程所在的节点上。所以如果有很多线程在构建哈希表的话,哈希表会在各个node中交错分布(并且node访问的越多,他就越可能是第一次访问数据,从而拥有更多的local page)。当只有一个node的时候,哈希表就会整个坐落于这个node上。 144 | 145 | ## NUMA-Aware Table Partitioning 146 | 147 | 最简单的分区方法就是用round-robin。 148 | 149 | 一个好一些的方法就是基于一些比较重要的属性来做哈希分区。这样的话在我们做join的时候,能够join的元组通常是在同一个socket上的。所以可以带来更少的跨socket的通信。 150 | 151 | 同时这个哈希函数也会被应用到哈希连接中哈希表桶的位置的最高位。(所以我猜测应该是哈希表的分布和数据的分布是相同的,这样查询哈希表的时候也是NUMA-Aware的) 152 | 153 | 这种分区方法会对我们的morsel-driven执行模型有好处,但并不是决定性的。因为本身的表扫描就具有NUMA-locality,输出的结果也是具有局部性的。上面的分区方案的作用只是增强在连接时候的性能,而不会影响其他地方。 154 | 155 | ## Grouping/Aggregation 156 | 157 | 在做aggregation的时候,如果我们只有很少的几个组,那么聚合就会非常快,因为缓存命中率高。但是当组很多的时候,就会出现很多的cache miss,从而导致性能降低。 158 | 159 | ![20220602090852](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220602090852.png) 160 | 161 | 算法分为两个阶段。第一阶段是thread-local pre-aggregation 162 | 163 | 先在本地做一个小的哈希表,对应图中就是ht。在本地将数据进行分区,得到若干个partition。 164 | 165 | 第二阶段就是每个线程扫描其他的partition,然后将他们聚合到本地的哈希表中。这里的意思应该是一个线程负责一个或者多个group,然后扫描其他线程的partition。从而得到自己负责的group的结果。 166 | 167 | 当一个partition处理完成后,会立刻被推到下一个算子中,这样他更有可能是fit-in-cache的。 168 | 169 | (有点类似分布式的aggregation,先做本地然后再去交换数据) 170 | 171 | 和join不同的是聚合操作必须要所有的数据都输入后才能有输出。所以选择了用分区的方法。而join则是可以pipeline的,所以用一个哈希表去检查是否可以连接就可以。 172 | 173 | ## Sorting 174 | 175 | ![20220602094728](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220602094728.png) 176 | 177 | 首先先执行local sort,或者local top-k 178 | 179 | 每个线程首先计算local separator。然后为了防止分布偏斜的情况,所有的线程的local separator会被结合起来,并计算global separator。 180 | 181 | 得到了global separator后,我们找到具体的separator的index,并通过这些index可以得出最终数组的具体分布,然后我们就可以直接把数据拷贝过去而不需要任何的同步。 182 | 183 | 比如figure9,我们有3个worker,那么就需要将数组分为三段做并发的merge。每个worker挑出两个local separator,然后合并成2个global separator。如图所示,三个数组的第一段会被红色的worker合并,第二段会被绿色的worker合并,第三段则是蓝色worker。这样worker之间就不需要任何的同步。(但是缺点就是worker越多,每个线程所访问的远端内存也就越多,可能导致局部性比较差) -------------------------------------------------------------------------------- /db/Morsel_Driven_Parallelism_A_NUMA_Aware_Query_Evaluation_Framework_for_the_Many_Core_Age/p743-leis.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ysj1173886760/paper_notes/df11876635eaa0da706671f51f6edd3b42e5391c/db/Morsel_Driven_Parallelism_A_NUMA_Aware_Query_Evaluation_Framework_for_the_Many_Core_Age/p743-leis.pdf -------------------------------------------------------------------------------- /db/Opportuinities_for_Optimism_in_Contented_Main_Memory_Multicore_Transactions/notes.md: -------------------------------------------------------------------------------- 1 | # Opportunities for Optimism in Contended Main-Memory Multicore Transactions 2 | 3 | 看这篇文章主要是看看各种并发控制的protocol 4 | 5 | # Abstract 6 | 7 | 他提到那些与concurrency control无关的implementation choices是导致性能下降原因。 8 | 9 | # Intrudoction 10 | 11 | partially-pessimistic concurrency control, dynamic transaction reordering以及MVCC修改了CC protocol来支持高冲突下的txn。 12 | 13 | 他们比较了Silo, DBx1000, Cicada, ERMIA和MOCC(之后可以仔细去看看),并发现了engineering choices会极大程度的影响这些系统。 14 | 15 | # Background 16 | 17 | ![20220710143528](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220710143528.png) 18 | 19 | 架构的Overview。索引上存的是RecordPtr。Record中存的是VersionChain。有些version可能被inline 20 | 21 | ## OSTO 22 | 23 | Silo OCC protocol 24 | 25 | 执行的时候,生成读写集。在提交的时候,有三个阶段。第一阶段会锁住写集中的所有的元组。如果出现死锁就abort。获取一个commit timestamp。第二阶段会验证读集中的元组没有被其他的txn修改或者锁住。如果有就会abort。第三阶段会install new version,更新他们的timestamp,然后释放锁 26 | 27 | (这不就是标准的OCC么?Silo没有用什么变种吗) 28 | 29 | OSTO目的是减少memory contention。他们用了RCU来减少读写锁。(是本文,而非Silo) 30 | 31 | ![20220710151124](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220710151124.png) 32 | 33 | 维护这些变量用来回收空间。要删除一个对象的话,txn会将它存入到一个链表中,并附带上freeing timestamp为wts_th。根据图上的解释可以知道。wts_th用来标识删除的对象。当所有人都读不到这个对象的时候,他就可以被安全的删除掉。 34 | 35 | 思路是这样的,global write不断递增。一个线程可以把他要删除的对象注册到write timestamp上。当所有人都不会再读到这个对象的时候,我们就可以把它安全的删除掉。那怎么表示一个线程不会读到这个对象呢? 36 | 37 | ![20220710153419](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220710153419.png) 38 | 39 | 所有并发的线程可能读到我们要删除的对象。而这些并发的线程的范围则是[min(wts_th), max(wts_th)]。比如T2,他所修改的对象必须要在T4结束后才能安全的删除。但是要注意我们不可能通过统计max(wts_th)去删除一个对象。因为他是不断递增的。而上面的范围说的是这一时刻的max(wts_th)。 40 | 41 | 这里做的是维护每个事务可以读到的最早的事务timestamp。每个事务开始的时候,为他分配read timestamp,即min(wts_th)。表示这个事务可以看到read timestamp及之后的事务的修改。那么小于min(read_timestamp)的对象就可以被安全的删除。 42 | 43 | 本质上是维护了一个low watermark,即每个事务的并发事务的最小时间戳。 44 | 45 | ## MSTO 46 | 47 | MSTO是一个MVCC的变体。基于Cicada。每个version上有两个值。分别是write timestamp和read timestamp。write timestamp就是创建的时间。read timestamp则是最近提交的读取过这个版本的事务。 48 | 49 | 对于version chain来说。我们有rtsi >= wtsi, wts(i+1) >= rtsi, wts(i+1) > wtsi 50 | 51 | 第一个属性说的是version要被提交才能被读到。第二个属性说的是如果存在更新的版本则要读更新的版本。保证串行化。加入当前版本的rts大于下一个版本的wts,说明这个rts对应的txn没有读到正确的版本。而最后一个属性则是版本链的wts递增。 52 | 53 | txn开始的时候会获取一个ts用来读取版本。对于只读事务来说用的是rtsg(即保证他读到的所有事务都是提交的,保证无冲突)。而对于rw事务,则是wtsg。 54 | 55 | 对于读来说,版本和元组都会存到read set中。对于写来说则存储元组。 56 | 57 | 在提交的时候。MSTO首先从wtsg中取一个commit timestamp。`ts := wtsg++` 58 | 59 | 然后在第一阶段他会原子的在write set的version chain中插入一个pending version。(并发的Pending会spin wait(死锁怎么办))。第二阶段会检查读集。他会根据commit ts来重新读取,如果和之前读到的版本不同。则abort。否则的话更新read timestamp。第三阶段,将Pending版本变成Committed。并将早版本的数据根据之前的方法删除,并等待回收。 60 | 61 | (怎么感觉唯一不同的就是多了个垃圾回收的机制,维护读取的low watermark,然后删除掉旧版本。而且也没有看到read timestamp的作用) 62 | 63 | # Basis Factors 64 | 65 | ## Contention regulation 66 | 67 | over eager retry会导致contention collapse。即冲突过大导致性能下降。 68 | 69 | over delayed retry会导致core idle 70 | 71 | 推荐方法是randomized exponential backoff 72 | 73 | ## Memory allocation 74 | 75 | 推荐是fast general purpose scalable memory allocator。比如rpmalloc 76 | 77 | 因为memory pool会引起争用。还会导致很多其他的overhead。 78 | 79 | preallocation会极大程度的影响性能。(他的意思应该是测试情况下) 80 | 81 | ## Abort mechanism 82 | 83 | C++ exceptions会获取一个全局的锁来保护exception handling data structures(来防止dynamic linker修改他) 84 | 85 | 推荐使用explicitly-checked return values 86 | 87 | ## Index types 88 | 89 | 哈希表更快 90 | 91 | ## Contention-aware indexes 92 | 93 | contention aware index说的就是不相交的range不会引起contention。 94 | 95 | ![20220710172016](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220710172016.png) 96 | 97 | ## Other factors 98 | 99 | 维护read set和write set的实现。通过哈希表来将RID映射到物理指针上。 100 | 101 | deadlock avoidance or detection strategy。 102 | 某些OCC会sort写集。比如通过memory address写入。 103 | 或者bounded spin,虽然会出现false positive,但是开销低。 -------------------------------------------------------------------------------- /db/Opportuinities_for_Optimism_in_Contented_Main_Memory_Multicore_Transactions/p629-huang.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ysj1173886760/paper_notes/df11876635eaa0da706671f51f6edd3b42e5391c/db/Opportuinities_for_Optimism_in_Contented_Main_Memory_Multicore_Transactions/p629-huang.pdf -------------------------------------------------------------------------------- /db/Serializable_Snapshot_Isolation_in_PostgreSQL/notes.md: -------------------------------------------------------------------------------- 1 | # Serializable Snapshot Isolation in PostgreSQL 2 | 3 | # Abstract 4 | 5 | 就是SSI在Postgres中的实现 6 | 7 | # Overview 8 | 9 | Postgres之前只有snapshot isolation。9.1版本提供了SSI的实现。 10 | 11 | Postgres的SSI实现必须要和现有的特性结合,而不能像research prototype一样忽略很多细节。比如要支持replication,two phase commit,subtransaction。还有控制memory useage。 12 | 13 | 和之前的系统,比如MySQL不同的是,mysql之前提供的是基于2PL的serializable,在port到SSI的时候,他可以利用已有的predicate locking infrastructure来处理冲突。对于Postgres,他们构建了一个新的LockManager来处理冲突。 14 | 15 | # Snapshot Isolation Versus Serializability 16 | 17 | 主要是提了一下SI发生的异常。一个是最简单的写偏斜。还有一个则是和read only transaction有关的 18 | 19 | ![20220709125526](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220709125526.png) 20 | 21 | 这里的report希望读到所有batch号为x - 1的receipts。但是在SI的隔离级别中,在figure2的执行顺序下,他没有达成目的。因为他没有读到T2插入的receipt。 22 | 23 | 和write skew不同的是,除掉t1后,t2 t3本身却是可串行化的。只有在加入t1这个只读事务后异常才会出现。 24 | 25 | 在[这篇paper](https://www.cs.umb.edu/~poneil/ROAnom.pdf)中作者说出了他的本质原因。`The fact that SI allows commit order different than serial order is what causes the anomaly` 26 | 27 | 在上面的例子中,serial order为 T2 < T3,然而由于t3提前提交,导致后来的txn看到了t3,却没看到t2。虽然看到了所有事务的更改,并且没有看到intermediate result,但是仍然发生了异常。 28 | 29 | 要注意的是,这些anomaly的直接表现都是违背了冲突可串行化的标准。但是他们的成因却有所不同。比如write skew是因为基于过期假设做了决策。而这个由read only txn所导致的异常是因为提交顺序和执行顺序不同,从而导致了读事务读到的状态和可串行化顺序的状态不同。(或者说叫解释的角度不同,冲突可串行化在page model解释事务。而write skew等则是在高层次语义角度解释异常) 30 | 31 | 有很多的技术是用来避免anomalies: 32 | * 有些workload不会出现异常,比如tpcc在si下也没有问题 33 | * explicit locking。比如`LOCK TABLE`可以锁住整个table,`SELECT FOR UPDATE`会在元组上上锁 34 | * materialized the conflict。比如有的冲突可以通过在dummy row上来将冲突显式表示。比如write skew的时候,我们可以固定更新某个特定的row,从而保证这些txn的可串行化。 35 | * integrity constraint。由DBMS保证,和隔离级别无关 36 | 37 | # Serializable Snapshot Isolation 38 | 39 | SSI会让txn运行在snapshot isolation中,但是额外增加了一些检查来避免异常。 40 | 41 | SSI的一个特性就是他不会有blocking,那些可能导致违反可串行化的事务会被abort。所以可以提供更好的性能。(有点OCC的感觉) 42 | 43 | ## Snapshot Isolation Anomalies 44 | 45 | ![20220709150844](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220709150844.png) 46 | 47 | Example1说的是write skew,两个事物都修改了对方读的结果。所以都对对方有rw-conflict。rw-conflict说的是,如果A读了一个t1版本,然后B写了这个tuple,为t2版本,那么A应该排在B前面,因为A没有读到B的t2版本。 48 | 49 | 而Example2说的就是read only txn引起的问题。2应该在3前面,而1应该在2前面。因为3修改了counter,而2修改了reciept。但是由于T1读到了T3修改的counter,所以出现了问题。 50 | 51 | 这个其实就是冲突可串行化的依赖图。只不过说的是object level,而非tuple level,目的是包含谓词读。 52 | 53 | ## Serializability Theory 54 | 55 | 在wr依赖中,如果A到B有wr依赖,那么A一定在B之前提交了。这样B的snapshot里才会有A。而ww也是一样的道理。A一定在B之前提交了。 56 | 57 | 对于rw来说,他发生于并发的事务。因为一定是一个事务活跃的时候,另一个事务开始了,所以读事务看不到写事务的结果,从而要求读事务排序在写事务之前。 58 | 59 | 有研究发现在SI中的anomaly,至少存在着两个rw-antidependency edge。并且这两个edge一定是邻接的。 60 | 61 | ![20220709154246](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220709154246.png) 62 | 63 | ![20220709154332](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220709154332.png) 64 | 65 | (TODO:感觉这里还需要深入理解一下。rw依赖是因为在SI中并发的事务无法看到对方的修改,从而隐式的确定的了一个顺序。至于为什么T3要最早提交,我感觉是因为如果T3没有提交的话,T3无法构成和T1的WR或者WW依赖。从而组不出环。) 66 | 67 | ## SSI 68 | 69 | SSI的思路类似serialization graph testing的并发控制方法(他会在运行的时候检测是否出现环,从而保证可串行化。个人猜测应该是一个读或者一个写就是一个object。每次新的object都要和现有的object做判断,如果有交集就连边,然后判断是否有环。) 70 | 71 | 不同的是他检查的是dangerous structure,即两个相邻的rw-antidependency edges。如果某一个txn有一个rw入边和一个rw出边,那么SSI就会abort掉其中一个txn。好处就是SSI不需要去检查wr以及ww。从而可以提高性能。 72 | 73 | 在S2PL中,以及OCC中,他们不会允许rw的产生。假设我们拆掉了之前例子中的只读事务T1,在OCC中,是不允许提交的。因为T2的读集被修改了。而在S2PL中,读会阻塞写者。所以T3会在T2完成后才能修改。而在SSI中,这是被允许的。并且没有异常会出现。从而允许了更高的并发度。 74 | 75 | SSI的论文中。要求我们在读的时候在元组上加上SIREAD的锁。他不会阻塞并发的写入。而是会检测在SIREAD上的写所引起的rw-antidependency。并且这个SIREAD在txn提交后还应该存在。比如write skew的情况下。我们在读的集合上加上SIREAD,然后T1写入x,T2也加上SIREAD,然后T1提交了,这时候会有T2 -> T1,T2这时候写入y的话,会检测到T1 -> T2,那么T2应该abort。如果释放掉SIREAD的话,在T2写入y的时候就会检测不到冲突。 76 | 77 | 上面的推论2中说明了SIREAD必须要在所有并发的事务都结束后才能释放。(怎么实现呢?维护high watermark?) 78 | 79 | ### Variants on SSI 80 | 81 | 在理论1中,说两个邻接的rw边,并且T3的提交时间要大于T1。如果我们可以保证T1或者T2先提交。那么就可以避免某些false positive的情况(即有相邻rw边,但是提交顺序不满足的时候)。Postgres使用了这个优化。然而他并不能清除掉所有的false positive。因为我们还需要保证有环。比如在上面的例子中,如果只读事务不读batch number,那么T3到T1不会有wr依赖。从而可以达到可串行化的执行顺序。 82 | 83 | # Read Only Optimizations 84 | 85 | ## Theory 86 | 87 | ![20220709170312](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220709170312.png) 88 | 89 | 证明这里就不说了。但是可以通过这个理论来减少read only txn的false positive的情况。即当T1是一个read only txn的时候,只有在t3在t1获取他的snapshot之前提交,才可能出现问题。即T1一定要能读到T3的snapshot。 90 | 91 | # Safe Snapshots 92 | 93 | 这个说的就是snapshot可能会出现问题,比如上面那个read only的txn生成的snapshot可能读到的是不对的数据。所以Postgres会跟踪生成snapshot时候的并发的事务。最开始他和普通的事务是一样的,要加SIREAD锁,当并发的事务都提交了以后,这时候的snapshot就可以不用再加SIREAD锁 94 | 95 | # Deferrable Transactions 96 | 97 | 说的是长时间运行的可能被abort,并且加SIREAD也会导致很大的开销。所以他会等待其他的并发事务运行完毕后,再开始运行,从而避免加SIREAD锁。 98 | 99 | # Implementing SSI in PostgreSQL 100 | 101 | ## PostgreSQL Background 102 | 103 | 之前的Postgres提供两种,SI和RC,其中RC每次query都会获取一个最新的snapshot。而SI只有一次。 104 | 105 | ## Detecting Conflicts 106 | 107 | 对于rw-conflict,这里有两种情况。分别是先写再读,以及先读再写。注意rw说的是读事务要排序在写事务之前,而非真正的发生在他之前。比如在写事务提交之前,读事务读到了一个之前的版本,这时候就会出现rw-conflict。在postgres现有的实现中,通过xmin和xmax来确定tuple的可见性。当一个读者在遍历tuple的时候,发现xmin是一个活跃的事务。或者他读到的xmax也是一个活跃的事务。说明出现了rw-conflict。这时候读者就必须在写者之前提交。 108 | 109 | 而对于读者先的情况,Postgres构建了单独的SIREAD锁表。用来添加SIREAD lock,以及检查冲突。这样就可以检测到所有的rw-conflict。 110 | 111 | ### Implementation of the SSI Lock Manager 112 | 113 | 读操作会在tuple上SIREAD lock。而对于索引读,则会在B+Tree的页级别来上锁。 114 | 115 | 虽然使用了多级粒度的锁(page, tuple, table),但是意图锁是没必要的。我们可以按顺序检查各个级别的锁。从而防止并发的锁更新的问题。(这个目前我也不太清楚,我个人感觉意图锁和多粒度的锁的目的是一样的) 116 | 117 | SSI lock mananger需要额外处理的东西就是当有DDL来的时候,lock manager中那些通过物理位置定位的锁会失效。比如锁住的是某个tuple id。当DDL修改table之后,tuple会移动位置。所以这时候我们会将SIREAD转移到表级别。同样在索引失效的时候,索引上的锁也会转移到表级别。这些问题在S2PL的LockManager中不会有问题,因为read lock会阻塞DDL(intention lock) 118 | 119 | ## Tracking Conflicts 120 | 121 | 我们的核心目的是跟踪连续的rw边。不同的实现有不同的方法。原始的SSI说每个transaction可以维护两个bit,代表是否有入边,以及是否有出边。 122 | 123 | Postgres的选择是跟踪所有的rw边。因为Postgres希望实现上面提到的commit ordering优化。即只有T3提交时间比T1和T2早才可能出现问题。所以我们不能简单的记录一个bit。而是需要记录具体的txn id,从而知道txn的提交时间。并且当有一个txn abort的时候,我们还可以去除掉这些rw边。 124 | 125 | Postgres提到他们没有选择跟踪wr和ww关系的一个原因是这些依赖可能存在于外部。 126 | 127 | 比如上面Example2中的例子。如果T1会划分为两个txn。一个读batch number,一个根据读到的x去读reciept。 128 | 129 | 这时候依赖图会变成这样 130 | 131 | ![20220710112131](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220710112131.png) 132 | 133 | 其中T1.1到T1.2的依赖会变成因果依赖。这是数据库系统跟踪不到的。 134 | 135 | ## Resolving Conflicts: Safe Retry 136 | 137 | 当发现了危险的结构的时候,我们希望要abort掉的txn具有某些性质: 138 | 139 | ![20220710112420](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220710112420.png) 140 | 141 | 这里有三条规则 142 | 1. T3 commit之前不要abort事务。因为我们要使用commit ordering optimization 143 | 2. 总是尝试abort T2。因为T3已经提交了。当T2 retry的时候,他一定不会和T3并发,构成rw依赖。所以不会出现相同的结构。 144 | 3. 假如T2已经提交了。那么就abort T1。这样也是安全的。因为T1不会再和T2,T3并发。 145 | 146 | (说白了就是T1和T2都存在的时候不要abort T1) 147 | 148 | 由于我们没有在出现危险结构的时候立刻abort txn。所以我们也需要在txn尝试提交的时候去检查一下。比如T3在提交的时候会发现这个结构。那么他会尝试abort掉T2,来让自己提交。 149 | 150 | 结合上面的理论我们可以猜测一下他的实现方案。由于只有T3是第一个提交的事务的时候才会构成问题。那么当T3提交的时候,我们可以检查T3所有的入边。对于起点的txn,如果他提交了,那么不会构成危险结构。忽略即可。对于未提交的txn,则需要再次遍历他的入边。只有找到了T1和T2都未提交的情况下,我们才需要abort掉链上的T2。 151 | -------------------------------------------------------------------------------- /db/Serializable_Snapshot_Isolation_in_PostgreSQL/p1850_danrkports_vldb2012.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ysj1173886760/paper_notes/df11876635eaa0da706671f51f6edd3b42e5391c/db/Serializable_Snapshot_Isolation_in_PostgreSQL/p1850_danrkports_vldb2012.pdf -------------------------------------------------------------------------------- /db/Socrates-The-New-SQL-Server-in-the-Cloud/socrates.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ysj1173886760/paper_notes/df11876635eaa0da706671f51f6edd3b42e5391c/db/Socrates-The-New-SQL-Server-in-the-Cloud/socrates.pdf -------------------------------------------------------------------------------- /db/TicToc_Time_Travling_Optimisitc_Concurrency_Control/notes.md: -------------------------------------------------------------------------------- 1 | # TicToc: Time Traveling Optimisitc Concurrency Control 2 | 3 | The key contribution of TicToc is a technique that we call data-driven timestamp management: instead of assigning timestamps to each transaction independently of the data it accesses, TicToc embeds the necessary timestamp information in each tuple to enable each transaction to compute a valid commit timestamp after it has run, right before it commits. 4 | 5 | This approach has two benefits. First, each transaction infers its timestamp from metadata associated to each tuple it reads or writes. No centralized timestamp allocator exists, and concurrent transactions accessing disjoint data do not communicate, eliminating the timestamp allocation bottleneck. 6 | 7 | Second, by determining timestamps lazily at commit time, TicToc finds a logical-time order that enforces serializability even among transactions that overlap in physical time and would cause aborts in other T/O-based protocols. In essence, TicToc allows commit timestamps to move forward in time to uncover more concurrency than existing schemes without violating serializability. 8 | 9 | DTA-based OCC会分配一个区间的timestamp,然后在validation的时候进行改变,从而降低abort率。(可能是通过推迟某些事务的验证阶段?) 10 | 11 | Silo通过epoch来分配粗粒度的时间戳。从而避免了ts分配的瓶颈。每40ms去分配一次时间戳,成为epoch。在epoch内部,通过txn id来标识版本并检查冲突。 12 | 13 | # The TicToc Algorithm 14 | 15 | TicToc也使用ts来标识txn的顺序。不同的是他不是分配一个ts给txn,而是在提交的时候,根据txn访问的tuple去计算他的ts 16 | 17 | ![20220716221102](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220716221102.png) 18 | 19 | ![20220716221046](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220716221046.png) 20 | 21 | ![20220716221222](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220716221222.png) 22 | 23 | ![20220716224854](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220716224854.png) 24 | 25 | ![20220717092151](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220717092151.png) 26 | 27 | 一个提交的例子,B虽然先提交,但是他计算得到了自己的ts为4。而A则得到了ts为3。 28 | 29 | Note that when the DBMS validates transaction A, it is not even aware that tuple x has been modified by another transaction. This is different from existing OCC algorithms (including Hekaton and Silo) which always recheck the tuples in the read set. 30 | 31 | 验证阶段他不需要去重复验证读集。因为他的commit ts不一定是最新的。所以TicToc不是commit order-preserving的 32 | 33 | tictoc的encoding方法,以及进行原子加载的方法 34 | 35 | ![20220717093636](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220717093636.png) 36 | 37 | ![20220717094200](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220717094200.png) 38 | 39 | 在延伸rts的时候,要注意防止overflow。要同时修改wts来防止delta overflow。修改wts的操作可以视为一个dummy write 40 | 41 | (shift的选取是不是也是一个要考虑的点?) 42 | 43 | 确定了新的ts后,通过CAS把它换进去。 44 | 45 | 虽然修改了wts,但是不需要上锁,因为他不会引起正确性问题。但是其他的txn可能因为wts的不同而abort。即产生false positive 46 | 47 | 目前的paper中没有提出防止phantom的方法。 48 | 49 | TicToc还可以利用parallel logging来加速。思路就是将log分为batch,同一个batch的txn的log可以是任意顺序的。而不同batch的log需要有序。我们可以计算出前一个batch的log的最大ts,然后当前batch的最小ts就设为前一个batch的最大ts即可。 50 | 51 | (如果只有redo的log的话,是不是不需要有顺序?) 52 | 53 | ![20220717102342](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220717102342.png) 54 | 55 | 这个equal-delta表示的是something is equal to something by definition. 56 | 57 | 即A在串行化顺序中小于B,是等于A的ts小于B,或者A的ts等于B,且A的pt小于B。如果他们没有冲突的话,那么A的ts是可能等于B的,那么他们两个的串行化顺序是无所谓的,所以这里用物理时间来标识。 58 | 59 | 其实还有另一种情况就是读事务读到了写事务写的内容,这时候读事务可能拥有和写事务相同的logical ts,但是在serial order中读事务一定要在写事务之后。所以物理时间也是需要的。所以只有在相同的物理时间且相同的逻辑时间的情况下,他们可以是任意的。否则我们仍然需要物理时间来确定serial order。 60 | 61 | # Optimizations 62 | 63 | validation阶段拿锁的时候,我们可以根据primary key order去拿锁,从而防止死锁问题。但是会引入额外的等待。 64 | 65 | ![20220717105746](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220717105746.png) 66 | 67 | 这个例子中,ABCD就会变成串行执行。所以一个优化就是利用no-wait的策略,当获取锁失败的时候,就放锁,睡眠一小段时间,然后重启validation phase。 68 | 69 | 这样的话上面的C就会立刻abort,从而允许B继续执行。 70 | 71 | 支持SI。SI允许读写ts发生在不同的ts中。并且要求这个时间段中没有其他的人写和当前事务的写有交集。所以在TicToc中,我们可以使用两个ts来分离开读写。即commit rts和commit wts。验证读集的时候只需要验证commit rts,验证写集的时候只需要验证commit wts。 72 | 73 | ![20220717111059](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220717111059.png) 74 | 75 | SI其实是将这个拆开,对于读集的元素,只要保证commit rts在他们之间,commit wts要大于写集的rts。当然还要保证commit wts大于等于commit rts。计算出这两个ts后,要验证一下write set中的wts不能存在于commit rts和commit wts之间。 76 | 77 | 具体的,可以先计算commit rts,然后设置commit wts最小值为commit rts,然后计算commit wts。最后验证write set的wts要小于commit rts。 78 | 79 | -------------------------------------------------------------------------------- /db/TicToc_Time_Travling_Optimisitc_Concurrency_Control/tictoc.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ysj1173886760/paper_notes/df11876635eaa0da706671f51f6edd3b42e5391c/db/TicToc_Time_Travling_Optimisitc_Concurrency_Control/tictoc.pdf -------------------------------------------------------------------------------- /db/architecture_of_database_system/linziyu-Architecture of a Database System(Chinese Version)-ALL.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ysj1173886760/paper_notes/df11876635eaa0da706671f51f6edd3b42e5391c/db/architecture_of_database_system/linziyu-Architecture of a Database System(Chinese Version)-ALL.pdf -------------------------------------------------------------------------------- /db/architecture_of_database_system/notes.md: -------------------------------------------------------------------------------- 1 | # 2 2 | 3 | ## 2.4 准入控制 4 | 5 | DBMS无法保证缓冲区的工作空间,导致出现不断的页替换的情况。比如在使用排序或者哈系join的时候就会消耗大量的内存 6 | 7 | 事务处理发生死锁后也会导致事务的重启,从而导致系统挂起 8 | 9 | 所以当系统拥有一个好的准入控制器的时候,在资源不够的情况下,新的请求将不会被接受 10 | 11 | DBMS的准入控制有两个层面 12 | 13 | 1是保证客户端连接数不会超过一个值,可以避免类似网络这样的基础资源被消耗。这种机制有的时候也会被上层应用提供 14 | 15 | 2是在查询处理器上实现的。 16 | 17 | 准入控制器在查询语句转换和优化完成后执行这一步操作,由该操作来决定,是否要推迟执行一个查询,是否要使用更少的资源来执行查询,以及是否需要额外的限制条件来执行查询。准入控制器依靠查询优化器的信息来执行,如查询所需的资源以及系统并发资源等信息。特殊情况下,优化器的查询计划可以: 18 | (1)确定查询所需要的磁盘设备以及对磁盘的 I/O 请求数; 19 | (2)根据查询中的操作以及要求的元组数目判断 CPU 负载; 20 | (3)评估查询数据结构的内存使用情况,包括在连接和其他操作期间的排序和哈希所消耗的内存 21 | 22 | 第三点最关键,因为内存通常是出现抖动的主要原因(不断的换页?) 23 | 24 | # 3 25 | 26 | 并行架构 27 | 28 | 1.共享内存,单机中,多个处理器核共享一个内存 29 | 30 | 2.无共享。由多个独立的计算机通过网络进行互联 31 | 32 | 每个表格会通过水平数据分区传播到集群中的多个系统中 33 | 34 | 典型的数据分区方案包括:元组属性基于哈希的分区,元组属性基于范围的分区,round-robin,以及混合型分区方案(前两种方案的混合) 35 | 36 | 数据库元组采用基于值的分区导致的一个很自然的结果就是,在这些系统中,只需要最少的协调工作。 37 | 38 | 然而,为了得到很好的性能,就需要很好地数据分区。这就给数据库管理员一个重大的负担――合理明智地确定表的分布。同时给查询优化器一个重大的负担――需要在负载分区方面做得很好。 39 | 40 | 在共享内存的系统中,当某一个处理器发生故障的时候通常会导致整个系统停止运行 41 | 42 | 而在无共享系统中,一个节点发生故障不一定会影响到其他的节点。 43 | 44 | 如果发生故障的时候,我们停止整个系统,其实本质上就是在模拟一个共享内存系统 45 | 46 | 第二种方法是跳过故障节点的执行。在数据的可用性比完整性更重要的情况下很有用 47 | 48 | 第三种方法是冗余,元组副本会分布在集群中的多个节点,当某一个节点故障的时候,系统可以将负载分配到剩下的节点 49 | 50 | 3.共享磁盘 51 | 52 | 所有的处理器可以访问大致性能相同的磁盘,并且当一个节点故障的时候,不会影响到其他节点的工作 53 | 54 | 但是单个磁盘也会导致单点故障的问题 55 | 56 | 并且,由于不会共享内存,我们不能很自然的去协调内存中的数据。所以我们需要显式的进行数据的共享协调。 57 | 58 | 共享磁盘系统依赖于一个分布式锁管理设备和一个管理分布式缓冲池的高速缓存一致性协议。(协调多个节点的数据) 59 | 60 | -------------------------------------------------------------------------------- /db/arkdb_a_key_value_engine_for_scalable_cloud_storage_services/ArkDB.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ysj1173886760/paper_notes/df11876635eaa0da706671f51f6edd3b42e5391c/db/arkdb_a_key_value_engine_for_scalable_cloud_storage_services/ArkDB.pdf -------------------------------------------------------------------------------- /db/arkdb_a_key_value_engine_for_scalable_cloud_storage_services/notes.md: -------------------------------------------------------------------------------- 1 | # ArkDB: A Key-Value Engine for Scalable Cloud Storage Services 2 | 3 | ![](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220806103427.png) 4 | 5 | ![](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220806103600.png) 6 | 7 | ![](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220806103832.png$$) -------------------------------------------------------------------------------- /db/bw-tree-cmu/mod342-wangA.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ysj1173886760/paper_notes/df11876635eaa0da706671f51f6edd3b42e5391c/db/bw-tree-cmu/mod342-wangA.pdf -------------------------------------------------------------------------------- /db/bw-tree-cmu/notes.md: -------------------------------------------------------------------------------- 1 | # Building a Bw-Tree Takes More Than Just Buzz Words 2 | 3 | 两个贡献,一个是Bw-Tree的实现教程,并且提出了新的优化策略。第二个则是发现BwTree并不如其他使用锁的并发数据结构更快 4 | 5 | # Introduction 6 | 7 | Lock-free的数据结构实现的难点: 8 | 1. 需要明白所有的race conditions 9 | 2. 并发线程的同步点通常不会放到算法中,导致人们实现出错,最后变成了busy-waiting loop 10 | 3. 需要保证所有的读者全部离开后才能回收内存(在mit os中提到过linux在使用RCU的时候,会等待一个时间上限再去清理) 11 | 4. 原子操作会成为performance bottleneck 12 | 13 | BwTree的思想就是通过间接层,将逻辑标识符映射到物理地址中,从而避免锁。每个线程通过追加delta record到modification log中来实现修改。后续的操作就必须重放这些delta record来获得当前的状态 14 | 15 | 间接层和delta record有两个优势: 16 | 1. 通过将global state变成atomic steps来避免锁的开销 17 | 2. 由于是追加delta record而不是修改,会减少cache invalidation的次数,从而获得更小的同步开销 18 | 19 | # BwTree Essentials 20 | 21 | BwTree和其他的基于B+Tree的区别就是BwTree避免了直接修改树上的节点(因为会导致cache invalidation)。他为每个节点都维护了一个delta chain,从而可以让BwTree通过CAS来更新。indirection layer的作用就是可以让我们原子的更新对树节点的引用(比如孩子节点和祖父节点都指向父亲节点,我们可以通过间接层原子的让他们都指向新的父亲节点) 22 | 23 | ![20220425102129](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220425102129.png) 24 | 25 | 每一个节点都有一个逻辑ID,节点在指向其他节点的时候会用这个逻辑ID。当一个线程需要他的物理地址的时候,就会去MappingTable中查。MappingTable允许我们通过CAS来更新节点的地址 26 | 27 | ## Base Nodes and Delta Chains 28 | 29 | 一个逻辑节点在BwTree中有两个部分,一个base node,以及一个delta chain 30 | 31 | 这里有两种类型的base node: 32 | 1. inner base node,存储的是有序的(key, NodeID)的数组 33 | 2. leaf based node,存储的是有序的(key, Value)的数组 34 | (和b树一样) 35 | 36 | 最开始的时候,BwTree有两个节点,一个空的叶子节点,以及一个inner节点,指向了叶子节点。 37 | 38 | Base Node是不可变的 39 | 40 | ![20220425103530](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220425103530.png) 41 | 42 | delta chain是一个单向链表,存储的是按序排的针对base node的修改历史 43 | 44 | base node和delta record都存储了可以表示那个时刻状态的元信息 45 | 46 | ![20220425103952](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220425103952.png) 47 | 48 | 这样我们就不需要一步一步重放操作来获得最新状态。(但是当需要数据的时候应该还是需要重放,这里只是保存了状态而已) 49 | 50 | 线程在修改数据的时候,会追加delta record,再修改mapping table让他指向新的delta record 51 | 52 | ## Mapping Table 53 | 54 | BwTree借用了BlinkTree的设计,每个节点有两个入指针,一个是从父亲来的,一个是从左兄弟来的。在更新的时候我们需要进行原子的修改这些指针,所以要么使用transactional memory,或者multi-wrod CAS 55 | 56 | 而BwTree通过用间接层来避免这些问题。从而允许一次CAS来完成原子修改 57 | 58 | 当CAS失败的时候,这次操作就会重启。每次重启都会从树根开始重新遍历。虽然更复杂的重启协议是可能的,但是我们认为从树根开始遍历简化了实现。并且我们要遍历的节点很可能会在cache中 59 | 60 | 他提了一点就是这里的Mapping Table只关注了BwTree的实现。实际上他也可以支持log-structured updates。(这里我不太明白,可能需要再去研究一下log-structured相关的细节。他的意思可能是LSM中一个数据会在多层出现,我们通过只修改间接层从而可以直接修改多层数据) 61 | 62 | ## Consolidation and Garbage Collection 63 | 64 | 当delta chain增长的时候,worker每次来就需要重新遍历并重建当前的状态。为了防止过长的delta chain,worker会周期性的压缩delta chain并重组成一个新的base node。 65 | 66 | 在压缩的最开始,线程首先把logical node的base node复制到他的私有内存中,然后开始应用delta chain。接着他会更新MappingTable中的指针,指向新的logical node。最后当所有的线程都离开老的节点的时候,我们就可以回收他的内存 67 | 68 | ## Structural Modification 69 | 70 | 和B+树一样,Bw树也会出现overflow或者underflow的情况。从而导致了splitting以及merging 71 | 72 | 核心思想就是利用特殊的delta record来表示内部结构的变化 73 | 74 | SMO(structural modification protocol)的操作有两个阶段,一个是logical phase,用来追加特殊的delta record来通知其他的线程这里会有一个SMO,以及一个physical phase,用来实际执行SMO(即split或者merge,然后通过CAS来替换掉老的节点) 75 | 76 | 尽管BwTree是lock-free的,但是如果CAS失败的话线程仍然无法make progress(比如在这个两阶段操作中,我们不能假设是同一个线程在做,否则就会阻塞其他的线程的进度)。一个解决这个的方法就是合作执行multi-stage的SMO,也被称为help-along protocol。线程必须在节点被遍历之前帮助完成SMO的未完成阶段 77 | 78 | (我怎么感觉他这块说的都好迷幻,应该看一下BwTree之前的文章) 79 | 80 | # Missing Components 81 | 82 | ## Non-unique Key Support 83 | 84 | 在遍历的过程中,BwTree会停在第一个匹配搜索键的地方。但是这样是无法支持non-unique key的 85 | 86 | 我们在遍历的时候维护两个集合,$S_{present}$和$S_{deleted}$。$S_{present}$ contains the values that are already known to be present. $S_{deleted}$ contains the value that are known to be deleted 87 | 88 | 如果发现了一个insert record,键为K,值为V,并且V不属于$S_{deleted}$,我们就会把V加入到$S_{present}$中。 89 | 90 | 如果发现了delete record,并且V不属于$S_{present}$,他就会把V加入到$S_{deleted}$中 91 | 92 | 更新操作是通过删除后插入来完成的。当遇到最后的base的时候,最终构建出的节点就是$S_{present} \cup (S_{base} - S_{deleted})$ 93 | 94 | ![20220425143451](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220425143451.png) 95 | 96 | 这里的查找是针对叶子节点单个K的,不是这个K的record会被忽略掉 97 | 98 | ## Iteration 99 | 100 | 直接在BwTree上进行遍历是非常困难的,因为我们很难定位当前迭代器的位置。并且还需要处理SMO以及并发插入删除的问题 101 | 102 | 我们的迭代器不直接放在树节点上。每个迭代器都会维护一个只读的logical node的副本。 103 | 104 | 当迭代器移动的时候,如果当前的副本用完了,我们就会通过low key或者high key来重新遍历一次 105 | 106 | ## Mapping Table Expansion 107 | 108 | 每个线程都会访问Mapping Table,所以不让他成为bottleneck是很关键的 109 | 110 | Mapping Table就是一个数组。动态增长的方法就是使用Lazy allocation。我们提前分配好虚拟空间,当实际使用的时候再分配物理内存 111 | 112 | 而对于Shinking的情况,没有很好的方法,我们只能阻塞住worker thread,然后重构索引 113 | 114 | (我在想他们的logical id是怎么复用的?用free list吗?个人猜想是先放到free list中,但是不用。每过一段时间冻结free list,然后再用free list中的内容) 115 | 116 | # Component Optimization 117 | 118 | ## Delta Record Pre-allocation 119 | 120 | Delta Chain in Bw-Tree is a linked list of delta records that is allocated on the heap. 遍历这个Delta chain会变得很慢,因为局部性很差。并且大量的并发内存分配会导致分配器的争用,从而导致了bottleneck 121 | 122 | 我们会在base node中提前分配delta record 123 | 124 | ![20220425153903](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220425153903.png) 125 | 126 | base node会在高地址的边缘。而delta record会从高到低进行分配(利用pre-fetch) 127 | 128 | 每个链也会维护一个allocation marker,指向的是最后一个delta record。当worker需要一个slot的时候,他就会减少这个marker(和栈类似),如果预先分配好的区域满了,他就会触发压缩操作 129 | 130 | delta record不会删除,所以没有free-list的问题。图中的交错是因为并发的操作导致的,比如第一个线程先分配了空间,但是是后面才append到链中(如果这里失效了,他会放弃掉这段内存吗?因为前面说了当CAS失败的时候要重新遍历,但是重新遍历有可能我们就不会回到相同的node了,是不是只有split/merge的情况才需要重新遍历呢?) 131 | 132 | ## Garbage Collection 133 | 134 | 之前的BwTree用的是Epoch-based GC,每过一段时间他会在一个全局的链表上追加一个epoch object。每个线程在访问索引之前必须先在epoch object上注册自己 135 | 136 | 让线程结束操作以后,他就会把自己从epoch object上移除。所有的删除操作都会被加入到当前epoch的garbage list中,当所有的线程都离开这个epoch的时候,我们就可以安全的回收garbage list中的对象 137 | 138 | (这样可以安全的保证吗?我感觉在这个操作结束之前的epoch都可能出问题,而不是只有开始的epoch。貌似文章中说了,在操作完成的时候,把删除的对象加入到当前epoch,而不是他进入的那个epoch,所以应该没问题。我们只要保证epoch是按序删除的就行) 139 | 140 | (第二个思考是他们为什么回避对单个node进行refcnt呢?这样最后一个人负责删除他就好了,可能是为了防止写元数据导致的cache invalidation) 141 | 142 | ![20220425160520](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220425160520.png) 143 | 144 | 这种中心化的方法会导致scalability的问题。因为cache coherence这时候会变成瓶颈 145 | 146 | OpenBwTree用了一种去中心化的方法。 147 | 148 | ![20220425161132](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220425161132.png) 149 | 150 | 索引自己维护了global epoch。每个线程自己维护了本地的epoch,以及他删除的对象列表。 151 | 152 | 在开始操作的时候,线程会把当前的全局epoch复制到本地的epoch中。当他完成操作的时候,他会再次将全局epoch复制到本地epoch中,并且把garbage打上最新的global epoch的标记(表示所有小于这个epoch的线程都有可能访问到他)。然后开始GC。他会收集其他线程的local epoch,并且删除所有小于最小local epoch的那些对象。 153 | 154 | 线程的local epoch表示的就是这个线程目前的操作正在那个epoch。如果所有人都高于garbage的epoch,就可以安全的回收 155 | 156 | 这样其实是分散了争用。之前用的是计数器。这里是维护的本地的watermark,从而获得全局最小。然后我们回收小于全局最小的对象。并且这里的删除分摊到了每个线程中 157 | 158 | (思考,其实是一种去中心化的维护watermark的方法。因为在中心化的实现中,我们完全可以用一个sorted-list来维护当前的active-operation的epoch,每次结束操作就把他产生的garbage打上最新epoch的标记,然后把自己从sorted-list中移除。这样的话垃圾回收器只要读第一个节点就可以,但是对于注册这个操作来说,他可能会引起争用,同时lock-free的双向链表也不清楚有没有。所以这个方法是写慢,但是读快。而去中心化之后,每个人其实相当于都遍历了一遍链表才能获得global minimum,读虽然慢了,但是他的写很快,并且没有争用。这么想的话其实lsm tree也有这个思路,加快写(通过append only),但是代价就是读变慢) 159 | 160 | ## Fast Consolidation 161 | 162 | ![20220425180433](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220425180433.png) 163 | 164 | consolidation分为两个阶段,第一个阶段是复用之前的non-unique key的方法来找到那些元组被删掉了,那些被插入了。然后第二个阶段则是把插入的新元组和之前的老节点做一次2路归并,从而生成新的base-node 165 | 166 | insert record和delete record在作用于base node的时候,会存储一个offset字段,用来表示这次操作会作用到哪里。然后我们用Spresent和Sdeleted来将base node划分成若干个片段。再用这些片段和insert record做归并 167 | 168 | (我在想维护这些segment的overhead难道不大么,直接sort感觉会更好一些) 169 | 170 | ## Node Search Shortcuts 171 | 172 | 当节点很大的时候,我们一次二分搜索可能会跨越多个cache line,从而导致效率不高 173 | 174 | 我们通过micro-indexing来减少节点内的搜索 175 | 176 | ![20220425182052](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220425182052.png) 177 | 178 | 这里是减少了最后的base node的扫描范围 179 | 180 | 因为之前的insert以及delete record记录的都是当前的Key对应在base node的偏移量。所以当我们在路上比较的时候就可以顺便获取base node中值的范围。从而在最后一步实现快速的查找 181 | 182 | # Evaluation 183 | 184 | 这里写几个我感觉比较有意思的点吧 185 | 186 | ![20220426094846](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220426094846.png) 187 | 188 | 从这个图大概可以看出来,BwTree的效率不高可能是因为访存太多,也就是memory footprint太大导致的。 189 | 190 | ![20220426095211](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220426095211.png) 191 | 192 | 这里也显示了,在HC(high contention)的情况下,abort rate是1000%,所以一次操作会abort10次才能成功,所以导致了大量的footprint。反而降低了lock-free的优势。因为本身lock-free的算法就是在高争用情况下保证可以make progress 193 | 194 | ![20220426095445](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220426095445.png) 195 | 196 | ![20220426095737](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220426095737.png) 197 | 198 | 表现最好的ART拥有相当高的局部性 199 | 200 | 这样看起来是BwTree的结构导致的他表现的不好,比如更多的Branch,更多的预测失败等 201 | 202 | 所以可能BwTree更适合在flash中,而非memory中。因为他的优势在于append-delta,所以cache invalidation的优化不能弥补更多的memory footprint带来的开销。但是flash中比较偏向这种结构,也就是log-structured形式。所以可能BwTree在flash中表现的会更好 203 | 204 | 最后他有一个performance decompisition,基本上把BwTree的feature都关掉,然后比较性能 205 | 206 | 但是结果是即便把feature都关掉,BwTree还是比B+Tree慢一些。原文说: Even Read-only operations perform considerable bookkeeping to maintain the consistency of the tree, limiting its performance. 207 | 208 | 这里我不太明白他的都维护了什么东西,但是可能就是因为为了latch-free所维护的各种信息吧,导致了高开销 -------------------------------------------------------------------------------- /db/bw-tree-ms/bwtree-ms.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ysj1173886760/paper_notes/df11876635eaa0da706671f51f6edd3b42e5391c/db/bw-tree-ms/bwtree-ms.pdf -------------------------------------------------------------------------------- /db/constant_time_recovery_in_Azure_SQL_Database/notes.md: -------------------------------------------------------------------------------- 1 | # Constant Time Recovery in Azure SQL Database 2 | 3 | # Abstract 4 | 5 | 这个恢复机制结合了ARIES和MVCC,从而实现了常数时间的恢复。 6 | 7 | 允许连续的log trucation,从而减少了日志空间的使用量,即使是有长事务的存在(对比Innodb,如果Undo table用完了就不能开新的事务了) 8 | 9 | 对于云数据库(Cloud database,应该是DBaaS)来说,这个能力是相当重要的,因为: 10 | 1. 数据库大小是不断增加的 11 | 2. 对于commodity hardware来说,failure很常见 12 | 3. 高可用的要求 13 | 4. 云平台负责管理和升级软件,所以可能导致对于用户不可预见的failure(这里指的是服务软件的升级由云平台决定,而非用户。如果是用户控制的话他们可能选择一个流量小的时间进行维护) 14 | 15 | # Introduction 16 | 17 | ARIES使用WAL,并且定义了不同的恢复阶段(Scan,Redo,Undo)来避免专用的恢复手段(ad-hoc recovery techniques,这里应该说的是针对不同种类的transaction有不同的恢复技术,而ARIES把这个过程泛化了) 18 | 19 | 恢复的一个很主要的点就是Undo,我们需要把那些未提交的事务全部Undo,对于长事务来说,这个过程可能持续很久。一个例子就是一个用户尝试在一个事务中加载亿级的数据,然而当数据库在这个过程中崩溃的话,我们需要把加载的这些数据整个undo 20 | 21 | 在SQL Server中,他有一些针对恢复阶段的优化,比如利用并行来加速恢复。尽管这些优化对于内部部署的数据库是足够的(指的应该是企业内部的服务器部署)。但是当数据库迁移到云端的时候,情况就不一样了: 22 | * 数据库大小的增加通常导致了更长的事务 23 | * 云平台所使用的commodity hardware失效的频率越高,就会导致在此之上的服务崩溃的概率也变高 24 | * 上面提到过的云平台负责管理和升级服务,导致用户不可预见的崩溃 25 | 26 | 我猜测应该是利用MVCC的版本机制来加速Undo。 27 | 28 | 有两个要点,一个就是恢复机制和MVCC怎么结合来实现常数时间的恢复,还有一个就是他是怎么允许aggressively truncating log的,对于长事务来说他的log应该只能在提交的时候被截断,否则我们无法保证原子性 29 | 30 | # Background On SQL Server 31 | 32 | 第一部分就是正常的ARIES算法 33 | 34 | ![20220517105420](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220517105420.png) 35 | 36 | 可以看到这里的日志就是物理逻辑的,即日志标识了数据的位置,并且记录了页内的逻辑操作。减少了日志空间的占用。并且相对于逻辑日志来说允许了更高的并发性,并且逻辑更简单,因为逻辑日志依赖数据库的元数据。 37 | 38 | 但是SQL server在Redo阶段有个变化 39 | 40 | ![20220517105336](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220517105336.png) 41 | 42 | 他为了提高可用性,他会记录最小的活跃事务,并获取对应的锁。这样就可以在Redo阶段把数据库恢复到失效前的状态,从而提高可用性(Undo阶段则是启动后再进行)。但是这会导致Redo阶段和最长的活跃事务大小相关(因为我们需要重新获得上面的锁) 43 | 44 | Undo阶段会扫描日志,并undo未提交的日志。这个阶段则和未提交的事务的大小相关(或者说是活跃事务的大小)。为了保证Undo的幂等性,我们还需要用CLR来记录undo操作。 45 | 46 | MVCC,SQL Server更新会原地更新,然后把老的版本放到version store中。 47 | 48 | ![20220517110814](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220517110814.png) 49 | 50 | version locator是(PageId, SlotId) 51 | 52 | SQLServer会在重启的时候释放掉Version store,因为新的事务看不到老版本的数据 53 | 54 | # Constant Time Recovery 55 | 56 | CTR支持了: 57 | 1. Database recovery in constant time, regardless of the user workload and transaction sizes. 58 | 2. Transaction rollback in constant time regardless of the transaction size. 59 | 3. Continuous transaction log truncation, even in the presence of long running transactions 60 | 61 | CTR将事务的操作分成了3类 62 | 63 | ### Data Modifications 64 | 65 | 之前的存储是把老版本存在version store,然后每次重启丢弃version store。现在则是把所有的版本都持久化下来。每个版本都记录了transaction id,从而允许我们去识别对应事务的状态(active,committed,aborted)。当一个事务回滚的时候,他可以简单的标记为rollback,这样后续的版本链遍历的时候,就会跳过这个版本(这样每次读版本都需要去看一个中心化的结构,不会慢么?)。这样就实现了常数时间的Undo,因为我们只需要打一个标记,不需要回滚具体的数据 66 | 67 | ### System Operations 68 | 69 | 这个是内部的操作,比如空间分配,B树的分裂等。这些信息不能被versioned。 70 | 71 | 这些操作会通过短时间的系统事务来执行并且提交。比如当用户需要插入大个的数据并请求空间分配的时候,我们就会通过system transaction来分配空间并且提交。当出现崩溃的时候,这些操作不会被undo。而那些空间以及其他被更新的内部数据结构则会在后台被回收以及修复。 72 | 73 | ### Logical And Other Non-versioned Operations 74 | 75 | 这个类别下的操作也是不能被versioned,因为: 76 | 1. 他们是逻辑操作,比如获得一个锁 77 | 2. 这些操作修改的是那些在数据库启动时候,但是在恢复过程开始之前要读取的数据 78 | 79 | 我们仍然需要log来进行redo/undo,但是这些操作一般都是schema changes等,并且数量会很少,所以CTR通过额外的一类log来记录他们,并且可以在很短的时间内恢复他们。(这个类别就是正常的ARIES了,没有额外的优化) 80 | 81 | ## Persistent Version Store 82 | 83 | PVS由于存储了所有的版本,所以他需要回收老的版本,并且处理数据库空间占用大的问题。 84 | 85 | 为了解决这个问题PVS分成了两层 86 | 87 | ### In-row Version Store 88 | 89 | ![20220517131721](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220517131721.png) 90 | 91 | 一个tuple(row)存储了不仅是最新的数据,还有delta,用来重建之前的版本 92 | 93 | in-row version可以减少存储开销,以及用日志记录新的版本,因为: 94 | 1. We effectively log the version together with the data modification 95 | 2. We only increase the log generated by the size of diff 96 | 97 | 这里说的应该是我们不需要额外的日志来记录版本的生成,现在版本的生成以及数据的改变都在行内执行,用一个log就可以记录下来。(第二点我不太清楚,可能意思是只log增量) 98 | 99 | 为了防止一个row过大,PVS还限制了一行的大小,以及diff的大小。当一行过大的时候,我们就会生成一个新的version 100 | 101 | ### Off-row Version Store 102 | 103 | 每个database(SQL-level)都有一个version table存储了所有表的off-row version。并且通过传统的恢复技术来保证持久化。就和正常存储一个元组没有区别,只是这个表不需要索引,因为我们是通过version locator来进行遍历的 104 | 105 | 通过table来简化version的存储,并且针对并发插入是高度优化的。accessor会被分区,并且日志只需要记录redo(保证持久化),而不需要额外实例化一个事务来插入。(分区这个我猜测应该是每个分配表的一部分用来插入,从而避免冲突) 106 | 107 | (感觉PVS的核心还是在于怎么回收历史版本,至于存储的话,这个应该相当于是一个append-only version store和delta store的混合版) 108 | 109 | ## Logical Revert 110 | 111 | 这里就是前面说的,我们在每个版本中记录txn id(可以和timestamp合并),当abort的时候,可以简单的修改事务的状态,而不需要去触碰具体的版本链中的数据。在遍历的时候遇到abort的数据就可以直接跳过。 112 | 113 | 但是读操作可能需要遍历很多的版本才能读到一个提交的版本。并且一个事务如果想基于aborted version更新的话,他必须先把回滚掉这个abort的数据,再写入新的数据(比如我们希望执行一个read-modify-write,必须先读到一个提交的版本,然后得到新的数据,再计算delta。(这样感觉本质还是因为我们需要很多的读)(也有一个可能是CTR只允许在commit version上进行更新,所以遇到abort version的时候要主动进行清理) 114 | 115 | CTR提供了两种机制来回滚abort txn的更新: 116 | 117 | Logical Revert: 118 | 119 | ![20220517141401](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220517141401.png) 120 | 121 | 用来把最新的提交的版本带回到main row中。从而避免后续额外的遍历 122 | 123 | 他会比较目前abort version以及commit version的状态,并做出补偿操作,从而让主存储中的这一行变成提交的版本。而这个操作是在system transaction中执行的。 124 | 125 | logical revert是在系统后台进行的,用来回收那些aborted version 126 | 127 | Overwrite Abort: 128 | 129 | ![20220517141838](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220517141838.png) 130 | 131 | 当一个新的事务需要更新一个带有abort version的行的时候,不去使用Logical Revert。(这里的意思应该是更新操作不允许在abort version上进行,而是要先去回滚到commit version再执行)我们可以直接覆盖掉这个abort version,并且将指针指向上一个提交的版本。(我在想他们不考虑in-row version store了么?这里感觉考虑的都是off-row的) 132 | 133 | (这么想的话是不是版本链只可能在最前面有一个abort version,其他都是commit version连起来的) 134 | 135 | (而且这个机制感觉就像是lazy abortion,abort的时候只打个标记,然后在后续读/写的时候再做清理,或者后台做清理) 136 | 137 | ### Transaction State Management 138 | 139 | CTR要求我们跟踪所有的abort txn的状态,直到他们的版本被logical reverted。 140 | 141 | (这里的用意应该是当所有的版本都不可见的时候,我们可以把这个txn移除掉。因为后续都不会有人来查询他的状态。) 142 | 143 | (我们可以通过read view来跟踪当前活跃事务,遇到非活跃事务的版本的时候,去查询他是否是aborted,就可以知道他是否可见) 144 | 145 | CTR将这个信息保存在了Aborted Transaction Map(ATM)中,他是一个哈希表。 146 | 147 | 当事务abort的时候,他会把自己加入到ATM中。并且添加一个abort的日志。当出现检查点的时候,ATM会被完整的序列化到log中。从而允许我们恢复ATM。 148 | 149 | 当一个事务所有的版本都被revert了,那么我们就可以把它从ATM中移除掉。移除的这个操作也需要进行log,因为我们需要保证ATM可恢复,所以所有操作都要log。通过FORGET来记录从ATM中移除一个txn 150 | 151 | ### Short Transaction Optimization 152 | 153 | 对于short oltp transaction,他们的恢复时间本身就很短,当使用了上述提到的技术后会导致增加开销,从而导致性能下降。 154 | 155 | CTR动态的决定txn的种类,根据txn的大小,CTR决定txn是否应该被简单的标记为abort(lazy abortion),或者是直接进行undo。(这样看的话任何操作都需要有redo/undo log,但是CTR的Undo貌似不依赖undo log,而是通过版本来进行。正常的undo貌似也可以直接通过版本信息来进行。) 156 | 157 | ## Non-versioned Operations 158 | 159 | 前面提到的,获取锁,更新元数据等操作不能被版本化,所以我们需要额外的机制来处理 160 | 161 | ### SLog: A Secondary Log Stream 162 | 163 | 用一个额外的日志流来记录这些信息,并且这些操作一般很少,所以我们可以实现快速的恢复。 164 | 165 | SLog一般用作回滚那些non-versioned operation。但是他也可以用在Redo阶段,用来重做logical operation,比如重新获得锁。 166 | 167 | SLog在内存中是一个链表 168 | 169 | ![20220517150938](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220517150938.png) 170 | 171 | 日志同样指向了相同事务的前一个日志,用来快速进行undo 172 | 173 | 在检查点的时候,我们可以把SLog序列化到transaction log中。并且平常的单个操作也会加入到transaction log中。所以在分析阶段,我们就可以构造出目前的in-memory SLog,并在后续的Redo/Undo阶段使用 174 | 175 | 我总感觉他这个SLog怪怪的,可能是因为我没有了解过元信息的恢复 176 | 177 | 我感觉只需要一个特殊的标记就可以在分析阶段重构元信息了 178 | 179 | ![20220517152743](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220517152743.png) 180 | 181 | 这么看的话可能就是优化了,毕竟如果我们从最老的地方开始扫描的话,虽然也可以重建,但是比较慢。并且逻辑操作不能通过lsn来确定,所以SLog应该是为了保证快速恢复的一个优化 182 | 183 | ### Leveraging System Transactions 184 | 185 | 我们希望SLog尽量的少,因为恢复阶段都需要SLog来参与,并且SLog是存储在内存中的,相比磁盘更加昂贵。 186 | 187 | 空间的分配和回收是最常见的不可版本化的操作了。在SQL Server中,这个信息是存储在了一个特殊的元数据页中,包含了一个bitmap用来指示空间分配情况。而我们不希望用SLog来记录空间分配的信息,所以CTR通过系统事务来进行空间的分配以及回收。 188 | 189 | 在CTR中,新页的分配会通过系统事务立刻提交,并且会被标记为potentially containing unused space,为了防止用户事务回滚后,新页中的数据不再有效。CTR通过后台线程去扫描这样的数据页,并在他们不包含数据项的时候回收这个页。(我在想这个页的回收可不可以让Logical Revert来标识并通知后台线程呢?但是Logical Revert可能不会碰到这些新插入的数据项) 190 | 191 | 对于释放来说,则是在用户提交的时候,通过SLog来标记这些页。然后后台线程会扫描并通过系统事务来释放之前标记的页。 192 | 193 | ## Redo Locking Optimization 194 | 195 | 之前SQL Server可以通过在Redo阶段获取锁来提高可用性。但是CTR的Undo非常快,所以我们没必要去跳过Undo阶段。但是仍然是有一些情况需要我们获取锁的,比如分布式事务的情况,我们不能简单的认为事务提交了或者失败了。 196 | 197 | 前面提到过我们可以通过SLog来获得锁。然而SLog是用来跟踪和获取粗粒度的锁的,比如table lock,或者metadata lock。同时还引入了一个新的机制用来获取细粒度的锁。 198 | 199 | 在Redo的最后,每个未提交的txn都会锁定他的txn id。当新的事务尝试读取行的时候,他会首先尝试获得对应txn id上的锁。如果这个txn仍然在恢复阶段,那么他就会被阻塞住,直到事务提交或者abort,后续的访问才允许继续进行。当恢复阶段的所有事务都被处理完毕后,后续新的访问就不需要再去获取txn id上的锁了,从而避免了性能的损失。这个机制可以让我们跟踪所有的行级锁,并且不需要额外的跟踪锁的操作。(很显然,提交前,写入日志的那些数据都是已经获得锁的数据,我们直接通过txn id来将锁集中到一起) 200 | 201 | ## Aggressive Log Truncation 202 | 203 | 这里就可以回答是怎么实现的Log Truncation。对于长事务来说,我们不依赖undo log来实现rollback,而是通过多版本来进行rollback。所以这些log就可以被删除掉。 204 | 205 | 我们可以截断log到下面三个值的最小值上: 206 | 1. 上一次成功检查点的begin lsn 207 | 2. 最老的脏页的lsn 208 | 3. 最老的活跃系统事务的起始lsn 209 | 210 | 条件1是保证我们需要redo log来重做操作 211 | 条件2也是保证我们可以redo脏页上的内容 212 | 条件3则是保证系统事务的恢复,因为系统事务是non-versioned,所以我们需要保存undo 信息 213 | 214 | 可以发现只要我们及时checkpoint,以及刷盘,那么log可以被截的很短 215 | 216 | 即便是我们截断了用户的短事务的log,也可以通过version信息来恢复。所以在abort的时候就可以检查当log完整,并且事务大小不超过阈值的时候,就可以用正常的rollback过程。否则就使用标记Abort的过程 217 | 218 | ## Background Cleanup 219 | 220 | CTR的后台线程负责: 221 | * 对abort version做Logical Revert 222 | * 将txn从ATM中移除 223 | * 清理在PVS中不需要的版本数据(提交的数据) 224 | 225 | ### Logical Revert and In-row Version Cleanup 226 | 227 | 为了防止后台线程不断的扫描数据页,SQL Server维护了一个Page Free Space(PFS)页,用来跟踪每个页的大小。在数据更新前,他会用一个额外的位来标识这个页有version。所以后台线程就可以只扫描这些可能含有需要清理的version的页 228 | 229 | 每过一段时间我们就会唤醒清理线程,然后他会: 230 | 1. 对当前ATM中的txn id截取一个快照 231 | 2. 扫描PFS页,对于每一个可能有version的页,如果里面含有abort version,则执行Logical Revert。然后移除掉所有SI不再需要的版本(end—ts小于watermark的版本) 232 | 3. 清理结束后移除步骤1的snapshot。snapshot的作用就是我们只会删除那些我们已经Logical Revert的事务,而非刚刚abort的事务。(为什么清理新的abort的事务不可以呢?应该只是修改一下计数器而已,感觉遇到abort的就清理也没什么问题。由于我们每次清理后在snapshot中的所有txn都会从ATM中移除,所以我猜测可能是移除的一个优化?比如锁定当前的ATM,创建一个新的,然后后续的查询可以查两个。当清理结束后,我们可以直接移除一个ATM) 233 | 234 | ### Off-row Version Cleanup 235 | 236 | off-row的里面只有早期的版本。所以这里的清理只是清理早期版本然后释放空间,即垃圾回收。 237 | 238 | off-row table是append only,所以我们可以跟踪额外的信息从而实现页粒度的回收 239 | 240 | CTR通过一个哈希表来跟踪每个页中的off-row version,我们只保存每个页的最大的txn id。当这个页的最大txn id已经小于watermark的时候,我们就可以直接回收这个页。这里注意watermark由活跃事务和abort事务共同组成,因为我们需要已提交的版本来做Logical Revert 241 | 242 | 后面就是一点小优化。 243 | 244 | 最后的一个思考,CTR实际上是利用了MVCC的特性,将版本信息和Log结合到了一起,从而可以让我们只通过修改txn state来实现批量提交的目的,本质上是批量的将最新的版本通过txn state来删除掉。所谓的SLog实际上就是不能够通过MVCC来处理的信息,我们单独拿出来通过日志来进行redo/undo。所以可以看成是MVCC附带Log信息,然后独立处理非MVCC的数据的一个机制。 -------------------------------------------------------------------------------- /db/constant_time_recovery_in_Azure_SQL_Database/p2143-antonopoulos.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ysj1173886760/paper_notes/df11876635eaa0da706671f51f6edd3b42e5391c/db/constant_time_recovery_in_Azure_SQL_Database/p2143-antonopoulos.pdf -------------------------------------------------------------------------------- /db/fast_serializable_mvcc/notes.md: -------------------------------------------------------------------------------- 1 | introduction里就是一些对MVCC的介绍。不过最后他提了一点我觉得比较关键 2 | 3 | Careful engineering, however, matters as the performance of version maintenance greatly affects transaction and query processing. 4 | 5 | Main Contribution: 6 | 1. 低开销的MVCC implementation 7 | 2. 基于Precision Locking的变体的一种串行化的方法 8 | 3. 一种synopses-based方法来达到高效的单版本扫描 9 | 4. 对于MVCC的实验以及tradeoff的演示 10 | 11 | # MVCC Implementation 12 | 13 | ![20220421134302](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220421134302.png) 14 | 15 | 为了保证高效的扫描,我们没有创建一个新的版本放在新的地方,而是update inplace,并且在undo buffer中维护了backward delta。即新的版本(yet uncommitted)和之前版本的变化 16 | 17 | 存储的形式是newest2oldest,版本链中的每一项存储的都是对应列中的变化。并且为了实现GC,链接是双向的 18 | 19 | ## Version Maintenance 20 | 21 | 如果所有的txn都都在当前的version的timestamp后开始,那么说明这个version已经过时了 22 | 23 | VersionVector存了指向在undo buffer中最近的版本的指针 24 | 25 | 每一个txn都有两个timestamp:transactionID和startTime-stamps 26 | 27 | 在提交的时候,他会收到第三个timestamp,commitTime-stamp,用来决定串行化的顺序 28 | 29 | 系统保证了startTime-stamp总是会比transactionID小。目前还不清楚他这么做是为了什么 30 | 31 | 事务会原地修改数据,并把之前的版本存储在他的undo buffer中。这个old version有两个作用: 32 | 1. rollback的时候用来恢复 33 | 2. 他会作为一个最新的已经提交的版本 34 | 35 | 新创建的版本会被赋与transactionID,并且只有创建他的事务才能访问他 36 | 37 | 在commit的时候,事务会得到commitTime-stamp。并且从现在开始他的undo logs会被标记为与他无关。 38 | 39 | 回看一下figure1貌似可以明白,每个版本上的标号代表了他的删除时间。看Sally 7这个版本,这个版本链上对应了3个timestamp,分别是ty,t5和t3。如果是小于t3的事务就会读到10这个值,大于或者等于t3,但是小于t5则会读到9。对应的,如果是大于等于ty才能inplace的读。所以我们需要保证transactionID大于startTimestamp,这样就可以防止其他的事务读到未提交的值 40 | 41 | 所以我们才需要在undo buffer里把这些版本都存起来,这样最后提交的时候只要把所有的undo buffer中的值改成commitTimestamp就可以了。 42 | 43 | ## Version Access 44 | 45 | 访问一个元组的时候,他会跟着版本链找到满足条件的第一个版本: 46 | v.pred == null or v.pred.TS = T or v.pred.TS < T.startTime 47 | 其中pred表示版本v的前一个版本(版本链的后一个,因为我们是N2O) 48 | 49 | 第一个条件对应的是没有前一个版本的时候,即以前的版本都已经被GC掉了或者没有创建过 50 | 51 | 第二个条件对应的就是我们自己能看到自己的修改 52 | 53 | 第三个条件则是对应了看到StartTime > CommitTime的最新版本。因为v.pred.TS其实是创建当前版本的CommitTimestamp(我怀疑他们会这样分开存么?这样每次都会多一个间接访存,不如维护startTimestamp和endTimestamp查找更快一些 54 | 55 | # Serializability Validation 56 | 57 | 我们特意的避免了写写冲突,让一个事务尝试更新一个未提交的数据的时候,他就会abort并且restart。所以第一个VersionVector指针总会指向一个含有commit version的undo buffer。如果一个txn尝试更新数据多次,那么就会创建多个版本,但是最终他们还是会指向一个已提交的版本(我好奇为什么不原地更新呢?为了子事务么?) 58 | 59 | 为了提高并发性,算法是基于乐观执行的(MVOCC),也就是说为了保证可串行化,我们需要在最后添加一个验证阶段(貌似正常的SSI都需要最后的验证,先提交者胜) 60 | 61 | 我们必须保证所有的读都能保证在提交的时候也是不会改变的。即处理RW和WR依赖(我想了一下貌似只需要处理RW,因为WR来说,一个读遇到了已有的写,他只要读提交的版本就OK,因为这个写的CommitTimestamp一定在读的ReadTimestamp之后,所以我们无论如何也读不到他,这里和percolator是不同的,因为percolator可能出现延迟的更新。不对这里也可以出现,还是需要处理的。。) 62 | 63 | ![20220421185711](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220421185711.png) 64 | 65 | figure2列出了我们需要检测的几个现象。也就是要检测发生在T的生命期之间的改变。 66 | 67 | 这里使用了Precision Locking来进行检查,他消除了谓词锁所需要可满足性测试的问题。(这个可能还需要之后继续研究研究)。我们的检测阶段会检测正在提交事务中的对应的谓词读与最近提交事务的写入的冲突。当最近提交事务的写入与谓词读产生交集的时候,测试就会失败。(所以前面提到了是用undo buffer验证,因为undo buffer里存了事务所有的写入) 68 | 69 | ![20220421191553](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220421191553.png) 70 | 71 | 这个是验证阶段的演示 72 | 73 | 这些谓词构成了空间中的区域(读所涉及到的区域),然后我们把undo buffer里的东西往里丢,如果出现了交集说明出现了问题。对于实现来说,我们只需要把undo buffer里的东西都应用一遍谓词即可(这个思路不错,不是在索引等地方记录读取的信息,从而防止其他人写。而是在提交的时候把写集验证一下有没有影响谓词) 74 | 75 | 我们维护了最近提交的事务,并且含有指向undo buffer的指针。对于在T的lifetime期间提交的事务来说,我们取出每一个新创建的版本,并且检查他是否满足事务T的谓词。(这里检查要仔细考虑,因为我们要满足的是CO,也就是CommitTimestamp代表了串行化序列中的顺序。并且貌似不只是要考虑一个版本,因为有可能之前可以读到但是新版本不满足谓词,或者新版本满足谓词但是之前不满足) 76 | 77 | 论文中提到了,对于删除操作。我们要检查这个项是不是在T的读集中(也就是读到了被删除的东西),而对于修改操作,我们需要检查before-image和after-image,只有两个都不相交的时候才能满足条件 78 | 79 | (但是这还是解释不了为什么不原地修改,因为我们检查的应该都是commited version才对,否则不可能被读取到) 80 | 81 | 验证完成以后,首先把数据写入到redo-log中,然后修改undo buffer中的timestamp(一个问题,这里需要原子的修改么?我觉得需要)。论文中还提到了这个验证过程是可以并行的,那么就可能出现一个问题,如果正在验证的事务会影响另一个事务要怎么办呢?比如事务10刚开始验证,事务11也开始了,但是根据串行顺序来说,事务11必须知道事务10是否验证完毕了) 82 | 83 | ### Predicae Logging 84 | 85 | 谓词并不只是有where语句等,比如join连接等操作都会影响读集 86 | 87 | 对于table的扫描,我们记录下他对于某些属性的限制 88 | 89 | 而对于索引访问的时候对应的谓词也是类似的,比如记录下谓词的范围 90 | 91 | 索引连接则是不同的,我们会记录下所有从索引中读到的数据作为谓词,并把这些数据合并成为Ranges记录下来。其他类型的join则是作为全表扫描来记录 92 | 93 | 这里可能说的不太清楚,我仔细解释一下。比如说我们目前有一个index nested loop join,即首先按顺序扫描A表,对于A表中的每个元组,我们通过索引查找B表中有没有对应的值。这时候我们就可以记录下来所有的成功匹配的B表的值,合并成为Range作为谓词使用。那么后续的插入操作如果满足了这个谓词,就说明他会对这次index nested loop join有贡献。 94 | 95 | 其实思路还是比较清晰的,因为我们访问数据的方法其实就两种,一种是显式的谓词,这时候我们直接记录下这些谓词就行,比如应用在基表或者索引上的谓词。还有一种就是隐式的谓词,比如上面的index nested loop join,这里的谓词其实就是是否满足连接条件,而连接条件就是A表对应属性的范围,我们log下来即可。其实这里应该可以推广到任意连接中,只不过维护成本较大,比如哈希连接,就需要我们保存所有的属性,所以选择扩大粒度,直接相当于全表扫描就行 96 | 97 | ### Implementation Details 98 | 99 | 通过64位的摘要来快速的进行谓词判定,变长的数据会被hash成64位摘要 100 | 101 | 这里的摘要是针对每个属性的,而非元组,所以他会比一些“locking the record”的方法更好,因为record-level可能会导致false positive 102 | 103 | 我们还会额外的log那些属性被访问了,从而处理没有谓词的访问,以及优化检查粒度 104 | 105 | 所以在validation的时候,就可以跳过那些修改了没有访问过的属性的版本(比如我这个版本虽然满足了你的谓词,但是我修改的地方不是你读取的) 106 | 107 | ![20220421212236](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220421212236.png) 108 | 109 | 每一个关系表都会构建这样的一个predicate tree。不同的路径是OR连接。一个路径下由AND连接 110 | 111 | 所以每一个版本都会去检查他们是否满足了PT的某一个分支 112 | 113 | ## Garbage Collection 114 | 115 | 每次提交之后,我们都会在最近提交的事务里确定最老的可见事务ID是多少,那么所有在他之前的事务就都可以从最近提交事务列表里移除。指向他们的undo buffer的指针也会被移除。undo buffer会被打上tombstone的标记。但是我们不能立刻释放undo buffer的内存,因为有可能有其他的事务有对他的引用(我们可以确定他看不到undo buffer里的内容,但是他需要这里面的信息来终止version chain的遍历)。 116 | 117 | 但是如果已经标记的tombstone就可有直接终止了(那可能是先去检查一下tombstone,然后再访问) 118 | 119 | (个人猜想:可以通过维护undo buffer的refcnt来让最后一个人释放掉他,有点类似in-memory inode) 120 | 121 | ## Handling of Index Structures 122 | 123 | 当我们修改的元组涉及到索引的时候,对于关系表来说他会删除原本的版本并插入新的版本。而对于索引来说,他会存储原始的版本和新的版本。这样我们的索引中就存储了任何事务都可以看到的所有元组。 124 | 125 | 对于检查一致性来说,比如主键约束。论文中的意思应该是对于uncommited的版本也会主动中止。因为我们在index中存储了所有的版本,所以当一个新的主键插入的时候他会检查索引,如果索引中有这个主键了,说明这个快照里就有,或者有一个未提交的主键,我们就会主动abort。理由是大多数的事务都会提交,所以事务将uncommited数据视为已经存在的版本。 126 | 127 | 但是我感觉如果只是检查约束的话,我们可以为最后的检查阶段添加额外的谓词来实现。(可能是因为索引检查更快?不太清楚他们这里的tradeoff) 128 | 129 | ## Efficient Scanning 130 | 131 | 很多AP和TP的workload都依赖clock-rate scan performance。所以每次都对数据做可见性测试会导致性能下降 132 | 133 | 通过synopses of versioned record positions来加速扫描 134 | 135 | 在上面的figure1中,VersionedPositions就是这个摘要。他维护了具有版本的记录的位置的一个范围。 136 | 137 | 由于我们会不断的进行GC,所以大多数的元组都没有其他的版本。比如figure1中的前5个元组,他们是没有其他版本的,所以我们可以不需要去进行额外的version check,以达到最大的扫描速度 138 | 139 | 而对于已经修改的版本我们就需要根据version vector进行重建 140 | 141 | 这里有一个奇怪的点:Again, a range for modified records is determined in advance by scanning the VersionVector for set version pointers to avoid repeated testing whether a record is versioned. 142 | 143 | 我好奇这个范围不是一定被VersionedPositions确定了么?为什么还要再扫描VersionVector 144 | 145 | 他提到了实践中两个versioned object的跨越是很大的。 146 | 147 | 并且提前确定好versioned object的范围可以让我们在热点区域少查询VersionedPositions。(这块和上面的问题是一样的) 148 | 149 | 这里是因为文章中提到了,他们维护这个摘要的方法,对于插入的修改来说,我们只要更新这个范围就可以,而对于删除来说,他们会在下次扫描的时候修正摘要。所以对于我们维护的这个摘要可能并不是准确的摘要,所以他重新扫描version vector来得到更准确的值。并且他是列存储的,所以这个代价并不高,比起每次重新找version vector要高效 150 | 151 | ## Synchronization of DataStructures 152 | 153 | 简单提了一下怎么做的同步 154 | 155 | 在一个task中,获得MVCC数据上的latch(一个transaction通常由多个task组成,应该就是对应的sql statement。为了防止race,就在操作的时候加上短期锁) 156 | 157 | 提交的时候会进入一个临界区,首先拿到commitTimestamp,然后做validation,然后写redo log。最后的更新undo buffer中的timestamp可以通过原子操作在临界区外做。(所以这里的意思是还是只能一个一个的提交么?)他这个前后貌似不太一致,他前面说validation是可以并发的,但是后面说这个是在临界区的 158 | 159 | # Theory 160 | 161 | 这里说了txn是并发的运行,但是提交是串行的 162 | 163 | ![20220422140247](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220422140247.png) 164 | 165 | schedule的实例。其中图C值得仔细看一下。因为这里演示的是使用谓词的检查。我们用P和Q谓词读取了数据,在提交的时候就要验证最近的写入是否满足这两个谓词 166 | 167 | 后面是一个证明,但是感觉这个算法还比较直观,就不贴了。 -------------------------------------------------------------------------------- /db/fast_serializable_mvcc/p677-neumann.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ysj1173886760/paper_notes/df11876635eaa0da706671f51f6edd3b42e5391c/db/fast_serializable_mvcc/p677-neumann.pdf -------------------------------------------------------------------------------- /db/high-performance-transactions-in-deuteronomy/high-performance-transactions-in-Deuteronomy.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ysj1173886760/paper_notes/df11876635eaa0da706671f51f6edd3b42e5391c/db/high-performance-transactions-in-deuteronomy/high-performance-transactions-in-Deuteronomy.pdf -------------------------------------------------------------------------------- /db/htap-tutorial/htap database.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ysj1173886760/paper_notes/df11876635eaa0da706671f51f6edd3b42e5391c/db/htap-tutorial/htap database.pdf -------------------------------------------------------------------------------- /db/optimal_column_layout_for_hybrid_workloads/notes.md: -------------------------------------------------------------------------------- 1 | # Optimal Column Layout for Hybrid Workloads 2 | 3 | # Abstract 4 | 5 | 现代的analytical system是基于列存储。然后通过delta store来进行插入和更新 6 | 7 | 我们通过确定分区的数量,他们的大小和范围,以及缓冲区大小以及他们是如何分配的来组织数据的分布。 8 | 9 | 给出workload knowledge以及performance requirements,给出一个优化的物理布局 10 | 11 | # Introduction 12 | 13 | 目前的系统对于数据的布局都是固定的,这意味着他们会被局限在某个地方,而不能根据workload来达到比较好的效果。 14 | 15 | 而我们的insight则是这些设计决策不能被限制在一个先前的决定中,即在设计系统的时候就固定了这些决策。我们可以学习这些决策并且调整他们从而支持HTAP的workload 16 | 17 | 在这个paper中,我们关注三个比较关键的决策: 18 | 1. 数据的物理布局 19 | 2. 列存储是否是密集的 20 | 3. 怎么为更新操作分配buffer(delta store还是ghost values) 21 | 22 | ![20220428092752](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220428092752.png) 23 | 24 | Channenge: 25 | 1. Fast Layout Discovery。可以看到我们有很大的提升,但是这不是没有代价的。找到这个优化的布局是一个比较昂贵的操作。最坏情况下有指数级别的数据布局需要我们去枚举以及评估。我们将这个问题转化成binary optimization problem,并且用现成的solver来求解。并且利用数据是存储在column chunk的特性,我们为每个chunk单独的进行优化,从而降低复杂度 26 | 2. Workload Tailoring。我们需要有一个比较有代表性的workload的sample。然后分析数据的访问频率以及访问模式。 27 | 3. Robustness。当去优化layout的时候我们可能有过拟合的风险 28 | 29 | 我们针对的是analytical application,有着比较稳定的workload。我们的工具会分析workload,并离线的准备好data layout。类似现代系统的index advisor 30 | 31 | 他说支持service-level agreements,这个的意思应该是说保证给用户提供什么样的performance(应该和前面对应的performance requirement相关) 32 | 33 | # Column Layout Design Space 34 | 35 | Casper(就是本文的系统)有很大的设计空间: 36 | 1. 使用range partitioning作为额外的模式(现有的是根据key排序,或者根据插入顺序排列) 37 | 2. 支持原地的,非原地的,以及混合的更新 38 | 3. 支持无缓冲,全局缓冲,以及per-partition的缓冲 39 | 40 | ![20220428102234](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220428102234.png) 41 | 42 | 这里提到存储的scheme和buffer是正交的。还提到了水平分区和垂直分区,我感觉应该就是切分行以及切分列。并且他还支持tiles(不清楚是什么)和projection。对于projection来说,意思应该是读负载可以被projection分摊开,并且不同的projection还可以用不同的layout来得到更好的性能 43 | 44 | 分区数量也是一个比较关键的因素。 45 | 46 | 可以看这个图 47 | 48 | ![20220428105209](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220428105209.png) 49 | 50 | 比如说对于一个column chunk,里面有Mc个元素。我们有k个不相交的分区。那么读的开销大概就是Mc/k,而插入(删除)的开销则平均是k/2(因为我们可能需要跨分区移动一些数据) 51 | 52 | 所以对于读负载,他喜欢更大的分区数量(因为只要读一个分区),而写负载则更喜欢小的分区数量(因为不需要跨越很多分区去修改) 53 | 54 | 并且对于只读的负载,如果不同的区域有着不同的访问模式,那么等宽的分区可能就会导致不必要的读。对于不常访问的地方,粗粒度的分区就足够了 55 | 56 | 所以理想情况下,局部优化的分区策略可以让一些有skewed access的workload达到理想的性能(对比则是均衡access的workload) 57 | 58 | Ghost Values。当更新数据的时候,如果我们放松对于整个column都有序的这个要求,那么就可以减少数据的移动。对于一个删除操作来说,他只需要找到对应的partition,然后把对应的数据标记为删除即可,从而引入了empty-slot。为了更好的利用,我们可以把empty slot移动到partition的最后面,从而直接处理到来的插入请求。 59 | 60 | 这些empty slot就叫做ghost value,需要额外的维护,但是可以使更新操作更容易。Ghost value减少了更新的代价,但是带来的是额外的内存使用。trading space amplification for update performance 61 | 62 | ![20220428140304](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220428140304.png) 63 | 64 | 这个是通过增加buffer space来减少写的开销,代价是读变慢(如果每次都是把empty slot放到最后,为什么读会变慢呢?因为缓存吗?) 65 | 66 | 我们通过考虑数据区域的访问模式分布来达成细粒度的决策(it takes into account the access pattern distribution with respect to the data domain)。casper收集每一种操作访问分布的直方图,并传给我们的cost model 67 | 68 | # Accessing Partitioned Columns 69 | 70 | 下面,假设我们有k个可变大小的分区,以及一个轻量级的k叉树作为索引 71 | 72 | Point Queries。点查询就是在索引上先确定我们要找的分区,然后分区内部顺序扫描一下。一旦找到我们就可以返回这个值,或者元素的位置(那要是后面变了怎么办?),作为后面算子的输入 73 | 74 | Range Queries。首先通过索引找到区间的起始的分区以及结束的分区,然后通过范围过滤一下。中间的分区则可以直接复制给后面的算子 75 | 76 | 一个演示 77 | ![20220428143246](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220428143246.png) 78 | 79 | Inserts。通过ripple-insert algorithm,来达成O(k)的插入一个元素到分区的操作。 80 | 81 | ![20220428144155](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220428144155.png) 82 | 83 | ripple-insert把一个empty slot从column的最后移动到对应的partition中。我们插入到这个empty slot中,并且修正index中刚才移动的partition的边界。 84 | 85 | Deletes。对于删除来说,首先就是通过index来确定对应的partition,然后删除对应的元素,并得到一个empty slot。我们根据上面的步骤,再一步一步把empty slot移动到column的末尾。当然也要修正index 86 | 87 | ![20220428144754](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220428144754.png) 88 | 89 | update通过delete加insert来完成 90 | 91 | # Modeling Column Layouts 92 | 93 | 总共是5个操作,不同的决策对于不同的操作影响都是不一样的。我们首先考虑没有ghost value的情况 94 | 95 | ![20220428151838](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220428151838.png) 96 | 97 | 要优化的问题考虑了dataset以及workload。我们需要考虑数据的分布,然后在此之上覆盖访问模式,从而得到effective access distribution 98 | 99 | ## Representing a Partitioning Scheme 100 | 101 | 通过用位向量来标记分区的边界,从而表示一个分区的模式 102 | 103 | 一个列由Nb个大小相同的块组成。块的大小是和column chunk的大小一起决定的。我们通过Nb个布尔变量表示分区模式。当一个块的终点作为一个分区的终点的时候,我们把对应的变量设为1 104 | 105 | ![20220428152757](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220428152757.png) 106 | 107 | 一个示例,所以分区的大小要和block大小对齐? 108 | 109 | block的大小是cache line大小的整数倍,最小就是一个cache line,从而可以影响我们分区的粒度。 110 | 111 | ## The Frequency Model 112 | 113 | 基于上面的Partition Scheme上来介绍访问模式的表示 114 | 115 | 访问的数据会以logical block的形式整理起来,每一个block中的每一个操作的访问模式都被记录下来。logical block的大小是可变的 116 | 117 | 我们记录那些block都被那些operation使用了,从而构成若干个直方图,我们称这个直方图为Frequency Model。因为他存储的是每一个区域数据访问的频率 118 | 119 | FM利用了10个直方图: 120 | ![20220428155549](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220428155549.png) 121 | 122 | 每一个block都有这样的10个counter。最后的直方图的每一块都对应了一个block。 123 | 124 | 在sample workload上生成直方图的时候,我们不会真正的去计算结果,而只是去捕捉访问模式(更新counter) 125 | 126 | ![20220428160158](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220428160158.png) 127 | 128 | 这里的更新,当从3到16的时候,empty slot是向前,所以更新的是udf和utf。否则就是udb和utb 129 | 130 | 虽然示例是一个列,但是我们可以把它放入多个列(但是只能根据一个排序)。因为FM不会在意他内部具体存储的数据(也就是不在乎一个数据项内存的都是什么,只关注block的访问模式) 131 | 132 | ![20220428184617](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220428184617.png) 133 | 134 | 我们也可以从现有的access pattern中学习FM。我个人感觉就是有了访问每个block的分布了,然后把它转换成直方图而已 135 | 136 | ## Cost Functions 137 | 138 | 假设数据集中有M个值,block size为B,则我们最多可以有M/B=N个partition 139 | 140 | 假设访问block有四种IO方式,随机读RR,随机写RW,顺序读SR,顺序写SW 141 | 142 | ### Range Query 143 | 144 | 他这里给了个例子我觉得还挺有意思的 145 | 146 | ![20220428191003](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220428191003.png) 147 | 148 | 这个图中,每次我们的RQ尝试从第三个block开始读的时候,如果P1没有设置(在这里没有分区),那么我们就需要额外读第二个block,如果P0也没有设置,那我们就需要额外读第一个block。而如果P1设置的话,我们就不需要进行额外的读,最多读一个block就可以 149 | 150 | 那么一个block作为开始块进行访问的代价就是 151 | ![20220428191258](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220428191258.png) 152 | 153 | 其中第一项表示的是第一次随机读到这个块。而第二项则表示的是由于没有分区导致的额外的block reading,不过这些是顺序读 154 | 155 | 第二项是累乘的,比如对于第5个block,他前面的就是(1-p4) + (1-p4) * (1-p3) + (1-p4) * (1-p3) * (1-p2)这样的。因为只要后面的分区了,前面就不会有影响 156 | 157 | (从这里看大概可以明白他的思路,以block为粒度来分区,然后根据每个block分区或者不分区来计算操作的代价。变量只有每个block是否分区,就变成了01规划,最后我们可以得到一个好的分区方案) 158 | 159 | 对于block作为结束块的代价是类似的 160 | ![20220428192253](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220428192253.png) 161 | 162 | 也就是说我们只需要往后找就可以,但是这里最后一块也是顺序读 163 | 164 | 对于中间块的访问,则是顺序读 165 | ![20220428192500](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220428192500.png) 166 | 167 | 所以总和起来,所有的RQ的代价就是 168 | ![20220428192638](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220428192638.png) 169 | 170 | ### Point Query 171 | 172 | ![20220428192807](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220428192807.png) 173 | 174 | 有了前面的例子这里就会容易很多。fwd_read就是向前,也就是下标增大的方向额外读取的block,bck_read则是向下标减少的方向额外读取的block 175 | 176 | 所以这里代表的是找到partition是一个RR,然后读完是若干个SR 177 | 178 | ### Inserts 179 | 180 | ![20220428193224](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220428193224.png) 181 | 182 | 因为我们需要从最后面引入一个empty slot,所以我们的后面每有一个partition都会导致一次额外的交换,即读第一个元素,写入到最后一个元素的后一个位置,这里的加一指的是最后一个分区的读写(他的最后一块应该没有标志分区) 183 | 184 | ### Deletes 185 | 186 | ![20220428194438](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220428194438.png) 187 | 188 | delete和insert十分类似。不同的点就是我们需要一次点查询来得到要删除的数据 189 | 190 | 这里和insert不同的地方就是我们少一次RR,因为删除的时候直接覆盖就行,不需要知道之前的值的内容。(其实代价算在了点查询中) 191 | 192 | ![20220428194809](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220428194809.png) 193 | 194 | ### Updates 195 | 196 | 很多的系统中,update是delete加insert的结合。一个更有效的措施是我们可以直接把ripple从source移动到dst,而不需要放到最后 197 | 198 | 对于第一个partition来说,也就是删除的那个partition,我们需要访问两个block(最后一个和删除的那个) 199 | 200 | 假设现在的一个update操作会把一个在block i中的值删除,然后插入到block j中。首先需要做一次点查询。然后一次删除,把新出现的empty hole移动到分区的最后一个。代价则是 201 | ![20220428195452](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220428195452.png) 202 | 即先删除,然后读最后一个元素,然后写入到hole中(为什么不盲写呢?) 203 | 然后我们要考虑把empty hole移动到block j中。他们之间的分区数是Pi + ... Pj-1,也就是trail_parts(i) - trail_parts(j) 204 | 205 | ![20220428200047](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220428200047.png) 206 | 207 | 这里就是从左到右的代价。开始点需要一次点查询,然后移动hole。然后我们需要移动若干个分区到j中,最后一次插入(其实这里多统计了一个读,因为最后的分区就是j所在的分区,他其实只需要一次写入就可以,不需要读) 208 | 209 | 对于反过来的方向,操作则是一样的 210 | ![20220428200516](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220428200516.png) 211 | 212 | ![20220428201224](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220428201224.png) 213 | 214 | 最后把他们拆开看,最后的开销取决于FrequencyModel和PartitionStrategy,分别是(fixed_term, bck_term, fwd_term, parts_term)以及(bck_read(i), fwd_read(i), trail_parts(i))。也就是说我们的cost function是由access pattern和分区策略共同决定的(而access pattern由workload和data distribution共同决定) 215 | 216 | ## Cost Model Verification 217 | 218 | 每次部署的时候,我们需要确定有关读写的参数。我们通过一个micro-benchmarking来确定这些参数。并且通过插入和单点读操作就可以足够确定这些参数了。因为他们包含着模型中主要的两个代价函数:(1)后续的分区数,(2)每个分区的大小 219 | 220 | ![20220428202236](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220428202236.png) 221 | 222 | 在micro-benchmark中有若干个partition,并且分区的大小随指数增长(我好奇为什么insert在后面的partition开销这么大呢?我们应该只需要一次写才对) 223 | 224 | 这里显示了实际的开销和模型给出的结果的图,可以看出预估的效果 225 | 226 | 具体的操作应该就是线性回归,让model去拟合真实值 227 | 228 | 他原文中也说了cost和trailing partition有线性关系,但是这里为什么ID越大延迟越高呢?可能是ID越大越靠前? 229 | 230 | ## Considering Ghost Values 231 | 232 | 最后需要考虑Ghost value了 233 | 234 | ghost value就是没有被移动到最后面的empty slot,他是对于memory utilization和data movement的trade off 235 | 236 | 对于每次的插入和更新,ghost value可以避免使用ripple来获得empty slot 237 | 238 | ![20220428204011](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220428204011.png) 239 | 240 | ghost value有一个总体的数量(意思是一个chunk共用一堆ghost value) 241 | 242 | 这里的dm_part就表示第i个block中的insert和update引起的ripple(不算delete的目的可能是因为当用了ghost value后,delete就不会引起ripple了?) 243 | 244 | 然后我们根据权重来分配ghost value的slot。具体的实现的话,应该就是只有分区内部才考虑ghost value,其他分区应该不会向这个分区要ghost value(否则决策点就太多了),相当于给这个分区一些弹性,让他可以处理更新的操作 245 | 246 | # Optimal Column Layout 247 | 248 | 他这里说了Pn-1是固定为1的,那可能前面有些地方的推断是错的 249 | 250 | 和上面说的一样,最后就转化成了一个优化问题,我们需要找到最优的分区位置 251 | 252 | ![20220429093033](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220429093033.png) 253 | 254 | 由于之前的式子中,有P之间的乘积,无法通过线性的求解器来求解,因为引入了高次项。所以这里用新的项替换了之前的连乘,并通过加入约束来保证他和之前的项表达的是一致的。 255 | 256 | 而这里y就表示的是连乘,他的取值范围仍然是0和1,含义大概是从j到i - 1有没有分区。 257 | 258 | 他还给出了上面提到的SLA 259 | ![20220429093629](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220429093629.png) 260 | 保证更新不超过一定的延迟 261 | 262 | ![20220429093652](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220429093652.png) 263 | 保证读不超过一定的延迟。MPS表示的是最大的分区大小。也就是保证每过MPS个block,最小有一个1 264 | -------------------------------------------------------------------------------- /db/optimal_column_layout_for_hybrid_workloads/p2393-athanassoulis.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ysj1173886760/paper_notes/df11876635eaa0da706671f51f6edd3b42e5391c/db/optimal_column_layout_for_hybrid_workloads/p2393-athanassoulis.pdf -------------------------------------------------------------------------------- /db/scalable_garbage_collection_for_in_memory_mvcc_systems/notes.md: -------------------------------------------------------------------------------- 1 | # Abstract 2 | 3 | 他首先提出HTAP workload中,GC通常会成为bottleneck 4 | 5 | 现有的GC技术过于粗粒度。并且不能很好的处理sudden spike的workload 6 | 7 | # Introduction 8 | 9 | MVCC的一个问题就是如果workload中有很多的long-running transactions,那么活跃的版本就会增加的非常快,并且我们不能删除掉这些版本因为他们可能要被活跃事务使用 10 | 11 | ![20220423093805](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220423093805.png) 12 | 13 | 所以这些long-running transaction就会导致一个恶性循环 14 | 15 | 因为他们持续的越久,那么活跃的版本就越多,导致事务读的速度会更慢,从而导致更多的long-running transaction 16 | 17 | ![20220423094142](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220423094142.png) 18 | 19 | 一个比较直观的图,显示了version的数量对于performance的影响 20 | 21 | # Versioning In MVCC 22 | 23 | 还是MVOCC可见性核心的一点,我们只能看到commitTimestamp小于我们的事务开始时间的元组 24 | 25 | ![20220423101512](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220423101512.png) 26 | 27 | 这里给出了一个示例,中间有很多无用的元组。其实从这里我们也可以有一定的感觉,他所谓的细粒度的GC就是不去维护high water mark,而是具体的可见性。 28 | 29 | ## Identifying Obsolete Versions 30 | 31 | 可见的版本与并发运行的事务有关。即一个版本的生命周期取决于目前的活跃事务。 32 | 33 | 这里提到了,传统的垃圾回收器只会跟踪活跃事务中最老的那个timestamp。所以会得到一个比较粗粒度的估计,导致很多其实无用的版本不能被删除 34 | 35 | ## Practical Impacts of GC 36 | 37 | 这里其实是一个对于figure1的解释 38 | 39 | 当出现长事务的时候,version record就会堆积。当reader结束的时候,writer才能开始清理这些元组,并且在GC的时候新的事务不能进行写操作。(这里的意思应该是后台的GC线程需要锁住版本链才能就检查是否需要进行GC,版本链越长进入临界区的时间也就越久,导致其他的写入会被阻塞) 40 | 41 | 并且随着版本的增多,读事务会越来越慢,导致更多的长事务 42 | 43 | 总结起来就是传统的垃圾回收器有三个缺点: 44 | 1. scalability due to global synchronization 45 | 2. vulnerability to long-living transactions 46 | 3. inaccuracy in garbage identification 47 | 48 | (我感觉23其实是一个问题,细粒度的去检查就可以了,1他所谓的global synchronizatio我不太清楚,感觉可能是lock-free的GC方法,从而防止GC线程阻塞worker线程) 49 | 50 | # Garbage collection survey 51 | 52 | 通过在事务处理的过程中进行version prune,而非使用后台线程 53 | 54 | 他的系统还是基于HyPer的,所以应该是在txn提交的时候,我们应该有一段时间需要latch住undo buffer并修改timestamp,这个时候就可以进行GC 55 | 56 | ![20220423135151](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220423135151.png) 57 | 58 | 一个overview 59 | 60 | ## Tracking Level 61 | 62 | 最细粒度的方法是tuple-level,GC线程会扫描每一个元组并识别他们是不是过期的元组 63 | 64 | 我们可以每过一段时间开启这个线程(bg),也可以在运行txn的时候去检查这些过时的元组(fg) 65 | 66 | 系统也可有以transaction为粒度去清理,因为一个txn中创建的所有版本都共享着同一个commit timestamp,所以如果他们不可见的时候,应该是同时不可见的。所以我们可以一次性识别并清除同一个txn中的版本(他这里说清理独立的版本可能会更慢一些,这里我不太明白,因为一个版本如果不可见了,那么和他同一个事务的版本也就不可见了。他的意思可能是一次要清除一个事务的版本会慢一些?但是这个可以通过事务级别的undo buffer来保证locality) 67 | 68 | Epoch-based的方法是让若干个事务成为一个Epoch,这样我们就可以一个Epoch一个Epoch的清理 69 | 70 | 最粗粒度的方法就是table-level,当整个table不再使用的时候我们会清理他,或者说是一次锁住一整个表,然后清理,然后释放。 71 | 72 | 个人想法:所以粒度其实就是一次GC多少,tuple-level就是需要我们latch住tuple,然后回收。transaction level就是latch住一个txn的undo buffer,然后回收。这里具体的tracking level应该和我们怎么存储版本,怎么识别过时record是有关联的 73 | 74 | ## Frequency and Precision 75 | 76 | 多久GC一次,以及清理的多干净。 77 | 78 | 比如Epoch的方法就是当到达阈值的时候,我们会前进一个Epoch,并把前一个epoch的version清除掉 79 | 80 | 还有就是很常见的后台线程。每过一段时间就开启一次去进行GC 81 | 82 | 还有batch-level的,但是这个感觉和Epoch的有点类似,每个batch结束都会确定这个batch的txn已经结束了,我们就可以安全的回收那些老版本 83 | 84 | 和上面说的一样,thoroughness和我们识别的方法是有很大关联的。对于timestamp-based的方法,他只会移除那些timestamp比high watermark还小的版本。所以会被长事务所影响。对于interval-based的方法来说,他们会做出更仔细的识别,并只保留那些必须的版本 85 | 86 | ## Version Storage 87 | 88 | 很多的系统会将版本保存在一个全局的结构中,这可以让我们单独的回收每一个版本。但是缺点就是所有的版本都需要在这个global storage中去检查(我感觉他想说的意思是临界区太大,并且回收需要一个一个的扫描) 89 | 90 | HyPer和Steam会把这些版本存储在与txn相关的buffer中,叫Undo Log。当txn落后于high watermark的时候,所有的版本都可以被直接回收。并且,单个版本也可以被独立的回收,我们只需要断开版本链即可。并且使用Undo Log存储是一个比较直观的事情,因为我们无论如何也需要这个信息做回滚,现在只是把它暴露给全局,可以让其他的事务去访问before-image(他这里提到了说我们断开版本链,但是这块内存是属于undo log的,还是需要等待整体的回收,所以内存的回收会被delay,可能就是他上面说的清楚独立版本会慢一点的原因) 91 | 92 | ## Identification 93 | 94 | 这里说的就是检查的方法。 95 | 96 | 比如维护一个global txn map,可以让我们在常数时间内找到high watermark。虽然这个方法简化了version identification(我们只需要比较timestamp和high watermark),但是清理的不干净。 97 | 98 | Steam和HANA的方法则是使用更细粒度的检测方法,这时候我们需要跟踪所有的活跃事务,并且对每一条version chain都做细粒度的检查 99 | 100 | 更加粗粒度的方法则是用Epoch来确定版本是否过期,相当于是一个对high watermark的一个近似。然而epoch方法会让我们的memory management更加轻松。Epoch guard会等待所有的线程离开这个epoch后,回收这个epoch使用的内存 101 | 102 | 和检查方法无关,检查可以在后台线程进行,也可以在前台进行 103 | 104 | 我在想识别和实际的回收是不是不是同步的呢?在disk-oriented的system中,这个是分开的,因为我们需要重新整理碎片,比如postgres。在主存数据库中这里应该可以直接释放,因为不会出现碎片(碎片被runtime管理了) 105 | 106 | ## Removal 107 | 108 | 看起来Removal是实际清理的过程 109 | 110 | HANA中,GC是通过后台线程进行的。Hekaton中则是clean on-the-fly 111 | 112 | Epoch-based的系统貌似是在Epoch内进行重用,比如一个版本提交了,那么他的老版本就可以放到free-list中。(这里貌似说的不是很清楚,之后再看看) 113 | 114 | HyPer和Steam则是在txn之间进行回收。当存在过期版本的时候,在txn提交的时候worker thread就会回收他们 115 | 116 | 感觉刚才说的这个里面有很多都是类似的概念 117 | 118 | tracking level是一次回收多少版本 119 | frequency是多久进行检查和回收 120 | precision是指每次回收是不是干净 121 | version storage指怎么存储版本数据 122 | Identification指通过什么信息来识别过期版本 123 | Removal指什么时候回收,和frequency的区别是frequency说的是时间上的。比如在commit的时候,每过一分钟等。而Removal说的是具体清理version的过程,比如是在后台清理,还是在运行txn的时候清理等(分散在txn之间还是在后台) 124 | 125 | 举个例子就是peleton,每个epoch他会识别出所有的过期的版本,然后后台线程会执行回收 126 | 127 | 而Steam,则是在创建的时候就去检查是否可以回收,同时在commit的时候也会检查是否可以回收Undo buffer(他的细粒度检索我猜测应该是在遍历版本链的时候去检查,比如说打标记或者一边遍历一边剪枝。然后在最后提交的时候把那些过期的版本回收) 128 | 129 | # Steam Garbage Collection 130 | 131 | ## Basic Design 132 | 133 | ![20220423174650](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220423174650.png) 134 | 135 | 用两个链表来追踪active和committed txns(HANA和Hekaton用的是reference-counted list和map) 136 | 137 | 两个链表都是有序的,我们可以很快找到最老的active txn的timestamp 138 | 139 | 并且当active txn的timestamp大于commit txn的时候,我们就可以回收commit txn的Undo buffer 140 | 141 | ## Scalable Synchronization 142 | 143 | 前面描述的方法虽然可以在常数时间内进行GC,但是他需要全局的txn list。从而导致无法scale 144 | 145 | Steam没有像Hekaton那样使用了latch-free txn map,而是用了一种不需要同步的算法 146 | 147 | 这里还有一个点: For GC, we exploit the domain-specific fact that the correctness is not affected by keeping versions slightly longer than necessary-the versions can still be reclaimed in the "next round" 148 | 149 | Steam中每一个线程都维护了目前txn的不相交子集。每个thread只会把他的thread-local minimum共享出来(通过64bit atomic integer)。其他的线程就可以读这个值,从而获取全局最小值 150 | 151 | local minimum对应的是第一个活跃事务的时间戳。如果没有的话就设为一个极大值 152 | 153 | ![20220423183531](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220423183531.png) 154 | 155 | 要确定全局最小值,我们需要扫描每个线程的局部最小(但是我想这样不还是会受到多核缓存一致性的问题么?和latch-free没啥区别) 156 | 157 | ## Eager Pruning of Obsolete Versions 158 | 159 | 前面的方案只是避免了全局同步,但是没有处理long running txn的问题 160 | 161 | 这里提出来Eager Pruning,可以移除掉所有的不需要的版本 162 | 163 | 每个线程周期性的取出目前活跃事务的timestamp,并放到一个sorted list中 164 | 165 | 每当一个线程遇到了版本链的时候,他就会根据下面这个算法去清理掉所有过期的版本 166 | 167 | ![20220423185123](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220423185123.png) 168 | 169 | 其实就是针对active txn去合并版本链。 170 | 171 | 这里的意思就是找到第一个版本,然后对于所有的活跃事务,找到他看的版本。然后对于中间的版本(在figure1中就是中间无用的那些版本),我们会把它合并起来。但是注意这里只压缩了不属于attrs(visible)的版本 172 | 173 | ![20220423185717](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220423185717.png) 174 | 175 | 这个是一个剪枝的例子 176 | 177 | 一个直观的问题就是这样剪枝不会影响其他的txn么?比如我们压缩了其他txn要看到的东西 178 | 179 | 答案是不会,因为我们的合并是从最大到最小,所以第一个ai就是目前最大的txn,中间不会有其他的活跃事务,所以在figure6中v25会覆盖v50。并且在算法中他会把Vcurrent设成Vvisible,也就是上一次合并的版本。所以每次合并的时候,我们都会保证(Vcurrent, Vvisible)中间是没别人的 180 | 181 | 这就带来一个新的问题,他们怎么保证active txn是及时更新的呢?因为这个list的周期性更新的。我感觉我们还需要维护一些最值信息(比如只合并这个sortlist中的最大和最小的版本) 182 | 183 | 我突然想明白了,每个线程当前运行一个txn,所以线程数就是active txn的数量。所以我们从每个线程那里拿当前他的active txn id,然后创建成一个链表就可以。这么想我们找的第一个版本应该是当前事务看到的第一个版本。否则可能有更新的版本在被使用。 184 | 185 | 最准确的方法应该是每次latch住这个版本链的时候,拿到最新的活跃事务列表再去prune 186 | 187 | 但是他是每次update的时候才会去prune,那么如果已经有了新的txn来了,他如果更新了我们要去更新的地方,那么他就会进行prune,而我们会abort。(所以当更新失败的时候去更新active txn list是一个合理的选择?) 188 | 189 | ### Short-Lived Transactions 190 | 191 | 对于没有长事务运行的workload来说,平常使用的GC就已经够用了,使用EPO还会额外的增加负担 192 | 193 | 但是现实世界的workload是很难预测的,所以我们去tradeoff这里的effectiveness和overhead 194 | 195 | (这里的sorted list貌似是thread-local的)所以是每次运行transcation的时候去找活跃事务并创建sorted list 196 | 197 | 这里的优化就是不是每次都重新创建sorted list,而是重用他们(即更新的频率更小,但是对于长事务仍然有效,因为相比之下我们的活跃事务列表的更新还是更快一些) 198 | 199 | 但是貌似还是没说怎么取出活跃事务 200 | 201 | ### HANA's Interval-Based GC 202 | 203 | 这里说Steam中的prune发生在update的时候,貌似还是会出问题,如果我们不能保证有所有的活跃事务的信息的话 204 | 205 | ![20220423195329](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220423195329.png) 206 | 207 | 什么叫piggybacking the costs while the chain is locked anyway?因为一个线程在访问version chain的时候他需要latch住这个chain,所以我们无论如何也需要去latch住这个chain,就不会出现后台GC线程和worker线程争用的情况了 208 | 209 | ## Layout of Version Records 210 | 211 | ![20220423200234](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220423200234.png) 212 | 213 | 对version records的设计 214 | 215 | 这里提到了原子提交的事情,Version中有一个lock bit,用来保证提交是原子的(有点类似Hekaton) 216 | 217 | 他这里有一个bulk-insert的优化,就不提了 218 | 219 | 然后用AttributeMask可以加速我们检查一个attribute是否在另一个中。并且还节省了存储attributeID的空间 220 | 221 | 这里的结构应该和HyPer是一样的,就是原始的表中有数据,然后一个指针指向version vector,表示他这次的操作 222 | 223 | # Evaluation 224 | 225 | ![20220424092102](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220424092102.png) 226 | 227 | 他这个table4的东西比上面清楚多了,就讲的是具体的实现 228 | 229 | HANA中,整个GC的工作都是在后台进行的 230 | Hekaton中,后台线程负责更新全局最小值,并识别过期的版本。然后把任务分配给worker线程让他们去清理这些版本 -------------------------------------------------------------------------------- /db/scalable_garbage_collection_for_in_memory_mvcc_systems/p128-bottcher.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ysj1173886760/paper_notes/df11876635eaa0da706671f51f6edd3b42e5391c/db/scalable_garbage_collection_for_in_memory_mvcc_systems/p128-bottcher.pdf -------------------------------------------------------------------------------- /db/whatgoesaround-stonebraker.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ysj1173886760/paper_notes/df11876635eaa0da706671f51f6edd3b42e5391c/db/whatgoesaround-stonebraker.pdf -------------------------------------------------------------------------------- /distribute/GentleRain_Cheap_and_Scalable_Causal_Consistency_with_Physical_Clocks/2670979.2670983.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ysj1173886760/paper_notes/df11876635eaa0da706671f51f6edd3b42e5391c/distribute/GentleRain_Cheap_and_Scalable_Causal_Consistency_with_Physical_Clocks/2670979.2670983.pdf -------------------------------------------------------------------------------- /distribute/GentleRain_Cheap_and_Scalable_Causal_Consistency_with_Physical_Clocks/notes.md: -------------------------------------------------------------------------------- 1 | # GentleRain: Cheap and Scalable Causal Consistency with Physical Clocks 2 | 3 | # Abstract 4 | 5 | GentleRain是geo-replicated data store,提供因果一致性 6 | 7 | GentleRain用periodic aggregation protocol来决定更新是否能被其他人看到 8 | 9 | GentleRain通过一个标量的时间戳来实现因果一致性。时间戳是从loosely synchronized physical clocks导出的,时钟漂移不会影响因果一致性,但是可能导致推迟一个更新被他人可见的时间。 10 | 11 | # Introduction 12 | 13 | 一个在Geo-replication data store的关键决策就是一致性模型。选择强一致性模型可以让我们有更简单的语义,但是延迟会增加,并且不能容忍网络分区。而最终一致性模型可以提供很好的性能,并且可以容忍网络分区,缺点就是编程模型会变的更加复杂。 14 | 15 | 大型的数据中心会将数据分区用来存储非常大的数据集。(这里有一个比较有意思的地方,他说:Recent papers have shown how to implement causal consistency in a replicated partitioned data store without incurring the serialization bottleneck of going thought a single log. 他的意思应该是大型数据中心有一个接受者负责将接受到的数据写入log) 16 | 17 | 更新是异步复制的,casual dependencies也会随着更新一起发送。在远端的数据中心上,接受者会发送依赖检查消息给其他分区的方式来确认所有的依赖是否已经满足,然后安装新的版本。然而这个方法的缺点就是依赖检查消息的交换可能导致吞吐量的下降。 18 | 19 | GentleRain提供了不同的方案: 20 | * GentleRain eliminates dependency check messages for updates 21 | * GentleRain uses only a single physical timestamp to track dependencies 22 | 23 | GentleRain相较于其他系统而言会导致更长的延迟(在远端的数据中心中,这个更新可见的所需要的延迟更长) 24 | 25 | # Motivation 26 | 27 | 比较causal consistency和eventual consistency的性能 28 | 29 | ![20220526101412](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220526101412.png) 30 | 31 | 这个workload中,用户会读所有的partition,然后更新一个partition。从而导致跟踪因果一致性的开销变大。 32 | 33 | 他做的实验是移除掉causal consistency所需要的分片间依赖检查,为no remote dep check,以及不去掉检查,但是去掉计算逻辑,为fake remote dep check。 34 | 35 | 根据试验结果得到的motivation就是要想提高causal consistency的吞吐量,我们不能引入dependency check message 36 | 37 | # Definition and Model 38 | 39 | ## Causality 40 | 41 | 应该Causality都是从lamport的那个文章中来的。也就是happens-before的关系 42 | 43 | 当以下三种情况之一出现的时候,则出现了因果关系: 44 | * Thread-of-execution. a and b are operations in a single thread of execution, and a happens before b. 45 | * Reads-from. a is a write operation, b is a read operation, and b reads the value written by a. 46 | * Transitivity. There is some other operation c such that `a happens-before c` and `c happens-before b` 47 | 48 | A store is causally consistent if, when a certain version of a data item is visible to a client, then all of its causal dependencies are also visible. 49 | 50 | ## Architecture and Interface 51 | 52 | ![20220526103348](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220526103348.png) 53 | 54 | 接口: 55 | * PUT(key, val) 56 | * val <- GET(key) 57 | * {vals} <- GET-SNAPSHOT{keys}: 返回一个因果一致的快照。即对于任何一个数据,他依赖的其他数据也会存在于这个快照中。有一个特殊的点就是: Datastore is free is return a snapshot from any point in the past. It can therefore exclude values that have been already ready bt the client before executing the snapshot read.(如果用户曾经读到了一些值的话,说明这些值所依赖的所有数据都已经在这个store中了,我不太明白为什么他会去除掉这些值) 58 | * {vals} <- GET-ROTS{keys}: 和上面的因果一致的快照类似,但是他保证单调读。 59 | 60 | # GentleRain Protocol 61 | 62 | GentleRain的timestamp是物理时间加上partition和replica标识符。这个timestamp可以为所有的更新操作提供全序关系 63 | 64 | ## States 65 | 66 | ![20220526105718](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220526105718.png) 67 | 68 | Client States: 69 | 客户端会为其会话维护一个依赖时间DT。这个值是client读到的所有数据的最大的timestamp。 70 | 同时客户端还会维护GST。 71 | 72 | Server States: 73 | 每一个服务器会维护一个version vector。VV[i]表示当前服务器已经从副本i处接受了直到VV[i]之前所有的更新 74 | 75 | 定义LST,local stable time为服务器上的VV的最小值 76 | 77 | GST,global stable time为相同data center上的最小LST 78 | 79 | Item Version:每一个数据都有多个版本。通过{k, v, ut, sr}来表示一个版本。其中kv为键值,ut为update time,是源服务器创建这个版本的时间。sr为source replica,表示源服务器的replica id 80 | 81 | ## Protocol 82 | 83 | ![20220526124120](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220526124120.png) 84 | 85 | ![20220526124332](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220526124332.png) 86 | 87 | ![20220526132804](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220526132804.png) 88 | 89 | 分片之间独立的同步他们的时间戳。通过Version Vector来记录其他分片中的时间。最小的时间记为LST,即local stable time,表示所有小于这个时间的在这个分片上的更新,已经稳定了。 90 | 91 | 数据中心内部的分片会相互交换信息,统计出所有分片中最小的LST,作为GST。表示任何的更新,如果他的时间戳小于GST,则这个更新对用户可见。(因为在所有分片上的所有副本的时间都已经大于这个时间了,前面的算法可以保证一个更新的时间戳一定大于所有他依赖的更新的时间戳,所以可以保证因果一致性) 92 | 93 | ## Reads of Multiple Items 94 | 95 | ![20220526141715](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220526141715.png) 96 | 97 | 核心就是确定GST,然后在所有分区执行读就可以 98 | 99 | ![20220526141733](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220526141733.png) 100 | 101 | ROT要保证单调读,所以他需要保证之前读到的那些数据被同步到其他数据中心,并更新了GST后才能读。 102 | 103 | 所以他会等GST更新到DT,然后再执行algorithm3 104 | 105 | ## Conflict Detection 106 | 107 | 更新冲突的情况下,我们需要外部逻辑来处理这个冲突。 108 | 109 | GentleRain的冲突检测有点类似Raft,即新到来的Update会保存他前一个版本的信息。在install new version的时候,他会检查新的Update和前一个版本和当前版本链是否对应。如果出现冲突,则需要外部逻辑来处理。 110 | 111 | ## Efficient Global Stable Time Derivation 112 | 113 | 相同数据中心的服务器会周期性的交换他们的LST来计算GST。 114 | 115 | 全体广播的消息复杂度是n方的,所以当数据中心内的分区数很多的时候广播的方法会成为瓶颈 116 | 117 | 通过树形结构来聚合GST 118 | 119 | 叶子节点会发送LST给他的父节点,最终GST会聚合到根节点。然后再重新下发 120 | 121 | 消息的数量很小,但是需要log级别的RTT(数据中心内的延迟很小,相较于数据中心之间的Version Vector的聚合来说开销很小。) 122 | 123 | # Discussion 124 | 125 | ## Why Physical Clocks 126 | 127 | 因为lamport clock只有在接受到消息的时候才会增加他的值。从而可能导致不同服务器之间的clock移动的速率差距较大。而GST跟踪的是全局的最小时间,这种情况下可能导致GST推进的速度很慢。 128 | 129 | 通过物理始终可以避免不同服务器之间的时钟偏差较大。 130 | 131 | ## Throughput vs Latency Tradeoff 132 | 133 | 在现有的系统中,一个更新在远端变为可见的时间是消息从本地传输到远端的时间加上交换依赖检查消息的时间。而后者是在数据中心内部,所以可以简略的认为是本地到远端的时间。 134 | 135 | 然而在GentleRain中,延迟有很多的因素。首先就是距离最远的数据中心的传输时间。因为GentleRain要求所有的数据都到了所有的replica以后才能被客户看到。第二点,数据中心之间可能存在时钟漂移。第三,树形聚合协议需要计算GST的时间。最后,当请求数量少的时候,我们需要靠heartbeat protocol来传播时间。总体的时间也是接近于最远数据中心之间的传输时间。 136 | 137 | -------------------------------------------------------------------------------- /distribute/bigtable/68a74a85e1662fe02ff3967497f31fda7f32225c.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ysj1173886760/paper_notes/df11876635eaa0da706671f51f6edd3b42e5391c/distribute/bigtable/68a74a85e1662fe02ff3967497f31fda7f32225c.pdf -------------------------------------------------------------------------------- /distribute/chubby/chubby-osdi06.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ysj1173886760/paper_notes/df11876635eaa0da706671f51f6edd3b42e5391c/distribute/chubby/chubby-osdi06.pdf -------------------------------------------------------------------------------- /distribute/chubby/notes.md: -------------------------------------------------------------------------------- 1 | # Chubby lock service 2 | 3 | chubby的目的就是为一些低耦合的分布式系统提供粗粒度的锁服务,以及可靠(但是低容量)的存储。 4 | 5 | chubby提供的接口则类似于一个带有锁的分布式文件系统。但是chubby的设计重点在于可用性以及可靠性。而非高性能。 6 | 7 | # Introduction 8 | 9 | lock service的目的是让客户去同步他们的活动,并且对一些信息达成一致。 10 | 11 | 我们希望chubby为一些系统提供粗粒度的锁服务,特别的是处理选主的问题。比如GFS通过chubby来指定一个master。bigtable也通过chubby选主,并且允许master 12 | 13 | chubby的也用于存储一些小数据量的meta-data。他通常被用作一些分布式系统的根数据结构。比如bigtable的root table 14 | 15 | chubby的核心不是提出创新点,而是engineering。论文主要在说他们的决策以及理由 16 | 17 | # Design 18 | 19 | ## Rationale 20 | 21 | google实现了一个paxos库,但是为什么要选择锁服务而不是直接用paxos来做共识呢? 22 | 23 | 某些情况下,开发者最开始去prototype一个东西的时候,他们的负载比较小。并且不需要高可用性。并且代码没有经过去为了共识协议调整他们的结构。当服务成熟的时候,这时候可用性比较重要了,他们需要进行为现有的系统加上replication以及选主。虽然这个目的可以通过共识协议库来解决,一个lock server可以让我们可以更加容易的维持现有代码的结构。 24 | 25 | 比如我们要去选择一个master。我们可以通过添加简单的语句来实现这个操作。首先一个server会尝试获得锁,从而变成master,并且传递一个额外的整数(lock acquisition count),以及一个if语句用来判断acquisition count是不是比当前值小,从而防止延迟的包影响当前的服务。 26 | 27 | 相当于是一定程度上解偶了consensus模块和逻辑模块。 28 | 29 | 第二点是通过name service来提供给用户读写文件的接口。但是他这块理由我有点看不懂... 30 | 31 | 第三点是程序员更熟悉基于锁的接口。因为大多数人都遇到过锁。 32 | 33 | 最后,如果我们直接使用consensus服务的话,需要副本来达成高可用性。比如一个用户系统用的是lock service,他们只需要一个副本就可以获得锁。但是如果用共识库的话则需要多个副本。举个例子就是比如我们有100个客户端,如果在这里面嵌入共识我们就要有100个对等点参与共识,并对维护的状态进行复制。而使用lock service的话,则数量与客户端的数量无关。从而减少了副本的数量。 34 | 35 | 所以感觉核心点还是解偶,把共识模块独立出来处理元信息。然后用锁服务来提供接口,并且提供读写元信息的能力。 36 | 37 | 客户期待知道什么时候主副本变化了,所以我们需要实现一种通知机制来防止用户不断的轮询。但是用户仍然可能需要不断的去访问chubby中的数据,并且chubby中的数据变化的很少。所以我们提供了对文件的缓存。(我猜测应该是去监视这个文件,然后当文件内容变化的时候去通知用户,从而保证缓存的一致性) 38 | 39 | chubby使用coarse-grained lock。从而减少lock server的负载。同时可以减少lock server failure对于lock的影响。相对来说,细粒度的锁就更容易受到lock server failure的影响。 40 | 41 | 通过locking service,客户负责提供支持他们负载的服务器,而不是lock service,同时也从实现共识协议的复杂性中解脱出来。 42 | 43 | ## System structure 44 | 45 | ![20220330191039](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220330191039.png) 46 | 47 | 这里其实说的就是共识协议的一些实现,就不在这里多列了。 48 | 49 | ## Files, directories, and handles 50 | 51 | 提供了UNIX类似的文件系统。文件和目录都是Node,Node可以是permanent或者是ephemeral的。对于ephermal的节点,如果没有客户端打开他们的时候他们就会被自动删除。ephermal的节点通常被用作临时文件,或者是用来标识他们对应的client还存活。 52 | 53 | note的元数据有4个64位的整数: 54 | 1. instance number: 是一个比以前相同名字的node的instance number都大的数(用来去重) 55 | 2. content generation number: 看名字也可以看出来,用来标识文件内容改变的 56 | 3. lock generation number: 每次获得锁的时候就增加这个数 57 | 4. ACL generation number: 标识控制权限的更改 58 | 59 | chubby还提供了一个chucksum,从而保证文件的内容不会改变 60 | 61 | client打开文件的时候会获得handle,对应了UNIX中的file descriptor,一个handle中包含了: 62 | 1. check digits,用来防止用户猜测handle 63 | 2. sequence number,从而让master知道这个handle是当前master创建的还是之前的master创建的 64 | 3. mode information,从而可以让新的master遇到旧的handle的时候去重建他的状态 65 | 66 | ## Locks and sequencers 67 | 68 | chubby的node可以作为一个读写锁使用 69 | 70 | 还有就是分布式场景下的锁,我们需要保证接受到的请求需要和锁的持有者是对应的。否则由于出现message延迟的情况,可能一个没有持有锁的人去使用了服务。 71 | 72 | 我们通过sequencer来解决这个问题,锁持有者可以在任何时间获得锁的状态,当他需要被锁保护的服务的时候,他就会把sequencer附带上,那么服务器就可以去检查这个sequencer的状态。 73 | 74 | server可以通过chubby的cache,或者服务器最近发现的sequencer来检查请求的有效性。 75 | 76 | 有些server还不支持sequencer,所以通过lock-delay的方法来解决。通过延迟去将这个锁分配给其他的人,我们希望让那些延迟的消息都被处理或丢失后再分配新的lock holder,从而防止delayed message导致的问题。 77 | 78 | ## Events 79 | 80 | chubby客户端可以在创建handle的时候订阅一系列的事件。包括: 81 | 82 | * 文件内容更改 83 | * 子节点的增加删除或者修改 84 | * master失效 85 | * handle失效 86 | * 锁的获取(用来确定primary的选取) 87 | * 冲突的锁请求(用来允许锁缓存,论文里说没人用) 88 | 89 | ## Caching 90 | 91 | 为了减少read traffic,chubby的客户端会用一种write-through的方法缓存数据。通过租约的机制来保证缓存的有效性,同时通过master发送的消息来使缓存无效。 92 | 93 | master维护了一个列表,存储了客户端都会缓存什么东西。 94 | 95 | 当文件数据改变的时候,这个操作会被阻塞住,此时master会把数据无效的信息发送给所有具有这个缓存的客户端中。 96 | 97 | 当服务器知道所有的客户端都消除了缓存以后,他才会继续进行更改(相当于2PC) 98 | 99 | Chubby的这个论文感觉有点乱。后面我记一些比较重要的点,就不每一节都写了。 100 | 101 | ![20220331104005](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220331104005.png) 102 | 103 | grace period来保证切换master的时候我们还可以继续提供服务 104 | 105 | -------------------------------------------------------------------------------- /distribute/gfs/gfs.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ysj1173886760/paper_notes/df11876635eaa0da706671f51f6edd3b42e5391c/distribute/gfs/gfs.pdf -------------------------------------------------------------------------------- /distribute/mapreduce/mapreduce.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ysj1173886760/paper_notes/df11876635eaa0da706671f51f6edd3b42e5391c/distribute/mapreduce/mapreduce.pdf -------------------------------------------------------------------------------- /distribute/mapreduce/notes.md: -------------------------------------------------------------------------------- 1 | 通过functional model和用户指定的map和reduce操作,我们可以很容易的将这些计算并行化。 2 | 3 | 通过re-execution作为主要的fault tolerance的手段 4 | 5 | 2 programming model 6 | 7 | map接受input pair,并生成一组中间键值对。MapReduce Library会把相同key的键值对的所有value组合在一起,并把他们传给reduce 8 | 9 | reduce接受一个key以及对应的一组value,他将这一组值合并到一起,并返回给用户 10 | 11 | 一个计算每个document的单词数的例子 12 | 13 | ``` 14 | map(String key, String value): 15 | // key: document name 16 | // value: document contents 17 | for each word w in value: 18 | EmitIntermediate(w, "1"); 19 | 20 | reduce(String key, Iterator values): 21 | // key: a word 22 | // values: a list of counts 23 | int result = 0; 24 | for each v in values: 25 | result += ParseInt(v); 26 | Emit(AsString(result)); 27 | ``` 28 | 29 | 更多的例子: 30 | 31 | distributed grep: map对于每一行,如果他match pattern就放到输出中,reduce是直接输出 32 | 33 | count of url access frequency: map对于每一个网页请求生成(URL,1),reduce进行求和 34 | 35 | reverse web-link graph:对于每一个从source页面引出的URL target,生成(target, source),然后reduce组合他们生成(target, list(source)) 36 | 37 | term-vector per Host:term-vector是一个文档的一个(word, frequency)列表,即计算每个词出现的频率。对于一个host的下的每个document来说,都生成对应的(hostname, term-vector),然后reduce将这些term-vector加起来,并丢弃掉低频的词,就可以得到每个host的热点词 38 | 39 | inverted index: map对于每一个document中的word,生成一个(word, document ID)的序列,然后reduce用来合并并输出(word, list(document ID))。我们可以很容易的来增强这个计算来得到每个单词的位置 40 | 41 | distributed sort:map对于每个record生成(key, record),key就是用来排序的key,然后reduce直接输出即可。因为mapreduce的库会根据key进行排序 42 | 43 | 3 implementation 44 | 45 | ![20220208141117](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220208141117.png) 46 | 47 | 输入数据会被自动partition成M份,放到不同的机器中并行的处理 48 | 49 | 通过用户指定的partition function和partition number R,所有的中间结果会被分成R份,然后再应用reduce 50 | 51 | figure 1是一个overall flow 52 | 53 | 1. MapReduce Library会首先将数据分成M份,然后在集群中启动程序 54 | 2. 这些程序中有一个是master,其余的都是worker。master会将M个map task和R个reduce task分发给worker 55 | 3. 拥有map任务的worker会读取输入,从输入中解析键值对,并传给map函数。中间结果会被存储到内存中 56 | 4. 缓存的中间结果会周期性的写入到本地的磁盘中,并根据用户指定的分区函数分成R个区域。这些本地磁盘中的键值对的位置会被发送到master中,并由master转发给reduce worker 57 | 5. reduce worker会通过RPC来读取map worker本地磁盘中的结果。当所有的数据的都读取完的时候,他会根据key进行排序,从而将相同键的所有值group到一起。之所以需要排序是因为可能中间结果很大,我们不能把数据放到内存中 58 | 6. reduce worker会读取所有的中间数据,并将key和对应的一组value传给reduce function。最终结果会被追加到这个reduce分区中的final output file 59 | 7. 当所有的map和reduce task都完成了,master会唤醒user program,最终回到user的代码中 60 | 61 | 最后的结果会在R个output file中,文件名由user指定。通常user不需要去组合这些output file,因为这些文件通常作为下一个MapReduce的输入文件,或者传给其他的分布式应用 62 | 63 | 3.2 Master Data Structures 64 | 65 | master保存了每一个task的状态,以及对应的worker machine的标识 66 | 67 | master还是一个用来将map输出的中间结果文件位置转发给reduce worker的通道。所以对于每一个完成了的map task,master就会保存结果文件的size和location。这个信息会被逐步发送给reduce worker(原文中写的是incrementally,但是我不确定这里增量的含义是什么) 68 | 69 | 3.3 Fault tolerance 70 | 71 | master会定期的ping worker,如果没有回复则认定worker failed。所有的在failed的worker上完成的或者进行中的map任务都会被重置为idle,并会被调度器重新调度 72 | 73 | 因为中间结果是存储在本地磁盘上,所以在failed worker上完成的map任务也需要重新执行,因为我们无法访问这些结果。但是对于reduce来说,完成的任务不需要重新执行,因为reduce的结果存储在global file system中(GFS),提供了容错 74 | 75 | 当map任务切换机器的时候,所有的reduce task都会接到重新执行的通知。没有读取完数据的会从新的机器上重新读取数据(不太明白为什么要重新执行) 76 | 77 | 对于master failure,我们可以周期性的进行checkpoint。但是论文中提到我们只有一个master,不太会失效。但是如果master失效了,我们会终止MapReduce计算 78 | 79 | 3.4 Locality 80 | 81 | 为了节省网络带宽,master会考虑输入文件的位置信息,并尝试在包含相应输入数据副本的机器上安排map任务。如果失败的话,他也会享受将任务安排在副本附近(同一个交换机上) 82 | 83 | 3.5 Task Granularity 84 | 85 | 理想情况下,我们希望task的数量远大于worker的数量,因为这样我们可以实现动态的负载均衡,当一个worker失效的时候,我们可以将他的任务分发到其他的worker上 86 | 87 | 选择M的时候希望输入的数据是16MB到64MB之间,这样可以利用locality(配合GFS),而对于R来说一般是worker数量的一个小的倍数 88 | 89 | 3.6 backup tasks 90 | 91 | 延长计算总时间的通常是因为有机器掉队,我们需要很久才能计算出最后的几个map或者reduce task 92 | 93 | 掉队的原因有很多,比如原本机器上有任务,资源竞争,机器损坏等 94 | 95 | 当MapReduce操作接近完成时,master就会调度backup execution来执行剩下的task。只要primary或者backup一个完成了执行就完成了task。 96 | 97 | 4 Refinements 98 | 99 | 用户指定了reduce的数量R,默认情况下用hash分区,但是用户也可以自己提供分区函数来达到更好的效果 100 | 101 | MapReduce保证在给定的分区中,KV pair是按顺序处理的。所以我们也就可以得到一个sorted的输出文件。对于后续需要进行高效查找等操作是非常有用的 102 | 103 | 一般来说,每个map输出得到的kv pair很多时候都是重复的。比如word count的例子,我们会得到大量的值为1的记录。我们可以让用户指定一个combiner function,可以在跨网络传输之前对数据进行部分的合并,从而减少网络带宽的需求 104 | 105 | 一般使用相同的代码来实现reduce和combiner。combiner会在每个执行map task的机器上执行。 106 | 107 | 有的时候由于用户代码存在错误,导致在某些record上会出现确定性的crash。我们可以选择忽略一些record并继续前进 108 | 109 | 每一个worker都有signal handler用来捕捉错误。MapReduce库会将一个序列号存储到全局变量中,如果user code出现错误,signal handler会给master发送一个包含这个序列号的信息。如果master发现在某个record上出现多次错误,他就会指示在下次re-execution的时候跳过这个记录 110 | 111 | counter,MapReduce提供了一个counter object,可以放到user code中用来计数。worker会定期的将counter的值发送给master(放在ping的回复中),master聚合这些值并当MapReduce结束的时候返回给用户。这些值也会显示在MapReduce的主状态页上,用来查看实时计算的进度。 112 | 113 | 6 experience 114 | 115 | MapReduce的实现依赖于负责分发并运行用户task的集群管理系统 116 | 117 | MapReduce通过受限制的编程模型来把问题划分为大量细粒度的任务,从而让我们可以进行动态调度,以便让更快的worker处理更多的任务。同时还允许我们在接近结束的时候去执行那些比较慢的任务,从而加速执行时间 118 | 119 | MapReduce通过自动并行execution以及提供透明的容错机制来提高开发效率 120 | 121 | 8 conclusion 122 | 123 | 1. MapReduce模型容易使用,隐藏了并行化,容错机制,局部性优化以及负载均衡的细节 124 | 2. 大量的问题都可以通过MapReduce来表达 125 | 126 | Learned 127 | 128 | 1. 通过限制编程模型可以让并行和分布计计算,以及容错变得更简单 129 | 2. 网络带宽是缺乏的资源,大量的优化都是为了减少网络带宽 130 | 3. 通过redundant execution来减少较慢的机器带来的效应(木桶效应),同时进行容错 131 | -------------------------------------------------------------------------------- /distribute/more-gfs/notes.md: -------------------------------------------------------------------------------- 1 | 对GFS的一些补充,主要是来自mit pdos 2 | 3 | ### 为什么atomic record append是至少一次,而不是exactly-once? 4 | 5 | 如果一次写操作失败的话(有可能只是一个从副本失败了),客户端就会重试这次写操作。这会导致在没有失败的地方会出现重复的数据。 6 | 7 | 其实可以去修改设计让服务器检测到重复的请求,但是这样会影响performance,以及影响复杂性 8 | 9 | ### Application是怎么知道一个chunk中的数据是padding或者重复数据呢? 10 | 11 | 对于padding来说,用户可以在有效的record之前放上一个magic number,或者放上校验和,并且只有record有效的时候校验和才有用。 12 | 13 | 而对于重复元组可以为元组添加unique ID,这样在读的时候就可以知道这个元组是曾经读到过的。(大量的元组会不会导致去重困难?或者排序后去重?) 14 | 15 | GFS提供了一个库来处理这些情况 16 | 17 | ### 用户怎么找到他们的数据当atomic record append会在一个不确定的offset中写入? 18 | 19 | Append的应用主要是为了后续读全部文件,他们希望获得的是元组的集合,而不是元组的位置。所以offset不重要 20 | 21 | ### checksum 是什么? 22 | 23 | checksum就是输入一堆字节,然后返回他们聚合的值,比如是一个和。GFS在chunk内部存储了chunk的checksum。当GFS尝试写入一个chunk的时候,他会首先计算这个chunk的checksum,并和chunk一同写入磁盘。当读取的时候,他们就会重新计算校验和,并和已有的校验和进行检查。如果校验和不匹配,则说明数据损坏了。那么我们可以去别的chunk server读取数据。有的GFS应用会存储他们自己的校验和。从而区分padding和有效的元组。 24 | 25 | ### reference counts是什么? 26 | 27 | 在GFS中,他们是为了snapshot服务的。当GFS创建snapshot的时候,他不会去复制chunk,而是增加对应chunk的reference count。当写入的时候,master会发现这个reference count大于1。那么master会首先去复制这个chunk,从而让client来更新这个副本。就是COW技术的应用 28 | 29 | ### GFS怎么确定最近的副本在哪里? 30 | 31 | paper中说到是通过IP地址。Google可能特意的做了这样的操作,如果机器在同一个机房或者机架,那么他们的地址会相近 32 | 33 | ### 租约是什么? 34 | 35 | 租约就是在某一段时间中,一个chunk server会成为一个chunk的primary。从而防止我们需要重复的询问谁是primary 36 | 37 | ### GFS中,如果一个primary出现了partition,此时master分配了第二个primary,会不会出现两个primary? 38 | 39 | 不会,因为GFS保证了他只有在前一个人超过租约时间以后才会分配第二个primary。所以当出现第二个primary的时候,第一个primary的lease已经过期了。 40 | 41 | ### 64MB貌似太大了? 42 | 43 | GFS分割和存储文件的粒度就是64MB。客户端可以进行更小的操作,而不是每次必须进行64MB的数据处理。使用大块的目的主要是减少元数据的存储,并且减少需要大量数据传输的client的限制。第二点则是如果文件太小则我们无法获得太多的并行性。(overhead会更大) 44 | 45 | ### GFS是怎么处理数据的正确性和performance以及simplicity? 46 | 47 | 强一致性需要复杂的协议,同时需要机器之间的通信。通过利用特定程序可以容忍宽松一致性的方式,可以设计出性能良好的系统。比如GFS针对MapReduce进行优化,从而获取高文件读取性能,并且这些程序对于重复数据,文件中有hole,以及不一致的读取是可以容忍的。然而GFS则不适合去存储银行数据 48 | 49 | ### Master failed怎么办? 50 | 51 | 需要存在具有主状态完整副本的replica。比如可以通过Raft去切换到备份 52 | 53 | ### single master是一个好主意吗? 54 | 55 | 这个想法简化了最初的部署。但是长远来看,当文件数量足够多的时候,我们无法将所有文件的元数据存储在一个RAM中。client足够多以至于一个master没有足够的能力来为他们服务。并且GFS中从故障master切换到备机需要人来操作,使得恢复变慢。 56 | 57 | ### GFS通过弱一致性获得了什么? 58 | 59 | 通过思考我们怎么让GFS实现强一致性来思考这个问题 60 | 61 | 在写入数据的时候,我们需要一个2PC的过程来保证原子的写入(exactly-once) 62 | 63 | 我们还需要保证secondaries具有最新的primary数据,从而在primary宕掉的时候成为最新的primary 64 | 65 | 处理client重新发送的消息 66 | 67 | client会缓存chunk的位置,但是这个位置有可能是过时的。我们还需要保证这个操作不会成功 68 | 69 | 感觉这些问题都可以通过chunk副本之间的共识来完成。但是需要消耗的代价就太多了。 -------------------------------------------------------------------------------- /distribute/more-raft/link.txt: -------------------------------------------------------------------------------- 1 | https://github.com/OneSizeFitsQuorum/raft-thesis-zh_cn/blob/master/raft-thesis-zh_cn.md -------------------------------------------------------------------------------- /distribute/more-raft/notes.md: -------------------------------------------------------------------------------- 1 | # 领导权禅让 2 | 3 | 有的时候leader必须下台,比如他可能出现重新启动,或者已经从集群中删除 4 | 5 | 在某些情况下,一台或多台服务器可能比其他的服务器更适合领导集群。比如数据中心中的服务器,用来减少客户端和领导者之间的延迟 6 | 7 | 过程如下 8 | 9 | * 当前leader停止接受客户请求 10 | * 当前leader完整更新目标服务器的日志以使其与自己的日志匹配 11 | * 当前leader将timeoutNow请求发送到目标服务器,目标服务器将开始新的选举 12 | 13 | # 集群成员更改 14 | 15 | 和论文中不同的是,这里的集群更改是一个更加简单的算法 16 | 17 | 核心思路就是禁止会导致多数成员不相交的成员更改。所以这种更改方法一次只能从集群中添加或者删除一个服务器 18 | 19 | ![20220314151657](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220314151657.png) 20 | 21 | 当领导者收到从当前配置(Cold)中添加或删除服务器的请求时,它将新配置(Cnew)作为一个条目添加到其日志中,并使用常规的 Raft 机制复制该条目。新配置一旦添加到服务器的日志中,就会在这个服务器上生效:Cnew 条目被复制到 Cnew 指定的服务器上,而大部分服务器的新配置生效被用于确定 Cnew 条目的提交。这意味着服务器不会等待配置条目被提交,并且每个服务器总是使用在其日志中找到的最新配置。 22 | 23 | 一旦提交了 Cnew 条目,配置更改就完成了。此时,领导者知道大多数的 Cnew 指定的服务器已经采用了 Cnew。它还知道,没有收到 Cnew 条目的任意服务器都不能再构成集群的大多数,没有收到 Cnew 的服务器也不能再当选为领导者。Cnew 的提交让三件事得以继续: 24 | 25 | * 领导可以确认配置更改的成功完成。 26 | * 如果配置更改删除了服务器,则可以关闭该服务器。 27 | * 可以启动进一步的配置更改。在此之前,重叠的配置更改可能会降级为不安全的情况 28 | 29 | 但是由于我们是在接受到配置日志以后直接apply,所以这个日志是有可能被删除掉的,我们需要保存之前的日志来做恢复 30 | 31 | 在Raft中,我们是通过caller的配置来达成共识的 32 | 33 | * server会接收来自leader的AppendEntries RPC,无论这个leader是不是在这个server的最新configuration中。 34 | * server还允许投票给不属于服务器当前最新配置的candidate以保证集群可用。但是由于我们可以保证新旧成员的相交,所以这是安全的操作 35 | 36 | 所以服务器是直接处理RPC请求,而不需要查询他的配置(配置只在发起RPC时使用) 37 | 38 | # 可用性 39 | 40 | 当服务器被添加到集群中的时候,他的日志可能需要相当长的一段时间才能赶上leader的log 41 | 42 | 在此期间集群容易出现不可用的状况,比如本来3台机器,可以容忍一个机器宕掉。现在新加入了一台机器,majority变成4台。然后原本的机器宕掉一台,而新的机器没有追上log。就出现了不可用的状态 43 | 44 | ![20220315093536](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220315093536.png) 45 | 46 | ## 追赶 47 | 48 | 添加一个新的阶段,新的服务器作为无投票权的成员加入集群。leader复制日志给他,但是他不计入majority。当新的服务器赶上了集群的其余部分,就可以按照之前的方法进行重新配置 49 | 50 | 我们需要让leader确定什么时候新的服务器已经赶上了进度从而可以继续进行配置修改。我们的目标是将暂时的不可用性保证在一个election timeout下。所以当复制新日志的时间大于一个election timeout的时候,说明新的server还没有能够追赶上来的能力 51 | 52 | ![20220315094150](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220315094150.png) 53 | 54 | 作为新服务器追赶的第一步,leader必须发现新服务器的日志是空的。如果通过在AppendEntries中不断的失败来将leader的nextIndex下降到1,会影响性能。所以可以在AppendEntries中返回其日志的长度 55 | 56 | ## 破坏性服务器 57 | 58 | 有的服务器可能不在新的配置中,但是他自己并不知道,所以他会不断的超时并进行RequestVote RPC 59 | 60 | 消除干扰的第一个思路就是,如果一个服务器要考试election,他会首先检查他会不会浪费其他人的时间——他是否有机会赢得选举 61 | 62 | 引入pre-vote phase,只有candidate相信自己可以从集群中或的大多数投票的时候,他才会开始真正的选举 63 | 64 | 但是某些情况下,干扰服务器已经足够新,他仍然会造成干扰 65 | 66 | 比如这种情况 67 | 68 | ![20220315095805](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220315095805.png) 69 | 70 | Raft的解决方案是使用心跳来确定何时存在有效的leader。在raft中,如果leader可以保持跟随着的心跳状态,则他被认为是活跃的。因此服务器不应该干扰一个活跃的leader 71 | 72 | 所以如果服务器从当前leader哪里听到的最小选举超时内收到RequestVote请求,则他可以放弃请求,拒绝投票或者延迟投票 73 | 74 | 正常情况下,每个服务器应该等待最小选举超时时间再开始新的选举。对于切换配置的这种情况,我们拒绝小于最小选举超时时间的RequestVoteRPC即可。因为这说明请求的服务器是不正常的 75 | 76 | 最后是一个小总结 77 | 78 | 1. 可以在配置更改的所有步骤中选举一位领导者。 79 | 80 | * 如果新集群中具有最新日志的可用服务器具有 Cnew 条目,它可以从大多数 Cnew 那里收集选票并成为领导者。 81 | * 否则,Cnew 条目必然尚未提交。 在旧集群和新集群中,具有最新日志的可用服务器可以收集大多数 Cold 和大多数 Cnew 的投票,因此,无论使用哪种配置,它都可以成为领导者。 82 | 83 | 2. 领导一经选举便得到维持,假设他的心跳达到了正常状态, 除非它因不在 Cnew 中但已提交 Cnew 而有意退出。 84 | 85 | * 如果领导者可以可靠地将心跳发送到其自己的跟随者,则它或其跟随者都不会接受更高的任期:他们不会超时开始任何新的选举,并且他们将忽略来自其他服务器的更高任期的任何 RequestVote 消息。因此,领导者不会被迫下台。 86 | * 如果不在 Cnew 中的服务器提交 Cnew 条目并退出,则 Raft 将选出新的领导者。这个新领导者很可能将成为 Cnew 的一部分,从而完成配置更改。 但是,下台的服务器可能会再次成为领导者,这存在一些(较小)风险。 如果它再次当选,它将确认 Cnew 条目的提交并很快下台,并且 Cnew 中的服务器下次可能再次成功。 87 | 88 | 3. 在整个配置更改期间,领导者将为客户端请求提供服务。 89 | 90 | * 领导者可以在整个更改过程中继续将客户请求添加到他们的日志中。 91 | * 由于在将新服务器添加到集群之前对其进行了跟踪,因此领导者可以提前提交其提交索引并及时回复客户端。 92 | 93 | 4. 领导者将通过提交 Cnew 来推进并完成配置更改,并在必要时退出以允许 Cnew 中的服务器成为领导者。 94 | 95 | # Log Compaction 96 | 97 | ## Memory-Based Snapshots 98 | 99 | 这里指状态机是存在memory中 100 | 101 | 这个方法在Raft的论文中有提到,这里就不说细节了。说一下实现上的问题 102 | 103 | 为了避免可用性缺口,我们需要保证在写入快照时和正常的操作是并发进行的。 104 | 105 | 写时复制技术可以支持这种操作,有两种方法 106 | 107 | * 状态机可以用不可变的数据结构来支持这一点,快照保存某一刻状态机的引用,并复制即可(可持久化数据结构) 108 | * 利用操作系统的写时复制,用fork来复制服务器整个地址空间,然后子进程写入快照 109 | 110 | 服务器需要额外的内存来创建快照,并且由于false sharing(不相关的数据项恰好在一页内存中,即使只更改了第一项,第二项也要复制)导致我们需要更多的内存。所以在snapshot的时候可能出现内存耗尽的情况。所以最好准备流接口,让我们可以在snapshot的时候不必把整个快照都放在内存中 111 | 112 | 何时进行snapshot? 113 | 114 | 让快照的大小和日志的大小进行比较,如果快照小很多,那么就值得进行snapshot。但是计算快照的大小却比较复杂,因为我们会压缩快照文件。我们可以用上一个快照的大小来预估下一个的大小。 115 | 116 | 其实也可以对集群中的少数进行快照,因为正常的提交只需要大多数即可。让少数服务器暂时停止响应并进行snapshot。这样我们可以获得更大的吞吐量。(但是我感觉应该是所有人都需要snapshot) 117 | 118 | ### 实现问题 119 | 120 | * 保存和加载快照: 从状态机到磁盘上文件的流接口有助于避免将整个状态机状态缓存到内存中。通过将快照写入临时文件,然后在写入完成后重命名文件来保证服务器不会从中间状态加载数据 121 | * 传输性能可能不是很重要,因为他不涉及到新条目的提交,但是如果出现故障我们就希望尽快跟上以恢复可用性 122 | * 消除不安全的日志访问并丢弃日志条目,这个是因为丢弃log后以前的log就访问不到了,所以要保证不会越界等问题 123 | 124 | 还有两个我认为用处不大就没写 125 | 126 | ## Disk-Based Snapshots 127 | 128 | 这里的状态机是存在磁盘中的 129 | 130 | 这些状态机总是在磁盘上存储他的状态。每次apply日志都会更改磁盘上的状态,并且获得新的快照。所以一旦apply了entry,我们就可以丢弃这个entry 131 | 132 | 或者是在内存中缓存写操作,以此来提高磁盘效率(LSM) 133 | 134 | 主要的问题就是改变磁盘上的状态会导致性能下降。因为我们会用到若干个随机的磁盘写 135 | 136 | 基于磁盘的状态机也需要COW技术,来传输他给follower 137 | 138 | Linux上的LVM可以用于创建整个磁盘分区的快照。 139 | 140 | 通过一些算法来避免开销: 141 | 142 | * 跟踪每个磁盘块上次修改的时间 143 | * 将磁盘内容传输给follower,并记录他的最后修改时间 144 | * 对于磁盘块上的内容,用COW保存一致性快照并传输给follower 145 | * 最后重传那些在上次传输后修改过的磁盘块 146 | 147 | ## LSM Tree 148 | 149 | ![20220315153227](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220315153227.png) 150 | 151 | LSM Tree做状态机的算法 152 | 153 | 某些系统会为每个run创造一个bloom filter。从而加速键的查找 154 | 155 | 在Raft中使用LSM是相当容易的。每次apply新的log就把它放到内存的树结构中 156 | 157 | 我们用raft日志大小作为限制来做合并。当raft日志到达一个阈值的时候,我们就可以把当前的状态机序列化为一个Run,然后把log都清理掉。 158 | 159 | 发送给follower的时候,只需要发送所有的Run即可。内存中的树不需要,因为他们的信息存储在log中。并且因为Run是不可变的,所以我们可以很方便的传输Run(但是run还是会合并,所以我们应该也需要一个一致性的快照) 160 | 161 | 最后就是一个log compaction的总结 162 | 163 | * 每个服务器都独立的压缩其log 164 | * 状态机和Raft之间的基本交互涉及到把log compaction的责任从raft转移到状态机中。一旦状态机将command持久化了以后,他就要通知raft去discard对应的log 165 | * 一旦raftdiscard了log,那么状态机就需要负责两个新的任务:重启时加载快照,并提供一个一致性的快照用来发送给其他慢的follower 166 | 167 | # Client 168 | 169 | ![20220315161540](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220315161540.png) 170 | 171 | 可以通过这几个RPC看到,主要是我们需要防止重复 172 | 173 | clientID用来标识用户ID,然后对于每个用户跟踪sequenceNum 174 | 175 | 对于查询来说就不需要去重了,直接查就可以。但是由于读操作没有用log,所以我们需要保证这个leader是真的leader,也就是需要与majority交换一次心跳 176 | 177 | * 领导者:服务器可能处于领导者状态,但是如果它不是当前领导者,则可能不必要地延迟了客户端请求。例如,假设一个领导者已和集群的其余部分进行了分区,但是它仍然可以与特定的客户端进行通信。如果没有其他机制,它可能会永远延迟来自该客户端的请求,无法将日志条目复制到任何其他服务器。期间可能还有另一个新任期的领导者能够与大多数集群成员通信,并且能够提交客户的请求。因此,如果没有在其大部分集群中成功进行一轮心跳,选举超时就结束了,Raft 领导者就会下台。这样,客户端可以通过另一台服务器重试其请求。 178 | * 跟随者:跟随者保持跟踪领导者的身份,以便他们可以重定向或代理客户端的请求。他们在开始新的选举或更改任期时必须放弃此信息。否则,它们可能不必要地延迟客户端(例如,两台服务器可能会彼此重定向,从而使客户端陷入无限循环)。 179 | * 客户端:如果客户端失去与领导者(或任何特定服务器)的连接,则应仅简单随机重试一个服务器。如果该服务器发生故障,则坚持联系最后一位已知的领导者将导致不必要的延迟。 180 | 181 | 最主要的就是不要让过期的leader一直认为自己是leader,所以我们需要用lease机制,让leader与majority交换心跳 182 | 183 | 为了提供exactly-once语义,我们就需要去重的操作 184 | 185 | 去重的操作还可以更加普遍一些。我们不去跟踪每个client最后的sequence number,而是去跟踪一个sequenceNumber和回复的集合 186 | 187 | 每次request,client把他没收到回复的最小的sequenceNumber附带上。那么状态机就可以把更小的那些sequence和reply删除掉 188 | 189 | 但是client与server之间的会话不能永久的保存下去。server必须在某一时刻删除掉这个session。那么带来了两个问题:服务器之间怎么达到删除session的共识呢?以及怎么处理session被过早关闭的情况? 190 | 191 | 比如我们有一个server关闭了这个session,然后就会重新apply这个client的操作,但是另一个server没有关闭,所以他成功去重了。这时候服务器的state就是不一致的。 192 | 193 | 所以我们需要找到一个确定性的方法来关闭client的session 194 | 195 | 一个选项就是用LRU,设置session的上限。然后用LRU做evicit 196 | 197 | 另一个选项是leader会将他的时间加入到log中,其他的follower都会根据这个时间来去expire不活跃的session。client通过发送keep-alive RPC来让leader提交一个log,从而防止他们的session被断掉 198 | 199 | 另一个问题则是如何区分client是新的还是我们曾经关闭了session的client。因为如果我们为每一个没有session的client都新分配一个session的话,就有可能导致duplicate execution。所以有了上面的RegisterClient RPC,来注册一个clientID,后续的操作都会使用这个clientID。当我们发现一个clientID对应的session已经被销毁了,我们就会返回一个error(可以让client重新分配ID或者crash) 200 | 201 | ## 优化读 202 | 203 | 我们可以绕过日志来达成线性化的读。这个在上面的图片中有描述了具体过程 204 | 205 | 核心思路就是对于一个读请求,我们需要让leader去与majority交换心跳,从而确定自己是一个最新的leader。这样读就可以直接进行 206 | 207 | 注意在leader当选的时候,他不清楚当前的commitIndex到哪里了。我们可以保证leader有最新的commitindex对应的log,但是leader不能确定他目前是在那个位置。所以通过让leader提交一个空的log。当这个空的log提交的时候,leader就可以确定当前的commitindex在哪里。换一个思路理解就是由于read不走log,但是我们又需要让read是最新的数据,所以要保证那些提交了的log已经被apply了。但是切换leader的时候可能follower还没及时apply。所以这里提交空log的目的就是相当于一次同步,让leader的状态机达到最新的状态。 208 | 209 | 我们还可以进一步优化一下,通过一轮心跳来确认积累的任意个读请求 210 | 211 | follower也可以进行优化,他可以去询问leader当前的readIndex。然后leader可以发送readIndex。这样当follower的状态机apply到readIndex的时候,他就可以处理读请求。 212 | 213 | 注意这里不需要考虑往返RPC之间可能出现的其他写操作。因为在我们收到leader的readIndex这一刻其实相当于我们进行了读操作,在这之前出现的写操作在client的角度看是发生在这个读之前的,在这个之后的写则是在读之后的。我们仍然可以满足线性化语义。因为那些操作都是并发的,可以被划分到读之前或之后。 214 | 215 | 利用租约来减少信息传输 216 | 217 | 一旦集群的majority已经认可了leader的心跳,leader将假定在选举超时期间没有其他的服务器成为leader。这样leader将在此期间直接回复只读查询而无需任何其他通信 218 | 219 | 我们还可以提供单调读。服务器可以将与状态机对应的索引返回给客户端,然后客户端每次请求时将这些信息提供给服务器。如果服务器发现客户端的索引大于他自己应用到的索引,他就不会为这次请求提供服务。 220 | 221 | # Optimization 222 | 223 | ![20220315192518](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220315192518.png) 224 | 225 | 用pipeline来并发执行RPC和disk write 226 | 227 | 我们可以让follower在利用网络资源接收RPC的同时,用disk资源来进行写入 228 | 229 | 为了使用pipeline,leader需要在发送一个log的RPC后立刻更新他的nextIndex,而不是等待RPC的回复 230 | 231 | 这样我们就可以pipeline下一个entry 232 | 233 | 当RPC超时的时候,我们就需要去减少对应的nextIndex。当consistency check失败的时候,leader就需要减少nextIndex并重新发送前面的entry,或者等待前面的entry发送成功 234 | 235 | 同时我们还需要支持对每一个peer的多个并发RPC,这需要我们对每个peer创建多个thread来支持 236 | 237 | reorder的情况会导致pipeline的效率下降。所以如果我们可以去buffer out-of-order requests直到他们可以按照顺序append的话,就可以获得效率上的提升。(这就需要应用层的优化了,因为tcp层不知道我们的log谁先谁后) -------------------------------------------------------------------------------- /distribute/more-raft/stanford.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ysj1173886760/paper_notes/df11876635eaa0da706671f51f6edd3b42e5391c/distribute/more-raft/stanford.pdf -------------------------------------------------------------------------------- /distribute/percolator/Percolator.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ysj1173886760/paper_notes/df11876635eaa0da706671f51f6edd3b42e5391c/distribute/percolator/Percolator.pdf -------------------------------------------------------------------------------- /distribute/percolator/notes.md: -------------------------------------------------------------------------------- 1 | # Abstraction 2 | 3 | 论文中说这个percolator是用来进行增量处理的一个系统,用来替换MapReduce在google indexing system中的作用 4 | 5 | 但是没有提到transaction相关的东西 6 | 7 | # Introduction 8 | 9 | 考虑我们为网页构建索引的任务。索引系统首先会把每个网页都爬下来,如果有多个URL都指向了相同的内容,那么拥有最高的PageRank的URL会被保留下来放到索引中。我们还会把每一个链接对应的anchor text附在他指向的页面中。同时要保证对于链接指向的重复的内容,我们也会把它转发到PageRank最高的副本中 10 | 11 | 这个任务可以通过一系列的MapReduce来完成,一个用来聚合重复的页面,一个用来做链接反转。在MapReduce中,一个任务结束后才能开始另一个任务,所以在做链接反转的时候我们不需要考虑PageRank的变化 12 | 13 | 现在考虑这样的情况,我们重新爬取了一部分小的网页。要想计算新的PageRank,我们必须对整个集合做MapReduce,而不是新的这一部分增量。从而导致任务的规模是与整个仓库而非新增加的网页成比例的 14 | 15 | 我们可以通过把仓库存储在DBMS中,然后通过事务来帮助我们维护不变量(即多个副本指向PageRank最高的内容等)。虽然Bigtable有了可以处理这么多数据的能力,但是bigtable不能保证对于并发的操作去维护invariants 16 | 17 | 所以我们需要一个可以用来做增量处理的系统,我们可以每次爬取小部分的数据,然后可以并发的去更新他们 18 | 19 | Percolator提供了SI的隔离级别。同时提供了observers。当系统发现一些用户指定的列变更的时候,他就会调用observer,每一个observer会完成他对应的任务,并且得到的结果会通过写入table从而导致下游的observer继续执行任务 20 | 21 | # Design 22 | 23 | Percolator提供了两种抽象,一个是为分布式系统提供了ACID保证的事务,一个是observer,用来做增量计算 24 | 25 | ![20220407143742](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220407143742.png) 26 | 27 | Percolator由三个东西组成,Percolator worker,Bigtable tablet server以及GFS chunk server 28 | 29 | observer会通过RPC来在Bigtable上读写,然后Bigtable从而通过RPC来将数据存储在GFS上 30 | 31 | 这个系统还依赖两个小的设施,timestamp oracle以及lightweight lock service 32 | 33 | timestamp oracle用来保证实现严格递增的timestamp,从而在实现SI中使用到 34 | 35 | worker通过使用lightweight lock来使搜索dirty notifications更加高效(这块不明白,后面再看看) 36 | 37 | Percolator的设计点在于在大规模机器上的执行,以及没有对latency的严格要求。从而让我们可以使用一个lazy的方法来清理transaction带来的锁。这种方式让transaction的提交时间延迟了数十秒,但是在indexing system中是可以忍受的。percolator没有一个中心化的事务管理位置,并且没有一个全局的死锁检测器。这会导致延迟的进一步增加,但是同时也允许我们在大规模数据上进行扩展。 38 | 39 | ## Transactions 40 | 41 | ![20220407172014](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220407172014.png) 42 | 43 | 一个通过hash进行聚集重复的例子 44 | 45 | ![20220407201934](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220407201934.png) 46 | 47 | SI的一个例子,读取是看start timestamp,而写入则是在commit timestamp上 48 | 49 | Percolator中的节点会直接更改bigtable中的状态,而且没有一个地方可以让我们很方便的维护锁。所以Percolator会显式的维护锁,并且保证锁不会受到机器故障的影响 50 | 51 | ![20220407204251](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220407204251.png) 52 | 53 | ![20220407204315](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220407204315.png) 54 | 55 | ![20220407204327](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220407204327.png) 56 | 57 | ![20220407204417](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220407204417.png) 58 | 59 | transaction的构造函数会获得一个通过oracle获得一个timestamp作为start timestamp。用来决定Get的可见性。对于Set的操作会被缓存到write集合中,在commit的时候再去应用 60 | 61 | 对于commit写操作实际上是一个两阶段提交的操作,client用来做coordinater 62 | 63 | 在第一个阶段(prewrite)中,我们会锁定所有的元组,同时为了防止client的失效,我们会指定任意一个键作为primary。事务会首先读取每个要写的元组上的元数据,用来检测冲突。 64 | 65 | 如果有人在当前事务开始之后成功写入了该元组,或者有人已经锁上了这个元组,那么就会出现写写冲突,我们会中止当前事务。 66 | 67 | 如果没有冲突的话,我们就会在start timestamp上写入lock 68 | 69 | 如果没有元组产生了冲突,我们就会进入第二阶段,即提交阶段。首先我们会从oracle中获得一个timestamp作为commit timestamp。然后对于每一个写操作,我们会释放对应的锁,并写入一个指针指向真正数据的位置(是在prewrite阶段写入的数据)。一旦primary完成了写操作,那么这个事务就必须被提交 70 | 71 | 对于Get操作,他首先会检查[0, timestamp]中的lock,如果有lock说明有人在写入,那么我们就要等待直到他写入完成(因为我们要保证看到的是一致的快照,其实还是因为分布式情况下我们没法获得当前的活跃事务),大于timestamp的lock,无论如何我们都看不到,所以无所谓 72 | 73 | 如果client在提交的时候失效了,那么锁就会一直放在那,我们需要让Percolator去有方法清除这些锁。Percolator使用了一种lazy的方法,如果A发现了B的一个锁,他就可能认为B失效了,并且清除他对应的锁 74 | 75 | 然而A很难去很好的判断B是否失效了。所以我们必须防止只是A认为B失效了,而B实际没有失效这种情况。所以Percolator指定了primary,执行清除或者提交都需要我们在primary上进行操作。所以同时只会有一个操作成功。因此我们不会出现竞争的问题。 76 | 77 | 如果client在写入primary后失效了,我们就必须执行向前滚动。后来的事务可以通过检查对应的primary上的数据来决定是否向前滚动。对于向前滚动的操作,就是写入对应的write record 78 | 79 | Percolator通过chubby判断worker是否活跃来确定事务是否被worker执行。同时他会在lock中写入wall time,如果一个锁的wall time很老,他也会被清理掉。而对于long-running commit的情况,他们会周期性的更新wall time。 80 | 81 | ## Timestamps 82 | 83 | oracle server必须要可以scale,因为我们整个系统都依赖oracle server分配时间戳 84 | 85 | oracle server每过一段时间会从磁盘中分配一个范围的timestamp,这样后续的处理就可以直接在内存中进行。 86 | 87 | 并且每个worker会将timestamp的请求进行批处理,这样一次可以请求多个timestamp,并且不会受到RPC开销的影响 88 | 89 | Percolator是怎么保证我们每次读都是读到的一致性快照呢?可以这样考虑: 90 | 如果我们能够读到一个事务的写操作,那么就有Tw < Tr,即读操作的start timestamp要大于写操作的commit timestamp。而Tw只有在所有的锁都写入成功后才会请求到,所以我们可以在分配Tr的时候,Tw对应的锁都已经写入了,所以后续的读取就一定可以读到所有Tw的写入。 91 | 92 | 后面的这个Notifications用处不大,就不在这里说了。主要目的是做增量计算用,而不是去维护数据的完整性 93 | -------------------------------------------------------------------------------- /distribute/raft/notes.md: -------------------------------------------------------------------------------- 1 | 1 2 | 3 | 通过算法分解(leader选举,日志复制和安全)和减少状态机的状态来提升Raft的可理解性 4 | 5 | Raft独特的特性 6 | 7 | 1. 强leader: 日志只从leader发送给其他的服务器 8 | 2. leader选举: 使用随机计时器来选举领导人,在解决冲突的时候更加简单 9 | 3. 成员关系调整: 使用共同一致(Joint Consensus)的方法来处理集群成员变化的问题,处于调整过程中的两种不同配置的集群中大多数会有重叠,让我们可以在集群变化的时候保证可用性 10 | 11 | 2 Replicated state machine 12 | 13 | ![20220210080131](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220210080131.png) 14 | 15 | 在一组服务器上的状态机具有相同状态的副本,并且在一些机器宕掉的情况下也可以继续使用。比如一些大规模的系统通常有一个集群leader,在一个独立的复制状态机中去管理leader选举和存储配置信息,比如chubby和zookeeper 16 | 17 | replicated state machine通常是基于复制日志实现的,每一个服务器存储一个包含一系列指令的日志,并根据日志的顺序进行执行 18 | 19 | 一致性算法的任务就是保证复制日志的一致性,服务器上的一致性模块接收客户端发送的指令,然后添加到自己的日志中。它和其他的服务器上的一致性模块进行通信来保证每个服务器上的日志最终都以相同的顺序包含相同的请求。一旦指令被正确的复制,每个服务器的状态机按照日志顺序处理他们,然后将输出结果返回给客户端。这样服务器集群就(看起来)形成了一个可靠的状态机 20 | 21 | 实际系统中使用的一致性算法通常有以下特性: 22 | 23 | * 安全性保证(绝对不会返回一个错误的结果) 24 | * 可用性: 集群中只要有大多数的机器可运行并且能够相互通信,和客户端通信,就可以保证可用 25 | * 不依赖时序来保证一致性: 物理时钟错误或者极端的消息延迟在最坏情况下才会导致可用性问题 26 | * 一条指令可以在集群的大多数节点响应一轮RPC时完成,小部分比较慢的节点不会影响系统的整体性能 27 | 28 | 5 29 | 30 | Raft通过选举一个leader,然后给予他全部的管理复制日志的责任来实现一致性。leader从客户端接受日志并复制到其他的服务器上,并且告诉其他的服务器什么时候可以安全的将日志应用到他们的状态机中。拥有一个leader可以大大简化对复制日志的管理。当leader发生故障,或者出现网络分区时,一个新的leader会被选举出来 31 | 32 | ![20220210090840](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220210090840.png) 33 | 34 | ![20220210091057](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220210091057.png) 35 | 36 | ![20220210091356](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220210091356.png) 37 | 38 | ![20220210091413](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220210091413.png) 39 | 40 | ![20220210092223](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220210092223.png) 41 | 42 | 5.1 基础 43 | 44 | 服务器状态以及他们的转化关系 45 | 46 | ![20220210092803](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220210092803.png) 47 | 48 | 提一下从Leader变成Follower这种情况,比如现在我们有一个leader,然后发生了network partition,这个leader与其他的服务器失去了联系,其他的服务器就会选举出一个新的leader。然后当network partition恢复的时候,老的leader会发现这个新的leader拥有更高的任期,他就会自己变成follower 49 | 50 | ![20220210093119](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220210093119.png) 51 | 52 | 每个任期开始都是一次选举,然后就是正常的操作。有的时候选举会失败,那么这个任期就会因为没有leader而结束 53 | 54 | Raft可以保证在一个任期中最多只有一个领导人 55 | 56 | 任期在Raft中充当逻辑时钟的作用,使得服务器可以检测一些过时的信息:比如过时的leader。每次服务器通信的时候都会交换当前的任期号,如果一个服务器的任期号比其他人小,那么他就会更新自己的编号到较大的编号值。如果一个节点接收到一个包含过期任期号的请求,那么他就会直接拒绝这个请求 57 | 58 | 5.2 leader选举 59 | 60 | leader周期性的向所有的跟随者发送心跳包(不包含日志项的AppendEntries RPC)来维护自己的权威。如果一个follower在一段时间内没有收到任何消息(election timeout),他会认为没有leader,并开始一次选举来选择新的leader 61 | 62 | follower首先增加他的currentTerm,然后变成候选人状态。接着他为自己投票并向其他的服务器发送RequestVote RPC。直到(a)他赢得了选举,(b)其他的server赢得了选举,(c)一段时间后没有winner) 63 | 64 | 在一个任期中,一个server最多投一次票(存储在persistent storage中)。一旦一个服务器赢得了选举,他就会变成leader并向其他的服务器发送心跳包 65 | 66 | 在等待投票的时候,如果一个候选人收到了AppendEntries RPC,如果这个leader的任期号不小于当前候选人的任期号,那么这个候选人就会认为这个leader是合法的,并退回到follower状态。如果leader的任期号小于当前候选人的任期号,那么候选人就会拒绝这次RPC 67 | 68 | 还有一种情况就是split votes,有可能有多个候选人,导致我们找不到一个候选人有大多数的票。这时每个候选人都会超时并开启新的一轮的选举。Raft通过随机化超时的时间来避免split votes的情况。大多数时间只会有一个server超时,所以他会赢得选举并在其他人超时之前发送心跳包。 69 | 70 | 5.3 日志复制 71 | 72 | ![20220210102435](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220210102435.png) 73 | 74 | 在leader将创建的日志条目复制到大多数的服务器上的时候,日志条目就会被提交 75 | 76 | leader跟踪的最大的将会被提交的日志的索引,并且索引值会被包含在RPC中,这样其他的server可以知道leader的提交位置。一旦follower知道一条日志条目已经被提交,那么他就会将这个日志应用到本地的状态机中 77 | 78 | figure3中的log matching property,Raft保证: 79 | 80 | * 如果不同的日志中的两个条目拥有相同的索引和任期号,他们这两个条目包含的指令相同 81 | * 如果不同的日志中的两个条目拥有相同的索引和任期号,那么他们之前所有的日志条目也都完全相同 82 | 83 | 第一个特性来自于这样一个事实,leader最多在一个任期内在指定的一个日志索引位置创建一条日志条目。所以如果同位置的情况下任期相同,说明他们的日志也相同 84 | 85 | 第二个特性由AppendEntries RPC的一致性检查所保证,在发送AppendEntries RPC的时候,leader会把新的日志条目前一个条目的索引位置和任期号包含在日志内。如果follower在他的日志中找不到包含相同索引位置和任期号的条目,那么他就会拒绝接收新的日志条目。每一次我们Append都保证前一个条目相同,所以我们也就保证了前面所有的日志条目都完全相同 86 | 87 | 当leader崩溃的时候,就可能导致日志处于不一致的状态 88 | 89 | ![20220210104505](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220210104505.png) 90 | 91 | 当一个leader当选时,follower可能是下面的任何情况。a,b是缺少一些日志,c,d是有一些未提交的日志条目,e,f则是都存在。 92 | 93 | 比如f中,某个服务器是任期2的leader,他附加了一些日志条目到自己的日志中,但是在提交前崩溃了,他恢复以后又赢得了选举,成为任期3的leader,附加了一些日志以后又崩溃了 94 | 95 | Raft中leader通过强制follower直接复制自己的日志来处理不一致的问题,这意味着follower中冲突的日志会被leader的日志覆盖 96 | 97 | 要让follower的日志和自己一致,leader必须找到最后两者达成一致的地方,然后删除follower在那个点之后的所有日志条目,并发送自己在那个点之后的日志给follower 98 | 99 | leader为每个follower维护了一个nextIndex,表示下一个需要发送给follower的日志的索引。当一个leader刚上任的时候,他会初始化所有的nextIndex为自己的最后一条日志的index + 1,如果一个follower的日志和leader不一致,那么下次RPC时的一致性检查就会失败。leader就会减小nextIndex并进行重试,最终他们会在某个位置上达成一致。一旦AppendEntries RPC成功,那么follower的日志就会和leader保持一致,并在接下来的任期内一直保持 100 | 101 | 一个优化是的当follower检测到冲突的任期的时候,他可以返回冲突任期对应的最小的日志索引号,这样可以一次跳过一个冲突任期中所有的日志条目 102 | 103 | leader从不会覆盖或者删除自己的日志 104 | 105 | 5.4 安全性 106 | 107 | 当Leader提交了一些日志的时候,可能follower会进入不可用状态。之后这个follower可能被选举为leader并覆盖了这些日志条目,就出现了不同的机器执行不同的指令序列的情况。 108 | 109 | 通过在选举leader的时候添加一些限制来完善Raft。从而保证任何leader对于给定的任期号,都拥有了之前任期所有被提交的日志条目。 110 | 111 | Raft保证选举的时候的新的leader拥有所有之前任期中已经提交的日志条目,而不需要传送这些日志条目给leader。这意味着日志条目的传送是单向的,只从leader传送到follower 112 | 113 | 候选人为了赢得选举必须联系集群中的大部分节点,这意味着每一个已经提交的日志条目在这些服务器节点中的其中一个之上。如果候选人的日志至少和大多数服务器节点一样新,那么他一定拥有了所有已经提交的日志条目。RequestVote RPC中包含了候选人的日志信息,然后投票人会拒绝掉那些日志没有自己新的投票请求 114 | 115 | Raft通过比较两份日志中最后一条日志条目的索引值和任期号来定义谁的比较新。如果两份日志最后的条目的任期号不同,那么任期号大的日志更新。如果任期号相同,那么日志比较长的那个就更新。 116 | 117 | ![20220210111955](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220210111955.png) 118 | 119 | 如果一个leader在提交日志之前崩溃了,未来后续的leader会继续尝试复制这条日志。然后一个leader不能断定之前任期里的日志被保存在大多数的服务器上的时候就一定已经提交了。 120 | 121 | 上面的图就展示了一种情况,黄色的日志虽然已经被复制到的大多数的server上,但是还是有可能被蓝色日志覆盖的 122 | 123 | 为了防止这样的问题发生,Raft不会去尝试在当前任期通过计算副本的数量来commit日志。在c情况中,任期4的时候,尽管2号日志已经有了大多数的副本,但是他不会被commit。对于当前任期的日志,如果他被提交了,那么之前的那些日志也都会被间接的提交(通过Log Matching Property)。所以Leader就可以安全的声称之前的log也都被commit了 124 | 125 | 5.5 follower崩溃 126 | 127 | 由于Raft的RPC都是幂等的,所以当follower崩溃后leader就会进行无限的重试,并且重试不会造成任何问题 128 | 129 | 5.6 时间 130 | 131 | 我们要保证broadcastTime ≪ electionTimeout ≪ MTBF(平均故障时间) 132 | 133 | 这个比较容易理解,广播时间要小于选举超时时间,不然很容易出现服务器开始新的选举 134 | 135 | 当leader崩溃后,系统会在选举时间内不可用,所以选举时间不能很长 136 | 137 | 6 集群成员变化 138 | 139 | ![20220210120011](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220210120011.png) 140 | 141 | 在集群添加新机器的时候,我们就有可能出现两个leader 142 | 143 | Raft中,在切换的时候我们需要先切换到一个过度状态,叫做共同一致 144 | 145 | 共同一致是新老配置的结合: 146 | 147 | * 日志条目被复制给集群中新,老配置所有的服务器 148 | * 新,老配置的服务器都可以成为leader 149 | * 达成一致(选举和提交)需要分别在两种配置上获得大多数的支持 150 | 151 | ![20220210120351](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220210120351.png) 152 | 153 | 当一个leader接收到从C-old改变配置到C-new的时候,他会将C-old-new作为log发给其他的副本。一旦一个服务器将新的配置放到他的log中,他就会用这个配置(无论这个配置有没有提交)。leader会使用C-old-new中的规则来决定什么时候去提交这个配置。 154 | 155 | 如果leader崩溃了,一个新的leader可能会有C-old或者C-old-new来作为配置 156 | 157 | 当C-old-new提交了之后,我们可以保证新的leader已经有C-old-new的log了。然后我们可以创建C-new的log并提交他。 158 | 159 | 在图中可以看到,没有C-old和C-new可以同时进行决策的时候,所以可以保证安全性 160 | 161 | 新的服务器可能没有存储任何的日志,当新的服务器加入集群时,他们需要一定的时间去追赶,这时候还不能提交新的日志条目。所以Raft使用一种额外的阶段,在这个阶段,新的服务器会接收日志,但没有投票权。直到他们追上了其他机器 162 | 163 | 集群的leader可能不是新配置的一员,在这种情况下,leader会在提交了C-new以后回到follower状态。因为在C-new提交的时候,是最早的新的配置可以独立工作的时间 164 | 165 | 移除不在C-new中的服务器可能会扰乱集群,这些服务器不再收到心跳,所以当超时时他们会开始新的选举。新的leader会被选举出来,但是这些服务器会再次超时。为了避免这个问题,当服务器确认当前leader存在时,服务器将会忽略RequestVote RPC。当服务器在当前的最小选举时间内收到一个RequestVote RPC,他会忽略这个请求。 166 | 167 | 7 日志压缩 168 | 169 | ![20220210150238](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220210150238.png) 170 | 171 | Raft的快照主要包括将状态机的状态写入到快照中。最后被包含的索引以及最后被包含的任期用来做一致性检查。为了支持集群成员更新,也将最后一次配置存储下来。一旦服务器完成一个快照,他就可以删除最后索引位置之前的所有日志和快照了 172 | 173 | 当leader已经删除了下一条需要发送给follower的日志时,他就需要将快照发生给他们 174 | 175 | ![20220210150538](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220210150538.png) 176 | 177 | follower可以在不知道leader的情况下创建快照。因为创建快照的时候一致性已经达成,这时候不存在冲突了。 178 | 179 | 通过COW来解决创建快照时可能导致的阻塞问题 180 | 181 | 8 客户端交互 182 | 183 | Raft的目标是实现线性化语义(每一个操作立即执行并且只执行一次,在他调用和回复之间)。但是我们有可能执行同一个命令多次。比如Raft提交一个日志以后,但是在响应客户端之前崩溃了。那么客户端和新的leader会重试这条指令,导致这条指令被再次执行了。解决方法是客户端对每一条指令都赋予一个唯一的序列号,如果接收到相同的指令,我们就可以立即返回结果 184 | 185 | 只读的操作可以直接处理而不需要记录日志。但是在没有限制的情况下,我们可能读到过期的数据(一个old leader恢复以后接受到了新的客户端的读请求,他就会返回过期的数据) 186 | 187 | 为了解决这个问题,Raft使用额外的措施:leader必须有关于被提交的日志的最新信息,在leader任期开始的时候,他可能不知道那些是被提交的。他在他的任期里提交一条空白的日志条目来实现这一点。leader在处理只读的请求之前必须检查自己是否已经被废除了。Raft通过让leader在响应只读请求之前,先和集群中的大多数节点交换一次心跳信息来处理这个问题 188 | 189 | -------------------------------------------------------------------------------- /distribute/raft/raft-extended.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ysj1173886760/paper_notes/df11876635eaa0da706671f51f6edd3b42e5391c/distribute/raft/raft-extended.pdf -------------------------------------------------------------------------------- /distribute/session_guarantee_for_weak_consistent_replicated_data/SessionGuaranteesPDIS.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ysj1173886760/paper_notes/df11876635eaa0da706671f51f6edd3b42e5391c/distribute/session_guarantee_for_weak_consistent_replicated_data/SessionGuaranteesPDIS.pdf -------------------------------------------------------------------------------- /distribute/session_guarantee_for_weak_consistent_replicated_data/notes.md: -------------------------------------------------------------------------------- 1 | 这个paper提出了session guarantee从而可以避免弱一致性级别带来的问题,同时还可以保持弱隔离级别的优势 2 | 3 | A session is an abstraction for the sequence of read and write operations performed during the execution of an application 4 | 5 | 提出session的目的不是为了和事务对应(事务是用来保证ACID的),session的目的则是为了给用户提供一个一致性的视角。 6 | 7 | 贴一下原文:Sessions are not intended to correspond to atomic transactions that ensure atomicity and serializability. Instead, the intent is to present individual applications with a view of the database that is consistent with their own actions, even if they read and write from various, potentially inconsistent servers. 8 | 9 | 我们希望在一个session中的操作可以像是在一个服务器上进行的一样 10 | 11 | 在session的基础上,提供了四个保证: 12 | * Read Your Writes - read operations reflect previous writes 13 | * Monotonic Reas - successive reads reflect a non-decreasing set of writes 14 | * Writes Follow Reads - writes are propagated after reads on which they depend 15 | * Monotonic Writes - writes are propagated after writes that logically precede them 16 | 17 | 我们可以通过在一些弱一致性的系统上构建一个层来提供上面的这些保证。我们通过让应用操作的这些服务器是足够up-to-date的,来提供上面的这些保证 18 | 19 | # Data storage model and terminology 20 | 21 | 假设下层的系统: Such a system consists of a number of servers that each hold a full copy of some replicated database and clients that run applications desiring access to the database. 22 | 23 | session guarantees适用于那些客户和服务器处于不同的机器上,并且客户的访问会访问到不同的机器中的场景 24 | 25 | 比如一个移动客户端可能会根据他的地区来选择server 26 | 27 | 每一个写操作都有一个全局独一的标识符,称为WID 28 | 29 | 定义DB(S, t)为服务器S在t时间及以前收到的写操作的有序序列。概念上,服务器S从一个空的数据库开始,然后根据顺序apply接受到的写操作,与此同时处理读请求。现实中,服务器可以按照不同的顺序应用写操作,只要他们的效果是不变的就行(我猜测,可能是类似tomas写规则那样的?)。并且DB(S)中写操作的序列并不代表是他们接受到写操作的顺序(也就是说不是先接受就先应用,应该是根据WID来的) 30 | 31 | 我们假设底层的系统提供了最终一致性,那么也就提供了能够保证最终一致性的机制,即: total propagation和 consistent ordering 32 | 33 | 写操作通过反熵(anti-entropy)来在服务器之间进行传播,这个操作有的时候也叫做rumor mongering, lazy propagation, update dissemination。反熵保证了每一个写操作最终都会被每一个服务器接收到。(反熵带来了total propagation) 34 | 35 | 对于不可交换的操作(比如覆盖写),所有服务器必须有相同的应用顺序。形式化的说:定义WriteOrder(W1, W2)为一个布尔谓词,代表W1是否应该在W2之前。系统保证了如果有WriteOrder(W1, W2),那么对于任何的接收到W1和W2的服务器来说,W1被排序到W2之前。这篇文章对于系统如何保证Order没有任何的假设,他只假设了在每台服务器上写操作都有一个一致的顺序。(比如想要写操作match real time,我们可以用一个TSO来做order,如果没有要求的话可以通过logical lock来做order) 36 | 37 | 最后,弱一致性系统通常允许写冲突,比如两个并发的写同一个数据项。在某些系统中,Write order会决定那个写胜利了,也有的系统会让人来解决这些冲突。系统怎么解决这些冲突对于用户很关键,但是对我们的session guarantees不重要。(我感觉是因为我们的session guarantee说的是一个人的一系列操作能看到的事情,即看到一个一致的数据库,而上面的冲突解决说的是怎么处理并发的操作) 38 | 39 | # Read/Write guarantees 40 | 41 | 这一节就是定义一下,然后给了几个例子 42 | 43 | ## Read Your Writes 44 | 45 | RYW-guarantee: If Read R follows Write W in a session and R is performed at server S at time t, then W is included in DB(S, t) 46 | 47 | ## Monotonic Reads 48 | 49 | We say a Write set WS is `complete` for Read R and DB(S, t) if and only if WS is a subset of DB(S, t) and for any set WS2 that contains WS and is also a subset of DB(S, t), the result of R applied to WS2 is the same as the result of R applied to DB(S, t) 50 | 51 | 即对于R来说,WS是包含R中所有元素的最新的写操作的集合 52 | 53 | Let RelevantWrites(S, t, R) denote the function that returns the smallest set of Writes that is complete for Read R and DB(S, t) 54 | 55 | MR-guarantee: If Read R1 occurs before R2 in a session and R1 accesses server S1 at time t1 and R2 accesses server S2 at time t2, then RelevantWrites(S1, t1, R1) is a subset of DB(S2, t2) 56 | 57 | 也就是说t2时刻的S2至少要包含t1时刻S1中关于R1的写操作 58 | 59 | ## Writes Follow Reads 60 | 61 | 这个我的理解就是因果一致性了(类似因果一致性但是不是,后面会说) 62 | 63 | WFR-guarantee: If Read R1 precedes Write W2 in a session and R1 is performed at server S1 at time t1, then for any server S2, if W2 is in DB(S2), then any W1 in RelevantWrites(S1, t1, R1) is also in DB(S2) and WriteOrder(W1, W2) 64 | 65 | 解释一下就是如果读后写的时候,对于新的写操作应用在服务器上的时候,要保证之前读到的那些值也已经被应用了,并且之前的写要排序在新的写操作之前。 66 | 67 | 这个guarantee和之前的区别在于他是在说session之外的事情,即其他的client应该也能看到相同的写操作的顺序 68 | 69 | 对于这个保证,其实我们是保证了两件事。一个是写操作的顺序,我们保证有相关性的写操作是有序的。第二个则是传播的顺序,我们保证只有当看到了依赖的写操作后,才能看到之后的写操作。 70 | 71 | WFRO(Order)-guarantee: If Read R1 precedes Write W2 in a session and R1 is performed at server S1 at time t1, then WriteOrder(W1, W2) for any W1 in RelevantWrites(S1, t1, R1) 72 | 73 | WFRP(propagate)-guarantee: If Read R1 precedes Write W2 in a session and R1 is performed at server S1 at time t1, then for any server S2, if W2 is in DB(S2) then any W1 in RelevantWrites(S1, t1, R1) is also in DB(S2) 74 | 75 | 其实就是把上面的这个拆开了,一个保证的是WriteOrder(W1, W2),另一个保证的则是if W2 is in DB(S2), then W1 is also in DB(S2) 76 | 77 | ## Monotonic Writes 78 | 79 | MW-guarantee: If Write W1 precedes Write W2 in a session, then, for any server S2, if W2 in DB(S2) then W1 is also in DB(S2) and WriteOrder(W1, W2) 80 | 81 | 就是写操作要有序并且按序传播 82 | 83 | 防止immortal write 84 | 85 | 在某些情况下,MW可以通过WFR和RYW推出。但是有的时候不能,比如操作W1 R W2,对于MW来说,W1应该在W2前面,但是对于WFR和RYW来说,R有可能读到了其他的写操作Wx,那么因果就变成了Wx happen before W2了,而W1和W2就没有了关系 86 | 87 | 通过jepsen的这个图来想一下 88 | 89 | ![20220419151234](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220419151234.png) 90 | 91 | PRAM是pipelined random access memory,指的是读写操作是向流水线一样产生效果的,所以我们天然的就有了MW,MR以及RYW,因为读写操作不会沿着流水线往回走,所以他们只会越来越新 92 | 93 | PRAM没有对跨越session的操作做任何保证,所以我们加上WFR,来保证跨session的操作的因果一致性,就得到了Causal consistency。那么WFR和Causal的区别是什么呢?就是对于单个session的操作的顺序的保证。因为WFR只保证了读写依赖的情况下的顺序,但是没有保证单个session情况下的读写顺序,而causal consistency是保证了的。更准确的来说,应该是happen before的关系 94 | 95 | 而对于sequential来说,则没有什么更多额外的保证,只是在不可比较的事件之间提供了一个全序关系,让我们可以排序没有因果的事件。那么有个问题就是,sequential是不是就没什么作用了,毕竟他也没提供real time的保证,只是排序了不相关的东西。其实不是,因为当我们无法排序不相关的事件的时候,那么我们就无法保证某一时刻数据库的一致性,比如两个写操作同时写入同一个数据项,怎么确保谁先谁后呢?就算是我们有确定的手段确保这个顺序,还是会有异常。比如我们某一时刻的读可能在两个不同的服务器上读到的是不同的状态,那么我们之后所做的抉择就取决于去那个服务器上读了,也是一个比较奇怪的现象。所以目前的系统如果可以都会提供这样的全序关系。 96 | 97 | # Providing the guarantees 98 | 99 | 有一个session manager负责维护信息,并负责与服务器通信 100 | 101 | 服务器需要提供给我们新的写操作对应的WID,读操作读到的数据对应的WID,以及当前数据库中所有的WID 102 | 103 | session manager维护了两组集合 104 | 105 | read-set = set of WIDs for the Writes that are relevant to session reads 106 | write-set = set of WIDs for those writes performed in the session 107 | 108 | RYW: 每次写操作被接受的时候,我们把返回的新的WID加入到write-set中,每次读的时候,session manager要检查write-set是当前数据库DB(S, t)的子集 109 | 110 | MR:每次读取的时候,session manager要保证read-set是DB(S, t)的子集,并且每次读取结束以后,要把读到的数据对应的RelevantWrites(S, t, R)加入到读集中 111 | 112 | 实现WFR和MW需要我们添加两个限制: 113 | C1. 当服务器S在t时刻接受一个新的写操作W2的时候,他要保证对于现在数据库中的所有写W1,都有WriteOrder(W1, W2). 即新的写操作要保证被排序到已有的写操作后面 114 | C2. 在t时刻进行反熵,把W2从S1传播到S2的时候,我们要保证对于DB(S1, t)中所有的满足WriteOrder(W1, W2)的写操作W1,都已经被传播到了S2中。 即传播是按序的 115 | 116 | 个人的想法:其实这里的C2间接的让WFRP依赖于WFRO了,也就是传播顺序依赖于Ordering。其实应该有其他的方法去避免,但是我们这里没有细分WFR,所以就直接结合起来了。从这里也可以看出来ordering在propagating中的重要性 117 | 118 | 实际上,我们并不需要对于DB中所有的写操作都这样执行,而是只要保证session中的read-set和write-set中的写操作遵守这个规则就可以,因为有的写可以不走session。但是这需要服务器额外追踪读写集,不如全部满足更好 119 | 120 | 幸运的是,很多的系统都满足这样的条件。对于新的写操作来说,他应该被放置在已有的写操作的后面。如果我们通过timestamp来计算WriteOrder这个谓词的话,那么我们只要保证新的写操作的谓词在已有的后面即可。 121 | 122 | WFR:和MR中相同,我们保证每次读取结束以后把对应的RelevantWrites(S, t, R)加入到读集中,然后在写的时候,我们保证当前服务器S1的DB(S1, t)包含read-set 123 | MW:每次服务器接受一个写操作的时候,他的DB(S, t)必须已经包含了当前的write-set。并且返回的WID要被加入到write-set中 124 | 125 | ![20220419211131](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220419211131.png) 126 | 127 | # Practical implementation of the guarantees 128 | 129 | 上一节说到做法可能会导致一些问题: 130 | * WID的集合可能很大 131 | * 读操作对应的WID集合可能很大 132 | * 在检查读写操作时候使用的WID集合可能很大 133 | * 服务器记录WID的信息可能很大 134 | * 查找一个有效的服务器可能会很费时 135 | * 记录读操作对应的写操作这个过程可能会很多 136 | 137 | 这一节通过version vector来解决大多数的问题 138 | 139 | 每次写的时候,我们递增clock字段,这样(server, clock)就可以作为WID使用 140 | 141 | invariant: if a server has (S, c) in its version vector, then it has received all Writes that were assigned a WID by server S before or at logical time c on S's clock. 142 | 143 | 根据这个invariant,服务器需要保证他们在反熵的时候,传播的顺序是根据WID的顺序来的。即当我们有(S, c)的数据的时候,之前的数据也应该被传过来。同时反熵的过程也会更新服务器的version vector 144 | 145 | 要为一组写集Ws获得一个version vector。我们令V[S] = the time of the latest WID assigned by server S in Ws(or 0 if no Writes are from S) 146 | 147 | 要获得两个写集的version vector的交集,我们只需要对于每一个服务器S,让V[S] = MAX(V1[S], V2[S])即可 148 | 149 | 要检查两个集合是否存在包含关系,我们只需要判断一个vector是不是dominates另一个,即对于每一个元素都存在大于或等于的关系 150 | 151 | 个人的想法:这样的话我们应该需要用logical clock作为version vector中的clock,否则无法满足新的写操作被排序在已有的写操作的后面。并且还有一个额外的问题,就是反熵的时候,我们不仅需要保证同一个服务器中的数据是按序的,还需要保证跨服务器的有依赖的写操作也是按序传播的 152 | 153 | 并且我们无法很好的计算出读请求的RelevantWrites,因为这需要我们跟踪每个元组对应的version vector。所以我们可以让当前服务器的version vector作为这次读操作的RelevantWrites的一个近似 154 | 155 | ![20220420095316](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220420095316.png) 156 | 157 | 通过version vector简化了实现 158 | 159 | 这里是我自己想的一个反熵的算法,目的是为了保证传播顺序 160 | 161 | 因为version vector可以天然的帮我们保证一个服务器上的写是按序传播的。但是不保证不同服务器上有依赖的写是按序的。比如我们有两个写对应的是,先在S1上写,然后到S2上,发现S1的写被传播过来,然后再写。这时候我们就需要保证MW,即在S2传播给其他服务器,比如说S3的时候,要保证首先把S1的写传过去,再传S2的写。 162 | 163 | 首先通过logic clock保证C2 > C1,即WriteOrder(,) 164 | 165 | 然后在反熵的过程中,S2会给S3传数据,首先他会要求获得S3的version vector 166 | 167 | 然后他会尝试传播属于自己的写操作,即这个数据。在传播之前检查,S3的version vector里是否满足了对于所有的满足WriteOrder(, )的数据项,他们是否已经被传播到S3中了。 168 | 169 | 换句话说,我们不希望存在一个S2中的数据项,使得,WriteOrder(, )且 > where from S3 170 | 171 | 实现上来说,我们可以简单的对比S2和S3的version vector,如果他小于S3肯定是没问题的,但是对于某一些服务器对应的项大于S3的version vector的,同时这个项是小于我们要传播的项的,我们就可以把他放到前面,保证写操作的按序的 172 | 173 | 对于存储对应的数据,可能要通过logging,或者让WID作为主键直接查找。logging更容易一些,但是需要我们做GC。什么时候GC也是一个要考虑的问题 -------------------------------------------------------------------------------- /distribute/time_clocks_and_ordering_of_events/Time-Clocks-and-the-Ordering-of-Events-in-a-Distributed-System.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ysj1173886760/paper_notes/df11876635eaa0da706671f51f6edd3b42e5391c/distribute/time_clocks_and_ordering_of_events/Time-Clocks-and-the-Ordering-of-Events-in-a-Distributed-System.pdf -------------------------------------------------------------------------------- /distribute/time_clocks_and_ordering_of_events/notes.md: -------------------------------------------------------------------------------- 1 | 这篇论文讲的就是lamport的logic lock 2 | 3 | # The Partial Ordering 4 | 5 | 如果一个事件A发生早于B,大多数人会说A发生在B之前。因为他们会用物理时间来证明这个定义。然而,如果一个系统要满足规范,那么这个规范就必须根据系统内可观察到的事件给出。如果我们的规格是物理时间,那么系统必须包含真正的时钟,而且就算是他包含了真正的时钟,我们也会遇到时钟不准确的可能性。所以我们不通过物理时钟定义“happened before“ 6 | 7 | 我们假设一个系统由若干个进程组成,每个进程包含了具有全序关系的若干个事件。同时将进程间的发送消息和接受消息也定义为事件 8 | 9 | 我们让->表示happend before,然后有如下的定义: 10 | 1. 如果a和b是在同一个进程中的事件,并且a在b的前面,则a->b 11 | 2. 如果a是一个进程发送一个消息,而b是另一个进程接收这个消息,那么a->b 12 | 3. 如果a->b,b->c,则a->c 13 | 14 | 对于两个事件,如果不满足a->b,也不满足b->a,那么说a和b是并发的 15 | 16 | 同时也假设不存在a->a这种情况。所以综上可以发现->是一个自反的偏序关系 17 | 18 | ![20220412101435](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220412101435.png) 19 | 20 | 图中是一个演示,其中点表示了事件,而曲线表示的是消息的发送。通过图可以看出如果a->b,那么我们可以沿着进程线以及消息线按照时间增加的方向从a移动到b 21 | 22 | 另一种看待这个定义的方式是说如果a->b,那么就有可能a对b有因果影响(casually affect),如果两个事件没有因果关系,那么两个事件就是并发的 23 | 24 | 后面他提到了这个定义和狭义相对论的近似。但是狭义相对论中的事件指的是"messages that could be send",而我们的系统中则是"messages that actually are send" 25 | 26 | # Logical Clocks 27 | 28 | 我们定义系统中的clock。对于Ci来说,他就是进程Pi对应的时钟。那么$Ci\langle a \rangle$则代表了对应进程中的事件a的时间。而对于整个系统的始终C以及任意的事件b来说,则有$C\langle b \rangle = Cj\langle b \rangle$ 29 | 30 | 结合之前的符号->,我们可以给出一个定义,对于任意的事件a,b,如果a->b,则$C\langle a \rangle < C\langle b \rangle$ 31 | 32 | 注意对于反过来的情况是不成立的,因为他们可能是并发的关系,而非happen before 33 | 34 | 然后再根据->符号的定义,我们可以得到两个条件: 35 | * C1: If a and b are events in process Pi, and a comes before b, then $C_i \langle a \rangle < C_i \langle b \rangle $ 36 | * C2: If a is the sending of a message by process Pi and b is the receipt of that message by process Pj, then $C_i \langle a \rangle < C_j \langle b \rangle$ 37 | 38 | 然后将clock加入到之前的figure 1中 39 | 40 | ![20220412134726](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220412134726.png) 41 | 42 | 图中的虚线代表了tick line,即每次clock tick 43 | 44 | C1指定了,同一个进程线下的每两个事件之间都必须有一次tick 45 | 46 | 而C2指定了对于每一条message line,都必须穿过一个tick line 47 | 48 | 我们可以将tick line作为时间轴上的坐标线(我不知道咋翻译了,含义就是平行于时间轴的线),然后重新画出figure 2 49 | 50 | ![20220412135504](https://picsheep.oss-cn-beijing.aliyuncs.com/pic/20220412135504.png) 51 | 52 | 现在假设Ci是进程Pi的时钟寄存器,即当事件a发生的时候,Ci的值是$C_i \langle a \rangle$,然后下面说我们怎么实现上面提到的clock 53 | 54 | 对于C1条件来说,我们有: 55 | IR1:Each process Pi increments Ci between any two successive events. 56 | 57 | 对于C2来说,我们要求每个信息m都要包含他的timestamp Tm,也就是发送消息时候的时间。而进程则必须将其时钟调整到大于Tm。具体的: 58 | IR2(a): If event a is the sending of a message m by process Pi, then the message m contains a timestamp Tm = $C_i\langle a \rangle$. (b): Upon receiving a message m, process Pj sets Cj greater than or equal to it's present value and greater than Tm. 59 | 60 | 通过这两个规则,我们就可以让我们的clock满足上面的条件,也就可以追踪到因果关系 61 | 62 | # Ordering the Events Totally 63 | 64 | 对于构造全序关系,我们可以结合用任意的一个进程间的全序关系即可。如果两个事件发生的时间相同,那么就用进程之间的全序关系导出事件的全序关系即可。 65 | 66 | 然后论文中给出了一个通过全序关系解决问题的例子 67 | 68 | 核心就是通过在消息之间传递时间戳,从而传递因果关系。即如果一个进程收到了其他进程的确认信息,那么就可以保证该进程已经知道了所有在他之前的请求。 69 | 70 | 这里的因果有两点,如果进程i确认了进程j的请求,那么进程j后续的请求一定会排在进程i现有请求的后面。如果进程i收到了进程j的请求,那么进程i后续的请求一定会排在进程j现有请求的后面。 71 | 72 | 这样我们就可以构造出两个进程的请求的前后顺序,从而防止出现因果颠倒的情况。 73 | 74 | # Anomalous Behavior 75 | 76 | 这里讲的就是对于系统检测不到的因果关系,我们是没办法去构造一个预期的顺序的。 77 | 78 | 第一种解决的方法就是人工来去指定涉及到外部消息时候的timestamp。 79 | 80 | 而第二种方法则是构造一个新的系统,满足Strong Clock Condition,即可以探测到外部事件的情况。而很棒的一点是,我们是有可能通过相互独立的物理时钟来构造这样的系统的。所以我们可以通过物理时钟来消除掉这些异常现象。 81 | 82 | # Physical Clocks 83 | 84 | 我们在之前的时空图下引入物理时间轴,然后令$C_i(t)$代表在物理时间t下读取Ci的值。为了数学方面的方便,我们假设时钟是连续的,而非是在离散的tick中运行的 85 | 86 | 更准确的说,我们可以假设$C_i(t)$是一个连续可微的函数,那么对应的导数就代表了时钟增加的速率 87 | 88 | 为了确保时钟Ci是一个物理时钟,我们需要确保他在近似正确的速率上运行,即$dC_i(t) / dt \approx 1$ 89 | 90 | 更具体的,我们这样假设: 91 | PC1. There exists a constant $\kappa << 1$, such that for all i: $|dC_i(t)/dt - 1| < \kappa$ 92 | 93 | 对于ctystal controlled clocks,$\kappa \leq 10^{-6}$ 94 | 95 | 每一个clock都独立的运行在正确的速率上还不够,我们还需要保证他们是同步的,即: 96 | PC2. For all i,j: $| C_i(t) - C_j(t) | < \epsilon$ 97 | 98 | 对于figure2来说,如果我们假设tick line代表了物理时间,那么一个tick line在高度上的变化就不能超过$\epsilon$ 99 | 100 | 现在我们考虑,我们需要多小的$\kappa$以及$\epsilon$才能保证不会出现之前提到的异常现象 101 | 102 | 我们首先假设我们的时钟已经满足了之前的要求,即在不出现系统外部的消息的情况下,我们可以保证顺序。现在只需要考虑的情况就是当用之前的规则错误的时候,我们怎么通过物理时钟来保证不会出现这种情况 103 | 104 | 我们假设当出现$a \boldsymbol{\rightarrow} b$(即Strong Clock Condition的条件下)的时候,满足a发生在$t$时刻,而b发生在$t + \mu$时刻后。换句话说就是$\mu$是进程间通信的最小的时间 105 | 106 | 为了防止异常情况的发生,我们需要保证$C_i(t + \mu) - C_j(t) > 0$。然后把这条规则和上面的PC1以及PC2结合到一起,就可以得到一个关系。注意我们假设当clock重置的时候,我们不会让时钟回调(因为要保证C1的成立) 107 | 108 | PC1则指出$C_i(t + \mu) - C_i(t) > (1 - \kappa)\mu$ 109 | 然后根据PC2可以推导出当$\epsilon/(1 - \kappa) \leq \mu$的时候满足 $C_i(t + \mu) - C_j(t) > 0$ 110 | 111 | 然后说一下怎么保证PC2的条件,因为时钟会偏移,所以随着时间流逝他们会偏移的越来越多 112 | 113 | m代表一个消息,从物理时间t发送,并在物理时间t\`接收。我们定义vm = t\` - t,作为消息m的总延迟。这个延迟对于收到消息的进程来说是感知不到的。我们可以假设一个最小延迟$\mu_m \geq 0$,并且$\mu_m \leq v_m$。我们称$\xi_m = v_m - \mu_m$为不可预测的延迟(unpredictable delay) 114 | 115 | 我们现在为物理时钟定义一些规则: 116 | IR1\`: For each i, if Pi does not receive a message at physical time t, then Ci is differentiable at t and $dC_i(t) / dt > 0$ 117 | IR2\`: (a) If Pi sends a message m at physical time t, then m contains a timestamp $T_m = C_i(t)$. (b) Upon receiving a message m at time t\`, process Pj sets Cj(t\`) equal to maximum(Cj(t\` - 0), Tm + $\mu_m$) 118 | 119 | 现在考虑进程之间的关系为一个图,如果Pi到Pj有一条弧,则有一条消息会在每$\tau$秒从Pi向Pj发送一条消息。图的直径d则是对于图中的任意两个点,他们的之间最多有d条弧 120 | 121 | 假设任意一个强连通的图都可以满足IR1\`以及IR2\`。假设对于任意的消息m,有$\mu_m \leq \mu$,以及对于所有的$t \geq t_0$,则有(a) PC1 holds. (b) There are constants $\tau$ and $\xi$ such that every $\tau$ seconds a message with an unpredictable delay less thant $\xi$ is send over every arc. Then PC2 is satisfied with $\epsilon \approx d(2\kappa\tau + \xi)$ for all $t \gtrsim t_0 + \tau d$, where the approximations assume $\mu + \xi \ll \tau$ 122 | 123 | --------------------------------------------------------------------------------