Улучшая общую производительность Parqet, финтех-стартапа для визуализации вашего портфеля и богатства, я многому научился. Сегодня я хотел бы поделиться этими знаниями и дать несколько советов по улучшению производительности MongoDB.

Некоторые примеры кода будут на JavaScript/TypeScript, однако они применимы и к другим языкам. Давайте погрузимся прямо в.

Инструменты

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

Подобно другим базам данных, MongoDB имеет функцию Объяснить. Это позволяет вам глубже понять план выполнения и производительность вашего запроса. Есть несколько инструментов, которые помогут вам:

Компас MongoDB

MongoDB Compass — это мультиплатформенный графический интерфейс, разработанный MongoDB Inc. Я лично всегда использую его и могу настоятельно рекомендовать для анализа производительности запросов или выполнения других распространенных задач базы данных.

При подключении к вашей базе данных выберите «Объяснить Plain», чтобы проверить производительность вашего запроса и получить некоторое представление о том, что именно сделал запрос.

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

Вы можете сделать то же самое в представлении агрегации, нажав кнопку «Объяснить».

Атлас MongoDB

MongoDB Atlas, официальный облачный сервис для MongoDB, предлагает хорошие сведения о производительности и даже пытается дать вам несколько советов о том, как повысить производительность запросов. Atlas Profiler предоставляет вам информацию о проверенных ключах, времени выполнения,…

Кроме того, Atlas также пытается посоветовать вам, как повысить производительность (меньше поисковых запросов, добавить индекс и т. д.).

Хотя это не исправит наши запросы, оно покажет нам узкие места и даст нам хорошую отправную точку.

Оболочка MongoDB

Если вы знакомы с CLI, вам может понравиться MongoDB Shell. Добавив explain() к запросу, вы сможете получить представление о своем запросе.

Мониторинг

После того, как вы развернули изменения, вы должны продолжать отслеживать загрузку ЦП, хранилище, время запросов и т. д., чтобы убедиться, что ваши изменения действительно полезны.

Я уверен, что есть и другие проприетарные инструменты (не разработанные MongoDB), просто мне никогда не приходилось их использовать. Есть ли у вас положительный опыт работы с другими инструментами для оптимизации производительности запросов или получения информации?

Я хотел бы услышать об этом.

Индексы

Поля, которые вы запрашиваете, должны быть покрыты индексом.

Вместо того, чтобы просматривать каждую строку (сканирование коллекции) и проверять, соответствует ли ваш запрос строке, индекс ускоряет процесс. См. официальные документы для более глубокого понимания того, как работает индекс.

MongoDB по умолчанию добавляет индекс в поле _id. Кроме того, вам нужно добавить индексы в свои коллекции. Возьмем образец коллекции с миллионом записей. Наша структура данных выглядит так:

Давайте сначала запросим данные без какого-либо индекса. Найдите активы, соответствующие ISIN:

db.assets.find({ isin: "US123456890" })

Всего запрос занял 398 мс (см. «Фактическое время выполнения запроса (мс)»). Вы можете видеть, что MongoDB пришлось просмотреть миллион записей, чтобы найти актив. Давайте добавим соответствующий индекс и попробуем еще раз.

db.assets.createIndex({ isin: 1 }) // ascending

Как вы можете видеть, производительность запросов значительно увеличилась (менее миллисекунды — она показывает только 0 мс), поскольку наши запросы покрываются индексом.

Недостатки при добавлении индекса включают снижение производительности записи и увеличение объема памяти. Вы должны помнить об этом и не индексировать каждое поле, известное человечеству. Однако часто запрашиваемые поля обязательно должны быть включены в индекс.

Составные индексы

При запросе нескольких полей MongoDB старается изо всех сил работать с существующими индексами и оптимизировать ваш запрос. Если у вас есть несколько полей, которые обычно запрашиваются вместе, может быть хорошей идеей использовать составной индекс. Составной индекс — это индекс, основанный на нескольких полях. Допустим, вы часто запрашиваете дату IPO и даты дивидендов.

