【发布时间】: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,锁定HashMap的MVar,将Client插入HashMap,并放入新的HashMap在MVar。代码基本上是:
modifyMVar hashmap_mvar (\hm ->
let c = Client id ...
in return $! M.insert id c hm)
这种方法效果很好,但是随着客户数量的增长(我们现在有成千上万的客户),出现了几个问题:
- 客户端非常频繁地发送心跳消息(大约每 30 秒),导致
ClientMap的访问争用。 - 程序的内存消耗似乎相当高。我的理解是,频繁更新包裹在
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