Постановка проблемы
Ваше приложение сталкивается с медленным временем отклика, высоким потреблением ресурсов или неудовлетворительным пользовательским опытом. Вам необходимо выявить узкие места и системно внедрить оптимизации.
Процесс оптимизации производительности
┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Measure │───▶│ Identify │───▶│ Optimize │───▶│ Verify │
│ Baseline │ │ Bottlenecks │ │ Target │ │ Impact │
└─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘
│ │
└────────────────────────────────────────────────────────┘
Iterate
1. Производительность базы данных
Выявление медленных запросов
PostgreSQL
-- Включить логирование медленных запросов
ALTER SYSTEM SET log_min_duration_statement = '500'; -- Логировать запросы > 500 ms
SELECT pg_reload_conf();
-- Найти медленные запросы
SELECT
query,
calls,
total_time / 1000 as total_seconds,
mean_time / 1000 as mean_seconds,
rows
FROM pg_stat_statements
ORDER BY total_time DESC
LIMIT 20;
-- Найти отсутствующие индексы
SELECT
schemaname,
relname,
seq_scan,
seq_tup_read,
idx_scan,
idx_tup_fetch,
n_tup_ins,
n_tup_upd,
n_tup_del
FROM pg_stat_user_tables
WHERE seq_scan > 0
ORDER BY seq_tup_read DESC
LIMIT 20;
MySQL
-- Включить лог медленных запросов
SET GLOBAL slow_query_log = 'ON';
SET GLOBAL long_query_time = 1;
SET GLOBAL log_queries_not_using_indexes = 'ON';
-- Найти медленные запросы с помощью Performance Schema
SELECT
DIGEST_TEXT,
COUNT_STAR,
SUM_TIMER_WAIT/1000000000000 as total_seconds,
AVG_TIMER_WAIT/1000000000000 as avg_seconds,
SUM_ROWS_EXAMINED,
SUM_ROWS_SENT
FROM performance_schema.events_statements_summary_by_digest
ORDER BY SUM_TIMER_WAIT DESC
LIMIT 20;
Оптимизация запросов
Добавление отсутствующих индексов
-- Проанализировать план выполнения запроса
EXPLAIN ANALYZE
SELECT * FROM orders
WHERE user_id = 123
AND status = 'pending'
AND created_at > '2024-01-01';
-- Создать составной индекс
CREATE INDEX CONCURRENTLY idx_orders_user_status_date
ON orders(user_id, status, created_at);
-- Частичный индекс для частых запросов
CREATE INDEX CONCURRENTLY idx_orders_pending
ON orders(user_id, created_at)
WHERE status = 'pending';
Оптимизация JOIN
-- Плохо: JOIN больших таблиц без корректных индексов
SELECT u.*, COUNT(o.id) as order_count
FROM users u
LEFT JOIN orders o ON o.user_id = u.id
GROUP BY u.id;
-- Лучше: использовать подзапрос с индексом
SELECT u.*,
(SELECT COUNT(*) FROM orders WHERE user_id = u.id) as order_count
FROM users u;
-- Или использовать materialized view для сложных агрегаций
CREATE MATERIALIZED VIEW user_stats AS
SELECT
user_id,
COUNT(*) as order_count,
SUM(total) as total_spent
FROM orders
GROUP BY user_id;
-- Обновлять периодически
REFRESH MATERIALIZED VIEW CONCURRENTLY user_stats;
Пулинг соединений
PgBouncer для PostgreSQL
# pgbouncer.ini
[databases]
myapp = host=localhost port=5432 dbname=myapp
[pgbouncer]
listen_addr = 0.0.0.0
listen_port = 6432
auth_type = md5
auth_file = /etc/pgbouncer/userlist.txt
pool_mode = transaction
max_client_conn = 1000
default_pool_size = 20
min_pool_size = 5
reserve_pool_size = 5
reserve_pool_timeout = 3
2. Стратегии кэширования
Многоуровневое кэширование
┌─────────────────────────────────────────────────────────────────┐
│ Browser Cache │
│ Static assets, API responses with Cache-Control │
└─────────────────────────────────────────────────────────────────┘
│
┌─────────────────────────────────────────────────────────────────┐
│ CDN Cache │
│ Static files, edge caching for API │
└─────────────────────────────────────────────────────────────────┘
│
┌─────────────────────────────────────────────────────────────────┐
│ Application Cache │
│ Redis, Memcached, In-memory │
└─────────────────────────────────────────────────────────────────┘
│
┌─────────────────────────────────────────────────────────────────┐
│ Database Cache │
│ Query cache, buffer pool │
└─────────────────────────────────────────────────────────────────┘
Реализация кэширования Redis
// Пример кэширования Laravel
class ProductService
{
public function getProduct($id)
{
return Cache::tags(['products'])->remember(
"product:{$id}",
now()->addMinutes(60),
fn() => Product::with('category', 'images')->findOrFail($id)
);
}
public function getPopularProducts()
{
return Cache::remember(
'products:popular',
now()->addMinutes(15),
fn() => Product::popular()->limit(20)->get()
);
}
public function updateProduct($id, $data)
{
$product = Product::findOrFail($id);
$product->update($data);
// Инвалидировать связанные кэши
Cache::tags(['products'])->forget("product:{$id}");
Cache::forget('products:popular');
return $product;
}
}
// Пример кэширования Node.js
const Redis = require('ioredis');
const redis = new Redis(process.env.REDIS_URL);
class CacheService {
async get(key) {
const data = await redis.get(key);
return data ? JSON.parse(data) : null;
}
async set(key, value, ttl = 3600) {
await redis.setex(key, ttl, JSON.stringify(value));
}
async getOrSet(key, fetchFn, ttl = 3600) {
let data = await this.get(key);
if (data) return data;
data = await fetchFn();
await this.set(key, data, ttl);
return data;
}
async invalidatePattern(pattern) {
const keys = await redis.keys(pattern);
if (keys.length > 0) {
await redis.del(...keys);
}
}
}
// Использование
const product = await cache.getOrSet(
`product:${id}`,
() => db.products.findUnique({ where: { id } }),
3600
);
Паттерн Cache-Aside
async function getUser(userId) {
const cacheKey = `user:${userId}`;
// Сначала попробовать кэш
let user = await cache.get(cacheKey);
if (!user) {
// Промах кэша: получить из базы данных
user = await db.users.findUnique({ where: { id: userId } });
if (user) {
// Сохранить в кэш
await cache.set(cacheKey, user, 3600);
}
}
return user;
}
// Инвалидация кэша при обновлении
async function updateUser(userId, data) {
const user = await db.users.update({
where: { id: userId },
data,
});
// Инвалидировать кэш
await cache.delete(`user:${userId}`);
return user;
}
3. Производительность фронтенда
Оптимизация бандла
// vite.config.js
export default defineConfig({
build: {
rollupOptions: {
output: {
manualChunks: {
// Vendor chunk для редко меняющихся зависимостей
vendor: ['react', 'react-dom', 'react-router-dom'],
// Chunk для UI-библиотеки
ui: ['@radix-ui/react-dialog', '@radix-ui/react-dropdown-menu'],
},
},
},
// Включить минификацию и tree-shaking
minify: 'terser',
terserOptions: {
compress: {
drop_console: true,
drop_debugger: true,
},
},
},
// Предварительно собрать зависимости (pre-bundle)
optimizeDeps: {
include: ['react', 'react-dom'],
},
});
Разделение кода (Code Splitting)
// Ленивая загрузка React
import { lazy, Suspense } from 'react';
const Dashboard = lazy(() => import('./pages/Dashboard'));
const Settings = lazy(() => import('./pages/Settings'));
const Reports = lazy(() => import('./pages/Reports'));
function App() {
return (
<Suspense fallback={<LoadingSpinner />}>
<Routes>
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/settings" element={<Settings />} />
<Route path="/reports" element={<Reports />} />
</Routes>
</Suspense>
);
}
Оптимизация изображений
// Компонент Image в Next.js
import Image from 'next/image';
export function ProductImage({ src, alt }) {
return (
<Image
src={src}
alt={alt}
width={400}
height={300}
loading="lazy"
placeholder="blur"
blurDataURL="/placeholder.png"
sizes="(max-width: 768px) 100vw, 50vw"
/>
);
}
// Ручная оптимизация с srcset
<picture>
<source srcset="image.webp" type="image/webp" />
<source srcset="image.jpg" type="image/jpeg" />
<img
src="image.jpg"
alt="Product"
loading="lazy"
decoding="async"
srcset="image-400.jpg 400w, image-800.jpg 800w"
sizes="(max-width: 600px) 400px, 800px"
/>
</picture>
4. Производительность API
Сжатие ответов
// Express со сжатием
const compression = require('compression');
app.use(compression({
filter: (req, res) => {
if (req.headers['x-no-compression']) return false;
return compression.filter(req, res);
},
threshold: 1024, // Сжимать только ответы > 1 KB
}));
HTTP/2 и Keep-Alive
# nginx.conf
http {
# Enable HTTP/2
listen 443 ssl http2;
# Keep-alive settings
keepalive_timeout 65;
keepalive_requests 1000;
# Gzip compression
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_proxied any;
gzip_types text/plain text/css application/json application/javascript;
# Static file caching
location ~* \.(jpg|jpeg|png|gif|ico|css|js|woff2)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
}
Пагинация и выбор полей
// Выбор полей GraphQL
const userResolvers = {
Query: {
users: async (_, { first, after, fields }, { db }) => {
// Выбирать только запрошенные поля
const select = buildSelectFromFields(fields);
return db.users.findMany({
take: first,
skip: after ? 1 : 0,
cursor: after ? { id: after } : undefined,
select,
});
},
},
};
// REST API с разреженными наборами полей (sparse fieldsets)
app.get('/api/users', async (req, res) => {
const fields = req.query.fields?.split(',') || ['id', 'name', 'email'];
const page = parseInt(req.query.page) || 1;
const limit = Math.min(parseInt(req.query.limit) || 20, 100);
const users = await db.users.findMany({
select: Object.fromEntries(fields.map(f => [f, true])),
skip: (page - 1) * limit,
take: limit,
});
res.json({ data: users, meta: { page, limit } });
});
5. Фоновая обработка
Вынесение тяжёлых операций в очередь
// Laravel Job
class ProcessOrderJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public function __construct(public Order $order) {}
public function handle()
{
// Тяжёлая обработка
$this->generateInvoice();
$this->sendNotifications();
$this->updateInventory();
$this->syncExternalSystems();
}
public function failed(\Throwable $exception)
{
Log::error('Order processing failed', [
'order_id' => $this->order->id,
'error' => $exception->getMessage(),
]);
}
}
// Отправить job
ProcessOrderJob::dispatch($order)->onQueue('orders');
Node.js с BullMQ
import { Queue, Worker } from 'bullmq';
const orderQueue = new Queue('orders', {
connection: { host: 'redis', port: 6379 },
});
// Добавить job
await orderQueue.add('process-order', {
orderId: order.id,
}, {
attempts: 3,
backoff: { type: 'exponential', delay: 1000 },
});
// Worker
const worker = new Worker('orders', async (job) => {
const { orderId } = job.data;
await generateInvoice(orderId);
await sendNotifications(orderId);
await updateInventory(orderId);
return { success: true };
}, {
connection: { host: 'redis', port: 6379 },
concurrency: 5,
});
worker.on('completed', (job) => {
console.log(`Job ${job.id} completed`);
});
worker.on('failed', (job, err) => {
console.error(`Job ${job.id} failed:`, err);
});
Мониторинг производительности
Ключевые метрики для отслеживания
// Пользовательские метрики с prom-client
const histogram = new client.Histogram({
name: 'http_request_duration_seconds',
help: 'HTTP request duration',
labelNames: ['method', 'route', 'status'],
buckets: [0.01, 0.05, 0.1, 0.5, 1, 5],
});
const dbQueryDuration = new client.Histogram({
name: 'db_query_duration_seconds',
help: 'Database query duration',
labelNames: ['operation', 'table'],
buckets: [0.001, 0.01, 0.1, 0.5, 1],
});
const cacheHitRatio = new client.Gauge({
name: 'cache_hit_ratio',
help: 'Cache hit ratio',
});
Мышление senior-инженера о производительности
«Первые 60 секунд» на горящем сервере
Когда вы подключаетесь по SSH к серверу под нагрузкой, выполните эти команды по порядку:
# 1. Load averages (растут или снижаются?)
uptime
# 2. Ошибки ядра — OOM kills, ошибки диска?
dmesg | tail
# 3. Общесистемный обзор процессов, памяти, swap, CPU
vmstat 1
# 4. Какой процесс создаёт нагрузку?
pidstat 1
# 5. Задержка диска и насыщение
iostat -xz 1
# 6. Использование памяти и cache
free -m
# 7. Пропускная способность сети
sar -n DEV 1
Судебная экспертиза производительности базы данных
Когда срабатывает пейджер из‑за того, что «база данных тормозит», вам нужна мгновенная видимость происходящего:
-- PostgreSQL: что выполняется ПРЯМО СЕЙЧАС?
SELECT
pid,
usename,
state,
age(clock_timestamp(), query_start) AS duration,
query
FROM pg_stat_activity
WHERE state <> 'idle'
AND query NOT LIKE '%pg_stat_activity%'
AND age(clock_timestamp(), query_start) > interval '5 seconds'
ORDER BY duration DESC;
Совет senior: запрос, зависший в состоянии idle in transaction, препятствует vacuuming, что приводит к раздуванию таблиц. Завершайте такие сессии, если они превышают заданный порог.
Завершение «зомби»-запросов
-- Мягкое завершение (сначала попробуйте это — безопасно останавливает запрос)
SELECT pg_cancel_backend(<pid>);
-- Жёсткое завершение (закрывает сокет — может вызвать 500, если приложение не обрабатывает переподключение)
SELECT pg_terminate_backend(<pid>);
Лимиты файловых дескрипторов Linux
«Too many open files» (EMFILE) — классическая ошибка в production:
# Проверить текущие лимиты
ulimit -Sn # Soft limit
ulimit -Hn # Hard limit
# Исправить навсегда в /etc/security/limits.conf:
* soft nofile 200000
* hard nofile 500000
Проектирование с учётом отказов: Circuit Breakers
В распределённых системах отказ скрыт до момента проявления. Сеть обязательно даст сбой.
Проблема шторма повторных попыток (Retry Storm):
- Service A вызывает Service B. Service B работает медленно.
- Наивный подход: Service A немедленно повторяет попытку 3 раза.
- Результат: Service B с трудом справлялся со 100 req/s. Теперь на него приходится 300 req/s. Он падает ещё сильнее.
Паттерн уровня senior: Exponential Backoff + Jitter
- Подождать 1s, затем 2s, затем 4s.
- Добавить случайный шум (Jitter), чтобы все клиенты не повторяли попытки одновременно.
Машина состояний Circuit Breaker: 1. Closed (Normal): запросы проходят. 2. Open (Broken): доля ошибок > 50%. Немедленно возвращать ошибку. Не вызывать downstream. 3. Half-Open (Testing): после таймаута пропустить 1 запрос. Успех = перейти в Close. Ошибка = снова Open.
Библиотеки: Resilience4j (Java), Polly (.NET), opossum (Node.js).
Bulkheads: изоляция отказов
Если «Image Processing» использует 100% потоков, «Login» не должен падать.
Решение: Thread Pools на каждую зависимость.
- Connection Pool A (Payment): 10 потоков
- Connection Pool B (Images): 10 потоков
- Если Pool B переполнен, Pool A не затрагивается
Чек-лист по производительности
База данных
- [ ] Включено логирование медленных запросов
- [ ] Выявлены и добавлены отсутствующие индексы
- [ ] Настроен пулинг соединений (PgBouncer и т. п.)
- [ ] Результаты запросов кэшируются соответствующим образом
- [ ] Устранены запросы N+1
- [ ] Сессии
idle in transaction мониторятся и завершаются
Приложение
- [ ] Реализовано кэширование ответов
- [ ] Тяжёлые операции вынесены в очереди
- [ ] Выявлены и исправлены утечки памяти
- [ ] Async/await используется корректно
- [ ] Circuit breakers для внешних зависимостей
- [ ] Exponential backoff с jitter при повторных попытках
Фронтенд
- [ ] Оптимизирован размер бандла
- [ ] Реализовано разделение кода (code splitting)
- [ ] Изображения оптимизированы и загружаются лениво
- [ ] Critical CSS встроен (inlined)
- [ ] Кэширование через service worker
Инфраструктура
- [ ] Настроен CDN для статических ресурсов
- [ ] Включён HTTP/2
- [ ] Включено сжатие Gzip/Brotli
- [ ] Настроен connection keep-alive
- [ ] Увеличены лимиты файловых дескрипторов
- [ ] Настроены системные лимиты ресурсов
Связанные статьи Wiki