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

Как поступишь если на этапе тестирования есть узкое горлышко

1.0 Junior🔥 141 комментариев
#Soft Skills и карьера

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

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

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

# Как найти и решить узкое горлышко при тестировании

Что такое узкое горлышко (bottleneck)

Это место в коде, где производительность упадает. На этапе тестирования это может быть:

  • Медленная база данных (1000 запросов вместо 1)
  • Медленный третий сервис (платежи, SMS, геокодирование)
  • Неоптимальный алгоритм (O(n²) вместо O(n))
  • Утечка памяти (Memory leak)
  • Блокировка потоков

Шаг 1: Определи где узкое горлышко

Способ 1: Профилирование (Profiling)

# Используешь JProfiler, YourKit, JetBrains Profiler
# Запускаешь тесты под профайлером

java -javaagent:/path/to/yourkit/bin/yjpagent.jar=onexit=snapshot \
     -cp target/classes:target/test-classes \
     org.junit.runner.JUnitCore com.example.MyTest

Профайлер покажет:

  • Какой метод занимает 95% времени
  • Сколько вызовов было
  • Сколько памяти используется

Способ 2: Bench4j (микробенчмарки)

import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.RunnerException;

@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
@State(Scope.Thread)
public class PerformanceBenchmark {
    
    private List<Integer> numbers = new ArrayList<>();
    
    @Setup
    public void setUp() {
        for (int i = 0; i < 1000; i++) {
            numbers.add(i);
        }
    }
    
    @Benchmark
    public int slowSort() {
        // Bubble sort O(n²)
        List<Integer> copy = new ArrayList<>(numbers);
        for (int i = 0; i < copy.size(); i++) {
            for (int j = 0; j < copy.size() - i - 1; j++) {
                if (copy.get(j) > copy.get(j + 1)) {
                    int temp = copy.get(j);
                    copy.set(j, copy.get(j + 1));
                    copy.set(j + 1, temp);
                }
            }
        }
        return copy.size();
    }
    
    @Benchmark
    public int fastSort() {
        // Quick sort O(n log n)
        List<Integer> copy = new ArrayList<>(numbers);
        Collections.sort(copy);
        return copy.size();
    }
}

Вывод покажет, что fastSort в 100 раз быстрее.

Способ 3: System.nanoTime()

Проста, но эффективна для быстрого поиска:

@Test
public void testDatabaseQuery() {
    long start = System.nanoTime();
    
    List<User> users = userRepository.findAll();
    
    long middle = System.nanoTime();
    
    // Обработка
    int count = 0;
    for (User u : users) {
        if (u.isActive()) count++;
    }
    
    long end = System.nanoTime();
    
    System.out.println("Query time: " + (middle - start) / 1_000_000 + "ms");
    System.out.println("Process time: " + (end - middle) / 1_000_000 + "ms");
    // Результат: Query time: 500ms, Process time: 5ms
    // Узкое горлышко — база данных!
}

Шаг 2: Проанализируй причину

Узкое горлышко: Медленная БД

// МЕДЛЕННО: Full table scan
@Test
public void testSlowQuery() {
    long start = System.nanoTime();
    List<Order> orders = orderRepository.findByStatus("PENDING");
    System.out.println("Time: " + (System.nanoTime() - start) / 1_000_000 + "ms");
    // Результат: 5000ms ← ОЧЕНЬ МЕДЛЕННО!
}

// Причина: нет индекса на status
// РЕШЕНИЕ: добавить индекс
CREATE INDEX idx_order_status ON orders(status);

Узкое горлышко: N+1 проблема

// МЕДЛЕННО: N+1 запросов
@Test
public void testNPlusOne() {
    List<User> users = userRepository.findAll(); // 1 запрос
    
    for (User user : users) {
        List<Order> orders = user.getOrders(); // N запросов (каждый пользователь)
    }
    // Итого: 1 + N запросов
}

// РЕШЕНИЕ: JOIN или eager loading
@Query("SELECT u FROM User u LEFT JOIN FETCH u.orders")
List<User> findAllWithOrders();

Узкое горлышко: Неоптимальный алгоритм

// МЕДЛЕННО: O(n²)
@Test
public void testBubbleSort() {
    List<Integer> numbers = new ArrayList<>();
    for (int i = 0; i < 10000; i++) {
        numbers.add((int)(Math.random() * 10000));
    }
    
    long start = System.nanoTime();
    
    // Bubble sort — самый медленный
    for (int i = 0; i < numbers.size(); i++) {
        for (int j = 0; j < numbers.size() - i - 1; j++) {
            if (numbers.get(j) > numbers.get(j + 1)) {
                Collections.swap(numbers, j, j + 1);
            }
        }
    }
    
    System.out.println("Time: " + (System.nanoTime() - start) / 1_000_000 + "ms");
    // Результат: 3000ms+
}

// РЕШЕНИЕ: использовать Collections.sort() → O(n log n)
@Test
public void testQuickSort() {
    // Тот же список
    Collections.sort(numbers);
    // Результат: 10ms
}

Узкое горлышко: Утечка памяти

// ПЛОХО: Static коллекция растет бесконечно
public class CacheService {
    private static Map<String, byte[]> cache = new HashMap<>(); // Static!
    
    public void cacheData(String key, byte[] data) {
        cache.put(key, data); // Никогда не удаляется
    }
    // После часа работы: 10GB памяти потреблено
}

