2

私は、apache ログ ファイルから何らかの興味深い (実際にはそうではない) 統計を読み込んで解析し、導出するための小さなスクリプトを用意しています。これまでのところ、ログ ファイル内のすべての要求で送信されたバイトの合計数と、最も一般的な IP アドレスの上位 10 という 2 つの簡単なオプションを作成しました。

最初の「モード」は、解析されたすべてのバイトの単純な合計です。2 つ目は、マップ (Data.Map) の折り畳みでinsertWith (+) 1'、出現回数をカウントするために使用します。

最初のものは期待どおりに実行され、ほとんどの時間は解析に費やされ、一定のスペースで実行されます。

42,359,709,344 バイトがヒープに割り当てられた 72,405,840 バイトが GC 中にコピーされた

世代 0: 76311 コレクション、
0 並列、0.89 秒、0.99 秒経過
世代 1: 1553 コレクション、0 並列、0.21 秒、0.22 秒経過

INIT 時間 0.00 秒 ( 0.00 秒経過) MUT 時間 21.76 秒 ( 24.82 秒経過) GC 時間 1.10 秒 ( 1.20 秒経過) EXIT 時間
0.00 秒 ( 0.00 秒経過) 合計時間 22.87 秒 ( 26.02 秒経過)

%GC 時間 4.8% (4.6% 経過)

割り当てレート 1,946,258,962 バイト/MUT 秒

生産性 総ユーザーの 95.2%、総経過時間の 83.6%

しかし、2番目のものはそうではありません!

ヒープに割り当てられた 49,398,834,152 バイト GC 中にコピーされた 580,579,208 バイト 最大常駐 (15 サンプル) 718,385,088 バイト 最大スロップ 134,532,128 バイト

ジェネレーション 0: 91275 コレクション、
0 並列、252.65 秒、254.46 秒経過
ジェネレーション 1: 15 コレクション、0 並列、0.12 秒、0.12 秒経過

INIT 時間 0.00 秒 (0.00 秒経過) MUT 時間 41.11 秒 (48.87 秒経過) GC 時間 252.77 秒 (254.58 秒経過) EXIT 時間
0.00 秒 (0.01 秒経過) 合計時間 293.88 秒 (303.45 秒経過)

%GC 時間 86.0% (83.9% 経過)

割り当て率 1,201,635,385 バイト/MUT 秒

生産性 総ユーザーの 14.0%、総経過時間の 13.5%

そして、これがコードです。

