Net-Base מגזין

06.06.2026

שרת REST בעל ביצועים גבוהים ב-Delphi: מגבלות בקשות, Thread-Pool והתנהגות מבוקרת בעת עומס יתר (קטע מקור)

שרת High Performance REST ב-Delphi אינו מהיר רק בגלל "JSON מהיר", אלא בזכות מקביליות מבוקרת, גבולות זמן נוקשים והתנהגות עומס מסודרת. מאמר זה מציג Concurrency-Gate פרקטי עם Semaphore, תגובות HTTP 429/503...

06.06.2026

מהנושא במגזין ליישום בפרויקט

דפי שירות וטכניים רלוונטיים למאמר

מדוע „High Performance“ ב-REST ב-Delphi נכשלת לעתים קרובות בגלל מקביליות

שרת High Performance REST Delphi מוגבל במציאות לעתים נדירות על ידי זמן CPU טהור לכל בקשה, אלא על ידי מקביליות בלתי נשלטת: יותר מדי בקשות בו-זמנית, יותר מדי שאילתות למסד הנתונים בו-זמנית או I/O חוסם (קובץ, רשת, מסד נתונים). התוצאה אינה מרגישה כ“קצת יותר איטי“, אלא כתהליך שרשרת: יותר threads, תורים ארוכים יותר, קריסת Connection-Pool, עלייה בהשהיה (latency), חריגי זמן בצד הלקוח ולבסוף שרת שעדיין „חי“ אך כבר אינו מספק תשובות יציבות.

הפתרון אינו תחבולה בודדת אלא התנהגות מודעת של Overload-Verhalten: כשהשרת מגיע לגבולותיו, עליו לדחות מוקדם ובאופן דטרמיניסטי (בדרך כלל HTTP 429 או 503), במקום לאפשר לצבור בקשות בתור אינסופי. בדיוק לזה מיועד קטע המקור הזה: שער מקביליות קל־משקל (Semaphore) בתוספת timeouts, שניתן לשלבו ב‑REST‑endpoints קיימים — ללא תלות האם אתם משתמשים ב‑Indy, WebBroker, Horse או בשכבת HTTP משלכם.

Architekturidee: Concurrency-Gate vor dem „teuren Teil“

רעיון היסוד פשוט: לפני החלק היקר (גישה למסד הנתונים, דוחות מורכבים, תשובות JSON גדולות) מוזמן טוקן מתוך Semaphore. אם אין טוקן פנוי, מוחזרת מייד תשובה מבוקרת. חשוב: יש לשחרר את השער באופן מהימן (try/finally), והוא צריך להיות בנתיב הקוד שממש יקר — לא רק בתחילת ה‑request handler, כאשר לאחר מכן עדיין מתבצעים parser/router/authentifizierung.

כך העומס לא „מוסר“ אלא מנותב: השרת עונה לפחות בקשות בו‑זמנית, אך עם השהיות יציבות יותר. ביישומי ארגון מותאמים הדבר בדרך כלל בעל ערך רב יותר מאשר זמני שיא ספורדיים בבדיקות ביצועים סינתטיות.

Source-Schnipsel: Request-Limiter mit Timeout, 429/503 und Telemetrie-Hooks

הקוד Delphi שלהלן מממש שער מקביליות ככיתה TRestRequestGate. הוא מבוסס על TSemaphore (מה־System.SyncObjs; Semaphore הוא מונה למספר מוגבל של גישות מקבילות). קריאת השער מחזירה או אובייקט „Lease“ (בדומה ל‑RAII: שחרור ב‑Destructor) או מקבלת החלטה להחזיר תגובת עומס מיידית. בנוסף קיימים Hooks ל‑Logging/Monitoring, כדי שתוכלו בתפעול לראות מדוע בקשות סורבו.

Delphi
unit RESTRequestGate;

interface

uses
  System.SysUtils,
  System.Classes,
  System.SyncObjs,
  System.Diagnostics;

type
  // הקשר מינימלי ל-Logging/Tracing; ניתן למשל להרחיב ב-User/Route.
  TRESTGateContext = record
    RequestId: string;
    Route: string;
    RemoteIp: string;
  end;

  TRESTOverloadDecision = (odAccepted, odRejectedBusy, odRejectedTimeout);

  // Hook לטלמטריה תפעולית (למשל בקובץ, Syslog, Prometheus-Exporter וכו').
  TRESTGateEvent = reference to procedure(const Ctx: TRESTGateContext;
                                         Decision: TRESTOverloadDecision;
                                         WaitedMs: Integer;
                                         InFlight: Integer);

  // אובייקט Lease: שחרור הטוקן ב-Destructor.
  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;

  // תחילה הפחתת ה-Counter, ואז שחרור ה-Semaphore.
  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 אתם מגדירים כמה Requests יכולים להיכנס בו־זמנית לחלק ה„יקר“. זה במודע אינו „מספר ליבות מעבד“, אלא פרמטר תפעולי. בנקודות קצה עם עומס מסד נתונים לעיתים כדאי לכוונן את MaxInFlight ביחס ל-DB-Connection-Pool (למשל Pool = 20, MaxInFlight = 12 עד 16), כדי שלא כל Request יחסום חיבור ואז יסחפו אחריו Threads נוספים.

