Powershellで実行しているときとVisual Studioで実行しているときのHttpClient同時動作が異なる


10

B2Cでユーザーを作成するためにMS Graph APIを使用して、数百万のユーザーをオンプレミスADからAzure AD B2Cに移行しています。この移行を実行するための.Net Core 3.1コンソールアプリケーションを作成しました。速度を上げるために、Graph APIを同時に呼び出しています。これはうまく機能しています。

開発中にVisual Studio 2019から実行しているときに許容できるパフォーマンスを体験しましたが、テストのためにPowershell 7のコマンドラインから実行しています。Powershellから、HttpClientへの同時呼び出しのパフォーマンスは非常に悪いです。Powershellからの実行時にHttpClientが許可する同時呼び出しの数には制限があるため、40から50を超える同時バッチでの要求がスタックし始めます。残りをブロックしている間、40から50の同時リクエストを実行しているようです。

私は非同期プログラミングの支援を探していません。Visual Studioの実行時の動作とPowershellコマンドラインの実行時の動作の違いをトラブルシューティングする方法を探しています。Visual Studioの緑の矢印ボタンからリリースモードで実行すると、期待どおりに動作します。コマンドラインからの実行ではできません。

タスクリストに非同期呼び出しを入力し、Task.WhenAll(tasks)を待ちます。各呼び出しには300〜400ミリ秒かかります。Visual Studioから実行すると、期待どおりに動作します。私は1000の呼び出しの同時バッチを作成し、それぞれが予想時間内に個別に完了します。タスクブロック全体は、最長の個々の呼び出しよりも数ミリ秒長くかかります。

Powershellコマンドラインから同じビルドを実行すると、動作が変わります。最初の40から50の呼び出しは、予想される300から400ミリ秒かかりますが、その後、個々の呼び出し時間はそれぞれ最大20秒になります。呼び出しはシリアル化されていると思うので、他の呼び出しが待機している間、一度に実行されるのは40〜50だけです。

何時間もの試行錯誤の末、HttpClientに絞り込むことができました。問題を特定するために、Task.Delay(300)を実行してモック結果を返すメソッドを使用して、HttpClient.SendAsyncの呼び出しをモック化しました。この場合、コンソールからの実行は、Visual Studioからの実行と同じように動作します。

私はIHttpClientFactoryを使用していますが、ServicePointManagerの接続制限を調整しようとしました。

これが私の登録コードです。

    public static IServiceCollection RegisterHttpClient(this IServiceCollection services, int batchSize)
    {
        ServicePointManager.DefaultConnectionLimit = batchSize;
        ServicePointManager.MaxServicePoints = batchSize;
        ServicePointManager.SetTcpKeepAlive(true, 1000, 5000);

        services.AddHttpClient(MSGraphRequestManager.HttpClientName, c =>
        {
            c.Timeout = TimeSpan.FromSeconds(360);
            c.DefaultRequestHeaders.Add("User-Agent", "xxxxxxxxxxxx");
        })
        .ConfigurePrimaryHttpMessageHandler(() => new DefaultHttpClientHandler(batchSize));

        return services;
    }

これがDefaultHttpClientHandlerです。

internal class DefaultHttpClientHandler : HttpClientHandler
{
    public DefaultHttpClientHandler(int maxConnections)
    {
        this.MaxConnectionsPerServer = maxConnections;
        this.UseProxy = false;
        this.AutomaticDecompression = System.Net.DecompressionMethods.GZip | System.Net.DecompressionMethods.Deflate;
    }
}

タスクを設定するコードは次のとおりです。

        var timer = Stopwatch.StartNew();
        var tasks = new Task<(UpsertUserResult, TimeSpan)>[users.Length];
        for (var i = 0; i < users.Length; ++i)
        {
            tasks[i] = this.CreateUserAsync(users[i]);
        }

        var results = await Task.WhenAll(tasks);
        timer.Stop();

これがHttpClientをモックアウトした方法です。

        var httpClient = this.httpClientFactory.CreateClient(HttpClientName);
        #if use_http
            using var response = await httpClient.SendAsync(request);
        #else
            await Task.Delay(300);
            var graphUser = new User { Id = "mockid" };
            using var response = new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(JsonConvert.SerializeObject(graphUser)) };
        #endif
        var responseContent = await response.Content.ReadAsStringAsync();

500の同時リクエストを使用してGraphAPI経由で作成された10k B2Cユーザーのメトリックを次に示します。TCP接続が作成されているため、最初の500リクエストは通常​​より長くなります。

これは、コンソール実行メトリックへのリンクです。

Visual Studioの実行メトリックへのリンクは次のとおりです。

テストの実行で問題のあるコードをできるだけ分離するために、すべての同期ファイルアクセスをプロセスの最後に移動したため、VS実行メトリックのブロック時間はこの投稿で述べたものとは異なります。

プロジェクトは.Net Core 3.1を使用してコンパイルされます。Visual Studio 2019 16.4.5を使用しています。


