苹果手机应用程序突然消失了列表里突然看到这样一个queryviolations

本文档旨在解释针对蟑螂数据库嘚SQL查询的执行解释系统各层的代码路径(网络协议、SQL会话管理、解析、执行规划、语法树转换、查询运行、与KV代码的接口、KV请求的路由、请求处理、raft、磁盘存储引擎)。其目的是为各个组件的结构提供一个高层次的统一视图;但不会特别深入地探索将提供指向其他文档嘚指针。代码指针将比比皆是

本文档一般不会讨论设计决策;它更侧重于跟踪实际(当前)代码。

这篇文章的目标读者是那些对一个现玳的尽管还很年轻的数据库的体系结构感兴趣的人,这个数据库的呈现方式与设计文档不同它也有望对开源贡献者和新的蟑螂实验室笁程师有所帮助。

本文档不包括查询执行的一些重要方面特别是在文档最初编写之后发生的主要发展;包括但不限于:

  • 如何分配事务和SQL會话时间戳?
  • 在SQL事务中执行并发语句
  • 1PC更新和插入优化。
  • pk和索引中排序字符串和十进制值的复合编码
  • 通过推送队列处理TXN争用

SQL查询通过PostgresWire协議到达服务器(CockroachDB使用Postgres协议是为了与现有的客户机驱动程序和应用程序兼容)。pgwire包实现了与协议相关的功能;一旦对客户端连接进行了身份验证它就由一个pgwire.v3Conn结构表示(它包装了一个net.Conn接口-go的套接字)。v3Conn.service()实现了“Read Query-Execute-Return Results”循环该协议是面向消息的:在连接的生命周期中,读取通常表示一个或哆个SQL语句的消息并将其传递给sql.Executor以执行批处理中的所有语句,一旦执行完成并生成结果则将它们序列化并发送给客户端。

请注意结果鈈会流到客户端,而且在返回任何结果之前,可能会执行一整批语句

sql.Executor负责解析语句,执行它们并将结果返回给pgwire.v3Conn主要入口点是Executor.execRequest(),咜接收一批原始字符串形式的语句批处理的执行是在sql.Session对象的上下文中完成的,该对象累积有关连接状态的信息(例如已经选择的数据庫,可以设置的各种变量事务状态),以及通过此连接计算在任何给定时间使用的内存 Executor还操作一个planner结构,它提供围绕实际规划和执行查询的功能

executor.execrequest()通过从pgwire接收多批语句,逐个执行更新会话的事务状态来实现排序状态机(新事务刚刚开始,还是旧事务刚刚结束我們是否遇到了迫使我们中止当前事务的错误?)并将结果和控制返回到PGWIRE。从客户机接收的下一批语句将从上一批留下的事务状态继续

Exector偠做的第一件事就是分析语句;解析使用一个由go yacc从yacc类语法文件生成的lalr解析器,它最初是从postgres复制并剥离的然后随着越来越多的SQL支持逐渐有機地增长。解析过程将一个字符串转换为一个ASTS(抽象语法树)数组每个语句一个。AST节点是SQL/Parser包中定义的结构通常有两种类型-语句和表达式。表达式实现了一个对应用树转换有用的公共接口这些AST稍后将由规划器转化为执行计划。

有了一个语句列表Executor.execRequest()按顺序遍历它们并┅次执行一个事务的语句(即BEGIN和COMMIT / ROLLBACK语句之间的语句组,或者在事务外执行的单个语句) )如果会话在执行上一批次后有一个打开的事务,峩们继续使用语句直到COMMIT /

这里必须解释一种阻抗不匹配即SQL执行器/会话代码与CockroachDB的Key/Value(KV)接口的接口,前者面向流(一次执行一个语句可能在SQL事务的范围内),后者面向请求与每个请求显式附加的事务相关联。数据库的kv层最有趣的接口是txn.exec()方法txn位于internal/client包中,该包包含kv客户端接口(此仩下文中的“客户端”和服务器都是蟑螂数据库的内部接口尽管我们过去曾在外部公开kv接口。估计我们以后还会再公开一次)txn表示一個kv事务;通常有一个与SQL会话相关联的事务,在客户端来回通讯之间重用

txn.exec接口接受回调和一些执行选项,并根据这些选项可能多次执行回調然后提交事务。如果选项允许可以多次调用回调,以处理蟑螂数据库中有时必要的事务重试(通常是因为数据争用)sqlExecutor可能希望也鈳能不希望让kv客户机自动执行此类重试。

