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

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

Этот вопрос основан на ответах на вопрос Вилиама об использовании Google 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(). Это, по-видимому, изменится со следующей версией гуавы, которая скоро будет выпущена, но, к сожалению, выпуск моего проекта назначен раньше.

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

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

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

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

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

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

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

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

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

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

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


person Paul Bellora    schedule 15.08.2011    source источник
comment
Проблемы code.google.com/p/guava-libraries/issues /detail?id=681 и code.google .com/p/guava-libraries/issues/detail?id=608 связаны с этим.   -  person Darren Gilroy    schedule 16.08.2011
comment
@Darren спасибо, что указали на это   -  person Paul Bellora    schedule 16.08.2011


Ответы (7)


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

person fry    schedule 02.09.2011
comment
какая разница между cleanUp() и invalidateAll()? Оба, кажется, удаляют/изгоняют записи в кеше, присутствующие в то время. - person asgs; 09.09.2015
comment
cleanUp() следует удалять только недействительные записи в соответствии с конфигурацией вашего кеша. Если все еще присутствуют действительные записи (т. е. срок их действия не истек), они не будут затронуты. И наоборот, invalidateAll() буквально сделает недействительными все записи, даже те, которые иначе не считались бы просроченными. - person Jeff Evans; 30.10.2019

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

Обновление:

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

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

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

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

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

person Mark Bouchard    schedule 15.08.2011
comment
+1 хорошая работа по переходу к источнику - я должен был сделать то же самое. Интересно, что containsKey() не вызывает выселение в выпуске 9. Я также использую этот выпуск, что объясняет, почему я не вижу эффекта, поскольку его нет! - person Paul Bellora; 15.08.2011
comment
@Kublai Khan: я обновил ссылки в своем ответе. Google недавно перешел с svn на git для Guava. - person Mark Bouchard; 17.08.2011
comment
спасибо за обновления. Я думаю, что CacheBuilder показывает нам, что команда Guava заботится об этих вариантах использования, и я рад видеть, что они разветвляются, чтобы охватить их. - person Paul Bellora; 23.08.2011

Помните, что 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();
    }
}

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

person maaartinus    schedule 20.08.2011
comment
+1 спасибо за понимание и указание на этот хак. Я закодировал его в хаке на основе отражения, опубликованном в качестве ответа ниже. Причина этого в том, что изменение исходного кода Guava было неудобным для моего конкретного проекта. В любом случае, я надеюсь, что некоторые люди из Гуавы скоро смогут это оценить. Я назначу награду, как только смогу подтвердить, что это жизнеспособно. - person Paul Bellora; 22.08.2011
comment
Спасибо за щедрость, я рад, что это помогло вам найти рабочее решение. - person maaartinus; 25.08.2011
comment
@maaartinus У меня есть аналогичный вопрос здесь о загрузке гуавы Cache и мне тоже нужно сделать то же самое. У меня есть путаница, если я вызываю cleanUp() каждую минуту из фонового потока (используя ScheduledExecutorService), тогда будет вызван мой RemovalListener? Я не вижу, чтобы это называлось. Или, может быть, я смешиваю здесь много вещей. Я просто хочу загрузить все метрики в свой кеш и периодически сбрасывать эти метрики каждую минуту, отправляя их в какую-то другую систему. - person john; 13.12.2016

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

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

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

person schmoe    schedule 24.08.2011
comment
+1 Спасибо, это то подтверждение, на которое я надеялся (хотя и анонимно). Хорошая мысль о CacheBuilder, если мы перейдем на r10. Мне нравится работать с Guava, и я с нетерпением жду будущих разработок. - person Paul Bellora; 24.08.2011

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

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

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

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

person Ben Manes    schedule 24.08.2011
comment
+1 Спасибо за содержательные предложения. Ваши средние два абзаца заслуживают рассмотрения в более долгосрочной перспективе (у меня сейчас мало времени). Мне нужно было бы увидеть примеры кода третьего абзаца, чтобы понять его полностью, или же мне нужно было бы уделить больше времени пониманию модели амортизации блокировки. Спасибо, что уделили это внимание. - person Paul Bellora; 24.08.2011
comment
См. этот дизайн-документ длиной 1000 футов или соответствующий код, который является основой для Алгоритмы MapMaker. Мы с Чарльзом проведем презентацию с более подробной информацией в середине сентября. - person Ben Manes; 25.08.2011
comment
Спасибо, обязательно посмотрю этот документ. Я ничего не знал о внутренней работе Гуавы до изучения этого вопроса, который многому научил меня о том, что происходит за кулисами. С нетерпением жду возможности узнать больше и упомянутой вами презентации. - person Paul Bellora; 25.08.2011

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

person Morten    schedule 25.08.2011
comment
Спасибо, это стоило поднять. На самом деле, я изначально использовал softValues() и перешел на вытеснение по времени после того, как решил, что они слишком неуклюжи и непредсказуемы, по крайней мере, с нашей JVM. См. stackoverflow.com/questions/6592183/ и stackoverflow.com/questions/6778743/my-ideal-cache-using-guava для моего мыслительного процесса. - person Paul Bellora; 25.08.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, особенно от членов команды Guava, когда у них появится минутка.

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

person Paul Bellora    schedule 22.08.2011
comment
ИМХО, о проигрыше производительности говорить не стоит, так как очистка — довольно нетривиальная операция. Тем не менее, я бы предпочел взломать исходный код отражению, поскольку он намного короче и, следовательно, менее подвержен ошибкам. При изменении гуавы оба могут сломаться в любом случае. Конечно, у вас могут быть веские причины предпочесть рефлексию. - person maaartinus; 22.08.2011
comment
@maaartinus - на самом деле, просто добраться до CustomConcurrentHashMap оказалось самой большой битвой - что-то подобное могло быть необходимо в любом случае, или же множество небольших взаимосвязанных хаков, сделанных для нескольких классов гуавы. Тем не менее, я все еще в долгу перед вами за то, что вы придумали основной код. - person Paul Bellora; 23.08.2011