問題:大量のSQLクエリ(約10k〜20k)があり、それらを50(以上)のスレッドで非同期に実行したい。
このジョブ用にPowerShellスクリプトを記述しましたが、非常に低速です(すべての実行に約20時間かかりました)。 望ましい結果は最大3-4時間です
質問:このPowerShellスクリプトをどのように最適化できますか? python
やc#
などの別のテクノロジーを再考して使用する必要がありますか?
whoisactive
で確認するとクエリが高速に実行されているため、これはPowerShellの問題だと思います。各スレッドに対して個別のPSインスタンスが作成されるため、ジョブの作成、終了、アンロードには多くの時間がかかります。
マイコード:
$NumberOfParallerThreads = 50;
$Arr_AllQueries = @('Exec [mystoredproc] @param1=1, @param2=2',
'Exec [mystoredproc] @param1=11, @param2=22',
'Exec [mystoredproc] @param1=111, @param2=222')
#Creating the batches
$counter = [pscustomobject] @{ Value = 0 };
$Batches_AllQueries = $Arr_AllQueries | Group-Object -Property {
[math]::Floor($counter.Value++ / $NumberOfParallerThreads)
};
forEach ($item in $Batches_AllQueries) {
$tmpBatch = $item.Group;
$tmpBatch | % {
$ScriptBlock = {
# accept the loop variable across the job-context barrier
param($query)
# Execute a command
Try
{
Write-Host "[processing '$query']"
$objConnection = New-Object System.Data.SqlClient.SqlConnection;
$objConnection.ConnectionString = 'Data Source=...';
$ObjCmd = New-Object System.Data.SqlClient.SqlCommand;
$ObjCmd.CommandText = $query;
$ObjCmd.Connection = $objConnection;
$ObjCmd.CommandTimeout = 0;
$objAdapter = New-Object System.Data.SqlClient.SqlDataAdapter;
$objAdapter.SelectCommand = $ObjCmd;
$objDataTable = New-Object System.Data.DataTable;
$objAdapter.Fill($objDataTable) | Out-Null;
$objConnection.Close();
$objConnection = $null;
}
Catch
{
$ErrorMessage = $_.Exception.Message
$FailedItem = $_.Exception.ItemName
Write-Host "[Error processing: $($query)]" -BackgroundColor Red;
Write-Host $ErrorMessage
}
}
# pass the loop variable across the job-context barrier
Start-Job $ScriptBlock -ArgumentList $_ | Out-Null
}
# Wait for all to complete
While (Get-Job -State "Running") { Start-Sleep 2 }
# Display output from all jobs
Get-Job | Receive-Job | Out-Null
# Cleanup
Remove-Job *
}
[〜#〜]更新[〜#〜]:
Resources: DBサーバーはリモートマシン上にあります。
最大のCPUパワーを使用したいと考えています。
フレームワークの制限:唯一の制限はnottoですSQL Serverを使用してクエリを実行します。リクエストは、Powershell、C#、Pythonなどの外部ソースから送信されます。
RunspacePoolはここに行く方法です、これを試してください:
$AllQueries = @( ... )
$MaxThreads = 5
# Each thread keeps its own connection but shares the query queue
$ScriptBlock = {
Param($WorkQueue)
$objConnection = New-Object System.Data.SqlClient.SqlConnection
$objConnection.ConnectionString = 'Data Source=...'
$objCmd = New-Object System.Data.SqlClient.SqlCommand
$objCmd.Connection = $objConnection
$objCmd.CommandTimeout = 0
$query = ""
while ($WorkQueue.TryDequeue([ref]$query)) {
$objCmd.CommandText = $query
$objAdapter = New-Object System.Data.SqlClient.SqlDataAdapter $objCmd
$objDataTable = New-Object System.Data.DataTable
$objAdapter.Fill($objDataTable) | Out-Null
}
$objConnection.Close()
}
# create a pool
$pool = [RunspaceFactory]::CreateRunspacePool(1, $MaxThreads)
$pool.ApartmentState = 'STA'
$pool.Open()
# convert the query array into a concurrent queue
$workQueue = New-Object System.Collections.Concurrent.ConcurrentQueue[object]
$AllQueries | % { $workQueue.Enqueue($_) }
$threads = @()
# Create each powershell thread and add them to the pool
1..$MaxThreads | % {
$ps = [powershell]::Create()
$ps.RunspacePool = $pool
$ps.AddScript($ScriptBlock) | Out-Null
$ps.AddParameter('WorkQueue', $workQueue) | Out-Null
$threads += [pscustomobject]@{
Ps = $ps
Handle = $null
}
}
# Start all the threads
$threads | % { $_.Handle = $_.Ps.BeginInvoke() }
# Wait for all the threads to complete - errors will still set the IsCompleted flag
while ($threads | ? { !$_.Handle.IsCompleted }) {
Start-Sleep -Seconds 1
}
# Get any results and display an errors
$threads | % {
$_.Ps.EndInvoke($_.Handle) | Write-Output
if ($_.Ps.HadErrors) {
$_.Ps.Streams.Error.ReadAll() | Write-Error
}
}
Powershellジョブとは異なり、RunspacePoolsはリソースを共有できます。したがって、すべてのクエリの1つの同時キューがあり、各スレッドはデータベースへの独自の接続を維持します。
他の人が言ったように-データベースのストレステストを行わない限り、クエリを一括挿入に再編成する方が良いでしょう。
各ワーカースレッドでデータベース接続を開いたままにして、そのスレッドによって実行されるすべてのクエリに使用できるように、スクリプトを再編成する必要があります。現在、クエリごとに新しいデータベース接続を開いているため、大量のオーバーヘッドが発生します。そのオーバーヘッドを排除することで、目標までまたはそれを超える速度が得られます。
したがって、最初にデータセットを調べ、次に上記の2つの項目を実行して、すべてのクエリが並列かつ効率的に実行されているものを簡単に識別できるようにします。
これがいくつかのアイデアを与えることを願っています。 pythonスクリプトを使用することをお勧めします。これにより、複数のプロセスを簡単にトリガーし、そのアクティビティを監視することもできます。
悲しいことに、この瞬間にこれに完全に答える適切な時間はありませんが、これは役立つはずです:
最初に、CPU全体を使用して、ほとんど約束されたほど多くのレコードを挿入することはありません。だが!
表示されているので、SQL文字列コマンドを使用しています。
POCとしてのこのようなもの:
$query = "INSERT INTO [dbo].[Attributes] ([Name],[PetName]) VALUES "
for ($alot = 0; $alot -le 10; $alot++){
for ($i = 65; $i -le 85; $i++) {
$query += "('" + [char]$i + "', '" + [char]$i + "')";
if ($i -ne 85 -or $alot -ne 10) {$query += ",";}
}
}
バッチが構築されたら、既存のコードを効果的に使用して、挿入のためにバッチをSQLに渡します。
Buld挿入は次のようになります。
INSERT INTO [dbo].[Attributes] ([Name],[PetName]) VALUES ('A', 'A'),('B', 'B'),('C', 'C'),('D', 'D'),('E', 'E'),('F', 'F'),('G', 'G'),('H', 'H'),('I', 'I'),('J', 'J'),('K', 'K'),('L', 'L'),('M', 'M'),('N', 'N'),('O', 'O'),('P', 'P'),('Q', 'Q'),('R', 'R'),('S', 'S')
これだけで、挿入速度が1トン速くなるはずです。
私が想像するこれら2つのことだけで、挿入を数分に短縮できます(基本的にこのアプローチを使用して、約90秒で80k +を一度実行しました)。
最後の部分は、各コアが独自のSql接続を取得するようにリファクタリングし、すべてのスレッドを破棄する準備ができるまで開いたままにしておきます。
SqlCmd を使用してみてください。
Process.Start() を使用して複数のプロセスを実行し、sqlcmdを使用して並列プロセスでクエリを実行できます。
もちろん、スレッドで行う義務がある場合、この答えはもはや解決策にはなりません。
私はpowershellについてよく知りませんが、SQLはC#で常に仕事をしています。
C#の新しいasync/awaitキーワードにより、話していることを非常に簡単に実行できます。 C#は、マシンに最適なスレッド数のスレッドプールも作成します。
async Task<DataTable> ExecuteQueryAsync(query)
{
return await Task.Run(() => ExecuteQuerySync(query));
}
async Task ExecuteAllQueriesAsync()
{
IList<Task<DataTable>> queryTasks = new List<Task<DataTable>>();
foreach query
{
queryTasks.Add(ExecuteQueryAsync(query));
}
foreach task in queryTasks
{
await task;
}
}
上記のコードは、すべてのクエリをスレッドプールの作業キューに追加します。その後、完了する前にそれらをすべて待ちます。その結果、SQLの並列処理が最大レベルに達します。
お役に立てれば!