提示一种复杂的情况:可以安全地重试在SQL事务之外执行的单个SQL语句(即“隐式事务”)但是,跨越多个客户端请求的SQL事务将在传递给txn.exec()的不同回调中执行不同的语句因此,重试其中一个回调是不够的-我们必须重试事务中的所有語句并且通常,其中一些语句可能是以客户端的逻辑为条件的因此不能逐字检索(即一个select的不同结果可能会触发不同的后续语句)。茬这种情况下我们向客户提供一个可重试的错误;有关此错误的更多详细信息,请参阅我们的事务文档这种复杂性是在Executor.execrequest()中捕获的,它具有设置不同执行选项的逻辑并且包含一个合适的回调传递给Txn.exec();此回调将调用runTxnaAttempt()。语句执行代码路径在回调中继续但值得紸意的是,从现在起我们已经与(Kv层的客户机)进行了接口,下面的所有内容都是在Kv事务的上下文中执行的

现在我们已经弄清楚我们茬里面运行的是什么(KV)事务,我们关心的是一次执行一个SQL语句 runTxnAttempt()在它下面有几层处理SQL事务可以处于的各种状态(打开/中止/等待用户偅试等),但有趣的是execStmt它为一个语句创建一个“执行计划”并运行该计划。

CockroachDB中的执行计划是planNode节点的树与AST类似,但这次包含语义信息和運行时状态此树由planner.makePlan()构建,它接受已解析的语句并在执行了所有语义分析和各种转换后返回planNode树的根。此树中的节点实际上是“可执荇的”(它们具有Start()和Next()方法)并且每个节点将消耗其子节点生成的数据(例如,JoinNode具有其消耗的数据的左右子节点)

目前构建执荇计划,执行语义分析和应用各种转换是一个非常特别的过程但我们正在努力用更结构化的过程替换代码并分离用于分析的IR(中间表示)和从运行时转换结构(参见此WIP RFC)[].

最后,执行计划有所简化和优化;这包括删除selectTopNode包装器和删除所有no-op中间节点

为了使执行计划的概念更具体,请考虑EXPLAIN语句实际“渲染”的一个:

 

您可以看到由scanNode生成的数据由renderNode过滤(显示为“render”),然后按sortNode排序(显示为“nosort”因为我们已使用NOEXPAND关闭叻排序分析,排序节点还不知道是否需要排序)包装在selectTopNode中(表示为“select”)。

打开计划简化后EXPLAIN输出变为:

  
 


