כיצד לזהות באופן שיטתי צוואר בקבוק בביצועים
בדיקת העומס שלך מראה ירידה בביצועים: זמן האחזור קופץ מ-50 אלפיות שנייה ל-2000 אלפיות שנייה, והלוגים שותקים. ניתוח צוואר בקבוק מקיף זה חושף גורמי שורש. ניתוח הביצועים שלנו זוכה לאמון לקוחות ארגוניים כבר למעלה מעשור. במקרה אחד, הגורם היה p95 latency איטי בנתיב חם — החלפתו ב-Высокая latency или ошибки │ ├── p95 latency высокая, CPU < 70%, memory ОК │ └── → База данных: медленные запросы, блокировки, N+1 │ ├── CPU 90–100%, latency растёт пропорционально │ └── → Вычислительный bottleneck: профилировать CPU-hot paths │ ├── Memory растёт, swap активен │ └── → Утечка памяти или heap too small │ ├── ENOMEM / EMFILE / ECONNREFUSED │ └── → Системные лимиты: ulimit, file descriptors, TCP backlog │ └── Ошибки 502/504, приложение ОК └── → Nginx upstream, load balancer timeout הפחיתה את זמן האחזור p95 מ-200 אלפיות שנייה ל-30 אלפיות שנייה (הפחתה של 85%), ועלויות התשתית ירדו ביותר מ-60% (חיסכון של 7,000 דולר לחודש). הגישה פשוטה: מדוד → מצא → חסל → חזור.
מסגרת אבחון: ממדדים לצוואר בקבוק
ראשית, הסתכל על מדדים ברמה גבוהה. אם -- Запущенные запросы прямо сейчас (выполнять во время теста) SELECT pid, now() - query_start AS duration, state, wait_event_type, wait_event, left(query, 100) AS query_preview FROM pg_stat_activity WHERE state != 'idle' AND query NOT LIKE '%pg_stat_activity%' ORDER BY duration DESC; -- Блокировки: кто кого блокирует SELECT blocked.pid, blocked.query, blocking.pid AS blocking_pid, blocking.query AS blocking_query FROM pg_stat_activity blocked JOIN pg_stat_activity blocking ON blocking.pid = ANY(pg_blocking_pids(blocked.pid)) WHERE blocked.cardinality(pg_blocking_pids(blocked.pid)) > 0; -- Самые тяжёлые запросы (pg_stat_statements) SELECT query, calls, mean_exec_time, total_exec_time, stddev_exec_time, rows FROM pg_stat_statements ORDER BY total_exec_time DESC LIMIT 20; -- Missing indexes: sequential scans на больших таблицах SELECT relname, seq_scan, seq_tup_read, idx_scan, seq_tup_read / nullif(seq_scan, 0) AS avg_rows_per_seqscan FROM pg_stat_user_tables WHERE seq_scan > 100 AND seq_tup_read > 10000 ORDER BY seq_tup_read DESC; גבוה אבל ה-CPU מתחת ל-70%, חשד במסד הנתונים או בקריאות חיצוניות. אם ה-CPU הוא 90–100%, פרופיל את הקוד. אם הזיכרון גדל ו-swap פעיל, חפש דליפה. שגיאות מערכת (ENOMEM, EMFILE) מצביעות על מגבלות מערכת הפעלה. שגיאות 502/504 — בדוק את מאזן העומס.
Высокая latency или ошибки
│ ├── p95 latency высокая, CPU < 70%, memory ОК
│ └── → База данных: медленные запросы, блокировки, N+1
│ ├── CPU 90–100%, latency растёт пропорционально
│ └── → Вычислительный bottleneck: профилировать CPU-hot paths
│ ├── Memory растёт, swap активен
│ └── → Утечка памяти или heap too small
│ ├── ENOMEM / EMFILE / ECONNREFUSED
│ └── → Системные лимиты: ulimit, file descriptors, TCP backlog
│ └── Ошибки 502/504, приложение ОК
└── → Nginx upstream, load balancer timeout אופטימיזציית מסד נתונים: שאילתות איטיות ו-N+1
כיצד למצוא שאילתות איטיות ב-PostgreSQL?
הרץ שאילתות אלה במהלך בדיקת העומס. הראשונה מציגה שאילתות פעילות עם משך זמן. השנייה — נעילות (מי ממתין למי). השלישית — השאילתות הכבדות ביותר לפי זמן כולל. הרביעית — טבלאות עם סריקות רציפות (אינדקסים חסרים פוטנציאליים). לפרטים נוספים, ראה את תיעוד PostgreSQL על pg_stat_statements.
-- Запущенные запросы прямо сейчас (выполнять во время теста)
SELECT pid, now() - query_start AS duration, state, wait_event_type, wait_event, left(query, 100) AS query_preview
FROM pg_stat_activity
WHERE state != 'idle' AND query NOT LIKE '%pg_stat_activity%'
ORDER BY duration DESC;
-- Блокировки: кто кого блокирует
SELECT blocked.pid, blocked.query, blocking.pid AS blocking_pid, blocking.query AS blocking_query
FROM pg_stat_activity blocked
JOIN pg_stat_activity blocking ON blocking.pid = ANY(pg_blocking_pids(blocked.pid))
WHERE blocked.cardinality(pg_blocking_pids(blocked.pid)) > 0;
-- Самые тяжёлые запросы (pg_stat_statements)
SELECT query, calls, mean_exec_time, total_exec_time, stddev_exec_time, rows
FROM pg_stat_statements
ORDER BY total_exec_time DESC
LIMIT 20;
-- Missing indexes: sequential scans на больших таблицах
SELECT relname, seq_scan, seq_tup_read, idx_scan, seq_tup_read / nullif(seq_scan, 0) AS avg_rows_per_seqscan
FROM pg_stat_user_tables
WHERE seq_scan > 100 AND seq_tup_read > 10000
ORDER BY seq_tup_read DESC; מדוע שאילתות N+1 הן גורם נפוץ לירידה בביצועים?
N+1 מתרחש כאשר ORM מוציא שאילתה נפרדת עבור כל אובייקט אב כדי להביא נתונים קשורים. עם 1000 משתמשים, מדובר ב-1001 שאילתות במקום JOIN יחיד. תסמינים: מספר active connections למסד הנתונים שווה למספר המשתמשים הווירטואליים, ו-pg_stat_statements מציג את אותה שאילתה עם מספר גדול של קריאות. פתרון: טעינה מוקדמת, DataLoader, או JOIN ידני. אנו מתמחים בחיסול שאילתות N+1. שימוש ב-kill -USR1 <pid> לזיהוי N+1 מהיר פי 10 מניתוח לוגים ידני.
פרופילינג יישומים: Node.js, Python ומאגר חיבורים
פרופילינג CPU ב-Node.js
השיטה הפשוטה ביותר היא להפעיל את פרופיל ה-V8 באמצעות אות. הרץ את הקוד תחת עומס, שלח cpu-profile.cpuprofile, לאחר 30 שניות קבל // server.js — включить V8 profiling через сигнал process.on('SIGUSR1', () => { const { Session } = require('inspector') const session = new Session() session.connect() session.post('Profiler.enable') session.post('Profiler.start') // Профилировать 30 секунд setTimeout(() => { session.post('Profiler.stop', (err, { profile }) => { require('fs').writeFileSync('./cpu-profile.cpuprofile', JSON.stringify(profile)) console.log('CPU profile saved to cpu-profile.cpuprofile') session.disconnect() }) }, 30000) }) // Запустить под нагрузкой: kill -USR1 <pid> // Открыть в Chrome DevTools → More Tools → JavaScript Profiler , פתח ב-Chrome DevTools.
// server.js — включить V8 profiling через сигнал
process.on('SIGUSR1', () => {
const { Session } = require('inspector')
const session = new Session()
session.connect()
session.post('Profiler.enable')
session.post('Profiler.start')
// Профилировать 30 секунд
setTimeout(() => {
session.post('Profiler.stop', (err, { profile }) => {
require('fs').writeFileSync('./cpu-profile.cpuprofile', JSON.stringify(profile))
console.log('CPU profile saved to cpu-profile.cpuprofile')
session.disconnect()
})
}, 30000)
})
// Запустить под нагрузкой: kill -USR1 <pid>
// Открыть в Chrome DevTools → More Tools → JavaScript Profiler
חלופה היא להשתמש בכלי 0x כדי ליצור flamegraph. הוא אוסף מחסניות קריאות ומציג גרף אינטראקטיבי שבו רוחב העמודה מייצג זמן ביצוע. ממצאים אופייניים: JSON.parse/stringify בנתיבים חמים, bcrypt עם מקדם עלות גבוה, regex ללא מטמון, פעולות קבצים סינכרוניות.
npm install -g 0x
0x --output-dir profile node server.js &
APP_PID=$!
k6 run tests/load/main.js
kill -USR2 $APP_PID # Откроется flamegraph.html פרופילינג Python תחת עומס
לייצור אנו משתמשים ב-npm install -g 0x 0x --output-dir profile node server.js & APP_PID=$! k6 run tests/load/main.js kill -USR2 $APP_PID # Откроется flamegraph.html — הוא אינו דורש הפעלה מחדש ונותן דוח מפורט לכל בקשה. הוסף middleware שמאפשר פרופילינג באמצעות ?profile=true.
from pyinstrument import Profiler
from flask import request, g
@app.before_request
def start_profiler():
if request.args.get('profile') == 'true':
g.profiler = Profiler()
g.profiler.start()
@app.after_request
def stop_profiler(response):
if hasattr(g, 'profiler'):
g.profiler.stop()
response.data = g.profiler.output_html()
response.content_type = 'text/html'
return response
# Запрос с профилированием: GET /api/posts?profile=true
ניתוח מאגר חיבורים
אם לקוחות ממתינים לחיבור (from pyinstrument import Profiler from flask import request, g @app.before_request def start_profiler(): if request.args.get('profile') == 'true': g.profiler = Profiler() g.profiler.start() @app.after_request def stop_profiler(response): if hasattr(g, 'profiler'): g.profiler.stop() response.data = g.profiler.output_html() response.content_type = 'text/html' return response # Запрос с профилированием: GET /api/posts?profile=true ב-pgBouncer), המאגר קטן מדי. בדוק באמצעות cl_waiting > 0 או שאילתת SQL אל SHOW POOLS;.
-- PostgreSQL: статистика пула соединений
SELECT datname,
count(*) AS total_connections,
count(*) FILTER (WHERE state = 'active') AS active,
count(*) FILTER (WHERE state = 'idle') AS idle,
count(*) FILTER (WHERE wait_event_type = 'Lock') AS waiting_lock
FROM pg_stat_activity
GROUP BY datname; ניתוח סדרות זמן של k6: כיצד למצוא את נקודת הירידה?
הסקריפט מנתח את סדרת הזמן של זמן האחזור p95 ומוצא את הדקה הראשונה שבה הערך חרג מסף (לדוגמה, 500 אלפיות שנייה). זה עוזר לאתר את הירידה לרגע ספציפי בבדיקה.
סקריפט לניתוח סדרות זמן של k6 (לחץ להרחבה)
import json
import pandas as pd
def find_degradation_point(json_results: str):
"""Найти момент деградации по временному ряду метрик"""
records = []
with open(json_results) as f:
for line in f:
try:
record = json.loads(line)
if record.get('type') == 'Point':
records.append({
'timestamp': record['data']['time'],
'metric': record['metric'],
'value': record['data']['value']
})
except:
continue
df = pd.DataFrame(records)
df['timestamp'] = pd.to_datetime(df['timestamp'])
p95_df = df[df['metric'] == 'http_req_duration'].copy()
p95_df = p95_df.set_index('timestamp').resample('1min')['value'].quantile(0.95)
threshold = 500
degradation = p95_df[p95_df > threshold]
if not degradation.empty:
print(f"Degradation detected at: {degradation.index[0]}")
print(f"p95 at degradation: {degradation.iloc[0]:.0f}ms")
else:
print("No degradation detected (all within threshold)")
return p95_df
כלים ואופטימיזציות אופייניות
השוואת כלי פרופילינג
| כלי | היקף | עומק | השפעה על ייצור |
|---|---|---|---|
| pg_stat_statements | PostgreSQL | גבוה | אין |
| פרופיל V8 | Node.js | גבוה | מינימלי |
| pyinstrument | Python | בינוני | אין |
| 0x | Node.js | גבוה | דורש הפעלה מחדש |
| flamegraph | כללי | גבוה | תלוי באספן |
אופטימיזציות אופייניות לאחר ניתוח
| צוואר בקבוק | תסמין | פתרון |
|---|---|---|
| שאילתות N+1 למסד נתונים | שאילתות פעילות במסד הנתונים >> מספר משתמשים וירטואליים | DataLoader / טעינה מוקדמת / JOIN |
| אינדקס חסר | סריקה רציפה על טבלה גדולה | pg_stat_activity |
| סריאליזציית JSON איטית | CPU גבוה, נתיב חם בסריאליזציה | Protobuf / simdjson / msgpack |
| הצפת מאגר חיבורים | -- PostgreSQL: статистика пула соединений SELECT datname, count(*) AS total_connections, count(*) FILTER (WHERE state = 'active') AS active, count(*) FILTER (WHERE state = 'idle') AS idle, count(*) FILTER (WHERE wait_event_type = 'Lock') AS waiting_lock FROM pg_stat_activity GROUP BY datname; ב-pgBouncer | הגדל pool_size או הוסף רפליקות |
| השהאות GC | זמן אחזור לא יציב ללא עומס CPU | הגדל heap, כוונן דגלי GC |
| נעילות טבלה | import json import pandas as pd def find_degradation_point(json_results: str): """Найти момент деградации по временному ряду метрик""" records = [] with open(json_results) as f: for line in f: try: record = json.loads(line) if record.get('type') == 'Point': records.append({ 'timestamp': record['data']['time'], 'metric': record['metric'], 'value': record['data']['value'] }) except: continue df = pd.DataFrame(records) df['timestamp'] = pd.to_datetime(df['timestamp']) p95_df = df[df['metric'] == 'http_req_duration'].copy() p95_df = p95_df.set_index('timestamp').resample('1min')['value'].quantile(0.95) threshold = 500 degradation = p95_df[p95_df > threshold] if not degradation.empty: print(f"Degradation detected at: {degradation.index[0]}") print(f"p95 at degradation: {degradation.iloc[0]:.0f}ms") else: print("No degradation detected (all within threshold)") return p95_df ב-pg_stat | בצע אופטימיזציה לסדר הפעולות, NOWAIT |
תוצאות הניתוח ותהליך ההזמנה
לאחר הניתוח, אנו מספקים:
- דוח עם גרפי סדרות זמן (זמן אחזור, תפוקה, CPU, זיכרון, מדדי מסד נתונים)
- סקריפטים לשחזור העומס
- רשימה מפורטת של צווארי בקבוק עם קטעי קוד ותצורה
- המלצות אופטימיזציה (עדיפות, הערכות מאמץ)
- בדיקת אימות לאחר יישום השינויים
- ייעוץ ארכיטקטוני למניעת בעיות עתידיות
מה כלול בניתוח
החבילה המקיפה שלנו כוללת:
- תיעוד מפורט של הממצאים
- גישה לסקריפטים של בדיקות עומס וכלי פרופילינג
- מפגש הדרכה לצוות שלך על זיהוי צווארי בקבוק
- 30 ימים של תמיכה בדוא"ל לשאלות על אופטימיזציות
ניתוח מלא עם דוח ואימות אורך 1–2 ימי עסקים. התמחור מותאם אישית לפי מורכבות המערכת. צור קשר — נעריך את הפרויקט שלך ונמליץ על ההתקשרות האופטימלית. למהנדסים שלנו יש ניסיון של 10+ שנים בבדיקות עומס ואופטימיזציה. החיסכון הממוצע בעלויות ענן לאחר האופטימיזציות שלנו נע בין 3,000 ל-10,000 דולר לחודש.







