在node.js中如何存储和修改大型数据集?

6

基础概念

基本上,我编写了一个在Node中为MongoDB生成测试数据的程序。

问题

为此,该程序会读取模式文件,并从中生成指定数量的测试数据。问题在于,这些数据最终可能会变得非常庞大(想象一下创建100万个用户(具有所需的所有属性)和2000万条聊天消息(带有userFromuserTo),并且必须将所有数据保存在RAM中以进行修改/转换/映射,然后将其保存到文件中。

工作原理

该程序的工作原理如下:

  1. 读取模式文件
  2. 从模式中创建测试数据并将其存储在结构中(请查看下面的结构)
  3. 遍历该结构并将所有对象referenceTo链接到具有匹配referenceKey的随机对象。
  4. 将对象结构转换为MongoDB插入语句的string[]
  5. 将该string[]存储在文件中。

这是生成的测试数据的结构:

export interface IGeneratedCollection {
    dbName: string,                 // Name of the database
    collectionName: string,         // Name of the collection
    documents: IGeneratedDocument[] // One collection has many documents
}

export interface IGeneratedDocument {
    documentFields: IGeneratedField [] // One document has many fields (which are recursive, because of nested documents)
}

export interface IGeneratedField {
    fieldName: string, // Name of the property
    fieldValue: any,   // Value of the property (Can also be IGeneratedField, IGeneratedField[], ...)
    fieldNeedsQuotations?: boolean, // If the Value needs to be saved with " ... "
    fieldIsObject?: boolean,        // If the Value is a object (stored as IGeneratedField[]) (To handle it different when transforming to MongoDB inserts)
    fieldIsJsonObject?: boolean,    // If the Value is a plain JSON object
    fieldIsArray?: boolean,         // If the Value is array of objects (stored as array of IGeneratedField[])
    referenceKey?: number,          // Field flagged to be a key
    referenceTo?: number            // Value gets set to a random object with matching referenceKey
}

实际数据

以拥有100万用户和2000万消息的示例为例,情况如下:

  • 1个IGeneratedCollection(collectionName="users"
    • 100万个IGeneratedDocument
      • 每个用户包含10个IGeneratedField
  • 1个IGeneratedCollection(collectionName="messages"
    • 2000万个IGeneratedDocument
      • 其中包含3个IGeneratedField(message, userFrom, userTo

这将导致190M个IGeneratedField实例(1x1Mx10 + 1x20Mx3 = 190M)。

结论

很显然,这对于需要同时存储所有内容的RAM来说是一个巨大的挑战。

临时解决方案

目前的工作方式如下:

  1. 一次生成500个文档(在SQL中的行)
  2. JSON.stringify 这些500个文档,并将它们放入一个具有以下结构的 SQLite 表中 (dbName STRING, collectionName STRING, value JSON)
  3. 从JS中删除这500个文档,并让垃圾回收器完成其工作
  4. 重复上述步骤,直到所有数据都生成并存储在 SQLite 表中
  5. 每次取出一行(包含500个文档),应用 JSON.parse 并搜索其中的键
  6. 重复上述步骤,直到查询所有数据并检索所有键
  7. 每次取出一行,应用 JSON.parse 并搜索其中的键引用
  8. 如果必要(如果找到并解析了键引用),应用 JSON.stringify 并更新该行
  9. 重复上述步骤,直到查询所有数据并解析所有键
  10. 每次取出一行,应用 JSON.parse 并将文档转换为有效的 SQL/MongoDB 插入语句
  11. 将该插入语句(字符串)添加到一个具有以下结构的 SQLite 表中 (singleInsert STRING)
  12. 从 SQLite 表中删除旧的、现在已不再使用的行
  13. 将所有插入语句写入文件(如果从命令行运行),或者返回一个 dataHandle 以查询 SQLite 表中的数据(如果从其他 Node 应用程序运行)

这种解决方案可以处理内存问题,因为当 RAM 满时,SQLite 会自动切换到硬盘。

但是

正如您所看到的,其中涉及大量的 JSON.parseJSON.stringify,这会严重降低整个过程的速度。

我的想法:

也许我应该修改生成的字段(IGeneratedField),只使用变量的简短名称 (fieldName -> fn, fieldValue -> fv, fieldIsObject -> fio, fieldIsArray -> fia, ....)。

这将使在 SQLite 表中所需的存储更小,但也会使代码更难阅读。

使用面向文档的数据库(但我还没有真正找到这样的数据库),以更好地处理 JSON 数据。

问题

是否有更好的解决方案来处理这样的大对象?

我的临时解决方案是否可行?有什么问题?能否进行改进以提高性能?


1
我会使用SQLite和内存表。SQLite被设计用于处理RAM和磁盘交换管理。与第三种解决方案类似,但无需托管数据库或需要网络连接。SQLite数据库将存在于目标PC上。 - Jason
@Jason 感谢您的时间。您认为使用SQLite是一个好主意吗?它是一个关系型数据库,保存的对象具有嵌套元素等。这在SQLite中是否可行?(我对SQLite几乎一无所知)。 - MauriceNino
1个回答

2
概念上,在流中生成项目。
您不需要在数据库中拥有全部100万个用户。您可以每次添加1万个。
对于消息,从数据库中随机抽取2n个用户,让他们互相发送消息。重复此过程直到满意为止。
例子:
// Assume Users and Messages are both db.collections
// Assume functions generateUser() and generateMessage(u1, u2) exist.
const desiredUsers = 10000;
const desiredMessages = 5000000;
const blockSize = 1000;


(async () => {

for (const i of _.range(desiredUsers / blockSize) ) {
    const users = _.range(blockSize).map(generateUser);
    await Users.insertMany(users);
}


for (const i of _.range(desiredMessages / blockSize) ) {
    const users = await Users.aggregate([ { $sample: { size: 2 * blockSize } } ]).toArray();
    const messages = _.chunk(users, 2).map( (usr) => generateMessage(usr[0], usr[1]));
    await Messages.insertMany(messages);
}

})();


根据您调整流的方式,您会得到不同的分布。这是均匀分布。通过交错用户和消息,您可以获得更多的长尾分布。例如,您可能希望在留言板上这样做。

Memory usage

更改块大小为1000后,容量增加到了200MB。

Timing


用户->消息的例子只是一个非常小的例子。想象一下一个模型,其中每个用户都有一个朋友列表、一个朋友请求列表、一个群组列表等等。那么它会变得非常复杂。此外,不要忘记程序没有直接访问数据库的权限,因此它不能保存并再次获取数据。需要在两者之间建立一个临时数据库。这又会增加很多读写操作。 - MauriceNino
@MauriceNino“不要忘记程序没有直接访问数据库的权限。”抱歉,我没有看到这些要求。通常人们想要测试的是工作数据库的性能,无论使用什么API。实际上很少有人会在原始MongoDB上测试插入命令。无论如何,如果您真的遇到了这种问题,我个人会在外部生成一个数据库,然后将其mongodump/mongorestore到测试服务器上。 - blackening
@MauriceNino 如果您将其视为随机图问题,则并不复杂。样本节点,然后添加边。使用https://www.npmjs.com/package/dummy-json可以使用模式生成大量数据。您只需要从采样中添加“外键”(或嵌入式项目)即可。 - blackening
1
在我看来,有两种用例。1)创建测试数据以验证前端是否正常工作/使用“真实数据”进行测试。2)如您所描述->渗透测试您的数据库。为了支持这两种用例,我必须先在程序中创建要插入的内容,然后将其写出到我想要的任何位置(直接到数据库或文件)。 - MauriceNino
1
谢谢这个工具。如果我在开始写这个程序之前有它,我本可以省下很多功夫。不过现在我还是想把它完成。 - MauriceNino

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接