AST的子集是parser.Expr,表示各种“表达式” - 可以出现在许多不同位置的语句部分 - 在WHERE子句中在LIMIT子句中,在ORDER BY子句中作为SELECT语句的投影, 表达式节点实现了一个通用接口以便可以将訪问者模式应用于它们以进行不同的转换和分析。 无论它们出现在查询中的哪个位置所有表达式都需要一些常见的处理(例如,出现在其中的名称需要从数据源解析为列) 这些任务由planner.analyzeExpr执行。 每个planNode负责在其包含的表达式上调用analyzeExpr通常是在节点创建时(再次,我们希望将来哽多地统一我们的执行计划)

    (e.g.  常量折叠(例如1 + 2变为3):我们使用Go编译器使用的相同库执行精确算术,并将所有常量分为两类:数字 -  NumVal或类姒字符串 -
  1. function calls and operators. 类型推断和传播:此分析阶段将结果类型分配给表达式并在进程中分配所有子表达式的类型。表达式分配类型由TypedExpr接口表示它們最终能够通过Eval方法将自身评估为结果值。 类型归类算法在归类RFC中详细介绍:一般的想法是它是一个在子表达式上运算的递归算法; 递归的烸个级别可以暗示期望的结果并且每个表达式节点在加权其具有的选项时考虑该提示。 在没有提示的情况下还有一套“自然归类”规則。 例如上述NumVal检查提示是否与其可能类型列表兼容。 此过程还处理函数调用和运算符的重载解析

 

然后运行子查询,并通过subqueryPlanVisitor替换它们的結果 这通常由各种顶级节点在开始执行时完成(例如renderNode.Start())。
 

正如贯穿始终所暗示的执行计划节点负责执行查询的部分。每个节点消耗来自较低级别节点的数据执行一些逻辑,并将数据馈送到较高级别节点

在构造之后,它们的主要方法是start(启动)启动处理,和next(丅一步)重复调用以生成下一行。

为了将其与上面的sql executor部分联系起来executor.execlocal(),该方法负责执行一条语句调用plan.next()重复并累积结果。

考虑運行SELECT语句所涉及的一些planNodes使用上面定义的表和
 

作为一个有点人为的例子。 这应该从以“C”开头并且其地址包含字符串“Infinite”的州返回客户 為了激动,让我们看看这个语句的查询计划:
 

针对此查询生成的计划从顶部(最高级别)到底部,看起来像:

  
 

在我们依次检查节点之前有一点值得解释:indexJoinNode(表明查询将使用“SI”索引)是如何形成的?此查询使用索引的事实在SELECT语句的语法结构中并不明显因此该计划不仅僅机械地从上面提到的树构建的产品。实际上我们之前没有提到过一个步骤:“计划扩张”。除此之外这一步执行“索引选择”(有關当前用于索引选择的算法的更多信息可以在Radu的博客文章中找到)。我们正在寻找可以扫描的索引以便只有效地检索匹配(部分)过滤器的行。在我们的例子中可以扫描“SI”索引(索引状态)以有效地仅检索满足状态LIKE'C%'表达式的候选行(在狂喜到痛苦时刻,我们看到我們的索引选择/表达式规范化代码足够聪明可以推断状态LIKE'C%'意味着状态> ='C'和状态<'D',但是不够聪明无法推断这两个表达式实际上是等价的,洇此过滤器可以完全被省略)我们不会在此处进行计划扩展或索引选择,但索引选择过程在SelectNode的扩展中发生并且作为副产品,生成使用偠扫描的索引跨度配置的indexJoinNodes

现在让我们来看看这些planNodes是如何运行的:
  1. sortNode:sortNode对其子级生成的行进行排序,对应order by子句构造器包含一系列逻辑,这些逻辑与sql92/99中用于名称解析的奇怪规则相关另一个有趣的事实是,如果我们用一个非平凡的表达式(例如select ab… order by a+b),a+b的值(每行)由较低级別的节点生成这是通过另一个节点中也存在的模式实现的:能够评估表达式并呈现其结果的较低节点rendeNode;sortnode构造函数检查该节点是否已经呈現了所需的表达式,如果没有则要求通过renderNode.addOrMergeRenders()方法生成这些表达式。实际排序在sortnode.next()方法中执行第一次调用它时,它消耗子节点生成嘚所有数据并将其累积到n.sortStrategy(一个隐藏多个排序算法的接口)。当消耗最后一行时调用n.sortStrategy.finish(),此时排序算法完成其处理对sortnode.next()的后续調用只需迭代排序算法的结果。

  2. indexJoinNode:indexJoinNode实现将索引的结果与表的行连接当索引可用于查询时使用它,但它不包含所有必需的列;需要从主键(PK)键值中检索索引中不可用的列 indexJoinNode位于两个扫描节点之上 - 一个配置为扫描索引,另一个经常重新配置为通过PK执行“点查找”在我们的查詢的例子中,我们可以看到“SI”索引用于读取与“state”过滤器匹配的紧凑行集但由于它不包含“address”列,因此PK也需要要使用的每个索引KV对嘟包含该行的主键,因此有足够的信息来进行PK查找 indexJoinNode.Next继续从索引中读取行,并为每个行添加一个由PK读取的跨度一旦足够的这样的跨度被批处理,它们都是从PK中读取的如设计文档中关于SQL行到KV对的部分所述,每个SQL行在索引中表示为单个KV对但在PK中表示为多个连续行(由“键跨度”表示)。
  3. 一个有趣的细节与过滤器的处理方式有关:请注意state LIKE 'C%' 条件由索引扫描,strpos(address“Infinite”)=0条件由pk扫描评估。这很好因为这意味著我们将尽可能多地过滤索引端的内容,并且减少昂贵的pk查找用于确定哪个连接将在splitFilter()中进行求值的代码,由indexJoinNode构造函数调用

  4. scanNode:scanNode通常構成renderNode或filterNode的源; 它负责扫描表或索引的键/值对并将它们重建为行。 这个节点开始像橡胶一样闻到道路因为我们越来越接近实际数据 - 单调,分咘式KV map 您将看到Next()方法不是终点,因为它将工作委托给rowFetcher如下所述。 scanNode有一件有趣的事情:它运行一个过滤器表达式就像filterNode一样。 那是因為我们试图尽可能地将WHERE子句往下推 这通常是一项正在进行的工作,请参阅filter_opt.go 这个想法就像是一个查询

 
 

将被编译为两个scanNodes,一个用于Customers一个鼡于Orders。 他们中的每一个都可以执行过滤的部分专门针对各自的表,然后更高级别的joinNode只需要评估需要来自两者的数据的表达式(即age(c.JoinDateo.Date)<INTERVAL' 1 year')。

让我们继续向下看看scanNode用于实际读取数据的结构。
  1. rowFetcher:rowFetcher负责迭代键值对计算SQL表或索引行的结束位置(记住SQL行可能在多个KV条目中编码),并解码SQL列值中的所有键和值 处理主索引和其他索引之间的差异以及表的布局。 有关SQL行和KV对之间映射的详细信息请参阅Design Doc和编码技术说奣中的相应部分。
  2. rowFetcher还执行从磁盘上的字节数组到我们最常处理的数据表示的解码:parser.Datum接口的实现 有关不同数据类型的磁盘格式的详细信息,请浏览util / encoding目录
  3. kvBatchFetcher:kvBatchFetcher最终从KV数据库中读取数据。 它不了解SQL概念例如表,行或列 在创建它时,它配置了需要读取的许多“键跨度”(例如这些可能是用于读取整个表的单个跨度,或者用于读取PK的部分的几个跨度或者 一个索引)

  4. 结果并将它们返回到planNodes的层次结构。 在此只读查询的情况下发送到KV层的请求是ScanRequests。

 

本文档的其余部分将介绍KV请求的“执行”例如kvBatchFetcher发送的请求。
 
 
CockroachDB的KV层处理“请求”的执行 基于协议缓沖区的API在api.proto中定义,列出了各种类型的请求和响应 实际上,KV的客户端总是发送BatchRequests这是一个包含其他请求集合的通用请求。 所有请求都有一個Header其中包含路由信息(请求所指向的副本)和事务信息
  • 在事务的上下文中是要执行的请求。
 
 
 
客户端使用客户端接口“发送”KV请求(当前此接口是内部的由SQL使用,但我们可能会在将来以某种形式直接向用户提供)此客户端接口包含用于启动(KV)事务的原语(请记住,SQL Executor使鼡它来运行事务上下文中的每个语句)之后,Txn对象可用于在该事务的上下文中执行请求
  • 例如这就是kvBatchFetcher使用的内容。如果你跟踪Txn.Run()方法Φ发生的事情你最终会得到txn.db.sender.Send(...,batch):请求开始渗透通过发件人的层次结构
  • 执行各种外围任务并最终将请求路由到副本以供执行的对象發件人只有一种方法
  • >Replica。前两个请求在与接收SQL查询的节点相同的节点上运行并且正在执行SQL处理(“网关节点”)其他请求在负责正在访问嘚数据的节点上运行(“范围节点”) 。
 
 

最顶层的client.Sender是txnCordSendertxnCordSender负责处理事务的状态(请参阅设计文档的事务管理部分)。事务启动后txnCordSender开始异步哋向该事务的“txn记录”发送心跳消息,以使其保持活动状态它还可以在事务过程中跟踪每个写入的key或key范围。当事务被提交或中止时它清除事务的累积写入意图。事务的所有请求都必须经过同一个txncordsender以便对所有写入意图进行记录,并最终清除在执行这个簿记之后,请求被传递给DistSender
 

DistSender确实是一个主力:它处理网关节点和(可能很多)范围节点之间的通信,将“分布式”放在“分布式数据库”中 它接收BatchRequests,查看批处理内的请求确定每个命令需要去的范围,找到负责该范围的节点/副本将请求路由到那里,然后收集并重新组合结果

我们稍微討论一下代码:
 
  • 请求被细分为范围:DistSender.Send()调用DistSender.divideAndSendBatchToRanges(),它使用RangeIterator迭代请求的组成范围(单个请求例如ScanRequest可以引用可能跨越的key范围)。很多东西嘟隐藏在这个看似无辜的迭代背后:需要访问集群的范围元数据以便找到键到范围的映射(有关此元数据的信息可以在设计文档的范围え数据部分找到)。范围元数据作为常规数据存储在集群中在两级索引映射范围结束键到关于相应范围的副本的描述符(存储该索引的范围称为“元范围”)。 RangeIterator以范围键顺序逻辑迭代这些描述符支持你自己:为了从一个范围移动到另一个范围,迭代器回调到DistSender它知道如哬找到负责一个特定键的范围的描述符。 DistSender委托将描述符的密钥解析为rangeDescriptorCache(LRU树缓存由范围结束键索引)。此缓存信息在群集的范围分离或移動时可能与真实情况不一致; 当发现条目过时时我们将在下面看到DistSender将其从缓存中删除。
  • 在乐观的情况下缓存中存在我们感兴趣的Key的描述苻信息。 在悲观的情况下它需要执行元范围的扫描。 为此我们需要知道包含我们感兴趣的描述符的元范围的描述符,该描述符是使用對缓存的递归调用来检索的 这种递归不能永远持续下去 - 常规范围的描述符在meta2范围内(第二级索引范围),meta2范围的描述符存在于(唯一的)meta1范围内 一旦我们想要扫描的范围的元描述符已知,缓存就会再次委托给DistSender后者发送直接寻址到元范围的RangeLookupRequest KV命令(因此DistSender不会递归地参与路甴此请求)。
  • 每个子请求(部分批处理)都发送到其范围 这是通过调用DistSender.sendPartialBatchAsync()完成的,该调用将批处理中的所有请求截断到当前范围然後将截断的批处理发送到范围。 所有这些部分批次同时发送
 

sendPartialBatch()是处理从陈旧的rangeDescriptorCache信息产生的错误的级别:检测到过时的范围描述符从缓存中逐出,并且重新处理部分批处理

将部分批处理发送到单个范围意味着选择该范围的正确副本并对其执行RPC。默认情况下每个范围有彡个副本,但三个副本中只有一个是“租赁持有者” - 该范围的临时设计所有者负责协调对范围的所有读取和写入(请参阅中的范围租赁蔀分设计doc)。确定哪个副本具有租约是通过另一个缓存完成的 - leaseHolderCache其信息也可能变得陈旧。

DistSender处理此情况的方法是sendSingleRange它将使用缓存将请求发送給租约持有者,但它也准备按照“邻近”的顺序尝试其他副本缓存所说的副本是租约持有者,只需将租约持有者移动到尝试副本列表的朂前面然后按顺序将RPC发送给所有副本。



实际上发送RPC隐藏在传输接口后面。具体而言grpcTransport.SendNext()对包含目标副本的节点执行gRPC调用,即对实现內部服务的服务执行gRPC调用

来自不同副本的(异步)响应组合成单个BatchResponse,最终从Send()方法返回

我们现在已经了解了网关节点上发生的相关倳情。此外我们将在每个范围节点内查看“远程”侧发生的情况。


我们已经看到distssender如何将batchrequest拆分为部分批每个批包含单个副本的本地命令,以及如何通过rpcs将这些命令发送给其范围的租用持有者我们现在要转到这些RPC的“服务器”端。实现RPC服务的结构是节点节点不做任何有意义的事情;它将请求委托给它的Stores存储成员,该成员代表一组“存储”(在磁盘上数据库设计为每个物理磁盘一个,请参阅设计文档的架构部分)Stores实现sender接口,就像我们以前看到的网关层一样恢复包装另一个sender的模式,并通过send()方法向下传递请求

Stores.Send()标识哪个特定存儲包含目标副本(基于DistsSender填充到请求中的路由信息),然后路由请求到那个存储存储做的一件有趣的事情是,如果当前事务的请求已经在此节点上处理则更新当前请求使用的不确定度间隔的上限(有关不确定度间隔的详细信息,请参阅设计文档的“选择时间戳”部分)鈈确定间隔指示哪些时间戳的值不明确,因为节点之间存在时钟偏差(不知道这些值是在当前txn的序列化点之前还是之后写入的)此代码認识到,如果以前在该节点上处理过来自当前txn的请求那么在该节点的时间戳之后写入的值在其他请求处理时是不明确的。
 

Store表示一个物理磁盘设备出于我们的目的,Store主要将请求委托给副本但它有一个重要的角色 - 如果请求遇到“写入意图”(即未提交的值),它会处理这些意图这会处理事务之间的读写和写写冲突。请注意调用副本的代码位于一个大的无限重试循环中,并且其中的一堆代码处理WriteIntentError当我們看到这样的错误时,我们尝试使用intentResolver“解决”它解决意味着确定意图所属的事务是否仍然挂起(它可能已经提交或中止,在这种情况下意图被“解决”)或者可能“推动”有问题的事务(强制它在更高的时间戳重新启动,使其不与当前的txn冲突)如果冲突的txn不再挂起或鍺它被推动,则可以正确地解决意图(即由提交的值替换,或者简单地丢弃)第一部分 - 搞清楚txn状态或推动它
  • 在intentresolver.maybepushTransaction中完成:我们可以看到┅系列pushtxnRequest被批处理并发送到集群(也就是说,将使用当前节点上发送者的层次结构从上到下,将请求路由到各种事务记录-请参见设计文档嘚“事务执行流”部分)如果我们试图推动的交易仍悬而未决,则根据推动者/推动者Txn的相对优先级决定推动是否成功是在深入处理pushtxnRequest(低于我们在这里讨论的store级别的几个级别,在用于剥离的堆栈中PushTxnRequest)的过程中完成的
 

第二部分 - 替换现在可以解决的意图,是通过调用intentResolver.resolveIntents完成的 回到我们在Store.Send()中停止的地方,对intentResolver的调用如果成功,将更改WriteIntentError的resolved字段这将导致我们立即重试。 否则我们将根据指数退避重试,等待峩们无法推动的未决的事务 - 我们不想太快重试因为我们几乎肯定会再次遇到相同的意图( 等待冲突的txn完成时,我们正在努力用反应机制取代这种“轮询”机制)
 

副本 - 执行读取,提交Raft命令

 

一个副本表示范围的一个副本而该副本又是一个由raft共识算法的一个实例管理的连续鍵空间。默认情况下系统尝试将范围保持在64MB左右。副本是我们层次结构中的最终发送者所有其他发送者的作用主要是将请求路由到当湔充当范围的租约持有的副本(一个主要副本,承担了我们将在下面探讨的一系列协调责任)副本处理读请求的方式与处理写请求的方式不同。读取被直接评估而写作将进入他们生命的另一个大篇章,并通过RAFT共识协议

读取请求和写入请求的路径之间的差异立即显示:replica.send()根据请求类型快速分支。我们将依次讨论读/写路径
 

对读取请求所做的第一件事是检查请求是否到达正确的位置(即当前副本是租约歭有者);请记住,很多路由都是基于缓存或正确的猜测完成的这项检查由replica.redirectOnOrAcquireLease()执行,这是一个独立的兔子洞我们只是说,如果当前副夲不是租约持有者redirectOnOrAcquireLease重定向到租约持有者,如果存在有效租约(请记住DistSender将处理此类重定向)或者不存在请求新租约,总之希望它将成為租约持有者。请求租约是通过pendingLeaseRequest帮助器结构完成的该结构为同一个租约合并多个请求,并最终构造一个RequestLeaseRequest并将其直接发送到副本(正如我們在其他情况下所见绕过所有发送器以避免无限地递归)。如果请求租约redirectOnOrAcquireLease将等待该请求完成并检查它是否成功。

一旦解决了租用情况接下来要做的就是将读取与可能正在进行的写入进行同步-如果正在对重叠的键跨度进行写入,则读取可能需要查看其值因此我们无法與之竞争;我们必须等待写入完成。这种同步是通过CommandQueue结构完成的该结构是一个间隔树,用于维护所有正在进行的请求并通过它们所接觸的键或键的跨度进行索引。等待写入操作在replica.beginCmds()内完成请注意,在确定需要等待哪些命令之后我们会自动地将当前读取添加到命令隊列中,以阻止将来的写入这在精神上与下面描述的TimestampCache结构的使用重叠,事实上有一个建议不将读取放入队列。从队列中删除命令是稍後完成通过BeginCmds返回的回调这篇结语还做了一些其他重要的事情:它在TimeStampCache中记录了读取,这是一个内存缓存从键范围到读取它的最新时间戳。此结构用于防止违反快照隔离事务隔离级别(蟑螂数据库提供的最低级别)该级别要求必须保留读取结果,即在低于以前读取时间戳嘚时间戳处写入key不得成功(请参阅读取写入冲突-读取时间戳缓存部分In马特的博客帖子)正如我们将在“写入”部分看到的,写入操作参栲此结构以确保它们不会在已执行的“读取”下写入。

map切换请求类型并将执行传递给特定的请求方法。一个典型的读取请求是ScanRequest;这是由evalScan評估的代码非常简短 - 它立即调用引擎上的相应代码 - 磁盘上RocksDB数据库的句柄。在我们深入研究这个引擎之前让我们看一下evalScan下一步会做什么:它会将意图返回到更高级别。这些是扫描遇到的意图但它们并没有禁止它继续(例如时间戳高于我们正在读取的时间戳的意图 - 读取并鈈关心这些意图是否被提交);这与阻止读取的意图形成对比 - 正如我们将在下面看到的那样,它们被转换为WriteIntentErrors我们已经看到它们由Store处理。这些非干扰意图是为了清理目的而收集的 - 它们可能是死交易留下的垃圾我们希望主动清理它们。它们被返回堆栈直到replica.addReadOnlyCmd尝试使用我们的老萠友,intentResolver来清理它们
 

我们已经到了CockroachDB堆栈的底部 - Engine是一个抽象出不同的磁盘存储的接口。我们目前使用的唯一实现是RocksDB它是RocksDB C ++库的包装器。除了說它使用cgo与C ++代码连接之外我们不会进入这个包装器。我们也不会进入RocksDB代码尽管它显然是服务请求的重要部分,但并不是CockroachDB开发人员通常會处理的内容


对于读取,引擎包的入口点是mvccScanInternal()这将对KV数据库执行扫描,处理我们用于MultiVersion并发控制(MVCC)的数据表示它迭代所请求范围嘚键/值,并将每个键附加到结果中 MVCC的详细信息,例如我们保留每个键的多个版本(针对不同的时间戳)和意图的事实由MVCCIterate()处理,它使用Engine提供的迭代器来扫描键/值它委托读取键/值l并将迭代器推进到mvccGetInternal()。
 

写请求在概念上比读更有趣因为它们不只是由一个节点/副本提供服务。相反它们通过raft共识算法,该算法维护一个有序的提交日志然后日志由范围的所有副本应用(更多详细信息,请参见设计文档嘚raft部分)启动这个过程的副本,和读取一样是租约持有者。因此在租约持有者的执行分为两个阶段:Raft前(“代码中的上游”)和Raft后(“代码中的下游”)。上游阶段最终会阻止相应的Raft命令在本地应用(命令在本地应用后未来的读取保证看到其效果)。

接下来我们将介绍一些术语 我们已经看到副本(以及一般的KV子系统)接收请求。 接下来将评估这些请求,将其转换为Raft命令 这些命令反过来被提议給Raft共识小组,并且在Raft小组接受提议并提交它们之后控制权返回到所有副本(这次是范围的所有复制品,而不仅仅是租赁持有者)所有副本应用提议。

执行写命令镜像读取,从replica.addWriteCmd()开始 此方法只包含一个重试循环,用于处理需要重复计算请求并委派给replica.tryAddWriteCmd的异常情况 这镓伙做了很多事情:
    • overlapping read).  这意味着检查我们上面讨论过的TimestampCache,看看写入是否可以在它尝试修改该数据库的时间戳进行 如果它不能(因为最近有偅叠读取),写入的时间戳会比任何重叠读取的时间戳晚
  1. 我们将在下面描述这个过程(它会很有趣)但是,在我们开始之前让我们看看当前的方法将会做些什么。

  2. 当(本地)副本应用了相应的命令时通道将接收结果,这可能仅在命令已提交到共享的Raft日志(全局操作)の后才会发生

 


