Подтвердить что ты не робот

Надежно принудить выселение карты Гуава

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

Этот вопрос основан на ответах на вопрос Viliam в отношении использования Guava Maps ленивого выселения: Лень выселения на картах Гуавы

Пожалуйста, сначала прочтите этот вопрос и его ответ, но по существу вывод состоит в том, что карты Гуавы не асинхронно вычисляют и не принуждают к выселению. Учитывая следующую карту:

ConcurrentMap<String, MyObject> cache = new MapMaker()
        .expireAfterAccess(10, TimeUnit.MINUTES)
        .makeMap();

Как только десять минут пройдут после доступа к записи, оно все равно не будет выведено, пока карта не будет "тронута" снова. Известные способы сделать это включают обычные аксессоры - get() и put() и containsKey().

Первая часть моего вопроса [решена]: какие другие вызовы заставляют карту "трогать"? В частности, кто-нибудь знает, попадает ли size() в эту категорию?

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

public static void nudgeEviction() {
    cache.containsKey("");
}

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

Ответ: Итак, Марк указал, что в версии 9 выселение вызывается только методами get(), put() и replace(), что объясняет, почему я не был видя эффект для containsKey(). Это, очевидно, изменится со следующей версией guava, которая скоро будет выпущена, но, к сожалению, моя версия проекта установлена ​​раньше.

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

ConcurrentMap<String, MyObject> cache = new MapMaker()
        .expireAfterAccess(10, TimeUnit.MINUTES)
        .makeComputingMap(loadFunction);

где loadFunction загружает MyObject, соответствующий ключу из базы данных. Это начинает выглядеть так, будто у меня нет простого способа вытеснить до r10. Но даже возможность надежного принудительного выселения ставится под сомнение второй частью моего вопроса:

Вторая часть моего вопроса [решена]: В ответ на один из ответов на связанный вопрос, касается касания карты надежно выселить все истекшие записи? В связанном ответе Niraj Tolia указывает на другое, заявив, что выселение потенциально может обрабатываться только партиями, что означало бы, что несколько вызовов касаться карты могут потребоваться для обеспечения того, чтобы все объекты с истекшим сроком действия были выселены. Он не уточнил, однако это похоже на то, что карта разделена на сегменты на основе уровня concurrency. Предполагая, что я использовал r10, в котором a containsKey("") вызывает выселение, будет ли это тогда для всей карты или только для одного из сегментов?

Ответ: maaartinus рассмотрел эту часть вопроса:

Остерегайтесь, что containsKey и другие методы чтения работают только postReadCleanup, что ничего не делает, кроме каждого 64-го вызова (см. DRAIN_THRESHOLD). Более того, похоже, что все методы очистки работают только с одним сегментом.

Итак, похоже, что вызов containsKey("") не будет жизнеспособным исправлением, даже в r10. Это уменьшает мой вопрос до заголовка: как я могу надежно заставить выселение произойти?

Примечание.. По той причине, что моя веб-приложение заметно затронута этой проблемой, заключается в том, что когда я реализовал кэширование, я решил использовать несколько карт - по одному для каждого класса моих объектов данных. Таким образом, с этой проблемой существует вероятность того, что выполняется одна область кода, в результате чего кешируется куча объектов Foo, а затем кеш Foo не тронут снова в течение длительного времени, чтобы он не выселял что-нибудь. Тем временем объекты Bar и Baz кэшируются из других областей кода, а память еется. Я устанавливаю максимальный размер на этих картах, но это лучшая защита в лучшем случае (я предполагаю, что ее эффект немедленный - все равно нужно подтвердить это).

ОБНОВЛЕНИЕ 1: Спасибо Даррену за то, что связали соответствующие вопросы - теперь у них есть мои голоса. Таким образом, похоже, что разрешение находится в стадии разработки, но вряд ли оно будет в r10. Тем временем мой вопрос остается.

ОБНОВЛЕНИЕ 2: На данный момент я просто жду, чтобы член команды Guava дал отзыв о хаке maaartinus, и я собрал вместе (см. ответы ниже).

ПОСЛЕДНЕЕ ОБНОВЛЕНИЕ: полученная обратная связь!

4b9b3361

Ответ 1

Я только добавил метод Cache.cleanUp() в Guava. После перехода с MapMaker на CacheBuilder вы можете использовать это для принудительного выселения.

Ответ 2

Мне было интересно узнать о той же проблеме, которую вы описали в первой части вашего вопроса. Из того, что я могу сказать, глядя на исходный код для Guava CustomConcurrentHashMap (выпуск 9), кажется, что записи вытесняются на get(), put() и replace(). Метод containsKey(), похоже, не вызывает выселения. Я не уверен на 100%, потому что я быстро выполнил код.

Update:

Я также нашел более новую версию CustomConcurrentHashmap в репозитории Guava git, и она выглядит как containsKey() была обновлена вызывать выселение.

Оба релиза 9 и последняя версия, которую я только что нашел, не вызывают выселения при вызове size().

Обновление 2:

Недавно я заметил, что Guava r10 (еще не выпущенный) имеет новый класс CacheBuilder. В основном этот класс представляет собой разветвленную версию MapMaker, но с учетом кэширования. Документация предполагает, что она будет поддерживать некоторые из требований выселения, которые вы ищете.

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

Ответ 3

Остерегайтесь того, что containsKey и другие методы чтения работают только postReadCleanup, что ничего не делает, кроме каждого 64-го вызова (см. DRAIN_THRESHOLD). Более того, похоже, что все методы очистки работают только с одним сегментом.

Самый простой способ принудительного выселения, похоже, состоит в том, чтобы помещать некоторый фиктивный объект в каждый сегмент. Чтобы это сработало, вам нужно проанализировать CustomConcurrentHashMap.hash(Object), что, безусловно, не является хорошей идеей, так как этот метод может измениться в любое время. Кроме того, в зависимости от класса ключа может быть сложно найти ключ с хэш-кодом, который гарантирует, что он попадет в данный сегмент.

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

Возможно, вы могли бы взломать исходный код CustomConcurrentHashMap, это может быть так же тривиально, как

public void runCleanup() {
    final Segment<K, V>[] segments = this.segments;
    for (int i = 0; i < segments.length; ++i) {
        segments[i].runCleanup();
    }
}

но я бы не сделал этого без большого количества тестов и/или ОК членом команды guava.

Ответ 4

Да, мы несколько раз возвращались назад и вперед о том, должны ли эти задачи очистки выполняться в фоновом потоке (или пуле) или должны выполняться в потоках пользователей. Если бы они были сделаны в фоновом потоке, это в конечном итоге произойдет автоматически; поскольку это так, это произойдет только по мере использования каждого сегмента. Мы по-прежнему пытаемся придумать правильный подход здесь - я не удивлюсь, увидев это изменение в какой-то будущей версии, но я также ничего не могу обещать или даже сделать убедительное предположение о том, как это изменится. Тем не менее, вы представили разумный прецедент для какого-то фона или пользовательской очистки.

Ваш взлом является разумным, если вы помните, что он взломан и может сломаться (возможно, тонким образом) в будущих выпусках. Как вы можете видеть в источнике, Segment.runCleanup() вызывает runLockedCleanup и runUnlockedCleanup: runLockedCleanup() не будет иметь никакого эффекта, если он не сможет заблокировать сегмент, но если он не может заблокировать сегмент, потому что какой-то другой поток имеет сегмент заблокирован, и можно ожидать, что другой поток вызовет runLockedCleanup как часть его работы.

Кроме того, в r10 существует CacheBuilder/Cache, аналогичный MapMaker/Map. Кэш - это предпочтительный подход для многих современных пользователей makeComputingMap. Он использует отдельную CustomConcurrentHashMap в пакете common.cache; в зависимости от ваших потребностей, вы можете захотеть, чтобы ваш GuavaEvictionHacker работал с обоими. (Механизм тот же, но они разные Классы и, следовательно, разные методы.)

Ответ 5

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

Легким подходом является использование очереди приоритетов слабых опорных задач и выделенного потока. Это имеет недостаток в создании многих устаревших задач no-op, которые могут стать чрезмерными из-за штрафа за вставку O (lg n). Он работает достаточно хорошо для небольших, менее часто используемых кэшей. Это был оригинальный подход MapMaker и его простой способ написать собственный декоратор.

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

Самым простым является использование #concurrencyLevel (1), чтобы заставить MapMaker использовать один сегмент. Это уменьшает запись concurrency, но большинство кешей считываются тяжелыми, поэтому потеря минимальна. Исходный взлом для подталкивания карты с помощью фиктивного ключа затем будет работать нормально. Это был бы мой предпочтительный подход, но два других варианта в порядке, если у вас большие нагрузки на запись.

Ответ 6

Я не знаю, подходит ли это для вашего случая использования, но ваша основная забота об отсутствии выключения фонового кэша, по-видимому, является потреблением памяти, поэтому я подумал бы, что использование softValues ​​() на MapMaker позволяет Garbage Collector для восстановления записей из кеша при возникновении ситуации с низкой памятью. Это может быть легко для вас. Я использовал это на сервере подписки (ATOM), где записи подаются через кеш Гуавы, используя SoftReferences для значений.

Ответ 7

Основываясь на ответе maaartinus, я придумал следующий код, который использует отражение, а не прямое изменение источника (если вы найдете это полезным, пожалуйста, поддержите его ответ!). В то время как для использования рефлексии это приведет к штрафу за производительность, разница должна быть незначительной, так как я буду запускать ее примерно каждые 20 минут для каждой карты кэширования (я также кэширую динамический поиск в статическом блоке, который поможет). Я провел некоторое начальное тестирование и, похоже, работает по своему усмотрению:

public class GuavaEvictionHacker {

   //Class objects necessary for reflection on Guava classes - see Guava docs for info
   private static final Class<?> computingMapAdapterClass;
   private static final Class<?> nullConcurrentMapClass;
   private static final Class<?> nullComputingConcurrentMapClass;
   private static final Class<?> customConcurrentHashMapClass;
   private static final Class<?> computingConcurrentHashMapClass;
   private static final Class<?> segmentClass;

