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

Какая интересная история с благополучным концом была на рабочем проекте?

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

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

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

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

История проблемы с базой данных и благополучный конец

Предистория

Работал я на достаточно крупном проекте — платформа для управления подписками. Пиковая нагрузка: 100K пользователей, 10K одновременных запросов к БД. Всё было хорошо, пока не наступил день, который я никогда не забуду.

Проблема: The Slowdown

Вторник, 11:30 утра. На Slack получаю сообщение от support'а: "Платформа работает как черепаха, пользователи жалуются".

Я запускаю мониторинг (Prometheus + Grafana):

  • CPU: нормально
  • Память: нормально
  • Дисковое пространство: нормально
  • Время ответа API: 8-10 секунд вместо обычных 100-200ms
// Мои первые шаги — логирование SQL запросов
@Configuration
public class LoggingConfiguration {
    @Bean
    public DataSource loggingDataSource(DataSource dataSource) {
        // Включил логирование всех SQL запросов
        // (На production это плохо для performance, но для диагностики критично)
        return LoggingDataSourceProxy.wrap(dataSource);
    }
}

// Результат: обнаружил запросы на 30+ секунд!
SELECT * FROM orders 
WHERE user_id = ? AND status IN ('active', 'pending')
JOIN subscriptions ON ...
JOIN users ON ...
JOIN payments ON ...
JOIN invoices ON ...
// Без индексов на WHERE условиях!

Диагностика: N+1 Query Problem

// Вот что происходило в коде:
@Entity
public class User {
    @OneToMany
    private List<Order> orders; // Без @EntityGraph!
}

@Service
public class OrderService {
    
    @Autowired
    private UserRepository userRepository;
    
    public List<OrderResponse> getUserOrders(Long userId) {
        // 1 запрос: SELECT * FROM users WHERE id = ?
        User user = userRepository.findById(userId).orElseThrow();
        
        // На каждый order — ещё запрос!
        for (Order order : user.getOrders()) { // N запросов!
            // SELECT * FROM subscriptions WHERE order_id = ?
            // SELECT * FROM payments WHERE order_id = ?
            processOrder(order);
        }
    }
}

Первое решение (неправильное)

В панике я думал: "Может быть, это deadlock'и в БД?"

// Попробовал просто кэшировать результаты
@Cacheable(value = "orders", key = "#userId")
public List<OrderResponse> getUserOrders(Long userId) {
    return fetchOrders(userId);
}

// Результат: работало быстрее, но маскировало реальную проблему!
// Через час кэш переполнился и приложение упало с OutOfMemoryError

Анализ: Использование JPA Query плана

// Включил логирование планов выполнения
@Configuration
public class JpaConfiguration {
    @Bean
    public PersistenceUnitPostProcessor persistenceUnitPostProcessor() {
        return persistenceUnitInfo -> {
            Map<String, Object> properties = persistenceUnitInfo.getProperties();
            // Логируем SQL explain
            properties.put("hibernate.generate_statistics", true);
        };
    }
}

// Статистика показала:
// - 1 базовый запрос: 10ms
// - 50 запросов на загрузку related данных: 8s
// Итого: 50-200 запросов на один HTTP запрос!

Решение: Entity Graph + JOIN FETCH

// Это было моментом эврики!

// Определили Entity Graph
@NamedEntityGraph(
    name = "User.withOrders",
    attributeNodes = {
        @NamedAttributeNode(value = "orders", subgraph = "orderSubgraph")
    },
    subgraphs = @NamedSubgraph(
        name = "orderSubgraph",
        attributeNodes = {
            @NamedAttributeNode("subscriptions"),
            @NamedAttributeNode("payments")
        }
    )
)
@Entity
public class User {
    @OneToMany
    private List<Order> orders;
}

// Использовали Entity Graph в repository
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
    
    @EntityGraph(value = "User.withOrders", type = EntityGraphType.LOAD)
    Optional<User> findById(Long id);
}

// ИЛИ через JPQL с JOIN FETCH (мой финальный выбор)
@Repository
public interface OrderRepository extends JpaRepository<Order, Long> {
    
    @Query("""
        SELECT DISTINCT o 
        FROM Order o 
        LEFT JOIN FETCH o.subscriptions 
        LEFT JOIN FETCH o.payments 
        WHERE o.userId = :userId 
        AND o.status IN ('active', 'pending')
    """)
    List<Order> findActiveOrdersForUser(@Param("userId") Long userId);
}