如果愿意,最后一种方法模拟请求的执行并将引擎的所有可能更改记录到“批处理”中(这些批次是RocksDB模型事务的方式)。 该批次将被序列化为Raft命令 如果我们现在提交此批处理,则更改将是实时的但仅在此一个副本上,这将是潜在的数据一致性违规 相反,我们放弃它 正如我们所看到的,当命令“从Raft里出来”时它会再次复活

它切换不同类型的请求,并调用特定于每种类型的方法 一種这样的方法就是beevalPut,它为Key写入一个值 在其中我们将看到对引擎的调用以执行此写入(但请记住,它们都在RocksDB事务中执行即engine.Batch)。

这一切都昰为了引擎记录的改变需要向Raft提出 让我们展开堆栈replica.propose(本节开始的方法),看看requestToProposal的结果会发生什么 例如,它被插入到“待定提议map”中 - 这個结构将在正在应用的命令和tryAddWriteCmd之间建立连接这将在等待本地应用程序的通道上被阻塞。 更重要的是它传递给replica.submitProposalLocked,最终调用raftGroup.Propose() 这个raftGroup是┅个共识组的句柄,由Etcd Raft库实现我们提交了一个黑盒子,通过多数投票将它们序列化为连贯的分布式日志 该库负责将命令传递给应用程序的所有副本。

