Go API перегружен: как ограничить число одновременно работающих запросов
Выгрузка заказов строится за несколько секунд. Пока пользователей мало, обработчик работает нормально. После рассылки каждый посетитель запускает ещё одну выгрузку: растёт число goroutine, очередь запросов к базе и время ответа. Добавление таймаута ограничивает ожидание клиента, но до его срабатывания сервер успевает принять очередную порцию работы.
Сначала решите, где ждать
Для тяжёлой синхронной операции полезно явно ограничить число одновременно выполняемых обработчиков. Когда все места заняты, новый запрос сразу получает 503. Клиент понимает, что сейчас ресурс недоступен, а сервер сохраняет возможность завершить уже принятую работу. Такой входной барьер называют admission control. Он особенно полезен перед дорогим запросом к базе, генерацией документа или обращением к зависимому API.
Канал с буфером подходит для учёта занятых мест: отправка занимает место, чтение освобождает. Важна ветка default у select. Без неё новый обработчик заблокируется на отправке, и очередь продолжит расти, только теперь внутри приложения. Размер буфера выражает предел активной работы, а не количество ожидающих посетителей. Начинать следует с небольшого измеренного значения, которое выдерживает конкретная зависимость.
Проверьте отказ при занятом месте
В примере доступно одно место. Первый запрос удерживается каналом release, поэтому второй гарантированно приходит во время его обработки. Это позволяет проверить перегрузку без случайных задержек. После освобождения первого запроса третий снова должен пройти. Сохраните программу в main.go и выполните go run main.go.
package main
import (
"fmt"
"net/http"
"net/http/httptest"
)
func limit(n int, next http.Handler) http.Handler {
slots := make(chan struct{}, n)
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
select {
case slots <- struct{}{}:
defer func() { <-slots }()
default:
http.Error(w, "busy", http.StatusServiceUnavailable)
return
}
next.ServeHTTP(w, r)
})
}
func main() {
entered := make(chan struct{}, 1)
release := make(chan struct{})
done := make(chan struct{})
h := limit(1, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
entered <- struct{}{}
<-release
w.WriteHeader(204)
}))
first := httptest.NewRecorder()
go func() { h.ServeHTTP(first, httptest.NewRequest("GET", "/export", nil)); close(done) }()
<-entered
second := httptest.NewRecorder()
h.ServeHTTP(second, httptest.NewRequest("GET", "/export", nil))
fmt.Println(second.Code)
close(release)
<-done
fmt.Println(first.Code)
third := httptest.NewRecorder()
h.ServeHTTP(third, httptest.NewRequest("GET", "/export", nil))
fmt.Println(third.Code)
}
Программа выводит 503, затем два раза 204. Статус второго запроса получен, пока первый действительно находится внутри защищённого обработчика. Закрытие done подтверждает, что defer уже вернул место. Если освобождать слот только в успешной ветке, любая ранняя ошибка будет постепенно уменьшать доступную вместимость сервиса.
Подключите барьер к конкретной нагрузке
Оборачивайте дорогой маршрут или группу маршрутов с общей зависимостью. Один глобальный лимит для всех URL может позволить экспорту занять места, необходимые лёгкому чтению профиля. Проверка готовности также должна оставаться доступной: иначе кратковременная перегрузка превратится в вывод экземпляра из балансировки и усилит нагрузку на соседей. Отдельные барьеры полезны только тогда, когда соответствуют реальным независимым ресурсам.
503 сообщает о временной недоступности сервера. Если у приложения есть ограничение частоты запросов конкретного пользователя, для него обычно проектируют отдельное правило и ответ 429. Не назначайте точное Retry-After, когда время освобождения ресурса неизвестно. Повтор клиентом должен учитывать характер операции: создание заказа требует своей защиты от повторного выполнения, даже если один из ответов был 503.
Измеряйте активные обработчики, количество отклонений и длительность принятой работы. Если отказов много, но обработчики всё время ждут соединение с базой, увеличение лимита лишь удлинит внутреннюю очередь. Сопоставьте барьер с пулом БД и дедлайном запроса. Для долгих выгрузок разумным следующим шагом бывает сохранённое фоновое задание с отдельным API состояния, где ожидание перестаёт удерживать HTTP-запрос.
Проверьте также ошибки зависимости и панику обработчика: defer возвращает слот при раскрутке стека. При отмене клиента сама операция должна уважать context. Барьер считает работающие вызовы и не умеет принудительно остановить зависший код. Связанный разбор передачи context в HTTP-вызов помогает довести отмену до зависимости.
Не удерживайте место после принятой работы
Если обработчик вызывает следующий сервис и затем возвращает ошибку, это всё равно один завершённый вход в барьер. Добавьте счётчик вызовов зависимости и проверьте, что отклонённый запрос не увеличивает его. При размещении нескольких экземпляров лимит действует отдельно в каждом процессе: четыре реплики с пределом восемь способны одновременно запустить до тридцати двух операций. Если зависимость выдерживает только десять, локальные лимиты нужно согласовать с числом реплик или перенести управление вместимостью к общему ресурсу. При масштабировании этот предел пересматривают вместе с пулом соединений.
Продолжите до взаимодействия сервисов
Локальный барьер определяет, какой запрос начать. Между сервисами предстоит согласовать дедлайн, отказ и безопасный повтор. В платном курсе «Микросервисы на Go: HTTP, PostgreSQL, gRPC, Kafka и Docker» вы будете проектировать HTTP-вызовы, разбирать неизвестный исход POST и проверять сценарии отказа. Это помогает связать поведение клиента с тем, что действительно успело выполниться на сервере. Откройте программу курса «Микросервисы на Go: HTTP, PostgreSQL, gRPC, Kafka и Docker».
Продолжить на практике
- Микросервисы на Go: HTTP, PostgreSQL, gRPC, Kafka и Docker ↗
От работающего Go API — к взаимодействию сервисов, которому можно доверять. Разберитесь, как проектировать контракты, сохранять согласованность данных и восстанавливать обработку после сбоев. HTTP, PostgreSQL, gRPC, Kafka и Docker — через код и наглядные разборы.
