Как разработчик, я часто разрабатываю скрипты развертывания. В одном из проектов передо мной возникла задача автоматизировать развертывание проекта, которое состояло из нескольких десятков заданий, с возможностью настраивать состав разворачиваемых на стенд компонентов.
В первую очередь была проведена работа по унификации интерфейсов заданий и были выделены следующие методы:
Учитывая, что подобных шагов становилось все больше и больше, поддерживать в таком виде скрипты становилось все сложнее. Изучив возможные решения, было принято решение реализовать каждое задание как отдельный объект:
В таком виде скрипты развертывания работали продолжительное время и ничто не предвещало беды. В один прекрасный момент передо мной встала задача провести развертывание на удаленном сервере. В powershell есть очень удобный механизм WinRM, который мы ранее очень активно использовали, и, соответственно, для решения поставленной задачи остановился на нем-же.
Решение работало нестабильно. На некоторых заданиях развертывания возникали либо ошибка либо Invoke-Command показывал, что удаленный скрипт выполнился корректно, но по факту он прерывался.
В EventViewer смог найти, что процесс на удаленной машине завершался с ошибкой 1726, но никакой вразумительной информации об ошибке обнаружить не удавалось. При этом запуск того-же самого задания на удаленной машине всегда завершалось успешно.
В ходе многочисленных экспериментов поймал в ошибку The script failed due to call depth overflow которая определила дальнейшее направление исследований.
Со времен PowerShell v2 максимальная глубина стека в скриптах powershell составляет 1000 вызовов, в последующих версиях это значение было еще существенно поднято и ошибок типа stack overflow никогда не возникало.
Решил провести несколько тестов для определения глубины стека при вызове локально и через WinRM. Для этого подготовил инструментарий тестирования.
Первый тест определял возможную глубину рекурсии:
По результату — локально глубина стека более 3000, удаленно — немного больше 150.
150 — довольно большое значение. Достичь его в реальной работе скриптов развертывания нереально.
Второй тест определяет возможную глубину рекурсии при использовании объектов:
Результаты немного хуже. Удаленно глубина стека 130-133. Но для работы это тоже очень большое значение.
Дальнейшее изучение исходных скриптов развертывания натолкнуло на мысль проверить, как работают try-catch блоки:
И вот тут меня ожидал огромный сюрприз. При использовании «объектов» и генерации исключительной ситуации возможная глубина стека локально составила около 130, а удаленно всего 5.
Но при отказе от использования «объектов» проблема исчезала. Значения глубины стека оказались на уровне первого теста.
В powershell 5 появились классы. Провел тест с их использованием:
Особого выигрыша не получили. При вызове через WinRM глубина стека составила всего 7 хопов. Чего так-же недостаточно для нормальной работы скриптов.
Работая со скриптами тестирования пришла мысль реализовать объекты при помощи hash + script block.
Глубина стека в 55 хопов — это уже вполне достаточное значение.
Ниже свел в одну таблицу результаты тестирования доступной глубина стека:
Надеюсь, что эта информация окажется полезной не только мне! :)
В первую очередь была проведена работа по унификации интерфейсов заданий и были выделены следующие методы:
Интерфейс задания развертывания
$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 |
Надеюсь, что эта информация окажется полезной не только мне! :)
