web-dev-qa-db-ja.com

動的にサイズ設定されたワーカープロセスのプールを管理するための戦略

私はそれを解決するために問題を抱えていますが、これはスレッドプールに非常によく似ています。プールのサイズの管理に関する情報を入手したり、リソースを見つけたりしたいと思っていました。

私が以下を持っているとしましょう:


_public interface IWorkerBee : IDisposable
{
    Task Configure(WorkerBeeConfiguration config);
    Task<WorkItemResult> DoWork(WorkItemData workData);
}

public class WorkerBeeConfiguration 
    : Equ.MemberwiseEquatable<WorkerBeeConfiguration>
{ 
    /* initialization stuff */ 
}

public class WorkItemData
{
    public string CacheKey { get; set; }
    public TimeSpan ExpireAfter { get; set; }
    /* other per-work-item stuff */
}

public class WorkItemResult { }
_

システムについてのいくつかの事実はここにあります:

  • 1秒あたり50〜100個のWorkItemDataが処理のために送信されます。
  • それらは数百のユニークなWorkerBeeConfigurationに広がっています。
  • IWorkerBee.Configure()は高価です-数秒かかり、新しい_System.Diagnostics.Process_が作成されます。
  • IWorkerBee.DoWork()の結果がキャッシュにない場合、作成に200〜300ミリ秒かかり、IWorkerBeeの子Processとのプロセス間通信が含まれます。
  • IWorkerBeeは構成後に再利用できますが、DoWorkはシングルスレッドであり、一度に1つのワークアイテムしか処理できません。
  • プールを拡張し、ワーカーインスタンスを長期間保持することに積極的になることは問題ありません。主な制約はシステムメモリです。低レイテンシと高スループットが必要です。
  • WorkItemDataは、多数の異なるソースからデータを取得できる大規模なシステムによって提供されます。このコードは同じプロセス内で実行され、キャッシュされたデータのために10〜20 GBのメモリを消費する可能性があります。サーバーには、キャッシュがデータ用に消費するメモリの少なくとも2倍のメモリが必要です。
  • WorkItemDataからWorkerBeeConfigurationへの分布は、エンドユーザーの使用状況、および使用可能なWorkerBeeConfigurationの追加と削除によって、時間とともに変化します。大規模なユーザーがオンラインになり、ディストリビューションを著しく変更することは珍しくありません。履歴ログから、_WorkerBeeConfiguration-A_はWorkItemDataの3〜4%を占め、_WorkerBeeConfiguration-B, C, D & E_は1〜2%を占めていることを知っているかもしれませんが、その情報には依存したくないサイジングのため-私はそれが自動調整されることを望みます。

IWorkerBeeの実装は次のようになります。

_public class WorkerBee : IWorkerBee
{
    private readonly AsyncLock Lock = new AsyncLock();
    private readonly ICache Cache;
    private System.Diagnostics.Process WorkerProcess; // each process commits 20-30MB of private, unshared working set memory

    // This is expensive - both creating and configuring the process - about a second each
    public async Task Configure(WorkerBeeConfiguration config)
    {
        using (await Lock.LockAsync()) {
            if (WorkerProcess == null)
                WorkerProcess = await CreateProcess(config);

            await ConfigureProcess(WorkerProcess, config);
        }
    }

    private Task<System.Diagnostics.Process> CreateProcess(WorkerBeeConfiguration config) 
        => TaskConstants<System.Diagnostics.Process>.Default;

    private Task ConfigureProcess(System.Diagnostics.Process process, WorkerBeeConfiguration config)
        => Task.CompletedTask;

    public void Dispose() { WorkerProcess.Dispose(); }

    // Only one item can be processed at a time. Each item takes 200-300 ms.
    public Task<WorkItemResult> DoWork(WorkItemData workData)
        => Cache.GetOrSetAsync(
            workData.CacheKey,
            workData.ExpireAfter,
            async () => {
                using (await Lock.LockAsync())
                    return await DoWorkInner(workData);
            });

    private Task<WorkItemResult> DoWorkInner(WorkItemData workData)
        => TaskConstants<WorkItemResult>.Default; // inter-process communication to perform work
}
_

使用中の一意のWorkerBeeConfigurationごとにプールをホットに保ちたいと思います(使用されなくなったプールを完全に破棄する必要があるのではないかと思いますが、より完全な実装ではそれが可能です):

_public class WorkerBeeProvider
{
    private readonly ConcurrentDictionary<WorkerBeeConfiguration, Task<IWorkerBee>> Colony
        = new ConcurrentDictionary<WorkerBeeConfiguration, Task<IWorkerBee>>();

    public Task<IWorkerBee> GetWorkerBee(WorkerBeeConfiguration config)
        => Colony.GetOrAdd(config, CreateAndConfigureWorkerBeePool);

    static private async Task<IWorkerBee> CreateAndConfigureWorkerBeePool(WorkerBeeConfiguration config)
    {
        var Hive = new WorkerBeePool();
        await Hive.Configure(config);
        return Hive;
    }
}
_

