解析大型文本文件最终导致内存和性能问题

Parsing Large Text Files Eventually Leading to Memory and Performance Issues

我正在尝试处理包含多行事件的大型文本文件(500 MB - 2+ GB)并通过系统日志将它们发送出去。到目前为止,我的脚本似乎可以正常运行一段时间,但一段时间后它会导致 ISE(64 位)不响应并耗尽所有系统内存。

我也很好奇是否有提高速度的方法,因为当前脚本仅以每秒大约 300 个事件的速度发送到系统日志。

示例数据

START--random stuff here 
more random stuff on this new line 
more stuff and things 
START--some random things 
additional random things 
blah blah 
START--data data more data 
START--things 
blah data

代码

Function SendSyslogEvent {

    $Server = '1.1.1.1'
    $Message = $global:Event
    #0=EMERG 1=Alert 2=CRIT 3=ERR 4=WARNING 5=NOTICE  6=INFO  7=DEBUG
    $Severity = '10'
    #(16-23)=LOCAL0-LOCAL7
    $Facility = '22'
    $Hostname= 'ServerSyslogEvents'
    # Create a UDP Client Object
    $UDPCLient = New-Object System.Net.Sockets.UdpClient
    $UDPCLient.Connect($Server, 514)
    # Calculate the priority
    $Priority = ([int]$Facility * 8) + [int]$Severity
    #Time format the SW syslog understands
    $Timestamp = Get-Date -Format "MMM dd HH:mm:ss"
    # Assemble the full syslog formatted message
    $FullSyslogMessage = "<{0}>{1} {2} {3}" -f $Priority, $Timestamp, $Hostname, $Message
    # create an ASCII Encoding object
    $Encoding = [System.Text.Encoding]::ASCII
    # Convert into byte array representation
    $ByteSyslogMessage = $Encoding.GetBytes($FullSyslogMessage)
    # Send the Message
    $UDPCLient.Send($ByteSyslogMessage, $ByteSyslogMessage.Length) | out-null
}

$LogFiles = Get-ChildItem -Path E:\Unzipped\

foreach ($File in $LogFiles){
    $EventCount = 0
    $global:Event = ''
    switch -Regex -File $File.fullname {
      '^START--' {  #Regex to find events
        if ($global:Event) {
            # send previous events' lines to syslog
            write-host "Send event to syslog........................."
            $EventCount ++
            SendSyslogEvent
        }
        # Current line is the start of a new event.
        $global:Event = $_
      }
      default { 
        # Event-interior line, append it.
        $global:Event += [Environment]::NewLine + $_
      }
    }
    # Process last block.
    if ($global:Event) { 
        # send last event's lines to syslog
        write-host "Send last event to syslog-------------------------"
        $EventCount ++
        SendSyslogEvent
    }
}

你的脚本中有一些非常糟糕的东西,但在我们开始之前让我们看看你如何参数化你的系统日志函数。

参数化你的函数

powershell 中的脚本块和函数支持在适当命名的 param-块中可选类型的参数声明。

为了这个答案的目的,让我们只关注调用当前函数时唯一改变的东西,即 消息 。如果我们把它变成一个参数,我们最终会得到一个看起来更像这样的函数定义:

function Send-SyslogEvent {
    param(
        [string]$Message
    )

    $Server = '1.1.1.1'
    $Severity = '10'
    $Facility = '22'
    # ... rest of the function here
}

(我冒昧的改成了PowerShell特有的Verb-Noun命令命名规范)

使用参数而不是全局变量有 性能优势,但真正的好处是您最终会得到干净和正确的代码,这会让你省去其他的麻烦。


IDisposable

.NET 是一个 "managed" 运行时,这意味着我们真的不需要担心资源管理(例如分配和释放内存),但在某些情况下我们必须管理运行时 外部 的资源 - 例如 UDPClient 对象使用的网络套接字 :)

依赖这些外部资源的类型通常实现IDisposable接口,这里的黄金法则是:

Who-ever creates a new IDisposable object should also dispose of it as soon as possible, preferably at latest when exiting the scope in which it was created.

因此,当您在 Send-SyslogEvent 中创建 UDPClient 的新实例时,您还应确保在从 Send-SyslogEvent 返回之前始终调用 $UDPClient.Dispose()。我们可以用一组 try/finally 个块来做到这一点:


