Какая часть этого фрагмента кода Powershell заставляет работать долго? - PullRequest
1 голос
/ 09 марта 2020

Мне поручено составить отчет о времени последнего входа в систему для каждого пользователя в нашей среде AD. Я, очевидно, сначала попросил мать Google что-то, что я мог бы изменить, но не смог найти ничего, что проверило бы несколько контроллеров домена и согласовало бы последний, а затем выплюнуть, если он прошел произвольно установленную дату / количество дней.

Вот код:

foreach ($user in $usernames) {
    $percentCmpUser = [math]::Truncate(($usernames.IndexOf($user)/$usernames.count)*100)
    Write-Progress -Id 3 -Activity "Finding Inactive Accounts" -Status "$($percentCmpUser)% Complete:" -PercentComplete $percentCmpUser
    $allLogons = $AllUsers | Where-Object {$_.SamAccountName -match $user}
    $finalLogon = $allLogons| Sort-Object LastLogon -Descending |Select-Object -First 1
    if ($finalLogon.LastLogon -lt $time.ToFileTime()) {
        $inactiveAccounts += $finalLogon
    } 
}

$usernames - это список из примерно 6000 имен пользователей

$AllUsers - это список из 18000 пользователей, он включает 10 различных свойств, к которым я хотел бы получить доступ в своем окончательном отчете. Я получил это, нажав три из наших 20 или около того D C для всех пользователей в определенных c OU, которые меня интересуют. Окончательный сценарий будет на самом деле 6k * 20 будет c Мне нужно нажимать каждый D C, чтобы убедиться, что я не пропустил вход любого пользователя.

Вот как рассчитывается $time:

$DaysInactive = 60
$todayDate = Get-Date
$time = ($todayDate).Adddays(-($DaysInactive))

Каждая переменная используется в другом месте скрипта, поэтому я разбил ее так.

Прежде чем вы предложите LastLogonTimestamp, мне сказали, что она недостаточно актуальна, и когда я На вопрос об изменении времени репликации, чтобы оно было более актуальным, мне ответили: «Нет, этого не произойдет».

Search-ADAccount также не дает точного представления о неактивных пользователях.

Я открыт ко всем предложениям о том, как заставить этот конкретный c фрагмент работать быстрее или как использовать другую методологию для достижения того же результата в короткие сроки.

На данный момент попадание в каждый D C для всех пользователей в указанных c подразделениях занимает около 10-20 с c на D C, а затем приведенный фрагмент занимает 30-40 мин.

1 Ответ

5 голосов
/ 09 марта 2020

Несколько вещей выделяются, но, вероятно, самый большой убийца производительности здесь - это два утверждения:

$percentCmpUser = [math]::Truncate(($usernames.IndexOf($user)/$usernames.count)*100)
# and
$allLogons = $AllUsers | Where-Object {$_.SamAccountName -match $user}

... оба эти утверждения будут иметь O (N ^ 2) (или quadrati c) эксплуатационные характеристики - то есть каждый раз, когда вы удваиваете размер ввода, время, затрачиваемое в четыре раза!


  1. Array.IndexOf() фактически является oop

Давайте посмотрим на первый:

$percentCmpUser = [math]::Truncate(($usernames.IndexOf($user)/$usernames.count)*100)

Возможно, не быть самоочевидным, но этот вызов метода: $usernames.IndexOf() может потребовать итерации по всему списку $usernames каждый раз, когда он выполняется - к тому времени, когда вы достигнете последнего $user, ему нужно go до конца и сравнить $user всех 6000 наименований.

Два способа решения этой проблемы:

Используйте обычные for l oop:

for($i = 0; $i -lt $usernames.Count; $i++) {
    $user = $usernames[$i]
    $percent = ($i / $usernames.Count) * 100
    # ...
}

Остановить вывод прогресса вообще

Write-Progress очень медленно - даже если вызывающая сторона подавляет вывод Progress (например, $ProgressPreference = 'SilentlyContinue'), usi При этом поток выполнения по-прежнему несет издержки, особенно при вызове в каждой итерации l oop.

Удаление Write-Progress в целом приведет к удалению требования для расчета процента:)

Если вам все еще нужно выводя информацию о прогрессе, вы можете уменьшить некоторые накладные расходы, просто вызвав Write-Progress иногда - например, один раз каждые 100 итераций:

for($i = 0; $i -lt $usernames.Count; $i++) {
    $user = $usernames[$i]
    if($i % 100 -eq 0){
        $percent = ($i / $usernames.Count) * 100
        Write-Progress -Id 3 -Activity "Finding Inactive Accounts" -PercentComplete $percent
    }
    # ...
}

... |Where-Object это также просто al oop

Теперь для второго:

$allLogons = $AllUsers | Where-Object {$_.SamAccountName -match $user}

. .. 6000 раз, powershell должен перечислить все 18000 объектов в $AllUsers и проверить их на наличие фильтра Where-Object.

Вместо использования массива и Where-Object рассмотрите возможность загрузки всех пользователей в хеш-таблицу:

# Only need to run this once, before the loop
$AllLogonsTable = @{}
$AllUsers |ForEach-Object {
    # Check if the hashtable already contains an item associated with the user name
    if(-not $AllLogonsTable.ContainsKey($_.SamAccountName)){
        # Before adding the first item, create an array we can add subsequent items to
        $AllLogonsTable[$_.SamAccountName] = @()
    }

    # Add the item to the array associated with the username
    $AllUsersTable[$_.SamAccountName] += $_
}

foreach($user in $users){
    # This will be _much faster_ than $AllUsers |Where-Object ...
    $allLogons = $AllLogonsTable[$user]
}

Хеш-таблицы имеют crazy-fast lookups - поиск объекта по ключу намного быстрее, чем использование Where-Object в массиве.

...