אתם מעלים שירות לייצור. 12 שעות לאחר מכן הוא קורס עם OOM—דליפת זיכרון. מבחני עומס קצרים (5–10 דקות) לא הראו כלום. נשמע מוכר? זהו תרחיש קלאסי שבו יש צורך במבחן ספיגה (Soak Test). אנחנו, המהנדסים ב-True Tech, מתכננים מבחנים ארוכי טווח כדי לחשוף ירידות ביצועים נסתרות לפני שהן פוגעות בייצור שלכם. לקוח אחד איבד 8 שעות עבודה בגלל דליפת זיכרון שלא התגלתה באפליקציית Node.js—לאחר מבחן ספיגה, מצאנו אותה ב-3 השעות הראשונות של הניתוח.
מבחן ספיגה (נקרא גם מבחן סבולת) מריץ מערכת תחת עומס רגיל או מתון למשך 4–24 שעות. הוא חושף בעיות שלא מופיעות תוך דקות: דליפות זיכרון, הצטברות של מתארי קבצים, ירידה בביצועי מאגר חיבורי מסד נתונים, גידול בשאילתות איטיות עקב נפיחות טבלאות. ללא מבחן ספיגה, אתם מסתכנים בכשלים בלתי מוסברים לאחר מספר שעות של פעילות.
למה מבחני ספיגה תופסים דליפות זיכרון
דליפות זיכרון הן אפקט קלאסי של "רתיחה איטית". אפליקציה גדלה ב-100–200 MB לשעה וקורסת עם OOM לאחר 12 שעות. מבחנים קצרים פשוט לא שמים לב לגידול. מבחן ספיגה עם ניטור RSS ו-heap לוכד את המגמה הליניארית ומנבא את נקודת הכשל. לפי התיעוד של k6, מבחני ספיגה של 8 שעות מזהים 90% מדליפות הזיכרון שמבחנים קצרים מפספסים.
מבחן ספיגה יעיל פי 10 ממבחנים קצרים במציאת דליפות זיכרון—מאושר על ידי הניסיון שלנו ביותר מ-50 פרויקטים.
אילו בעיות מבחן ספיגה חושף?
- דליפות זיכרון: האפליקציה גדלה ב-100–200 MB לשעה וקורסת עם OOM לאחר 12 שעות.
- מיצוי מאגר חיבורים: חיבורי מסד נתונים לא מוחזרים למאגר; לאחר 6 שעות המאגר ריק ובקשות חדשות ממתינות עד לפסק זמן.
- הצטברות Heap: JVM/Node.js GC מטפל בזה ב-2 השעות הראשונות, ואז השהיות של Full GC מתחילות להשפיע על זמן התגובה.
- נפיחות טבלאות ללא autovacuum: נפיחות ב-PostgreSQL לאחר מיליוני פעולות UPDATE/DELETE פוגעת בביצועים ללא vacuum.
- דליפת מתארי קבצים: כל בקשה פותחת קובץ לוג או socket ולא סוגרת אותו; לאחר 8 שעות ה-ulimit ממוצה.
| סוג מבחן | משך | מטרה | חושף |
|---|---|---|---|
| מבחן עומס | 10–30 דקות | בדיקה תחת עומס צפוי | תפוקה, זמן תגובה |
| מבחן לחץ | 5–15 דקות | בדיקה תחת עומס שיא | נקודת כשל, שגיאות עומס יתר |
| מבחן ספיגה | 4–24 שעות | בדיקה תחת עומס מתון | דליפות זיכרון, ירידת ביצועים, שגיאות מצטברות |
איך אנחנו מבצעים מבחני ספיגה: תהליך וכלים
שלבים
- אנליטיקה: איסוף פרופיל העומס בייצור שלכם (תעבורה, נקודות קצה, תרחישים).
- עיצוב תרחיש: כתיבת סקריפטים של k6 עם תמהיל ריאלי של בקשות (קריאה, כתיבה, חיפוש).
- הגדרת ניטור: הפעלת מדדי זיכרון, מתארי קבצים ומסד נתונים (PostgreSQL, MySQL).
- ביצוע המבחן: בסביבת staging עם חלון של 8 שעות.
- ניתוח מגמות: ריצת רגרסיה על RSS, P95 latency, יחס טופלים מתים; חיפוש גידול מובהק סטטיסטית.
- הכנת דוח: ויזואליזציה של ירידת הביצועים, מתן המלצות לתיקון קוד ותצורה.
דוגמת תרחיש k6
// tests/soak/endurance.js
import http from 'k6/http'
import { check, sleep } from 'k6'
import { Rate, Trend, Gauge } from 'k6/metrics'
const errorRate = new Rate('errors')
const p95Latency = new Trend('p95_latency_trend', true)
const activeUsers = new Gauge('active_users')
export const options = {
stages: [
{ duration: '5m', target: 50 }, // разогрев
{ duration: '8h', target: 50 }, // 8 часов нормальной нагрузки
{ duration: '5m', target: 0 }, // остывание
],
thresholds: {
// Latency не должна деградировать в течение теста
http_req_duration: ['p(95)<600'],
// Ошибок не должно быть вообще (утечки проявляются через ошибки)
errors: ['rate<0.001'],
// Время подключения к БД не должно расти
http_req_connecting: ['p(95)<50'],
}
}
const BASE_URL = __ENV.BASE_URL || 'https://staging.example.com'
export function setup() {
const res = http.post(`${BASE_URL}/api/auth/login`, JSON.stringify({
email: '[email protected]',
password: __ENV.TEST_PASSWORD
}), {
headers: { 'Content-Type': 'application/json' }
})
return { token: res.json('token') }
}
export default function(data) {
const headers = {
'Authorization': `Bearer ${data.token}`,
'Content-Type': 'application/json'
}
activeUsers.add(1)
// Микс операций, типичных для реального трафика
const scenario = Math.random()
if (scenario < 0.6) {
// 60%: чтение данных
const r = http.get(`${BASE_URL}/api/products?page=${Math.ceil(Math.random() * 50)}`, { headers })
check(r, { 'read: 200': (r) => r.status === 200 })
errorRate.add(r.status !== 200)
} else if (scenario < 0.8) {
// 20%: запись данных (создаём реальные записи)
const r = http.post(`${BASE_URL}/api/cart/items`, JSON.stringify({
productId: Math.ceil(Math.random() * 1000),
quantity: 1
}), { headers })
check(r, { 'write: 2xx': (r) => r.status < 300 })
errorRate.add(r.status >= 400)
} else if (scenario < 0.9) {
// 10%: поиск
const r = http.get(`${BASE_URL}/api/search?q=test&limit=20`, { headers })
check(r, { 'search: 200': (r) => r.status === 200 })
errorRate.add(r.status !== 200)
} else {
// 10%: профиль пользователя
const r = http.get(`${BASE_URL}/api/me`, { headers })
check(r, { 'profile: 200': (r) => r.status === 200 })
errorRate.add(r.status !== 200)
}
// Добавить p95 для временного ряда
p95Latency.add(http.get(`${BASE_URL}/api/health`).timings.duration)
sleep(Math.random() * 2 + 0.5) // 0.5–2.5 секунды между запросами
} ניטור דליפות זיכרון
במקביל ל-k6, הריצו סקריפט לניטור RSS ומתארי קבצים:
#!/bin/bash
# scripts/memory-soak-monitor.sh
APP_PID=$(pgrep -f "node server.js")
LOG_FILE="soak-memory-$(date +%Y%m%d-%H%M).csv"
echo "timestamp,rss_mb,heap_used_mb,heap_total_mb,external_mb,fd_count" > $LOG_FILE
while true; do
TS=$(date -u +%Y-%m-%dT%H:%M:%SZ)
METRICS=$(curl -s http://localhost:3000/metrics/memory)
RSS=$(echo $METRICS | jq -r '.rss')
HEAP_USED=$(echo $METRICS | jq -r '.heapUsed')
HEAP_TOTAL=$(echo $METRICS | jq -r '.heapTotal')
EXTERNAL=$(echo $METRICS | jq -r '.external')
FD_COUNT=$(ls /proc/$APP_PID/fd 2>/dev/null | wc -l)
echo "$TS,$RSS,$HEAP_USED,$HEAP_TOTAL,$EXTERNAL,$FD_COUNT" >> $LOG_FILE
sleep 60
doneובצד האפליקציה, חשפו מדדים דרך נקודת קצה:
// Express/Fastify endpoint для экспонирования памяти
app.get('/metrics/memory', (req, res) => {
const mem = process.memoryUsage()
res.json({
rss: Math.round(mem.rss / 1024 / 1024),
heapUsed: Math.round(mem.heapUsed / 1024 / 1024),
heapTotal: Math.round(mem.heapTotal / 1024 / 1024),
external: Math.round(mem.external / 1024 / 1024),
})
}) ניטור PostgreSQL במהלך מבחן ספיגה
-- Рост таблиц (bloat)
SELECT relname, n_live_tup, n_dead_tup,
round(n_dead_tup::numeric / nullif(n_live_tup + n_dead_tup, 0) * 100, 1) AS dead_pct,
last_vacuum, last_autovacuum
FROM pg_stat_user_tables
ORDER BY n_dead_tup DESC
LIMIT 10;
-- Накопление idle транзакций (connection leak)
SELECT count(*), state, wait_event_type
FROM pg_stat_activity
WHERE pid != pg_backend_pid()
GROUP BY state, wait_event_type
ORDER BY count DESC;
-- Рост размеров временных файлов
SELECT temp_files, temp_bytes
FROM pg_stat_database
WHERE datname = current_database();
ניתוח מגמת ירידת ביצועים
לאחר המבחן, הריצו סקריפט Python לחישוב רגרסיית RSS:
# analyze_soak.py
import pandas as pd
import numpy as np
from scipy import stats
def analyze_memory_trend(csv_file: str):
df = pd.read_csv(csv_file, parse_dates=['timestamp'])
df['minutes'] = (df['timestamp'] - df['timestamp'].iloc[0]).dt.total_seconds() / 60
slope, intercept, r_value, p_value, std_err = stats.linregress(df['minutes'], df['rss_mb'])
hours_to_oom = None
if slope > 0:
oom_threshold = 4096
current_rss = df['rss_mb'].iloc[-1]
hours_to_oom = (oom_threshold - current_rss) / (slope * 60)
print(f"Memory growth rate: {slope:.2f} MB/min ({slope*60:.1f} MB/hour)")
if hours_to_oom:
print(f"Estimated OOM in: {hours_to_oom:.1f} hours")
if p_value < 0.01 and slope > 0.1:
print("MEMORY LEAK DETECTED (statistically significant growth)")
else:
print("No significant memory leak detected")
return {'slope_mb_per_min': slope, 'r_squared': r_value**2, 'hours_to_oom': hours_to_oom, 'leak_detected': p_value < 0.01 and slope > 0.1} איך לפרש תוצאות מבחן ספיגה
סדרות הזמן שנבנו של RSS ו-P95 latency הן המפתח לזיהוי ירידת ביצועים. אם שיפוע מגמת ה-RSS חיובי ומובהק סטטיסטית, מדובר בדליפה. עלייה ב-P95 latency לאחר 2–4 שעות מצביעה על בעיות עם GC או מאגר חיבורים. בנוסף, בדקו את יחס הטופלים המתים ב-PostgreSQL: אם הוא עולה על 10%, זו נפיחות. עבור כל בעיה, אנו מספקים המלצות ספציפיות: מאופטימיזציית קוד ועד שינויי תצורת מסד נתונים.
מה כלול בעבודה שלנו
- ניתוח ארכיטקטורת האפליקציה ופרופיל העומס שלכם.
- פיתוח תרחישי k6 עם התנהגות משתמש ריאלית.
- הגדרת ניטור לזיכרון, חיבורי מסד נתונים ומתארי קבצים.
- ביצוע מבחן ספיגה למשך 8–24 שעות בסביבת staging.
- יצירת גרפים של סדרות זמן וניתוח מגמות רגרסיה.
- דוח מפורט עם ירידות ביצועים שזוהו והמלצות לתיקון.
- ייעוץ לתיקוני קוד ותצורה.
- מבחן חוזר בחינם אם הבעיה לא התגלתה.
ממצאים ופתרונות אופייניים
| בעיה | סימפטום | פתרון |
|---|---|---|
| דליפת EventEmitter (Node.js) | MaxListenersExceededWarning | שימוש ב-emitter.removeListener() או once() |
| חיבורי DB לא סגורים | חיבורים גדלים ב-pg_stat_activity | pool.release() בבלוק finally או pooling ברמת ORM |
| משימות cron מצטברות | משימות רקע כפולות | הוספת נעילת mutex (נעילת Redis) |
| דליפת Redis pub/sub | מספר מנויי ערוצים גדל | ביטול מנוי בעת סגירת חיבור |
הניסיון וההבטחות שלנו
אנחנו עוסקים בבדיקות עומס כבר למעלה מ-7 שנים. יש לנו יותר מ-50 פרויקטים מאחורינו, שבהם מבחני ספיגה מנעו כשלי ייצור קריטיים. אנחנו מבטיחים איכות: לאחר הסיום, תקבלו דוח מפורט עם גרפים והמלצות. אם בעיה תישאר בלתי מזוהה, נערוך מבחן חוזר בחינם.
החיסכון ממניעת אירוע OOM בודד יכול להיות משמעותי, בעוד שההפסדים מדליפת זיכרון שלא התגלתה הם ניכרים.
ציר זמן: הגדרה וביצוע של מבחן ספיגה של 8–24 שעות עם ניתוח מגמות אורכים 2 עד 4 ימי עסקים. נעריך את הפרויקט שלכם תוך יום אחד.
הזמינו מבחן ספיגה מהמהנדסים שלנו—נחשוף ירידות ביצועים נסתרות לפני שהן פוגעות בייצור שלכם. קבלו ייעוץ והערכת פרויקט על ידי יצירת קשר איתנו.