// РЕШЕНИЕ: использовать Guava Cache с TTL
public class CacheService {
    private final Cache<String, byte[]> cache = CacheBuilder.newBuilder()
        .expireAfterAccess(1, TimeUnit.HOURS)
        .maximumSize(10000)
        .build();
    
    public void cacheData(String key, byte[] data) {
        cache.put(key, data);
    }
}

Шаг 3: Реши проблему

Стратегия 1: Кэширование

@Service
public class UserService {
    @Autowired
    private UserRepository userRepository;
    
    // Результаты кэшируются на 5 минут
    @Cacheable(value = "users", key = "#id")
    public User getUserById(Long id) {
        System.out.println("DB query");
        return userRepository.findById(id).orElse(null);
    }
    
    @CacheEvict(value = "users", key = "#id")
    public void updateUser(Long id, User user) {
        userRepository.save(user);
    }
}

// Результат:
// Первый вызов: "DB query" → идёт в БД
// Второй вызов (в течение 5 мин): кэш → нет запроса к БД

Стратегия 2: Асинхронность

@Service
public class OrderService {
    @Autowired
    private PaymentGateway paymentGateway;
    
    // Медленный платёж обрабатывается в фоне
    @Async
    public CompletableFuture<PaymentResult> processPaymentAsync(Order order) {
        PaymentResult result = paymentGateway.process(order);
        return CompletableFuture.completedFuture(result);
    }
}

// Результат: запрос возвращается за 50ms вместо 5000ms (платеж идет в фоне)

Стратегия 3: Параллельная обработка

@Test
public void testParallelProcessing() {
    List<Integer> numbers = new ArrayList<>();
    for (int i = 0; i < 1_000_000; i++) {
        numbers.add(i);
    }
    
    long start = System.nanoTime();
    
    // Последовательная обработка
    long sum1 = numbers.stream()
        .filter(n -> n % 2 == 0)
        .mapToLong(Integer::longValue)
        .sum();
    
    long sequentialTime = System.nanoTime() - start;
    
    start = System.nanoTime();
    
    // Параллельная обработка
    long sum2 = numbers.parallelStream()
        .filter(n -> n % 2 == 0)
        .mapToLong(Integer::longValue)
        .sum();
    
    long parallelTime = System.nanoTime() - start;
    
    System.out.println("Sequential: " + sequentialTime / 1_000_000 + "ms");
    System.out.println("Parallel: " + parallelTime / 1_000_000 + "ms");
    // Sequential: 100ms
    // Parallel: 30ms (на 4-ядерном процессоре)
}

Пример: Полный анализ узкого горлышка

@Test
public void testEndToEnd() {
    // Сценарий: сервис медленно обрабатывает заказы
    
    List<Order> orders = generateOrders(10000); // 10K заказов
    
    // Время 1: Загрузка заказов
    long t1 = System.nanoTime();
    List<Order> loaded = orderRepository.findAll();
    long loadTime = System.nanoTime() - t1;
    System.out.println("Load orders: " + loadTime / 1_000_000 + "ms");
    
    // Время 2: Для каждого заказа загружаем пользователя
    long t2 = System.nanoTime();
    for (Order order : loaded) {
        User user = userRepository.findById(order.getUserId()).orElse(null);
        // Используем пользователя
    }
    long userLoadTime = System.nanoTime() - t2;
    System.out.println("Load users (N+1): " + userLoadTime / 1_000_000 + "ms");
    
    // Время 3: Обработка
    long t3 = System.nanoTime();
    int processed = 0;
    for (Order order : loaded) {
        if (order.getTotal() > 100) {
            processed++;
        }
    }
    long processTime = System.nanoTime() - t3;
    System.out.println("Process: " + processTime / 1_000_000 + "ms");
    
    // Результат:
    // Load orders: 200ms
    // Load users (N+1): 5000ms ← УЗКОЕ ГОРЛЫШКО!
    // Process: 10ms
    
    // РЕШЕНИЕ: JOIN вместо N+1
    // @Query("SELECT o FROM Order o JOIN FETCH o.user")
    // List<Order> findAllWithUser();
}

Процесс решения узкого горлышка

1. ПРОФИЛИРУЙ
   ↓
2. НАЙДИ что медленно (DB, API, алгоритм, память)
   ↓
3. ИЗМЕРЬ текущее время (baseline)
   ↓
4. ПРИМЕНИ решение (индекс, кэш, асинхронность, JOIN)
   ↓
5. ИЗМЕРЬ новое время
   ↓
6. ЕСЛИ улучшение < 50% → вернись к шагу 3
   ↓
7. ЗАКОММИТИ решение

Инструменты для анализа

  • JProfiler — визуальный профайлер
  • YourKit — детальный анализ памяти
  • JMH — бенчмарки
  • EXPLAIN ANALYZE — план выполнения SQL
  • VisualVM — встроенный в JDK монитор

Совет для собеседования

Если спросят: "На тестировании обнаружили узкое горлышко, как поступишь?"

Отвечай: "1. Профилирую код чтобы найти где именно упадок производительности 2. Анализирую причину (DB, алгоритм, утечка памяти) 3. Применяю решение (индекс, кэш, JOIN вместо N+1, асинхронность) 4. Измеряю улучшение 5. Документирую что было сделано"

Это покажет системное мышление.