しばたテックブログ

気分で書いている技術ブログです。

PowerShell Core 6.1で導入されるThreadJobについて

ちょっと前の話なのですが、PowerShell Core 6.1 Preview.4からジョブ関連でThreadJobという新機能が追加されました。

従来のジョブ

これまでのPowerShellではStart-Jobなどのコマンドを使うことで重たい処理をバックグラウンドで実行させるジョブの仕組みが提供されてきました。
PowerShell Core 6.0からはStart-Jobの代わりに&を使うこともできます。

blog.shibata.tech

このジョブの仕組みは、個々のジョブをPowerShellのプロセスとして実行するものであるため、ジョブの独立性には優れているものの、複数のジョブを同時に並列して実行する様な場合はプロセス起動のコストの高さからパフォーマンスが急激に悪くなってしまう問題を抱えていました。

f:id:stknohg:20180905174141p:plain

(PowerShellのジョブ実行の仕組み)

例として以下の様な単純なジョブでも10並列にすると異常に時間がかかってしまいます。

Measure-Command {
    # 10並列でジョブ実行
    1..10 | ForEach-Object {
        # 与えられた引数を表示するだけの単純なジョブを実行
        Start-Job -ArgumentList $_ -ScriptBlock { param($arg) Write-Output "arg = $arg" }
    }
    # ジョブの終了待ち
    Get-Job | Receive-Job -Wait -OutVariable Output
}
# 個々のジョブの処理時間を表示
Get-Job | Select-Object Id, Name, PSBeginTime, PSEndTime

手元の環境(Surface Pro4)で約22秒もかかっています。

f:id:stknohg:20180905174308p:plain

ちなみにこの処理を実行しているときはこんな感じで大量のPowerShellプロセスが起動されリソースを消費していることが分かります。

f:id:stknohg:20180905174415p:plain

ThreadJob

この問題を解消するために導入されたのがThreadJobになります。

こちらは中の人であるPaulさんのモジュールとして機能が実装され、PowerShell Core 6.1 Preview.4より標準に組み込まれる形になっています。

github.com

このモジュールで提供される機能はStart-ThreadJobコマンドただ一つです。
その使い方は基本的にはStart-Jobと同じで、Start-ThreadJobコマンドでジョブが作られ、作られたジョブはGet-JobReceive-JobRemove-Jobといったコマンドで扱うことができます。

f:id:stknohg:20180905174450p:plain

従来のジョブと違うのはPSJobTypeNameの内容がThreadJobとなっているくらいでしょうか。

ThreadJobの仕組み

ThreadJobでは個々のジョブは別プロセスとはならず、同一プロセス内でRunspaceを分けることで行われています。

f:id:stknohg:20180905174508p:plain

(ThreadJobの仕組み)

このRunspaceを分けることで並列処理を行う手法自体は昔からあるもので、詳細については以下のブログを見ると参考になるでしょう。

ThreadJobはこの並列処理の手法をPowerShellのジョブのインターフェイスに合わせたものとなります。

性能差

ThreadJobは同一プロセス内の別Runspaceで実行されるため従来のジョブほどの独立性はありませんが、起動コストが少なく複数のジョブを並列実行するのに向いています。

最初に示したジョブの例をThreadJobで試してみると、約0.3秒と1秒もかからずに終わってしまいます。

Measure-Command {
    # 10並列でThreadJob実行
    1..10 | ForEach-Object {
        # 与えられた引数を表示するだけの単純なジョブを実行
        Start-ThreadJob -ArgumentList $_ -ScriptBlock { param($arg) Write-Output "arg = $arg" }
    }
    # ジョブの終了待ち
    Get-Job | Receive-Job -Wait -OutVariable Output
}
# 個々のジョブの処理時間を表示
Get-Job | Select-Object Id, Name, PSBeginTime, PSEndTime

f:id:stknohg:20180905174617p:plain

Windows PowerShellでのThreadJobの利用

このTreadJobはモジュールとして提供されているため、PowerShell CoreだけでなくWindows PowerShell(PowerShell 3.0以降)でも利用可能です。

インストール方法はいろいろありますがInstall-Moduleをするのが手っ取り早いでしょう。

Install-Module ThreadJob -Scope CurrentUser

先ほどの例もこの様にサクッと利用可能です。

f:id:stknohg:20180905174650p:plain

従来のジョブとThreadJobの使い分け

最後に従来のジョブとThreadJobの使い分けについて簡単に私見を述べたいと思います。

