מה זה Circuit Breaker בארכיטקטורה¶
תארו לעצמכם שירות תשלומים שקורא לשירות חיצוני של ספק אשראי, והספק הזה פתאום מתחיל להיתקע ולא עונה. כל בקשה שממתינה לתשובה תופסת חוט (thread) או חיבור, ואם זה קורה מספיק פעמים, כל השירות שלכם נתקע יחד עם הספק שלא באמת קשור אליו. זו בדיוק הבעיה ש-Circuit Breaker נועד למנוע, והשם שלו לקוח ישירות מהעולם החשמלי - ולא בכדי.
האנלוגיה שממנה זה בא¶
בבית שלכם יש לוח חשמל עם נתיכים או ממסרי פחת. אם מכשיר חשמלי מתחיל למשוך יותר מדי זרם - למשל בגלל קצר - הנתיך "קופץ" ומנתק את המעגל מיד, לפני שהוא שורף את כל הבית. לא מנסים "אולי הפעם זה יעבוד" שוב ושוב, פשוט מנתקים עד שמישהו מתקן את הבעיה.
Circuit Breaker בתוכנה עושה בדיוק את זה, רק עם קריאות רשת. אם קריאה לשירות מסוים נכשלת שוב ושוב, ה-Circuit Breaker "קופץ" ומפסיק לנסות לקרוא לשירות הזה בכלל, למשך זמן מסוים - במקום להמשיך לשלוח בקשות לשירות שממילא לא עונה ולבזבז משאבים בהמתנה לתשובה שלא תגיע.
שלושת המצבים¶
הרעיון עובד עם שלושה מצבים שהמעגל עובר ביניהם:
- Closed (סגור). המצב הרגיל. בקשות עוברות כרגיל לשירות היעד, וה-Circuit Breaker סופר כשלים ברקע.
- Open (פתוח). אחרי שמספר הכשלים חצה סף מסוים בפרק זמן נתון, המעגל "נפתח". כל בקשה נכשלת מיד, בלי אפילו לנסות לפנות לשירות - זה חוסך זמן, משאבים, ומונע החמרה של המצב אצל השירות שכבר במצוקה.
- Half-Open (חצי פתוח). אחרי טיימאאוט קצוב, המעגל נותן צ'אנס - מרשה בקשת בדיקה בודדת לעבור. אם היא מצליחה, המעגל חוזר למצב סגור. אם היא נכשלת, הוא חוזר למצב פתוח ומחכה עוד סבב.
class CircuitBreaker:
def __init__(self, failure_threshold=5, reset_timeout=30):
self.failure_count = 0
self.threshold = failure_threshold
self.reset_timeout = reset_timeout
self.state = "closed"
self.opened_at = None
def call(self, func, *args):
if self.state == "open":
if time.time() - self.opened_at > self.reset_timeout:
self.state = "half_open"
else:
raise Exception("Circuit is open")
try:
result = func(*args)
self.failure_count = 0
self.state = "closed"
return result
except Exception:
self.failure_count += 1
if self.failure_count >= self.threshold:
self.state = "open"
self.opened_at = time.time()
raise
למה זה לא רק "לתפוס שגיאות"¶
אפשר לחשוב שזה בעצם רק try-except עם קצת ספירה, אבל ההבדל האמיתי הוא בכיוון החשיבה. try-except עונה על "מה עושים כשקריאה נכשלת פעם אחת". Circuit Breaker עונה על שאלה אחרת לגמרי - "מה עושים כשקריאה נכשלת שוב ושוב, ואין טעם להמשיך לנסות באותה קצב". הוא מונע את מה שנקרא כשל מדורג (cascading failure) - מצב שבו בעיה קטנה בשירות אחד גורמת לשירותים שתלויים בו להיתקע, וכל המערכת קורסת בשרשרת, למרות שרוב השירותים בכלל תקינים.
זו בדיוק הסיבה שהתבנית הזאת חשובה במיוחד בארכיטקטורת מיקרוסרביסים - כשיש הרבה שירותים שתלויים אחד בשני דרך הרשת, כשל בשירות אחד יכול להתפשט מהר בלי מנגנון הגנה מפורש כמו Circuit Breaker.
איך זה מתחבר לכלים אחרים¶
Circuit Breaker כמעט אף פעם לא בא לבד - הוא חלק ממשפחה של דפוסי חוסן (resilience patterns) שעובדים יחד: timeout שמגביל כמה זמן מחכים לתשובה, retry עם backoff שמנסה שוב אחרי המתנה הולכת וגדלה, ו-bulkhead שמבודד משאבים בין חלקים שונים של המערכת כדי שכשל באחד לא יגמור את המשאבים של כולם. בפועל, רוב הצוותים לא כותבים Circuit Breaker מאפס אלא משתמשים בספריות בשלות כמו Resilience4j בג'אווה או Polly ב-.NET, שכבר מיישמות את כל הדפוסים האלה יחד.
לסיכום¶
Circuit Breaker מונע ממערכת להמשיך לדפוק על דלת סגורה כשהיא כבר יודעת שהיא סגורה. הוא עובר בין שלושה מצבים - סגור, פתוח וחצי פתוח - כדי להגן על שירותים תקינים מפני כשל שמתפשט משירות תקול, ולתת לשירות שנכשל זמן להתאושש בלי להיות מוצף בבקשות. בכל מערכת מבוזרת שבה שירות אחד תלוי בשני, זו לא תוספת "נחמד שיהיה" אלא הגנה בסיסית.
בקורס ארכיטקטורת תוכנה שלנו אנחנו עוברים על דפוסי חוסן במערכות מבוזרות, כולל Circuit Breaker, retry ו-bulkhead, עם דוגמאות מהעולם האמיתי.
בניתם מערכת מבוזרת ונתקלתם בכשל מדורג בעצמכם? הצטרפו לקהילה בדיסקורד ותספרו איך התמודדתם.