Наверняка кому-то из читателей приходилось иметь дело с операциями, которые необходимо выполнять на нескольких компьютерах, и при этом результаты нужно получить как можно быстрее. Один из способов решения этой задачи - разделить входной список имен узлов на несколько списков, меньших по размеру, и независимо обрабатывать их во множестве окон командной оболочки. Однако при разделении входного списка можно случайно пропустить или, наоборот, указать несколько раз имя какого-либо узла. Еще одна проблема данного метода заключается в том, что необходимо объединять все выходные файлы для получения итогового результата. Кроме того, придется следить за тем, чтобы все независимые процессы были завершены до объединения результатов, и, возможно, вам потребуется, чтобы результаты выдавались в том же порядке, в котором узлы были указаны во входном файле. При объединении результатов вручную (вырезать–вставить) вы снова рискуете пропустить, продублировать или неправильно расположить записи. Так как множество процессов используют отдельные входные и выходные файлы, необходимо создавать независимые копии сценария или кода сценария, для того чтобы учитывать параметры путей к входному и выходному файлам.

Разве не удобно иметь возможность запускать задачу со списком из нескольких сотен или даже нескольких тысяч узлов, автоматически разбивать входной список и обрабатывать его посредством множества процессов, и в конце автоматически объединять результаты в один файл? А что вы скажете о возможности опрашивать список из 2000 узлов не за 20 и более, а всего за 5 минут? И если вам это кажется фантастикой, держитесь крепче за свою клавиатуру, потому что я создал сценарий ListSplitter.bat, который делает эти мечты реальностью. Я собираюсь использовать сценарий ListSplitter для выполнения относительно простых запросов, чтобы продемонстрировать принцип работы сценария, но вы можете применять его для выполнения других типов запросов.

Принцип работы сценария ListSplitter

Логика сценария строится на следующей базовой схеме:

  1. Сценарий автоматически разбивает список на отдельные части и позволяет пользователю настроить количество записей во вторичных (или разделенных) списках.
  2. Сценарий начинает параллельное выполнение операций сразу после завершения формирования каждого разделенного списка.
  3. Сценарий ведет аудит всех выполняемых процессов.
  4. Когда все списки будут сформированы и для всех узлов каждого списка будут выполнены все операции, сценарий собирает все независимые журналы процессов в один основной отчет, который отображает все узлы в их первоначальном порядке.

Первая часть сценария ListSplitter, показанная в Листинге 1, выполняет различные действия по настройке, обнуляет и устанавливает счетчики, определяет путь к папке, в которой хранится сценарий, и анализирует входной файл, перед тем как разбить его на вторичные файлы. После выполнения предварительных задач код в блоке D копирует определенное количество строк из входного файла в новый вторичный файл с уникальным именем. Мы используем выражение Set BrkOutNum= в разделе настроек сценария (в начале Листинга 1) для настройки числа копируемых строк (в нашем примере копируется 200 строк). Переменная %L1counter% отслеживает, какая часть сценария выполняет обработку. Эта переменная также формирует часть имени вторичного файла.

После того как сценарий ListSplitter копирует определенное число строк, он запускает сценарий RunOps, приведенный в Листинге 2. RunOps вызывает сервисную функцию, например Ping, для опроса узлов из первого разделенного списка. В то время как сценарий RunOps опрашивает узлы, управление возвращается к сценарию ListSplitter, который создает новый файл и копирует в него следующий набор записей, до тех пор, пока не обработает все строки в исходном входном файле. Код в блоке C Листинга 1 содержит выражение If, которое изменяет имя вторичного файла после того, как счетчик достигнет указанного количества записей и запустит сценарий RunOps для вновь созданного вторичного файла.

Раздел с меткой RunOps :Task (в блоке B Листинга 2) содержит код, непосредственно выполняющий операции опроса (то есть Ping). Вы можете легко изменить этот код для виртуального выполнения любого запроса (например, для проверки параметров реестра, дат файла сигнатур вирусов, создания файлов или членства в локальной группе) на основе списка узлов. Единственное требование - вы должны записывать результаты в файл журнала, имеющий имя, соответствующее определенному вторичному файлу, с которым работает сценарий RunOps. Для этого можно использовать код, подобный приведенному ниже:

Echo %target% OFF>>%Logfile%

После того как сценарий RunOps закончит опрос узлов, но до того как сценарий ListSplitter сможет объединять файлы, нам потребуется проверить, все ли экземпляры сценария RunOps закончили работу. Хотя эти процессы запускают последовательно, мы не знаем, когда и в каком порядке они завершат работу. Код блока A в Листинге 2 использует команду Move для изменения имени каждого вторичного файла, после того как успешно завершится операция, работающая с ним. Измененное имя файла говорит о том, что процесс сценария RunOps, связанный с этим файлом, успешно завершил работу.