本エントリの例で示した様に単純な処理時間だけをみるとThreadJobだけ使えば良さそうに見えます。
性能は重要な要素ですのでThreadJobが使える環境では積極的にThreadJobを使うのが良いと思います。

ただし、従来のジョブにもメリットはあります。
個々のジョブが別プロセスであるため、仮にジョブ内部で予期しない例外が発生した場合でも呼び出し元に影響を及ぼすことは決してありません。
また、例えばメモリなどのリソースを大量に消費する処理をジョブにした場合、呼び出し元にそのリソース消費が影響することはありませんし、ジョブが終了されればプロセス自体が消えてしまうためリソースの解放漏れといったことを気にする必要が一切ありません。
ThreadJobではRunSpaceが分かれていても同一プロセスであるため、例外の種類によってはプロセス自体が落ちてしまう可能性は残りますし、リソースの消費や解放漏れについても気にかけてやる必要があります。

実行するジョブの内容に応じて従来のジョブのメリットを得ることができるのであればそれを利用するのが良いでしょう。

もしくは単純に

重たい処理には従来のジョブ、大量の並列処理にはThreadJob。

くらいの使い分けでも良いかと思います。

AzurePSDriveの実装を調べてみた

azure-mokumoku.connpass.com

先週ちょっと東京へ行く用事があり、それに合わせてAzureもくもく会@新宿に参加した際に調べた内容をまとめたエントリです。

AzurePSDriveとは

AzurePSDriveはAzure CloudShell(PowerShell)を起動した際のカレントロケーションがPS Azure:\>となるアレを提供する仕組みです。

f:id:stknohg:20180829203208p:plain

PowerShellのドライブはファイルシステム以外にも階層構造をもつデータであれば拡張して扱うことが可能であり、AzurePSDriveではAzureのサブスクリプションとそれに紐づく各リソースをドライブとして扱うことができる様になります。

類似のものとしてはIISの設定を行うWebAdminstrationモジュールのIISドライブや、SqlServerモジュールSQL Serverドライブなどがあり、こちらの方がイメージがつかみやすいかもしれません。

ソースコード

AzurePSDriveはGitHubでそのソースが公開されています。

github.com

英語ですが基本的な概念や使い方もこちらに記載されています。

AzurePSDriveの実装について

ここから本題に入ります。
Azure Cloud Shellの環境で実際にAzurePSDriveを使いながらその実装を追っていきます。

Simple Hierarchy in PowerShell(SHiPS)

AzurePSDriveでは独自のドライブを実装するのにSimple Hierarchy in PowerShell(SHiPS)と呼ばれるPowerShellモジュールを利用しています。

github.com

このモジュールは従来独自の実装が必要であったPSドライブを簡易に作成するためのモジュールであり、SHiPSDirectorySHiPSLeafといったクラスを継承したクラスを作ることで簡単に階層構造を表現することができる様になります。

詳しい話と実装例がぎたぱそ先生の本(4.9.14 SHiPSとクラスを使ったPSドライブの自作)に書いてますので気になる人はぜひご覧ください。(宣伝)

PowerShell実践ガイドブック ~クロスプラットフォーム対応の次世代シェルを徹底解説~

PowerShell実践ガイドブック ~クロスプラットフォーム対応の次世代シェルを徹底解説~

以下同書のサンプルコードを例として紹介します。

# SHiPSの実装例
#   https://github.com/guitarrapc/Book-PowerShell-Samples/blob/master/Book-PowerShell-Samples/Chapter4/4.9_Class/PSDrive/SolarSystem.psm1 より抜粋
using namespace Microsoft.PowerShell.SHiPS

# SHiPSDirectory を継承したクラスでコンテナとなる要素を定義
class SolarSystem : SHiPSDirectory {

    # ・・・中略・・・

}

# SHiPSLeaf を継承したクラスで枝要素を定義
class Moon : SHiPSLeaf {
    
    # ・・・中略・・・
}

Azure PowerShellモジュールと独自の型定義

AzurePSDriveは内部でAzureのリソースにアクセスする際にAzure PowerShell moduleの機能を利用しています。
Windows PowerShell、PowerShell Core両方に対応しており、現時点のバージョン(Ver.0.8.7)では、

  • AzureRM.Profile (AzureRM.Profile.NetCore)
  • AzureRM.Resources (AzureRM.Resources.NetCore)
  • AzureRM.Compute (AzureRM.Compute.NetCore)
  • AzureRM.Websites (AzureRM.Websites.NetCore)
  • Azure.Storage (Azure.Storage.NetCore)

が依存モジュールとなっています。

