SoftReference Read-Write Cache для Hibernate — с «хвостами» для реализации кластера

    В этом топике попробую рассказать о реализации системы кеширования данных в CMS ArpSite. Эта подсистема уже не раз переписывалась, но она является именно тем, что позволяет системе вообще работать с приемлимым временем отклика.

    Использование SoftReference даёт возможность оставить поведение кеширования «на самотёк» — за пореблением памяти следит сама JVM, она же очищает старые элементы.

    «Cluster» означает, что кеш самостоятельно отслеживает события инвалидации (устаревания, обновления) значений и следит за тем, чтобы на других серверах в кластере устаревшие элементы выкидывались из кеша. Есть и другие определения кластерного кеша. Например, основной режим работы JBoss Cache может даже «вытягивать» элементы с других узлов кластера, если их нет на текущей машине. Но для нашей системы, где скорость получения элемента с другой машины ненамного меньше скорости генерации элемента, достаточно инвалидации.

    Read-Write означает, что мы не используем блокировку элементов кеша и вообще не думаем (почти) о том, что в нашей системе есть какие-то независимые транзакции. Для CMS это нормально, но, разумеется, для какого-нибудь enterprise application это было бы недопустимо.

    В настоящий момент кеширование в ArpSite разделяется на три уровня:
    • Кеширование сгенерированных страниц
    • Остальные кеши
    • Кеширование данных

    Остальные кеши не случайно находятся по середине. Сгененрированные страницы это то, что будет отдано пользователю немедленно — если совпадёт URL и некоторые другие параметры. Обращений к базе для этого не нужно вообще. Кеширование данных — это то, что сейчас называется кешем второго уровня (second level cache) в Hibernate — кешируются отдельные строки базы данных (по ключу) и некоторые популярные запросы к базе данных (по аргументам). Под остальными кешами понимаются различные механизмы кеширования промежуточных результатов генерирования страниц, кеширования таблиц стилей XSLT, кусочков XML и прочих.

    В случае, если в системе происходит какое-либо изменение, нам нужно очищать кеши. Для этого система при каждом изменении данных генерирует одно или несколько событий (event'ов). Эти события асинхронно рассылаются по остальным нодам кластера, после чего кеши реагируют на эти события полной очисткой или иинвалидацией отдельных элементов.

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

    Кеш данных свой на каждом сервере. И раньше, до внедрения кеша на JGroups, если какое-то событие происходило, то приложение вызывало команды инвалидации кеша данных или его полной очистки на основании информации из пришедшего события. Подобная процедура работала, но имела серьёзный недостаток — любое кеширование данных требовало либо очень детально продумывать процедуру реакции на события, либо просто очищать кеш по каждому изменению. То, что сейчас и происходит в «остальных» кешах. Особенно проблемно было писать кеширование отдельных запросов к базе данных — нужно было запоминать, какие таблицы используются в запросе и очищать кеш, когда данные в этих таблицах поменялись. И подобный код нужно было писать для каждого запроса, который мы хотим закешировать…

    Но ведь это то, что Hibernate уже делает самостоятельно! Hibernate знает о том, какие кеши нужно очищать при том или ином изменении данных, и нужно всего лишь «подсмотреть», что он делает на одной ноде, и сделать тоже самое — на остальных.

    Новый Cache API для Hibernate


    В терминологии Hibernate один Cache — это отдельный кусок кеша, содержащий набор одинаково типизированных данных. Например, данных из одной таблицы, либо данных, например, сразу по всем закешированным запросам к базе данных. Для того, чтобы управлять поведением кеширования в Hibernate, программист указывает какой CacheProvider использовать. Это может быть, например, EhCacheProvider или тривиальные HashtableCacheProvider или NoCacheProvider.

    В версии 3.3 Hibernate ввёл новый API для кеширования, разделив один старый интерфейс Cache на несколько:
    Region
    ** General Data Region
    **** Query Results Region
    **** Timestamps Region
    ** Transactional Data Region
    **** Collection Region
    **** Entity Region

    Также каждый Region теперь предоставляет методы для работы вне контекста (destroy, getSize, getName()), General Data Region — стандартные методы get / put, а transaction data region организует работу с данными через паттерн Strategy — CollectionRegionAccessStrategy для Collection Region и EntityRegionAccessStrategy для Entity Region. Эти интерфейсы содержат, в числе прочего, команды для управления блокировками данных в рамках транзакций.

    У нас уже был раньше кеш для Hibernate, основанный на старом API. Но от него пришлось отказаться, так как старый API не позволял отличить добавление данных в кеш из базы данных от их обновления (так как они устарели). Поэтому первым шагом была реализация нового API, но уже на основе Region / Strategy. Большую помощь оказало то, что Hibernate имеет реализации адаптеров из нового API в старый. Можно просто подсмотреть реализацию, например, EntityRegionAccessStrategy на основе non-transaction cache. Что и было сделано.

    Основу API составляет Region:
    1. abstract class SoftRegion implements ArpSiteRegion, Cache, SoftRegionMBean {
    2.  
    3.   /**
    4.    * Default no-operation implementation of {@link ArpSiteRegionListener}.
    5.    * Will be replaced by cluster listener if cluster extension is installed
    6.    *
    7.    * @author vlsergey {at} gmail {dot} com
    8.    */
    9.   private static final class NoOpListener implements ArpSiteRegionListener {
    10.     @Override
    11.     public void onClear() {
    12.       // noop
    13.     }
    14.  
    15.     @Override
    16.     public void onRemove(Object key) {
    17.       // noop
    18.     }
    19.   }
    20.  
    21.   private AtomicLong clears = new AtomicLong(0);
    22.  
    23.   private AtomicLong hits = new AtomicLong(0);
    24.  
    25.   protected final AtomicLong lastTimestamp = new AtomicLong();
    26.  
    27.   protected volatile ArpSiteRegionListener listener = new NoOpListener();
    28.  
    29.   private final Map<Object, Object> map;
    30.  
    31.   private AtomicLong miss = new AtomicLong(0);
    32.  
    33.   private final String regionName;
    34.  
    35.   /**
    36.    * @param regionName
    37.    *      The name of the cache region.
    38.    * @param useHardReferences
    39.    *      <code>true</code> if cache must use hard references instead of
    40.    *      {@link java.lang.ref.SoftReference}s. <code>false</code>
    41.    *      otherwise.
    42.    */
    43.   @SuppressWarnings("unchecked")
    44.   SoftRegion(String regionName, boolean useHardReferences) {
    45.     this.regionName = regionName;
    46.  
    47.     if (useHardReferences) {
    48.       map = new ConcurrentHashMap<Object, Object>();
    49.     } else {
    50.       map = Collections
    51.           .<Object, Object> synchronizedMap(new ReferenceMap(
    52.               ReferenceMap.SOFT, ReferenceMap.SOFT, true));
    53.     }
    54.   }
    55.  
    56.   @Override
    57.   public void clear() {
    58.     map.clear();
    59.     clears.incrementAndGet();
    60.     listener.onClear();
    61.   }
    62.  
    63.   @Override
    64.   public boolean contains(Object key) {
    65.     Object value = map.get(key);
    66.     if (value != null) {
    67.       // to let JVM know it shall not GC it because of timeout
    68.       value.hashCode();
    69.       return true;
    70.     }
    71.     return false;
    72.   }
    73.  
    74.   @Override
    75.   public void destroy() throws CacheException {
    76.     map.clear();
    77.   }
    78.  
    79.   protected Object get(Object key) {
    80.     final Object result = map.get(key);
    81.  
    82.     if (result == null) {
    83.       miss.incrementAndGet();
    84.     } else {
    85.       hits.incrementAndGet();
    86.     }
    87.  
    88.     return result;
    89.   }
    90.  
    91.   @Override
    92.   public long getBuildCalls() {
    93.     return miss.get();
    94.   }
    95.  
    96.   @Override
    97.   public int getCacheSize() {
    98.     return map.size();
    99.   }
    100.  
    101.   @Override
    102.   public long getClearCalls() {
    103.     return clears.get();
    104.   }
    105.  
    106.   @Override
    107.   public long getElementCountInMemory() {
    108.     return map.size();
    109.   }
    110.  
    111.   @Override
    112.   public long getElementCountOnDisk() {
    113.     return 0;
    114.   }
    115.  
    116.   @Override
    117.   public long getGetCalls() {
    118.     return hits.get() + miss.get();
    119.   }
    120.  
    121.   @Override
    122.   public long getHits() {
    123.     return hits.get();
    124.   }
    125.  
    126.   @Override
    127.   public int getHitsPercent() {
    128.     long hits = this.hits.get();
    129.     long total = hits + miss.get();
    130.     if (total == 0) {
    131.       return 0;
    132.     }
    133.     return (int) (hits * 100 / total);
    134.   }
    135.  
    136.   @Override
    137.   public String getName() {
    138.     return regionName;
    139.   }
    140.  
    141.   @Override
    142.   public long getSizeInMemory() {
    143.     return -1;
    144.   }
    145.  
    146.   @Override
    147.   public int getTimeout() {
    148.     return Timestamper.ONE_MS * 60000; // ie. 60 seconds
    149.   }
    150.  
    151.   @Override
    152.   public long nextTimestamp() {
    153.     final long next = Timestamper.next();
    154.     lastTimestamp.set(next);
    155.     return next;
    156.   }
    157.  
    158.   protected void put(Object key, Object value) {
    159.     map.put(key, value);
    160.   }
    161.  
    162.   @Override
    163.   public void remove(Object key) {
    164.     map.remove(key);
    165.     listener.onRemove(key);
    166.   }
    167.  
    168.   @Override
    169.   public void setListener(ArpSiteRegionListener listener) {
    170.     this.listener = listener;
    171.   }
    172.  
    173.   @Override
    174.   public Map<?, ?> toMap() {
    175.     return Collections.unmodifiableMap(map);
    176.   }
    177.  
    178.   @Override
    179.   public String toString() {
    180.     return "SoftReferenceCache [" + regionName + ", "
    181.         + this.getSizeInMemory() + ']';
    182.   }
    183.  
    184. }
    * This source code was highlighted with Source Code Highlighter.

    Реализация основных методов довольна тривиальна. Мы реализуем интерфейс ArpSiteRegion, так как он содержит дополнительный метод setListener(), который нам позже понадобится для реализации работы в кластере. Методы nextTimestamp/getTimeout нам потребуются для TimestampRegionImpl. Дополнительные методы вроде getClearCalls/getBuildCalls/getHitsPercent/clear определены в интерфейсе Cache нашего приложения и нужны для управления кешем через JMX. Hibernate, к сожалению, подобный функционал не предоставляет. Да и откуда он знает про особенности нашего кеша ;)

    SoftCollectionRegion и SoftEntityRegion являются очень похожими, поэтому приведу только второй из них:
    1. class SoftEntityRegion extends SoftTransactionalDataRegion implements
    2.     EntityRegion {
    3.  
    4.   /**
    5.    * @param regionName
    6.    *      The name of the cache region.
    7.    */
    8.   SoftEntityRegion(String regionName,
    9.       CacheDataDescription cacheDataDescription) {
    10.     super(regionName, cacheDataDescription);
    11.   }
    12.  
    13.   @Override
    14.   public EntityRegionAccessStrategy buildAccessStrategy(AccessType accessType) {
    15.     return new EntityRegionAccessStrategy() {
    16.  
    17.       @Override
    18.       public boolean afterInsert(Object key, Object value, Object version) {
    19.         return false;
    20.       }
    21.  
    22.       @Override
    23.       public boolean afterUpdate(Object key, Object value,
    24.           Object currentVersion, Object previousVersion, SoftLock lock) {
    25.         SoftEntityRegion.this.remove(key);
    26.         return false;
    27.       }
    28.  
    29.       @Override
    30.       public void evict(Object key) {
    31.         SoftEntityRegion.this.remove(key);
    32.       }
    33.  
    34.       @Override
    35.       public void evictAll() {
    36.         SoftEntityRegion.this.clear();
    37.       }
    38.  
    39.       @Override
    40.       public Object get(Object key, long txTimestamp) {
    41.         return SoftEntityRegion.this.get(key);
    42.       }
    43.  
    44.       @Override
    45.       public EntityRegion getRegion() {
    46.         return SoftEntityRegion.this;
    47.       }
    48.  
    49.       @Override
    50.       public boolean insert(Object key, Object value, Object version) {
    51.         return false;
    52.       }
    53.  
    54.       @Override
    55.       public SoftLock lockItem(Object key, Object version) {
    56.         return null;
    57.       }
    58.  
    59.       @Override
    60.       public SoftLock lockRegion() {
    61.         return null;
    62.       }
    63.  
    64.       @Override
    65.       public boolean putFromLoad(Object key, Object value,
    66.           long txTimestamp, Object version) {
    67.         SoftEntityRegion.this.put(key, value);
    68.         return true;
    69.       }
    70.  
    71.       @Override
    72.       public boolean putFromLoad(Object key, Object value,
    73.           long txTimestamp, Object version, boolean minimalPutOverride) {
    74.         if (minimalPutOverride && contains(key)) {
    75.           return false;
    76.         }
    77.  
    78.         SoftEntityRegion.this.put(key, value);
    79.         return true;
    80.       }
    81.  
    82.       @Override
    83.       public void remove(Object key) {
    84.         SoftEntityRegion.this.remove(key);
    85.       }
    86.  
    87.       @Override
    88.       public void removeAll() {
    89.         SoftEntityRegion.this.clear();
    90.       }
    91.  
    92.       @Override
    93.       public void unlockItem(Object key, SoftLock lock) {
    94.         SoftEntityRegion.this.remove(key);
    95.       }
    96.  
    97.       @Override
    98.       public void unlockRegion(SoftLock lock) {
    99.         SoftEntityRegion.this.clear();
    100.       }
    101.  
    102.       @Override
    103.       public boolean update(Object key, Object value,
    104.           Object currentVersion, Object previousVersion) {
    105.         SoftEntityRegion.this.remove(key);
    106.         return false;
    107.       }
    108.     };
    109.   }
    110.  
    111. }
    * This source code was highlighted with Source Code Highlighter.


    Основная работа выполняется через интерфейс стратегии. Идея использования отдельного интерфейса в том, что в транзакционной реализации каждая транзакция будет использовать отдельный экземпляр EntityRegionAccessStrategy. Однако у нас мы просто перенаправляем все запросы к SoftEntityRegion. Особенности реализации методов update(), unlockItem() и unlockRegion() были подсмотрены у класса EntityAccessStrategyAdapter. С точки зрения будущего «прикручивания» кластера важно отметить, что листенер из SoftRegion будет реагировать только на методы remove() и clear().

    Кто их будет вызывать? Сам Hibernate будет их вызывать в те моменты, когда закончит работать с отдельными объектами. Например, он всегда будет вызывать evict(), или же evictAll() — если мы что-то делали с помощью запросов. Именно такая предсказуемая работа с кешем и даст нам возможность в будущем легко добавить поддержку кластера — всего лишь отследив вызовы clear / remove.

    А вот с QueryCache не всё так просто. Проблема в том, что Hibernate… не удаляет старые запросы из кеша. То есть не инвалидирует значения в кеше если что-то произошло. Его поведение хитрее: рядом с QueryResultsRegion трудится TimestampsRegion, который содержит, судя по его названию, timestamp'ы последних изменений отдельных таблиц в базе данных. Поэтому если нам надо выполнить запрос к базе данных, Hibernate делает следующее:
    1. Взять timestamp для таблицы из TimestampsRegion
    2. Взять результат из QueryResultsRegion
    3. У результата взять Timestamp
    4. Сравнить с тем, что было взять из TimestampsRegion. Если значение больше — то результат в QueryResultsRegion считается более новым, чем последнее изменение, и его можно использовать.
    5. Важно — если вTimestampsRegion нет значения для определённой таблицы, Hibernate считает, что её данные не менялись
    6. Если же timestamp результата меньше, чем взятый из TimestampsRegion, то значение считается устаревшим. Однако, при этом значение не удаляется из кеша, а заменяется тем, что будет получено из базы

    При этом в случае каких-либо запросов на UPDATE или INSERT в TimestampsRegion будет помещён новый timestamp для таблицы, но при этом QueryCache об этом никак оповещён не будет!

    Из 5-го пункта, кстати, следует, что для нормальной работы даже без кластера ни в коему случае нельзя использовать SoftReference для хранения значений в TimestampsRegion. Даже в описании UpdateTimestampsCache сказано — "we recommend that the the underlying cache not be configured for expiry at all" — то есть не удалять значения из этого кеша вообще. Благо, он не очень-то много места занимает в памяти.

    Из описания выше ясно, что простой перехват clear()/remove() ничего не даст — они просто не вызываются. Перехватывать put() у QueryCache бесполезно, да и может оказаться поздно — timestamp мог уже обновиться. Поэтому возможны два варианта:
    • Делать полноценную реализацию TimestampsRegion на уровне кластера. А именно — отслеживать выдаваемые на каждой машине timestapm'ы и корректно их синхронизировать на уровне кластера
    • Делать грязные хаки

    К сожалению, первый подход для CMS Arp.Site оказался недопустим — синхронизация Timestamp предполагает некий общий счётчик вроде AtomicLong, но работающий в рамках кластера. То есть все обращения к нему должны быть синхронизированы — каждое добавления объекта в QueryCache кеш. А это очень отрицательно сказалось бы на производительности — синхронная обработка в сети намного дороже, чем асинхронная.

    Чтобы и здесь реализовать асинхронный подход, мы совершили грязный хак. А именно — мы написали TimestampCache таким образом, что он знает, какие значения Hibernate в него помещает. Это плохо — потому что Hibernate в следующей версии может и поменять тип значений. Поэтому это и называется хаком. Зато работает:

    1. class TimestampRegionImpl extends SoftRegion implements ArpSiteTimestampRegion {
    2.   /**
    3.    * Default no-operation implementation of
    4.    * {@link ArpSiteTimestampRegionListener}. Will be replaced by cluster
    5.    * listener if cluster extension is installed
    6.    *
    7.    * @author vlsergey {at} gmail {dot} com
    8.    */
    9.   private static final class NoOpListener implements
    10.       ArpSiteTimestampRegionListener {
    11.  
    12.     @Override
    13.     public void onInvalidate(Serializable space) {
    14.       // no op
    15.     }
    16.  
    17.     @Override
    18.     public void onPreInvalidate(Serializable space) {
    19.       // no op
    20.     }
    21.  
    22.   }
    23.  
    24.   private ArpSiteTimestampRegionListener listener = new NoOpListener();
    25.  
    26.   /**
    27.    * @param regionName
    28.    *      The name of the region.
    29.    */
    30.   TimestampRegionImpl(String regionName) {
    31.     super(regionName, true);
    32.   }
    33.  
    34.   @Override
    35.   public void evict(Object key) {
    36.     super.remove(key);
    37.   }
    38.  
    39.   @Override
    40.   public void evictAll() {
    41.     super.clear();
    42.   }
    43.  
    44.   @Override
    45.   public Object get(Object key) {
    46.     return super.get(key);
    47.   }
    48.  
    49.   @Override
    50.   public void invalidate(Serializable space) {
    51.     Long ts = Long.valueOf(nextTimestamp());
    52.     super.put(space, ts);
    53.   }
    54.  
    55.   @Override
    56.   public void preInvalidate(Serializable space) {
    57.     Long ts = Long.valueOf(nextTimestamp() + getTimeout());
    58.     super.put(space, ts);
    59.   }
    60.  
    61.   @Override
    62.   public void put(Object key, Object value) {
    63.     super.put(key, value);
    64.  
    65.     final Serializable space = (Serializable) key;
    66.     final Long timestamp = (Long) value;
    67.  
    68.     final boolean inFuture = timestamp.longValue() > lastTimestamp.get();
    69.     if (inFuture) {
    70.       listener.onPreInvalidate(space);
    71.     } else {
    72.       listener.onInvalidate(space);
    73.     }
    74.   }
    75.  
    76.   @Override
    77.   public void setTimstampListener(
    78.       ArpSiteTimestampRegionListener timestampListener) {
    79.     this.listener = timestampListener;
    80.   }
    81. }
    * This source code was highlighted with Source Code Highlighter.


    Основная работа делается в методе put(). Чтобы понять логику метода нужно остановиться на том, что именно делает Hibernate c timestamp cache. А именно делаются три операции:
    • Операция get — взять timestamp для region (скрывается за Serializable)
    • Операция invalidate — взять новый timestamp и поместить его в кеш для region
    • Операция preinvalidate — взять новый timestamp, прибавить 60 секунд и поместить в кеш

    Наша реализация пытается отследить, какое именно из последних двух действий выполняет Hibernate, и, в будущем, распространить данное действие на все узлы кластера. Для этого, разумеется, нам потребуется ещё один Listener (ArpSiteTimestampRegionListener). Разницу между invalidate и preinvalidate мы отслеживаем по тому значению, которое помещается в кеш, после чего соответствующим образом уведомляется листенер. А для кластера, для уведомления с других машин, мы сделали свои два метода (доступны через интерфейс ArpSiteTimestampRegion) invalidate и preInvalidate, которые повторяют логику Hibernate. Особенностью реализации является то, что нам не нужно знать текущие значения timestamp на разных машинах для того, чтобы инвалидировать кеши в кластере. Например, метод invalidate(region) возьмёт новый timestamp на текущей машине чтобы инвалидировать QueryCache на текущей же машине. Ему не интересно, какое было значение на той машине, где Hibernate запрашивал инвалидацию.

    Сама логика Hibernate, кстати, кому интересно, находится в классе UpdateTimestampsCache и StandardQueryCache. Если они поменяется, нашу реализацию TimestampRegionImpl, возможно, придётся переписывать. Это та цена, которую мы платим за «хак».

    Продолжение следует


    В этом топике я описал, как сделана реализация кешей с «зарубками» на то, чтобы можно было связать между собой отдельные кеши на отдельных машинах в единое кластерное решение. Зарубки состоят в следующем:
    — классы SoftRegion и TimestampRegionImpl вызывают методы интерфейсов ArpSiteRegionListener и ArpSiteTimestampRegionListener, если нужно уведомить другие машины в кластере о происходящих событиях
    — в свою очередь через интерфейсы ArpSiteRegion и ArpSiteTimestampRegion они предоставляют возможность уведомить их о событиях в кластере.

    И хотя работа с кластером пока не описана, кеши уже рабочие.

    В следующем топике — как с помощью JGroups теперь сделать кластер.
    Поделиться публикацией

    Похожие публикации

    Комментарии 23

      0
      Как вариант, есть уже готовая реализация которая вполне хорошо себя зарекомендовала.
      code.google.com/p/hazelcast/source/browse/#svn/trunk/hazelcast-hibernate%3Fstate%3Dclosed
        0
        Эта реализация примерно аналогичная, когда на одной машине, но её нельзя расширить на кластер — из-за отсутствия интерфейсов для Listener'ов, а также из-за игнорирования особенностей использования Timestamp Region
          0
          На самом деле она изначально кластерная. Посмотрите внимательно ;) Отсутствие Listener'ов на высоком уровне не означает что их нет на более низком, там используется общая мапа для hazelcast cluster. В конструкторе класса HazelcastCache как раз и берется инстанс cache = Hazelcast.getMap(regionName); Из видео становится понятней что из себя представляет Hazelcast впринципе www.hazelcast.com/gettingstarted.htm.
            0
            Если там нет листенеров на высоком уровне, значит, кластер не учитывает особенности hibernate, и, единственное работающее решение — это кластерный кеш вроде старого jboss, где общее хранилище для всех нод.

            Более того, судя по реализации Region у них будут проблемы с реализацией timestamp'ов — они у них разные на разных машинах :) И если часы рассинхронизированы (а у нас они часто в таком состоянии) кеш начнёт глючить.
      0
      А где же потолок этого кэша? Он что, кэширует сколько памяти хватит в надежде что в случае чего, SoftReference подсобит? Правильно я понимаю?
        0
        В этом и идея, что за памятью следит JVM, а не администратор, который не знает, сколько памяти в байтах на что можно отводить.
          –1
          Это плохая идея. Куча будет всё время переплнена, будет постоянная сборка мусора и количество объёктов в куче будет очень велико. Результат: много частых сборок кучи, причём больших с Compact and Sweep. Кроме того, каждая такая сборка будет особенно дорогой, так как производительность сборки обратно пропорциональна количеству аллоцированных объектов, а у нас как раз объектов под завязку.
            +1
            1) Давайте различать old & eden generation. Old generation на практике выше 80-85% у нас не поднимается.

            2) Сборка мусора и так постоянна, она не зависит от того, soft или hard referenc'ы в памяти. То есть замена soft references на hard не уменьшит количества объектов в памяти (без уменьшения КПД), но увеличит вероятность OutOfMemory, если мы неправильно настроим пределы кеша.

            То есть использование soft references вместо hard references не влияет на производительность сборки мусора.

            3) Полная сборка мусора на 4 Гб занимает 0.3 секунды real time — в среднем один раз за 4 часа (old generation с ConcurrentMarkSweep). Хотя и много, но допустимо. Надеемся уменьшить это время после выхода 1.7.0 и использования G1 (сейчас не используем, так как нельзя мониторить через JMX)
              0
              P.S.: Постоянная сборка мусора это не плохо. Главное, чтобы это был не stop the world full gc.
                0
                Поскольку эти объекты будут жить в кеше долго, то они переедут в oldgen и займут много места. Постоянная сборка в eden не опасна. Постоянная сборка во всех зонах кучи — это уже гораздо хуже. Заполненность кучи не должна быть запредельной.

                >> То есть использование soft references вместо hard references не влияет на производительность сборки мусора.
                Разве VM'а не должна следить за такими ссылками? Вряд ли это полностью бесплатная фича. Думаю soft-линка всё же дороже обойдётся.

                >> Полная сборка мусора на 4 Гб занимает 0.3 секунды real time
                Всё это так до тех пор пока в куче на самом деле мусор и просто всё выбрасывается. Хуже когда он долго долго проверяет видимость и в итоге ничего не выбрасывается. Легко проверить, что в кучах реальных приложений сборка выполняется во много раз дольше. А уж если начать складывать на длительном промежутке времени, то суммарное время сборки может оказаться совсем не маленьким.

                  +1
                  > Заполненность кучи не должна быть запредельной
                  а куда она денется-то в случае обычного кеша? Всё равно придётся собирать мусор и в old generation, от этого hard reference cache не спасёт.

                  > Разве VM'а не должна следить за такими ссылками? Вряд ли это полностью бесплатная фича. Думаю soft-линка всё же дороже обойдётся
                  Кто-то же должен следить за тем, какие объекты из кеша викидывать (хотя бы и по переполнению). Одно дело когда за ссылками (в том числе — last used time) следит JVM, и другое дело — когда за этим следит Java-код, реализующий кеш. Очевидно, первое дешевле.

                  > Легко проверить, что в кучах реальных приложений сборка выполняется во много раз дольше
                  А я Вам как раз про реальное приложение привёл пример :)
                    0
                    >> Одно дело когда за ссылками (в том числе — last used time) следит JVM, и другое дело — когда за этим следит Java-код, реализующий кеш. Очевидно, первое дешевле.

                    Нет, совсем не очевидно. Это создаёт лишнюю нагрузку на виртуальную машину.

                    >> а куда она денется-то в случае обычного кеша? Всё равно придётся собирать мусор и в old generation, от этого hard reference cache не спасёт.
                    В случае обычного кэша количество объектов не будет запредельным. Оно будет таким, какое будет указано — ни граммом больше.

                    Пожалуй я бы согласился скрестить ужа и ежа и сделать ограниченный Java-кодом кэш на мягких ссылках (на случай какого-то экстремального пика).
                      +1
                      >> Это создаёт лишнюю нагрузку на виртуальную машину
                      <ирония>А Java-код, реализующий логику кеша выполняется на какой-то другой машине? Или он «бесплатный»?</ирония>
                      Для меня очевидно, что если какая-то логика реализуется в JVM и в Java-коде, первая реализация выходит дешевле. Пример с System.arraycopy() уж много раз это показывал.

                      >> Оно будет таким, какое будет указано — ни граммом больше
                      Для этого нужно:
                      а) Таки правильно указать каким оно должно быть — но при количестве кешей около 100 распредить память между ними сложно.
                      б) При складывании в кеш объектов их сериализовывать — чтобы получать массив байтов
                      в) При выборке из кеша их десериализовывать
                      И всем этим должен заниматься кеш. Да, это можно делать (так делает JBoss Cache). Но это дорого с точки зрения производительности и сложно в настройке.
                        0
                        System.arraycopy() не пример, т.к. это слишком мелкая деталь. Просто ко всему может быть общий подход, а может быть подход в зависимости от конкретной ситуации (индивидуальный). JVM действует обобщённо, она не будет лезть в конкретику. Чаще всего обобщённые методе менее эффективны. Например, обобщённый метод для Map — это просто хранить глупую линейную таблицу — это просто и всегда работает. Если же углубляться в конкретику и наложить кое какие ограничения, то оказывается, что можно и побыстрее сделать.
                          +1
                          Мне кажется, спор становится малоэффективным.

                          Как я уже указал, SoftReference, по моему мнению, позволяет достичь следующего:
                          1) Объём кеша указывается в процентах к общей памяти JVM, а не в байтах, что упрощает настройку, особенно межсерверную (когда у серверов разные характеристики). Особенно когда не известно, сколько памяти займут остальные данные хранящиеся в Old Space — то есть неизвестно заранее, сколько вообще можно памяти отдать на кеш.
                          2) Объём указывается как часть настройки old space (де-факто -XX:CMSInitiatingOccupancyFraction), что позволяет нам забыть о вычислении того, сколько занимает кеш, а сколько — все остальные данные из old generation.
                          3) Отсутствует необходимость сериализации и десериализации, так как JVM и сама знает, сколько памяти занимает кеш
                          4) За evict следит JVM, что позволяет достичь преимущества в скорости по сравнению с другими кешами. Однако существует ограничение — поддерживается только NotUsedForNMinutes стратегия (где N, кажется, Xmx делённое на 40 Мб — по умолчанию), что только в приближении можно считать как LRU. Однако чтобы при SoftReference кеше добиться OutOfMemory — нужно очень сильно постараться.

                          Однако любой из существующих Java hard reference кешей не сможет без сериализации (либо без детальной интроспекции) данных удовлетворить первому требованию, так как не знает объём данных, что затруднит настройку серверов. Тем более он не знает, сколько свободной памяти у JVM ещё можно себе забрать. (требование два)

                          Для меня наличие сериализации в любом виде автоматически означает, что кеш будет медленнее, чем Soft Reference.

                          Если у вас есть сомнения, что это так, прошу вас привести пример реализации или идеи реализации кеша, который бы смог бы работать как минимум так же быстро, как SoftReferenceMap, а также поддерживать LRU (или хотя бы NotUsedForNMinutes ), и позволять указывать максимальный объём занимаемой памяти (в байтах или процентах от общей). И недопускать OOM за исключением каких-то критических случаев.
                            0
                            1. Сериализация совершенно не обязательная вещь — не хватает места — выкинуть к чёртовой матери и всё. Это кэш а не хранилище данных.

                            2. Это же кэш, опять-таки. Нам не нужно строгое следование правилу LRU или NotUsedForNMinutes. Нам достаточно примерного следования какому-то принципу. А если что-то не влезает, то надо жёстко выбрасывать. Да, я согласен что написать правило инвалидации кэша сложно так, чтобы он гарантированно очищался при каком-либо переполнении. Границу можно определить только исходя из конкретики приложения. Однако, следует понимать, что всё же если вся куча занята кэшом на SoftReferenceMap, то вместе с ним никакое нормальное приложение уже не сможет работать нормально, оно будет работать всё время в экстремальном режиме.
                              +1
                              1. А откуда вы знаете, что места не хватает, если не считаете его? JVM знает, когда места мало — и именно тогда выкидывает SoftReference'ы. А обычный кеш может лишь предполагать, основываясь на своём размере.

                              2. > «вместе с ним никакое нормальное приложение»…
                              То самое приложение, которое кеш использует, прекрасно работает. Постоянные данные — в old generation, а временные — в eden. А кеш — в кеше. Что ещё нужно?

                              Речь, разумеется, о серверных приложения, где текущие данные в основном в eden.
                    0
                    И вот ещё про 0.3 сек. на сборку 4Гб кучи. Вот у меня работает нетбинс:
                    Current heap size: 
                    113 831 kbytes
                    Maximum heap size: 
                    466 048 kbytes
                    Committed memory: 
                    218 304 kbytes
                    Pending finalization: 
                    0 objects
                    Garbage collector: 
                    Name = 'PS Scavenge', Collections = 160, Total time spent = 20,397 seconds
                    Garbage collector: 
                    Name = 'PS MarkSweep', Collections = 10, Total time spent = 9,786 seconds

                    При такой крошечной неполной куче уже показатель сопоставимый. Что же будет если взять 4Гб под завязку набитых? будет жесть. Тот же нетбинс при переполненной куче порой уходит на пару секунд в сборку.

                    К сожалению у меня нет прямо сейчас под рукой настоящего EE-приложения, но как доберусь до него, посмотрю сколько в среднем на сборку выходит. Мне кажется, что 0.3 сек на 4Г это уж больно хорошо. Хотя, возможно у вас просто хорошая тачка ;)
                      0
                      Давайте-ка возьмём серверное приложение. К сожалению, только 13 часов аптайм, но:

                      Серверная «тачка» — 2 процессора Intel® Xeon(TM) CPU 3.60GHz

                      Current heap size:
                      2 874 277 kbytes
                      Maximum heap size:
                      4 827 840 kbytes
                      Committed memory:
                      4 827 840 kbytes
                      Pending finalization:
                      0 objects

                      Garbage collector:
                      Name = 'ParNew', Collections = 3085, Total time spent = 11 minutes
                      Garbage collector:
                      Name = 'ConcurrentMarkSweep', Collections = 3, Total time spent = 1,731 seconds

                      (хм… а в логах 0.3 было… наверно завершающая фаза).

                      Опять же,
                      JAVA_OPTS="-server -Xmx4800m -Xms4800m -Xmn1g -XX:MaxPermSize=128m -XX:+DisableExplicitGC"
                      JAVA_OPTS="$JAVA_OPTS -XX:+UseConcMarkSweepGC -XX:CMSInitiatingOccupancyFraction=80 -XX:+UseParNewGC -XX:SurvivorRatio=10"
                      JAVA_OPTS="$JAVA_OPTS -Djava.lang.Integer.IntegerCache.high=1048575"
                        0
                        Вот, например, наш продакшн-сервер:

                        Uptime: 
                        9 days 19 hours 58 minutes
                        Maximum heap size: 
                        760 256 kbytes
                        Garbage collector: 
                        Name = 'Copy', Collections = 271, Total time spent = 6,957 seconds
                        Garbage collector: 
                        Name = 'MarkSweepCompact', Collections = 252, Total time spent = 2 minutes

                        Показатели выходят даже хуже (~0.5s), хотя куча почти свободна.

                        Теория теорией, а факт есть факт. Видимо, ваша идея не так уж плоха ;)
                    0
                    Да, это именно stop the world full gc и будет получаться, когда куча переполнена.
                      0
                      Мы это не допускаем с помощью -XX:CMSInitiatingOccupancyFraction=80

            Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

            Самое читаемое