Перейти к содержимому
ГоМастер

Поиск по урокам

Найдите урок, курс, термин или раздел

Урок 24 из 25 ~18 мин +50 XP

Пул соединений и производительность

mongo производительность складывается из трёх слоёв, и слой драйвера — лишь первый: пул соединений решает, сколько операций поедет одновременно, план запроса — прочитает ли сервер десять документов или всю коллекцию, а конвейер агрегаций — где произойдут вычисления. В этом уроке разберём maxpoolsize go-клиента с соседними настройками, исчерпание пула и его диагностику, explain как рентген медленных запросов и приёмы удешевления агрегаций. Все цифры — из исходников драйвера v2: дефолты пула, поведение при блокировке и события мониторинга, без фольклора из форумов.

Содержание урока · 10 разделов

Пятничный вечер, дашборд менеджеров грузится восемь секунд, все смотрят в сторону базы. Первая реакция — поднять лимиты: больше соединений, больше памяти, быстрее машина. Опыт показывает, что в девяти случаях из десяти узкое место сидит не там: сто коннектов простаивают, пока один медленный запрос держит сервер за горло, а объяснение запроса показывает скан всей коллекции ради десятка строк. Производительность mongo-сервиса — это дисциплина из трёх уровней, и важно понимать, что настраивается в клиенте, что — планом запроса, а что — формой конвейера. В этом уроке пройдём все три: сначала пул соединений с его потолками и очередями, затем индексы против сканов с инструментом проверки, потом агрегации с правилом «сузить, потом свернуть». Каждый раздел заканчивается диагностируемым симптомом: что вы увидите в логах, метриках и explain, когда уровень плохо настроен.

Пул соединений: как операция берёт коннект#

Клиент драйвера держит пул соединений к каждому серверу отдельным хозяйством: операция берёт коннект из пула, выполняет команду и возвращает его обратно. Дефолтный потолок — сто соединений на сервер: параметр maxPoolSize, а ноль означает отсутствие лимита. Когда потолок достигнут, новые операции не разворачиваются и не падают — они блокируются в очереди до освобождения коннекта или до дедлайна своего контекста. Отсюда простая арифметика пропускной способности: сто коннектов и операция в пятьдесят миллисекунд дают около двух тысяч операций в секунду на сервер, а тот же пул с операциями по секунде — сотню. Рядом живут три настройки, которые дополняют потолок. minPoolSize поддерживает минимальный запас готовых коннектов: по умолчанию ноль, и фоновая горутина клиента подогревает запас, если его опустошили. maxConnecting ограничивает, сколько рукопожатий устанавливается одновременно, — по умолчанию два, чтобы всплеск нагрузки не обрушил на сервер сотню параллельных подключений. Сердцебиение в десять секунд держит карту живости серверов свежей, а connectTimeout ограничивает установку одного соединения тридцатью секундами. Все пять параметров задаются через options.Client() при создании клиента, и менять их стоит осознанно: дефолты подобраны так, чтобы обычный сервис не трогал их вовсе.

Клиент с настроенным пулом

GO
client, err := mongo.Connect(options.Client().
        ApplyURI("mongodb://localhost:27017").
        SetMaxPoolSize(50).              // потолок коннектов к каждому серверу
        SetMinPoolSize(5).               // фоновая поддержка запаса
        SetMaxConnecting(4).             // параллельных рукопожатий
        SetMaxConnIdleTime(5*time.Minute).
        SetConnectTimeout(10*time.Second).
        SetHeartbeatInterval(10*time.Second))
if err != nil {
        return fmt.Errorf("connect: %w", err)
}
defer client.Disconnect(context.Background())

Каждый метод задаёт один параметр пула: потолок в пятьдесят коннектов на сервер, запас из пяти готовых, не более четырёх рукопожатий одновременно. SetMaxConnIdleTime закрывает простаивающие коннекты через пять минут — против нулевого дефолта, когда коннект живёт вечно. Пул на каждый сервер свой, поэтому цифры относятся к mongod или каждой ноде реплики отдельно.

Параметры пула: дефолты и смысл#

ПараметрДефолтЗа что отвечает
maxPoolSize100 на серверпотолок коннектов; при исчерпании операции ждут; 0 — без лимита
minPoolSize0минимальный запас готовых коннектов, поддерживается фоном
maxConnecting2сколько соединений устанавливается одновременно
maxIdleTimeMS0 — вечночерез сколько простоя коннект закрывается (SetMaxConnIdleTime)
connectTimeoutMS30000лимит на установку одного соединения (SetConnectTimeout)
heartbeatFrequencyMS10000частота проверки живости серверов (SetHeartbeatInterval)
serverSelectionTimeoutMS30000ожидание подходящего живого сервера перед операцией