2
最初のバッチの後で、netstatユーティリティを使用した接続の状態を確認しましたか?最初のいくつかのタスクが完了した後に何が起こっているかについての洞察を提供するかもしれません。
Pranav Negandhi

この方法で解決しない場合(HTTP要求を非同期にする)、ConcurrentQueue [object]コンシューマー/プロデューサーの並列処理で各ユーザーの同期HTTP呼び出しを常に使用できます。私は最近、PowerShellで約2億のファイルに対してこれを行いました。
thepip3r

1
@ thepip3r私はあなたの賞賛を読み直し、今回それを理解しました。ちゃんと覚えておきますよ。
マークラウター

1
いいえ、c#の代わりにPowerShellに移行したい場合は、leeholmes.com / blog / 2018/09/05 / …と言っています。
thepip3r

1
@ thepip3rスティーブンクリアリーのブログエントリを読んでください。私は良いはずです。
マークラウター

回答:


3

2つのことが頭に浮かびます。ほとんどのMicrosoft Powershellはバージョン1および2で記述されています。バージョン1および2にはMTAのSystem.Threading.Thread.ApartmentStateがあります。バージョン3〜5では、アパートメントの状態はデフォルトでSTAに変更されました。

2番目の考えは、System.Threading.ThreadPoolを使用してスレッドを管理しているように聞こえるということです。スレッドプールの大きさは?

それらが問題を解決しない場合は、System.Threadingの下で掘り始めます。

あなたの質問を読んだとき、このブログを思いました。https://devblogs.microsoft.com/oldnewthing/20170623-00/?p=96455

同僚は、1,000個の作業項目を作成するサンプルプログラムでデモを行いました。各作業項目は、完了するまでに500ミリ秒かかるネットワーク呼び出しをシミュレートします。最初のデモでは、ネットワーク呼び出しが同期呼び出しをブロックしており、サンプルプログラムでは、効果をより明確にするために、スレッドプールを10スレッドに制限しました。この構成では、最初の数個の作業項目がすぐにスレッドにディスパッチされましたが、新しい作業項目を処理するために使用できるスレッドがなくなったため、レイテンシの構築が開始され、残りの作業項目はスレッドが実行されるのを長く待たなければなりませんでしたサービスできるようになります。ワークアイテムの開始までの平均待ち時間は2分を超えていました。

更新1:スタートメニューからPowerShell 7.0を実行したところ、スレッドの状態はSTAでした。2つのバージョンでスレッドの状態は異なりますか?

PS C:\Program Files\PowerShell\7>  [System.Threading.Thread]::CurrentThread

ManagedThreadId    : 12
IsAlive            : True
IsBackground       : False
IsThreadPoolThread : False
Priority           : Normal
ThreadState        : Running
CurrentCulture     : en-US
CurrentUICulture   : en-US
ExecutionContext   : System.Threading.ExecutionContext
Name               : Pipeline Execution Thread
ApartmentState     : STA

更新2:私はもっと良い答えを望みますが、何かが際立つまで2つの環境を比較する必要があります。

PS C:\Windows\system32> [System.Net.ServicePointManager].GetProperties() | select name

Name                               
----                               
SecurityProtocol                   
MaxServicePoints                   
DefaultConnectionLimit             
MaxServicePointIdleTime            
UseNagleAlgorithm                  
Expect100Continue                  
EnableDnsRoundRobin                
DnsRefreshTimeout                  
CertificatePolicy                  
ServerCertificateValidationCallback
ReusePort                          
CheckCertificateRevocationList     
EncryptionPolicy            

更新3:

https://docs.microsoft.com/en-us/uwp/api/windows.web.http.httpclient

さらに、すべてのHttpClientインスタンスは独自の接続プールを使用し、そのリクエストを他のHttpClientインスタンスによって実行されるリクエストから分離します。

Windows.Web.Http名前空間のHttpClientおよび関連クラスを使用するアプリが大量のデータ(50メガバイト以上)をダウンロードする場合、アプリはそれらのダウンロードをストリーミングし、デフォルトのバッファリングを使用しないでください。デフォルトのバッファリングを使用すると、クライアントのメモリ使用量が非常に大きくなり、パフォーマンスが低下する可能性があります。

2つの環境を比較し続けるだけで、問題は目立つはずです

Add-Type -AssemblyName System.Net.Http
$client = New-Object -TypeName System.Net.Http.Httpclient
$client | format-list *

DefaultRequestHeaders        : {}
BaseAddress                  : 
Timeout                      : 00:01:40
MaxResponseContentBufferSize : 2147483647

Powershell 7.0で実行すると、System.Threading.Thread.CurrentThread.GetApartmentState()がProgram.Main()内からMTAを返す
Mark Lauter

デフォルトの最小スレッドプールは12でしたが、最小プールサイズをバッチサイズ(テスト用に500)に増やしてみました。これは動作に影響を与えませんでした。
マークラウター

両方の環境でいくつのスレッドが生成されますか?
アーロン

「HttpClient」がすべての作業を行っているため、「HttpClient」にはいくつのスレッドがあるのだろうと思いました。
アーロン

両方のバージョンのアパートの状態は何ですか?
アーロン
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.