надежное принудительное выселение карты Гуава - PullRequest
19 голосов
/ 15 августа 2011

РЕДАКТИРОВАТЬ: Я реорганизовал этот вопрос, чтобы отразить новую информацию, которая с тех пор стала доступна.

Этот вопрос основан на ответах на вопрос Viliam относительно использования ленивым выселением на картах Гуавы: лени выселения на картах Гуавы

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

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

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

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

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

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 указывает на иное, говоря, что выселение потенциально может быть обработано только партиями, что может означать, что для того, чтобы все объекты с истекшим сроком действия были удалены, может потребоваться несколько вызовов для прикосновения к карте. Он не уточнил, однако это связано с тем, что карта разбита на сегменты в зависимости от уровня параллелизма. Предполагая, что я использовал r10, в котором containsKey("") вызывает выселение, будет ли это тогда для всей карты или только для одного из сегментов?

Ответ: Мааартин обратился к этой части вопроса:

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

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

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

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

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

ПОСЛЕДНИЕ ОБНОВЛЕНИЯ: отзыв получен!

Ответы [ 7 ]

7 голосов
/ 03 сентября 2011

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

6 голосов
/ 15 августа 2011

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

Обновление:

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

И выпуск 9, и последняя найденная мной версия не вызывают выселение при вызове size().

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

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

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

5 голосов
/ 20 августа 2011

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

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

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

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

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

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

3 голосов
/ 24 августа 2011

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

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

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

2 голосов
/ 24 августа 2011

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

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

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

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

1 голос
/ 25 августа 2011

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

0 голосов
/ 22 августа 2011

Основываясь на ответе 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's a CustomConcurrentHashMap we have what we need
         if (guavaMapClass == customConcurrentHashMapClass) {
            customConcurrentHashMap = guavaMap;
         }
         //if it's a NullConcurrentMap (auto-evictor), return early
         else if (guavaMapClass == nullConcurrentMapClass) {
            return;
         }
         //if it's a computing map we need to pull the instance from the adapter's "cache" field
         else if (guavaMapClass == computingMapAdapterClass) {
            customConcurrentHashMap = cacheField.get(guavaMap);
            //get the type of what we pulled out
            Class<?> innerCacheClass = customConcurrentHashMap.getClass();
            //if it's a NullComputingConcurrentMap (auto-evictor), return early
            if (innerCacheClass == nullComputingConcurrentMapClass) {
               return;
            }
            //otherwise make sure it's a ComputingConcurrentHashMap - error if it isn't
            else if (innerCacheClass != computingConcurrentHashMapClass) {
               throw new IllegalArgumentException("Provided ComputingMapAdapter's 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, особенно изчлены команды гуавы, когда у них появляется минута.

РЕДАКТИРОВАТЬ: обновил решение, чтобы разрешить автоматическое выселение карт (NullConcurrentMap или NullComputingConcurrentMap, находящихся в ComputingMapAdapter),Это оказалось необходимым в моем случае, так как я вызываю этот метод на всех своих картах, и некоторые из них являются автоэвакуаторами.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...