Opa:如何有效地读/写大量记录

nrw*_*nrw 5 opa

问题

我需要读取和写入大量记录(大约1000个).以下示例需要长达20分钟才能写入1000条记录,并且只需12秒即可读取它们(在进行"读取"测试时,我会注释掉该行do create_notes()).

来源

这是一个完整的例子(构建和运行).它只将输出打印到控制台(而不是浏览器).

type User.t =
  { id : int
  ; notes : list(int) // a list of note ids
  }

type Note.t =
  { id : int
  ; uid : int // id of the user this note belongs to
  ; content : string
  }

db /user : intmap(User.t)
db /note : intmap(Note.t)

get_notes(uid:int) : list(Note.t) =
  noteids = /user[uid]/notes
  List.fold(
    (h,acc -> 
      match ?/note[h] with
      | {none} -> acc
      | {some = note} -> [note|acc]
    ), noteids, [])

create_user() =
  match ?/user[0] with
  | {none} -> /user[0] <- {id=0 notes=[]}
  | _ -> void

create_note() =
  key = Db.fresh_key(@/note)
  do /note[key] <- {id = key uid = 0 content = "note"}
  noteids = /user[0]/notes
  /user[0]/notes <- [key|noteids]

create_notes() =
  repeat(1000, create_note)

page() =
  do create_user()
  do create_notes()
  do Debug.alert("{get_notes(0)}")
  <>Notes</>

server = one_page_server("Notes", page)
Run Code Online (Sandbox Code Playgroud)

还有一件事

我也试过通过交易获得笔记(如下所示).看起来Db.transaction可能是正确的工具,但我还没有找到成功应用它的方法.我发现这个get_notes_via_transaction方法和它一样慢get_notes.

get_notes_via_transaction(uid:int) : list(Note.t) =
  result = Db.transaction( ->
    noteids = /user[uid]/notes
    List.fold(
      (h,acc -> 
        match ?/note[h] with
        | {none} -> acc
        | {some = note} -> [note|acc]
      ), noteids, [])
  )
  match result with
  | {none} -> []
  |~{some} -> some
Run Code Online (Sandbox Code Playgroud)

谢谢你的帮助.

编辑:更多详细信息

一些可能有用的额外信息:

经过更多测试后,我注意到编写前100条记录只需5秒钟.每条记录的写入时间比前一条记录要长.在第500条记录中,每条记录需要5秒钟.

如果我中断程序(当它开始感觉很慢)并再次启动它(不清除数据库),它会以我打断它时写入的相同(慢)速度写入记录.

这会让我们更接近解决方案吗?

ako*_*ski 3

尼克,这可能不是您所希望的答案,但它是:

  1. 我建议这种性能实验改变框架;例如根本不使用客户端。create_node我将函数中的代码替换为:

    counter = Reference.create(0)
    create_note() =
      key = Db.fresh_key(@/note)
      do /note[key] <- {id = key uid = 0 content = "note"}
      noteids = /user[0]/notes
      do Reference.update(counter, _ + 1)
      do /user[0]/notes <- [key|noteids]
      cntr = Reference.get(counter)
      do if mod(cntr, 100) == 0 then
           Log.info("notes", "{cntr} notes created")
         else
           void
      void
    
    import stdlib.profiler
    
    create_notes() =
      repeat(1000, -> P.execute(create_note, "create_note"))
    
    P = Server_profiler
    
    _ =
      do P.init()
      do create_user()
      do create_notes()
      do P.execute(-> get_notes(0), "get_notes(0)")
      P.summarize()
    
    Run Code Online (Sandbox Code Playgroud)
  2. 由于中间计时是每 100 个插入的打印机,您很快就会发现插入时间与插入项目的数量成二次方,而不是线性的。这是因为列表更新/user[0]/notes <- [key|noteids]显然导致整个列表被再次写入。据我所知,我们进行了优化来避免这种情况,但要么我错了,要么由于某些原因它们在这里不起作用——我会尝试调查这一点,并在我了解更多信息后通知您。

  3. 除了前面提到的优化之外,在 Opa 中对这些数据进行建模的更好方法是使用集合,如以下程序所示:

    type Note.t =
    { id : int
    ; uid : int // id of the user this note belongs to
    ; content : string
    }
    
    db /user_notes[{user_id; note_id}] : { user_id : int; note_id : int }
    db /note : intmap(Note.t)
    
    get_notes(uid:int) : list(Note.t) =
      add_note(acc : list(Note.t), user_note) =
        note = /note[user_note.note_id]
        [note | acc]
      noteids = /user_notes[{user_id=uid}] : dbset({user_id:int; note_id:int})
      DbSet.fold(noteids, [], add_note)
    
    counter = Reference.create(0)
    
    create_note() =
      key = Db.fresh_key(@/note)
      do /note[key] <- {id = key uid = 0 content = "note"}
      do DbVirtual.write(@/user_notes[{user_id=0}], {note_id = key})
      do Reference.update(counter, _ + 1)
      cntr = Reference.get(counter)
      do if mod(cntr, 100) == 0 then
           Log.info("notes", "{cntr} notes created")
         else
           void
      void
    
    import stdlib.profiler
    
    create_notes() =
      repeat(1000, -> Server_profiler.execute(create_note, "create_note"))
    
    _ =
      do Server_profiler.init()
      do create_notes()
      do Server_profiler.execute(-> get_notes(0), "get_notes(0)")
      Server_profiler.summarize()
    
    Run Code Online (Sandbox Code Playgroud)

    您可以在其中设置填充数据库大约需要 2 秒。不幸的是,这个功能还处于实验阶段,因此没有文档记录,正如您将看到的,它确实在这个示例中爆炸了。

  4. 恐怕我们并不真正打算改进(3)和(4),因为我们意识到提供符合工业标准的内部数据库解决方案不太现实。因此,目前我们正在集中精力将 Opa 与现有 No-SQL 数据库紧密集成。我们希望在未来几周内能听到一些好消息。

我将尝试从我们的团队中了解有关此问题的更多信息,如果我发现我遗漏/出错了,我将进行纠正。

  • 这就是我陷入困境的地方:我倾向于使用 Opa 构建应用程序,因为它的“即时可扩展性”属性。然而,如果您可以保证您的数据集不需要随应用程序的其余部分一起扩展,那么 Opa *似乎* 才可以“立即扩展”。如果您的数据集需要扩展(这是常见的需要),Opa 可以像任何其他工具一样进行扩展:扩展需要在更多机器上设置和维护更广泛的工具集。因此,就我而言,我正在考虑管理 Opa 和一组 CouchDb 服务器。Opa 是否适合在需要可扩展数据持久性的应用程序中使用?如果是这样,这就是一个很大的障碍。:-/ (2认同)