【问题标题】:Design choice of Haskell data types in multithreaded programs多线程程序中 Haskell 数据类型的设计选择
【发布时间】:2016-10-11 22:12:48
【问题描述】:

在多线程服务器应用程序中,我使用类型Client 来表示客户端。 Client 的本质是非常可变的:客户端发送 UDP 心跳消息以保持与服务器的注册,消息还可能包含一些实时数据(想想传感器)。我需要跟踪很多东西,例如上次心跳的时间戳和源地址、实时数据等。结果是一个包含许多状态的相当大的结构。每个客户端都有一个客户端 ID,我使用 HashMap 包裹在 MVar 中来存储客户端,因此查找既简单又快速。

type ID        = ByteString
type ClientMap = MVar (HashMap ID Client)

每个线程都可以使用ClientMap 的“全局”值。它与许多其他全局值一起存储在 ReaderT 转换器中。

Client 本身就是一个大的不可变结构,使用严格的字段来防止空间泄漏:

data Client  = Client
  {
    _c_id        :: !ID
  , _c_timestamp :: !POSIXTime
  , _c_addr      :: !SockAddr
  , _c_load      :: !Int
    ...
  }
makeLenses ''Client

根据Parallel and Concurrent Programming in Haskell,在 Concurrent Haskell 的通用设计模式中使用可变包装器中的不可变数据结构。当收到心跳消息时,处理该消息的线程会构造一个新的Client,锁定HashMapMVar,将Client插入HashMap,并放入新的HashMapMVar。代码基本上是:

modifyMVar hashmap_mvar (\hm ->
  let c = Client id ...
  in return $! M.insert id c hm)

这种方法效果很好,但是随着客户数量的增长(我们现在有成千上万的客户),出现了几个问题:

  1. 客户端非常频繁地发送心跳消息(大约每 30 秒),导致ClientMap 的访问争用。
  2. 程序的内存消耗似乎相当高。我的理解是,频繁更新包裹在MVar 中的大型不可变结构会使垃圾收集器非常忙碌。

现在,为了减少全局hashmap_mvar 的争用,我尝试为每个客户端将Client 的可变字段包装在MVar 中,例如:

data ClientState  = ClientState
  {
    _c_timestamp :: !POSIXTime
  , _c_addr      :: !SockAddr
  , _c_load      :: !Int
    ...
  }
makeLenses ''ClientState

data Client = Client
  {
    c_id    :: !ID
  , c_state :: MVar CameraState
  }

这似乎降低了争用程度(因为现在我只需要更新每个Client中的MVar,粒度更细),但是程序的内存占用仍然很高。我也尝试解包一些字段,但没有帮助。

有什么建议吗? STM会解决争用问题吗?除了 MVar 中的不可变数据结构之外,我应该求助于可变数据结构吗?

另见Updating a Big State Fast in Haskell


编辑:

正如 Nikita Volkov 所指出的,在典型的基于 TCP 的服务器-客户端应用程序中,共享地图听起来像是糟糕的设计。但是,就我而言,系统是基于 UDP 的,这意味着没有“连接”之类的东西。服务器使用单个线程从所有客户端接收 UDP 消息,解析它们并相应地执行操作,例如更新客户端数据。另一个线程定期读取地图,检查心跳的时间戳,并删除最近 5 分钟内未发送心跳的那些。似乎共享地图是不可避免的?无论如何,我知道使用 UDP 最初是一个糟糕的设计选择,但我仍然想知道如何改善使用 UDP 的情况。

【问题讨论】:

  • 您真的是为每个心跳构建一个新客户端,还是只为来自特定来源(IP、clientId 或其他某种机制)的第一个心跳构建一个新客户端?如果每次心跳都是一个新客户端,那么什么会修剪旧客户端?我怀疑问题的一部分是您在关键部分做了太多工作,在这种情况下,stm-containers 只会部分缓解问题。先读后更新可能会减少争用;这种模式在 STM 中效果更好,因为 STM TVar 读取可以在完全不加锁的情况下执行。
  • @JohnL 不幸的是,在第一个不可变客户端版本中,我确实为每个心跳构建了一个新客户端,以便更新 c_timestamp、c_addr(可能会更改客户端)和其他字段(实时数据) .除了争用问题之外,我还想减少内存占用,如果我坚持这种“包裹在 MVar 中的不可变结构”方法,我会发现这尤其困难。
  • 那你如何摆脱旧的客户端数据呢?我认为您需要定期从工作集中逐出数据。
  • @JohnL 如果我在 ClientMap 中插入一个新客户端,旧值不会被 GC'ed 吗?
  • 如果它插入同一个键,是的。但是当客户端断开连接时呢?无论如何,您可能需要对 WRT 内存使用情况进行一些堆分析以查看保留的内容。

标签: haskell


【解决方案1】:

首先,您究竟为什么需要共享地图?你真的需要与任何东西分享客户的私人状态吗?如果不是(这是客户端-服务器应用程序的典型情况),那么您可以在没有任何共享地图的情况下简单地绕过。

事实上,a "remotion" library 包含所有客户端-服务器通信,允许您通过使用自定义协议扩展它来创建服务。你应该看看。

其次,在某些实体的字段上使用多个 MVars 总是潜在的竞争条件错误。当您需要以原子方式更新多个事物时,您应该使用 STM。我不确定您的应用中是否存在这种情况,但您应该注意这一点。

第三,

客户端非常频繁地发送心跳消息(大约每 30 秒),导致 ClientMap 的访问争用

似乎只是最近发布的"stm-containers" library 中的Map 的工作。有关该库的介绍,请参阅this blog post。有了这个,您将能够回到不可变的 Client 模型。

【讨论】:

  • 确实,在典型的基于 TCP 的客户端-服务器应用程序中,服务器使用一个线程来服务每个客户端连接,在这种情况下,通常不需要共享任何内容。但是在这种情况下,客户端并没有连接到服务器,而是使用无连接的 UDP 消息来保持向服务器注册(这是一个糟糕的设计选择,现在改变已经太迟了)。 “stm-containers”库看起来确实很有趣,我稍后会研究它。谢谢!
  • @Aufheben 好吧,那么“stm-containers”似乎就是您所需要的。
  • remotion 已弃用。有替代品吗?
  • 不幸的是我不知道。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2014-10-27
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多