API, що легко проходить стадію тестування (staging), може розвалитися в ту саму мить, коли на нього навалиться реальний трафік. Поломка рідко буває драматичною. Це смерть від тисячі порізів. Заблокований потік тут. Зайвий запит там. При малому навантаженні ці дрібні неефективності непомітні. Під тиском продакшну вони накопичуються, призводячи до виснаження пулу потоків (thread pool starvation), перевантаження бази даних та часу відповіді, що підриває довіру користувачів. Тюнінг продуктивності — це не пошук одного магічного рішення. Це побудова системи, де кожен рівень враховує обмеженість потоків, пам'яті та з'єднань з базою даних. Коли ви ставитеся до цих ресурсів як до обмежених, ви перестаєте реагувати на збої та починаєте їх запобігати.
Усуньте Sync-over-Async, поки вона не вбила вас
Найруйнівнішим патерном у високонавантажених застосунках ASP.NET Core є sync-over-async. Ви бачите це, коли хтось викликає .Result або .Wait() на асинхронному методі, бо йому потрібне значення негайно і він не хоче рефакторити стек викликів. Це рішення блокує потік, що викликає метод. Потік простоює, чекаючи на роботу, яка вже виконується деінде, але середовище виконання (runtime) не може використати його для іншого запиту.
Коли так робить достатньо запитів, пул потоків виснажується. Графік завантаження CPU виглядає здоровим, бо процесори не зайняті, проте затримка (latency) стрімко зростає. Запити накопичуються в черзі, чекаючи на потоки, які ніколи не звільняться. Виправлення є механічним, але потребує дисципліни: використовуйте await по всьому стеку викликів. Якщо метод викликає асинхронний API, він сам має бути асинхронним. Тут немає коротких шляхів. Кожне усунене синхронне блокування звільняє простір для реального трафіку.
Припиніть витрачати ресурси на відключених клієнтів
Клієнти відключаються. Браузери закривають вкладки. Мобільні додатки втрачають сигнал. Якщо ваш сервер не знає, що клієнт пішов, він продовжує виконувати запити до бази даних, парсити JSON і витрачати потоки на відповідь, яку ніхто не отримає. Передавайте CancellationToken у кожну асинхронну операцію, яка його підтримує. Це стосується запитів Entity Framework, HTTP-викликів через HttpClient та будь-якої тривалої фонової роботи.
Коли з'єднання розривається, токен ініціює скасування, і робота негайно припиняється. Це зберігає цикли CPU бази даних і швидше повертає потоки в пул. Це невелика зміна в сигнатурах методів, яка окупається під навантаженням.
Кешуйте те, що не змінюється
Ваш каталог товарів, ймовірно, не змінюється між кожним запитом. Ваші прапорці конфігурації точно не змінюються. Проте багато API неодноразово звертаються до бази даних за одними й тими самими статичними даними. Output caching в ASP.NET Core дозволяє зберігати відрендерені відповіді та віддавати їх безпосередньо з пам'яті, більше не звертаючись до контролерів чи бази даних.
Використовуйте інвалідацію на основі тегів обережно. Коли ви оновлюєте товар, інвалідуйте лише тег, пов'язаний із цією категорією або позицією. Вам не потрібно очищати весь кеш. Це дозволяє підтримувати високий показник cache hit ratio та низьку кількість запитів до бази даних.
Спочатку виправте доступ до даних
Виклики бази даних споживають більшу частину часу запиту в більшості API. Перш ніж оптимізувати щось інше, подивіться сюди.
Якщо ви робите запит до даних лише для того, щоб відобразити їх, і не плануєте їх оновлювати, додайте AsNoTracking() до ваших запитів Entity Framework. EF Core пропускає відстеження змін (change tracking) та створення знімків (snapshot creation)