   //MapMaker$ComputingMapAdapter#cache points to the wrapped CustomConcurrentHashMap
   private static final Field cacheField;

   //CustomConcurrentHashMap#segments points to the array of Segments (map partitions)
   private static final Field segmentsField;

   //CustomConcurrentHashMap$Segment#runCleanup() enforces eviction on the calling Segment
   private static final Method runCleanupMethod;

   static {
      try {

         //look up Classes
         computingMapAdapterClass = Class.forName("com.google.common.collect.MapMaker$ComputingMapAdapter");
         nullConcurrentMapClass = Class.forName("com.google.common.collect.MapMaker$NullConcurrentMap");
         nullComputingConcurrentMapClass = Class.forName("com.google.common.collect.MapMaker$NullComputingConcurrentMap");
         customConcurrentHashMapClass = Class.forName("com.google.common.collect.CustomConcurrentHashMap");
         computingConcurrentHashMapClass = Class.forName("com.google.common.collect.ComputingConcurrentHashMap");
         segmentClass = Class.forName("com.google.common.collect.CustomConcurrentHashMap$Segment");

         //look up Fields and set accessible
         cacheField = computingMapAdapterClass.getDeclaredField("cache");
         segmentsField = customConcurrentHashMapClass.getDeclaredField("segments");
         cacheField.setAccessible(true);
         segmentsField.setAccessible(true);

         //look up the cleanup Method and set accessible
         runCleanupMethod = segmentClass.getDeclaredMethod("runCleanup");
         runCleanupMethod.setAccessible(true);
      }
      catch (ClassNotFoundException cnfe) {
         throw new RuntimeException("ClassNotFoundException thrown in GuavaEvictionHacker static initialization block.", cnfe);
      }
      catch (NoSuchFieldException nsfe) {
         throw new RuntimeException("NoSuchFieldException thrown in GuavaEvictionHacker static initialization block.", nsfe);
      }
      catch (NoSuchMethodException nsme) {
         throw new RuntimeException("NoSuchMethodException thrown in GuavaEvictionHacker static initialization block.", nsme);
      }
   }

   /**
    * Forces eviction to take place on the provided Guava Map. The Map must be an instance
    * of either {@code CustomConcurrentHashMap} or {@code MapMaker$ComputingMapAdapter}.
    * 
    * @param guavaMap the Guava Map to force eviction on.
    */
   public static void forceEvictionOnGuavaMap(ConcurrentMap<?, ?> guavaMap) {

      try {

         //we need to get the CustomConcurrentHashMap instance
         Object customConcurrentHashMap;

         //get the type of what was passed in
         Class<?> guavaMapClass = guavaMap.getClass();

         //if it a CustomConcurrentHashMap we have what we need
         if (guavaMapClass == customConcurrentHashMapClass) {
            customConcurrentHashMap = guavaMap;
         }
         //if it a NullConcurrentMap (auto-evictor), return early
         else if (guavaMapClass == nullConcurrentMapClass) {
            return;
         }
         //if it a computing map we need to pull the instance from the adapter "cache" field
         else if (guavaMapClass == computingMapAdapterClass) {
            customConcurrentHashMap = cacheField.get(guavaMap);
            //get the type of what we pulled out
            Class<?> innerCacheClass = customConcurrentHashMap.getClass();
            //if it a NullComputingConcurrentMap (auto-evictor), return early
            if (innerCacheClass == nullComputingConcurrentMapClass) {
               return;
            }
            //otherwise make sure it a ComputingConcurrentHashMap - error if it isn't
            else if (innerCacheClass != computingConcurrentHashMapClass) {
               throw new IllegalArgumentException("Provided ComputingMapAdapter inner cache was an unexpected type: " + innerCacheClass);
            }
         }
         //error for anything else passed in
         else {
            throw new IllegalArgumentException("Provided ConcurrentMap was not an expected Guava Map: " + guavaMapClass);
         }

         //pull the array of Segments out of the CustomConcurrentHashMap instance
         Object[] segments = (Object[])segmentsField.get(customConcurrentHashMap);

         //loop over them and invoke the cleanup method on each one
         for (Object segment : segments) {
            runCleanupMethod.invoke(segment);
         }
      }
      catch (IllegalAccessException iae) {
         throw new RuntimeException(iae);
      }
      catch (InvocationTargetException ite) {
         throw new RuntimeException(ite.getCause());
      }
   }
}

Я ищу отзыв о том, целесообразно ли использовать этот подход в качестве остановки, пока проблема не будет решена в выпуске Guava, особенно от членов команды Guava, когда они получат минута.

EDIT: обновлено решение, позволяющее автоматически выделять карты (NullConcurrentMap или NullComputingConcurrentMap, находящиеся в ComputingMapAdapter). Это оказалось необходимым в моем случае, так как я называю этот метод на всех моих картах, а некоторые из них являются автоэквивалентами.