http://www.yankay.com/google-dremel-rationale/
在看Dremel paper之前, 觉得这是一个神奇的技术, 对于海量的数据可以在秒级别给出查询结果.
但答案是, Dremel的速度是建立在大量的并发上的, 做个简单的算术, 磁盘的顺序读速度在100MB/S上下,那么在1S内处理1TB数据,意味着至少需要有1万个磁盘的并发读.
所以Google真是神奇的公司, 总能用看上去很平淡的技术创造奇迹
为什么需要Dremel?
这个问题看起来有些傻, 谁不想在秒级别对PB级别的数据进行查询...兼顾海量和速度.
场景(引用上文),
我们的美女数据分析师,她有一个新的想法要验证。要验证她的想法,需要在一个上亿条数据上面,跑一个查询,看看结果和她的想法是不是一样,她可不希望等太长时间,最好几秒钟结果就出来。当然她的想法不一定完善,还需要不断调整语句。然后她验证了想法,发现了数据中的价值。最后,她可以将这个语句完善成一个长期运行的任务
从现在看, Dremel并不是用来替代MR, 而是辅助, 所谓的交互分析, 用于算法验证和设计阶段, 最终再使用MR的长期任务. 使用Dremel来开发数据分析模型,MapReduce来执行数据分析模型。
而且Dremel的高效来自他的按列读, 而MR是需要读所有数据的, 测试如下
Dremel的按列读的MR只需要读0.5TB的数据,而MR按行需要读87TB
所以Dremel可以用来分析MapReduce的结果集,只需要将MapReduce的OutputFormat修改为Dremel的格式,就可以几乎不引入额外开销,将数据导入Dremel.
嵌套结构数据的按列存储 (nested, column-based)
这可以说是Dremel Paper的核心技术, 按列存储不是个新概念, HBase也是column-based, 有区别吗?
我的理解是, 首先列存储的思路是没有区别的, 都是为了提高读取效率, 压缩效率...
但是问题是怎么样实现列存储, 并保证在读数据的可以把各个列读出的数据拼成row record?
对于关系型数据库, 这个很容易, 规范化的数据, 数据怎么写, 按顺序读出来, 自然是对齐的
对于HBase, 数据模型相对简单, 基于kv, 读取的时候用row key可以检索出该row所有的数据, key一定对应一个value, 不会多个或没有
对于Dremel, 较复杂的数据模型文档类型, 存储格式是Protocol Buffers(类似Json), 这种支持嵌套的数据结构比较复杂, 会出现repeat或option的field, 之前没有相关的技术可以支持
下图是这份数据在Dremel实际的存储的格式。
直接看例子, 对于一个doc, 在Links中的Forward或Backward都是可以有多个的(repeat情况)
而在Name中的Url就是可有可无(optional情况)
在对照下面实际按列存储的格式, 如果没有特殊的表示, 如何能恢复出回来的文档格式?
对于Json这种非规范化的数据, 里面有很多的optional和repeated数据, 这个确实比较难解决. 你可以try想一想是否有比较好的解决方法...
Google的方法就是用repetition level和Definition level来区分, 其中 repetition level用来表示repeat关系, 即嵌套关系, 而Definition level用来表示optional关系
Repetition levels
what repeated field in the field’s path the value has repeated
在fields的路径上, 从第几个repeated field开始, 和之前有重复 (只考虑repeated field)?
举例, Name.Language.Code, path上有两个repeated fields, name, language
所以对于code, repetition level, 只可能为0, 1, 2
0, path上的field都是首次出现, 不存在repeat
1, name已经第二次或多次出现, 在name级别存在repeat
2, language级别的repeat
对于'en-us’, path上的repeated fields(name, language)之前都没有出现过, 所以rl=0
对于'en’, name首次出现不存在重复, language出现repeat, 所以rl=2
对于'en-gb’, name出现repeat, 所以rl=1 (虽然language也repeat, 但rl以高级别优先, 因为name的repeat必然导致language的repeat)
Definition levels
how many fields in path that could be undefined (because they are optional or repeated) are actually present
在field path上有几个optional or repeated field, 被实际定义? (注意, 只统计optional or repeated field, 而不考虑required field)
为何不直接用0,1来表示当前field是否被定义?
因为要同时考虑祖先field的情况, 比如对于图中2的null, 不光code field没有定义, language field也没有被定义, 所以需要用dl=1来表示表达这种情况
否则光用0,1, 只知道code是否被定义, 这是不够的
为何需要null填充?
从下面的例子看, 对于Name.Language.Code, 第一个unll不加的话, 恢复的时候, en-gb就会被加到第二个Name里面, 而不是第3个name, 因为无法知道第二个里面的是空的, 所以对于optional的field一定要加Null作为占位符.
举例, Name.Language.Country
path上, name, language是repeated field, 而country为optional field, 所以dl范围为1,2,3
对于图中1的Null, path上Name.Language, 都实际存在, 所以dl为2
对于图中2的Null, path上只有Name实际存在, 所以dl为1
而对于'us’, Name.Language.Country, 所有field都是实际存在的, 所以dl为3
但对于Name.Language.Code
path上, code是required field, 所以dl只考虑name和language, 取值的范围为1,2
Encoding
Each column is stored as a set of blocks. Each block contains the repetition and definition levels (henceforth, simply called levels) and compressed field values.
NULLs are not stored explicitly as they are determined by the definition levels: any definition level smaller than the number of repeated and optional fields in a field’s path denotes a NULL.
Definition levels are not stored for values that are always defined.
Similarly, repetition levels are stored only if required; for example, definition level 0 implies repetition level 0, so the latter can be omitted.
In fact, in Figure 3, no levels are stored for DocId.
Levels are packed as bit sequences. We only use as many bits as necessary;
for example, if the maximum definition level is 3, we use 2 bits per definition level.
实际存储的时候, 每个column存储成block, 包含rl, dl和压缩过的field value
Null不用实际存储value, 从dl就可以判断出是否是null
并且为了节省空间, rl和dl只有当真正需要的时候才会存储, 并且level的存储的实际bit数取决于实际情况.
Splitting Records into Columns
The next challenge we address is how to produce column stripes with repetition and definition levels efficiently.
数据稀疏性问题
As illustrated earlier, repetition and definition levels may need to be computed even if field values are missing.
Many datasets used at Google are sparse; it is not uncommon to have a schema with thousands of fields, only a hundred of which are used in a given record.
Hence, we try to process missing fields as cheaply as possible.
To produce column stripes, we create a tree of field writers, whose structure matches the field hierarchy in the schema.
The primary job of the DissectRecord procedure is to maintain the current repetitionLevel.
The current definitionLevel is uniquely determined by the tree position of the current writer, as the sum of the number of optional and repeated fields in the field’s path.
有空具体来研究算法
Record Assembly
在读取恢复的时候, 使用finite state machine (FSM), 可以参考下面的状态机
每个状态都是代表列, 通过repetition level经行状态迁移, 如果理解了上面的rl和dl, 应该不难理解
比如Name.Language.Code, 读完总要读Name.Language.Country, 所以0,1,2都迁移过去, 而Name.Language.Country如果是2, 说明还有repeated的language层, 所以迁徙到Name.Language.Code
当然这需要自动产生FSM的算法
并行查询技术
这是Google的强项, 无数积累, 借鉴了Web搜索中的“查询树”的概念,将一个相对巨大复杂的查询,分割成较小较简单的查询。大事化小,小事化了,能并发的在大量节点上跑.
同时要考虑fault toleration, 一个节点的slow会拖慢整个查询.
其次, Dremel可以提供了一个SQL-like的接口,就像Hive和Pig那样
这个不是文章的重点, 因为这些在google看来, 没有啥好说的