雑誌のテーマからプロジェクト実践へ
該当記事に関連するサービス・技術ページ
なぜ REST における「ハイパフォーマンス」が Delphi でしばしば並列処理に敗れるのか
High Performance REST Server Delphi は実務では、リクエストあたりの純粋なCPU時間で制約されることは稀であり、むしろ制御されていない並列性が原因になります:同時リクエストが多すぎる、同時データベースクエリが多すぎる、あるいはブロッキングI/O(ファイル、ネットワーク、データベース)。その結果は「少し遅くなる」程度ではなく連鎖的に悪化します:スレッド増加、待ち行列の肥大、コネクションプールの崩壊、レイテンシの上昇、クライアント側のタイムアウト、最終的にはサーバが外見上は「生きている」ものの安定した応答を返せなくなる、という状態です。
対策は単一のトリックではなく、意図的な過負荷時の挙動です:サーバが限界に達したときは、リクエストを無限に待たせるのではなく、早期かつ決定論的に拒否する必要があります(典型的にはHTTP 429または503)。このソーススニペットはまさにそのためのものです:軽量なConcurrency-Gate(Semaphore)とタイムアウトを組み合わせた実装で、Indy、WebBroker、Horse、あるいは独自のHTTP層を使用しているかに関わらず既存のRESTエンドポイントに組み込めます。
アーキテクチャのアイデア:高負荷処理の前にConcurrency-Gateを置く
基本的な考えは単純です:高負荷処理(データベースアクセス、複雑なレポート、大きなJSON応答)の前にSemaphoreからトークンを確保します。トークンが利用できない場合は、即座に制御された応答を返します。重要なのは:このゲートは確実に解放されなければならない(try/finally)こと、そしてゲートは実際にコストのかかるコードパスに配置する必要があるという点です—リクエストハンドラの先頭だけに置き、その後にパーサ/ルータ/認証が続くような配置は不十分です。
こうすることで負荷を「取り除く」のではなく整流します:サーバは同時に処理するリクエスト数を減らしますが、その代わりレイテンシは安定します。個別の企業向けアプリケーションでは、合成ベンチマークでの一時的な最高タイムよりも、こちらの方が重要であることが多いです。
ソーススニペット:タイムアウト、429/503、テレメトリフックを備えたリクエストリミッター
以下の Delphi コードは、クラス TRestRequestGate として Concurrency-Gate を実装しています。これは TSemaphore に基づいています(System.SyncObjs より;Semaphore は同時アクセスを制限するためのカウンタです)。ゲートの呼び出しは「“Lease”オブジェクト」(RAII に類似し、解放はデストラクタで行われます)を返すか、即時の過負荷応答を選択します。さらに、運用中にどのような理由でリクエストが拒否されたかを確認できるよう、ロギング/モニタリング用のフックも用意されています。
unit RESTRequestGate;
interface
uses
System.SysUtils,
System.Classes,
System.SyncObjs,
System.Diagnostics;
type
// ログ/トレース用の最小コンテキスト。例: User/Route 等で拡張可能。
TRESTGateContext = record
RequestId: string;
Route: string;
RemoteIp: string;
end;
TRESTOverloadDecision = (odAccepted, odRejectedBusy, odRejectedTimeout);
// 運用テレメトリ用のフック(例: ファイル、Syslog、Prometheus-Exporter 等)。
TRESTGateEvent = reference to procedure(const Ctx: TRESTGateContext;
Decision: TRESTOverloadDecision;
WaitedMs: Integer;
InFlight: Integer);
// リースオブジェクト: デストラクタでトークンを解放。
TRESTGateLease = class
private
FSemaphore: TSemaphore;
FInFlightCounter: PInteger;
FReleased: Boolean;
public
constructor Create(ASem: TSemaphore; ACounter: PInteger);
destructor Destroy; override;
procedure Release;
end;
TRESTRequestGate = class
private
FSem: TSemaphore;
FMaxInFlight: Integer;
FInFlight: Integer;
FOnEvent: TRESTGateEvent;
public
constructor Create(AMaxInFlight: Integer);
destructor Destroy; override;
// TimeoutMs = 0: 待機せず即時 429/503
function TryAcquire(const Ctx: TRESTGateContext; TimeoutMs: Cardinal;
out Lease: TRESTGateLease;
out WaitedMs: Integer;
out Decision: TRESTOverloadDecision): Boolean;
property OnEvent: TRESTGateEvent read FOnEvent write FOnEvent;
property MaxInFlight: Integer read FMaxInFlight;
function InFlight: Integer;
end;
implementation
uses
System.Math;
{ TRESTGateLease }
constructor TRESTGateLease.Create(ASem: TSemaphore; ACounter: PInteger);
begin
inherited Create;
FSemaphore := ASem;
FInFlightCounter := ACounter;
FReleased := False;
end;
destructor TRESTGateLease.Destroy;
begin
Release;
inherited;
end;
procedure TRESTGateLease.Release;
begin
if FReleased then
Exit;
FReleased := True;
// まずカウンタを減算し、その後セマフォを解放。
TInterlocked.Decrement(FInFlightCounter^);
FSemaphore.Release;
end;
{ TRESTRequestGate }
constructor TRESTRequestGate.Create(AMaxInFlight: Integer);
begin
inherited Create;
if AMaxInFlight <= 0 then
raise EArgumentException.Create('AMaxInFlight は > 0 でなければなりません');
FMaxInFlight := AMaxInFlight;
FInFlight := 0;
// InitialCount = MaxCount = AMaxInFlight
FSem := TSemaphore.Create(nil, AMaxInFlight, AMaxInFlight, '');
end;
destructor TRESTRequestGate.Destroy;
begin
FSem.Free;
inherited;
end;
function TRESTRequestGate.InFlight: Integer;
begin
Result := TInterlocked.CompareExchange(FInFlight, 0, 0);
end;
function TRESTRequestGate.TryAcquire(const Ctx: TRESTGateContext; TimeoutMs: Cardinal;
out Lease: TRESTGateLease; out WaitedMs: Integer; out Decision: TRESTOverloadDecision): Boolean;
var
Sw: TStopwatch;
WaitRes: TWaitResult;
CurrentInFlight: Integer;
begin
Lease := nil;
WaitedMs := 0;
Decision := odRejectedBusy;
Sw := TStopwatch.StartNew;
if TimeoutMs = 0 then
WaitRes := FSem.WaitFor(0)
else
WaitRes := FSem.WaitFor(TimeoutMs);
WaitedMs := Integer(Min(Sw.ElapsedMilliseconds, High(Integer)));
case WaitRes of
wrSignaled:
begin
CurrentInFlight := TInterlocked.Increment(FInFlight);
Lease := TRESTGateLease.Create(FSem, @FInFlight);
Decision := odAccepted;
Result := True;
if Assigned(FOnEvent) then
FOnEvent(Ctx, Decision, WaitedMs, CurrentInFlight);
end;
wrTimeout:
begin
// wrTimeout は TimeoutMs > 0 の場合: 意図的に待機するが上限を設ける。
Decision := odRejectedTimeout;
Result := False;
if Assigned(FOnEvent) then
FOnEvent(Ctx, Decision, WaitedMs, InFlight);
end;
else
begin
// wrAbandoned/エラーケース: 保守的に拒否する
Decision := odRejectedBusy;
Result := False;
if Assigned(FOnEvent) then
FOnEvent(Ctx, Decision, WaitedMs, InFlight);
end;
end;
end;
end.目的: 「同時に全部」ではなく負荷下での安定性
MaxInFlightで「高コスト部分」に同時に入るリクエスト数を定義します。ここでの指標は意図的に「CPUコア数」ではなく運用上の設計値です。データベース負荷の高いエンドポイントでは、MaxInFlightをDB接続プールに対する比率で設定するのが有効なことが多いです(例えば Pool = 20、MaxInFlight = 12〜16)。これにより各リクエストが接続を占有してしまい、さらなるスレッドが待機状態になる事態を避けられます。
前提条件と落とし穴
- Try/Finallyは必須: リースは必ず解放されなければなりません。エンドポイントで例外が発生した場合に解放されないとゲートが「穴あき」になり、サーバーが永久に「busy」の状態になります。
- タイムアウトは適切に設定:
TimeoutMs=0は厳格な制限(即時拒否)です。短いタイムアウト(一般的に50〜150 ms)はピークを平滑化し、実際の待ち行列を構築せずに済みます。 - ゲートの位置に注意: 認証(例: Bearer/JWT)やルーティングは前段で済ませたほうがよい場合があります;セマフォは真に高コストな処理の手前に置くべきです。逆に認証処理自体が外部IDシステム等に対して重い場合は、そちらも制限対象にする必要があります。
- 429 と 503 の使い分け: HTTP 429(「Too Many Requests」)はクライアントがリトライを意図する想定のときに適しています。503(「Service Unavailable」)はサービスが一時的にまともにリクエストを受けられない場合に適します。どちらの場合も
Retry-Afterヘッダーの付与を推奨します。
Integration in REST-Handler: Indy/WebBroker/Horse pragmatisch
このスニペットは意図的にフレームワーク非依存にしています。リクエストが「通過する」場所があれば組み込めます。典型はグローバルなシングルトンか、ルートグループ毎のゲート(例: 「/reports」は小さめ、「/health」はゲートなし)です。組み込みの典型パターンは次の通りです:
- コンテキストを設定(RequestId、Route、RemoteIp)
- 短いタイムアウトで
TryAcquireを行う - 拒否されたら即座にレスポンスを書き(429/503)、処理を終了する
- リーースは高コスト部分が終わるまでスコープ内で保持する(Lease lebt im Scope bis nach dem teuren Teil)
Horse(ミドルウェア)ではゲートはルートグループに近い位置に置きます。WebBrokerでは各アクションハンドラ内で扱えます。Indyではリクエストごとにスレッドを割り当てるかどうかによりますが、いずれにせよ高コスト部分を明確に限定すればゲートは有効に働きます。
High Performance REST Server Delphi: Overload-Antworten, die Clients nicht „vergiften“
過負荷時の応答は単なるステータスコード以上の意味を持ちます。クライアントが429/503で攻撃的に即リトライを繰り返すとリトライの嵐が発生します。モバイルアプリ、C# Services、レガシークライアントなどが混在する環境では一貫した挙動が有効です:
- Retry-After: エンドポイントに応じて例えば1〜3秒等。明確な再試行の間隔を示します。
- 短いボディ: 小さなJSON(例:
{"error":"server_busy","requestId":"..."})で十分です。大きなエラーオブジェクトは再びCPUと帯域を消費します。 - Health-Endpointは制限しない: 監視は負荷時でも情報を返すべきです(必要に応じて「degraded」フラグを含める)。
前段に nginx のようなリバースプロキシを置いている場合は、そこでのタイムアウトやバッファリングを調整してください。プロキシは(TLS-Termination、Keep-Alive などで)負荷を軽減できますが、大きなリクエストボディをバッファして負荷を後段に移すこともあります。運用上重要なのは制限値の一貫性です: Proxy-Timeout > App-Timeout でなければ、アプリが適切に拒否したにもかかわらずクライアント側に「Gateway Timeout」が見えてしまいます。
Threading、DBプール、Keep-Alive:実務で破綻するポイント
Gateは「同時に多すぎる」問題を解消しますが、単一のリクエストが過度に多くのリソースを占有するのを自動的に防ぐわけではありません。Delphiプロジェクトで典型的に発生する3つの破綻ポイントは、まさにThreading、データベース、HTTP接続のインターフェースにあります:
- 単一リクエストが複数の希少リソースをブロックする: まずDB接続、次に外部へのHTTPコール、さらにファイルアクセス。これらが同一のリクエストスレッド内で発生すると、ブロック時間が累積します。Gateは並列度を制限しますが、スループットは大幅に低下します。ここでは依存関係の切り離し(例:外部コールの非同期化、ジョブキューによる事前計算)が有効です。
- BDE-Ablosung mit nativer Anbindung-Pooling und Transaktionen: FireDACはコネクションをプールできますが、いわゆる「長い」トランザクション(例:JSON作成やビジネスチェックがStartTransactionとCommitの間に入る場合)は接続を不必要に占有します。実務的な対策は、トランザクションを実際のステートメントの周りにできるだけ狭く適用し、可能であればトランザクション外で直列化や検証を行うことです。
- HTTP Keep-Alive:隠れたメモリ消費要因: Keep-Aliveはハンドシェイクを削減しますが、多数のアイドルクライアントがいるとオープンソケットが増えます。特にWindows- und Linux-Servicesでは「CPUが上がる」ではなく「Handles/FDsが枯渇する」やバッファによるRAM消費が顕在化します。サーバーおよびリバースプロキシで明確なアイドルタイムアウトを設定し、環境が許すならクライアントIPごとの上限を設けることが有効です。
結論:MaxInFlightは静的な値ではありません。最も遅い、最も逼迫したリソース(DB、外部システム、ストレージ)と、リクエストがそれらのリソースをどれだけ「保持」してしまうかに依存します。
Gate横のパフォーマンスレバー:JSON、DB、I/Oを混在させない
Gateは安定化をもたらしますが、エンドポイント設計の良さに代わるものではありません。Delphi RESTサーバーで繰り返し見られる3つのボトルネックは次の通りです:
- 不必要な中間文字列を伴うJSON生成: 多数の一時的なUnicode文字列によって負荷が発生することがよくあります。可能な限りストリーミング指向(Writer/Stream)で構築し、大きな中間オブジェクトを避けること、特にリスト系エンドポイントで有効です。
- アイテムごとのデータベースアクセス: N+1クエリや行単位のルックアップは典型的な問題です。対策は目的に応じたJOIN、バッチクエリ、サーバー側集約です。非常に大きな結果セットでは、ページングと安定したソートを併用するとページの「ずれ」を防げます。
- リクエストスレッド内のブロッキングI/O: ファイルアクセスや外部HTTPコールは、厳格に制限するか非同期パイプラインに移行してください。さもなければ高価なスレッドを「待ち」で占有してしまいます。
成長した企業向けデジタルソリューションではここが肝になります:エンドポイントが「ちょっと追加」で動作していても、実負荷やデータ量が増えると限界が露呈します。そのときにデータアクセス層、キャッシング、バルク戦略、明確なタイムアウトなど、アーキテクチャ上の境界が適切に引かれているかが試されます。
デバッグと運用:測定すべき項目
Hook OnEventは意図的にシンプルです。実運用では少なくとも次の値を収集してください:
- InFlight (Gateにおける現在の並列数)
- WaitedMs (どの程度「キューイング」を許容したか)
- Decision (accepted/busy/timeout)
- Route/RemoteIp(大まかな原因分析、データ保護を無視せず)
これにより、制限が厳しすぎるか(429が多発)あるいは緩すぎるか(高い WaitedMs、レイテンシーの上昇)を把握できます。また、特定のルートが支配的かどうかも確認できます。Windows-および Linux-Services にとっては日常的に重要です:テレメトリがなければ、パフォーマンス問題はネットワーク、データベース、プロキシ、アプリケーションのどこに原因があるかの当て推量になりやすいからです。
一見珍しいが極めて有用:「WaitedMs」を早期警告指標として
多くのチームは応答時間と CPU のみを監視します。WaitedMs はしばしばより有効な指標です。なぜならリクエストが実際の処理の前に既に待機していることを示すからです。WaitedMs が上昇し CPU が中程度に留まる場合、ボトルネックは多くの場合 CPU ではなく、接続プール(DB 接続)、ビジネスロジック内のロック、または外部のダウンストリームサービスです。これにより原因分析の時間を節約できます。なぜなら「コンパイラ最適化」ではなく「プール/ロック/I/O」の方向に的を絞って調査できるからです。
バリエーション:ルートごとのゲート、優先順位、そして「Fast Lane」
すべてを一つのゲートで扱うのは簡単ですが、常に最適とは限りません。実用的なバリエーション:
- ルートグループごとのゲート:「/reports」は厳格に、「/api/orders」は中程度に、「/health」は開放。これにより高コストなレポートリクエストがコアプロセスを圧迫するのを防げます。
- 管理/モニタリング用の Fast Lane:小さな並列度を持つ別ゲートを設けることで、負荷時でも運用作業が可能になります。
- バジェットベースの制限:レスポンスサイズが大きく変動する場合、バイト単位の予算(例:同時に生成される合計で最大 X MB)を追加することが有効です。複雑にはなりますが、大容量ダウンロードが発生する環境では現実的な手法です。
重要:優先順位付けは容易に政治的な問題(「私のエンドポイントの方が重要だ」)になります。技術的に安定させるには、優先順位を役割や部署ではなくプロセスに紐付けること(例:受注処理をレポーティングより優先)です。
結論:ゲートは有効か — どこでこの手法は破綻するか?
同時実行ゲートは、Delphi における高性能 REST サーバの実務的な構成要素です。オーバーロードを制御可能にし、ピーク時のシステムを安定化させるからです。データベース依存のエンドポイントがある場合、リバースプロキシが前段にある場合、あるいは複数のクライアント(レガシー、ポータル、サービス)が波状的に負荷を発生させる場合には特に有効です。
限界は明確です:リクエストごとの実処理が高コスト(非効率なクエリ、大きな JSON オブジェクト、ブロッキングする外部システム)である場合、ゲートは症状を覆い隠すだけになります。その際はデータアクセス、キャッシング戦略、タイムアウト、必要に応じて非同期処理(Queue/Job システム)を改善する必要があります。ただし運用上の安全帯としてゲートはしばしば「多少遅い」か「完全に使い物にならない」かの差をもたらします。
既存の Delphi REST-API および REST-サーバ にオーバーロード制御を導入する、または制限をデータベースやプロキシのタイムアウトと整合させて調整したい場合は、Net-Base とプロジェクトまたはモダナイゼーションの案件を相談してください。
業務的な文脈では、統合、データフロー、今後の開発を整合させる必要がある場合に、スレッドプール Delphi と HTTP 429 Too Many Requests も重要な要素となります。
次のステップ
テーマが実際のプロジェクトになる場合、アーキテクチャ、既存資産、運用は早い段階でまとめて検討するべきです。
私たちは単なる個別の問い合わせへの対応にとどまらず、ソースの断片やレガシー課題、ポータルの構想が堅牢な企業向けプロジェクトへと成長する段階まで支援します。
- 既存環境、目標像、技術的リスクを一体として評価します。
- REST、データアクセス、ポータル、ロールアウトは後工程として先送りされることはありません。
- 早期に、どのアプローチが経済的かつ運用面で実行可能かを判断できます。