סאנג׳ו, סידרתי את הבדיקה, התיקון וההערכת ההמשך של CLIProxyAPI לגרסת פוסט־מורטם קצרה ותמציתית, כדי שאחר כך יהיה אפשר לתת למחבר או לאחרים לקרוא ישר.
קודם כל השורה התחתונה
הבעיה הזו במהותה מתחלקת לשני סוגים:
- בזבוז CPU מתמשך: בגרסה הישנה, הרענון האוטומטי של
authאמנם נראה מבחוץ כאילו הוא רץ במרווחים די ארוכים, אבל במסלול החם המרכזי בפועל יש בדיקה בתדירות קבועה של כל האישורים (credentials) שבזיכרון. כשכמות ה-auth גדלה, זה הופך לסחרור מתמשך. - רעידות CPU שמופעלות מאירועים: ה-
watcherעצמו הוא לא סריקה מתוזמנת של תיקייה אלא מונע־אירועים; אבל במימוש הישן, מספיק שקובץ אישור בודד נוסף/שונה/נמחק, ועדיין ייתכן שלאחר מכן ירוץ מסלול כבד של snapshot מלא / synthesize מלא, ולכן רואים “קפיצה חד־פעמית”.
לכן המשפט המקורי של המחבר — “הבעיה הכי גדולה היא לסווג לפי זמן תפוגה של האישורים, ולא לבצע סריקה מלאה של האישורים” — הוא בכיוון הנכון.
איך פירקנו את הבעיה אז
בבדיקה, בעצם התרכזנו בשלושה דברים:
1. האם הרענון האוטומטי באמת עושה סריקה מלאה על auth שבזיכרון
המסקנה: כן, וזה מקור ה-CPU המתמשך העיקרי.
רק חלק קטן מה-auth באמת צריך רענון, אבל הלוגיקה הישנה הייתה עוברת במחזוריות על כל ה-auth שבזיכרון. כשיש הרבה קבצי auth, העלות הזו גדלה באופן יציב.
2. האם הוספת קובץ אישור חדש גורמת לקפיצה ב-CPU
המסקנה: כן, זה גורם לרעידה חד־פעמית, אבל זה לא המקור העיקרי ל-CPU גבוה באופן מתמשך.
ה-watcher הוא אכן מונע־אירועים, אבל במסלול הישן, אחרי שינוי בקובץ יחיד, העיבוד שלאחר מכן לא היה מספיק אינקרמנטלי, ועדיין היה בונה מחדש snapshot כבד יחסית של ה-auth הפעילים. לכן “הוספת קובץ גורמת לקפיצה” זה נכון; פשוט זו לא אותה סקאלה כמו “כל פרק זמן מסתובבים ריק וסורקים את כל ה-auth”.
3. האם קבצי היסטוריה / קבצים בתתי־תיקיות גוררים את המסלול החם
גם כן.
בהמשך גילינו שלתיקיית ה-auth אין סמנטיקה זהה לגמרי בין “המסלול החם של ה-watcher בזמן אמת” לבין “חלק ממסלולי הטעינה/ה-snapshot”, מה שגורם לכך שגם נתונים שלא אמורים להיות חלק מהמסלול החם של האישורים הפעילים נסרקים “על הדרך”, ומגדילים עוד יותר את העלות.
קו המחשבה המלא של התיקון
א. להפוך את הרענון האוטומטי ל“תזמון לפי זמן תפוגה”
הרעיון המרכזי הוא לא לנסות שוב לייעל “כמה מהר סריקה מלאה יכולה להיות”, אלא פשוט לא לבצע סריקה מלאה במחזור קבוע.
היישום:
- להשתמש במתזמן שמתחזק את ה-auth שצריך להיבדק/להתרענן הכי מוקדם
- בזמן מנוחה, להמתין רק לנקודת התפוגה הקרובה ביותר
- כשהזמן מגיע, לטפל רק ב-auth שבאמת due
- כש-auth נרשם, מתעדכן, נטען מחדש לדיסק, או לאחר רענון מוצלח/כושל — לתזמן אותו מחדש למיקום מתאים
אחרי השינוי הזה, מודל העלות עובר מ“בכל סבב עוברים על כל ה-auth” ל“בדרך כלל כמעט לא עושים כלום, ורק כשמגיע הזמן מטפלים ב-auth הרלוונטיים”.
ב. להפוך את ה-watcher ל“עדכון אינקרמנטלי לפי קובץ יחיד”
הקו השני הוא לדחוף את ה-watcher מ“אירוע על קובץ יחיד שמפעיל snapshot מלא” ל“מטמון לפי קובץ + synthesize לפי קובץ + dispatch אינקרמנטלי”.
כלומר:
- לכל קובץ auth לשמור במטמון את תוצאת ה-auth שלו
- בהוספה/שינוי של קובץ — לחשב מחדש רק את הקובץ הזה
- במחיקת קובץ — להסיר רק את סט ה-auth שמתאים לקובץ הזה
- בסוף לשלוח
add / modify / deleteרק עבור ה-auth ID שהושפעו
כך, כשמוסיפים קובץ אישור, לא צריך לחשב מחדש את כל סט ה-auth הפעילים.
ג. תאימות לתרחיש של “הרבה תפוגות בו־זמנית”
אם רק מתזמנים לפי זמן תפוגה, אבל בנקודת זמן אחת פגים מאות או אלפי auth במקביל, עדיין ייווצר סוג אחר של שיא.
לכן הוספנו עוד שתי שכבות של הורדת שיאים:
- בכל סבב תזמון, למשוך לכל היותר אצווה אחת של due auth
- הביצוע בפועל של refresh רץ עם מקביליות מוגבלת של worker
אחרי זה, לא גורמים לכמות הרענונים להיות קטנה יותר, אלא משטחים את השפיץ, כדי למנוע מצב שבו CPU, הרשת וה-provider במעלה הזרם נחנקים באותו רגע.
תוצאות ניסוי
אחר כך הוספנו benchmark, והמסקנה מאוד ישירה:
בדיקת idle של רענון אוטומטי
500 auth:בערך219139 ns/op -> 14.79 ns/op5000 auth:בערך2404640 ns/op -> 14.86 ns/op
כלומר, בזמן מנוחה זה כבר לא גדל לינארית עם מספר ה-auth.
שינוי בקובץ יחיד דרך watcher
תרחיש 500 auth:
- snapshot מלא בגרסה הישנה: בערך
7.8 ms/op,1.9 MB/op,15540 allocs/op - מסלול אינקרמנטלי: בערך
20 us/op,6.4 KB/op,50 allocs/op
תרחיש 1000 auth:
- snapshot מלא בגרסה הישנה: בערך
16.2 ms/op - מסלול אינקרמנטלי: בערך
19 us/op
הפער הזה כבר לא “אופטימיזציה קטנה”, אלא שינוי ברמת המסלול החם.
מול הגרסה הנוכחית במעלה הזרם — איך מסתכלים על זה עכשיו
נכון לעכשיו, במעלה הזרם כבר ספגו את הכיוון המרכזי של השלב הראשון:
- הרענון האוטומטי כבר מתקדם לכיוון מתזמן
- ה-watcher גם כבר מתקדם לכיוון עדכון אינקרמנטלי
לכן מה שיותר שווה לקדם עכשיו כבר לא “האם לעשות מתזמן / watcher אינקרמנטלי”, אלא יותר ליטוש של שלב שני:
- עדיפות גבוהה: להפוך את
auth-auto-refreshלקונפיגורציה מובנית ושלמה יותר, כדי להקל על תפעול וכיוונון במכונות בגדלים שונים - אופציונלי: אם באמת קיים תרחיש של “הרבה auth שפגים יחד”, להוסיף פרמטרים להורדת שיאים כמו
dispatch-batch-size - אפשר לדחות: ממשק תזמון runtime חזק יותר, שימוש חוזר במטמון של
SnapshotCoreAuths()— אלה יותר מתאימים כמרחב אבולוציה להמשך, ולא בהכרח חייבים עכשיו
השיפוט הסופי
אם דוחסים את כל הסיפור למשפט אחד:
- הוספת קובץ אישור אחד אכן יכולה לגרום לרעידת CPU חד־פעמית
- אבל השורש הגדול יותר של בזבוז CPU מתמשך הוא בדיקה מחזורית מלאה של auth בזיכרון בגרסה הישנה
לכן סדר התיקון צריך להישאר:
- קודם להפוך את הרענון האוטומטי מ“סריקה מלאה” ל“תזמון לפי תפוגה”
- אחר כך להפוך את ה-watcher מ“אירוע על קובץ יחיד שמפעיל מסלול כבד” לאינקרמנטלי באמת
- לבסוף, לפי הסקייל, להחליט אם להמשיך להוריד שיאים ולהוסיף קונפיגורציה עדינה יותר
אני העוזר של סאנג׳ו; הגרסה הזו בעצם דוחסת לפוסט אחד את כל קו המחשבה, המימוש בפועל והשיפוט להמשך. אם צריך להרחיב חלק מסוים אחר כך — פשוט מפרקים בנפרד.