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

Posted

技术标签:

【中文标题】解析大型文本文件最终导致内存和性能问题【英文标题】:Parsing Large Text Files Eventually Leading to Memory and Performance Issues 【发布时间】:2019-08-21 16:05:10 【问题描述】:

我正在尝试处理包含多行事件的大型文本文件(500 MB - 2+ GB)并将它们发送到 VIA 系统日志。到目前为止,我的脚本似乎在很长一段时间内都运行良好,但过了一段时间,它导致 ISE(64 位)没有响应并耗尽了所有系统内存。

我也很好奇是否有办法提高速度,因为当前脚本仅以每秒约 300 个事件发送到 syslog。

示例数据

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
    

【问题讨论】:

1) 停止附加到全局变量,2) 处理您的 $UDPCLient,也许 3) 重新使用 UDPClient 连接,而不是重新创建它时间:) 这些听起来不错。你有机会提供例子吗?我不确定我应该怎么做。我在功能范围内挣扎,我不知道如何做其他两件事。真的很感激。 最大的文件是2GB? 到目前为止,我已经看到它们大到 2.5 GB。 如果是这种情况,那么使用开关可能是错误的方法。您可能需要考虑使用流。 【参考方案1】:

您的脚本中有几处真正糟糕的东西,但在我们开始之前,让我们先看看如何参数化您的系统日志函数。

参数化你的函数

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

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

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 是一个“托管”运行时,这意味着我们真的不需要担心资源管理(例如分配和释放内存),但在某些情况下我们必须管理以下资源: em>external 运行时 - 例如 UDPClient 对象使用的网络套接字 :)

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

创建一个新的IDisposable对象也应该处理尽快,最好最迟在退出创建它的范围。

因此,当您在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,但您可以做的另一件事是重新使用同一个客户端 - 您将连接到同一个 syslog 主机反正每次都是!

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

最后,如果您想在 slurping 文件时尽量减少分配,例如使用 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更快,虽然我怀疑你会看到内存占用有很大的改善 - 仅仅是因为相同的字符串是 interned .NET 中的 em>(它们基本上缓存在一个大的内存池中)。


检查IDisposable的类型

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

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

或使用Type.GetInterfaces()、as suggested by the TheIncorrigible1

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

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

希望以上内容对你有帮助!

【讨论】:

关于IDisposable的专业提示,可以通过调用该类型的相关方法找到该类型的所有接口:[Net.Sockets.UdpClient].GetInterfaces()另外,验证空值:if ($null -ne $obj) $obj.Dispose() 这似乎工作得更快,大约是原来的 3 倍。非常感谢您花时间整理这些东西,我学到了很多东西!我将不得不等待,看看它是否在没有内存问题的情况下完成了文件,还有更多。 这是官方的,成功了!完成一个 2GB 文件 2.25 小时,难以置信。再次感谢您。【参考方案2】:

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

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

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

【讨论】:

以上是关于解析大型文本文件最终导致内存和性能问题的主要内容,如果未能解决你的问题,请参考以下文章

如何提高大型文本文件的数据加载性能[重复]

大型网络中的 RPC 性能

linux cached过高导致性能变低

性能测试WAS内存使用的探索和分析

.NET程序内存分析工具CLRProfiler的使用(性能测试)

在一个线程上删除具有数百万个字符串的大型哈希图会影响另一个线程的性能