Без составного запроса (один индекс на дату IPO):

db.assets.createIndex({ ipoDate: -1 })

Несмотря на то, что мы попали в индекс, MongoDB все еще нужно просмотреть ~ 568 000 записей, поскольку эти записи соответствуют критерию даты IPO. Давайте добавим составной индекс, охватывающий оба поля:

db.assets.createIndex({ ipoDate: -1, "dividends.date": -1 })

С составным индексом MongoDB просматривает только ~9150 документов (все соответствующие документы). Общее время выполнения запроса также увеличилось с 1800 мс до 755 мс. Еще не оптимально, но уже значительное улучшение.

Индексирование массивов

Также возможно индексировать поля внутри массивов. Обратите внимание, что вы не можете добавить составной индекс к двум полям массива. При использовании запроса $exists:

Вы можете добавить индекс к myArray.0, чтобы покрыть запрос индексом. Когда обычно запрашивают свойство внутри массива:

Вы можете добавить индекс к myArray.myProperty, и он будет использовать индекс при сопоставлении свойства внутри массива.

Прогнозы

По умолчанию MongoDB возвращает полный документ. Проекции позволяют уменьшить, добавить или изменить документы перед их получением. Это позволяет оптимизировать полезную нагрузку документа и максимально повысить производительность запросов.

Удаление полей из документа

Удаление нескольких полей из документа

При удалении полей все остальные поля останутся.

Явное включение поля

При включении поля с помощью <field>: 1 другие поля опускаются. MongoDB всегда будет включать поле _id, если вы явно не используете _id: 0 в своей проекции.

Добавление поля

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

Этот прогноз добавляет marketShares, который рассчитывается на основе последних полей цены и рыночной капитализации.

Уменьшение массивов

Проекции довольно гибки, поэтому вы также можете просто вернуть подмножество массива, если вас интересуют только первые несколько элементов.

Это всего лишь несколько примеров того, как использовать проекции для уменьшения или увеличения полезной нагрузки документа перед выборкой.

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

Охватываемые запросы

Ваши запросы могут быть очень быстрыми, однако MongoDB по-прежнему должна считывать фактические данные с диска, чтобы получить их. Хотя это быстро, есть еще один способ сделать это еще быстрее.

Когда вас интересует только подмножество свойств из вашего документа и все поля покрыты индексом или составным индексом, MongoDB может фактически считывать данные из индекса, еще больше повышая производительность запросов. Это называется Покрытый запрос.

Это, скорее всего, сэкономит вам всего несколько миллисекунд, но все же стоит посмотреть, если вы действительно хотите выжать из каждой частички производительности.

Структура данных

Мы имеем дело с нереляционной базой данных NoSQL — хотя объединение данных возможно (используя $lookup), вы должны избегать этого, пока можете, так как это не так эффективно. Таким образом, вам нужно подумать о своей структуре данных и в идеале сгруппировать связанные данные в единый документ.

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

Создание «представлений» данных

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

В портфеле есть n холдингов, а в холдинге есть актив. Мы хотели бы запросить портфель для активов, чтобы проверить, содержит ли портфель определенные активы.

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

Давайте агрегируем данные (т. е. ежечасно) и записываем их в отдельную коллекцию, которая доступна для запросов и также может извлечь выгоду из индексов, не выполняя поиск на лету.

Следующий запрос группирует все идентификаторы активов по портфелям и записывает их в отдельные коллекции portfolio_assets:

Теперь мы можем запросить новую коллекцию portfolio_assets. Хотя данные недоступны в режиме реального времени, производительность запросов, вероятно, снизилась с 1–2 секунд до нескольких миллисекунд. С MongoDB Atlas вы также можете запустить это как задание CRON.

Читать предпочтения

Если не настроено иначе, операции чтения обычно выполняются на основном узле кластера.

Когда частота данных не имеет решающего значения (задержка в несколько миллисекунд или секунд допустима), вы можете явно указать MongoDB читать данные со вторичного узла.

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

