领先的免费Web技术教程,涵盖HTML到ASP.NET

网站首页 > 知识剖析 正文

CSV Parquet Avro:为正确的工作选择合适的工具

nixiaole 2024-11-17 00:24:20 知识剖析 21 ℃


最近在SSENSE,负责构建推荐引擎一部分的我的团队"大砍刀"处理了一项我们必须准备并使用相对较大的数据集的任务。 数据还不够大,无法保证使用Spark或Hadoop MapReduce之类的东西,但它又大到足以迫使我们考虑分别存储和读取此数据的时空复杂性。 简而言之,我们需要一种可以合理压缩并提供快速读取时间的文件格式。 在研究此问题的解决方案时,我有机会探索了一些用于存储数据的常用文件类型的内部工作原理。 在本文中,我将分享我的笔记和从该过程中学到的东西。

竞争者

测试的3种文件类型是:

· CSV

· Parquet

· Avro

CSV

为了测试CSV,我生成了一个伪造的目录,其中包含约70,000种产品,每种产品都有特定的分数和任意字段,只是向文件添加了一些额外的字段。

鉴于我们的要求极低,因此文件仅包含时间戳,产品ID和产品评分。

示例输出为:

spring_exclusives.csv

timestamp,product_id,product_score

1555480889,46260,0.3381890166444166

1555480889,85461,0.34786700678437343

1555480889,85462,0.2932997952484385

1555480889,113723,0.3803736086217753

1555480889,113724,0.39684375226672763

1555480889,113727,0.38768682121073605

最后结果

一个大约有70,000行的文件,大小为1.3MB。 使用Node.js内置的fs createReadStream,整个文件解析和处理每一行大约需要122毫秒。

十分简单!

更大的数据集

CSV适用于70,000行的小型数据集。 但是,如果您需要增加到200万条记录怎么办? 如果每个记录都具有嵌套属性怎么办? 尽管CSV文件简单易读,但不幸的是它们无法很好地缩放。 随着文件大小的增加,加载时间变得不切实际,并且无法优化读取。 如上所示,它们的原始性质使它们成为较小数据集的理想选择,但对管理较大数据集非常不便。 这是Parquet和Avro都加入的地方。

以下示例假设一个假设的场景,该场景尝试存储成员及其品牌色彩偏好是什么。 例如:Sarah的ID为10,她非常喜欢红色的Nike(ID 1),蓝色的Adidas(ID 2)和绿色的Reebok(ID 3)。

Parquet

Apache Parquet定义为:

"无论数据处理框架,数据模型或编程语言的选择如何,Hadoop生态系统中任何项目均可使用的列式存储格式"

列存储与基于行的存储从根本上不同。 尽管基于行的存储已针对读取一系列行进行了优化,但对列式存储进行了优化以选择一部分列。 让我们看一些代码。 可以在Parquet的网站上找到详细的文档。

使用Parquet,我创建了以下形状的嵌套模式:

var schema = new parquet.ParquetSchema({ memberID: { type: 'UINT_32'}, brand_color: { repeated: true, fields: { brand: { type: 'UINT_32'}, color: { type: 'UTF8' }, score: { type: 'FLOAT'} } }, timestamp: { type: 'TIMESTAMP_MILLIS' } });

这样,我生成了一个文件,该文件包含2,000,000个成员,每个成员具有10个品牌颜色首选项。 总文件大小为335 MB。

为了稍微减小文件大小,我应用了快速的编解码器压缩。 这样做是微不足道的,因为这只是对模式的一小部分更改。

var schema = new parquet.ParquetSchema({ memberID: { type: 'UINT_32'}, brand_color: { repeated: true, fields: { brand: { type: 'UINT_32', compression: 'SNAPPY'}, color: { type: 'UTF8', compression: 'SNAPPY' }, score: { type: 'FLOAT', compression: 'SNAPPY'} } }, timestamp: { type: 'TIMESTAMP_MILLIS' }, });

文件大小从335 MB减少到69.7 MB。

我的脚本花了大约50秒钟才能浏览整个文件,仅需简单阅读即可。 如前所述,Parquet的主要优点是它使用了列式存储系统,这意味着如果只需要每个记录的一部分,则读取的延迟会大大降低。 这是10次读取的结果:

通过只拔出memberID列而不是整个记录,读取时间从50秒变为仅1秒!

这是只提取memberID的读取函数:

async function testParquetRead(){ 
	let reader = await parquet.ParquetReader.openFile('test.parquet'); 
  let cursor = reader.getCursor(['memberID']); 
  let record = null; 
  let i = 0; 
  let lastone; 
  while(record = await cursor.next()) { 
    lastone = record; i++; 
  } 
  await reader.close(); 
  console.log(lastone); 
  console.log(i);
}

我决定运行10次,以便对结果有一定的意义,并确保1秒不仅仅是一次。

但是,就我们而言,我们一直都需要整个记录,因此这并不是什么优势。

Avro

Avro的一大优势是架构,它比Parquet的架构要丰富得多。 它使我们能够通过添加,删除或修改记录的列来发展模式,这比Parquet更加容易。

根据Avro自己的网站:

" Avro依赖于架构。 读取Avro数据时,将始终存在写入数据时使用的架构。 这样就可以在没有每个值开销的情况下写入每个数据,从而使序列化既快又小。 这也方便了动态脚本语言的使用,因为数据及其模式完全是自描述的"

此外,Avro的存储基于行而不是列。 这使得它更适合读取整个行系列。

使用Avro,我创建了一个具有以下形状的非常相似的嵌套模式:

var schema = new avro.Type.forSchema({ type: 'record', fields: [ {name: 'memberID', type:'int'}, { name: 'brand_color', type: { type: 'array', items: { "type": "record", "name":"brandColor", "fields":[ {name: 'brand', type: 'int'}, {name: 'color', type: 'string'}, {name: 'score', type: 'float'} ] }, } }, {name: 'timestamp', type:'long'} ] });

使用此架构,然后生成了一个文件,其中包含2,000,000个具有10个品牌颜色首选项的成员。 文件总大小为181 MB,完全没有压缩,仅为Parquet文件的一半!

尽管这看起来很有希望,但是为了测试它的缩放比例,我决定将品牌颜色偏好增加10倍,对于2,000,000个成员,最多增加100倍,以查看此文件的大小。 数据的增加也密切反映了我们的实际需求。 文件总大小增加到1.5GB。 虽然内存占用量的增加几乎与数据大小的增加成正比,但对于快速访问和读取而言,它太大了。

下一步是进行一些压缩,看看可以将文件缩小多少。 我决定使用与Parquet测试中使用的相同的快速代码进行尝试。 应用代码是微不足道的,所有必要的就是从以下位置更改写入:

var writer = avro.createFileEncoder('test.avro', schema);

var writer = avro.createFileEncoder('test.avro', schema, {
  codec: 'snappy', codecs: {
    snappy: snappy.compress
  }
});

结果:

文件大小为93MB! 先前大小的一小部分。 仅使用10个品牌颜色偏好,文件大小就减少到只有16.3 MB。

以下JavaScript代码遍历整个文件,将每一行变成一个JSON对象,并对操作进行基准测试。

function testAvroRead(schema){ 
  let i =0; 
  console.time('read') 
  let lastone; 
  avro.createFileDecoder('test.avro', {
    codecs: {
      "snappy": snappy.uncompress 
    }
  }) 
  .on('metadata', (data)=> { console.log(data); })
  .on('data', (row) => { lastone = schema.toString(row); i++; })
  .on('end', () => { console.log(i); console.log(lastone) console.timeEnd('read') 
                   });
}

请注意,传递到testAvroRead方法的架构与上面定义的架构相同。 这保证了模式验证,该验证必须使用Parquet明确执行。 整个文件的读取,解压缩和验证花费了不到3分钟的时间。 运行读取速度测试10次以确保结果显着,并且输出如下:

虽然我们在读取速度上有些损失,但我们在存储上获得了更多的东西,并且免费获得了验证!

结论

对于我们的特定案例,赢家显然是Avro。 但是请注意,这不会以任何方式抹杀Parquet。 SSENSE的数据湖在很大程度上依赖于Parquet,考虑到我们处理不可变的数据和分析查询(列式存储是最佳的),这很有意义。

所有这些概念证明都是使用Node.js和JavaScript生成的。 这使我们能够以健壮和可扩展的方式压缩许多数据。 内置的架构会检查每条记录,如果什么时候与定义不匹配,则会引发错误,从而使验证变得轻而易举。

(本文翻译自Mikhail Levkovsky的文章《CSV vs Parquet vs Avro: Choosing the Right Tool for the Right Job》,参考:https://medium.com/ssense-tech/csv-vs-parquet-vs-avro-choosing-the-right-tool-for-the-right-job-79c9f56914a8)

Tags:

最近发表
标签列表