Overview
crdb内部分为五层架构.
- SQL Layer: 负责SQL的语法解析,计划的生成与执行。
- Transaction Layer: 负责实现分布式事务,提供SI和SSI两种隔离级别。
- Distribution Layer: 实现全局排序的map,负责数据的分布规则。
- Replication Layer: 通过raft实现数据副本的强一致性。
- Storage Layer: 使用RocksDB作为底层存储,包括raft log以及user data的存储。
SQL Layer
将用户提交的SQL语句转化为底层存储的KV操作。
Overview
开发者使用一个连接串即可进行关系操作。crdb节点是对等关系,可以选择任意一个节点进行连接,此节点程为gateway node
. 用户发送的SQL语句在这一层需要转换成对KV的操作,然后传递到Transaction Layer。
Components
Relational Structure
crdb给用户展现的是关系型结构,对象有表、行、列等。同时还支持一些关系特性,如外键约束。
SQL API
crdb实现了大部分的ANSI SQL语法以及事务相关的语法,如BEGIN
, END
, ISOLATION LEVELS
PostgreSQL Wire Protocol
通信协议兼容PG协议
SQL Parser, Planner, Executor
接收到SQL后,crdb解析sql,生成执行计划,并执行。
Parsing
通过解析yacc文件pkg/sql/parser/sql.y
, 生成抽象语法树AST.
Planning
根据AST, 生成树形的执行计划planNodes,每个planNode节点是对KV的一组操作。
可以通过EXPLAIN查看执行计划。
Executing
planNodes的执行会直接和Transaction Layer进行交互,此阶段包括消息的encoding和decoding。
Encoding
虽然SQL语句是可读的字符流,但是KV是按照字节流进行存储,所以在和Transaction Layer交互的时候,需要将字符流转化为字节流,然后传递给Transaction Layer, 而Transaction Layer返回的数据是字节流,在返回给client端的时候,需要转换成用户可识别的字符流。
DistSQL
Distributed SQL.
non-distributed
SQL, coordinating node
接受所有的数据,单点进行后续的计算处理。
DistSQL将计算推给多个节点进行,coordinating node
聚集多个执行节点的结果集。
DistSQL可以降低传递给coordinating node
的数据量,同时利用并行计算的能力,可以大大降低执行时间。
Technical Interactions with Other Layers
planNodes
中的kv操作传递给Transaction Layer.
Transaction Layer
事务层负责实现分布式事务,保证事务的ACID特性。
Overview
Writes & Reads (Phase 1)
Writing
执行写操作时,不直接将数据落盘:
- 先写Transaction Record,写到事务中第一条write语句涉及的range。状态有
PENDING
,COMMITTED
,ABORTED
。 - 所有write操作会写Write Intents, 表示临时的未提交状态,同时数据中包含了指向
Transaction Record
的值。
Reading
如果读到正常数据,OK,如果读到write intents
,需要进入transaction conflict
处理流程。
Commits (Phase 2)
检查Transaction Record
, 如果发现ABORTED
状态,restart事务。
Cleanup (Asynchronous Phase 3)
异步清理状态, Transaction Record
状态设为COMMIT
后,需要异步清理write intents
.
存在专门的coordinating node
,做异步清理工作:
- 清理
write intent
指向Transaction Record
的指针数据。 - 删除
write intent
, 修改原记录
How CockroachDB Does Distributed, Atomic Transactions
Technical Details & Components
Time & Hybrid Logical Clocks
分布式系统中,顺序和因果非常重要,但是很难解决。虽然raft可以实现全局排序,但是效率非常低下。crdb实现了hybrid-logical clocks
(HLC
),有物理部分和逻辑部分组成。物理部分由本地时钟决定,逻辑部分用于区分相同本地时钟的事件。
事务执行时,通过HLC获取时间戳。当节点向其它节点发送请求时,会带上本地时间戳,当其它节点收到请求后,会根据收到的时间戳调整本地的HLC,保证下一个时间戳一定大于收到的时间戳。
Max Clock Offset Enforcement
为了协调不同节点之间的时间,可以指定最大的时钟偏差。如果节点的时钟偏差超过设定的值,则此节点会自杀。
Timestamp Cache
时间戳缓存用于缓存本地读操作对应的时间戳,如果写操作小于cache中对应的最新的读时间戳,则写操作的事务会以新的时间戳进行重试。
Transaction Records
在事务修改第一个kv之前,需要先在此key所在的rang上生成一个transaction记录。
Transaction Record的状态:
PENDING
: 初始状态,事务写入还在进行状态COMMITTED
: 提交状态,其它事务可见。ABORTED
: 事务失败或者回滚。
此为switch状态,PENDING===OFF,COMMIT=ON
Write Intents
Switch
: Transaction Records设置为off状态,此值不允许并发读写。Stage
: 写入的值不直接修改key,而是新做一个临时key0,紧挨着原key, key0包行指向Transaction record的指针。Filter
: 其它线程读取时,如果碰到staged状态的可以,需要检查switch状态,如果为OFF,则返回原始VALUE,如果为ON,则返回staged valueFlip
: 写入结束后,commit阶段,把Transaction Records设置为ON状态。Unstage
: commit结束后,需要清理staged value,把key的value替换为key0的value,删除key0
Resolving Write Intent
当一个操作读到write intent
后,根据Transaction record
的状态可以有如下处理:
COMMITTED
: 删除staged value的指针。ABORTED
: 删除staged value。PENDING
: 需要进行Transaction Conflicts
Isolation Levels
- Serializable Snapshot Isolation 需要检查读写冲突
- Snapshot Isolation 只检查写写冲突, 存在
write skew
写倾斜的问题
Transaction Conflicts
两种冲突:
- Write/Write : 两个
PENDING
的事务对同一个key进行write intent. - Write/Read : 读遇到了比自己时间戳小的write intent。
三种处理方式:
- 看优先级:如果优先级不一样,则优先级低的事务aborted.
- 第二个事务将第一个事务的timestamp提升(TxnA必须是SI,TxnB是读操作)
- TxnB进入等待,等待TxnA完成。
Technical Interactions with Other Layers
Transaction & SQL Layer
SQL Layer发送planNodes
给Transaction Layer
Transaction & Distribution Layer
TxnCoordSender
发送KV请求给Distribution Layer的DistSender
.
Distribution Layer
Overview
crdb以全局排序进行数据存储用来:
- Simple lookups: 很容易知道数据存住在哪个节点,定位数据很简单。
- Efficient scans: 通过指定数据的顺序,高效扫描数据
Monolithic Sorted Map Structure
全局排序map包含两种数据:
- System data : meta ranges
- User data
Meta Ranges
二级索引:meta1
和meta2
. 首先查询meta1,然后查询meta2,最后定位到具体的key所在的range。 每个节点都有meta1
range所在的位置,此range不会分裂。所有node都会缓存已经查询的meta2的记录。
Table Data
data按照range为最小单位,每个range默认是64MB。range也是副本同步的基本单元。
Using the Monolithic Sorted Map
当节点接收到用户请求时,查询meta range,找到key对应的rang所在的node。通过meta2,找到具体range的Leaseholder
, 然后将请求发给Leaseholder. Leaseholder是三个副本中负责接收读写请求的副本。
Technical Details & Components
gRPC
Distribution Layer是第一个与其它node通信的Layer, crdb使用gRPC进行节点间通信。
gRPC的消息通过protobufs进行打包和解包。crdb实现了基于pb的APIapi.proto
.
BatchRequest
KV请求打包到一起,批量进行传递,减少RPC的调用。
DistSender
gateway节点的DistSender
接受本地TxnCoordSender
传递的BatchRequests,DistSender
将所有请求按照需要发送到的不同的node进行分类。
Meta Range KV Structure
|
|
meta1
contains the address for the nodes containing themeta2
replicas.
|
|
meta2
contains addresses for the nodes containing the replicas of each range in the cluster, the first of which is the Leaseholder.
|
|
Table Data KV Structure
|
|
Range Descriptors
每个range都有元数据,成为Range Descriptors
,包含如下:
- 顺序递增的RangeID
- keyspace: 本range包含的minkey和maxkey
- 其它副本所在的node的地址。
range描述信息会存储在meta2中。
Range Splits
当range达到了64MB后,分裂成两个32MB的range。
Technical Interactions with Other Layers
Distribution & Transaction Layer
Distribution Layer的DistSender,接收BatchRequests.
Distribution & Replication Layer
Distribution Layer将BatchRequests
发送给Replication Layer的Raft group leader
or Leaseholder
。
Replication Layer
Overview
高可用和数据完全一致是数据库系统的一个挑战,为了解决这个问题,crdb使用了一致性协议保证任何修改必须得到多数节点的同意才能完成。
当某个节点挂了之后,crdb会将数据重新分布,达到最大副本数。当新节点加入集群后,数据rebalance.
Components
Raft
将一个range的所有副本组织一个raft group,每个raft group有一个leader
,其它的为follower
,leader
由raft group进行选举,协调raft group中所有节点的写入。
BatchRequest
首先传递给Leaseholder,然后KV操作映射为对应的raft command发给raft group leader
。(Leaseholder
是接受read,write的节点,记录在meta2中;raft leader
是raft协议选出的group leader,负责raft节点的协调,最好的情况是Leaseholder
和raft leader
是一个节点)
Raft Logs
当写入请求达到多数支持,被raft group leader提交后,此操作记入raft log。raft log是序列化的操作记录,可以replay。
Snapshots
每个副本都可以进行快照,根据MVCC实现快照。使用快照恢复的时候,配合raft group log,进行增量恢复。
Leases
Raft group中的一个节点作为Leaseholder,接受读请求,同事讲写请求提交给raft group leader.
读请求可以不用管raft.
When serving reads, Leaseholders bypass Raft; for the Leaseholder’s writes to have been committed in the first place, they must have already achieved consensus, so a second consensus on the same data is unnecessary.
Co-location with Raft Leadership
Leaseholder在lease过期之后,会进行renew,或者将leaseholder转让给其它节点,目的是让leaseholder与raft group leader是同一个节点。
Epoch-Based Leases (Table Data)
表数据也有租约,使用epochs
进行实现,在一个node加入集群和离开集群成为一个epoch,如果node脱离集群,则epoch进行变化,此node就会失去所有相关的lease。
这避免了跟踪每个range的lease,这里假设所有的lease都不会过期,直到node脱离集群。
Expiration-Based Leases (Meta & System Ranges)
meta range也有租约,不是epoch,而是expiration-based lease
。定期重新选leaseholder。
Membership Changes: Rebalance/Repair
集群的节点数出现变化后,raft group的成员也会变化,range副本需要rebalance。
- Nodes added: 新节点与其它节点通信,表示自己空闲,集群便转移一部分replica过去。
- Nodes going offline: 如果一个raft group中的成员长时间不响应,超过5分钟,则集群开始进行rebalance操作,复制数据到其它节点,以达到约定的副本数。
Rebalancing Replicas
集群检查到节点变化后,会进行replica的迁移。
这是通过在Leaseholder上进行snapshot,然后通过gRPC传递快照到其它节点。快照传递完成后,新的节点加入到raft group中来, 此间会通过raft log同步增量数据。
Interactions with Other Layers
Replication & Distribution Layers
- Distribution Layer的DistSender发送kv请求给replication层。
- Replication layer 发送
BatchResponses
给DistSender
Replication & Storage Layers
提交的raft command写到raft logs,最终写入到Storage Layer.
Leaseholder从Storage Layer层的RocksDB读取数据。
Storage Layer
Overview
每个crdb节点至少包含一个store
, 数据按照KV存储在RocksDB上。每个store
包含三个RocksDB实例:
- One for the Raft log
- One for storing temporary Distributed SQL data
- One for all other data on the node
一个节点中的所有store共享一个block cache
.
Components
RocksDB
crdb使用RocksDB的原因:
- KV存储,和Distribution Layer直接映射
- 原子批量写,快照,事务
RocksDB是Facebook基于Google的LevelDB开发的一个分支,加了一些优化措施。
MVCC
crdb严重依赖MVCC机制,时间戳通过hybrid logical clock (HLC) 进行确定。所有的MVCC数据都存在RockDB。
MVCC在存储层存储,被 Transaction Layer利用。
Time-Travel
SELECT...AS OF SYSTEM TIME
获取指定时间戳的数据, 只要数据没有被GC。
Garbage Collection
crdb定期清理老的MVCC数据来减少磁盘数据的大小。存在一个garbage collection period
,如果old value的时间戳距离当前时间的差值大于此值,则可以进行清除。
Interactions with Other Layers
Storage & Replication Layers
Storage Layer通过raft log将数据写入磁盘,同时将数据返回给Replication Layer.