תנאים ומלכודות

  • Try/Finally ist Pflicht: יש לשחרר את ה-Lease באופן מובטח. אם יהיו Exceptions ב-Endpoint, ה-Gate יהפוך ל„דולף“ והשרת יישאר בקביעות במצב „busy“.
  • בחירת Timeout באופן הגיוני: TimeoutMs=0 הוא גבול קשיח (דחייה מיידית). Timeout קצר (טיפוסי 50 עד 150 ms) מרכך שיאים מבלי לבנות תורים ממשיים.
  • לא להפעיל את ה-Gate מוקדם מדי: אימות (למשל Bearer/JWT) או ניתוב יכולים להיות מתאימים; ה-Semaphore צריך לתפוס את הכניסה לפני החלק היקר באמת. להיפך: אם האימות יקר (למשל מול מערכת זהות חיצונית), גם אותו יש להגביל.
  • 429 מול 503: HTTP 429 („Too Many Requests“) מתאים כשלקוחות אמורים לבצע retry ממוקד. 503 („Service Unavailable“) מתאים כאשר השירות זמנית אינו מסוגל לקבל בקשות באופן מועיל. בכל מקרה מומלץ לכלול כותרת Retry-After.

אינטגרציה ב-REST-Handler: פתרון פרגמטי ל-Indy/WebBroker/Horse

ה-Snippet ניטרלי מבחינת framework במודע. אתם צריכים רק מקום שבו Requests „עוברים“. אופייני הוא Singleton גלובלי או Gate לכל קבוצת נתיבים (למשל „/reports“ קטן יותר, „/health“ ללא Gate). להלן דוגמת ההטמעה כמודל:

  • מילוי הקשר (RequestId, Route, RemoteIp)
  • TryAcquire עם Timeout קצר
  • במקרה של דחייה כתוב Response מיד (429/503) וסיים
  • ה-Lease נמשך בתוך ה-scope עד אחרי החלק היקר

ב-Horse (Middleware) ה-Gate ממוקם קרוב לקבוצת נתיבים. ב-WebBroker תוכלו לעבוד בתוך ה-Action-Handler המתאים. ב-Indy זה תלוי אם יש לכם Thread לכל Request; ה-Gate עדיין פועל כל עוד המקטעים היקרים מוגבלים בצורה נקייה.

שרתים High Performance REST Delphi: תגובות עומס שאינן „מרעילות“ את הלקוחות