{-# LANGUAGE OverloadedStrings #-}

module Main where

import qualified Data.Attoparsec.Lazy as AL
import Data.Attoparsec.Char8 hiding (space, take)
import qualified Data.ByteString.Char8 as S
import qualified Data.ByteString.Lazy.Char8 as L
import Control.Monad (liftM)
import System.Environment (getArgs)
import Prelude hiding (takeWhile)
import qualified Data.Map as M
import Data.List (foldl', sortBy)
import Text.Printf (printf)
import Data.Maybe (fromMaybe)

type Command = String

data LogLine = LogLine {
    getIP     :: S.ByteString,
    getIdent  :: S.ByteString,
    getUser   :: S.ByteString,
    getDate   :: S.ByteString,
    getReq    :: S.ByteString,
    getStatus :: S.ByteString,
    getBytes  :: S.ByteString,
    getPath   :: S.ByteString,
    getUA     :: S.ByteString
} deriving (Ord, Show, Eq)

quote, lbrack, rbrack, space :: Parser Char
quote  = satisfy (== '\"')
lbrack = satisfy (== '[')
rbrack = satisfy (== ']')
space  = satisfy (== ' ')

quotedVal :: Parser S.ByteString
quotedVal = do
    quote
    res <- takeTill (== '\"')
    quote
    return res

bracketedVal :: Parser S.ByteString
bracketedVal = do
    lbrack
    res <- takeTill (== ']')
    rbrack
    return res

val :: Parser S.ByteString
val = takeTill (== ' ')

line :: Parser LogLine
l    ine = do
    ip <- val
    space
    identity <- val
    space
    user <- val
    space
    date <- bracketedVal
    space
    req <- quotedVal
    space
    status <- val
    space
    bytes <- val
    (path,ua) <- option ("","") combined
    return $ LogLine ip identity user date req status bytes path ua

combined :: Parser (S.ByteString,S.ByteString)
combined = do
    space
    path <- quotedVal
    space
    ua <- quotedVal
    return (path,ua)

countBytes :: [L.ByteString] -> Int
countBytes = foldl' count 0
    where
        count acc l = case AL.maybeResult $ AL.parse line l of
            Just x  -> (acc +) . maybe 0 fst . S.readInt . getBytes $ x
            Nothing -> acc

countIPs :: [L.ByteString] -> M.Map S.ByteString Int
countIPs = foldl' count M.empty
    where
        count acc l = case AL.maybeResult $ AL.parse line l of
            Just x -> M.insertWith' (+) (getIP x) 1 acc
            Nothing -> acc

---------------------------------------------------------------------------------

main :: IO ()
main = do
  [cmd,path] <- getArgs
  dispatch cmd path

pretty :: Show a => Int -> (a, Int) -> String
pretty i (bs, n) = printf "%d: %s, %d" i (show bs) n

dispatch :: Command -> FilePath -> IO ()
dispatch cmd path = action path
    where
        action = fromMaybe err (lookup cmd actions)
        err    = printf "Error: %s is not a valid command." cmd

actions :: [(Command, FilePath -> IO ())]
actions = [("bytes", countTotalBytes)
          ,("ips",  topListIP)]

countTotalBytes :: FilePath -> IO ()
countTotalBytes path = print . countBytes . L.lines =<< L.readFile path

topListIP :: FilePath -> IO ()
topListIP path = do
    f <- liftM L.lines $ L.readFile path
    let mostPopular (_,a) (_,b) = compare b a
        m = countIPs f
    mapM_ putStrLn . zipWith pretty [1..] . take 10 . sortBy mostPopular . M.toList $ m

編集:

+RTS -A16M を追加すると、GC が 20% に減少しました。もちろんメモリ使用量は変わりません。

4

2 に答える 2

3

コードに次の変更を加えることをお勧めします。

@@ -1,4 +1,4 @@
-{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE BangPatterns, OverloadedStrings #-}

 module Main where

@@ -9,7 +9,7 @@
 import Control.Monad (liftM)
 import System.Environment (getArgs)
 import Prelude hiding (takeWhile)
-import qualified Data.Map as M
+import qualified Data.HashMap.Strict as M
 import Data.List (foldl', sortBy)
 import Text.Printf (printf)
 import Data.Maybe (fromMaybe)
@@ -17,15 +17,15 @@
 type Command = String

 data LogLine = LogLine {
-    getIP     :: S.ByteString,
-    getIdent  :: S.ByteString,
-    getUser   :: S.ByteString,
-    getDate   :: S.ByteString,
-    getReq    :: S.ByteString,
-    getStatus :: S.ByteString,
-    getBytes  :: S.ByteString,
-    getPath   :: S.ByteString,
-    getUA     :: S.ByteString
+    getIP     :: !S.ByteString,
+    getIdent  :: !S.ByteString,
+    getUser   :: !S.ByteString,
+    getDate   :: !S.ByteString,
+    getReq    :: !S.ByteString,
+    getStatus :: !S.ByteString,
+    getBytes  :: !S.ByteString,
+    getPath   :: !S.ByteString,
+    getUA     :: !S.ByteString
 } deriving (Ord, Show, Eq)

 quote, lbrack, rbrack, space :: Parser Char
@@ -39,14 +39,14 @@
     quote
     res <- takeTill (== '\"')
     quote
-    return res
+    return $! res

 bracketedVal :: Parser S.ByteString
 bracketedVal = do
     lbrack
     res <- takeTill (== ']')
     rbrack
-    return res
+    return $! res

 val :: Parser S.ByteString
 val = takeTill (== ' ')
@@ -67,14 +67,14 @@
     space
     bytes <- val
     (path,ua) <- option ("","") combined
-    return $ LogLine ip identity user date req status bytes path ua
+    return $! LogLine ip identity user date req status bytes path ua

 combined :: Parser (S.ByteString,S.ByteString)
 combined = do
     space
-    path <- quotedVal
+    !path <- quotedVal
     space
-    ua <- quotedVal
+    !ua <- quotedVal
     return (path,ua)

 countBytes :: [L.ByteString] -> Int
@@ -84,11 +84,11 @@
             Just x  -> (acc +) . maybe 0 fst . S.readInt . getBytes $ x
             Nothing -> acc

-countIPs :: [L.ByteString] -> M.Map S.ByteString Int
+countIPs :: [L.ByteString] -> M.HashMap S.ByteString Int
 countIPs = foldl' count M.empty
     where
         count acc l = case AL.maybeResult $ AL.parse line l of
-            Just x -> M.insertWith' (+) (getIP x) 1 acc
+            Just x -> M.insertWith (+) (getIP x) 1 acc
             Nothing -> acc

 ---------------------------------------------------------------------------------

LogLine構文解析に関連する式を参照するサンクが含まれないように、strictのフィールドを作成しました。本当に怠け者にする必要がない限り、フィールドを厳密にすることをお勧めします。

解析結果ができるだけ早く作成されるようにしました (これ$!は変更の一部です)。また、実際に の個々のフィールドを検査するまで解析が遅れないようにしましたLogLine

最後に、 unordered-containers パッケージHashMapから、より優れたデータ構造に切り替えました。のすべての関数は厳密な値であることに注意してください。つまり、単純なバリアントを使用できます。Data.HashMap.StrictinsertWith

a の部分文字列を取得するByteStringと、基になるストレージが共有されるため、元の文字列が強制的にメモリに保持されることに注意してください (これは Java の の場合と同じですString)。余分なメモリが保持されないようにしたい場合は、パッケージのcopy関数を使用してください。の結果bytestringを呼び出して、違いがあるかどうかを確認できます。ここでのトレードオフは、余分な計算を使用して文字列をコピーする代わりに、スペースの使用量を減らすことです。copy(getIP x)

-A<high number>を使用すると、実行時間の短いプログラム (つまり、ベンチマーク) のパフォーマンスが向上する傾向がありますが、実際のプログラムでは必ずしもそうではないことに注意してください。についても同様です-H。少なくともより高い-H値 (例: 1G) は、プログラムのパフォーマンスを損なうことはありません。

于 2011-06-23T19:24:57.223 に答える
0

最も明白な点は、最初のスクリプトはデータを検出するとすぐに破棄できるのに対し、2 番目のスクリプトは検出したすべてのデータを保持する必要があるということです。したがって、最初のスクリプトは一定のスペースで実行できるのに対し、2 番目のスクリプトは少なくとも O(N) のメモリを必要とすることが予想されます。

ヒープ プロファイリングをオンにして実行してみましたか? コード内で過剰な割り当てが発生しそうな場所を突き止めることはできますが、ハード データに代わるものはありません。

私は Data.Map.insertWith' 呼び出しを自分自身で疑いを持って見ています。それぞれが既存の Map 余剰のチャンクを要件にレンダリングし、コピーとリバランスを必要とするためですが、それは私の部分の純粋な推測です。insertWith' 呼び出しが原因である場合は、インタースティシャル Map エントリが必要ないため、マップ全体を 1 回のパスで (IP をカウントするためのインクリメントなしで) 構築し、2 回目のパスでカウントを実行する方が速い場合あります。 . そうすれば、マップの再調整に時間を無駄にすることはありません。また、キーのデータ型が Int に収まるという事実を利用して (少なくとも IPv4 アドレスの場合はそうです)、代わりに Data.IntMap を使用すると、メモリ オーバーヘッドがはるかに低くなります。

于 2011-06-23T13:22:58.130 に答える