Исчерпание пула: что видит операция#

Очередь за коннектом имеет предел — дедлайн контекста операции. Если все сто коннектов заняты, а контекст истёк раньше, чем кто-то вернул свой, операция получает WaitQueueTimeoutError с текстом «timed out while checking out a connection from connection pool»: внутри сидит причина из контекста, а предикат IsTimeout из урока про ошибки возвращает на него true. Кто держит коннекты долго? Незакрытый курсор: Find возвращает соединение в пул только после исчерпания всех батчей или явного Close — горутина, прочитавшая первую страницу и вышедшая по return, утащила коннект навсегда. Долгая агрегация и транзакция держат коннект на всё время работы. Симптом исчерпания характерен: сервис здоров, но с некоторого момента каждая операция начинается с миллисекунд ожидания, потом сыплются таймауты с текстом про connection pool, хотя сама база отвечает мгновенно. Похожую картину даёт и сетевой сбой: пул после ошибки соединения чистится и наращивается заново, и кратковременный провал пропускной способности после каждого обрыва — нормальное поведение, а не отдельная поломка. Диагноз ставится событиями пула, о которых дальше, а лечение всегда одно: найти того, кто держит коннекты, а не растить потолок.

Наблюдение за пулом: события вместо Stat#

У пула нет метода вроде Stat с готовыми счётчиками — сравнить с пулом pgx не получится, и это честно задокументировано архитектурой драйвера. Наблюдение строится на событиях: options.Client().SetPoolMonitor принимает PoolMonitor, чей колбэк вызывается на каждое событие жизненного цикла соединений. Полезные типы: ConnectionCheckedOut и ConnectionCheckedIn — взятие и возврат коннекта, ConnectionCheckOutFailed — операция не дождалась соединения, с полем Reason и ошибкой контекста, ConnectionCreated и ConnectionClosed — рождение и смерть коннекта. Каждое событие несёт адрес сервера и длительность, поэтому пары CheckedOut и CheckedIn дают живую картину: сколько коннектов занято прямо сейчас, сколько операций ждало и на чём именно. На графах обычно достаточно трёх серий: занятые коннекты, время ожидания коннекта и число CheckOutFailed — последняя серия обязана быть нулевой, и каждая единица там — будущий таймаут в хендлере. Рядом стоит SetMonitor для команд: CommandSucceededEvent и CommandFailedEvent приходят на каждую команду с полем Duration — готовый материал для замера p95 без внешних агентов. Оба монитора ставятся при создании клиента и пишут в вашу метрику одной строкой кода.

Индексы против сканов: explain как рентген#

Пул — транспорт, а тормоза живут в плане запроса. Скан коллекции читает все документы подряд, ход по индексу — только нужные записи дерева: именно эту разницу делает CreateOne из урока про индексы. Проверяется она explain: команда explain оборачивает ваш запрос и с параметром verbosity executionStats возвращает, что сервер делал на самом деле. Ключевые поля лежат в executionStats: nReturned — сколько документов вернулось, totalDocsExamined — сколько прочитано, totalKeysExamined — сколько записей индекса перебрано, executionTimeMillis — время в миллисекундах. Здоровый запрос читает близко к тому, что возвращает: examined примерно равен returned. Особый случай — покрытие: когда все нужные поля лежат в индексе, totalDocsExamined падает до нуля, и сервер вообще не заглядывает в документы. Запрос, который на десяток строк перечитал миллион документов, — кандидат на индекс или на перестройку фильтра. Опцию SetHint можно принудить запрос к конкретному индексу, когда планировщик промахивается, а compound-индексы проектируются по правилу равенство, сортировка, диапазон — все эти буквы живут в уроке про индексы. С таймингами в v2 проще, чем в v1: отдельных опций maxTime у операций нет, дедлайн берётся из контекста, а клиентский SetTimeout уезжает на сервер как maxTimeMS сам.

Explain проблемного запроса через RunCommand

GO
cmd := bson.D{
        {Key: "explain", Value: bson.M{
                "find":   "users",
                "filter": bson.M{"email": "ada@example.com"},
        }},
        {Key: "verbosity", Value: "executionStats"},
}

