• Партнер

  • 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

     

    Форматируем вывод

    При большом количестве данных их визуальный анализ становится затруднительным: например, очень трудно найти сообщения об ошибке во множестве записей. Но в 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

    В итоге мы можем достаточно просто получить любую информацию о состоянии системы.

    Подписаться
    Уведомить о
    0 комментариев
    Межтекстовые Отзывы
    Посмотреть все комментарии