מאמרים הנדסיים
איך שמרנו על בקאנד מהיר עם יותר מ־300 מיליון משתמשים: אינדקסים, Redis ו־failover ל־RPC
מאת Muhammad Shahid · · 4 דקות קריאה
תשובה קצרה
ב־DOP, פרוטוקול בלוקצ׳יין לפרטיות שגייס 185 מיליון דולר ועבר 300 מיליון משתמשים, נקודות קצה עמוסות קריאה התחילו להיתקע וה־CPU של MongoDB היה קרוב לתקרה. איתור השאילתות האיטיות, אינדקסים מורכבים וקאש ב־Redis לנתיבים עמוסי קריאה פתרו את ה־timeouts, ופיזור הקריאות לבלוקצ׳יין בין 5 ספקי RPC עם failover עצר את ההשבתות בטרנזקציות.
| פרויקט | DOP (Data Ownership Protocol) |
|---|---|
| היקף | יותר מ־300 מיליון משתמשים, גיוס של 185 מיליון דולר |
| הבעיה | timeouts ב־API, CPU של MongoDB ליד התקרה, נפילות RPC |
| הפתרון | אינדקסים מורכבים, קאש ב־Redis, 5 ספקי RPC עם failover |
| טכנולוגיות | Node.js, Express, MongoDB, Redis, Web3 |
מה השתבש?
כשבסיס המשתמשים גדל הופיעו שתי בעיות שונות. הראשונה: נקודות קצה שקוראים מהן הרבה יותר משכותבים אליהן התחילו להיתקע, וה־CPU של MongoDB נשאר קרוב ל־100%. השנייה: טרנזקציות בבלוקצ׳יין נכשלו מדי פעם בלי שגיאה ברורה, כי הבקאנד דיבר עם הרשת דרך ספק RPC אחד, וספק זה נפל לפעמים.
אף אחת מהבעיות לא דרשה כתיבה מחדש. שתיהן דרשו שהבקאנד יפסיק לעשות עבודה שהוא לא צריך לעשות.
איך מוצאים את השאילתות שמאטות הכול?
מתחילים מנתונים, לא מניחושים. מפעילים ב־MongoDB רישום של שאילתות איטיות, ואז מריצים explain("executionStats") על הגרועות שבהן. שני המספרים החשובים הם totalDocsExamined ו־nReturned: אם שאילתה בודקת 200,000 מסמכים כדי להחזיר 20, היא סורקת במקום להשתמש באינדקס, ושם הולך ה־CPU.
db.claims.find({ wallet: w, status: "active" })
.sort({ createdAt: -1 })
.limit(20)
.explain("executionStats")
// Look for: COLLSCAN, or totalDocsExamined far above nReturned
אילו אינדקסים פותרים את זה?
אינדקס מורכב שמתאים למבנה השאילתה. מסדרים את השדות כך: קודם שדות סינון לפי שוויון, אחר כך שדה המיון, ואחר כך סינונים לפי טווח. לשאילתה שלמעלה זה אומר wallet, אחריו status ואחריו createdAt:
db.claims.createIndex({ wallet: 1, status: 1, createdAt: -1 })
עם האינדקס הנכון MongoDB קורא רק את 20 המסמכים שהוא מחזיר, כבר ממוינים. חוזרים על זה לקומץ השאילתות שמהוות את רוב העומס; לרוב לא צריך יותר מכמה אינדקסים חדשים, וכל אינדקס נוסף מאט כתיבות, אז מוסיפים אותם איפה שהמדידה מראה, לא בכל מקום.
איך קאש ב־Redis עוזר לנקודות קצה עמוסות קריאה?
הרבה נקודות קצה מחזירות את אותה תשובה לאלפי משתמשים למשך כמה שניות או דקות: סטטיסטיקות ציבוריות, הגדרות, נתוני טוקן. הגשה שלהן מ־Redis במקום מ־MongoDB מורידה את רוב העומס ממסד הנתונים. הדפוס הפשוט ביותר הוא cache-aside: קוראים מ־Redis, ואם אין שם, קוראים מ־MongoDB ושומרים את התוצאה עם תוקף קצר.
async function getStats() {
const cached = await redis.get("stats:v1")
if (cached) return JSON.parse(cached)
const stats = await Stats.aggregate(pipeline) // the expensive part
await redis.set("stats:v1", JSON.stringify(stats), { EX: 60 })
return stats
}
שומרים תוקף קצר לנתונים שמשתנים, מוסיפים גרסה למפתח (v1) כדי ששינוי מבנה לא יגיש נתונים ישנים, ואף פעם לא שומרים מידע פרטי של משתמש תחת מפתח משותף.
איך מונעים מצומת RPC אחד לשבור טרנזקציות?
לא תלויים בספק אחד. חיברנו 5 ספקי RPC, בחרנו אחד אקראי לכל קריאה כדי לפזר את העומס, ובמקרה של כישלון ניסינו את הבא, כך שצומת שנפל אף פעם לא הגיע למשתמש.
const providers = RPC_URLS.map((url) => new JsonRpcProvider(url))
async function withFailover(call) {
const order = [...providers].sort(() => Math.random() - 0.5)
let lastError
for (const provider of order) {
try {
return await call(provider)
} catch (err) {
lastError = err // try the next provider
}
}
throw lastError
}
const balance = await withFailover((p) => p.getBalance(address))
בפרודקשן מוסיפים timeout לכל קריאה ורושמים איזה ספק נכשל, כדי שאפשר יהיה להוריד ספק שנכשל שוב ושוב.
מה היו התוצאות?
- ה־timeouts ב־API נעלמו וזמני התגובה נשארו יציבים גם בשיא העומס.
- ה־CPU של MongoDB חזר לטווח בטוח.
- נפילות של צמתי RPC בודדים כבר לא גרמו להשבתה בטרנזקציות.
- הבקאנד החזיק לאורך הגיוס של 185 מיליון דולר והצמיחה מעבר ל־300 מיליון משתמשים.
מה לבדוק קודם בכל בקאנד איטי?
- אילו 5 שאילתות או נקודות קצה לוקחות הכי הרבה זמן בסך הכול? מתקנים אותן קודם.
- האם השאילתות האלה משתמשות באינדקס שמתאים לסינון ולמיון שלהן?
- אילו תשובות זהות להרבה משתמשים? שומרים אותן בקאש עם תוקף קצר.
- איזה שירות חיצוני, אם ייפול, יפיל גם אתכם? מוסיפים עוד אחד.
שאלות נפוצות
למה ה־CPU של MongoDB ב־100%?
בדרך כלל כי כמה שאילתות נפוצות סורקות אוספים שלמים במקום להשתמש באינדקס. בודקים אותן עם explain("executionStats") ומוסיפים אינדקסים מורכבים שמתאימים לסינון ולמיון שלהן.
איך יודעים איזה אינדקס ליצור ב־MongoDB?
מתאימים למבנה השאילתה: קודם שדות שוויון, אחר כך שדה המיון, ואז סינוני טווח. מוודאים עם explain() שהשאילתה בודקת בערך כמה מסמכים שהיא מחזירה.
מתי כדאי להוסיף קאש ב־Redis?
כשהרבה משתמשים מבקשים את אותם נתונים והם יכולים להיות ישנים בכמה שניות או דקות: סטטיסטיקות ציבוריות, הגדרות, מחירים, רשימות. לא שומרים מידע פרטי של משתמש תחת מפתח משותף.
צריך לכתוב מחדש בקאנד איטי כדי להגדיל אותו?
לעיתים רחוקות. ב־DOP אינדקסים, קאש ו־failover ל־RPC הספיקו כדי לשרת יותר מ־300 מיליון משתמשים בלי לכתוב מחדש.
כמה ספקי RPC צריך בקאנד של Web3?
לפחות שניים, כדי שנפילה אחת לא תעצור טרנזקציות. ב־DOP השתמשנו בחמישה, שנבחרו באקראי לכל קריאה, עם failover אוטומטי.
אתה יכול לתקן API איטי אצלי?
כן. אני מאבחן את נקודות הקצה האיטיות, מתקן את הסיבה האמיתית ומוסיף ניטור כדי שזה לא יחזור. ראו את עמוד שירות הבקאנד ב־Node.js.