tags:

views:

166

answers:

7

So I have a script that will go through and ping all the servers from a list that is stored in SQL Server. The script works fine but it does it all sequentially (lame).

Can someone help me out as to how I would change this to use multithreading instead of a foreach loop?

    $Server = "ServerName"
$Database = "DatabaseName"

$con = "server=$Server;database=$Database;Integrated Security=sspi"
$cmd = "SELECT ServerName FROM dbo.vwServerListActive"

  $da = new-object System.Data.SqlClient.SqlDataAdapter ($cmd, $con)

  $dt = new-object System.Data.DataTable

  $da.fill($dt) | out-null


  foreach ($srv in $dt)
    {

    $ping = new-object System.Net.NetworkInformation.Ping
    $Reply = $ping.send($srv.ServerName)

    $ServerName = $srv.ServerName 

    $ServerName
    $Reply.status

    if ($Reply.status –eq “Success”)
    {
        $sql = "UPDATE dbo.ServerList SET GoodPing = 1 WHERE GoodPing <> 1 AND ServerName = '$ServerName'"

    }
    else
    {
        $sql = "UPDATE dbo.ServerList SET GoodPing = 0 WHERE GoodPing <> 0 AND ServerName = '$ServerName'"
    }

    $Reply = ""

    invoke-sqlcmd -serverinstance $Server -database $Database -query $sql


    }
A: 

Here's a page with a script which might be useful for you. I haven't used it myself yet, so I can't comment on it beyond that.

Tom H.
Hey TomThanks for the link... funny thing is that is what I've been working through and trying to understand... where I'm getting stuck is how to: * dynamically call the Invoke-ExpressionInRunspace based on the server list that is getting pulled from my SQL DB * Pass in the server name variable to the call * BONUS: Limit the number of threads (ie. 10 at a time?)I'm thinking I'm going to have to create a $variable and loop through/append to it to create my Invoke-ExpressionInRunspace commandNo idea how this is gonna fly LOL
ColinStasiuk
A: 

Powershell doesn't really do multithreading at all. I've managed to crowbar it into place by faking it with a fire-and-forget script kicked off with "start [powershell path] scriptname.ps1". It'll fire off multiple insances, but you can't get data back from them without doing an end-run by way of a database or other message passing mechanism. Tracking when the child processes terminate is tricky as well.

cmd /c "start /min /low C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe -command .\evtlogparse.ps1  "

In your case as you're setting a SQL string as part of the foreach loop, you can try to put the DB update code into a second script that you fire off. You'll potentially have many different processes attempting to update the same database table, so the potential for timing issues is pretty large.

Also, since you're kicking off umpty new powershell instances you're going to eat a lot of memory to make it work. The foreach loop may just be faster than kicking off a bunch of processes.

So, it can be done, but it isn't anything even resembling pretty.

sysadmin1138
so what you're suggesting as a possible solution is within my foreach loop to open up multiple powershell.exe instances?Yeah I can see where that wouldn't be pretty... especially if I have 1000+ servers hehehe.Not sure how I would be able to assign the -command parameter with it needing to have the $ServerName included in the call.
ColinStasiuk
Yeah, this doesn't scale well. Good if you need to simultaneously retrieve and crunch several large XML files. Not so good if you're just pinging a server. And yes, you have to pass variables into the child scripts as command-line options.
sysadmin1138
A: 

First, I suggest to only create once the variable $ping outside of the 'foreach..'.
Maybe a simpler solution... now that you're using SQL 2008, why not using the SMO method 'enumAvailableSqlServers: "...SMOApplication]::EnumAvailableSqlServers($false)". This will give you a list of all available server on the network. Here's the Microsoft MSDN link so you can read about it: http://msdn.microsoft.com/en-us/library/ms210350.aspx

Max Trinidad
Hi MaxGood Catch on the $ping within the loop... thanks! :)I'm not using enumAvailableSQLServers because I am not just looking to do this for SQL Servers. I have a different script that populates/updates my vwServerListActive table/view with all servers in my domain.thanks
ColinStasiuk
+4  A: 

