Explore. Dream. Discover.

Samuel Chen's life

Twenty years from now you will be more disappointed by
the things that you didn't do than by the ones you did do.
So throw off the bowlines. Sail away from the safe harbor.
Catch the trade winds in your sails.

Explore. Dream. Discover.
—— Mark Twain

2010年5月23日星期日

非结构化的大数据存取

前两天和朋友聊到一个有意思的话题,那就是非结构化的大数据如何去存储。内容大概是这样:

一个数据库系统可以实现非结构化的存储,其方法是采用xml来定义数据,并将数据作为一个大数据字段。那么问题来了,当这个字段特别大的时候,比如1G,不能简单的载入到内存中,那么我们应该怎么做?

感觉这个挺有意思,就分析一下。

对于这个Feature,我们可以假定一些前提,

1、这是一个基于文件存储的数据库,并且该文件系统可以支持无限大的文件。

2、这个数据库系统是个key/value的,key是用于访问,value是用于存储真正的数据。key, value的访问api是基于行和位移的,其他的辅助信息不额外考虑。

3、数据是xml格式的,直接存储在value中。例如有个职员信息 info 是

<fname>Samuel</fname>
<lname>Chen</lname>
<address>
  <city>Beijing</city>
  <country>China</country>
</address>
<photo>
  <![CDATA[
    binary data of photo
  ]]>
</photo>


那么 select * from tab where fname = 'Samuel' 返回一个对象person(或文档对象),具有这么些个属性。(这种类型的数据库查询不一定适合用传统的关系型数据库的SQL,很有可能是xPath、query与SQL的结合,这里仅用来作为示意。)

问题来了,如果这个文档对象非常之大,达到了1G,那么当用户修改了一个字段,比如city变成了Shanghai,这样的一个功能,我们该如何实现?

分析一下,需要解决的问题有下面这么几个:

1、首先是大数据,再者是非结构化,也就是基于行列的存储是不合适的,那么该怎么存储才能使得空间足够也不浪费?

2、这么大的数据内容,直接载入到内存,显然是不太合适的,需要直接在文件中更新。

3、传统的关系型数据库操作只有CRUD,也就是增、删、查、改,其基本结构是字段,如果整个来更新是否合适?如果不整个更新,只更新修改的部分,怎么去实现。

按照这个思路,一个个来解决。

存取

一般来说,对于xml数据存储,是使用字符型blob(binary亦可)字段,可以防止空间浪费或不足。在用的时候读出,存的时候写入。那么当该xml文档非常大的时候,直接读取到内存中就不太现实,此时就需要直接在文件中访问。所以问题就成了这个key/value的系统怎么去设计才能完成这个任务。

要解决空间浪费或不足的问题,那么存储段就需要具有scalability,可以根据数据大小伸缩,因此可以将其设计成分段的,当需要的时候增加,不需要的时候就释放,如下


sequencekeystructure




value
offsetwe don't careheadlengthnextdeletedactual lengthdata
4B2B + 4KB1B (1b)16B(16777216TB)4B1B (1b)2B(max=64KB)64KB








0x00000001
1143360(140K)0x00000002 065536(64KB)xxx
0x00000002
01433600x00000003065536xxx
0x00000003
01433600012288(12KB)xx

如表格所示,key 是固定长度字段,我们在这里并不关心,所需的只是其长度,假定其总长度为 key_len.

Sequence 是每一个record的首地址,可以记录,也可以不记录,列在这里是因为我们需要这个值来做为基本偏移量。

Structure 是每个value所需要的信息,包括是否第一个数据段(head),value 总长是多少(length),下一个段偏移量(next),是否已经删除(deleted),以及本段真实长度(actual length)。通过这些,我们就可以通过计算来访问一块完整数据。

Value 是存储的真实数据,当然也可以有一些其他信息,比如encoding之类。

那么有了这么一个系统,我们就可以存储任意大小的数据(最大长度由length或者系统定义决定,本例中是16777216TB,实际上目前没这么大的).

例如我们有一块140KB的数据,那么存储的时候会分成如表格所示3段存储,前两段是满存储,最后一段存了12KB,有一定浪费,因此分段大小也是需要考虑的,过大过小都不好(4~8K比较合适),也可以通过让用户配置来决定。当然,如果用一个单独的文件来存储,甚至可以不必分段,只需维护一个偏移+长度表即可,这里就不讨论这种方法了。

当需要修改时,如果数据长度增加了,变成了200KB,我们就可以通过修改偏移量、长度,并增加新段来解决,如表


