sanpobiyori.info

PowerShellで並列処理して遊ぶ

相変わらず、PowerShellの貝殻本こと『PowerShell実践ガイドブック』を少しずつ読み進めている今日この頃ですが、PowerShellのジョブ実行について書かれているところを読んでいて、以前から少しやってみたかったことを思い出したのでやってみました。

2019/05/18 追記

文章を全体的に見直ししました

やりたかったことというのは、SharePoint Onlineのカスタムリストへ対し情報を書き込むにあたり、書き込み処理を並列化するというものです。

はじめに

事前準備

  1. SharePoint Onlineの操作を行うため、こちらのページからSDKをインストールします。
  2. PowerShellスクリプトを実行するため、ExecutionPolicyを変更します。変更方法は管理権限でPowerShellを起動し、以下のコマンドを実行します。 PowerShell Set-ExecutionPolicy RemoteSigned

並列化前

並列化を行う前に通常SharePoint Onlineのカスタムリストへ書き込むコードを紹介します。

$SiteUrl = "https://xxxxx.sharepoint.com/sites/xxxxx"
$ListName = "<CustomList Title>"
$UserName  = "<Account MailAddress>"
$Password = Read-Host -Prompt "Enter Password" -AsSecureString

Import-Module Microsoft.Online.SharePoint.PowerShell -DisableNameChecking

$Context = New-Object Microsoft.SharePoint.Client.ClientContext($SiteUrl)
$Credentials = New-Object Microsoft.SharePoint.Client.SharePointOnlineCredentials($UserName, $Password)
$Context.Credentials = $Credentials
$Context.RequestTimeOut = 5000 * 60 * 10;
$Web = $Context.Web
$List = $Web.Lists.GetByTitle($ListName)
$Context.Load($List)
$Context.ExecuteQuery()

1..10 | %{
    $ListItemInfo = New-Object Microsoft.SharePoint.Client.ListItemCreationInformation
    $Item = $List.AddItem($ListItemInfo)
    $Title = 1000 + [int]$_
    $Item["Title"] = $Title
    $Item.Update()
    $Context.ExecuteQuery()
}

Write-Output "done"

処理の内容としてはシンプルに、指定したカスタムリストの「タイトル」列へ1001から1010までの数字を順番にリストへ書き込みます。
この処理を並列化することにより、タイトル列に1001~1010、2001~2010、3001~3010、4001~4010、5001~5010を書き込む処理を並列に行いたいと思います。

並列化

並列化概要

処理を並列化するにあたり、「Start-Job」コマンドレットを使用します。
「Start-Job」を使用することにより、現在実行中の処理とは別のプロセスが作成・実行されます。
ただし、「Start-Job」単体では処理が投げっぱなしになってしまい、処理結果は実行元のプロセスに返ってきません。
そのため、「Wait-Job」で実行した処理が終了を待ち、「Remove-Job」で結果を取得します。

失敗例

成功例の前に失敗例を紹介します。

Import-Module Microsoft.Online.SharePoint.PowerShell -DisableNameChecking

$Context = New-Object Microsoft.SharePoint.Client.ClientContext($SiteUrl)
$Credentials = New-Object Microsoft.SharePoint.Client.SharePointOnlineCredentials($UserName, $Password)
$Context.Credentials = $Credentials
$Context.RequestTimeOut = 5000 * 60 * 10;
$Web = $Context.Web
$List = $Web.Lists.GetByTitle($ListName)
$Context.Load($List)
$Context.ExecuteQuery()

Functions = {
    function Write-CustomList
    {
        param (
            $Prefix
            ,$Context
        )

        1..10 | %{
            $ListItemInfo = New-Object Microsoft.SharePoint.Client.ListItemCreationInformation
            $Item = $List.AddItem($ListItemInfo)
            $Title = $Prefix * 1000 + [int]$_
            $Item["Title"] = $Title
            $Item.Update()
            $Context.ExecuteQuery()

            sleep -Seconds 5
        }
    }
}

$Job = 1..5 | %{ 
    Start-Job Write-CustomList $_ $Context
}

Wait-Job -Job $Job
Remove-Job -Job $Job

Write-Output "done"

「Start-Job」コマンドレットを使った並列化を行う場合、新規プロセスが作成され処理が実行される点に気を付ける必要があるのですが、その点が全然考慮されていません。
では具体的にはどういった点に気を付ける必要があるかというと、

  • 変数や関数は新しく発行されたプロセスに引き継がれない
  • スクリプトの実行パスは既定の場所が使用される

