← Назад к вопросам

Что делать если веб-сервис начал потреблять много памяти?

1.0 Junior🔥 51 комментариев
#DevOps и инфраструктура#Django

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI23 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Диагностика и решение проблемы утечки памяти в веб-сервисе

Утечка памяти в веб-сервисе — серьёзная проблема, которая может привести к краху приложения. Я расскажу о систематическом подходе для её решения.

Шаг 1: Подтверждение проблемы (мониторинг)

Сначала нужно подтвердить, что это именно утечка:

import psutil
import os
import time

process = psutil.Process(os.getpid())
memory_snapshots = []
for i in range(10):
    memory_info = process.memory_info()
    rss_mb = memory_info.rss / 1024 / 1024
    memory_snapshots.append(rss_mb)
    print(f"Память: {rss_mb:.2f} MB")
    time.sleep(5)

if memory_snapshots[-1] > memory_snapshots[0] * 1.5:
    print("ВНИМАНИЕ: Утечка памяти!")

Шаг 2: Профилирование памяти

Использую memory_profiler и tracemalloc:

from memory_profiler import profile
import tracemalloc

@profile
def process_request(data):
    large_list = [i for i in range(1000000)]
    return sum(large_list)

tracemalloc.start()
snapshot1 = tracemalloc.take_snapshot()
data = process_request([])
snapshot2 = tracemalloc.take_snapshot()

top_stats = snapshot2.compare_to(snapshot1, 'lineno')
for stat in top_stats[:10]:
    print(stat)

Шаг 3: Анализ объектов в памяти

import gc

def analyze_memory():
    gc.collect()
    from collections import Counter
    objects = gc.get_objects()
    counts = Counter(type(obj).__name__ for obj in objects)
    
    for obj_type, count in counts.most_common(10):
        print(f"{obj_type}: {count}")

analyze_memory()

Шаг 4: Проблемы в веб-приложениях

Частая ошибка — неконтролируемые кеши:

# УТЕЧКА
request_cache = {}

@app.get("/process/{id}")
def process(id: str):
    result = expensive_computation()
    request_cache[id] = result  # Растёт навсегда!
    return result

# ИСПРАВЛЕНО
from functools import lru_cache

@lru_cache(maxsize=128)
def cached_computation(key: str):
    return expensive_computation()

# ИЛИ с TTL
from datetime import datetime, timedelta

class TTLCache:
    def __init__(self, ttl_seconds=3600):
        self.cache = {}
        self.ttl = ttl_seconds
    
    def get(self, key):
        if key in self.cache:
            value, timestamp = self.cache[key]
            if datetime.now() - timestamp < timedelta(seconds=self.ttl):
                return value
            del self.cache[key]
        return None
    
    def set(self, key, value):
        self.cache[key] = (value, datetime.now())

Шаг 5: Слушатели событий

Слушатели часто не удаляются:

# УТЕЧКА
event_listeners = []

def add_listener(callback):
    event_listeners.append(callback)

# ИСПРАВЛЕНО
from weakref import WeakSet

class EventManager:
    def __init__(self):
        self._listeners = WeakSet()
    
    def subscribe(self, callback):
        self._listeners.add(callback)
    
    def notify(self, event):
        for callback in self._listeners:
            if callback:
                callback(event)

# ИЛИ явное управление
class EventManager:
    def __init__(self):
        self._listeners = {}
    
    def subscribe(self, event_type, callback):
        if event_type not in self._listeners:
            self._listeners[event_type] = []
        self._listeners[event_type].append(callback)
    
    def unsubscribe(self, event_type, callback):
        if event_type in self._listeners:
            self._listeners[event_type].remove(callback)

Шаг 6: Потоки и асинхронные задачи

from concurrent.futures import ThreadPoolExecutor
import asyncio

# УТЕЧКА: неограниченные потоки
thread_pool = ThreadPoolExecutor()
for i in range(100000):
    thread_pool.submit(heavy_task)

# ИСПРАВЛЕНО: ограничиваем
thread_pool = ThreadPoolExecutor(max_workers=10)

# Асинхронные утечки
# УТЕЧКА
async def background_work():
    while True:
        await asyncio.sleep(1)

# ИСПРАВЛЕНО
class BackgroundWorker:
    def __init__(self):
        self.task = None
    
    async def start(self):
        self.task = asyncio.create_task(self._work())
    
    async def stop(self):
        if self.task:
            self.task.cancel()
            try:
                await self.task
            except asyncio.CancelledError:
                pass
    
    async def _work(self):
        while True:
            try:
                await asyncio.sleep(1)
            except asyncio.CancelledError:
                break

Шаг 7: Мониторинг в продакшене

from prometheus_client import Gauge, Counter
import gc

memory_gauge = Gauge('app_memory_mb', 'Память в МБ')
objects_gauge = Gauge('app_objects', 'Объекты')

async def monitor():
    while True:
        process = psutil.Process(os.getpid())
        memory_mb = process.memory_info().rss / 1024 / 1024
        memory_gauge.set(memory_mb)
        
        gc.collect()
        objects_gauge.set(len(gc.get_objects()))
        
        if memory_mb > 1024:
            logger.critical(f"Память {memory_mb}MB!")
        
        await asyncio.sleep(60)

Чеклист поиска утечек

  1. Глобальные переменные — растут ли в цикле?
  2. Кеши — есть ли лимит размера?
  3. Слушатели — удаляются ли?
  4. Потоки — отменяются ли?
  5. Задачи — управляются ли?
  6. Циклические ссылки — обрабатываются ли?
  7. Библиотеки — имеют ли утечки?

Вывод: утечка обычно вызвана неконтролируемым ростом кешей, слушателей или задач. Профилирование быстро выявит проблему.

Что делать если веб-сервис начал потреблять много памяти? | PrepBro