Сценарий ListSplitter использует код в блоке A из Листинга 1, чтобы определить, какие входные файлы выполнены (то есть переименованы). Сценарий повторяет эту проверку каждые 10 секунд до тех пор, пока все файлы не будут переименованы. При необходимости можно изменить код, что позволит увеличить или уменьшить интервал между проверками. Далее, код блока B использует достаточно простую, но важную технологию для связывания между собой файлов с результатами. Первые шесть строк кода используют команду Dir для определения местоположения файлов с журналами, и оператор присваивания + - для создания строки, которая содержит имена соединяемых файлов с журналами. Имена файлов выстраиваются в том же порядке, в каком они возникали при разделении, чтобы гарантировать, что порядок вывода результатов соответствует порядку задания узлов в исходном входном файле. Команда Copy будет использовать строку для копирования содержимого всех файлов с журналами в один файл. Эта строка должна создаваться динамически, так как число файлов с журналами будет полностью зависеть от числа строк во вторичных файлах и общего числа строк во входном файле.

Код в блоке B корректен, так как в начале сценария присутствует команда Setlocal EnableDelayedExpansion. По умолчанию, когда процессор команд сталкивается с переменной окружения в теле команды, он немедленно расширяет переменную, прежде чем выполнять команду. Аргумент EnableDelayedExpansion задерживает расширение любой переменной окружения до тех пор, пока не будут достигнуты соответствующая команда Конецlocal или конец сценария.

Использование сценариев ListSplitter и RunOps

Я тестировал сценарии ListSplitter и RunOps на сервере с системой Windows 2000 Server Service Pack 3 (SP3) и на рабочей станции с системами Windows XP SP1 и Windows 2000 SP3. Для запуска сценариев выполните следующие шаги:

  1. Создайте папку для двух сценариев. Сценарии автоматически создают и размещают свои временные файлы в этой папке.
  2. Просмотрите комментарии к коду в обоих сценариях. Комментарии содержат дополнительную информацию по принципу работы сценариев.
  3. Настройте местоположение входного файла, изменив следующую строку в разделе настроек сценария ListSplitter:
    Set InputFile=server4share
    MyInputFileName.txt
  4. Установите местоположение выходного файла в строке:
    Set OutFile=server4share
    MyOutputFileName.txt
  5. Укажите местоположение сервисной службы Sleep, изменив строку:
    Set SleepLoc=server3
    esourcekitsleep.exe
  6. Укажите количество строк, которое будет в каждом вторичном файле, изменив строку:
    Set BrkOutNum=number
  7. Чтобы определить значение, необходимо оценить общее число элементов в главном входном списке. Если установить слишком маленькое количество строк, это может привести к появлению огромного количества командных сессий и чрезмерному расходованию ресурсов компьютера. Необходимо подобрать значение, отвечающее вашей ситуации, на основе факторов, таких как количество ресурсов сервера или рабочей станции и пропускная способность сети. В качестве основного ориентира используйте число, в десять раз меньшее количества записей во входном файле.
  8. Если вам нужен заголовок в начале окончательного отчета, создайте файл с именем headerfile.txt, который будет содержать заголовок отчета. Вставьте символ возврата каретки в конец заголовка, чтобы отделить данные отчета от данных заголовка. Поместите файл в одну папку со сценариями ListSplitter и RunOps.

Сценарий ListSplitter имеет один недостаток: он наиболее эффективен для крупных задач, например, для входных списков, содержащих сотни и тысячи записей. Так как обработка сценария сопряжена с определенными издержками, сценарий ListSplitter, вероятно, не даст выигрыш во времени при работе с небольшим количеством узлов.

Время разделять и властвовать

Сценарий ListSplitter может ускорить крупномасштабные операции запросов, поскольку избавляет вас от необходимости вручную вырезать и вставлять входные и выходные данные. Используйте его для обнаружения новых вирусов, лазеек в системе безопасности или других проблем, которые требуют быстрого сбора информации о вашей рабочей станции и серверном окружении.


ЛИСТИНГ 1: ListSplitter.bat
:: Чтобы запретить вывод в консоль, снимите комментарий с данной строки:
:: @ECHO OFF
Setlocal EnableDelayedExpansion
Title List Splitter Script
Color 4F
:: Настройка местоположения входного файла.
Set InputFile=Server4shareMyInputFileName.txt
:: Настройка местоположения окончательного выходного файла.
Set OutFile=Server4shareMyOutputFileName.txt
:: Настройка местоположения сервисной функции «SLEEP».
Set SleepLoc=Server3ResourceKitSleep.exe
:: Настройка количества строк
:: в каждом вторичном файле
Set BrkOutNum=200
:: Далее настройки не требуются.
:: Обнуление и установка счетчиков.
Set L1counter=1
Set L2counter=0
Set Filecntr=0
:: Указание пути к каталогу, в котором хранится сценарий.
Call :FindLoc %0
Goto :Начало
:FindLoc
Set ScriptLoc=%~dp1
Goto :EOF
:Начало
For /f "tokens=* usebackq" %%i in ("%InputFile%")
 Do (Set Entry=%%i) & (Call :Split)
If Exist "%ScriptLoc%testlist%L1counter%.txt"
 (Start /I Cmd /C "%ScriptLoc%RunOps.bat" %L1counter%)