これは単純なプールの実装ですが、需要に応じたサイズ変更については触れていません。

_public interface INextBeeStrategy
{
    Task<IWorkerBee> GetNextBee(List<IWorkerBee> Hive);
}

public class WorkerBeePool : IWorkerBee
{
    private List<IWorkerBee> Hive;
    private INextBeeStrategy NextBee;

    public WorkerBeePool(int initialSize = 1, INextBeeStrategy nextBeeStrategy = null)
    {
        NextBee = nextBeeStrategy ?? new RoundRobin();
        Hive = new List<IWorkerBee>(initialSize);
        for (var i = 0; i < initialSize; i++)
            Hive.Add(new WorkerBee());
    }

    public Task Configure(WorkerBeeConfiguration config)
        => Task.WhenAll(Hive.Select(h => h.Configure(config)));

    public void Dispose()
    {
        foreach (var bee in Hive)
            bee.Dispose();
    }

    public async Task<WorkItemResult> DoWork(WorkItemData workData)
        => await (await NextBee.GetNextBee(Hive)).DoWork(workData);


    private class RoundRobin : INextBeeStrategy
    {
        private int _counter = -1;

        public Task<IWorkerBee> GetNextBee(List<IWorkerBee> Hive)
            => Task.FromResult(Hive[Interlocked.Increment(ref _counter) % Hive.Count]);
    }
}
_

では、システムリソースを使い果たして他の数百のプールとのバランスを保つことなく、リクエストをキューに入れすぎるのではなく、需要に応じてWorkerBeePoolを拡張するにはどうすればよいでしょうか。

2
quentin-starin

ここでは少し試行錯誤する必要があるようですが、私が試みる戦略は、実行する必要のある作業に対して少し統計分析を実行し、その情報を使用して必要に応じてスレッド数を増減することです。

つまり、キュー内の現在の項目数を時折(頻繁にスリープ状態で)ポーリングして処理する以外は何もしないスレッドが必要です。この数が以前にポーリングされた数よりも多い場合は、アクティブワーカーの数を1つ増やすことを検討してください。処理するキュー内のアイテム数が減少している場合は、アクティブワーカーの数を1つ減らすことを検討してください。ワーカーの数は変わらないはずですが、単にワーカーがジョブを終了したときに、別のワーカーに割り当てます。

より低いとは、ワーカースレッドの理想的な数を1つ減らし、より多くのワーカーを作成するのではなく、1つが自然に終了するまで待ってから、非アクティブにすることを意味します。ジョブが完了する前にこの数が再び増加した場合、ワーカースレッドの数を増やすと、以前の数のワーカースレッドに戻るだけで、何も変化しません。

おそらく、新しいワーカースレッドを生成する必要があるかどうかの適切な指標は、ワーカーを最後に作成または破棄したときにジョブの数が10%増加または減少した場合です。もちろん、この割合はあなた次第です。

ここで変更される要素は次のとおりです。

  • 許可されるワーカーの最大数。一般に、CPUコアの数よりも多くのスレッドを使用することによるパフォーマンス上の利点はzilchであるため、CPUコアの数に設定することを強く検討してください。このカウントの「分析」スレッドは考慮しません。その時間のほとんどが睡眠に費やされることになるためです。このワーカーの最大数では、各スレッドが動作するために必要なメモリの量も考慮する必要があります。そのため、メモリによって許容される最大値とCPUコアの数の間の最小値を実行します。
  • 各ポーリングの時間。これにより、分析がワーカースレッドの数を変更する速度が決まりますが、低すぎると、システムにかなりの負荷がかかる可能性もあります。
  • 許容パーセンテージ。 10%と言いましたが、キューの変更にすばやく対応したい場合は、おそらく5%のほうが適しています。許容誤差が低いと、常にワーカーを作成または破棄することになるため、コストが高くなるため、バランスが取れている必要があります。
  • 労働者を開始します。可能であれば、これを「最小」労働者と呼んでください。何スレッドから始めますか?

いくつかのテストを行うことで、要求に応じて増加し、要求が満たされると減少する適切な動的スレッドプールを実現できるでしょう。これを特にトリッキーにする唯一のことは、スレッドの作成/破棄に必要な時間です。これが特に高価であることがわかった場合は、より高い許容誤差を追加してください。そうでない場合は、毎回作成または破棄されるスレッドの数を1ではなく2に増やすことを検討してください。

幸運を!

2
Neil