0x00000001
1204800(200K)0x00000002 065536(64KB)xxx
0x00000002
02048000x00000003065536xxx
0x00000003
02048000x00000005065536xxx
0x00000004
1102400010240yyy
0x00000005
0204800008192xx
同样的,删除也可以通过标记段已删除来实现,下次再变大的时候,可以继续使用,或者用新的段来代替。多次删除之后,可能会有大量的删除段存在,这就需要重整,亦或使用其他方法来防止大量删除段的出现。这些细节以及实现在这里暂不细说了。

这样,我们就解决了第一个问题,如何去存储这种非结构化大数据。当然,各个数据库都有自己的实现,性能优劣各不相同,也可参考。

访问

数据库访问一般来说都是由各数据库厂商提供api,比如oracle的OCI/Pro* C。 也有一些通用的包装,如ODBC,JDBC等。无论是哪种方式,当你在程序中使用的时候,访问的都是内存中的buffer,例如


idemp_iddept info
9527samcPB<fname>Samuel</fname>
<lname>Chen</lname>
<address>
  <city>Beijing</city>
  <country>China</country>
</address>
<photo>
  <![CDATA[
    binary data of photo
  ]]>
</photo>

此时的各个字段如 id, emp_id, dept 都是存在于内存的buffer之中,当你使用api访问行、列或者名字的时候(比如ado的recordset),api会计算并返回相应的值。

当碰到非常大的数据时,怎么办?比如info里面有个人信息,还包括照片等等,达到了几十、百兆甚至上G,那么一个查询取出几百条数据,总共的数据超过了内存限制,该怎么办?

这个问题涉及到了两个层面,一个是多行数据过大,一个是单个数据过大。对于第一个问题,多行数据过大,一般来说API都有考虑,会再访问的时候再去取,或者预读等等,暂且不提,这里咱们讨论第二个问题。

其实单个数据的访问也是可以在访问的时候去取的,也就是所谓的access on demand。我们可以在buffer该数据内容中存储访问信息而不是真实数据,在真正用户用到的时候再去数据库取,如表


idemp_iddept
infoinfo_data
9527samcPB
key | offset | len | ...
null 

这样,在访问到的时候,将数据从磁盘读到内存,并更新内存buffer为:

idemp_iddept
infoinfo_data
9527samcPB
loaded...
0x03677d2d ->

当然,这个内存结构和存储的信息可以根据实际情况来定义。

无可否认的是,这个方法是有效率问题的,时间换空间。可以根据实际情况考虑采用预读,比如当用户使用单向遍历(如ado.net的IDataReader)时,可以在内存允许的范围内预读接下来的N条。

修改

还是这个例子,如果我们修改了个人信息,把city改为Shanghai,按照传统做法,是要update info,把整个info字段更新一下。


<fname>Samuel</fname>
<lname>Chen</lname>
<address>
  <city>Shanghai</city>
  <country>China</country>
</address>
<photo>
  <![CDATA[
    binary data of photo
  ]]>
</photo>


那么在该数据系统中,仅仅因为更新了整块数据中非常小的一部分(几个字节),我们就需要修改整块大数据(1G),这是非常影响效率的。

因此,我们可以计算该修改的位置,看它是处于哪一个数据块,而仅仅修改一个数据段,从而大幅提高效率。同时,如果修改的范围过大,可以考虑整个更新。这些都需要记录修改的信息来实现。


0x00000001
1204800(200K)0x00000002 065536(64KB)xxx (仅修改该段)
0x00000002
02048000x00000003065536xxx
0x00000003
02048000x00000005065536xxx
0x00000004
1102400010240yyy
0x00000005
0204800
008192xx

以上的方法可以适用于binary或者字符blob,但是,由于在这个case中,我们考虑的是xml数据,所以需要有一些更进一步的细节考虑。比如根据xml parser实现的不同,数据不一定是按节点顺序存放的,而且节点也不一定是有序的。因此,需要在每个节点的meta中存储段信息(或者其他的方法,根据xml parser/dom的不同具体考虑)

总结

这样,我们就能够很好的处理非结构化大数据的存储和访问了。

当然,这只是个原型,还有数据库的transaction,log等等许多问题需要考虑,同时perfomance也需要长时间的优化。

标签: , , , ,

2010年5月17日星期一

.Net Frameowrk 4.0 Compatibility

关于pb如何支持 deploy 到.NET 4.0的一个research

标签: , , ,