var res bson.M
if err := client.Database("shop").RunCommand(ctx, cmd).Decode(&res); err != nil {
        return fmt.Errorf("explain: %w", err)
}
if stats, ok := res["executionStats"].(bson.M); ok {
        log.Printf("nReturned=%v totalDocsExamined=%v",
                stats["nReturned"], stats["totalDocsExamined"])
}

Команда explain заворачивает обычный find в документ с verbosity executionStats и уезжает через RunCommand как любой другой документ. Ответ декодируется в bson.M, из executionStats берутся nReturned и totalDocsExamined: равенство говорит об индексном пути, разрыв на порядки — о скане, который лечится индексом из урока про CreateOne.

Агрегации по дешевле: сузить, потом свернуть#

Конвейер агрегаций стоит ровно столько, сколько данных через него проехало. Первое правило из базового урока агрегаций — $match первым: до группировки сервер опирается на индексы, после — фильтрует уже свёрнутые итоги. Второе — отсекать объём как можно раньше: $limit и $project перед тяжёлыми этапами уменьшают и число документов, и ширину каждого, а сортировка с лимитом после сужения — дешевле той же сортировки по всей выборке. Третье — помнить потолки: память этапа ограничена сотней мегабайт, а документ — шестнадцатью, и длинный $push добирается до обоих; SetAllowDiskUse разрешает выгрузку на диск, но это не ускорение, а возможность долететь — обмен с файлами медленнее памяти. Четвёртое — не считать в Go то, что считает сервер: выкачивать сырые документы циклом и суммировать в приложении — значит перекачивать сеть и держать коннекты дольше необходимого. Размер батчей курсора регулируется SetBatchSize: крупные батчи экономят поездки, мелкие — память; для панельных отчётов обычно достаточно дефолта. Все эти приёмы не требуют ничего, кроме перестановки этапов, — самая дешёвая оптимизация из возможных.

Правила производительности одним списком#

  • Дефолты пула: сто коннектов на сервер, ожидание в очереди при исчерпании, два рукопожатия одновременно.
  • У каждого курсора — дочитывание или Close: коннект возвращается только освобождённым.
  • Статистики-метода у пула нет: ставьте SetPoolMonitor и считайте события CheckedOut и CheckedIn сами.
  • Медленный запрос доказывает explain: examined близко к returned — хорошо, разрыв в тысячи раз — скан.
  • $match первым, $limit и $project раньше тяжёлых этапов, allowDiskUse — костыль, а не ускорение.
  • Считает сервер, а не цикл в Go: перекачка сырых документов съедает сеть и держит коннекты.

Проверка: исчерпание пула

Все коннекты пула заняты, дедлайн контекста операции истёк. Что вернёт операция?

Проверка: чтение explain

В executionStats значение totalDocsExamined равно nReturned. О чём это говорит?

Что напечатают два предиката?

Прочитайте код и выберите, что он выведет:

// клиент: SetMaxPoolSize(1); mongod отвечает за 300 мс
// горутины А и Б уже держат свои операции,
// у горутины В дедлайн контекста — 100 мс
err := findOne(ctxC)

fmt.Println(mongo.IsTimeout(err), mongo.IsNetworkError(err))

Соберите настройку пула

Восстановите три вызова: потолок коннектов, параллельные рукопожатия и лимит простоя.

client, err := mongo.Connect(options.Client(). ApplyURI("mongodb://localhost:27017"). (50). SetMinPoolSize(5). (4). (5*time.Minute))

Соберите диагностику тормозов

Расставьте строки функции по порядку: explain, запуск, чтение статистики, вывод вердикта.

Строки

Ваша программа

Нажимайте на строки слева, чтобы собрать программу

Карточки: пул и скорость

Термин

maxPoolSize

нажмите, чтобы перевернуть
Определение

Потолок коннектов к каждому серверу: сто по умолчанию, ноль — без лимита. При исчерпании операции ждут в очереди до дедлайна контекста. Настраивается SetMaxPoolSize.

1 / 6