以上是对租约持有者副本特定部分的讨论:如何向Raft提出命令以及在向(KV)客户端返回答复之前,租赁持有者如何等待它們被应用 缺少的是关于它们如何应用的讨论。


我们已经看到命令是如何“放入”Raft的但他们如何“走出去”? Etcd Raft库实现了一个分布式状态機其描述超出了当前的范围。我只想说我们有一个raftProcessor接口它说明从这个库调用转换为。我们的老朋友Store实现了这个接口重要的方法是Store.processReady()。这最终会回调到一个特定的副本(每个命令正在修改的范围的副本)即它将调用handleRaftReadyRaftMuLocked。这将遍历新提交的命令为每个命令调用processRaftCommand。这将采用序列化的engine.Batch并用它调用replica.applyRaftCommand这里批处理被反序列化并应用于引擎,这次与applyRaftCommandInBatch中的建议方不同,这些更改实际上已提交给存储该命令现已應用(在一个特定副本上,但请记住本节中描述的过程发生在每个副本上)。

我们掩盖了processRaftCommand中的一些重要信息:在应用命令之后如果当湔副本是提议者(即租约持有者),我们需要发信号通知提交者(正如我们在上一节中看到的那样在tryAddWriteCmd中被阻止))。 这发生在最后 我們现在已经完全循环 - 提议者现在将被解除阻塞并在它正在等待的频道上收到响应,它可以解除堆栈让其客户端知道请求已完成。 此回复鈳以通过发件人的层次结构从租约持有者节点返回到SQL网关节点,到planNodes的SQL树到SQL Executor,以及通过pgwire实现到SQL客户端

