Powershell и глубина стека

в 14:25, , рубрики: devops, powershell, StackDepth, WinRM

Как разработчик, я часто разрабатываю скрипты развертывания. В одном из проектов передо мной возникла задача автоматизировать развертывание проекта, которое состояло из нескольких десятков заданий, с возможностью настраивать состав разворачиваемых на стенд компонентов.

В первую очередь была проведена работа по унификации интерфейсов заданий и были выделены следующие методы:

Интерфейс задания развертывания

$Task1_Config = ...;

# проверить, возможно ли выполнить шаг развертывания.
function Task1_CheckRequirements() {}

# проверить, необходимо ли выполнять шаг развертывания.
function Task1_CanExecute($project) {}

# выполнить шаг развертывания.
function Task1_Execute($project, $context) {}

Учитывая, что подобных шагов становилось все больше и больше, поддерживать в таком виде скрипты становилось все сложнее. Изучив возможные решения, было принято решение реализовать каждое задание как отдельный объект:

Интерфейс `объекта` для задания развертывания

function Task1()
{
    $result = New-Object -Typename PSObject -Property `
    @{
        "name" = "Task1"
        "config" = ...
    }

    Add-Member -InputObject $result -MemberType ScriptMethod -Name CheckRequirements -Value `
    { }

    Add-Member -InputObject $result -MemberType ScriptMethod -Name CanExecute -Value `
    {
        Param($project)
    }

    Add-Member -InputObject $result -MemberType ScriptMethod -Name Execute -Value `
    {
        Param($project, $context)
    }

    return $result
}

В таком виде скрипты развертывания работали продолжительное время и ничто не предвещало беды. В один прекрасный момент передо мной встала задача провести развертывание на удаленном сервере. В powershell есть очень удобный механизм WinRM, который мы ранее очень активно использовали, и, соответственно, для решения поставленной задачи остановился на нем-же.

Решение работало нестабильно. На некоторых заданиях развертывания возникали либо ошибка либо Invoke-Command показывал, что удаленный скрипт выполнился корректно, но по факту он прерывался.

Не удалось обработать данные удаленной команды. Сообщение об ошибке: Ведущий процесс поставщика WSMan не вернул правильный ответ. Поставщик в ведущем процессе может вести себя неправильно

Processing data for a remote command failed with the following error message: The WSMan provider host process did not return a proper response. A provider in the host process may have behaved improperly.

В EventViewer смог найти, что процесс на удаленной машине завершался с ошибкой 1726, но никакой вразумительной информации об ошибке обнаружить не удавалось. При этом запуск того-же самого задания на удаленной машине всегда завершалось успешно.

В ходе многочисленных экспериментов поймал в ошибку The script failed due to call depth overflow которая определила дальнейшее направление исследований.

Со времен PowerShell v2 максимальная глубина стека в скриптах powershell составляет 1000 вызовов, в последующих версиях это значение было еще существенно поднято и ошибок типа stack overflow никогда не возникало.

Решил провести несколько тестов для определения глубины стека при вызове локально и через WinRM. Для этого подготовил инструментарий тестирования.

Инструментарий тестирования

$ErrorActionPreference = "Stop"
$cred = New-Object System.Management.Automation.PsCredential(...)

function runLocal($sb, $cnt)
{
    Write-Host "Local $cnt"
    Invoke-Command -ScriptBlock $sb -ArgumentList @($cnt)
}

function runRemote($sb, $cnt)
{
    Write-Host "Remote $cnt"

    $s = New-PSSession "." -credential $cred
    try
    {
        Invoke-Command -Session $s -ScriptBlock $sb -ArgumentList @($cnt)
    }
    finally
    {
        Remove-PSSession -Session $s
    }
}

Первый тест определял возможную глубину рекурсии:

Определение глубины рекурсии

$scriptBlock1 = 
{
    Param($cnt)

    function test($cnt)
    {
        if($cnt -ne 0)
        {
            test $($cnt - 1)
            return
        }

        Write-Host "  Call depth: $($(Get-PSCallStack).Count)"
    }

    test $cnt
}

runLocal $scriptBlock1 3000
runRemote $scriptBlock1 150
runRemote $scriptBlock1 160
----------
Local 3000
  Call depth: 3004
Remote 150
  Call depth: 152
Remote 160
The script failed due to call depth overflow.

По результату — локально глубина стека более 3000, удаленно — немного больше 150.

150 — довольно большое значение. Достичь его в реальной работе скриптов развертывания нереально.

Второй тест определяет возможную глубину рекурсии при использовании объектов:

Определение глубины рекурсии при использовании объектов