Вот скриншот до и после сознательного использования предпочтений чтения и предпочтения чтения из вторичных (реплицированных) экземпляров для множества запросов. Главный правый крайний справа. После изменения группы часто используемых запросов на другой параметр чтения нагрузка становится более сбалансированной по всему кластеру.

Чтобы узнать о дополнительных параметрах предпочтений чтения, ознакомьтесь с официальной документацией.

Написать о беспокойстве

В кластерной настройке по умолчанию все операции записи записываются на основной узел и в oplog — оттуда операции будут применяться к узлам-репликам.

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

db.insertOne({ ... }, { w: 0 })

В зависимости от ваших потребностей вы можете значительно увеличить пропускную способность данных, изменив задачу записи. Однако используйте с осторожностью. Чтобы узнать о дополнительных параметрах записи, ознакомьтесь с официальной документацией.

Оптимизируйте свои агрегаты

Хотя Агрегации — очень мощная функция, вы можете легко испортить производительность.

Лучший способ улучшить ваши агрегаты — сравнить их и проверить планы выполнения на предмет возможной оптимизации. Попробуйте сначала уменьшить данные (например, сгруппировав или используя фильтр $match), прежде чем выполнять поиск или применять дальнейшие мутации. Я не могу дать вам общий совет по производительности, так как агрегаты очень индивидуальны.

С помощью MongoDB Atlas Profiler мы рассмотрели агрегаты, выполнение которых занимало больше всего времени, и оптимизировали их (добавление индексов, уменьшение полезной нагрузки, фильтрация, группировка, реструктуризация данных и т. д.).

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

Сжатие

Если не настроено, MongoDB будет отправлять/получать данные в несжатом виде. Как вы, наверное, знаете, несжатые текстовые данные, вероятно, примерно на 80–90% больше, чем сжатые JSON, а это означает, что вы будете получать намного больше данных, а нагрузка на вашу сеть возрастет.

Сжатие — это всегда компромисс между скоростью, вычислительными ресурсами (ЦП) и размером/загрузкой сети. MongoDB поддерживает следующие алгоритмы сжатия:

  • ZLib
  • ZStd
  • Быстрый

Вы можете указать MongoDB, какое сжатие использовать при подключении к серверу, добавив параметр запроса compressors.

mongodb://localhost:27017/?compressors=snappy

Драйверы MongoDB уже должны справиться с этим за вас, вот пример того, как использовать Google Snappy с драйвером Node.

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

Мы остановились на Snappy (поскольку мы также используем Snappy при записи в Redis). Нагрузка на сеть значительно снижена, а производительность увеличена.

Распараллелить выборку

Ваш запрос выполняется за 3 мс, но вашему приложению по-прежнему требуется 500 мс для получения данных? Извлечение данных занимает много времени, поэтому чем больше вы запрашиваете, тем больше времени потребуется вашему приложению для извлечения данных с сервера MongoDB (после запроса).

Вы уже используете сжатие и проецирование, чтобы уменьшить объем данных, отправляемых в ваше приложение. Еще один способ повысить производительность — распараллелить запрос.

Если вы знаете, что собираетесь запрашивать данные за 25 лет, вы можете разделить их на фрагменты за 5 лет и отправить все пять запросов параллельно. MongoDB может легко обработать эти 5 запросов, как и ваше приложение. Используйте с осторожностью, так как вы можете легко перегрузить свое приложение или MongoDB, если у вас слишком много элементов в вашем фрагменте.

Нам удалось сократить общее время выполнения некоторых запросов, которые извлекают › 6000 строк, в 3–4 раза, разбив запрос на фрагменты и выполняя запросы параллельно.

Пул соединений

Большинство, если не все, драйверы MongoDB поддерживают пул соединений.

Благодаря пулу соединений приложение поддерживает соединения с MongoDB открытыми, ожидая запросов, ускоряя запросы, поскольку начальную фазу соединения можно пропустить. В зависимости от ваших настроек и потребностей вы можете изменить размеры пула в своем приложении и, возможно, повысить пропускную способность.