function Send-SyslogEvent {
    param(
        [string]$Message
    )

    $Server = '1.1.1.1'
    $Severity = '10'
    $Facility = '22'
    $Hostname= 'ServerSyslogEvents'
    try{
        $UDPCLient = New-Object System.Net.Sockets.UdpClient
        $UDPCLient.Connect($Server, 514)

        $Priority = ([int]$Facility * 8) + [int]$Severity

        $Timestamp = Get-Date -Format "MMM dd HH:mm:ss"

        $FullSyslogMessage = "<{0}>{1} {2} {3}" -f $Priority, $Timestamp, $Hostname, $Message

        $Encoding = [System.Text.Encoding]::ASCII

        $ByteSyslogMessage = $Encoding.GetBytes($FullSyslogMessage)
        $UDPCLient.Send($ByteSyslogMessage, $ByteSyslogMessage.Length) | out-null
    }
    finally {
        # this is the important part
        if($UDPCLient){
            $UDPCLient.Dispose()
        }
    }
}

未能处理 IDisposable 对象是泄漏内存并在您 运行 所在的操作系统中引起资源争用的最可靠方法之一,因此这绝对是 必须,尤其是对性能敏感或频繁调用的代码。


重复使用实例!

现在,我在上面展示了您应该如何处理 UDPClient 的处置,但您可以做的另一件事是 重新使用 同一个客户端 - 您将无论如何每次都连接到同一个系统日志主机!

function Send-SyslogEvent {
    param(
        [Parameter(Mandatory = $true)]
        [string]$Message,

        [Parameter(Mandatory = $false)]
        [System.Net.Sockets.UdpClient]$Client
    )

    $Server = '1.1.1.1'
    $Severity = '10'
    $Facility = '22'
    $Hostname= 'ServerSyslogEvents'
    try{
        # check if an already connected UDPClient object was passed
        if($PSBoundParameters.ContainsKey('Client') -and $Client.Available){
            $UDPClient = $Client
            $borrowedClient = $true
        }
        else{
            $UDPClient = New-Object System.Net.Sockets.UdpClient
            $UDPClient.Connect($Server, 514)
        }

        $Priority = ([int]$Facility * 8) + [int]$Severity

        $Timestamp = Get-Date -Format "MMM dd HH:mm:ss"

        $FullSyslogMessage = "<{0}>{1} {2} {3}" -f $Priority, $Timestamp, $Hostname, $Message

        $Encoding = [System.Text.Encoding]::ASCII

        $ByteSyslogMessage = $Encoding.GetBytes($FullSyslogMessage)
        $UDPCLient.Send($ByteSyslogMessage, $ByteSyslogMessage.Length) | out-null
    }
    finally {
        # this is the important part
        # if we "borrowed" the client from the caller we won't dispose of it 
        if($UDPCLient -and -not $borrowedClient){
            $UDPCLient.Dispose()
        }
    }
}

最后的修改将允许我们创建 UDPClient 一次 并一遍又一遍地重复使用它:

# ...
$SyslogClient = New-Object System.Net.Sockets.UdpClient
$SyslogClient.Connect($SyslogServer, 514)

foreach($file in $LogFiles)
{
    # ... assign the relevant output from the logs to $message, or pass $_ directly: 
    Send-SyslogEvent -Message $message -Client $SyslogClient 
    # ...
}

使用 StreamReader 而不是 switch

最后,如果您想在读取文件时尽量减少分配,例如使用 File.OpenText() 创建一个 StreamReader 来逐行读取文件:

$SyslogClient = New-Object System.Net.Sockets.UdpClient
$SyslogClient.Connect($SyslogServer, 514)

foreach($File in $LogFiles)
{
    try{
        $reader = [System.IO.File]::OpenText($File.FullName)

        $msg = ''

        while($null -ne ($line = $reader.ReadLine()))
        {
            if($line.StartsWith('START--'))
            {
                if($msg){
                    Send-SyslogEvent -Message $msg -Client $SyslogClient
                }
                $msg = $line
            }
            else
            {
                $msg = $msg,$line -join [System.Environment]::NewLine
            }
        }
        if($msg){
            # last block
            Send-SyslogEvent -Message $msg -Client $SyslogClient
        }
    }
    finally{
        # Same as with UDPClient, remember to dispose of the reader.
        if($reader){
            $reader.Dispose()
        }
    }
}

这可能 switch,尽管我怀疑您是否会看到内存占用量有很大改善 - 仅仅是因为相同的字符串在 .NET 中 interned(它们基本上缓存在一个大的内存池中)。


正在检查 IDisposable

的类型

您可以使用 -is 运算符测试对象是否实现了 IDisposable

PS C:\> $reader -is [System.IDisposable]
True

或使用Type.GetInterfaces(),

PS C:\> [System.Net.Sockets.UdpClient].GetInterfaces()

IsPublic IsSerial Name
-------- -------- ----
True     False    IDisposable

希望以上内容对您有所帮助!

这是一次一行切换文件的方法示例。

get-content file.log | foreach { 
  switch -regex ($_) { 
    '^START--' { "start line is $_"} 
    default    { "line is $_" } 
  } 
}

实际上,我认为 switch -file 不是问题。根据另一个 window 中的 "ps powershell",似乎已优化为不使用过多内存。我用一个演出文件试了一下。