$scriptBlock2 = 
{
    Param($cnt)

    function test()
    {
        $result = New-Object -Typename PSObject -Property @{ }

        Add-Member -InputObject $result -MemberType ScriptMethod -Name Execute -Value `
        {
            Param($cnt)

            if($cnt -ne 0)
            {
                $this.Execute($cnt - 1)
                return
            }

            Write-Host "  Call depth: $($(Get-PSCallStack).Count)"
        }

        return $result
    }

    $obj = test
    $obj.Execute($cnt)
}

runLocal $scriptBlock2 3000
runRemote $scriptBlock2 130
runRemote $scriptBlock2 135
----------
Local 3000
  Call depth: 3004
Remote 130
  Call depth: 132
Remote 135
Processing data for a remote command failed with the following error message: The WSMan provider host process did not return a proper response. 

Результаты немного хуже. Удаленно глубина стека 130-133. Но для работы это тоже очень большое значение.

Дальнейшее изучение исходных скриптов развертывания натолкнуло на мысль проверить, как работают try-catch блоки:

Определение глубины рекурсии при использовании объектов и try-catch

$scriptBlock3 = 
{
    Param($cnt)

    function test()
    {
        $result = New-Object -Typename PSObject -Property @{ }

        Add-Member -InputObject $result -MemberType ScriptMethod -Name Execute -Value `
        {
            Param($cnt)

            if($cnt -ne 0)
            {
                $this.Execute($cnt - 1)
                return
            }

            Write-Host "  Call depth: $($(Get-PSCallStack).Count)"
            throw "error"
        }

        return $result
    }

    try
    {
        $obj = test
        $obj.Execute($cnt)
    }
    catch
    {
        Write-Host "  Exception catched"
    }
}

runLocal $scriptBlock3 130
runRemote $scriptBlock3 5
runRemote $scriptBlock3 6
----------
Local 130
  Call depth: 134
  Exception catched
Remote 5
  Call depth: 7
  Exception catched
Remote 6
  Call depth: 8
The script failed due to call depth overflow.

И вот тут меня ожидал огромный сюрприз. При использовании «объектов» и генерации исключительной ситуации возможная глубина стека локально составила около 130, а удаленно всего 5.

Определение глубины рекурсии при использовании try-catch без объектов

$scriptBlock4 = 
{
    Param($cnt)

    function test($cnt)
    {
        if($cnt -ne 0)
        {
            test $($cnt - 1)
            return
        }

        Write-Host "  Call depth: $($(Get-PSCallStack).Count)"
        throw "error"
    }

    try
    {
        test $cnt
    }
    catch
    {
        Write-Host "  Exception catched"
    }    
}

runLocal $scriptBlock4 2000
runRemote $scriptBlock4 150
----------
Local 2000
  Call depth: 2004
  Exception catched
Remote 150
  Call depth: 152
  Exception catched

Но при отказе от использования «объектов» проблема исчезала. Значения глубины стека оказались на уровне первого теста.

В powershell 5 появились классы. Провел тест с их использованием:

Определение глубины рекурсии при использовании try-catch без объектов

$scriptBlock5 = 
{
    Param($cnt)

    Class test
    {
        Execute($cnt)
        {
            if($cnt -ne 0)
            {
                $this.Execute($cnt - 1)
                return
            }

            Write-Host "  Call depth: $($(Get-PSCallStack).Count)"
            throw "error"
        }
    }

    try
    {
        $t = [test]::new()
        $t.Execute($cnt)
    }
    catch
    {
        Write-Host "Exception catched"
    }    
}

runLocal $scriptBlock5 130
runRemote $scriptBlock5 7
runRemote $scriptBlock5 8
----------
Local 130
  Call depth: 134
Exception catched
Remote 7
  Call depth: 9
Exception catched
Remote 8
  Call depth: 10
The script failed due to call depth overflow.

Особого выигрыша не получили. При вызове через WinRM глубина стека составила всего 7 хопов. Чего так-же недостаточно для нормальной работы скриптов.

Работая со скриптами тестирования пришла мысль реализовать объекты при помощи hash + script block.

Определение глубины рекурсии при использовании try-catch и hash + script block

$scriptBlock6 = 
{
    Param($cnt)

    function Call($self, $scriptName, [parameter(ValueFromRemainingArguments = $true)] $args)
    {
        $args2 = @($self) + $args
        Invoke-Command -ScriptBlock $self.$scriptName -ArgumentList $args2
    }

    function test()
    {
        $result = @{ }

        $result.Execute =
        {
            Param($self, $cnt)

            if($cnt -ne 0)
            {
                Call $self Execute $($cnt - 1)
                return
            }

            Write-Host "  Call depth: $($(Get-PSCallStack).Count)"
            throw "error"
        }

        return $result
    }

    try
    {
        $obj = test
        Call $obj Execute $cnt
    }
    catch
    {
        Write-Host "Exception catched"
    }
}

runLocal $scriptBlock6 1000
runRemote $scriptBlock6 55
runRemote $scriptBlock6 60
----------
runLocal $scriptBlock6 1000
runRemote $scriptBlock6 55
runRemote $scriptBlock6 60
Local 1000
  Call depth: 2005
Exception catched
Remote 55
  Call depth: 113
Exception catched
Remote 60
Exception catched

Глубина стека в 55 хопов — это уже вполне достаточное значение.

Ниже свел в одну таблицу результаты тестирования доступной глубина стека:

локально через winRM
Функция >3000 ~150
Метода объекта >3000 ~130
Метода объекта с try-catch ~130 5
Функция с try-catch >2000 ~150
Метода класса (PS5) с try-catch ~130 7
Hash + script block с try-catch >1000 ~55

Надеюсь, что эта информация окажется полезной не только мне! :)

Автор: kuda78

Источник

Поделиться

* - обязательные к заполнению поля