תגובות על עומס הן יותר ממקודי סטטוס. אם לקוחות על 429/503 ישלחו באופן אגרסיבי ויידי, תתקבל סופת retries. בסביבות מערכות הטרוגניות (Mobile Apps, C# Services, Legacy-Clients) התנהגות עקבית מסייעת:

  • Retry-After: למשל 1 עד 3 שניות, תלוי ב-Endpoint. זה מסמן קצב ברור.
  • גוף קצר: JSON קטן כמו {"error":"server_busy","requestId":"..."} מספק. אובייקטי שגיאה גדולים צורכים שוב CPU ורוחב פס.
  • Health-Endpoint ללא דילול: ה-Monitoring צריך להמשיך לספק נתונים גם בעומס (אופציונלי עם הדגל „degraded“).

אם אתם מפעילים Reverse Proxy כגון nginx מקדימה: כווננו שם Timeouts ו-Buffering. Proxy יכול להקל (TLS-Termination, Keep-Alive), אך גם להזיז עומס (למשל לאחסן ב-buffer בקשות גוף גדולות). בתפעול חשוב שהגבולות יהיו עקביים: Proxy-Timeout > App-Timeout, אחרת הלקוחות יראו „Gateway Timeout“, אף על פי שהאפליקציה הייתה אמורה לדחות כראוי.

Threading, DB-Pools ו-Keep-Alive: איפה זה מתמוטט בפועל

ה-Gate פותר את בעיית „יותר מדי במקביל“, אבל הוא לא מונע אוטומטית שבקשה בודדת תקשור משאבים רבים מדי. שלוש נקודות שבר טיפוסיות מפרויקטים של Delphi נוצרות בדיוק בממשקים בין Threading, מסד-נתונים ו־HTTP-חיבורים:

  • בקשה חוסמת מספר משאבים מצומצמים: ראשית חיבור DB, אחר כך קריאת HTTP חיצונית, ואז גישה לקובץ. אם כל זה קורה באותו Request-Thread, זמני החסימה מתרבים. ה-Gate אמנם מגביל את המקבילות, אך התפוקה צונחת באופן דרמטי. כאן כדאי לפרק את התלויות (למשל: קריאות חיצוניות אסינכרוניות, חישוב מקדים באמצעות Job-Queue).
  • BDE-החלפה עם חיבור מקורי-Pooling und Transaktionen: BDE-Ablosung mit nativer Anbindung תומכת ב-Connection pooling, אבל טרנזקציה „ארוכה“ (למשל כשיצירת JSON או בדיקות עסקיות נמצאות בין StartTransaction ל-Commit) מחזיקה את החיבור ללא צורך. שיטה נקייה היא לצמצם את הטרנזקציה ככל האפשר סביב הסטייטמנטים עצמם ולבצע מחוץ לטרנזקציה סיריאליזציה או אימות, כאשר זה אפשרי מבחינה מקצועית.
  • HTTP Keep-Alive כזולל זיכרון סמוי: Keep-Alive מפחית Handshakes, אך יכול להוביל לעודף סוקט פתוח כאשר יש הרבה קליינטים במצב idle. במיוחד ב-Windows- ו-Linux-Services לא רואים „עליית CPU“, אלא „Handles/FDs מלאים“ או שימוש בזיכרון בגלל buffers. כאן עוזרים Idle-Timeouts ברורים בשרת וב- Reverse Proxy וכן הגבלה לכל Client-IP, אם הסביבה מאפשרת.

המסקנה: MaxInFlight אינו ערך סטטי. הוא תלוי במשאב האיטי והמצומצם ביותר שלכם (DB, מערכות חיצוניות, אחסון) ובמידת היכולת של בקשה „להחזיק“ את אותם משאבים.

מנופי ביצועים לצד ה-Gate: לא לערבב JSON, DB ו-I/O

ה-Gate מייצב, אבל הוא לא מחליף כלכלת Endpoint נקייה. שלושה צווארי בקבוק בשרתים של Delphi REST מופיעים שוב ושוב:

  • בניית JSON עם מחרוזות ביניים מיותרות: לעיתים קרובות העומס נוצר מריבוי מחרוזות Unicode זמניות. היכן שאפשר, לבנות בכיוון סטרימינג (Writer/Stream) במקום ליצור אובייקטי ביניים ענקיים, במיוחד ב-endpoints של רשימות.
  • גישה למסד נתונים „לפריט“: N+1-Queries ו-per-Row Lookups הן הקלאסיקה. עדיף: joins ממוקדים, batch-queries, אגgregציה בצד השרת. לתוצאות גדולות מאוד כדאי גם Pagination עם מיון יציב (כדי שהעמודים לא „יקפצו“).
  • I/O חוסם ב-Request-Thread: גישות לקבצים או קריאות HTTP חיצוניות יש להגביל קפדנית או להעביר לצינור אסינכרוני. אחרת אתם חוסמים ת’רדים יקרים עבור „המתנה“.

עבור פתרונות ארגוניים דיגיטליים שהתפתרו לאורך זמן זה לעתים הקריטי: Endpoint נוסף „מהר“ ועובד, עד שמגיעים עומסים ונפחי נתונים אמיתיים. אז מתגלה האם גבולות הארכיטקטורה צוירו בצורה נקייה (שכבת גישה לנתונים, Caching, Bulk-אסטרטגיות, Timeouts ברורים).

Debugging und Betrieb: Was Sie messen sollten

ה-Hook OnEvent מתוכנן בפשטות בכוונה. בפרקטיקה כדאי ללכוד לפחות את הערכים הבאים:

  • InFlight (רמת המקבילות הנוכחית ב-Gate)
  • WaitedMs (כמה „תור“/המתנה אתם מאפשרים)
  • Decision (accepted/busy/timeout)
  • Route/RemoteIp (ניתוח ס Ursachen ראשוני, בלי להתעלם מהגנת הפרטיות)

כך תקבלו איתות האם המגבלות הדוקות מדי (יותר מדי 429) או רופפות מדי (גבוהות WaitedMs, עלייה בזמני השהיה). ותראו האם נתיבים יחידים דומיננטיים. עבור Windows- ו-Linux-Services זה חיוני ביום-יום: בלי טלמטריה בעיית ביצועים מהפכת במהרה למשחק ניחושים בין הרשת, מסד הנתונים, הפרוקסי והיישום.

Ungewöhnlich, aber extrem hilfreich: „WaitedMs“ als Frühwarnindikator

צוותים רבים מסתכלים רק על זמן תגובה ו-CPU. WaitedMs הוא לעתים קרובות האינדיקטור הטוב יותר, כי הוא מראה שהבקשות כבר מחכות לפני העבודה עצמה. אם WaitedMs עולה בעוד ה-CPU נשאר מתון, המשאב המצומצם לעיתים קרובות אינו ה-CPU אלא Pool (חיבורי DB), נעילה בלוגיקה העסקית או שירות downstream חיצוני. זה חוסך זמן בניתוח סיבות, כי תוכלו לחפש ממוקדת יותר בכיוון „Pool/Lock/I/O“ במקום „אופטימיזציית קומפיילר“.

Varianten: Pro-Route-Gates, Prioritäten und „Fast Lane“

שער אחד לכל התעבורה פשוט, אך לא תמיד אידיאלי. וריאנטים מועילים:

  • Gate לפי קבוצת נתיבים: „/reports“ מחמיר, „/api/orders“ מתון, „/health“ פתוח. כך תמנעו שבקשות דוחות יקרות ידחיקו תהליכי ליבה.
  • Fast Lane עבור Admin/Monitoring: Gate נפרד עם מקביליות קטנה, כדי שפעולות תפעוליות יישארו אפשריות גם תחת עומס.
  • מגבלות מבוססות תקציב: אם גדלי התגובות משתנים במידה רבה, תקציב בבייטים יכול לעזור בנוסף (למשל מקסימום X MB בו-זמנית ביצירה). זה מורכב יותר, אך מציאותי בהורדות גדולות.

חשוב: מתן עדיפויות נהפך במהירות לנושא פוליטי („ה-endpoint שלי חשוב יותר“). מבחינה טכנית זה נשאר יציב אם העדיפויות מקושרות לתהליכים (למשל קליטת הזמנות לפני דיווח), ולא לתפקידים או מחלקות.

Fazit: Lohnt sich das Gate – und wo kippt der Ansatz?

Concurrency-Gate הוא בלק בלוק פרגמטי לשרת High Performance REST ב-Delphi, כיוון שהוא הופך Overload לניתן לשליטה ושומר על יציבות המערכות בעומסי שיא. זה משתלם במיוחד אם יש לכם endpoints התלויים במסד נתונים, אם עומד לפניו Reverse Proxy או אם מספר לקוחות (Legacy, פורטלים, שירותים) מייצרים עומס בגלים.

הגבולות ברורים: אם העבודה עצמה לכל בקשה יקרה מדי (שאילתות לא יעילות, אובייקטי JSON גדולים, מערכות חיצוניות חוסמות), ה-Gate רק מסתיר תסמינים. אז יש לטפל בגישה לנתונים, אסטרטגיות קאשינג, Timeouts ובמידת הצורך בעיבוד אסינכרוני (Queue/Job-System). כ“חגורת בטיחות“ בתפעול ה-Gate הוא לעתים ההבדל בין „קצת איטי“ ל“חסר תועלת לחלוטין“.

אם אתם רוצים להטמיע התנהגות Overload ב-Delphi REST-API und REST-Server קיימים או לאזן בצורה נקייה מגבלות מול זמן-פסק של מסד נתונים ופרוקסי: שוחחו על פרויקט או מהלך מודרניזציה עם Net-Base.

בהקשר המקצועי גם Thread-Pool Delphi ו-Http 429 Too Many Requests משחקים תפקיד חשוב, כאשר אינטגרציות, זרמי נתונים ופיתוח המשך חייבים לשחק יחד בצורה נקייה.

דונו בפרויקט או במהלך מודרניזציה עם Net-Base.

השלב הבא

כאשר הנושא הופך לפרויקט ממשי, יש לבחון כבר בשלב מוקדם את הארכיטקטורה, המצב הקיים והתפעול יחד.

אנו תומכים לא רק בשאלות נקודתיות, אלא גם כשמקטעי קוד מקור, נושאי Legacy או רעיונות פורטל אמורים להפוך לפרויקט ארגוני מהימן ועמיד.

  • המצב הקיים, תמונת היעד והסיכונים הטכניים מוערכים יחד.
  • REST, גישה לנתונים, פורטלים ו-Rollout לא יידחו כתוצאות מאוחרות.
  • אתם מזהים מוקדם איזה נתיב בר-קיימא מבחינה כלכלית ותפעולית.

שתף פוסט

לשתף את הפוסט הזה ישירות

LinkedIn, X, XING, Facebook, WhatsApp ודוא"ל זמינים מיידית. עבור Instagram אנו מכינים קישור וטקסט קצר באופן מיידי.

דוא״ל

אינסטגרם נפתח בכרטיסייה חדשה. הקישור וטקסט קצר מועתקים מראש ללוח.