CQRS בתשתית: היכן נשבר ההפרדה בין קריאה לכתיבה
CQRS הוא לא ספרייה ולא שירות נפרד, אלא הפרדה בין שני מעגלים עם דרישות שונות לכשל. פקודות נכתבות דרך מודל טרנזקציוני: עקביות קפדנית, נעילה קצרה, השהיה צפויה.
את הקריאה משרתות פרוג'קציות (projections): אפשר לבנות אותן מחדש, הן סובלניות לפיגור ומנורמלות-יתר (denormalized) עבור מסך מסוים.
רוב התקלות חיות במפגש שבין השניים. חוסר סנכרון של פרוג'קציות מתבטא כקריאה אחרי כתיבה: המשתמש שלח פקודה, הממשק ביקש את המודל וקיבל את המצב הקודם. זה לא מטופל בהשהיה לפני הבקשה, אלא בגרסת האגרגט בתשובה לפקודה — הלקוח קורא עד שהפרוג'קציה מדביקה את הגרסה הזו.
כפילויות פקודות — נקודת הכשל הטיפוסית השנייה: התעבורה מספקת אירוע לפחות פעם אחת, שליחה חוזרת אחרי timeout עוברת כפקודה חדשה ומבצעת את הפעולה פעמיים. אידמפוטנטיות הופכת לחלק מחוזה הפקודה: מפתח דה-דופליקציה בקלט, אינדקס ייחודי לפי מזהה, בדיקת הגרסה הצפויה בעת הכתיבה.
הידרדרות הכתיבה תחת עומס אנליטי היא כמעט תמיד תוצאה של תשתית משותפת: דוחות כבדים תופסים את מאגר החיבורים והדיסק, והשהיית הזנב (tail latency) של הכתיבה גדלה באופן לא ליניארי.
לכן את מעגל הקריאה מוציאים לרפליקות ומאגרי נתונים נפרדים — למשל פרוג'קציות על PostgreSQL עם פרופיל עומס משלהן — ובכתיבה מציבים timeouts, הגבלת בקשות בו-זמניות וסירוב מפורש במקום הצטברות תור.
מה נותן מעגל CQRS: כתיבה, קריאה וסוג הכשלים שנמנעו
ההפרדה בין כתיבה לקריאה נותנת שני מעגלים בלתי תלויים, וכל אחד נמדד במטריקה משלו. הפקודות כותבות למאגר מנורמל עם רפליקציה סינכרונית (במעגל שלנו — PostgreSQL): זמן התגובה של הכתיבה נמדד בשברירי שנייה ואינו תלוי במספר הקוראים הבונים דוחות בו-זמנית. את הקריאה משרתות פרוג'קציות מנורמלות-יתר, שמתמלאות באופן אסינכרוני מיומן האירועים.
עמידות הקריאה בפני כשלים נשענת על כך שהפרוג'קציות נגזרות מהיומן ואינן מקור האמת. ניתן לאבד צומת קריאה שלם: קליטת ההזמנות נמשכת, והפרוג'קציה משוחזרת על ידי הרצת האירועים מהמיקום האחרון שנשמר. זמן השחזור נקבע אז לפי נפח היומן היומי, ולא לפי גודל בסיס הנתונים הראשי ומהירות הפריסה שלו.
העומס על הכתיבה מפסיק להתחרות באנליטיקה. שאילתות וחילוצים כבדים עוברים לרפליקות נפרדות, ולכן תעבורת השיא של הדוחות כבר לא חוסמת טרנזקציות.
גם סוג הכשלים משתנה: במקום „בסיס הנתונים קרס תחת שאילתת דוח" אנחנו מקבלים הידרדרות של פרוג'קציה אחת, שמדביקה את הזרם תוך דקות ואינה מפריעה לביצוע הזמנות.
את עלות הזמן שבו המערכת מושבתת אנחנו סופרים בפעולות שאבדו, ולא בשעות של חוסר זמינות מוחלט. כל עוד מעגל הכתיבה אינו זמין, הזמנות חדשות לא מתקבלות — אבל קריאת ההיסטוריה, הסטטוסים והאזור האישי ממשיכים לעבוד, כי הם עוברים דרך הרפליקות שלהם.
זמן התגובה של הכתיבה, זמן השחזור ושיעור כשלי הקריאה — שלוש מטריקות שלפיהן אנחנו מאשרים את העבודה לאחר ההטמעה.
אפשרויות להפרדת קריאה וכתיבה: השוואת גישות
ההפרדה בין קריאה לכתיבה היא לא מתג, אלא סקאלה: מתפקידים נפרדים בתוך בסיס נתונים אחד ועד יומן אירועים עם פרוג'קציות אסינכרוניות. כל צעד ימינה מסיר עומס מהכתיבה הטרנזקציונית, אבל מוסיף השהיה בין הנכתב לנקרא.
הבחירה נקבעת לפי הדרישה לרעננות הנתונים: במקום מסוים מספיק „בעוד שנייה", ובמקום אחר החשבון הוא בשברירי שנייה.
| אפשרות | מתי מתאימה | מגבלות |
|---|---|---|
| תפקידים נפרדים בבסיס נתונים אחד | אנליטיקה וממשק ניהול מפריעים לכתיבה הטרנזקציונית | משאב משותף: דוח כבד עדיין מעכב את הכתיבה |
| רפליקה לקריאה | העומס בעיקר על קריאה, פיגור של שניות מקובל | השהיית רפליקציה: קריאה מיד אחרי כתיבה דורשת ניתוב למאסטר |
| תצוגות ממומשות (materialized views) | אגרגטים כבדים, התיישנות נתונים מקובלת | החישוב מחדש נתקע בנעילות; סט התצוגות כבול באופן קשיח לשאילתות |
| יומן אירועים ופרוג'קציות אסינכרוניות | כמה מודלים של קריאה, נדרשים היסטוריה ובנייה מחדש | עקביות בסופו של דבר (eventual consistency): כפילויות, עיבוד חוזר, פיגור הפרוג'קציה |
עקביות בסופו של דבר היא לא פגם, אלא פרמטר: את ההשהיה שלה מגדירים באופן מפורש ובודקים לפי מטריקת הפיגור של הפרוג'קציות. מתחילים מתפקידים נפרדים במקום שבו זה מספיק, ועוברים ליומן אירועים רק במקום שבו נדרשים היסטוריה ובנייה מחדש של מודלי הקריאה.
סדר ההטמעה: מביקורת סכימת הנתונים ועד מסירת הנהלים
הטמעת CQRS מתחילה לא בקוד, אלא בתיאור של אילו שאילתות המערכת משרתת והיכן הכתיבה מפריעה לקריאה. בלי הצעד הזה ההפרדה בין המודלים הופכת לשני מאגרי נתונים עם נתונים סותרים ובדיקה ידנית.
מסלול ההטמעה
- ביקורת סכימת הנתונים. משרטטים מפה של הישויות: מה נקרא בלבד, מה נכתב כל הזמן, והיכן פעולות מתחרות על נעילות שורות. בפלט — רשימת תרחישים עם פרופיל עומס לכל אחד.
- סכימת זרימות הפקודות. קובעים את גבולות האגרגטים ואיזו פקודה מפיקה איזה אירוע. כאן גם מחליטים אילו שינויים מותר ליישם באופן אסינכרוני ואילו חייבים להיות עקביים בקפדנות.
- תכנון מודלי הקריאה. לכל מסך ודוח — תצוגה משלו עם האינדקסים הנדרשים, בלי טבלה אוניברסלית „להכול". בנפרד מסכמים את הפיגור המותר של הפרוג'קציה לכל תרחיש.
- תוכנית ההטמעה. הולכים קונטקסט מוגבל אחד בכל פעם, תוך שמירה על נתיב הכתיבה הקודם כגיבוי. כל עוד המעגל החדש לא אושר, חזרה לאחור אפשרית בהחלפה בלי אובדן נתונים.
- הגדרת הסנכרון. מגדירים תור, סדר אספקת האירועים, אידמפוטנטיות של המטפלים והתנהגות בעת אספקה חוזרת. עבור הודעות כשל — ענף נפרד לניתוח, כדי שאירוע אחד לא יחסום את הזרם.
- בדיקה תחת עומס. מריצים כתיבה וקריאה בנפרד: בודקים את פיגור הפרוג'קציות בשיא, את ההתנהגות כשהצרכן אינו זמין ואת זמן ההתחממות של התצוגות לאחר בנייה מחדש.
- מסירת נהלים. מוסרים הוראות להרצה חוזרת של אירועים, לשחזור פרוג'קציה מאפס ולטיפול בפערים, וכן סט מטריקות של מצב המעגל למשמרת תורנית.
כל צעד מסתיים בארטיפקט: סכימה, קונפיגורציה או נוהל. כך ההטמעה נשארת מנוהלת, והתפעול אינו תלוי במחברי הפתרון.
החבילה בסיום: סכימות זרימה, מדיניות גישה, נהלים
חבילת התיעוד היא חלק מהתוצאה, ולא נספח לה. בלעדיה מעגל CQRS עובד, אבל צוות הלקוח לא יכול לשנות אותו: לא ברור היכן נכתבת האמת, מי קורא את הפרוג'קציות ומה לעשות בעת פער. לכן יחד עם הקוד אנחנו מוסרים ארטיפקטים, שכל אחד מהם סוגר סוג משלו של משימות תפעול.
- מפת זרימות האירועים — אילו פקודות מפיקות אותם, מי הצרכנים, איזה סדר אספקה מובטח ומה מדכא עיבוד חוזר.
- סכימת הנתונים — ההפרדה בין צד הכתיבה לצד הקריאה, מודל הפרוג'קציות, מפתחות ואינדקסים לשאילתות המסכים המסוימות.
- תיעוד הפרוג'קציות — מקור האירועים, כלל הבנייה מחדש, הפיגור התקין מהזרם, סדר החישוב המלא מחדש בלי לעצור את הקריאה.
- מדיניות גישה — תפקידים לשליחת פקודות ולקריאה, חלוקה לפי אגרגטים, הרשאה נפרדת לבנייה מחדש של פרוג'קציה.
- נוהל תחזוקה — כיצד לשנות את סכימת האירועים, כיצד לנהל גרסאות של אירועים ומה לעשות עם צרכנים שטרם עברו לגרסה החדשה.
- סדר הטיפול בתקלות — סימנים לפער בין קריאה לכתיבה, צעדי אבחון לפי היומנים, חזרה לגרסה הקודמת של הפרוג'קציה.
- סכימת הפריסה — הרכב הצמתים, פרמטרי הברוקר, תורי החזרות והמגבלות שמעבר להן מתערב מהנדס.
החבילה נמצאת במאגר הקוד של הלקוח ומתעדכנת יחד עם הקוד. אנחנו בודקים אותה בקבלה בפשטות: אם לפיה אי אפשר לבנות מחדש פרוג'קציה או להכניס מהנדס חדש לעבודה — המשימה לא הושלמה.
מקרה בוחן: הפרדת קריאה וכתיבה בשירות בעל עומס גבוה
„השירות התחיל מצומת אחד: גם הכתיבה וגם הקריאה הלכו לאותה טבלה. בשיא, טרנזקציות הכתיבה נתקעו בנעילות — זמן התגובה של הכתיבה עלה משנייה לעשירייה, ושיעור כשלי הקריאה הגיע ל-8%. הכי גרוע היה עם הכשלים: אובדן צומת פירושו שחזור ידני ועד חצי שעה של חוסר זמינות.
המודלים הופרדו. הכתיבה נשארה במאגר הטרנזקציוני PostgreSQL, והקריאה עברה לפרוג'קציות מנורמלות-יתר נפרדות, שמתעדכנות מיומן השינויים. את המטפלים הפכנו לאידמפוטנטיים, ואת היומן — למקור שממנו אפשר לבנות מחדש את הפרוג'קציה כולה לאחר תקלה. רפליקות הקריאה מתחלפות אוטומטית, בלי התערבות ידנית.
מה השתנה לפי המטריקות. זמן התגובה של הכתיבה בשיא הצטמצם פי 6–8 והפסיק להיות תלוי בנפח הקריאות. שיעור כשלי הקריאה ירד מתחת לעשיריות האחוז: הבקשה כבר לא ממתינה לנעילות, ונפילה של רפליקה בודדת לא מפילה את התפוקה. השחזור אחרי כשל בצומת — אוטומטי, תוך דקותיים, בלי אובדן של כתיבות מאושרות.
בנפרד הוצאנו את הנראות: פיגור הפרוג'קציות, עומק התור והפער בין המודלים נראים בלוח הניטור, ולכן ההידרדרות נתפסת לפני שהמשתמש מבחין בה."
במה CQRS שונה מרפליקה לקריאה?
רפליקה לקריאה היא אותו מודל כתיבה, משוכפל על פני צמתים: אותן טבלאות, אותן מגבלות, אותו חוזה. מה שמשתנה הוא רק מי עונה לבקשות, והפער מוגבל לפיגור הרפליקה — בדרך כלל שברירי שנייה, ולרוב שאילתות הדוחות הוא בלתי מורגש.
ההפרדה בין המודלים מתחילה במקום שבו דרישות הקריאה והכתיבה נפרדות עד כדי כך שסכימה אחת כבר לא משרתת אותן.
ב-CQRS מודל הכתיבה אחראי למצב ולנו-אינווריאנטים (invariants): טרנזקציות, בדיקות, נעילות. מודל הקריאה — פרוג'קציה נפרדת לשאילתות מסוימות: טבלאות מנורמלות-יתר, אגרגטים שחושבו מראש, מבני חיפוש. היא נבנית מזרם השינויים, ולא מהעתק של הטבלאות, וחיה לפי כללים משלה.
מכאן ההבדל המרכזי. ברפליקה הפיגור הוא השהיית ההעתקה כשמשמעות הנתונים זהה. ב-CQRS המודלים נפרדים בהגדרה: הפרוג'קציה אינה מקור האמת, אפשר לבנות אותה מחדש מאפס, וטעות בחישוב מטופלת בחישוב מחדש ולא בשחזור מהעתק. המחיר של זה — אסינכרוניות.
מי שקורא מיד אחרי הכתיבה עלול לא לראות את השינויים שלו. במקום שבו זה בלתי מתקבל על הדעת, מנהלים גרסאות של הפרוג'קציה וממתינים לאישור הגרסה הנדרשת, או משאירים נתיב סינכרוני לסט מצומצם של פעולות.
תוך כדי כך עוקבים אחרי מטריקות שונות: פיגור רפליקה והשהיית עדכון פרוג'קציה הם לא אותו דבר. אם הקריאה והכתיבה סימטריות בעומס, אין צורך בהפרדת מודלים — די ברפליקות ובפיזור הבקשות.
ספקות טכניים: עקביות, כפילות פקודות, מיגרציה
ההסתייגויות מ-CQRS מוסרות בשלב התכנון, ולא אחרי ההשקה: לכל אחת מהן יש פתרון הנדסי קונקרטי, שנבדק בבדיקה או בעומס. להלן תשובות לאלה שצפות לרוב לפני תחילת העבודה.
כיצד מובטחת אידמפוטנטיות הפקודות?
כל פקודה מגיעה עם מפתח ייחודי, והמטפל מנהל יומן של מפתחות שכבר יושמו. אספקה חוזרת בעת שליחה מחדש לא יוצרת רשומה שנייה: הפעולה או שכבר יושמה, או שמתקבלת כחדשה.
מה עם עקביות הנתונים בין הכתיבה לקריאה?
הפרוג'קציה מדביקה את היומן בהשהיה של שברירי שנייה, וחלון הזמן הזה מדיד — רואים אותו במטריקות. בתרחישים קריטיים הקריאה מתבצעת בהמתנה לגרסה הנדרשת של הפרוג'קציה או ישירות מהיומן, והממשק מציג „התקבל, מעובד" במקום נתונים מיושנים.
האם אפשר לבנות מחדש פרוג'קציות בלי השבתה?
הפרוג'קציה נגזרת מהיומן, ולכן הגרסה החדשה נבנית לסכימה נפרדת במקביל לזו שעובדת, מדביקה את הזרם, ומוחלפת בניתוב הקריאה. חזרה לאחור — החזרת הניתוב לסכימה הקודמת.
כיצד מתבצעת מיגרציה בלי לעצור את הכתיבה?
מרחיבים את הסכימה, כותבים לפורמט הישן והחדש בו-זמנית, מעבירים את ההיסטוריה במנות רקע, משווים סכומי ביקורת (checksums) לפי מפתחות, ורק אחר כך מסירים את הנתיב הישן. כל צעד הפיך ואינו חוסם את הכתיבה.
מה יקרה אם תעבורת האירועים לא תהיה זמינה?
האירוע נכתב באותה טרנזקציה שבה נכתב שינוי המצב, ואותו מפרסם וורקר (worker) נפרד עם אישור אספקה. אלה שלא אושרו נשלחים שוב, והמקבל משליך כפילויות לפי מזהה האירוע.
נדון במשימה: מה נדרש להערכה ולתוכנית העבודה
כדי שהערכת העבודה תישען על עובדות ולא על הנחות, אנחנו זקוקים לניתוח קצר של המעגל הנוכחי: היכן עוברת הכתיבה, היכן הקריאה וכיצד הן קשורות זו לזו. ככל שנתוני המוצא קונקרטיים יותר, כך תוכנית ההטמעה מדויקת יותר ופחות הנחות בלוחות הזמנים ובהיקף השינויים.
מתאים גם מה שיש בהישג יד: סכימות, קונפיגורציות של שירותים, חילוצי מטריקות, תיאור של צווארי בקבוק ידועים. אם אין תיעוד, נשחזר את התמונה לפי תשובותיכם ולפי היומנים שתסכימו להראות.
מה שמאיץ באופן ניכר את ההכנה:
- סכימת המעגל: שירותים, נקודות הכתיבה והקריאה, כיצד עוברות הבקשות כעת;
- פרופיל העומס: שיעור הקריאה והכתיבה, שיאים, סובלנות לפיגור הפרוג'קציות;
- דרישות לעקביות: היכן מותרת קריאה עם השהיה והיכן נדרשת עדכניות;
- מאגרי נתונים ותורים: מה כבר בשימוש, מגבלות גישה וסביבות;
- פריסה וחזרה לאחור: כיצד משחררים שינויים, האם יש סט בדיקות רגרסיה;
- קריטריוני קבלה: לפי אילו מטריקות נבין שהפתרון עובד;
- מגבלות: נהלים, חלונות עבודה, מי מקבל החלטות בצד שלכם.
בתשובה אנחנו שולחים הערכה לפי שלבים, תוכנית הטמעה עם סדר העבודה ונקודות החזרה לאחור, וכן את הרכב העבודה עם גבולות האחריות. תארו את המשימה בצורה הנוחה לכם — נחזור עם הערכה ותוכנית.







