Haskell Conduit Aeson:解析大型JSON并过滤匹配键/值

时间:2018-01-18 21:35:16

标签: json haskell aeson conduit

我在Haskell中编写了一个应用程序,它执行以下操作:

  1. 递归列出目录
  2. 从目录列表中解析JSON文件
  3. 查找匹配的键值对,
  4. 返回找到匹配项的文件名。
  5. 这个应用程序的第一个版本是我可以编写的最简单,最天真的版本,但我注意到空间使用似乎单调增加。

    结果,我切换到conduit,现在我的主要功能如下:

    conduitFilesFilter :: ProjectFilter -> Path Abs Dir -> IO [Path Abs File]
    conduitFilesFilter projFilter dirname' = do
      (_, allFiles) <- listDirRecur dirname'
      C.runConduit $
        C.yieldMany allFiles
        .| C.filterMC (filterMatchingFile projFilter)
        .| C.sinkList
    

    现在我的应用程序限制了内存使用,但它仍然很慢。出于这个原因,我有两个问题。

    1)

    我使用stack new生成骨架来创建此应用程序,默认情况下使用ghc选项-threaded -rtsopts -with-rtsopts=-N

    令我惊讶的是(对我来说)应用程序使用它可用的所有处理器(目标机器中大约40个),当我实际运行它时。但是,我没有编写要并行运行的应用程序的任何部分(实际上我考虑过它)。

    并行运行什么?

    2)

    此外,大多数JSON文件都非常大(10mb),并且可能有500k个要遍历。这意味着我的程序由于所有Aeson解码而非常慢。我的想法是并行运行我的filterMatchingFile部分,但是查看stm-conduit库,我无法看到一种明显的方法可以在少数几个处理器上并行运行这个中间操作。 / p>

    有人可以建议使用stm-conduit或其他方法巧妙地将我的功能并行化吗?

    修改

    我意识到我可以将readFile -> decodeObject -> runFilterFunction分解为conduit的不同部分,然后我可以在stm-conduit处使用有界通道。也许我会试一试......

    我使用+RTS -s运行了我的应用程序(我将其重新配置为-N4),我看到以下内容:

     115,961,554,600 bytes allocated in the heap
      35,870,639,768 bytes copied during GC
          56,467,720 bytes maximum residency (681 sample(s))
           1,283,008 bytes maximum slop
                 145 MB total memory in use (0 MB lost due to fragmentation)
    
                                         Tot time (elapsed)  Avg pause  Max pause
      Gen  0     108716 colls, 108716 par   76.915s  20.571s     0.0002s    0.0266s
      Gen  1       681 colls,   680 par    0.530s   0.147s     0.0002s    0.0009s
    
      Parallel GC work balance: 14.99% (serial 0%, perfect 100%)
    
      TASKS: 10 (1 bound, 9 peak workers (9 total), using -N4)
    
      SPARKS: 0 (0 converted, 0 overflowed, 0 dud, 0 GC'd, 0 fizzled)
    
      INIT    time    0.001s  (  0.007s elapsed)
      MUT     time   34.813s  ( 42.938s elapsed)
      GC      time   77.445s  ( 20.718s elapsed)
      EXIT    time    0.000s  (  0.010s elapsed)
      Total   time  112.260s  ( 63.672s elapsed)
    
      Alloc rate    3,330,960,996 bytes per MUT second
    
      Productivity  31.0% of total user, 67.5% of total elapsed
    
    gc_alloc_block_sync: 188614
    whitehole_spin: 0
    gen[0].sync: 33
    gen[1].sync: 811204
    

2 个答案:

答案 0 :(得分:1)

从您的程序说明中,没有理由增加内存使用量。我认为这是因错过懒惰计算而导致的意外内存泄漏。这可以通过堆分析轻松检测到:https://downloads.haskell.org/~ghc/latest/docs/html/users_guide/profiling.html#hp2ps-rendering-heap-profiles-to-postscript。其他可能的原因是运行时不会将所有内存释放回操作系统。直到某个阈值,它将保持与处理的最大文件成比例的内存。如果通过进程RSS大小进行跟踪,这可能看起来像是内存泄漏。

-A32m选项会增加托儿所的大小。它允许程序在触发垃圾收集之前分配更多内存。统计数据显示,GC期间保留的内存非常少,因此更少发生,程序花费更多时间进行实际工作。

答案 1 :(得分:0)

Michael Snoyman在Haskell Cafe上提示,他指出我的第一个版本没有真正利用Conduit的流媒体功能,我重新编写了我的应用程序的Conduit版本(不使用stm-conduit)。这是一个很大的改进:我的第一个管道版本是在所有数据上运行的,我没有意识到这一点。

我还增加了托儿所的规模,通过减少垃圾收集来提高我的工作效率。

我修改后的功能最终看起来像这样:

module Search where

import           Conduit               ((.|))
import qualified Conduit               as C
import           Control.Monad
import           Control.Monad.IO.Class   (MonadIO, liftIO)
import           Control.Monad.Trans.Resource (MonadResource)
import qualified Data.ByteString       as B
import           Data.List             (isPrefixOf)
import           Data.Maybe            (fromJust, isJust)
import           System.Path.NameManip (guess_dotdot, absolute_path)
import           System.FilePath       (addTrailingPathSeparator, normalise)
import           System.Directory      (getHomeDirectory)

import           Filters


sourceFilesFilter :: (MonadResource m, MonadIO m) => ProjectFilter -> FilePath -> C.ConduitM () String m ()
sourceFilesFilter projFilter dirname' =
    C.sourceDirectoryDeep False dirname'
    .| parseProject projFilter

parseProject :: (MonadResource m, MonadIO m) => ProjectFilter -> C.ConduitM FilePath String m ()
parseProject (ProjectFilter filterFunc) = do
  C.awaitForever go
  where
    go path' = do
      bytes <- liftIO $ B.readFile path'
      let isProj = validProject bytes
      when (isJust isProj) $ do
        let proj' = fromJust isProj
        when (filterFunc proj') $ C.yield path'

我的主要只是运行管道并打印通过过滤器的那些:

mainStreamingConduit :: IO ()
mainStreamingConduit = do
  options <- getRecord "Search JSON Files"
  let filterFunc = makeProjectFilter options
  searchDir <- absolutize (searchPath options)
  itExists <- doesDirectoryExist searchDir
  case itExists of
    False -> putStrLn "Search Directory does not exist" >> exitWith (ExitFailure 1)
    True -> C.runConduitRes $ sourceFilesFilter filterFunc searchDir .| C.mapM_ (liftIO . putStrLn)

我像这样运行(通常没有统计数据):

stack exec search-json -- --searchPath $FILES --name NAME +RTS -s -A32m -n4m

在不增加幼儿园规模的情况下,我的生产力提高了30%左右。但是,如上所述,它看起来像这样:

  72,308,248,744 bytes allocated in the heap
     733,911,752 bytes copied during GC
       7,410,520 bytes maximum residency (8 sample(s))
         863,480 bytes maximum slop
             187 MB total memory in use (27 MB lost due to fragmentation)

                                     Tot time (elapsed)  Avg pause  Max pause
  Gen  0       580 colls,   580 par    2.731s   0.772s     0.0013s    0.0105s
  Gen  1         8 colls,     7 par    0.163s   0.044s     0.0055s    0.0109s

  Parallel GC work balance: 35.12% (serial 0%, perfect 100%)

  TASKS: 10 (1 bound, 9 peak workers (9 total), using -N4)

  SPARKS: 0 (0 converted, 0 overflowed, 0 dud, 0 GC'd, 0 fizzled)

  INIT    time    0.001s  (  0.006s elapsed)
  MUT     time   26.155s  ( 31.602s elapsed)
  GC      time    2.894s  (  0.816s elapsed)
  EXIT    time   -0.003s  (  0.008s elapsed)
  Total   time   29.048s  ( 32.432s elapsed)

  Alloc rate    2,764,643,665 bytes per MUT second

  Productivity  90.0% of total user, 97.5% of total elapsed

gc_alloc_block_sync: 3494
whitehole_spin: 0
gen[0].sync: 15527
gen[1].sync: 177

我仍然想知道如何并行化filterProj . parseJson . readFile部分,但是现在我对此感到满意。