解析大型文本文件最终导致内存和性能问题
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
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",似乎已优化为不使用过多内存。我用一个演出文件试了一下。
我正在尝试处理包含多行事件的大型文本文件(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
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",似乎已优化为不使用过多内存。我用一个演出文件试了一下。