Circuit Breaker ב-API: איך עוצרים מפולת שלגים במערכות מבוזרות
מאת צוות מדיה דיל · 07.08.2026 · API Engineering · 8 דק׳
כשתלות חיצונית נופלת, כישלון מהיר ומבוקר עדיף על המתנה שמכפילה את הנזק. מדריך מעמיק למכונת המצבים שמאחורי Circuit Breaker ולשילוב שלו עם Retry ו-Timeout.
שירות אימות משתמשים פנימי מתחיל לזחול באיטיות בגלל עומס לא צפוי במסד הנתונים שלו. תוך דקות, כל שירותי המערכת שתלויים בו נתקעים גם הם - כל בקשה ממתינה לתשובה שלא מגיעה, ה-thread pool של כל שירות שקורא אליו מתמלא בבקשות תקועות, וכל המערכת, לא רק השירות המקורי, הופכת ללא זמינה. זו מפולת שלגים קלאסית (Cascading Failure) - תקלה מקומית קטנה שמתפשטת ומכפילה את עצמה עד שהיא מפילה מערכת שלמה. Circuit Breaker הוא המנגנון שנועד לעצור בדיוק את זה: לזהות ששירות מסוים לא מגיב כראוי, ולהפסיק לנסות לקרוא אליו זמנית, במקום להמשיך "לחכות" עד שהכל קורס יחד.
ההשראה מהחשמל - ולמה המטאפורה מדויקת
השם Circuit Breaker שאול ישירות ממאמן חשמל ביתי: כשזרם חריג עובר במעגל, המאמן "נופל" ומנתק את המעגל לגמרי, במקום לתת לזרם החריג להמשיך ולשרוף את החיווט. באותה צורה, Circuit Breaker בתוכנה עוקב אחרי כשלונות בקריאות לשירות מסוים, וברגע שהם עוברים סף מוגדר, הוא "מנתק את המעגל" - כל קריאה נוספת נכשלת מיידית בצד הלקוח, בלי בכלל לנסות לפנות לשירות התקוע. זה נשמע כמו ויתור, אבל זו בדיוק הנקודה: כישלון מהיר ומבוקר (Fail Fast) עדיף לאין שיעור על המתנה ארוכה שמכפילה את הנזק ומונעת מהשירות הבעייתי כל סיכוי להתאושש.
שלושת המצבים - Closed, Open, Half-Open
הליבה של המנגנון היא מכונת מצבים (State Machine) עם שלושה מצבים:
- Closed - המצב הרגיל. בקשות עוברות כרגיל לשירות היעד, וה-Circuit Breaker סופר כשלונות ברקע. כל עוד שיעור הכשלונות מתחת לסף, שום דבר לא משתנה.
- Open - ברגע שהסף נחצה, המעגל "נפתח". כל בקשה חדשה נכשלת מיידית בצד הלקוח (בדרך כלל עם שגיאה ייעודית או תשובת Fallback), בלי כל ניסיון לפנות לשירות היעד בכלל. זה נמשך פרק זמן קבוע מראש (Timeout).
- Half-Open - בתום ה-Timeout, המעגל עובר למצב ביניים שמאפשר מספר קטן של בקשות "ניסיון" לעבור בפועל אל השירות. אם הן מצליחות, המעגל חוזר ל-Closed; אם הן נכשלות, הוא חוזר ל-Open לעוד סבב המתנה.
class CircuitBreaker {
constructor(threshold = 5, timeoutMs = 30000) {
this.state = 'CLOSED';
this.failures = 0;
this.threshold = threshold;
this.timeoutMs = timeoutMs;
}
async call(fn) {
if (this.state === 'OPEN') {
if (Date.now() < this.openedAt + this.timeoutMs) {
throw new CircuitOpenError();
}
this.state = 'HALF_OPEN';
}
try {
const result = await fn();
this.onSuccess();
return result;
} catch (err) {
this.onFailure();
throw err;
}
}
onSuccess() { this.failures = 0; this.state = 'CLOSED'; }
onFailure() {
this.failures++;
if (this.failures >= this.threshold) {
this.state = 'OPEN';
this.openedAt = Date.now();
}
}
}
קביעת הסף - ספירת כשלונות מול חלון זמן נע
המימוש הפשוט ביותר סופר כשלונות רצופים - חמישה כשלונות ברצף פותחים את המעגל. זה קל ליישום אבל לא מדויק: אם השירות מצליח פעם אחת מכל שתי בקשות, המונה מתאפס שוב ושוב ואף פעם לא מגיע לסף, למרות ששיעור כשלון 50 אחוז הוא בעיה חמורה. הגישה הבוגרת יותר משתמשת בחלון זמן נע (Sliding Window) - למשל, אחוז הכשלונות מתוך 20 הבקשות האחרונות, או מתוך חלון של 60 שניות. זה נותן תמונה מדויקת יותר של מצב השירות בזמן אמת, במחיר של מורכבות מימוש מעט גבוהה יותר - צריך לשמור היסטוריה קצרה של תוצאות, לא רק מונה יחיד.
מצב Half-Open - למה לא לפתוח את השער בבת אחת
נקודה עדינה שקל לפספס היא כמה בקשות לתת לעבור במצב Half-Open. פתיחת השער לגמרי ברגע שה-Timeout חלף - כלומר לתת לכל הבקשות לעבור בבת אחת - מסוכנת: אם השירות עדיין לא התאושש באמת, זה בדיוק כמו לפתוח את המעגל מחדש בעוצמה מלאה, מה שעלול להכשיל אותו שוב מיידית. הפתרון הנכון הוא להגביל את מספר הבקשות שעוברות במצב Half-Open למספר קטן מאוד (למשל בקשה אחת או שתיים), ולבדוק את תוצאתן לפני שמחליטים לפתוח את השער לגמרי. זה מאפשר "בדיקת דופק" זהירה במקום הימור מלא על שירות שאולי עדיין לא באמת חזר לפעילות תקינה.
Fallback - מה קורה כשהמעגל פתוח
Circuit Breaker שרק זורק שגיאה כשהמעגל פתוח פותר את בעיית ההצפה אבל לא בהכרח נותן את חוויית המשתמש הטובה ביותר. במקרים רבים אפשר להגדיר התנהגות Fallback חכמה יותר - החזרת נתון מה-Cache האחרון הידוע (גם אם הוא לא עדכני לגמרי), החזרת תשובת ברירת מחדל סבירה, או דחיית הפעולה לתור לעיבוד מאוחר יותר כשהשירות יחזור לפעול. הבחירה תלויה לגמרי בהקשר העסקי - עבור שירות המלצות, הצגת המלצות ישנות עדיפה לאין שיעור על מסך ריק; עבור שירות תשלומים, לעומת זאת, אין שום Fallback סביר מלבד דחיית הפעולה בבירור ללקוח, כי אי אפשר "לנחש" תוצאת תשלום.
Circuit Breaker לכל תלות בנפרד - לא מעגל גלובלי אחד
טעות ארכיטקטונית נפוצה היא מימוש Circuit Breaker גלובלי יחיד לכל השירות, במקום מעגל נפרד לכל תלות חיצונית (כל שירות, כל API חיצוני, לפעמים אפילו כל endpoint בנפרד). אם שירות A קורא גם לשירות תשלומים וגם לשירות התראות, וגם ל-CRM חיצוני, כל אחד מהם צריך Circuit Breaker משלו - כשלון בשירות ההתראות לא אמור לחסום בקשות לשירות התשלומים. ספריות בוגרות כמו Resilience4j ב-Java או Polly ב-.NET כבר תומכות במופעים מרובים ומבודדים של Circuit Breaker באופן טבעי, וזו סיבה טובה להעדיף ספרייה קיימת ובדוקה על פני מימוש עצמאי מאפס.
שילוב עם Retry, Timeout ו-Bulkhead
Circuit Breaker לא עובד לבד - הוא חלק ממשפחת דפוסי עמידות (Resilience Patterns) שמשלימים זה את זה. Timeout מוגדר נכון מונע מבקשה בודדת "לתקוע" thread לנצח ולתרום לכשלונות שנספרים ב-Circuit Breaker. Retry מטפל בכשלונות בודדים וזמניים לפני שהם בכלל מגיעים לסף שפותח את המעגל. Bulkhead - בידוד משאבים (כמו thread pool נפרד לכל תלות חיצונית) - מונע ממשאב אחד תקוע לצרוך את כל המשאבים הזמינים למערכת כולה, גם כשה-Circuit עדיין סגור. הרחבה על מנגנון Retry ותקרות זמן זמינה במאמר על ארכיטקטורת Retry ב-API ובמאמר על אסטרטגיית Timeout. השילוב הנכון בין ארבעת הדפוסים - Timeout, Retry, Circuit Breaker ו-Bulkhead - הוא מה שיוצר מערכת שבאמת עמידה בפני תקלות חלקיות, ולא רק שכבה בודדת שמטפלת בחלק מהתמונה.
בדיקות אוטומטיות ל-Circuit Breaker - איך מדמים תקלה בכוונה
הבעיה עם Circuit Breaker היא שהוא נועד להתמודד עם תרחיש שכמעט לעולם לא קורה בסביבת פיתוח - שירות תלוי שנופל לגמרי. בלי בדיקות ייעודיות, הפעם הראשונה שהמנגנון באמת נבדק היא בפרודקשן, בדיוק ברגע שהוא הכי קריטי שיעבוד נכון. הפתרון הוא הזרקת תקלות מבוקרת (Fault Injection) - כלים כמו Toxiproxy מאפשרים לדמות latency גבוה, ניתוקי חיבור או שגיאות שרת באופן שליטתי בסביבת בדיקה, ולוודא שהמעגל אכן נפתח בזמן הצפוי, שה-Fallback מופעל כראוי, ושהמעבר בחזרה ל-Closed קורה בצורה הדרגתית ובטוחה דרך מצב Half-Open. בדיקות כאלה, שרצות כחלק מ-CI או לפחות כתרגיל תקופתי (Chaos Engineering בקנה מידה קטן), הן ההבדל בין לדעת שהמנגנון קיים בקוד לבין לדעת בוודאות שהוא באמת עובד כשצריך.
נראות בזמן אמת - Dashboard למצב כל המעגלים
מעבר לבדיקות, חשוב שלצוות התפעול תהיה תמונה מיידית של מצב כל מעגלי ה-Circuit Breaker במערכת - אילו פתוחים כרגע, מתי הם נפתחו, וכמה בקשות נחסמו בעקבות כך. Dashboard כזה, המבוסס על Metrics שנחשפים מכל מופע Circuit Breaker, הופך תקלה מבוזרת ומבלבלת ("למה השירות שלנו מחזיר שגיאות?") לתמונה ברורה ומיידית ("התלות ב-CRM נפלה לפני שלוש דקות, המעגל פתוח, זו הסיבה"). זה מקצר משמעותית את זמן האבחון בזמן תקלה אמיתית, ומאפשר לצוות להתמקד בטיפול בשירות שבאמת נכשל במקום לחקור מאפס איפה הבעיה.
טעויות נפוצות בפרודקשן
הטעות הראשונה היא סף רגיש מדי - Circuit Breaker שנפתח אחרי כשלון בודד או שניים הופך רועש מדי ופותח את המעגל גם על תנודתיות רגילה ברשת, מה שפוגע בזמינות בלי סיבה אמיתית. השנייה היא Timeout ארוך מדי במצב Open - מעגל שנשאר סגור לדקות ארוכות אחרי שהשירות כבר התאושש מבזבז זמינות בחינם. השלישית, שמעטים חושבים עליה, היא חוסר נראות (Observability) על מצב המעגל - אם אין Metric או Dashboard שמראה מתי מעגל עבר ל-Open ומתי חזר ל-Closed, הצוות לא יודע שהמנגנון בכלל פעל, ומפספס אינדיקציה מוקדמת וברורה לבעיה אמיתית בשירות תלוי. הרביעית היא מעגל אחד גלובלי לכל התלויות, כפי שהוזכר, שגורם לכשלון בתלות משנית לחסום גם תלויות קריטיות.
Circuit Breaker בממשק Gateway מול ספרייה מוטמעת בקוד
יש שתי גישות מימוש עיקריות שכדאי לבחור ביניהן במודע. הראשונה היא ספרייה מוטמעת בקוד השירות עצמו (In-process) - נותנת שליטה עדינה על כל תלות בנפרד, אבל דורשת מכל שירות לממש ולתחזק את הלוגיקה בעצמו, מה שיוצר חוסר עקביות אם צוותים שונים מגדירים ספים שונים בלי תיאום. השנייה היא Circuit Breaker ברמת ה-Service Mesh או ה-API Gateway (כמו Istio או Envoy) - נותנת עקביות ארגונית וניהול מרכזי של מדיניות עמידות בלי לגעת בקוד כל שירות בנפרד, אבל פחות גמישה להתאמות עדינות ברמת תלות בודדת. ארגונים גדולים עם עשרות מיקרו-שירותים נוטים לעבור בהדרגה לגישת ה-Mesh, בעוד צוותים קטנים יותר מסתפקים היטב בספרייה מוטמעת פר-שירות.
מתי Circuit Breaker נחוץ ומתי הוא מיותר
Circuit Breaker משתלם במיוחד במערכות מיקרו-שירותים שבהן שירות אחד תלוי בכמה שירותים אחרים, ובאינטגרציות עם API-ים חיצוניים שהזמינות שלהם לא בשליטתכם. הוא פחות נחוץ במונוליט פשוט שקורא רק למסד הנתונים שלו (שם הפתרון הנכון הוא לרוב Connection Pool מוגבל ו-Timeout, לא Circuit Breaker נפרד), ועלול להיות over-engineering במערכות קטנות עם מספר תלויות מועט וידוע. קו מנחה טוב הוא לשאול: אם התלות הזו נופלת לחלוטין לחמש דקות, האם זה יכול "להפיל" גם אותי? אם התשובה כן, Circuit Breaker כמעט תמיד מוצדק.
סיכום
Circuit Breaker הוא לא רק "עוד Try-Catch" - הוא מנגנון בידוד שמונע ממפולת שלגים להתפשט על פני מערכת שלמה, על ידי כישלון מהיר ומבוקר במקום המתנה שמכפילה את הנזק. ההשקעה הנדרשת - מכונת מצבים נכונה, סף מבוסס חלון זמן נע, Fallback הגיוני, ומעגלים נפרדים לכל תלות - קטנה משמעותית ביחס לנזק שהוא מונע כשתלות חיצונית באמת נופלת. במערכות מבוזרות עם כמה שירותים תלויים זה בזה, זה לא "nice to have" אלא תשתית בסיסית שכל צוות הנדסה צריך להכיר ולהטמיע לפני שהיא נדרשת בפועל - כי ברגע שהיא נדרשת, כבר מאוחר מדי לבנות אותה בזמן אמת. הצוותים שמטמיעים את זה נכון מגלים יתרון נוסף: המנגנון לא רק מגן בזמן תקלה, הוא גם הופך תקלות בתלויות חיצוניות לגלויות ומדידות הרבה לפני שהן מסלימות למשבר מלא.
תגיות: Circuit Breaker · Cascading Failure · Resilience Patterns · Microservices · Fallback · Bulkhead · Fail Fast