Kudu
为用户提供了两种一致性模型。默认的一致性模型是
snapshot consistency
。这种一致性模型保证用户每次读取出来的都是一个可用的快照,但这种一致性模型只能保证单个client
可以看到最新的数据,但
不能保证多个client每次取出的都是最新的数据
。
另一种一致性模型
external consistency
可以在多个
client
之间保证每次取到的都是最新数据,但是Kudu没有提供默认的实现,需要用户做一些额外工作。
为了实现
external consistency
,
Kudu提供了两种方式:
-
在 clients 之间显示的传递时间戳。当写入一条数据之后,用户要求 client 去拿一个时间戳作为 token,然后通过一个 external channel 的方式传递给另一个 client。然后另一个 client 就可以通过这个 token 去读取数据,这样就一定能保证读取到最新的数据了。不过这个方法实在是有点复杂。
-
提供类似 Spanner 的 commit-wait 机制。当写入一条数据之后,client 需要等待一段时间来确定写入成功。Kudu 并没有采用 Spanner TrueTime 的方案,而是使用了 HybridTime 的方案。HybridTime 依赖 NTP,这个可能导致 wait 的时间很长,但 Kudu 认为未来随着 read-time clock 的完善,这应该不是问题了。
第二部分 Kudu的架构
与
HDFS
和
HBase
相似,
Kudu
使用单个的
Master
节点,用来管理集群的元数据,并且使用任意数量的Tablet Server节点用来存储实际数据。可以部署多个
Master
节点来提高容错性。
Master
Kudu
的
master
节点负责整个集群的元数据管理和服务协调。它承担着以下功能:
- 作为catalog manager,master节点管理着集群中所有table和tablet的schema及一些其他的元数据。
- 作为cluster coordinator,master节点追踪着所有server节点是否存活,并且当server节点挂掉后协调数据的重新分布。
- 作为tablet directory,master跟踪每个tablet的位置。
Catalog Manager
Kudu
的
master
节点会持有一个
单tablet的table——catalog table
,但是用户是不能直接访问的。master将内部的
catalog
信息写入该
tablet
,并且将整个
catalog
的信息缓存到内存中。随着现在商用服 务器上的内存越来越大,并且元数据信息占用的空间其实并不大,所以master
不容易存在性能瓶颈。
catalog table保存了所有table的schema的版本以及table的状态
(创建、运行、删除等)。
Cluster Coordination
Kudu
集群中的每个
tablet server
都需要配置
master
的主机名列表。
当集群启动时,tablet server会向master注册,并发送所有tablet的信息
。
tablet server
第一次向
master
发送信息时会发送所有
tablet
的全量信息,后续每次发送则只会发送增量信息,仅包含新创建、删除或修改的tablet
的信息。 作为cluster coordination,
master
只是集群状态的观察者。对于
tablet server
中
tablet
的副本位置、
Raft
配置和schema
版本等信息的控制和修改由
tablet server
自身完成。
master只需要下发命令,tablet server执行
成功后会自动上报处理的结果。
Tablet Directory
因为
master
上缓存了集群的元数据,
所以client读写数据的时候,肯定是要通过master才能获取到tablet的位置等信息
。但是如果每次读写都要通过
master
节点的话,那
master
就会变成这个集群的性能瓶颈,所以client
会在本地缓存一份它需要访问的
tablet
的位置信息,这样就不用每次读写都从
master中获取。 因为tablet
的位置可能也会发生变化(比如某个
tablet server
节点
crash
掉了),所以
当tablet的位置发生变化的时候,client会收到相应的通知,然后再去master上获取一份新的元数据信息
。
Table
在数据存储方面,
Kudu
选择完全由自己实现,而没有借助于已有的开源方案。
tablet
存储主要想要实现的目标为:
RowSets
在
Kudu
中,
tablet
被细分为更小的单元,叫做
RowSets
。一些
RowSet
仅存在于内存中,被称为MemRowSets,而另一些则同时使用内存和硬盘,被称为
DiskRowSets
。
任何一行未被删除的数据都只能存在于一个RowSet中
。 无论任何时候,一个
tablet
仅有一个
MemRowSet
用来保存最新插入的数据,并且有一个后台线程会定期把内存中的数据flush
到硬盘上。 当一个
MemRowSet
被
flush
到硬盘上以后,一个新的MemRowSet
会替代它。而
原有的MemRowSet会变成一到多个DiskRowSet
。
flush
操作是完全同步进行的,在进行flush
时,
client
同样可以进行读写操作。
MemRowSets
是一个可以被并发访问并进行过锁优化的
B-tree
,主要是基于
MassTree
来设计的,但存在几点不同:
- Kudu并不支持直接删除操作,由于使用了MVCC,所以在Kudu中删除操作其实是插入一条标志着删除的数据,这样就可以推迟删除操作
- 类似删除操作,Kudu也不支持原地更新操作。
- 将tree的leaf链接起来,就像B+-tree。这一步关键的操作可以明显地提升scan操作的性能。
- 没有实现字典树(trie树),而是只用了单个tree,因为Kudu并不适用于极高的随机读写的场景。
与
Kudu
中其他模块中的数据结构不同,
MemRowSet中的数据使用行式存储
。因为数据都在内存中,所以性能也是可以接受的,而且Kudu
对在
MemRowSet
中的数据结构进行了一定的优化。
当
MemRowSet
被
flush
到硬盘上,就变成了
DiskRowSet
。当
MemRowSet
被
flush
到硬盘的时候,每32M就会形成一个新的
DiskRowSet
,这主要是为了保证每个
DiskRowSet
不会太大,便于后续的增量
compaction
操作。
Kudu
通过将数据分为
base data
和
delta data
,来实现数据的更新操作。
Kudu会将数据按列存储,数据被切分成多个page
,并使用
B-tree
进行索引。除了用户写入的数据,
Kudu
还会将主键索引存入一个列中,并且提供布隆过滤器来进行高效查找。
为了提高查询性能,
Kudu
会定期进行
compaction
操作,合并
delta data
与
base data
,对标记了删除的数据进行删除,并且会合并一些DiskRowSet
。
选择分区策略需要理解
数据模型
和
表的预期工作负载
:
- 对于写量大的工作负载,重要的是要设计分区,使写分散在各个tablet上,以避免单个tablet超载。
- 对于涉及许多短扫描的工作负载(其中联系远程服务器的开销占主导地位),如果扫描的所有数据都位于同一块tablet上,则可以提高性能。
没有默认分区
在创建表时,
Kudu
不提供默认的分区策略。
建议预期具有繁重读写工作负载的新表至少拥有与tablet服务器相同的tablet
。
和许多分布式存储系统一样,
Kudu
的
table
是水平分区的。
BigTable
只提供了
range
分区,
Cassandra
只提供hash
分区,而
Kudu
同时提供了这两种分区方式,使分区较为灵活。当用户创建一个
table
时,可以同时指定table
的的
partition schema
,
partition schema会将primary key映射为partition key
。一个partition schema包括
0
到多个
hash-partitioning
规则和一个
range-partitioning
规则。通过灵活地组合各种partition
规则,用户可以创造适用于自己业务场景的分区方式。
第三部分 kudu表设计
Tablet是kudu表的水平分区
,类似于
google Bigtable
的
tablet
,或者
HBase
的
region
。每个
tablet
存储 着一定连续range
的数据(
key
),且
tablet
两两间的
range
不会重叠。一张表的所有
tablet
包含了这张表的所有key
空间。
Tablet
由
RowSet
组成,
RowSet
由一组
rows
组成(
n
条数据、
n
行数据)。
RowSet
是不相交的,即不同的RowSet
间的
row
不会交叉,因此一条给定的数据,只会存在于一个
RowSet
中。虽然
Rowset
是不相交的,但是两两间的key
空间是可以相交的(
key
的
range
)。
Handling Insertions
一个
RowSet
存储在内存中,它被称为
MemRowSet
,
一个tablet中只有一个MemRowSet
。 MemRowSet是一个
in-memory
的
B-Tree
树,且按照表的主键排序。所有的
insert
直接写入进MemRowSet。受益于
MVCC
(
Multi-Version Concurrency Control
多版本并发控制,下文中会讲述),一旦数据写入到MemRowSet
,后续的
reader
能立马查询到。
注意:不同于
BigTable
,
Kudu
只有插入与
flush前的mutation才会被记录到MemRowSet。
mutation例如基于磁盘数据的update、delete
,下文会有介绍。
任何一条数据都以
entry
的形式精确的存在于一个
MemRowSet
中,
entry由一个特殊的header和实际的row data内容组成
。由于
MemRowSet
只存于内存中,最终会被写满,然后
Flush
到磁盘里(一个或者多个DiskRowSet
中)。(下文会详细介绍)
MVCC overview
Kudu
为了提供一些有用的特性,使用多版本并发控制:
Snapshot scanner
:快照查询,当创建了一个查询,系统会操作
tablet
指定时间的快照(
point-in-time)。
在这个查询过程中的任何针对这个tablet的update都会被忽略
。另外,指定时间的快照 (point-in-time
)可以被存储并在其他的查询中重复使用,例如,一个应用对一组连续的数据进行多次交互执行分析查询。
Time-travel scanners
:历史快照查询,与上边的快照查询一样。用户可以指定历史的一个时间点创建一个查询,MVCC
可以保证历史快照的一致性。这个功能可以被用来在某个时间点上的一致性备份。
Change-history queries
:历史变更查询,给定两个
MVCC
快照,用户可以查询这两个快照间任务数据。这个功能可以用来做增量备份,跨集群同步,或者离线审计分析。
Multi-row atomic updates within a tablet
:
tablet
内多行数据的原子更新,在一个
tablet
里,一个操作 (mutation
)可以修改多行数据,而且在一条数据里的原子操作里是可见的。(应该是针对
column
的原子操作)
为了提供
MVCC
功能,每个操作(
mutation
)会带有一个时间戳(
timestamp
)。
Timestamp
是由
TS-wide Clock实例提供的,
tablet
的
Mvcc Manager
能保证在这个
tablet
中
timestamp
是唯一的不重复的。 Mvcc Manager决定了数据提交的
timestamp
,从而这个时间点后的查询都可以获取到刚刚提交的数据。
查询在被创建的时候,scanner提取了一个MvccManager时间状态的快照,所有对于这个scanner可见的数据都会跟这个MvccSnapshot比较,从而决定到底是哪个insertion、update或者detete操作后的数据可见
。
每个
tablet
的
Timestamp
都是单调递增的。使用
HybridTime
技术来创建时间戳,它能保证节点之 间的时间戳一致。
为了支持快照和历史快照功能,多个版本的数据必须被存储。为了防止空间无限扩展,用户可以配置一个保留时间,并将这个时间之前的记录GC
(这个功能可以防止每次查询都从最原始版本开始读取)。
MVCC Mutations in MemRowSet
为了在
MemRowSet
中支持
MVCC
功能,每行插入的数据都会带着时间戳。而且,
row
会有一个指针,它指向紧随其后的mutations
列表,每个
mutation
都有带有
timestamp
:
在传统的关系型数据库术语里,这个有序的
mutations
列表可以被称作
“RODO log”
。
任何
reader
需要访问
MemRowSet
的
row
中的
mutations
,才能得到正确的快照。逻辑如下:
如果这行数据插入时的
timestamp
,不在
scanner
的
MVCC snapshot
里(即
scanner
快照指定的timestamp小于数据插入的时间戳,数据还没创建),忽略该行。如上如果不满足,将这行数据放入output
缓存里。
循环
list
里的
mutation
:
- 如果mutation的timestamp在MVCC snapshot里,在内存的缓存中执行这个更新。如果不在,则跳过此mutation。
- 如果mutation是一个DELETE操作,则在buffer中标记为已经被删除了,并清空之前加载缓存里的数据。
注意,
mutation
可以是如下的任何一种:
- UPDATE:更新value,一行数据里的一列或者多列
- DELETE: 删除一行数据
- REINSERT:重新插入一行数据(这种情况只在之前有一个DELETE mutation且数据在MemRowSet里时发生。)
举个真实例子,表结构
(key STRING, val UINT32)
,经过如下操作:
INSERT INTO t VALUES (“row”, 1); [timestamp 1]
UPDATE t SET val = 2 WHERE key = “row”; [timestamp 2]
DELETE FROM t WHERE key = “row”; [timestamp 3]
INSERT INTO t VALUES (“row”, 3); [timestamp 4]
在
MemRowSet
中,会有如下结构:
注意,当更新过于频繁时,会有如下的影响:
readers需要追踪linked list指针,导致生成很多CPU cache任务。更新需要追加到linked list的末尾,导致每次更新的时间复杂度是O(n)。
考虑到如上低效率的操作,我们给出如下假设:
Kudu
适用于相对低频率更新的场景,即假设数据不会过于频繁的更新。
整个数据中,只有一小部分存于
MemRowSet
中:一旦
MemRowSet
达到一定阈值,它会被
flush
到disk。因此即使
MemRowSet
的
mutation
会导致性能低,也只是占用整体查询时间的一小部分。
MemRowSet Flushes
当
MemRowSet
满了,会触发
Flush
操作,它会持续将数据写入
disk。
数据flush到disk成了CFiles文件(参见src/kudu/cfifile/README)。数据里的每行都通过一个有序的rowid标识了,而且这个rowid在DiskRowSet中是密集的、不可变的、唯一的。举个例子,如果一个给定的DiskRowSet包含有5行数据,那么它们会以key上升的顺序被分配为rowid0~4。不同的DiskRowSet,会有不同的行(rows),但却可能有相同rowid。
读取时,系统会使用一个索引结构,把用户可见的主键key和系统内部的rowid映射起来。
注意:rowid不是精确的跟每行数据的data存在一起,而是在这个cfifile里根据数据有序的index的一个隐式识别。在一部分源码中,将rowid定义为 “row indexes” 或者 “ordinal indexes”。
Historical MVCC in DiskRowSets
为了让
on-disk data
具备
MVCC
功能,每个
on-disk
的
Rowset
不仅仅包含当前版本
row
的
data
,还包含UNDO的记录,如此,可以获取这行数据的历史版本
当用户想读取
flush
后最新版本的数据时,只需要获取
base data
。因为
base data
是列式存储的,这种查询性能是非常高的。如果不是读取最新数据,而是time-travel
查询,就得回滚到指定历史时间的一个版本,此时就需要借助UNDO record
数据。
当一个查询拿到一条数据,它处理
MVCC
信息的流程是:
- 读取base data
- 循环每条UNDO record:如果相关的操作timestamp还未提交,则执行回滚操作。即查询指定的快照timestamp小于mutation的timestamp,mutation还未发生。
举个例子,回顾一下之前
MVCC Mutations in MemRowSet
章节例子的一系列操作:
当这条数据
flush
进磁盘,它将会被存成如下形式:
每条
UNDO record
是执行处理的反面。例如在
UNDO record
里,第一条
INSERT
事务会被转化成DELETE。
UNDO recod
旨在保留插入或者更新数据的时间戳:查询的
MVCC
快照指定的时间早于
Tx1时,Tx1
还未提交,此时将会执行
DELETE
操作,那么这时这条数据是不存在的。
最常见的场景是查询最新的数据。此时,需要优化查询策略,避免处理所有的UNDO records。
为了达到这个目标,我们引入文件级别的元数据,指向UNDO record
的数据范围。如果查询的
MVCC
快照符合的所有事务都已经提交了(查询最新的数据),这组deltas
就会短路(不处理
UNDO record
),这时查询将没有MVCC
开销。
Handling mutations against on-disk files
更新或者删除已经
flush
到
disk
的数据,不会操作
MemRowSet
。它的处理过程是这样的
:
为了确定update/delete的
key
在哪个
RowSet
里,系统将巡视所有
RowSet
。这个处理首先使用一个区间
tree
,去定位一组可能含有这key
的
RowSet
。然后,使用
boom fifilter
判断所有候选
RowSet
是否含有此
key
。如果某一些RowSet
同时通过了如上两个
check
,系统将在这些
RowSet
里寻找主键对应的
rowid
。
一旦确定了数据所在的
RowSet
,
mutation
将拿到主键对应的
rowid
,然后
mutation
会被写入到一个称为
DeltaMemStore的内存结构
中。
一个
DiskRowSet
里就一个
DeltaMemStore
,
DeltaMemStore
是一个并行
BTree
,
BTree
的
key
是使用rowid和
mutation
的
timestamp
混合成的。查询时,符合条件的
mutation
被执行后得到快照
timestamp对应数据,执行方式与新数据插入后的mutation
类似(
MemRowSet
)。
当DeltaMemStore存入的数据很大后,同样也会执行flush到disk,落地为DeltaFile文件
。
DeltaFile
的信息类型与
DeltaMemStore
是一致的,只是被压实和序列化在密集型的磁盘里。为了
把数据从base data更新成最新的数据,查询时需要执行这些DeltaFile里的mutation事务
,这些
DeltaFile
集合称作REDO
文件,而
fifile
里这些
mutation
称作
REDO record
。与存于
MemRowSet
里的
mutation
类似,当读取比base data
更新版本的数据时,它们需要被一次应用(执行)。
一条数据的
delta
信息可能包含在多个
DeltaFile
文件,这种情况下,
DeltaFile
是有序的,后边的变更会优先于前边的变更。
注意
,
mutation
存储结构没必要包含整行的数据。如果在一行中,仅仅只有一列数据被更新,那么mutation结构只会包含这一列的更新信息。不读取或者重写无关的列,这样更新数据操作就快而有效率。
Summary of delta file processing
总结一下,每个
DiskRowSet
逻辑上分三部分:
- Base data:MemRowSet flush到DiskRowSet时的最新数据,数据是列式存储的。
- UNDO records:历史数据,用来回滚到Base data之前一些历史版本数据。
- REDO records:Base data之后的一些更新数据,可以用来得到最新版本的数据。
- UNDO record 和REDO record存储格式是一样的,都称为DeltaFile。
Delta Compactions
当
DeltaFile
里的
mutation
堆积越来越多,读取
RowSet
数据效率就越来越低,最坏情况,读取最新版本数据需要遍历所有REDO record
并与
base data merge
。换一句话说,如果数据被更新了太多次,为了得到最新版本的数据,就需要执行这么多次的mutation
。
为了提高读取性能,
Kudu
在后台将低效率的物理布局转化成更加高效的布局,且转化后具有同样的逻 辑内容。这种转化称为:delta compaction。它的目标如下:
- 减少delta fifiles数量。RowSet flush的delta files文件越多,为了读取最新版本数据所要读取的独立的delta files就越多。这个工作不适于放在内存中(RAM),因为每次读取都会带有delta file的磁盘寻址,会遭受性能损失。
- 将REDO records迁移成UNDO records。如上所述,一个RowSet包含了一个base data,且是按列存储的,往后一段是UNDO records,往前一段是REDO records。大部分查询都是为了获取最新版本的数据,因此我们需要最小化REDO records数量。
- 回收old UNDO records。UNDO recods只需要保存用户设定最早时间点后的数据,这个时间之前的UNDO record都可以从磁盘中移除。
注意:
BigTable
的设计是
timestamp
绑定在
data
里,没有保留
change
信息(
insert update delete
); 而kudu
的设计是
timestamp
绑定在
change
里,而不是
data
。如果历史的
UNDO record
被删除,那么将获取不到某行数据或者某列数据是什么时候插入或者更新的。如果用户需要这个功能,他们需要保存插入或者更新的timestamp
列,就跟传统关系型数据库一样。
Types of Delta Compaction
delta campaction
分
minor
和
major
两种。
Minor compaction
是多个
delta file
的
compaction
,
不会包含base data,compact生成的也是delta file
。
Major compaction
是对
base data
和任意多个
delta file
的
compact
。
Major compaction
比
minor compaction
更耗性能,因为它需要读取和重写
base data
,并且
base data比delta data
大很多(
因为base data存了一行数据,而delta data是对某一些column的mutation
,需要注意的base data
是列式存储的,
delta data
不是)。
Major compaction
可以对
DiskRowSet
里的任意多个或者一个
column
进行
compact
。如果只有一列数据进行了多次重要的更新,那么compact
可以只针对这一列进行读取和重写。在企业级应用中会经常遇到这种情况,例如更新订单的状态、更新用户的访问量。
两种类型的
compaction
都维护
RowSet
里的
rowid
。因为它们完全在后台执行,且不会带锁。
compact的结果文件会采用原子swapping
的方式被引入进
RowSet
。
Swap操作结束后,compact前的那些老文件将会被删除
。
Merging compactions
随着越来越多的数据写入
tablet
,
DiskRowSet
数量也会累积的越来越多。如此这般将会降低
kudu
性能:
1.
随机访问(通过主键获取或者更新一条数据),这种情况下,每个
RowSet
只要它的
key
范围包含了这个主键,将各自去定位主键的位置。Boom filter可以缓解一定数量的物理寻址,但是特大的
bloom filter
访问会影响到
CPU
,并且同样会增加内存消耗。
2.
查询一定
key
范围数据(例如查询主键在
A
与
B
之间的数据),此时,每个
RowSet
,只要它的
key
范围与提供的范围重叠,将各自去寻址,不使用bloom filter。专门的索引结构可能会有帮助,但是同样会消耗内存。
3.
排序查询,如果用户要求查询的结果与主键有相同顺序,那么查询结果集必须经过一个
merge
过程。Merge
的消耗通常与输入的数据量成对数级增长,即随着数据量的增大,
merge
将越耗性能。
如上所述,我们应该
merge RowSet
以减少
RowSet
的数量:
与如上提到的
Delta Compaction
不同,请注意,
merging Compaction不会保持rowid一样
。这使得处理并发的mutation
错综复杂。这个过程在
compaction.txt
文件中有比较详细的描述。
Overall picture
Comparison to BigTable approach
1. kudu
中,一个给定的
key
只会存在于一个
tablet
的
RowSet
里。 在BigTable
里,一个
key
可以存在多个不同的
SSTable
里。
BigTable
的一整个
Tablet
类似于
kudu
的RowSet:读取一条数据需要
merge
所有
SSTable
里找到的数据(根据
key
),类似于
Kudu
,读取一条数据需要merge base data和所有
DeltaFile
数据。 Kudu的优势是,读取一条数据或者执行非排序查询,不需要
merge
。例如,聚合一定范围内的key可以独立的查询每个
RowSet(
甚至可以并行的
)
,然后执行求和,因为
key
的顺序是不重要的, 显然查询的效率更高。 Kudu的劣势是,不像
BigTable
,
insert
和
mutation
是不同的操作:
insert
写入数据至MemRowSet,而
mutation
(
delete
、
update
)写入存在这条数据的
RowSet
的
DeltaMemStore里。性能影响有一下几点:
a
)写入时必须确定这是一条新数据。这会产生一个
bloom filter查询所有
RowSet
。如果布隆过滤器得到一个可能的
match
(即计算出可能在一个
RowSet里),接着为了确定是否是insert
还是
update
,一个寻址就必须被执行。 假设,只要RowSet
足够小,
bloom filter的结果就会足够精确,那么大部分插入将不需要物理磁盘寻址。另外,如果插入的
key
是有序
的,例如
timeseries+“_”+xxx
,由于频繁使用,
key
所在的
block
可能会被保存在数据块缓存中。
b
)
Update
时,需要确定
key
在哪个
RowSet
。与上雷同,需要执行
bloom filter
。 这有点类似于关系型数据库RDBMS
,当插入一条主键存在的数据时会报错,且不会更新这条数
据。类似的,更新一条数据时,如果这条数据不存在也会报错。
BigTable
的语法却不是这样。
2. Mutation
操作磁盘数据,是通过
rowid
的,而不是实际意义上的
key
。 BigTable中,同一个主键数据是可以存在多个
SSTable
里的,为了让
mutation
和磁盘的存的
key
组合在一起,BigTable
需要基于
rowkey
执行
merge
。
Rowkey
可以是任意长度的字符串,因此对比rowkey是非常耗性能的。另外,在一个查询中,即使
key
列没有被使用(例如聚合计算),它们也要被读取出来,这导致了额外的IO
。复合主键在
BigTable
应用中很常见,主键的大小可能比你关注的列大一个数量级,特别是查询的列被压缩的情况下。 相比之下,kudu
的
mutation
是与
rowid
绑定的。所以
merge
会更加高效,通过维护计数器的方式:给定下一个需要保存的mutation
,我们可以简单的相减,就可以得到从
base data到当前版本有多少个
mutation
。或者,直接寻址可以用来高效的获取最新版本的数据。 另外,如果在查询中没有指定key
,那执行计划就不会查阅
key
,除了需要确定
key
边界情况。 举
例:
如上表的主键是
(host,unitx_time)
,在
kudu
里的执行伪代码如下:
sum = 0 foreach RowSet:
start_rowid = rowset.lookup_key(1349658729)
end_rowid = rowset.lookup_key(1352250720)
iter = rowset.new_iterator(“cpu_usage”) iter.seek(start_rowid)
remaining = end_rowid - start_rowid while remaining > 0:
block = iter.fetch_upto(remaining) sum += sum(block)。
获取
block
也非常的高效,因为
mutation
直接指向了
block
的索引地址。
3. timgstamp
不是数据模型里的一部分。 BigTable-like的系统中,每个
cell
的
timstamp
都是暴露给用户的,本质上组成了这个
cell
的一个符合主键。意味着,这种方式可以高效的直接访问指定版本的cell
,且它存储了一个
cell
的整个时间序列的所有版本。而Kudu
却不高效(需要执行多个
mutation
),它的
timestamp
是从
MVCC
实现而来的,它不是主键的另外一个描述。 作为替代,kudu
可以使用原生的复合主键来满足时间序列场景,例如主键(
host
,
unix_time
)。