4

假设我有一个这样的 .proto 结构(简化)

Message DataItem {
  required string name = 1;
  required int32 value = 2;
}

Message DataItemStream {
  repeated DataItem items = 1;
}

服务器将生成DataItemStream并将其写入磁盘。我们加载这个文件,一切都很顺利,没有问题。

这对我们来说效果很好,但我们的客户群已经增长,生成文件流的软件的使用也在增长。

问题出现了,因为重复items字段可以有成千上万个项目,但我们只对其中的一个子集感兴趣。我们已经挖了一点,只看到了遵循谷歌流式建议的解决方案(向我们存储的 s添加大小 前缀DataItem,然后单独解析每条消息或使用 CodedInputStream/CodedOutputStream或编码二进制线格式(base64)和用换行符分隔,那么我们就可以很容易地得到我们感兴趣的子集。

这些中的任何一个都对我们有用,但需要对生产代码进行一些更改以更改文件的保存方式(基于服务器的代码已经很长时间没有更改并且被他们的管理层认为几乎无法触及(在他们看来,不要'如果没有损坏就不要修复它)...)

我们已经为以不同方式流式传输消息的服务器重新创建了模块,但收到了来自那些维护人员关于推送我们的更改的抨击。对我们来说,根据需要更改代码要容易得多(政治上),因为我们可以完全控制其开发周期。

有没有办法仍然使用这个原始的消息流,但只选择要加载的消息子集是智能的?(如果这很重要,我们真的不在乎我们必须使用哪种语言,我们有 c++、python、java 和 .NET 方面的经验(按经验顺序))

4

5 回答 5

1

我会将此视为数据库问题:您有一个文件表示具有单个记录 (DataItems) 的表 (DataItemStream)。您似乎想从表中选择连续的 DataItems 范围。这意味着 DataItemStream 中 DataItem 的顺序很重要,实际上它编码了一个隐藏的主键 - DataItemStream 中 DataItem 的“数组”索引也就是行号。

在大多数数据库中,在数组数据结构中,每一行(或数组项)占用的空间相同,因此访问第 n 项很容易。但是,放在 DataItemStream 中的 DataItems 是可变长度的,所以这种简单的方法是行不通的。

使用数据库隐喻,另一种有效查找记录的方法是使用索引——本质上是另一个表,但要小得多,其中包含指向主要数据结构的指针。索引通常构造为(PK,指针)元组的表。在这种情况下,您可以拥有一个索引文件,它本质上是一个 int32 的内存映射数组。索引中的每个值都指向数据文件中该 DataItem 记录开始的字节偏移量。

例如,如果数据文件有 1m 条记录,那么您的索引将为 4MB(1m 条记录 * len(int32) = 1m * 4 字节)。如果您需要扫描数据文件中的记录 777777 到 888888,您:

  1. 读取索引以获取 DataItemStream 中感兴趣的字节范围。请注意,查找操作确实非常快:
    1. 打开索引文件
    2. 寻找(例如在 Java RandomAccessFile.seek() 中,在 Python fileObject.seek() 中)起始索引 int32 (777777*4) 并读取它。这是起始字节偏移量
    3. 寻找结束索引 int32 (888888*4) 并读取它。这是结束字节偏移
    4. 关闭索引文件
  2. 读取索引指定的DataItemStream文件的字节范围:
    1. 打开 DataItemStream 文件
    2. 在文件中寻找起始字节偏移量
    3. 读取流直到结束字节偏移(记得减1)
    4. 关闭 DataItemStream 文件

2. about 的一种稍微不同的方法可能是首先为指定的字节范围创建一个新文件。该文件现在包含那些感兴趣的记录。

索引文件是如何创建的?

编辑:PB 格式的描述:实际索引文件的构建可以通过简单地传递数据文件来生成。所有字段都以字节开头,消息类型后跟段。以“特殊”方式编码,使用每个字节的 MSB 作为延续信号,如此所述。这意味着可以避免几乎所有数据格式的复杂性,因此索引器可以非常简单。

您可以将索引文件视为缓存 - 如果存在,您的代码库可以使用最新的索引,或者如果缺少它,则自动创建它。

这种方法允许索引感知的代码有效地进行,并且不会更改任何遗留程序的数据格式。

于 2012-12-10T01:40:21.580 回答
0

鉴于您在评论中对我的问题的回答:文件的格式是什么?您可以在搜索到任意位置后同步到项目的开头吗?

面对类似的情况(超过 1 GB 的日志文件,在特定时间间隔内查找日志条目),我们最终对文件进行了二进制搜索。这有点棘手,而且代码不是真正可移植的,但基本思想是确定文件的长度(stat在 Unix 下使用,或在 Windows 下使用它的等价物),然后打开文件(在 Windows 下以二进制模式) ; 寻找到中间,向前扫描下一条记录的开始,然后与我们正在寻找的内容进行比较,依此类推,通过我们是在我们想要的位置后面还是前面来确定下一个寻找位置。如果您可以在从任意位置(这又取决于文件的格式)开始时找到记录的开头,则此方法有效。

于 2012-12-06T16:48:44.693 回答
0

如果您的主要瓶颈是 RAM 而不是磁盘访问速度,为什么不插入一个代理/过滤器,逐条读取整个文件消息(我的意思是DataItem消息),但只保留和转发您感兴趣的部分?听起来它可以缓冲您感兴趣的整个部分,而不会冒溢出的风险。

如果您能弄清楚如何检测消息中流的开始,您还可以改进代理以查找缓冲区文件,但这是一种性能改进,不会影响代理与其余部分的接口管道,或最大内存占用。

您的问题是,由于DataItem消息嵌入在 a 中DataItemStream,因此 google API 会强制您一次性加载整个消息DataItemStream。为了避免这种情况,您可以编写一些丑陋的代码来跳过DataItemStream信封并公开序列,DataItem就好像它们没有嵌入一样。这将取决于 PB 序列化的内部结构,但由于您的客户非常重视稳定性,您可以指望它不会很快改变。如果它确实发生了变化,那么是时候推送您喜欢的消息布局并切换到您已经开发的解决方案了。

如果实际的消息格式没有比您显示的复杂得多(例如,没有太多的可选字段或多级嵌入消息),那么导航文件布局应该很简单CodedInputStream(无需更改当前文件布局)。阅读第一个DataItem 应该只是从 and 开始skipRawBytes()skipField()然后阅读每条消息readMessage()

但我知道谷歌 API 的设计目的不是从单个流中读取消息序列,所以这可能太简单了。如果是这样,您应该仍然能够找到第一个字段 1 的偏移量DataItem,使用 读取它readString(),根据需要前进到字段 2 的开头,使用 读取它readInt32()等等。然后您的代理可以丢弃该消息或重新组合它并将其传递给其余代码。

我想你已经想到了这些方面的东西,所以这种方法可能出于某种原因不可行或不可取?或者也许它太丑了,你宁愿处理改变文件布局的政治成本......

于 2012-12-06T18:30:40.147 回答
0

恐怕,您必须自己进行外部 PB-stream 解码。一旦你找到了你的物品的开始(希望没有包装),你就可以对单个物品使用 PB。然后你就可以处理无穷无尽的流了。当然你必须做一些认真的测试,但我会试一试。

于 2012-12-10T23:34:01.943 回答
0

如果您确定使用正确规范化的模式运行的真正数据库引擎无法完成这项工作(我自己测试一下,数据库引擎人员一直在考虑如何解决这样的难题) ,然后试试这个:

批量处理足够多的记录,以使集合的最小字段的数据至少占据相当多的扇区。将这些记录中字段的数据写入以分隔从 4KB 边界开始的连续块,在每组的前面都有一个标题,说明在哪里可以找到块边界。然后只读取您想要的字段的块。

如果您确实需要性能,但仍然有一些棘手的问题,请将这些块放在不同磁盘上的单独文件中。在您跳过非常大的数据块之前,跳过这些内容的速度与阅读这些内容一样慢。

编辑:我看到您不想更改磁盘格式,但是由于您试图避免读取不需要的数据,因此实际上别无选择。

于 2012-12-15T04:34:12.080 回答