При такой конфигурации наше приложение будет иметь как минимум 50 открытых подключений и, возможно, масштабируется до 250. Если все 250 подключений используются, ваше приложение будет ждать, пока какое-либо соединение станет бездействующим, прежде чем выполнять запрос.

Также имейте в виду, что если ваше приложение кластеризовано, каждый отдельный экземпляр будет иметь эти настройки. Если вы установите минимальный размер пула 250 и у вас запущено 4 инстанса и вы выполняете зелено-синее развертывание, у вас, вероятно, некоторое время будет работать 6–8 инстансов, и вы уже используете минимум 1500–2000 подключений. При использовании MongoDB Atlas проверьте лимит подключения, исходя из вашего плана.

Расширенная конфигурация клиента

В зависимости от вашей платформы, возможно, стоит взглянуть на параметры конфигурации клиента. Например, в случае драйвера Node MongoDB вы можете отключить проверку UTF-8 для повышения производительности.

Проверьте документы вашего конкретного драйвера MongoDB для таких опций. Обязательно ознакомьтесь с влиянием этой опции, так как прирост производительности может иметь и другие недостатки (проверьте, влияют ли они на вас).

Шардинг и репликация

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

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

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

Временная последовательность

MongoDB представила коллекции TimeSeries с версией 5.0. Они также получили много любви в линейке релизов 6.x. При работе с данными TimeSeries вам, скорее всего, следует использовать тип коллекции TimeSeries вместо обычной коллекции.

Обратите внимание, что вы должны выбрать тип коллекции в начале, вы не можете просто переключиться на коллекцию TimeSeries впоследствии. Если вы хотите перенести свою коллекцию на TimeSeries, вам необходимо перенести данные, пример есть в официальных документах.

Хотя MongoDB TimeSeries определенно лучше подходит для данных временных рядов по сравнению с обычными коллекциями, MongoDB определенно не самая быстрая база данных TimeSeries.

Мы оценили временные ряды MongoDB и сравнили их с Redis TimeSeries. Среднее время выборки данных с Redis Timeseries составляло ~1–2 мс, с MongoDB — 20–30 мс. Хотя 20–30 мс — это неплохо, в нашем случае это было в 10–20 раз медленнее, и, поскольку эвуляция затронула одни из наших самых запрашиваемых данных, мы решили не использовать для этого MongoDB в будущем.

Атлас поиска

Если вам нужен мощный и гибкий полнотекстовый поиск, MongoDB предлагает Atlas Search.

Однако это доступно только в Atlas, а не при самостоятельном размещении версии сообщества MongoDB. Учитывая эту привязку к поставщику, я могу рекомендовать использовать эту функцию только в том случае, если вы уверены, что не перенесете свою установку MongoDB из Atlas в ближайшее время или когда-либо.

Мы интегрировали поиск Atlas и улучшили не только производительность поиска, но и результаты поиска. Раньше у нас был простой Текстовый указатель, который также предлагал своего рода полнотекстовый поиск, но не такой мощный, как Atlas Search.

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

Кэширование ваших запросов

Хотя это не совет по производительности MongoDB, это почетное упоминание. Однако, поскольку это руководство в основном посвящено производительности, мы не можем игнорировать тот факт, что кэширование, вероятно, является одним из лучших способов повысить производительность ваших приложений. Кэширование ваших данных в памяти или в распределенном кеше, таком как Redis, может обеспечить доступ к вашим данным за доли миллисекунды в любое время.

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

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

Заворачивать

Это обертка. Я попытался обобщить свои знания и то, что я нашел наиболее эффективным при оптимизации производительности запросов MongoDB. Надеюсь, вы почерпнули что-то из моих знаний, даже если это просто указатель того, куда смотреть дальше.

Некоторые дополнительные указатели:

Что из того, что вы сделали для улучшения производительности MongoDB, было наиболее важным? Нашли еще какие-нибудь хитрости?

С нетерпением жду ваших историй и спасибо за чтение!