私はそれを解決するために問題を抱えていますが、これはスレッドプールに非常によく似ています。プールのサイズの管理に関する情報を入手したり、リソースを見つけたりしたいと思っていました。
私が以下を持っているとしましょう:
_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 { }
_
システムについてのいくつかの事実はここにあります:
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
を拡張するにはどうすればよいでしょうか。
ここでは少し試行錯誤する必要があるようですが、私が試みる戦略は、実行する必要のある作業に対して少し統計分析を実行し、その情報を使用して必要に応じてスレッド数を増減することです。
つまり、キュー内の現在の項目数を時折(頻繁にスリープ状態で)ポーリングして処理する以外は何もしないスレッドが必要です。この数が以前にポーリングされた数よりも多い場合は、アクティブワーカーの数を1つ増やすことを検討してください。処理するキュー内のアイテム数が減少している場合は、アクティブワーカーの数を1つ減らすことを検討してください。ワーカーの数は変わらないはずですが、単にワーカーがジョブを終了したときに、別のワーカーに割り当てます。
より低いとは、ワーカースレッドの理想的な数を1つ減らし、より多くのワーカーを作成するのではなく、1つが自然に終了するまで待ってから、非アクティブにすることを意味します。ジョブが完了する前にこの数が再び増加した場合、ワーカースレッドの数を増やすと、以前の数のワーカースレッドに戻るだけで、何も変化しません。
おそらく、新しいワーカースレッドを生成する必要があるかどうかの適切な指標は、ワーカーを最後に作成または破棄したときにジョブの数が10%増加または減少した場合です。もちろん、この割合はあなた次第です。
ここで変更される要素は次のとおりです。
いくつかのテストを行うことで、要求に応じて増加し、要求が満たされると減少する適切な動的スレッドプールを実現できるでしょう。これを特にトリッキーにする唯一のことは、スレッドの作成/破棄に必要な時間です。これが特に高価であることがわかった場合は、より高い許容誤差を追加してください。そうでない場合は、毎回作成または破棄されるスレッドの数を1ではなく2に増やすことを検討してください。
幸運を!