Содержание статьи
PowerShell — очень удобный инструмент администратора, предоставляющий практически безграничные возможности по настройке серверов, виртуальных машин, а также сбору информации об их состоянии. Он достаточно прост: можно быстро писать скрипты, не вникая в детали. Но как и любой язык программирования, PS имеет свои тонкости и нюансы, не владея которыми, нельзя эффективно его использовать.
Берем только нужное
Командная оболочка оперирует множеством параметров объектов, к отбору которых необходимо подходить взвешенно, определяясь с их дальнейшей необходимостью.
Ведь каждый вызванный объект увеличивает количество памяти, требуемое для его хранения. Если взять больше, то в определенный момент получим ошибку "System. OutOfMemoryException". То есть сначала извлечь все параметры объекта и ненужные объекты, а затем отфильтровать то, что действительно необходимо, — плохая идея.
Использование лишних выборок существенно увеличивает время исполнения скрипта и повышает требования к системным ресурсам. Лучше сразу взять то, что планируется обрабатывать, или выводить дальше. Для примера проверь время исполнения двух команд:
PS> Get-Process | Where ($_.ProcessName -eq "explorer")
PS> Get-Process explorer
Вторая выполнится примерно в два раза быстрее, а полученный результат будет одинаков.
В больших скриптах при большом количестве данных разница в скорости выполнения будет весьма ощутимой.
Теперь ситуация, которая не менее редка в сценариях PowerShell. Есть список объектов, и нужно произвести с ними некоторые действия.
Для этих целей используют командлет ForEach-Object (алиас foreach) или стандартный оператор foreach (поэтому их часто путают). Например, очень часто в скриптах извлекают параметры и присваивают их переменным, которые затем последовательно обрабатывают.
PS> $computers = Get-ADComputer
PS> foreach ($computer in $computers) { что-то делаем }
Этот пример можно переписать несколько иначе:
PS> Get-ADComputer | ForEach-Object
{ что-то делаем }
В первом случае мы вначале присваиваем значение переменной, а затем считываем.
Использование каналов (pipelines, "|") и командлета ForEach-Object во втором примере позволит избежать избыточного хранения большого количества данных, так как они будут обрабатываться сразу, по мере поступления.
В итоге вторая команда выполнится быстрее, а ресурсов потребует меньше. При работе с командлетами ActiveDirectory не забываем импортировать нужный модуль:
PS> import-module ActiveDirectory
Аналогичная ситуация, только не используется явно заданная переменная:
PS> foreach ($computer in GetADComputer) { $computer }
Здесь все равно вначале извлекаются все команды, которые сохраняются в переменной (что загружена в память) и затем последовательно выполняются элементы. Однако время работы команды и затраты ресурсов будут все же на порядок больше, чем при использовании каналов. Но не все так гладко. На простых примерах можно прийти к выводу, что от использования оператора foreach лучше отказаться, на самом деле внутренняя оптимизация PS иногда приводит к тому, что в операциях чтения foreach показывает лучшую производительность. Кроме этого, foreach предпочтителен, если объект уже имеется в памяти, например сохранен в переменной, то есть нет нужды его извлекать, а надо просто обработать. В некоторых случаях необходимо получить некоторые свойства и обработать их дважды, но по-разному, или сохранить в файл и просмотреть в консоли. Можно конечно, вызвать команду дважды (будь внимателен, например Get-Process, вызванный дважды, покажет разный результат), или сохранить значение в переменной. Но в PS есть еще одна интересная возможность: направить вывод в два потока.
Для этой цели используется командлет TeeObject. Например, получаем список процессов, сохраняем в файл и выводем на консоль:
PS> Get-Process | Tee-Object
-filepath C:\process.txt
Так как второй получатель не указан, то вывод данных производится на консоль. При желании можно обработать данные любым удобным способом:
PS> Get-Process | Tee-Object -filepath C:\process.txt |
Sort-Object cpu
Чтобы сохранить второй поток в файл, используем командлет Out-File:
PS> Get-Process | Tee-Object -filepath C:\process.txt |
Sort-Object cpu | Out-File C:\process-sort.txt
В качестве входного параметра командлет Tee-Object может принимать другой объект, на который следует указать при помощи ключа "-inputObject".
Читаем файлы
Для чтения или разбора файлов используются командлеты GetContent, Select-String, которые проходят файл построчно и возвращают объект. С файлами большого размера могут быть проблемы, но их легко решить, используя дополнительные параметры. Например, в Get-Content можно указать количество строк, считываемых за раз и передаваемых далее по конвейеру (по умолчанию все). Например, по 100 строк:
PS> Get-Content С:\system.log -Read 100
Соответственно увеличение этого числа ускоряет процесс чтения, но и увеличивает необходимые объемы памяти. Причем при использовании Read, скорее всего, потребуется вставка конвейера "| ForEach-Object ($_) |", чтобы в последующем возможно было обработать всю запись. К слову, команда:
PS> Get-Content biglogfile.log -read 1000 | ForEachObject {$_} | Where {$_ -like '*x*'}
выполнится примерно в 3 раза быстрее, чем:
PS> Get-Content biglogfile.log | Where {$_ -like '*x*'}
Командлет Get-Content лишь читает файлы, остальная обработка отдана на откуп другим командлетам. Например, Select-String может читать файлы или брать данные из канала, отбирая информацию по шаблону. Например, переберем все скрипты PS в текущем каталоге в поисках подстроки «PowerShell»:
PS> Select-String -path *.ps1 -pattern "PowerShell"
Для примера просмотри вывод, казалось бы, подобной команды:
PS> Get-Content -path *.ps1 | where {$_ -match "PowerShell"}
Главное отличие — отсутствие имени файла в выводе результата при использовании where, ведь в этом случае выводятся только совпадения, а не объекты. По умолчанию ищутся все вхождения образца, но в некоторых случаях необходимо найти все строки, где образец отсутствует.
Например, вместо того, чтобы искать все сообщения об ошибках, предупреждения (Warning, Failed и т.п.), проще убрать из вывода Success.
При помощи дополнительного параметра Select-String "–notMatch" это сделать проще, а скрипт будет работать быстрее:
PS> Select-String "Success" *.log –notMatch
Одна строчка в выводе часто не дает достаточно информации о событии, здесь на помощь приходит параметр "–context", который позволяет получить необходимое количество строк до и после совпадения. Например, выведем две строки из журналов до и после события со статусом Failed:
PS> Select-String "Failed" *.log -content 2
По умолчанию поиск регистронезависим, чтобы научить Select-String понимать регистр, используем "-caseSensitive".
В некоторых обзорах, в том числе и написанных сертифицированными Microsoft, командлет SelectString часто сравнивается с юниксовыми утилитами grep/egrep. Благо, реализаций grep для Windows сегодня более чем предостаточно:
GnuWin32 (gnuwin32.sf.net), Windows grep (wingrep.com), GNU Grep For Windows (steve.org.uk/Software/grep), два варианта Grep For Windows (grepforwindows.com, pages.interlog.com/~tcharron/grep.html) и многие другие.
По результатам прогонов grep существенно выигрывает по скорости выполнения у Select-String.
> grep Warning *.log
Но при его использовании дальнейшую обработку данных необходимо производить самостоятельно, ведь на выходе мы получаем «сырые» данные, а не объекты .Net. Если же в этом нет необходимости, то вполне достаточно использовать и grep. Напомню, что в Windows есть утилита findstr.exe, позволяющая находить нужные строки в файлах, но ее функционал жутко урезан, поэтому использование grep предпочтительнее.
В контексте чтения файлов стоит вспомнить и о массивах. В PS вообще упрощена работа с переменными, строками, массивами и хеш-таблицами, нужный тип присваивается автоматически (проверяется GetType().
FullName), размер устанавливается динамически. В итоге работать с ними удобно, нет необходимости в дополнительных проверках.
Но при обработке больших объемов данных вроде бы простой скрипт начинает заметно тормозить. Все дело в том, что при добавлении новых элементов в массив он перестраивается, на что опять же нужны время и ресурсы. Поэтому, если размеры массива известны заранее, то его лучше задать сразу, что ускорит в последующем работу с ним:
PS> $arr = New-Object string[] 300
Проверяем параметры массива:
PS> $arr.GetType().Basetype
Для примера код:
$arr = new-object int[] 1000
for ($i=0; $i –lt 1000; $i++)
{$arr[$i] = $i*2}
выполнится более чем в 10 раз быстрее, по сравнению с:
$arr = @()
for ($i=0; $i –lt 1000; $i++)
{$arr += $i*2}
Выражаемся регулярно
В PS реализован механизм Perl-подобных регулярных выражений, что позволяет при необходимости легко найти иголку в стоге сена. Если быть точнее, то PS является оболочкой и использует все, что заложено в технологии .NET Framework (класс System.Text.RegularExpressions.Regex). Для поиска совпадения используется параметр "-match" и его варианты "–cmatch" (case-sensitive, регистрозависимый) и "-imatch" (case insensitive, регистронезависимый). Например, нам нужен список IP-адресов, полученных при помощи ipconfig. Проще простого:
PS> ipconfig | where {$_ -match "\d{3,}"}
В качестве параметра в PS принимается регулярное выражение, которое может содержать все принятые знаки — *, ?, +, \w, \s, \d и так далее. Проверим правильность почтового адреса:
PS> $regex = "^[a-z]+\.[a-z]+@synack.ru$"
> If ($email –notmatch $regex) {
> Write-Error "Invalid e-mail address $email"
>}
Теперь, если почтовый адрес не принадлежит домену synack.ru и не попадает под шаблон (то есть содержит запрещенные знаки), то пользователь получит сообщение об ошибке (о командлете Write-Error читай в мини-статье «Форматируем вывод»).
Не буду останавливаться на подробном разборе и перечислении всех возможных параметров, используемых в регулярных выражениях, это достаточно емкая тема (см. статью «Регулярные выражения Perl» www.xakep.ru/post/19474). Кстати, на сегодня доступны специальные утилиты, помогающие составлять регулярное выражение под требуемую оболочку. Например, RegexBuddy (regexbuddy.com/powershell.html) или RegexMagic (regexmagic.com). Кроме поиска совпадения, в PS реализована еще одна ценная возможность — замена содержимого по шаблону, для чего используется оператор «-replace» (а также «-ireplace» и «-creplace»). Шаблон для замены выглядит так:
-replace "шаблон_текста","шаблон_замены"
Например, прочитаем файл и заменим все строки "Warning", на "!!!Warning":
PS> Get-Content -path system.log | foreach {$_ -replace "Warning", "!!!Warning"}
Если второй параметр не указан, совпавшая запись будет просто удалена. Как и в Perl, захваченные в первой части выражения символы сохраняются в специальных переменных, которые могут быть использованы при замене. Так $0 соответствует всему совпавшему тексту, $1 — первое совпадение, $2 — второе и так далее. То есть предыдущее выражение можно переписать так:
PS> Get-Content -path system.log | foreach {$_ -replace "(Warning)", "!!!$0"}
Цигиль-цигиль
Теперь, собственно, как и при помощи чего проверять эффективность написанного скрипта. Разработчики Microsoft не стали усложнять нам жизнь, и в PS из коробки включен специальный командлет Measure-Command, позволяющий замерить время выполнения команды или скрипта.
Из командной строки PS реализован прямой доступ к командам оболочки CMD, объектам COM, WMI и .NET, поэтому очень удобно определять разницу во времени исполнения самых разных утилит. Просто вводим запрос в строке приглашения. Для примера произведем два замера:
PS> Measure-Command {ServerManagerCmd -query}
TotalMilliseconds: 7912,7428
PS> Measure-Command {Get-WindowsFeature}
TotalMilliseconds: 1248,9875
Отсюда видно, что нэйтивные команды в PS выполняются значительно быстрее.
Заключение
PowerShell сделан очень удобным и функциональным, на нем просто писать скрипты, выбирать и форматировать данные. Но за удобство приходится расплачиваться медлительностью написанных скриптов. Надеюсь, приведенные советы по оптимизации помогут тебе в изучении этой оболочки.
Links
- Официальные ресурсы, посвященные PowerShell — microsoft.com/powershell, blogs.msdn.com/PowerShell
- Специализированные ресурсы — powershellcommunity.org, pwrshell.net, powershelltools.com, powershell.wik.is.
- Пакет Unix утилит для Windows — gnuwin32.sf.net
- Статья «Регулярные выражения Perl». — www.xakep.ru/post/19474
Форматируем вывод
При большом количестве данных их визуальный анализ становится затруднительным: например, очень трудно найти сообщения об ошибке во множестве записей. Но в PS довольно просто выделить вывод цветом, что удобнее для восприятия. Для этого используется командлет Write-Host, которому в качестве параметра передаем два параметра:
цвет фона (-Backgroundcolor) и цвет текста (-Foregroundcolor).
PS> Get-Process | Write-Host -foregroundcolor DarkGreen -backgroundcolor white
В результате получим список процессов на белом фоне зелеными буквами. К сожалению, Write-Host никак не различает вывод других утилит. То есть если в выводе другого командлета присутствует, например Error, о цветовой раскраске такого сообщения необходимо позаботиться самостоятельно.
Работа с журналами сообщений
После настройки систем и сервисов роль админа сводится к наблюдению за их правильной работой и отслеживанию текущих параметров. В PS заложен целый ряд *-Eventlog командлетов, позволяющих легко считать записи в журнале событий как на локальной, так и удаленной системе. Данные легко сортируются и отбираются по нужным критериям. Например, чтобы вывести только последние события из журнала безопасности на двух компьютерах, используем параметр "Nevest" с указанием числового аргумента:
PS> Get-Eventlog Security -Nevest 20 -computername localhost, synack.ru
Теперь выведем только события, имеющие определенный статус:
PS> Get-Eventlog Security -Message "*failed*"
А вот так можно собрать все данные об успешной регистрации пользователей (события с EventID=4624):
PS> Get-Eventlog Security | Where-Object {$_.EventID -eq 4624}
Логокопатель Windows
В PS v2.0 CTP3 появился командлет Get-WinEvent, который в некоторых случаях предоставляет более удобный формат доступа к данным. Получим список провайдеров, отвечающих за обновления:
PS> Get-WinEvent -ListProvider *update*
Microsoft-Windows-WindowsUpdateClient {System, Microsoft-Windows-WindowsUpdateClient/Operational}
В зависимости от установленных ролей и компонентов, список будет разным, но нас интересует провайдер для Windows Update. Теперь смотрим установленные обновления:
PS> $provider = Get-WinEvent -ListProvider
Microsoft-Windows-WindowsUpdateClient
PS> $provider.events | ? {$_.description -match "success"} | select id,description | ft -AutoSize
В итоге мы можем достаточно просто получить любую информацию о состоянии системы.