Главная

Batch inserts на Go с использованием каналов

Batch inserts на Go с использованием каналов

Не так давно, работая в SuperJob, в рамках распила монолита, вынесли в микросервис на Go поиск рекламных объявлений по таргетингам соискателя. Возникла задача логировать в Clickhouse все таргетинги, по которым идет поиск объявлений. Обработчик запросов принимает параматеры (таргетинги) и ищет по ним объявления. Средняя нагрузка на сервис ~ 2470 запросов в минуту. Таким образом создается ~2470 запросов на вставку в БД Clickhouse.

Задача заключалась в том, чтобы не исполнять каждую команду в базу данных по отдельности, а вместо этого сгруппировать все команды вместе, в одну большую операцию.

Go предлагает очень элегантный способ решить данную задачу.

1. Создаем канал

CommandsChannel = make(chan AdvertisingRequest)

2. При каждом поступающем запросе на поиск объявлений добавляем в канал параметры таргетингов

CommandsChannel <- AdvertisingRequest{
    idRequest: uuid.New(),
    ip: uint32(100.64.2.2),
    bid: 500,
    eventId: "64393b4b03ca70.39019462",
    ...,
    createdAt: time.Now(),
}

3. В горутине создаем карту и счетчик

func sync() {
    commands := make(map[int]AdvertisingRequest)
    ctr := 0
    for {
        select {
        case cmd := <-CommandsChannel:
            ctr++
            commands[ctr] = cmd
        }
        case <-time.After(10 * time.Second):
            if ctr > 0 {
                batch, _ := conn.PrepareBatch(context.Background(), "INSERT INTO EventServiceAdvertisingLoggerRequest")

                for id, command := range commands {
                    batch.Append(
                        cmd.idRequest,
                        cmd.ip,
                        cmd.bid,
                        cmd.eventId,
                        ...,
                        cmd.createdAt,
                    )
                    delete(v, id)
                }
                batch.Send()
            }
        ctr = 0
    }
}

В горутине CommandReady происходит оптимизация. Вместо того, чтобы обрабатывать каждую команду по мере ее поступления, горутина использует оператор select, либо для получения команды, либо для тайм-аута после десяти секунд бездействия.

Пока сообщения поступают, оператор select будет выбирать случай получения сообщения и сохранять команду в карте readyCmd.

Когда сообщения перестанут приходить на десять секунд, оператор select перейдет во второй вариант: time.After(10 * time.Second).

Во втором случае карта readyCmd очищается и все команды отправляются как одна операция. Далее в коде большой оператор INSERT, включающий все команды, выполняется для базы данных Clickhouse.

По сути, этот алгоритм очень похож на японский shishi-odoshi (принцип работы показан на картинке статьи).

Текущая логика еще не оптимальна. В настоящее время он не устанавливает максимальный размер пакета, в основном потому, что в настоящее время в этом нет необходимости. В моей производственной среде максимальный размер запроса составляет ~ 220 байт, и этого достаточно, чтобы не беспокоиться об ограничении размера пакета.

Роберт Фатхуллин

Статья Роберт Фатхуллин

Backend Developer