openlang

hostprobe: лимит очереди 8, 4 воркера, но все 100 запросов возвращают 202, а горутин ~100

Делаю hostprobe — небольшой внутренний сервис, в который cron и пара скриптов шлют POST на /probe?host=NAME. Четыре воркера, очередь на 8, а сама проверка пока что просто sleep на 20 секунд (ssh-проверка будет позже). Хочу, чтобы сервис не раздувался, если какой-нибудь скрипт сорвётся с цепи.

Тест: hey -n 100 -c 100. Все 100 ответили 202 за пару миллисекунд. Я ожидал, что с 9-го запроса они будут замедляться или отклоняться. /stats показывает queue len=8 cap=8 и 4 running, так что канал и воркеры вроде в порядке. Но runtime.NumGoroutine() около 100, а /debug/pprof/goroutine?debug=1 показывает одну большую группу, которая висит в одном и том же месте. На 1000 запросов стало около 1000, при этом очередь так и осталась на 8. Память в Activity Monitor тоже растёт понемногу после каждой пачки запросов.

Хендлер и пул я давно не трогал, так что по сути смотрю на код, который уже забыл. Хендлер:

job := Job{Host: host, Enqueued: time.Now()}

go func() {
	q.jobs <- job
}()

atomic.AddInt64(&q.accepted, 1)
w.WriteHeader(http.StatusAccepted)
fmt.Fprintf(w, "queued %s\n", host)

Пул обычный: for job := range q.jobs в 4 горутинах и make(chan Job, cfg.QueueDepth).

Откуда берутся лишние горутины? Я подозреваю воркеры или канал, но по счётчикам с ними всё нормально.

Переведено с английскогоПоказать оригинал

Разговор продолжается 3

Следуя доводам
  1. Ссылка на ответ

    Мне кажется, весь ответ — это 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» — слегка щедрое слово для того, что говорил старый код. Там было скорее «обещал поставить в очередь, когда-нибудь».

    Переведено с английскогоПоказать оригинал

Другие стороны того же вопроса

  1. Контекст отменён, но горутины-воркеры живы после первой ошибки (всегда workers + 2)

    Всем привет, Пишу небольшой CLI на Go, shotcheck, который проверяет каждый PNG в папке на точное соответствие размеру, которы…

    Ответы 4Последняя активность go
  2. Пометка фильтра как async не помогла с фризом

    ledgerscan, поиск по 400k записей. сделал run async и вызываю через await из main, думал, что 100 мс heartbeat Timer продолжи…

    Ответы 2Последняя активность dart
  3. await for по ReceivePort никогда не завершается после того, как изоляты закончили

    всем привет, у меня небольшая тулза на Dart, она делает сводку по CSV с температурами, и я запускаю по одному изоляту на файл…

    Ответы 2Последняя активность dart