Добавили индексы в БД

-- Миграция (Goose)
CREATE INDEX idx_orders_user_id_status 
ON orders(user_id, status) 
WHERE status IN ('active', 'pending');

CREATE INDEX idx_subscriptions_order_id 
ON subscriptions(order_id);

CREATE INDEX idx_payments_order_id 
ON payments(order_id);
// Добавили аннотации в модели для документирования
@Entity
@Table(name = "orders", indexes = {
    @Index(name = "idx_orders_user_id_status", 
            columnList = "user_id,status")
})
public class Order {
    @Id
    private Long id;
    
    @Column(name = "user_id")
    private Long userId;
    
    @Column(name = "status")
    private String status;
}

Результат: Волшебство!

ДО исправления:
- Количество запросов: 150-200 на один HTTP запрос
- Время ответа: 8-10 секунд
- CPU: 95%+
- БД подключений: 150/200 (почти все заняты)

ПОСЛЕ исправления:
- Количество запросов: 2-3 на один HTTP запрос
- Время ответа: 80-120ms
- CPU: 15%
- БД подключений: 5-10 (спокойная жизнь)

Визуальное представление улучшения

Время ответа API (Grafana):

ДО:  |  ████████████████████ (8000ms)
ПОСЛЕ: |  ██ (100ms)
        |
      0ms ────────────────→ 10000ms

Что я выучил из этой истории

// 1. ВСЕГДА профилируй прежде, чем оптимизировать
public void diagnosis() {
    // Используй:
    // - Spring Boot Actuator metrics
    // - Prometheus + Grafana
    // - New Relic
    // - Database query logging
}

// 2. Проблемы N+1 обнаруживаются на testing с реальными данными
@Test
public void testWithManyOrders() {
    // Создать 1000 заказов
    // Загрузить всех пользователей
    // Проверить количество SQL запросов
    int queryCount = queryCountListener.getQueryCount();
    assertTrue(queryCount < 5, "N+1 problem detected!");
}

// 3. Индексы критичны
// @Index на @Entity
// + мониторинг unused индексов
// + EXPLAIN ANALYZE для slow queries

// 4. Entity Graph vs JOIN FETCH vs Query Hints
// - Entity Graph: декларативный, переиспользуемый
// - JOIN FETCH: гибкий, но нужен в каждом запросе
// - Lazy loading + Hibernate.initialize(): худший вариант

// 5. Кэширование — последняя оптимизация
// Не кэшируй плохо спроектированный запрос!

Как это предотвратить в новых проектах

public class BestPractices {
    
    // 1. Используй @EntityGraph из коробки
    public Optional<User> findUserWithEverything(Long userId) {
        // ...
    }
    
    // 2. Тестируй N+1 на Unit тестах
    @Test
    void testNoNPlusOneQueries() {
        // Использовать QueryCountListener
    }
    
    // 3. Мониторь query count в production
    @Transactional
    public void monitorQueries() {
        // Логируй количество запросов
        //Alert если > N (зависит от endpoint)
    }
    
    // 4. Code review для всех запросов к БД
    // - Есть ли JOIN FETCH или @EntityGraph?
    // - Есть ли индексы?
    // - Есть ли WHERE условия?
}

Благополучный конец

После исправления:

  • SLA улучшился с 95% до 99.9%
  • Стоимость облака упала на 60% (меньше серверов)
  • Дерегулировал alert'ы, которые звонили каждый час
  • Команда была счастлива (включая меня после 36 часов без сна)
  • На постмортеме решили:
    • Всегда писать тесты на количество SQL запросов
    • Требовать Entity Graph для всех fetch операций
    • Проводить регулярные аудиты медленных запросов

Мораль истории

public class Lesson {
    // 1. Измеряй перед тем, как оптимизировать
    // 2. N+1 проблема — частая ошибка Hibernate/JPA
    // 3. JOIN FETCH и Entity Graph — твои друзья
    // 4. Индексы спасают жизни
    // 5. Профилирование и мониторинг критичны
    
    // "От героической работы в последний момент
    // переходи к систематическому тестированию перформанса"
}

Эта история преподала мне одноиз самых ценных уроков в карьере: хорошее проектирование и архитектура намного дешевле, чем ночные вызовы из-за production падений!