Три уровня собраны в одну цепочку диагностики: метрики пула показывают, хватает ли транспорта, explain — куда уходит время сервера, форма конвейера — кто держит коннекты дольше необходимого. Индексы, которые превращают скан в ход по дереву, ставились в уроке про индексы — начните оптимизацию там, с ключей и правила равенство-сортировка-диапазон. Стоимость конвейера и потолки памяти разобраны в базовом уроке агрегаций, а WaitQueueTimeoutError и его братья классифицированы в уроке про ошибки драйвера тем же IsTimeout. Следующий логичный шаг — свести всё в итоговый проект курса: там каталог товаров заставит работать и индексы, и пул, и агрегации одновременно. Песочница сайта не исполняет драйвер, поэтому цифры из урока проверьте на своём стенде: снимите explain с двух-трёх своих запросов и сравните examined с returned — разрыв, если он есть, найдётся в первые минуты.

Песочница урока

Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.

Вывод

Запустите код, чтобы увидеть вывод.

Коротко: что запомнить

  • Пул соединений — по одному на сервер: maxPoolSize 100 по умолчанию, операции ждут коннект при исчерпании.
  • minPoolSize держит запас коннектов, maxConnecting ограничивает рукопожатия двумя, heartbeat живёт десять секунд.
  • Коннект возвращается пулу только после исчерпания или Close курсора — незакрытый курсор утащит соединение.
  • Метода Stat у пула нет: наблюдение через SetPoolMonitor и события CheckedOut, CheckedIn, CheckOutFailed.
  • IsTimeout распознаёт WaitQueueTimeoutError — таймаут проверки коннекта выглядит как обычный таймаут.
  • Explain с executionStats доказывает диагноз: examined близко к returned — индекс, разрыв на порядки — скан.
  • Отдельных maxTime-опций в v2 нет: дедлайн контекста и клиентский SetTimeout, уезжающий на сервер как maxTimeMS.
  • Агрегации дешевле, когда $match первым, лишние поля отсечены, а счёт выполняет сервер, а не цикл в Go.

Домашнее задание

Проведите полную диагностику одного своего запроса. Снимите explain через RunCommand и запишите nReturned и totalDocsExamined; если разрыв больше сотни, добавьте индекс и снимите explain снова. Затем повесьте SetPoolMonitor на клиент и посчитайте за минуту события CheckedOut и CheckedIn — сравните с потолком пула. Напоследок запустите два запроса с дедлайном контекста в миллисекунду и убедитесь по IsTimeout, что таймаут на исчерпании пула классифицируется так же, как таймаут сервера.

Личные заметкиПусто
Хранятся только в вашем браузере (MongoDB · Пул соединений и производительность)

Вопросы и ответы по уроку

Какое значение maxPoolSize выставить в реальном сервисе?

Начните со стопроцентного дефолта и меняйте только при доказанной нужде. Каждый коннект — файловый дескриптор и память на обоих концах, а лимит дескрипторов на сервере обычно жёстче, чем ваш потолок пула: тысяча коннектов из десяти инстансов сервиса легко упирается в него ещё до базы данных. Считайте потолок как произведение инстансов на maxPoolSize и держите запас, а причину исчерпания ищите в держателях коннектов, а не в цифре. Если операции короткие, а коннекты простаивают, — уменьшать тоже можно: меньше коннектов, меньше памяти на обоих концах.

Действительно ли нужно закрывать курсор, если читается всё через All?

cursor.All закрывает курсор сам — это его контракт, и там Close не нужен. Ручной цикл по Next требует Close, когда выход происходит раньше исчерпания батчей: по break, по ошибке декодирования, по отмене контекста. Дешевле всего правило кодстайла: ручной цикл — только там, где нужна потоковая обработка, а во всех остальных случаях All.

SetTimeout на клиенте вместо дедлайнов на операциях — хорошая идея?

Как страховка — да: клиенты, забывающие контекст, всё равно получат потолок. Как замена — нет: у дашборда и ночной выгрузки разный бюджет времени, и единая цифра либо режет длинные задачи, либо слишком щедра для быстрых. Правильная связка — дедлайн контекста от входящего запроса плюс клиентский SetTimeout как общий предохранитель.

Термины урока

Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.

Следующая ступень учебного пути

zerolog25 уроков~12 чпрактика25 уроков уже доступны

Zerolog в Go: быстрое JSON-логирование

Курс zerolog для Go: JSON-события, уровни, поля, ConsoleWriter, контекстные поля, ротация, sampling, HTTP-логи и тесты. 25 уроков с примерами.

  • Писать структурированные JSON-логи с нуля аллокаций
  • Выбирать уровни и поля события осознанно
  • Настраивать ConsoleWriter для dev и JSON для прода
  • Строить сублоггеры и контекстные поля на запрос