また、内部的にはAzure PowerShell moduleのオブジェクトをそのまま使っている部分があるのですが、一部オブジェクトについてはドライブ独自の型に差し替えられています。

例として、改めて後述しますが、AllResources要素ではGet-AzureRmResourceの結果である Microsoft.Azure.Commands.ResourceManager.Cmdlets.SdkModels.PSResource型のオブジェクトをそのまま使っているのですが、この型がAzurePSDriveではAzurePSDriveResourceTypeになります。

細かい説明は省きますが、実装としては以下の様にして取得したオブジェクトの型を差し替えています。

# AzurePSDriveResource.psm1 より一部抜粋
& "$script:AzureRM_Resources\Get-AzureRmResource" | %{ $_.psobject.typenames.Insert(0, "AzurePSDriveResourceType"); $_ }

CloudShellログイン時点

ここからはAzure Cloud Shell(PowerShell)の実際の画面を基に各階層の内容について説明していきます。

最初にAzure Cloud Shellにログインした時点では最上位の階層であるAzureドライブがカレントロケーションとなります。

f:id:stknohg:20180829203208p:plain

Get-PSDriveを使うとこのAzureドライブの情報を見ることができます。

PS Azure:\> Get-PSDrive | Format-Table -AutoSize

Name     Used (GB) Free (GB) Provider    Root               CurrentLocation
----     --------- --------- --------    ----               ---------------
/            15.01     33.40 FileSystem  /                      home/stknohg
Alias                        Alias
Azure                        SHiPS       AzurePSDrive#Azure
Env                          Environment
Function                     Function
Variable                     Variable

ファイルシステムなどのドライブの中にAzureドライブが存在し、SHiPSプロバイダーとして定義されていることが分かります。

第1階層 : サブスクリプション

AzureドライブでGet-ChildItem(dir)を実行すると子要素を一覧することができ、たとえば私のアカウントだと以下の様に表示されます。

PS Azure:\> dir


    Directory: Azure:


Mode SubscriptionName SubscriptionId                       TenantId                             State
---- ---------------- --------------                       --------                             -----
+    stknohg          ********-****-****-****-************ ********-****-****-****-************ Enabled

最初の階層はログインしたアカウントに紐づくサブスクリプションが列挙されます。

各サブスクリプションはSubscription型として定義されており、内部的にはACC_TID環境変数が定義されていればそのテナントID、ACC_TID環境変数が定義されていない場合は(Get-AzureRmTenant)[0].IdとなるテナントIDに紐づくサブスクリプションをGet-AzureRmSubscriptionで取得しています。

ちなみに現在Azure Cloud Shellは全てLinux(Ubuntu)のコンテナになってしまったため、lsコマンドは/bin/lsなのでAzureドライブの子要素を取得するのには使えませんので注意してください。

第2階層 : リソース分類

つづけてSet-Location(cd)でサブスクリプション(ここではstknohg)に移動し、子要素を一覧すると以下の様になります。

PS Azure:\> cd ./stknohg/
PS Azure:\> dir


    Directory: Azure:/stknohg


Mode Name
---- ----
+    AllResources
+    ResourceGroups
+    StorageAccounts
+    VirtualMachines
+    WebApps

SubScriptionの子要素は以下で固定されています。

  • AllResources
  • ResourceGroups
  • StorageAccounts
  • VirtualMachines
  • WebApps

それぞれ名前から予測はつくと思いますが詳細は後述します。

第3階層(AllResouces)

ここからはSubScriptionの各子要素について説明します。
最初にAllResources要素についてです。

この要素はAllResources型で表現され、内容としては単純にGet-AzureRmResourceの結果を返しているだけになります。
さらなる子要素はありません。

通常Get-AzureRmResourceの結果は Microsoft.Azure.Commands.ResourceManager.Cmdlets.SdkModels.PSResource型ですが、表向きの型をAzurePSDriveResourceTypeに更新しており、Get-Memberで取得できる型名は後者となっています。

第3階層(ResourceGroups)

ResourceGroups要素がおそらくAzurePSDriveで扱うメインの要素になるかと思います。

この要素はResourceGroups型で表現され、子要素にResourceGroup型のオブジェクトを持ちます。

実装としては、Get-AzureRmResourceGroupGet-AzureRmResourceを内部で使い、各リソースを呼出してリソースの階層構造を表現しています。
ここから第4階層(ResourceProvider)、第5階層(ResourceType)と続くのですが説明が冗長になる*1ので本エントリではこのくらいにしておきます。

第3階層(StorageAccounts)

