Memory leak using Powershell Remote Calls in C#

2019-04-06 06:55发布

问题:

I have a windows service that is doing a lot of exchange remote calls to get some server information. I noticed that as longs as the time passes the memory used by the service starts growing until a memory exception is thrown. I have searched and it looks like there is a known memory leak in the System.Management.Automation that does not dispose all the memory of the Runspace created while calling the close and/or dispose method. I reviewed a post that suggest using the CreateOutOfProcessRunspace of the RunspaceFactory but not sure how to use it.

Here is how the issue can be reproduced: (System.Management.Automation dll referenced)

for (int i = 0; i < 1000; i++)
{
    var runspace = RunspaceFactory.CreateRunspace();
    runspace.Open();
    runspace.Close();
    runspace.Dispose();
}

If you run this code, you will see how the memory is incremented. Due to the requirements, keeping a connection open as much as possible is not a good solution.

Do you know how I can fix this issue, even using the CreateOutOfProcessRunspace method of the RunspaceFactory or how to propery dispose the memory?

Thanks in advance

EDIT

I was using the V3 and change the runspace creation to use the CreateRunspacePool method and it looks like the leak is gone. Thanks so much for your help!

回答1:

I can see the problem in PS v3.0 but not in PS v2.0. Here is the code I use to see this (all examples are in PowerShell):

for() {
    $runspace = [runspacefactory]::CreateRunspace()
    $runspace.Open()
    $runspace.Close()
    $p = Get-Process -Id $PID
    '{0} {1}' -f $p.Handles, ($p.PrivateMemorySize / 1mb)
}

It looks like handles and memory are leaking in v3.0 in the code above.

As far as v2.0 does not have this problem, one possible workaround may be to start the service using PS v2.0, i.e. PowerShell.exe -Version 2.0.

If this is not possible I can think of two more workarounds. One of them is not to create runspaces directly but use [powershell] instead. For example, this code does not show the leak in v3.0:

for() {
    $ps = [powershell]::Create()
    $p = $ps.AddCommand('Get-Process').AddParameter('Id', $PID).Invoke()
    '{0} {1}' -f $p.Handles, ($p.PrivateMemorySize / 1mb)
    $ps.Dispose()
}

Another workaround, if it is applicable, may be use of [runspacefactory]::CreateRunspacePool(). This way also does not show the leak:

$rs = [runspacefactory]::CreateRunspacePool()
$rs.Open()
for() {
    $ps = [powershell]::Create()
    $ps.RunspacePool = $rs
    $p = $ps.AddCommand('Get-Process').AddParameter('Id', $PID).Invoke()
    '{0} {1}' -f $p.Handles, ($p.PrivateMemorySize / 1mb)
    $ps.Dispose()
}
#$rs.Close() # just a reminder, it's not called here due to the infinite loop

The last one also works much faster because the runspace is kind of reused.



回答2:

I was also facing the same issue when I was using v1 of System.Management.Automation. But the issue was solved with v3 of System.Management.Automation and changing the code to use the CreateOutOfProcessRunspace method

Here is the code

            using (PowerShellProcessInstance instance = new PowerShellProcessInstance(new Version(4, 0), null, null, false))
        {

            using (var runspace = RunspaceFactory.CreateOutOfProcessRunspace(new TypeTable(new string[0]), instance))
            {
                runspace.Open();

                using (PowerShell powerShellInstance = PowerShell.Create())
                {
                    powerShellInstance.Runspace = runspace;

                    var filePath = GetScriptFullName(powerShellScriptType);
                    powerShellInstance.Commands.AddScript(File.ReadAllText(filePath));

                    var includeScript = GetIncludeScript();
                    powerShellInstance.AddParameters(new List<string>
                {
                    userName,
                    plainPassword,
                    includeScript
                });
                    Collection<PSObject> psOutput = powerShellInstance.Invoke();

                    // check the other output streams (for example, the error stream)
                    if (powerShellInstance.Streams.Error.Count > 0)
                    {
                        // error records were written to the error stream.
                        // do something with the items found.
                        var exceptions = "";
                        foreach (var error in powerShellInstance.Streams.Error)
                        {
                            exceptions += error.Exception + "\n";
                        }

                        throw new InvalidPowerShellStateException(exceptions);

                    }
                    return psOutput;
                }
            }
        }