Haskell网络性能不佳

13

我正在编写一个类似于OpenVPN的程序,想用它来提高我对Haskell的了解。然而,我遇到了相当严重的性能问题。

它的功能是:打开TUN设备;绑定在一个UDP端口上,启动2个线程(使用forkIO,但由于fdRead,编译时使用了-threaded)。我没有使用tuntap包,在Haskell中完全自己实现了它。

线程1:从TUN设备读取数据包(fdRead)。使用UDP套接字将其发送出去。
线程2:从UDP套接字接收数据包(recv),将其发送到TUN设备(fdWrite)。

问题1:在这种配置中,fdRead返回String类型的数据,我使用了接受String类型的Network.Socket函数。我在本地系统上进行了一些iptables配置(一些魔法),并且可以在localhost上以15MB/s的速度运行该程序,但CPU占用率基本上达到了100%。 这太慢了。有什么方法可以改善性能吗?

问题2:我需要在发送的数据包前面添加一些内容;但是sendMany网络函数只接受ByteString类型的数据,而Fd读取的是String类型的数据。转换非常慢。将其转换为Handle似乎不能很好地与TUN设备配合使用....

问题3:我想在Data.Heap(函数式堆)中存储一些信息(我需要使用'takeMin'),虽然对于3个数据项来说有些过度了,但很容易实现。 因此,我创建了一个MVar,在每个接收到的数据包上,我从MVar中取出了Heap,在其中更新了新信息并将其放回到MVar中。现在程序开始占用大量内存。可能是因为旧堆没有被足够快/频繁地进行垃圾回收......?

是否有方法解决这些问题,或者我必须回到C语言...?我的操作应该主要是零复制操作-我是否使用了错误的库来实现它?

==================

我所做的事情: - 在将数据放入MVar时,执行以下操作:

a `seq` putMVar mvar a

那个完美地解决了内存泄漏问题。

  • 改为ByteString后,现在使用“读/写”而没有进一步处理时,我获得了42MB/s的速度。C语言版本大约为56MB/s,因此这是可以接受的。

1
你介意我问一下为什么你没有使用tuntap包吗?(我是维护者...所以我很好奇。) - John
我在考虑使用'TUN'部分,并想利用Haskell的“Handle”部分;但这并不是正确的方法。对我来说,我可能最终会使用它并使用TAP方式(虽然没有太大区别)- 因为它返回ByteString,这可能会加快速度。 - ondra
我将向tuntap包添加注释;使用标准SockAddr(而不是Word32)设置IP / netmask将非常受欢迎 :) - ondra
哎呀!我刚看到concurrent-strict将事物评估为正常形式(即深度序列),而不是文档所述的“头正常形式”(即seqs)。 回到惰性mvars,并自己使用seq强制评估可能会有很大的收益。 Data.Heap应该保持惰性以获得正确的摊销性能。 - sclv
我也发现了这一点,不过我最初是通过“错误地”输入rnf deepseq实例来做到的。使用带有seq的lazy mvars肯定更加简洁。 - ondra
3个回答

23

字符串很慢。非常、非常、非常慢。它是一个仅含有一个 Unicode 字符的 cons 单链表。将其写入套接字需要将每个字符转换为字节,将这些字节复制到数组中,并将该数组传递给系统调用。你想做的部分是哪一部分呢? :)

你应该专门使用 ByteString。ByteString IO 函数在可能的情况下实际上使用零复制 IO。特别是请查看位于 hackage 上的 network-bytestring 包。它包含了各种网络库的版本,这些版本都经过了优化,可以与 ByteString 高效地工作。


4
请注意,如果您升级到最新的网络包,network-bytestring 已经被合并进去了! - sclv
我也不知道那个。谢谢你提醒我。 - Carl
我正在使用network.bytestring;现在我甚至已经将我的“tun”调用转换为ByteString。现在我得到了32MB/s,对我来说仍然相当糟糕。我将尝试tuntap包,但我不认为那会解决问题... - ondra
1
在C代码中,我可以获得最大56MB/s的速度。在Haskell中,我只能获得42MB/s的速度;这可能可以通过使用“buf”函数和一些就地操作来进行调整,但这应该是一个可以接受的差异。 - ondra

6

Carl在你前两个问题上是正确的。关于最后一个问题,请考虑使用严格并发包


哦,我完全错过了那个问题。我有点觉得这个包有些过度,但根本问题在于堆被修改时结构没有被强制,而这个包肯定会解决这个问题。 - Carl

6

下面是两个示例程序:客户端和服务器。使用 GHC 7.0.1 和 network-2.3,在我的全新双核笔记本电脑上通过 loopback 获得了超过 7500 Mbps 的速度(总 CPU 使用率约为 90%)。我不知道 UDP 会引入多少开销,但这仍然是一个相当大的数字。

--------------------
-- Client program --
--------------------
module Main where

import qualified Data.ByteString as C
import Network.Socket hiding (recv)
import Network.Socket.ByteString (recv)

import System.IO
import Control.Monad

main :: IO ()
main = withSocketsDo $
    do devNull <- openFile "/dev/null" WriteMode
       addrinfos <- getAddrInfo Nothing (Just "localhost") (Just "3000")
       let serveraddr = head addrinfos
       sock <- socket (addrFamily serveraddr) Stream defaultProtocol
       connect sock (addrAddress serveraddr)
       forever $ do
         msg <- recv sock (256 * 1024) -- tuning recv size is important!
         C.hPutStr devNull msg
       sClose sock


--------------------
-- Server program --
--------------------
module Main where

-- import Control.Monad (unless)
import Network.Socket hiding (recv)
import qualified Data.ByteString.Lazy as S
import Network.Socket.ByteString.Lazy (
                                       --recv, 
                                       sendAll)

main :: IO ()
main = withSocketsDo $
       do addrinfos <- getAddrInfo
                        (Just (defaultHints {addrFlags = [AI_PASSIVE]}))
                        Nothing (Just "3000")
          let serveraddr = head addrinfos
          sock <- socket (addrFamily serveraddr) Stream defaultProtocol
          bindSocket sock (addrAddress serveraddr)
          listen sock 1
          (conn, _) <- accept sock
          talk conn
          sClose conn
          sClose sock

     where
       talk :: Socket -> IO ()
       talk conn = sendAll conn $ S.repeat 7

我无法调整接收大小;我正在使用UDP传输IP数据包,因此接收大小基本上固定为约1500。 - ondra

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