Статьи

SearcherManager Lucene упрощает повторное открытие с потоками


Современные компьютеры обладают прекрасным аппаратным параллелизмом, внутри и между ядрами ЦП, ресурсами ОЗУ и ввода-вывода, что означает, что ваше типичное серверное приложение поиска должно использовать несколько потоков, чтобы полностью использовать все ресурсы.

Для поиска это обычно означает, что у вас будет один поток, обрабатывающий каждый поисковый запрос, совместно использующий один экземпляр IndexSearcher. Эта модель эффективна: разработчики Lucene прилагают все усилия, чтобы минимизировать внутреннюю блокировку во всех классах Lucene. Фактически мы недавно устранили конфликт потоков во время индексации (в частности, очистки), что привело к значительному увеличению пропускной способности индексации на высококонкурентном оборудовании.

Поскольку IndexSearcher предоставляет фиксированное представление индекса на определенный момент времени, когда вы вносите изменения в индекс, вам нужно будет снова открыть его. К счастью, начиная с версии 2.9, Lucene предоставляет метод IndexReader.reopen, чтобы получить нового читателя, отражающего изменения.

Эта операция эффективна: новое считывающее устройство разделяет уже подогретые вспомогательные считыватели совместно со старым считывателем, поэтому оно открывает только вспомогательные считыватели для любых вновь созданных сегментов. Это означает, что время повторного открытия обычно пропорционально тому, сколько изменений вы внесли; однако, когда большое слияние завершится, оно будет длиннее. Прежде чем запускать его в эксплуатацию, лучше подогреть новый считыватель, выполнив набор «типичных» поисков для вашего приложения, чтобы Lucene выполняла однократную инициализацию для внутренних структур данных (норм, кэш-памяти полей и т. Д.).

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

Lucene пытается обнаружить, что вы сделали это, и выдает красивое исключение AlreadyClosedException, но мы не можем гарантировать, что исключение выдается, поскольку мы проверяем только заранее, когда поиск начинается: если вы закрываете читатель, когда поиск уже выполняется тогда все ставки сняты.

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

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

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

Это решение полностью параллельное: оно не имеет никакой блокировки, поэтому поиск никогда не блокируется, если вы используете отдельный поток для повторного открытия и прогрева. Время повторного открытия и прогрева нового считывателя не влияет на текущие поиски, за исключением того, что повторное открытие потребляет ресурсы ЦП, ОЗУ и ввода-вывода для выполнения своей работы (и иногда это может фактически мешать текущим поискам).

Так как именно вы реализуете этот подход? Самый простой способ — использовать API подсчета ссылок, уже предоставленные IndexReader, для отслеживания того, сколько потоков в настоящее время использует каждый поисковик. К счастью, в Lucene 3.5.0 появится новый класс утилит contrib / misc, SearcherManager, изначально созданный в качестве примера для Lucene in Action, 2-е издание , который сделает это за вас! LUCENE-3445 есть детали.)

Класс прост в использовании. Сначала вы создаете его, предоставляя каталог, содержащий ваш индекс, и экземпляр SearchWarmer:

  class MySearchWarmer implements SearchWarmer {
    @Override
    public void warm(IndexSearcher searcher) throws IOException {
      // Run some diverse searches, searching and sorting against all
      // fields that are used by your application
    }
  }

  Directory dir = FSDirectory.open(new File("/path/to/index"));
  SearcherManager mgr = new SearcherManager(dir,
                                            new MySearchWarmer());

Затем для каждого поискового запроса:

  IndexSearcher searcher = mgr.acquire();
  try {
    // Do your search, including loading any documents, etc.
  } finally {
    mgr.release(searcher);

    // Set to null to ensure we never again try to use
    // this searcher instance after releasing:
    searcher = null;
}

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

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

Остерегайтесь потенциально высокой переходной стоимости вновь открывать и нагревать! Во время повторного открытия, так как у вас должно быть открыто два считывателя, пока старый не будет закрыт, вы должны выделить много оперативной памяти на компьютере и кучу для JVM, чтобы комфортно справиться с наихудшим случаем, когда два считывателя не разделяют под-читателей (для Например, после полной оптимизации) и, таким образом, потребляют в 2 раза больше оперативной памяти одного считывателя. В противном случае вы можете столкнуться со штормом обмена или OutOfMemoryError, что приведет к эффективному уничтожению всего поискового приложения. Хуже того, вы не увидите эту проблему на ранних этапах: ваши первые несколько сотен открытий могут легко использовать только небольшое количество добавленной кучи, но затем неожиданно при некотором неожиданном открытии цена становится намного выше. Повторное открытие и прогрев также обычно требуют интенсивного ввода-вывода, поскольку считыватель должен загрузить определенные структуры данных индекса в память.

В следующий раз я опишу еще один служебный класс, NRTManager, доступный начиная с версии 3.3.0, который вы должны использовать вместо этого, если ваше приложение использует поиск с быстрым оборотом почти в реальном времени (NRT). Этот класс решает ту же проблему (потокобезопасность при повторном открытии), что и SearcherManager, но добавляет забавный поворот, поскольку он дает вам более конкретный контроль над тем, какие изменения должны быть видны во вновь открытом считывателе.