【问题标题】:STM-friendly list as a change logSTM 友好列表作为更改日志
【发布时间】:2016-03-22 21:13:36
【问题描述】:

我需要关于用作原子更改日志的数据结构的建议。

我正在尝试实现以下算法。有流入的 更改更新内存中的映射。在类似 Haskell 的伪代码中是

    update :: DataSet -> SomeListOf Change -> Change -> STM (DataSet, SomeListOf Change)
    update dataSet existingChanges newChange = do
      ...
      return (dataSet, existingChanges ++ [newChange])

其中 DataSet 是一个映射(目前它是来自 stm-containers 包的映射,https://hackage.haskell.org/package/stm-containers-0.2.10/docs/STMContainers-Map.html)。从任意数量的线程调用整个“更新”。由于域语义,某些更改可能会被拒绝,我使用 throwSTM 来丢弃事务的影响。在成功提交的情况下,“newChange”被添加到列表中。

存在调用以下函数的单独线程:

    flush :: STM (DataSet, SomeListOf Change) -> IO ()

这个函数应该将 DataSet 的当前快照与更改列表(它必须是一致的对)一起拍摄并刷新到文件系统,即

    flush data = do
      (dataSet, changes) <- atomically $ readTVar data_
      -- write them both to FS
      -- ...
      atomically $ writeTVar data_ (dataSet, [])

我需要有关用于“SomeListOf Change”的数据结构的建议。我不想用[Change],因为它“太有序”,怕冲突太多,会迫使整个事务重试。如果我在这里错了,请纠正我。

我不能使用 Set (https://hackage.haskell.org/package/stm-containers-0.2.10/docs/STMContainers-Set.html),因为我仍然需要保留 一些 顺序,例如事务提交的顺序。我可以为它使用 TChan,它看起来很匹配(恰好是事务提交的顺序),但我不知道如何实现“刷新”功能,以便它可以提供整个更改日志的一致视图与数据集。

它的当前实现在这里https://github.com/lolepezy/rpki-pub-server/blob/add-storage/src/RRDP/Repo.hs,分别在函数applyActionsToState 和rrdpSyncThread 中。它使用了 TChan,而且似乎以错误的方式进行。

提前谢谢你。

更新:一个合理的答案似乎是这样的

    type SomeListOf c = TChan [c] 

    update :: DataSet -> TChan [Change] -> Change -> STM DataSet
    update dataSet existingChanges newChange = do
      ...
      writeTChan changeChan $ reverse (newChange : existingChanges)
      return dataSet

   flush data_ = do
      (dataSet, changes) <- atomically $ (,) <$> readTVar data_ <*> readTChan changeChan
      -- write them both to FS
      -- ...

但我仍然不确定将整个列表作为频道元素传递是否是一个巧妙的解决方案。

【问题讨论】:

  • 我没有仔细阅读你的问题,但是TChan 是一个非常简单的([a], [a]) 函数式出队;听起来您在其上实现自己的变体可能是有意义的。
  • 让我问:预计有多少线程(至少是一个粗略的数字)来访问该结构?一次有多少个?您预计更改列表会增长到多大?
  • 还需要将update 与其他STM 操作组合起来,还是始终在自己的事务中运行?
  • 抱歉回复晚了。我预计负载不会很大,但可能会出现突发事件,比如同时请求几十个。数据集应该非常小(数十万个元素)但经常更新。而且我不认为这会成为更大交易的一部分。

标签: haskell stm


【解决方案1】:

我可能只是按照列表来看看它在性能方面需要多长时间。鉴于此,您应该考虑到追加到列表末尾和反转它都是 O(n) 操作,因此您应该尽量避免这种情况。也许您可以像这样预先添加传入的更改:

update dataSet existingChanges newChange = do
  -- ...
  return (dataSet, newChange : existingChanges)

此外,您的刷新示例存在读取和更新状态根本不是原子的问题。您必须使用单个 atomically 调用来完成此操作,如下所示:

flush data = do
  (dataSet, changes) <- atomically $ do
    result <- readTVar data_
    writeTVar data_ (dataSet, [])
    return result

  -- write them both to FS
  -- ...

然后您可以将它们以相反的顺序写出(因为现在changes 包含从最新到最旧的元素)或者如果将它们从最旧到最新写出很重要,则在此处反转一次。如果这很重要,我可能会使用一些允许 O(1) 元素访问的数据结构,就像一个好的旧向量一样。

当使用固定大小的向量时,您显然必须处理它可能变得“已满”的问题,这意味着您的作者必须等待flush 完成它的工作,然后才能添加新的更改。这就是为什么我个人会先选择简单的列表,看看它是否足够或需要改进的地方。

PS:dequeue 可能也很适合您的问题,但是固定大小会迫使您处理这样一个问题,即您的作者可能会产生比您的读者可以清除的更多的变化。 dequeue 可以无限增长,但你的 RAM 可能不是。而且向量的开销非常低。

【讨论】:

  • 我已经添加了一些实际测量的答案,因此与其他费用相比,我使用哪种更改日志实现几乎没有关系。
【解决方案2】:

我做了一些(非常简单的)调查 https://github.com/lolepezy/rpki-pub-server/tree/add-storage/test/changeLog 准确地模仿我应该拥有的负载类型。我对数据集使用相同的 STMContainers.Map,对更改日志使用通常的列表。为了跟踪事务重试次数,我使用了 Debug.Trace.trace,意思是跟踪打印的行数。 trace 打印的 unique 行数为我提供了已提交事务的数量。

结果在这里(https://github.com/lolepezy/rpki-pub-server/blob/add-storage/test/changeLog/numbers.txt)。第一列是线程数,第二列是总生成的变更集数。第三列是没有更改日志的案例的跟踪调用次数,最后一列是有更改日志的跟踪调用次数。

显然大部分时间更改日志都会添加一些额外的重试,但这几乎是微不足道的。所以,我想,公平地说,任何数据结构都足够好,因为大部分工作都与更新地图有关,而且大部分重试都是因为它而发生的。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2012-05-24
    • 1970-01-01
    • 2021-10-03
    • 2014-06-22
    • 2016-05-02
    • 2012-04-03
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多