ここからのStorageAccounts、VirtualMachines、WebAppsについてはリソースの中でもよく使うであろうものを特別視したものと思われます。
(もしかしたら子要素の表現が難しいものを特別視している可能性もありますが、ちょっと断言できません...)

この要素はStorageAccounts型で表現され、Get-AzureRmStorageAccountの結果を子要素にStorageAccount型として持ちます。

StorageAccountの子要素として各ストレージの種類に応じた、

  • Blobs
  • Files
  • Tables
  • Queues

がありそれぞれの情報にアクセスすることができます。
内部的にはAzure.Storage (Azure.Storage.NetCore)モジュールの各コマンドレットが使われ、その結果を利用しています。

第3階層(VirtualMachines)

この要素はVirtualMachines型で表現され、 Get-AzureRmVMの結果をAzurePSDriveVM型として子要素に持ちます。

これ以上の階層は無く、単純にVMの一覧が取得できて終わりの様です。

第3階層(WebApps)

この要素はWebApps型として表現され、Get-AzureRmWebAppの結果をAzurePSDriveWebApp型として子要素に持ちます。

この要素もこれ以上の階層は無く、単純にWebAppの一覧が取得できて終わりです。

最後に

とりあえずこんな感じです。

私自身Azureをさほど使いこなしていないため、ソースを読み間違えている部分があるかもしれません。
もし本エントリの内容におかしな点がありましたらフィードバックしていただけると嬉しいです。

*1:調べきれなかったとも言います...

PowerShellで外部プロセスのコマンドラインを取得する

何気に.NET Framework/.NET Coreには外部プロセスのコマンドラインを取得する方法が無く、このため、PowerShellからコマンドラインを取得しようとする場合少し手間をかけてやる必要があります。

PowerShellで外部プロセスのコマンドラインを取得する

簡単な関数Get-ProcessCommandlineを作ってGistに上げました。
使用例と併せてご覧ください。

厳密に対応バージョンを考慮していませんが、Windows PowerShell、PowerShell Core両方の大抵の環境で動くはずです。

gist.github.com

簡単な解説

各プラットフォーム毎の簡単な解説を補足しておきます。

Windowsの場合

Windowsにおいて外部プロセスのコマンドラインを取得するには、ReadProcessMemoryなどのプロセス情報を読み取るWin32 APIを使うかWMIを使う必要があります。

PowerShellからであればWMIを扱うほうが圧倒的に楽なので先述の関数でもWMIを使っています。
Win32_ProcessクラスにCommandLineというズバリなプロパティがあるのでこれを取得するだけでOKです。

# Windowsでは Win32_Process クラスの CommandLine プロパティからコマンドラインを取得可能
$proc = Get-WmiObject -Class Win32_Process -Filter "ProcessId = $Id" -Property "ProcessId", "CommandLine"
if ($null -eq $proc) {
    return ""
}
return $proc.CommandLine

# PowerShell Core なら Get-CimInstance を使う
$proc = Get-CimInstance -Class Win32_Process -Filter "ProcessId = $Id" -Property "ProcessId", "CommandLine"
if ($null -eq $proc) {
    return ""
}
return $proc.CommandLine

Linuxの場合

Linuxの場合プロセスの情報を/proc/から取得することができます。
プロセスのコマンドラインは/proc/[プロセスID]/cmdlineに記述されているのでGet-Contentで内容を読み取ってやればOKです。

注意すべきところはコマンドラインのデリミタがヌル文字(\0)である点くらいでしょうか。

# Linuxでは /proc/[プロセスID]/cmdline からコマンドラインを取得可能
if (-not (Test-Path -LiteralPath "/proc/$Id")) {
    return ""
}
return @(Get-Content -LiteralPath "/proc/$Id/cmdline")[0] -replace "\0", " "

macOSの場合

残念ながらmacOSには/proc/が存在せずLinuxと同じ様にはいきません。
macOSのシステムプログラミングには全く詳しくないため大した調査は出来なかったのですが、

stackoverflow.com

を見る限りではCで頑張らないとダメな様です。

ちょっと本末転倒な感じもありますが、PowerShellからだとpsコマンドの結果を抜くのが一番手っ取り早く確実です。
(これならはじめからpsコマンドだけで良いのでは?というお気持ちです...)

psコマンドで特定の要素だけ取得するには-oオプションを、取得結果からカラムヘッダーを除外するには=をつけてやればよいため、ps -o commnand=の様に指定してやればコマンドラインを取得できます。

# macOSの場合は psコマンドを使ってコマンドラインを取得可能
return (/bin/ps -o command= -p $Id)