:: НАЧАЛО БЛОКА A
:Top
Set Filecntr=0
:: Проверка наличия всех завершенных файлов
:: перед созданием окончательной объединенной копии.
For /F "tokens=*" %%i in
 ('Dir /B /A-D "%ScriptLoc%testlistDone*.txt"')
 Do Set /A Filecntr+=1
:: Если все файлы на месте - переход к коду, который их объединяет.
:: Если нет - задержка 10 секунд и повторная проверка.
 Echo Operations completed - Will combine log files.
 & Goto :TheКонец
"%SleepLoc%" 10
Goto :Top
:: КОНЕЦ БЛОКА A
:TheКонец
Set Vcntr1=0
:: НАЧАЛО БЛОКА B
:: Создание строки, которую будет использовать команда «Copy» для копирования содержания всех журналов 
:: в один файл.
For /F "tokens=*" %%i in ('Dir /B /A-D "%ScriptLoc%Log*.txt"') Do (
 If !Vcntr1! EQU 0 Set string="%ScriptLoc%%%i"
 If !Vcntr1! GTR 0 Set string=+"%ScriptLoc%%%i"
 Set /A Vcntr1+=1
 Set tstring=!tstring!!string!
)
If Exist "%ScriptLoc%HeaderFile.txt"
 Copy "%ScriptLoc%HeaderFile.txt"+!tstring! "%ScriptLoc%Temp.txt"
If Not Exist "%ScriptLoc%HeaderFile.txt"
 Copy !tstring! "%ScriptLoc%Temp.txt"
:: КОНЕЦ БЛОКА B
:: Очистка всех непечатаемых символов, оставшихся от команды «Copy».
:: Также удаляют пустые строки. При необходимости, вы можете закомментировать
:: данную строку и снять комментарий со следующей за ней строки.
Findstr /I "[a-z0-9]" "%ScriptLoc%Temp.txt">"%OutFile%"
:: Переименование "%ScriptLoc%Temp.txt" - "%OutFile%"
:: Удаление временных файлов.
Del "%ScriptLoc%testlistDone*.txt"
Del "%ScriptLoc%Log*.txt"
If Exist "%ScriptLoc%Temp.txt" Del "%ScriptLoc%Temp.txt"
Goto :EOF
:Split
:: Увеличение значения счетчика на 1.
Set /A L2counter+=1
:: Когда счетчик достигнет указанного значения числа строк во вторичном файле,
:: для файла запускается сценарий и создается новый файл.
:: НАЧАЛО БЛОКА C
If %L2counter% GTR %BrkOutNum% (Set L2counter=1) &
 (Start /I Cmd /C "%ScriptLoc%RunOps.bat" %L1counter%) &
 (Set /A L1counter+=1)
:: КОНЕЦ БЛОКА C
:: НАЧАЛО БЛОКА D
Echo %Entry%>>"%ScriptLoc%testlist%L1counter%.txt"
:: КОНЕЦ БЛОКА D
Goto :EOF

 

ЛИСТИНГ 2. Сценарий RunOps.bat
:: @ECHO OFF
:: Чтобы запретить вывод в консоль, снимите комментарий с данной строки:
:: @ECHO OFF line above.
Title Parallel Operation #%1 in Progress
Color 1F
SETLOCAL
:: *********************************************************************
:: Примечание: В данном файле не требуется настройка. Он помещается в один каталог со сценарием ListSplitter.bat.
:: *********************************************************************
:: Перехват параметра из сценария «ListSplitter.bat».
Set Param=%1
:: Этот код задает путь к папке, в которой хранится сценарий.
Call :FindLoc %0
Goto :Начало
:FindLoc
Set ScriptLoc=%~dp1
Goto :EOF
:Начало
SET InList=%ScriptLoc%testlist%Param%.txt
If %Param% LSS 10 SET Logfile=%ScriptLoc%Log0%Param%.txt &
 GOTO :Skip
SET Logfile=%ScriptLoc%Log%Param%.txt
:Skip
:: Приведенная ниже задача выполняется для каждой строки во вторичном файле.
For /F "tokens=1 usebackq" %%i in ("%InList%") do
 (Set target=%%i) & (Call :Task)
НАЧАЛО БЛОКА A
:: Команда «Move» переименовывает файл, сигнализируя, что
:: для данного списка задача была выполнена.
Move "%ScriptLoc%testlist%Param%.txt"
 "%ScriptLoc%testlistDone%Param%.txt"
КОНЕЦ БЛОКА A
ECHO Run complete
КОНЕЦLOCAL
GOTO :EOF
НАЧАЛО БЛОКА B
:Task
:: В этом разделе вы можете указать задачу, которую требуется выполнить.
:: Простая задача опроса указана в качестве примера.
FOR /F "tokens=1" %%i in ('ping -n 1 -w 2 %target% ^| find "Reply"')
 DO (ECHO %target%      ON>>"%Logfile%") & (Goto :Last)
Echo %target%      OFF>>"%Logfile%"
:Last
GOTO :EOF
КОНЕЦ БЛОКА B