О стартапе, mmap, mlock и --force-preread

Как упоминалось в статье " Загрузка индексов при старте ", теперь все индексы (файлы атрибутов и списков слов) не загружаются физически в ОЗУ, а вместо этого отображаются в памяти. Это позволяет загружать их гораздо быстрее при старте, но также имеет некоторые побочные последствия, которые я хочу объяснить.

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

Второе последствие - на самом деле отображение памяти занимает лишь область 'виртуального адресного пространства' процесса. Поскольку на любой современной системе у вас есть 64 бита для адресов, можно сказать, что мы можем загружать и обслуживать индекс практически любого размера, несмотря на фактическое свободное ОЗУ. Однако обратите внимание, что это относится только к данным индексов. Демон поиска Manticore также нуждается в физической ОЗУ для обычной работы, такой как внутренние хеши, буферы, массивы и т. д.

Если вы посмотрите на статистику памяти процесса, вы увидите какое-то число в столбце RSS (или RES), и это на самом деле занятая ОЗУ (в основном куча), но не загруженные индексы (если вы не установите mlock=1). Они в основном отражаются в столбце 'VSZ'. Также, если вы загрузите огромный индекс (примерно на все пространство ОЗУ), а затем выполните команду 'free', вы увидите, что это практически не отражается в 'используемом' пространстве, а в основном в 'buff/cache', и, следовательно, также в 'доступном'.

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

Что это значит на практике?

  1. Во-первых, никаких гарантий по умолчанию. 'Загрузка' индекса при старте с помощью отображения памяти, а затем переход по страницам внутри этого отображения просто загружает страницу, на которую вы переходите. ОС не гарантирует, что после перехода на одну страницу следующий шаг 'загрузки' блокирует предыдущую страницу постоянно в ОЗУ. Да, это может быть так - если, скажем, у вас есть 128 ГБ свободной ОЗУ, а загруженный индекс составляет всего 30 ГБ. Но если у вас индекс размером 120 ГБ и только 16 ГБ ОЗУ, 'загрузка' пройдет точно так же, но поскольку индекс не может поместиться в ОЗУ, он не будет полностью кэширован, и время его отклика увеличится.
  2. Во-вторых, нет гарантии, что загруженный индекс будет сохранять то же время отклика постоянно. Представьте, снова, что вы загружаете индекс размером 30 ГБ на системе с 50 ГБ свободной ОЗУ, и все кажется работать быстро. Но затем вы также загружаете другой процесс, требующий много ОЗУ, и он занимает 40 ГБ. Это означает, что из 30 ГБ вашего индекса только ~10 ГБ все еще будет кэшировано, а доступ к остальным теперь требует чтения с диска.

Таким образом, ни ленивый предварительный чтение, ни даже опция --force-preread не гарантируют, что весь индекс кэширован и будет реагировать постоянно и предсказуемо быстро. Никакой гарантии, только вероятность. Чем больше у вас ОЗУ - тем больше вероятность, что весь индекс кэширован и будет реагировать максимально быстро. Все это "массаж" mmap касается только вероятности.

Но мне нужна гарантия, а не вероятность! Это возможно?

Да! Единственный (и единственный) способ надежно заблокировать весь индекс в ОЗУ - это использование опции mlock . Она должна быть установлена в конфигурации индекса (не в параметрах командной строки). Это требует от вас наличия привилегий для этого (см. системный 'man mlock' для подробностей). Как это работает? Демон будет отображать файлы индексов, а затем вызывать 'mlock' на них. В этот момент ОС распознает, достаточно ли у нее ОЗУ для загрузки всех желаемых отображений, и если да, то немедленно выполнит загрузку. Это может быть относительно длительная операция (просто возьмите скорость вашего хранилища и оцените время, необходимое для последовательной загрузки необходимого объема данных).

Таким образом, мы можем достичь цели - иметь индекс, полностью заблокированный в ОЗУ, который реагирует предсказуемо быстро. Это хорошо.

Но также необходимо упомянуть некоторые вещи относительно mlock.

  1. Во-первых, как уже упоминалось - вам нужны привилегии для его выполнения. Это частично связано с тем, как он работает, и может повлиять на всю систему. Однако это не большая проблема в большинстве случаев, если вы не используете общий хостинг с очень ограниченными правами.
  2. Во-вторых, кэширование отображения (mlocking) - это блокирующий процесс, которым мы не можем управлять. Внутри мы просто вызываем mlock(), он делает какую-то магию внутри и возвращается через несколько секунд/минут после завершения. Невозможно прервать, невозможно ограничить ввод/вывод, просто ждите. Таким образом, процесс mlocking может повлиять на другие операции ввода/вывода на машине.
  3. Когда система ищет ОЗУ для mlocking, есть вероятность, что она вызовет OOM-killer, чтобы освободить ОЗУ для вас, что может убить другой процесс. Будьте осторожны!
  4. Даже если вы используете mlock, вам все равно может понадобиться использовать --force-preread во многих случаях. Дилемма здесь такова:
    • без --force-preread searchd начнет обслуживать соединения быстрее, но индексы будут холоднее, пока они полностью не будут предварительно прочитаны в фоновом режиме. Это может быть плохо для входящих запросов.
    • с --force-preread вам придется подождать (возможно, несколько минут), но после этого вы сможете обеспечить очень хорошую производительность.

Вот как это может выглядеть с mlock, но без --force-preread:

no_force_prereadА те же индексы на том же оборудовании с --force-preread:

force_prereadКак вы можете видеть, в этом случае имеет смысл подождать 6 минут при старте, в противном случае среднее время отклика становится значительно выше на десятки минут, а iowait также крайне высок из-за запросов, которые выполняют случайные чтения с диска. Конечно, могут быть и другие случаи, и ваша балансировка нагрузки может работать иначе и обрабатывать такие ситуации более умным способом, или у вас может просто не хватать ОЗУ, чтобы вместить весь индекс, или ваши запросы могут быть легче. Просто рассмотрите оба подхода и выберите тот, который подходит вам лучше всего.

Что еще может быть важным?

  1. Играйте с параметрами ОС, такими как 'swapinness', или полностью отключите своп, если можете себе это позволить. Это может помочь увеличить вероятность быстрого отклика (без mlocking). Обратите внимание, что в современных ядрах linux у вас есть такая замечательная вещь, как контрольные группы (также известные как cgroups). Вы можете поместить свой демон в выделенную контрольную группу и настроить любые системные параметры (такие как упомянутый swapinness) для него, не затрагивая глобальные системные настройки.
  2. Современные SSD достаточно быстры даже для случайного доступа, поэтому их использование может растворить разницу между просто 'mapped' ('mlocked') и 'cached' данными.

Установить Manticore Search

Установить Manticore Search