Мне кажется, весь ответ — это go func() { q.jobs <- job }(). Канал работает как надо: 8 лежат в буфере, 4 заняты воркерами, а каждая следующая отправка блокируется. Но каждая заблокированная отправка — это отдельная горутина, запаркованная на отправке в канал, а хендлер уже пошёл дальше и отдал 202. То есть очередь никого не сдерживает, у неё просто появилась за спиной комната ожидания из горутин. Это и есть ваша большая группа в pprof, и поэтому 1000 запросов дают около 1000 горутин. Каждая держит стек и ещё сам job, так что рост памяти после каждой пачки логично сходится.
Если хотите, чтобы с 9-го запроса отклоняло, делайте отправку прямо в хендлере через select:
select {
case q.jobs <- job:
atomic.AddInt64(&q.accepted, 1)
w.WriteHeader(http.StatusAccepted)
fmt.Fprintf(w, "queued %s\n", host)
case <-r.Context().Done():
return
default:
http.Error(w, "queue full", http.StatusServiceUnavailable)
}
С default отказ приходит сразу. Уберите default, если предпочитаете, чтобы вызывающая сторона ждала: case с контекстом позволит отвалиться, когда клиент уйдёт. Для cron-скрипта обычно отдают 503 (или 429, если хочется) с Retry-After.
Ещё мне кажется, что «queued» — слегка щедрое слово для того, что говорил старый код. Там было скорее «обещал поставить в очередь, когда-нибудь».
Переведено с английского·Показать оригиналI think the go func() { q.jobs <- job }() is your whole answer. The channel is doing its job: 8 sit in the buffer, 4 are held by workers, and every other send blocks. But each blocked send is its own goroutine, parked on the channel send, and the handler has already moved on to the 202. So the queue never pushes back on anyone, it just gets a waiting room of goroutines behind it. That's your big group in pprof, and it's why 1000 requests gives you about 1000 goroutines. Each one holds a stack and the job too, so memory creeping up after each burst fits.
If you want the 9th onward rejected, do the send in the handler with a select:
select {
case q.jobs <- job:
atomic.AddInt64(&q.accepted, 1)
w.WriteHeader(http.StatusAccepted)
fmt.Fprintf(w, "queued %s\n", host)
case <-r.Context().Done():
return
default:
http.Error(w, "queue full", http.StatusServiceUnavailable)
}
With the default it fails straight away. Drop the default if you'd rather the caller waits, since the context case lets it give up when the client goes. A 503 (or 429, if you fancy it) with a Retry-After is the usual thing for a cron script.
Also, I think "queued" is a slightly generous word for what the old code said. It was more "promised to queue, eventually".