Как правильно настроить свой web сервис для множества одновременных запросов по API?

Есть web сервис на Go (пока на встроенном сервере, а не на fasthttp) со своим документированным API. Сам сервис работает как промежуточное звено к API другого внешнего сервиса и у этого внешнего сервиса есть свои ограничения на число запросов к нему: не больше 10 запросов в секунду. Соответственно, мой сервис делает вынужденную паузу в 100 msc перед каждым запросом на него. Все OK - внешний сервис доволен и не банит.

Проблема. Есть запросы будет делать один пользователь (соблюдая лимит 10 запросов в секунду), то мой сервис будет нормально работать (по крайней мере при его локальном использовании все OK). Но если на удаленный вариант этого сервиса (сейчас он на heroku) 10 пользователей будет одновременно делать запросы по API, то общая нагрузка сервиса возрастет до 100 запросов в секунду. Но так как у меня есть таймауты между запуском горутин-запросов на внешний сервис внутри каждой горутины user-подключения, то получается что множество горутин от юзеров будут вынужденно спать и накапливаться (если я правильно понимаю). Как это все правильно разруливается? И поможет ли установка nginx? Установка nginx перед Go сервером пока в планах.

Тест сервиса через siege (запрос был по API и с возвратом небольшой порции данных) дал такие результаты:

siege -d1 -c50 -r50 -t60s
Transactions:                   2329 hits
Availability:                  99.70 %
Elapsed time:                  59.18 secs
/Data transferred:              0.20 MB
Response time:                  0.68 secs
Transaction rate:              39.36 trans/sec
Throughput:                     0.00 MB/sec
Concurrency:                   26.58
Successful transactions:        2329
Failed transactions:               7
Longest transaction:            1.48
Shortest transaction:           0.55

Код (основной) базового воркера, который запускается из http обработчика в виде горутины:

    for chunkText := range main.Sents.Iter(trs.API.Limit) {

        select {
        case <-ctx.Done():
            cancel = true
            break
        default:
            task := Task{}
            //task := poolTask.Get().(*Task)
            task.Lang = main.Lang
            task.Text = chunkText
            task.Id = chunkId
            task.Size = utf8.RuneCountInString(chunkText)
            chunkId += 1
            main.TaskCount += 1
            main.Wg.Add(1)
            log.Tracef("[WORK %s] [%d] %#v", category, chunkId, task)
            if goTimeOut > 0 {
                time.Sleep(goTimeOut) // 100 msc
            }
            go trs.Run(ctx, task, main) // запрос к внешнему сервису
        }
    }

    main.Wg.Wait()
    close(main.TaskChan)

Ответы (2 шт):

Автор решения: Ainar-G

В качетве одного из вариантов можете попробовать через time.Ticker:

var a = &api{
	tick: time.NewTicker(time.Second / 100),
}
defer a.tick.Stop()
func (a *api) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	<-a.tick.C
	log.Println("called")
}

Ссылка на полный пример.

→ Ссылка
Автор решения: Mark

Я бы предложил вариант разделить обязанности по получению запросов, управлению ими и передаче на исполнение.

Например, сделать три независимых сервиса: генератора заданий, менеджера заданий и исполнителя, и соединить их каналами необходимой емкости. Примерно так:

package main

import (
    "context"
    "sync"
    "time"
)

func main() {

    // контекст и группа ожидания
    ctx, cancel := context.WithCancel(context.Background())
    wg := new(sync.WaitGroup)

    // каналы передачи заданий
    taskInputChan := make(chan *Task, 128) // канал входящих заданий
    taskOutputChan := make(chan *Task, 1)  // канал заданий на исполнение

    // запуск сервера, к которому поступают запросы
    // он будет оформлять их заданиями и передавать в канал taskInputChan
    // канал большой емкости, так что переполнения быть не должно
    server := new(Server)
    wg.Add(1)
    go server.Run(ctx, wg, taskInputChan)

    // запуск менеджера заданий
    // он будет решать какое задание отправить в работу
    // и передавать исполнителю по каналу taskOutputChan
    // в канале может находиться только одно задание
    // менеджер сам решит какое, и если нужно - заменит его 
    wg.Add(1)
    go taskManager(ctx, wg, taskInputChan, taskOutputChan)

    // запуск исполнителя заданий
    // он будет получать задания от менеджера и отправлять их в работу
    wg.Add(1)
    go taskRunner(ctx, wg, taskOutputChan)

    // 10 секунд на работу сервиса
    time.Sleep(time.Second * 10)
    cancel()

    // ожидание окончания работы всех гороутин
    wg.Wait()
}

// Task задание
type Task struct{}

// Server
type Server struct{}

// Run запуск сервера
func (s *Server) Run(ctx context.Context, wg *sync.WaitGroup, taskChan chan *Task) {
    defer wg.Done()
}

// taskManager обработчик входящих заданий
func taskManager(ctx context.Context, wg *sync.WaitGroup, taskInputChan chan *Task, taskOutputChan chan *Task) {

    defer wg.Done()

Loop1:
    for {

        select {

        case <-ctx.Done():
            break Loop1

        // получение и обработка задания
        case task := <-taskInputChan:
            handleTask(task, taskOutputChan)
        }
    }
}

// handleTask отправка последнего задания исполнителю
func handleTask(task *Task, taskOutputChan chan *Task) {

    // тут можно реализовать любую логику

    // очистка канала
    select {
    case <-taskOutputChan:
    default:
    }

    // отправка задания в канал
    select {
    case taskOutputChan <- task:
    default:
    }

}

// taskRunner исполнитель заданий
func taskRunner(ctx context.Context, wg *sync.WaitGroup, taskOutputChan chan *Task) {

    defer wg.Done()
    ticker := time.NewTicker(time.Second)

Loop1:
    for {

        select {

        case <-ctx.Done():
            break Loop1

        case <-ticker.C:
            // получение и отправка задания в работу
            task := <-taskOutputChan
            go RunTask(task)
        }
    }
}

// RunTask отправка задания в работу
func RunTask(task *Task) {
    // запрос к внешнему сервису
}

В данной реализации менеджер самый примитивный и кладет в канал на исполнение последнее полученное задание. Логично иметь для менеджера очередь первостепенных заданий и добавлять их в канал на исполнение по мере того как исполнитель будет забирать предыдущие. Настройка и логика работы менеджера целиком зависит от ваших задач. Если запросы однотипные то следует передавать запрос от каждого клиента или даже кэшировать их, если же запросы наоборот имеют ценность только выполненные все целиком (например загрузка большого объема данных по частям), то следует отдать приоритет полной цепочке запросов от одного клиента.

→ Ссылка