(Edited as per Chad Miller's Suggestion + Throttling Requirement + Wait-Job fix + STA fix)

Support.ps1

powershell -File "Main.ps1" -Sta

Main.ps1

$Server = "ServerName"   
$Database = "DatabaseName"   

$con = "server=$Server;database=$Database;Integrated Security=sspi"   
$cmd = "SELECT ServerName FROM dbo.vwServerListActive"   

$da = New-Object System.Data.SqlClient.SqlDataAdapter -ArgumentList $cmd, $con  

$dt = New-Object System.Data.DataTable   

$da.Fill($dt) | Out-Null  

$ThrottleLimit = 10 
$activeJobs = New-Object 'System.Collections.Generic.List[Int32]' 

$JobStateChanged = { 
    param ( 
        [System.Object]$Sender, 
        [System.Management.Automation.JobStateEventArgs]$EventArgs 
    ) 

    switch ($EventArgs.JobStateInfo.State) 
    { 
        Blocked { return } 
        Completed { $activeJobs.Remove($Sender.Id); break } 
        Failed { $activeJobs.Remove($Sender.Id); break } 
        NotStarted { return } 
        Running { return } 
        Stopped { $activeJobs.Remove($Sender.Id); break } 
    }

    Unregister-Event -SourceIdentifier ("{0}.StateChanged" -f $Sender.Name)
} 

foreach ($srv in $dt)   
{ 
    while ($true) 
    { 
        if ($activeJobs.Count -lt $ThrottleLimit) 
        { 
            $job = Start-Job -InitializationScript {   
                Add-PSSnapin -Name SqlServerCmdletSnapin100   
            } -ScriptBlock {  
                param (  
                    [String]$Server,  
                    [String]$Database,  
                    [String]$ServerName  
                )  

                if (Test-Connection -ComputerName $ServerName -Quiet)   
                {   
                    $sql = "UPDATE dbo.ServerList SET GoodPing = 1 WHERE GoodPing <> 1 AND ServerName = '$ServerName'"  
                }   
                else   
                {   
                    $sql = "UPDATE dbo.ServerList SET GoodPing = 0 WHERE GoodPing <> 0 AND ServerName = '$ServerName'"  
                }  

                Invoke-SqlCmd -ServerInstance $Server -Database $Database -Query $sql  
            } -ArgumentList $Server, $Database, $srv.ServerName  

            $activeJobs.Add($job.Id) 

            Register-ObjectEvent -InputObject $job -EventName StateChanged -SourceIdentifier ("{0}.StateChanged" -f $job.Name) -Action $JobStateChanged 

            break 
        } 
    } 
} 

Get-Job | Where-Object { $_.State -eq "Running" } | Wait-Job
Get-Job | Remove-Job
George Howarth
Hi GeorgeSorry for the delay in responses but I did get this to work with adding a wait-job *to the end of the script. When I didn't have that when the script finished it would error out of my cmd prompt "Powershell has stpped working" and my data would not get updated in the database.The problem with using wait-job * is that if I have 100 server... if I'm waiting for all jobs to finish with each job spawning it's own Powershell process I pin my box.I assume there is no throttling for this? I think this is where Chad's suggestion of using groups probably comes into play?
ColinStasiuk
@Colin: Chad's script actually does the same as mine except you read from a file and execute it as a ScriptBlock instead. I have updated my answer with the Wait-Job fix (sorry about that) and some code to do some throttling. Hope that helps.
George Howarth
sorry... the latest edition looks to throttle fine... but after the first 10 get process/finished it doesn't move on to the next "set". Gonna look into it further but this solution (when it gets working) will eliminate the need for me to group my servers manually.
ColinStasiuk
@Colin: hmmmm yes, it works in ISE, but if I Open With > PowerShell it hangs after the first batch. I'll look into it.
George Howarth
OK, so I did a bit of research and found that it is something to do with the apartment state being different in ISE than at the default console. What you have to do now is create a file and name it, lets say, `Support.ps1`. In that file, call the main script which does the work (`Main.ps1`) and specify the `-Sta` parameter. That seems to work for me both in ISE and launching it in/from the console.
George Howarth
Fantastic... it's working now! :)Thanks so much to everyone for their suggestions... I'm gonna see what I can do to encorporate some of the other ideas for efficiencies.:D
ColinStasiuk
@Colin -- Nice use of eventing to achieve throttling.
Chad Miller
A: 

so close.... this is what I've got

add-pssnapin SqlServerCmdletSnapin100

$Server = "ServerName"
$Database = "DatabaseName"

$con = "server=$Server;database=$Database;Integrated Security=sspi"  
$cmd = "SELECT ServerName FROM dbo.vwServerListActive"  

$da = New-Object System.Data.SqlClient.SqlDataAdapter -ArgumentList $cmd, $con 

$dt = New-Object System.Data.DataTable  

$da.Fill($dt) | Out-Null 


foreach ($srv in $dt)  
{     
    Start-Job -ScriptBlock { 
        param ( 
            [String]$Server, 
            [String]$Database, 
            [String]$ServerName 
        ) 


    if (Test-Connection -ComputerName $ServerName -quiet)  
            {  
                $sql = "UPDATE dbo.ServerList SET GoodPing = 1 WHERE GoodPing <> 1 AND ServerName = '$ServerName'" 
            }  
            else  
            {  
                $sql = "UPDATE dbo.ServerList SET GoodPing = 0 WHERE GoodPing <> 0 AND ServerName = '$ServerName'" 
            } 

           Invoke-SqlCmd -ServerInstance $Server -Database $Database -Query $sql 
    } -ArgumentList $Server, $Database, $srv.ServerName 
} 

and it looks to be starting multiple jobs... but my table never gets updated. If I remove the "Start-Job" stuff and arguement list and use $srv.ServerName then it works sequentially as it did before. Any ideas? (Thanks so much BTW for all the responses)

ColinStasiuk
Yea, sorry I don't have anything to test this with. If you replace `Invoke-SqlCmd -ServerInstance $Server -Database $Database -Query $sql` with `"Calling Invoke-SqlCmd...{0}ServerInstance: {1}{2}Database: {3}{4}Query: {5}" -f [Environment]::NewLine, $Server, [Environment]::NewLine, $Database, [Environment]::NewLine, $sql | Out-File -FilePath "C:\SqlCommands.txt" -Append` you can see if the correct variables are being passed in and narrow the problem down to the `Invoke-SqlCmd` call.
George Howarth
Invoke-SqlCmd is part of SqlServerCmdletSnapin100 snapin. You'll need to add -InitializeScript param to Start-Job call. IMO it would be faster/easier to not load the snapin and just use your own ADO.NET code:$con = "server=$Server;database=$Database;Integrated Security=sspi"$cmd = "SELECT ServerName FROM dbo.vwServerListActive" $cmd.ExecuteNonQuery()
Chad Miller
@Chad Ah yes, that is the probable reason why this isn't working. Colin, you can try adding the parameter `-InitializationScript { Add-PSSnapin SqlServerCmdletSnapin100 }` before the `-ScriptBlock` parameter. I edited my original post.
George Howarth
+1  A: 

If have PowerShell 2.0 you could make use of background jobs. You'll need to break up your server list into "groups". Given a source table with serverName and groupName:

CREATE TABLE [dbo].[vwServerListActive](
    [serverName] [varchar](50) NULL,
    [groupName] [char](1) NULL
) 

A slight modification to your script (save as forum.ps1):

param($groupName)

$Server = "$env:computername\sql2k8"
$Database = "dbautility" 

$con = "server=$Server;database=$Database;Integrated Security=sspi" 
$cmd = "SELECT ServerName FROM dbo.vwServerListActive WHERE groupName ='$groupName'" 

  $da = new-object System.Data.SqlClient.SqlDataAdapter ($cmd, $con) 

  $dt = new-object System.Data.DataTable 

  $da.fill($dt) | out-null 


  foreach ($srv in $dt) 
    { 

    $ping = new-object System.Net.NetworkInformation.Ping 
    $Reply = $ping.send($srv.ServerName) 

    new-object PSObject -Property @{ServerName=$($srv.ServerName); Reply=$($Reply.status)} 

    } 

You can then call the script for different groups:

#groupName A
start-job -FilePath .\forum.ps1 -Name "Test" -ArgumentList "A"
#groupName B
start-job -FilePath .\forum.ps1 -Name "Test" -ArgumentList "B"

Get-Job -name "test" | wait-job | out-null
Get-Job -name "test" | receive-job

#get-job -name "test" |remove-job

If you're using PowerShell V1 or sqlps you could use System.Diagnostics.ProcessStartInfo to start separate powershell.exe processes and pass the group name.

param($groupName)

    $StartInfo = new-object System.Diagnostics.ProcessStartInfo
    $StartInfo.FileName = "$pshome\powershell.exe"
    $StartInfo.Arguments = " -NoProfile -Command C:\scripts\forum.ps1 $groupName"
    $StartInfo.WorkingDirectory = "C:\scripts"
    $StartInfo.LoadUserProfile = $true
    $StartInfo.UseShellExecute = $true
    [System.Diagnostics.Process]::Start($StartInfo) > $null
Chad Miller
so what you're suggesting is to keep my processing pretty well as-is and start x number of powershell sessions (splitting my server list up evenly across the groupings)?Having a separate job for each run is looking like it's adding alot of overhead so this might be a better solutiongonna play and I'll letcha know! :) thanks
ColinStasiuk
That's right using start-job or alternatively starting a PowerShell process incurrs overhead to setup the runspace or start the process. Because of this you usually want to limit the number of processes or background jobs. The types of processes I will background can run indepentently of parent process and take more than a few minutes to run--otherwise its not worth the overhead. I would also suggest not loading the SQL cmdlet just for invoke-sqlcmd--this adds to the overhead--instead use straight ADO.NET call as I mentioned in previous post.
Chad Miller
Also rather than the grouping approach I've used a couple of queue approach scripts where you configure the number of jobs or processes to execute in parallel--no need to manually group. Note: None of what we are doing is true threading--rather concurent processing. Download my PowerShell ETL presentation to get the scripts: http://sev17.com/2010/06/powershell-etl-presentation/
Chad Miller
Why I've never used StackOverflow before I'll never know... excellent advice and answers! Thanks Chad
ColinStasiuk
A: 

Here's a script from Jim Truher for background jobs in PowerShell v1.0:

http://jtruher.spaces.live.com/blog/cns!7143DA6E51A2628D!130.entry

PowerShell v2.0 has background jobs built-in:

http://technet.microsoft.com/en-us/library/dd347692.aspx

-Oisin

x0n