2015年终盘点~~养车?费用
来车轮嘚时间还没满月却已到年关,各种总结今年爱车的花销放这存个档,明年再来对比下:
?我的爱车型号:江淮14款新和悦手 舒
?爱车购车价:59800人民币
?爱车购买时间:2015.07
1??油费:没跑几个月1800元左右,油耗在郊区6-7.5个城区在7-9个;
2??保养:刚做完首保,具体保养一佽大概在400元左右这么便宜的车保养也贵不哪去;
3??改装维修:基本没有,底牌装甲算吧200搞定,保险杠刮擦都是自己描的漆;
4??汽車用品:由于4S送的座垫实在不堪入目心一横从某宝淘了套豪华版座垫260大洋,还有一些小件—挡泥板、迎宾踏板等共50元再就是找关系装叻套行车记录仪(由于原车没有导航就要了套集成导航倒影的),花费500大洋(以后不怕碰瓷了)合计下汽车用品在800元左右;
5??洗车打蠟:由于我时间比较不值钱,所以洗车打蜡的活自己包了在外面洗车也要等着,于是在没加入车轮前从某宝淘了神器—洗车水枪套装和龜牌车蜡共花销150元,从没去过洗车店现在就缺个吸尘器了,希望在社区能实现愿望!
6??保险:新车虽然不贵但是毕竟是血汗钱换来嘚找熟人买的全险—强险+车船税+30万三者+车损+驾乘+不计免赔=3000元整;
7??罚单:总是提心吊胆,值得高兴的是至今还没收到过我是个守法公民,安全驾驶是我们的责任更是我们的义务。
?今年最冤枉的费用:就是和中央联系不够紧密要降购置税也不提前说一声,以后得哆走动走动了
?今年最值的费用:行车记录仪很是让我满意。
?年度用车感想:我要是早一点来社区会不会省掉很多费用啊总之,不茬意别人说什么车是自己的,好不好够用就行不管开好车破车,都应该安全驾驶幸福万家!
?来年用车的期待:平平安安,才能幸福,也祝社区的所有认识的不认识的车友们,都能平平安安

我要回帖

更多关于 苹果手机应用程序突然消失了 的文章

 

随机推荐