今回、スクリプトの実行パスは関係ありませんが、変数や関数が引き継がれないという点が先ほどのスクリプトでは考慮されていません。
変数や関数が引き継がれないということは、読み込んだモジュールやSharePoint Onlineとのセッション情報も引き継がれないということになるので、その点も考慮する必要があります。

成功例

これらの点を考慮しつつ並列化すると、以下のスクリプトとなります。

$SiteUrl = "https://xxxxx.sharepoint.com/sites/xxxxx"
$ListName = "<CustomList Title>"
$UserName  = "<Account MailAddress>"
$Password = Read-Host -Prompt "Enter Password" -AsSecureString

$LogonData = @{
    'SiteUrl' = $SiteUrl;
    'ListName' = $ListName;
    'UserName' = $UserName;
    'Password' = $Password
}

$Functions = {
    function Write-CustomList
    {
        param (
            $Prefix
            ,$LogonData
        )

        $SiteUrl = $LogonData['SiteUrl']
        $ListName = $LogonData['ListName']
        $UserName = $LogonData['UserName']
        $Password = $LogonData['Password']

        Import-Module Microsoft.Online.SharePoint.PowerShell -DisableNameChecking

        $Context = New-Object Microsoft.SharePoint.Client.ClientContext($SiteUrl)
        $Credentials = New-Object Microsoft.SharePoint.Client.SharePointOnlineCredentials($UserName, $Password)
        $Context.Credentials = $Credentials
        $Context.RequestTimeOut = 5000 * 60 * 10;
        $Web = $Context.Web
        $List = $Web.Lists.GetByTitle($ListName)
        $Context.Load($List)
        $Context.ExecuteQuery()

        1..10 | %{
            $ListItemInfo = New-Object Microsoft.SharePoint.Client.ListItemCreationInformation
            $Item = $List.AddItem($ListItemInfo)
            $Title = $Prefix * 1000 + [int]$_
            $Item["Title"] = $Title
            $Item.Update()
            $Context.ExecuteQuery()

            sleep -Seconds 5
        }
    }
}

$Job = 1..5 | %{ 
    Start-Job -InitializationScript $Functions `
        -ScriptBlock{
            param ($Prefix, $LogonData)
            Write-CustomList $Prefix $LogonData
        } `
        -ArgumentList $_ , $LogonData
}
Wait-Job -Job $Job
Remove-Job -Job $Job

Write-Output "done"

実行結果がこちら

network-image

書き込まれている順番が1001~1010、2001~2010、3001~3010、4001~4010、5001~5010ではないため、正しく並列化されていることが分かります。

スクリプト詳細

Start-Jobでは変数や関数が引き継がれない為、実行したい処理や必要なパラメータを変数に格納し、実行時に引数として指定しています。

パラメータ説明

  • InitializationScript
    • 処理を開始する前に実行するコマンドを記載
    • 今回のケースではメイン処理で実行する関数を指定
    • 関数にはSharePoint Onlineに接続してカスタムリストに書き込む処理を指定しています
  • ScriptBlock
    • メインで処理するコマンドを記載
    • InitializationScriptに記載した関数を、ArgumentListで指定したパラメータを使って実行しています
  • ArgumentList
    • メインで処理するコマンドに引き渡すパラメータを記載
    • カスタムリストに書き込む内容や、SharePoint Onlineに接続するために必要なユーザ情報や接続先の情報を引き渡しています

余談

実際に使うとなった場合、入る順番が順不同となる可能性が高いため、そのあたりを考慮に入れる必要があるかと思いますが、マシンスペックと回線速度次第では、ある程度まとまった量のデータをSharePoint Onlineに書き込む必要がある場合でも、大幅な時間短縮ができそうです。
ただし、並列化のし過ぎでSharePoint Online側からセッションが切られる可能性はあるので程々にですが・・・。
また、基本的なSharePoint Serverでもある程度参考にはなるかと思いますが、こちらの場合はサーバスペックの問題があるので、現実的にはあまり並列化はできないと思われます。
コードはGitHubでも公開しています。

今更ながらGitHubに初めてコードを登録しました。
今まで興味はあったのですが、そういった機会やアップロードするようなコードもなかったので利用を後回しにしていたのですが、これからはなるべく積極的に使用して、使い方を覚えたいですね。

参考URL


comments powered by Disqus