Showing posts with label Performance. Show all posts
Showing posts with label Performance. Show all posts

Message Locker

Problem

Extreme requirements require interesting solutions. Sometimes there is a need to come up with hybrid solution, that not always seems to be beautiful on first sight. One example is a Message Locker solution.

In service oriented architecture, application consists of many services that interact with each other.

Inevitably comes a need to fight high latencies caused by remote calls and necessary serialization/deserialization ( in a huge chain of dependent services calls, where each call results in a network hop with required fees like data marshaling and passing data through the network, adds at least a few milliseconds extra for each call.)

Service that requires to gather output from multiple dependencies to do its job is an aggregating service.

Such service needs to be smart at how it calls dependencies. If they are called one by one, then their latencies would be accumulated. The most obvious way to prevent this is to call each dependency in parallel. Now, service own latency would be defined mostly by its slowest dependency. In this case, we say that slowest dependency is in the critical path.

Aggregating service isn’t complex because it needs to call multiple services in parallel. And usually, there is simple way to avoid creating another service, if only business value it adds is aggregating output of multiple dependencies.

But, aggregating service becomes complex when:
  1. It adds complex logic on top of the data returned by dependencies
  2. It has to be sophisticated at orchestrating calls to many dependencies.
The need to orchestrate comes from the nature of SOA: sometimes Service need to call one or more dependencies first to gather the data necessary to call another dependency. Often it’s not possible to call all dependencies in parallel and just aggregate replies once all are available. In many cases, Service needs to call dependency A, to get the data necessary to call dependency B, results from which are required to decide if Service needs to call dependency C or D and so on.

Optimal call of dependencies is often the most important thing to do when fighting high latencies. And thus, eventually comes a need to have Aggregating service, that can call multiple dependencies in a savvy way.

But, even when there is an aggregating service in use already, inevitably comes a need to fight high latencies. And there are only so many ways this can be done:
  1. decrease latency for each dependency in the critical path (often by pulling dependencies of own dependency, and call them first)
  2. call dependencies in even smarter way.

This post stops on the 2nd way. If aggregating service already parallelizes calls to dependencies as much as possible and there is no way to make it even better, then, to be honest, not much can be done anymore.

Seriously, when service A needs to call dependency B so it can call dependency C later, what else can be done to save extra 10 ms you need that much?

That’s where Message Locker comes useful. It goes to a bit nasty territory to allow save additional milliseconds in aggregating service.

Message Locker

"Message Locker" means a Locker for a Message. Service allows to store a message in the some kind of locker, so only specific client can grab it. If message is not received during certain period, message becomes unavailable.

Message Locker is a distributed service that stores all the data in the memory. Client that sends a message into the locker is called sender. Client that receives message from locker is called receiver.

Each message is stored in the locker using a unique random key. When sender puts a message into the locker, it also provides additional attributes, like:
  1. TTL - time to store the message in the locker,
  2. Reads - number of times the message can be received.

Message would be removed from the locker whenever received for defined number of times or once its TTL expired. These rules prevent Message Locker to be bloated with obsolete messages.

Even after message was removed, Message Locker is still aware of it previous presence. Whenever receiver tries to get evicted message, it gets an error immediately.

In case receiver tries to get a message that is not evicted yet, it is returned to the receiver, and number of reads is increased. This approach doesn’t handles retries properly though.

In case receiver tries to get a message that is not sent yet, then Message Locker will hold the request until message becomes available or timeout happens.

How to use Message Locker?

Given 3 services A, B and C. Service A is an aggregator service, that calls multiple other services, among them services B and C. Service B has to be called before service C, as its output is part of input for service C. Service A also uses output of service B for own needs later as well.

Normally, service A would call service B, wait for reply and then call service C. During this workflow, service A needs to do following work before it can call C. This extra work becomes part of critical path:
  1. wait for reply from service B
  2. read reply from service B
  3. construct and call service C.
Network and extra serialization/deserialization are often expensive operations, and when one works with large amounts of data, could take 5-10ms. In this case, construction request and making remote call to service C also can add additional 5-10ms.

Without Message Locker

This is where Message Locker becomes helpful. Workflow is now changed: service A calls service B with key K, and in parallel calls service A with key K, B puts its reply into MessageLocker using key K, service C receive this reply using key K. Service A also receives service B’s reply from Locker using key K, and does this in parallel with service C call.

With Message Locker

In this case, there are following notable changes:
  1. time to construct and call service C happens in parallel with call to service B, and as such is removed from critical path
  2. time to deserialize request and do necessary initial work by service C is also execute in parallel with call to service B, and as such is removed from critical path
  3. time to deserialize reply from service B in service A also happens in parallel with call to service C, and as such is removed from critical path
  4. time to call to Message Locker, receive and deserialize received data by service C are added to critical path. This would eliminate savings added by #2.

Using Message Locker also adds complexities:
  1. Service A, B and C need to be integrated with Message Locker
  2. Service A or B needs to know how many times message would be received from locker or what timeout to use in order to not overload Message Locker with not need message and not cause issues with message being removed to fast.

Why not use existing solutions like...

Message Locker by itself is very similar to well known existing solutions: Message Broker and Distributed Cache. Although similarities are strong, there are a few differences, that make Message Locker to stand out for its own very specific use case.

Message Broker?

Message Broker would usually have a predefined list of queues. Producers would send messages to the queue and Consumers would consume. It is possible to create temporary queue, but it is usually expensive operation. Message Broker usually assumes processing latencies are less important than other traits, like persistence or durability or transactionality.

In this case Message Broker can’t be a good replacement for Message Locker.

Distributed Cache?

Message Locker is more like a distributed cache, with additional limitations. Message is stored only for 1 or few reads or very limited amount of time. Message is removed from locker as soon as it becomes “received”.

In ordinary caching, it is expected that content becomes available for much longer period than it is in Message Locker.

Summary

Message Locker is a way to decrease latencies in aggregation services by enabling additional parallelization. This is possible, as dependency between services are organized through a proxy - Message Locker. It holds the replies from dependencies and provides them to the receiver as soon as they are available. This allows to further hide expensive operations: network call and serialization/deserialization.

This comes with additional complexities:
  1. Right value, for timeout and number of reads to evict, can be error prone to define,
  2. Message Locker development and support can be cumbersome as well,
  3. Restructuring services to benefit from Message Locker.

But when there is no other choice and latencies had to be increased, Message Locker could be a solution.

Identify hot data to cache using Multiple Bloom Filters

Recently was thinking how to support caching of hot data in highly loaded application with huge throughput. Here I'm describing a note about the idea, based on bloom filters, that I've ended up. Far from the ideal, and I haven't tested it yet. But at this moment, it seems optimal in terms of CPU and memory usage. Also, after some research over the internet, I found that idea is not new though (see link at the end of post.)

Bloom Filter (BF) gives a nice way to check if some value have been seen before. Of course, this is a probabilistic data structure, so result always goes with with some level of error.

Using multiple hash functions within single BF allows it to be better at avoiding conflicts, but in this case, it also requires the size of bloom filter to be larger. As an opposite to having one large BF and using multiple hash functions, it is possible to using multiple BFs, each relying only on single hash function, both have different size for each BF.

Multiple BFs could be also used to figure out if specified value has been seen for some number of times. As an example, imagine there is an application that accepts reads of data from storage. There is a need to store some data in the LRU cache, but because the number of different values is so large, it is impossible to just add every value to the cache. Cache limit + LRU policy will make sure that even hot data could be easily removed from cache. For example, assume we have a cache with N elements, and we reading from storage M elements, such that M is much much larger than N. If we would start adding every read element to the cache, we easily fill it with one-time read values, and the hot data would be evicted.

Merkle trees

Merkle tree is a hash tree, where leaves are hashes. And each intermediate node is a hash of its children. Root node is a hash of values in its children node. Thus to know the difference between two hash trees, it's just enough to compare the root nodes of those trees. It's also easy to find the actual difference between two trees: just traverse them top-to-bottom and find the nodes with different hashes. This is how Merkle tree helps to reduce the amount of data passed between two sources.

Merkle trees are used as anti-entropy mechanism that helps to detect cases of divergence between data stored in multiple location. Merkle trees, for example, are used by Dynamo and Cassandra to find the cases when replicas have different versions of data. Merkle trees are also an awesome way to detect changes before syncing up data, which is one of the primary tasks for Dropbox, Chrome Backup, Google Drive, BitTorrent Sync etc.

Merkle trees do not rely on timestamps, but on actual data differences. This makes hash trees a wonderful structure to synchronize data between source and target with minimal effort of detecting actual changes.

For example, assume that there is a backend B, and N clients, each client maintains copy of the data (like Dropbox, BitTorrent Sync, Google Drive etc.) And some of the clients changed the data locally, and uploads them to backend B. Then each client can determine the actual change and do exchange of only necessary data with backend.

Hash trees are used by Git and Bitcoin. As I mentioned before, Dynamo, Cassandra and Riak are using Merkle trees for anti-entropy as well.Merkle trees are also used in cryptography for message signatures (search for Lamport signature).

Links:

Android Emulator Snapshot

Here I'm going to describe a simple way to decrease a start up time of the Android Emulator. This is possible after snapshot saving and loading option is enabled.

Open your terminal and enter the command:

$ android

Open Android Virtual Device Manager from menu Tools -> Manage AVDs... You'll see a window like this:


AVD Manager


Select your working AVD from the list and click on "Edit" button. In the opened dialog check on to enable Snapshot. Save changes. Then click on the "Start..." button and check "Launch from snapshot" and "Save to snapshot" options.


Launch options


If you haven't enabled "Saving to snapshot" option before, it may take a time to start the emulator first time. After it is started, you can close it and start again. This time emulator will be started much faster.

tmpfs: work with your data even faster

Currently improving IDEA performance by copying cache files into the RAM using tmpfs. Actually, tmpfs and ramfs are good ideas.

As described in Wikipedia: tmpfs is intended to appear as a mounted file system, but stored in volatile memory instead of a persistent storage device. Simply put, when you copy a file to such FS, this means that you copy a file to the RAM. When you create a file in this FS, this means you create a file in RAM. When you delete a file in this FS, this means you delete a file in a RAM. And so on.

The negative side of tmpfs is that it's not backed by any storage: on restart or system crash you'll lost your files and data stored in tmpfs. But on system start you can either copy files from the disk to the memory again and continue to work.

In case of IDEA cache, I will need to write a simple script that periodically copies the cache from tmpfs to the disk. So, on restart, I can simply restore cache, and don't need to wait while re-caching is done.

So, how to create a tmpfs storage? It's pretty easy to do with next commands. Here I create an empty directory tmp and mount it as tmpfs filesystem.
# mkdir ~/tmp
# mount -t tmpfs -o size=500m tmpfs ~/tmp
Option size=500m limits memory usage to 500m. tmpfs also supports flushing content to the swap when need. This is one of the main differences between tmpfs and ramfs. ramfs is not limited and can grow dynamically, and the used memory can't be freed or flush to swap. The negative side of such dynamic nature of ramfs is that system can hung when no free memory left.

To read from and write content to RAM is much much faster than to do the same operations with a file on disk. It's pretty good optimization if you need to support read-only content or content that can easily be restored when need.

Such type of content is a hosted website. You can decrease page or resource loading time by moving them from hard disk to the memory filesystem.
 # mkdir /var/www/www.example.com
 # mount -t tmpfs -o size=50M tmpfs /var/www/www.example.com
 # cp -R /home/www/www.example.com/* /var/www/www.example/com

To mount tmpfs automatically on system load, you will need to add another record to yours /etc/fstab configuration file. The only you need to do now is execute next command on system start:
 # cp -R /home/www/www.example.com/* /var/www/www.example/com

As a summary, tmpfs is a good way to work with large amount of files or data that need to be accessed or processed quickly. Such data also is either read-only or can be easily recreated. Samples of such data are static websites, website resources, temporary cache etc. When need to process large amount of data, you can also split it into small pieces and process each one by one. tmpfs is also takes a limited amount of RAM, and can increase over that amount.

Performance Optimization for Android

Here is the list of the performance optimization tips when developing Android applications:

  • Avoid creating objects if you don’t need them.
  • Avoid memory allocation if you can work without it.
  • Array of ints is preferred than array of Integers.
  • Two parallel arrays of ints are a lot more efficient than an array of (int,int) objects. Two parallel arrays of Foo and Bar are a lot more efficient than an array of (Foo,Bar) objects.
  • Avoid creating short-term temporary objects if you can.
  • Make your method static. Invocations will be about 15%-20% faster. It's also good practice, because you can tell from the method signature that calling the method can't alter the object's state.
  • It's reasonable to follow common object-oriented programming practices and have getters and setters in the public interface, but within a class you should always access fields directly.
  • Direct field access is about 7x faster than invoking a trivial getter.
  • Use static final for constants.
  • Prefer for-each loop.
  • Declare fields and methods accessed by inner classes to have package access, rather than private access.

Read Designing for Performance article from Android Dev Guide to find more details and samples.

List also available as gist.

"A Story of Caching"

I was playing with memcached a few days ago. Never used it before, so spent some time to read documentation and FAQ.

While reading FAQ, I found the link to the page with A Story of Caching, and decided to share link to this story here.

IMHO, this is the best non-technical technical documentation, that I've read recently. In this story, they use simple but widespread use cases of developing web application and using cache in this web application.

If you don't know what's for memcached, than read this story.
If you don't know what's the primary features of memcached, than read this story.
If you don't know if you need memcached, than read this story.

Code Generation

We are writing the code and this is our job. That's the art that we make. We put our thoughts, our patient, our soul to make it ideal or near to ideal. Sometimes we just write the code asap as there is no time or code is simple or we wrote similar code hundreds of times. This small article is about writing similar code again and again.

When we are writing similar code second or fourth time it's not an art and it is tedious. But that's useful to polish previous code: we can find and fix bugs, add needed logging, rewrite with better performance, make the code clean and handy to read and change etc. Often we are changing previously written code to make it better as well.
But what if you need to write similar code tens or hundreds of times? Will you change previous 20 classes because now you find the best algorithm or fixed the annoying problem with performance? Will you be glad to fix 50 classes over the project because just now you find the bug? (so it must be fixed, right?)
That will take your time, that you could spend on coding something interesting or studying some new popular technology or have a beer with your friends. And what about customer for whom this software is a business.
That's why I'm writing about code generation, the thing we all know and use often. You remember, when you're creating new project or adding new class with your favorite IDE, you got initial code so you can write your important part rather that spend time on writing the same code again and again.
While reviewing code generation tools we can divide them into external and internal. External are provided by IDE or other providers (remember famous xdoclet toolkit?). Internal is written by your team and used withing some one or few projects.
Internal code generators could be simple enough to generate only basic template source code or UI that will be changed and extended. It also can be large and complex to generate layers (domain types, DAOs and repositories etc) of your software.

Lets review next types of code generators:
1. template code generators
2. partial code generators

Template code generators run once at the begin. They are responsible for generating source code that will be rewritten or extended by developer. It's important to have clean generated code with comments for generated code and for placements where to put your code. It's good to use such generators if you have already ideal code to be generated. Ideal means that you will not need ever to change template and regenerate the same code again and again. Regenerating code will remove code written by developers with hands.

Partial code generators could be run as much as you will need. They are separated from code written by developers. For C# that would be good to use partial keyword (as I remember partial class declaration was added to have distinct generated and written by developer code for WinForms and ASP.NET); for Java extending could be used as well. We use generator to generate only similar code and put in another file, e.g. partial class definition or base abstract class. The specific code is written in derived class. While we find new bugs and improvements we need just to change templates or generator configuration and regenerate the code again.

Although code is generated it also must be easy to read it. Document your code, as you need to do it only once - in templates, so don't be lazy and stingy! For partial code generator or template code generator that is twice as important, as you are working directly with generated code.
Don't forget to add comment with notion that code is generated automatically and can be regenerated again, so other developers will think twice before changing it by hands!

Also remember that it's okay to add code generation to project step-by-step. For example, I'm the only who is using the new generator for now. I need to do so to find and fix defects, find the parts of code that could and must be generated, improve configuration and way of use. In this case the generated base code (with partial code generator) is commited too.

Use the best tools to write templates. For example I'm using Freemarker template engine to describe templates and generate source code. There are few code generators that read configuration file and generate appropriate code. For such tools performance is not as important as flexibility of templates and configuration.

Document the generator. Share source code within team. Put your generator into source code repository near to the project, but not into it. Also make and tag latest stable version as runnable scripts or program, so other developers can update and use it without spending on it additional time.

GC and performance

Мабуть, нікому не секрет, що GC є дуже важливою річчю в JVM, і що вона займається звільненням памяті, що вже не використовуються. Також багатьом відомо, що є кілька різних політик виконання очистки пам'яті. Конфігурація не виглядає надто складною, є всього кілька можливих варіантів вибору політики GC:



  • SerialGC - встановлена по дефолту, працює найкраще якщо на машині тільки один процесор, коли виконуєть збір сміття, то програма тормозить, адже в даний момент вона не відповідає.

  • ParallelGC - використовується можливість виконувати мінорний (minor) збір сміття в паралельних потоках. Програма здатна відповідати на запити клієнта в момент збору сміття

  • ParallelOldGC - виконує мажорний (major) збір сміття в паралельних потоках

  • ConcMarkSweepGC - В даному випадку постійно відбувається збір сміття, програма не тормозить при зборах сміття, як це можна замітити в 3 попередніх випадках.


Таким чином, я вирішив переконфігурувати для прикладу IntellijIdea, що я використовую.

-Xms256m
-Xmx512m
-XX:MaxPermSize=120m
-XX:+UseConcMarkSweepGC
-ea
-Dawt.useSystemAAFontSettings=lcd


Для покращення швидкодії роботи, можна також установити значення ms = mx, так щоб

-Xms512m
-Xmx512m
-XX:MaxPermSize=120m
-XX:+UseConcMarkSweepGC
-ea
-Dawt.useSystemAAFontSettings=lcd


Ресурси


SoftReference & WeakReference: time of life

Well, I found some free time to write few articles. This one is about the SoftReference class from Java library.
SoftReference will help you to get the best variant between using memory and performance. Soft reference means that the value wrapped in soft reference will be garbage collected only and only if there are no free memory.
More about this. First I set -verbose:gc parameter for Java machine. If this parameter is set than GC logs will be output to the console. Also was set parameter -Xmx32m. The simple code:

void test() {
Reference<List> listRef = new SoftReference<List>(
Arrays.asList("1", "2", "3", "4", "5"));

List list = new LinkedList();
for (int i = 1; i <= 5; i++) {
list.add(new Object());

if (i == 5) {
if (listRef.get() == null) {
break;
}
i = 1;
}
}
}

Here the soft reference to the long string was created. The loop adds the references to the object to the linked list. Sometimes it checks whether the soft reference to the list of strings is not gc'ed; if it is than loop is breaked. Here is the output of GC:

[GC 896K->708K(5056K), 0.0059887 secs]
[GC 1604K->1603K(5056K), 0.0064902 secs]
[GC 2499K->2498K(5056K), 0.0064698 secs]
[GC 3394K->3393K(5056K), 0.0062198 secs]
[GC 4289K->4287K(5184K), 0.0061248 secs]
[Full GC 4287K->4287K(5184K), 0.0378679 secs]
[GC 5120K->5119K(8000K), 0.0141557 secs]
[GC 6015K->6014K(8000K), 0.0069241 secs]
[GC 6910K->6908K(8000K), 0.0064464 secs]
[GC 7804K->7804K(8768K), 0.0061396 secs]
[Full GC 7804K->7804K(8768K), 0.0595140 secs]

We may see that more than one Full GC were run before the soft reference was cleaned up.
The same test for the WeakReference (change new SoftReference(...) into new WeakReference(...)) prints next output:

[GC 896K->707K(5056K), 0.0063061 secs]

So, weak reference is cleaned up after the next first GC runs.
These tests were made on Java 1.6.0_10.
The outputs for Java 1.5.0_07 are respectively

[GC 512K->387K(1984K), 0.0047419 secs]
[GC 899K->898K(1984K), 0.0046456 secs]
[GC 1410K->1409K(1984K), 0.0048894 secs]
[GC 1921K->1920K(2496K), 0.0046743 secs]
[Full GC 1920K->1920K(2496K), 0.0203305 secs]
[GC 2431K->2430K(3776K), 0.0103943 secs]
[GC 2942K->2941K(3776K), 0.0048556 secs]
[GC 3453K->3452K(4032K), 0.0047855 secs]
[Full GC 3452K->3452K(4032K), 0.0340532 secs]
[GC 3964K->3963K(6332K), 0.0044411 secs]
[GC 4475K->4474K(6332K), 0.0049126 secs]
[GC 4986K->4985K(6332K), 0.0046232 secs]
[GC 5497K->5496K(6332K), 0.0045682 secs]
[GC 6008K->6007K(6588K), 0.0047892 secs]
[Full GC 6007K->6007K(6588K), 0.0523812 secs]
[GC 6775K->6774K(10848K), 0.0061404 secs]
[GC 7542K->7540K(10848K), 0.0065743 secs]
[GC 8308K->8307K(10848K), 0.0067296 secs]
[GC 9075K->9074K(10848K), 0.0066386 secs]
[GC 9842K->9841K(10848K), 0.0066439 secs]
[GC 10609K->10608K(11488K), 0.0066955 secs]
[Full GC 10608K->10606K(11488K), 0.1112694 secs]

and

[GC 512K->387K(1984K), 0.0049869 secs]


Since, these results show that Java 6 works with memory better than Java 5 and cleans up the soft reference faster than it is in Java 5.

Кешування другого рівня в Hibernate.

Частина II: Бібліотеки кешування для Hibernate

Кеш другого рівня в Hibernate може бути підключеним і може бути в межах процесу або кластера. Можуть бути різні реалізації кешу другого рівня, існують кілька вже готових реалізацій на основі готових движків кешування. Але можна реалізувати власний движок і підключити його за допомогою реалізації інтерфейсу org.hibernate.cache.CacheProvider.

Визначають наступні провайдери кешу другого рівня

- EHCache. Підтримка кешування другого рівня в межах одного процесу однієї віртуальної машини. Може виконувати зберігання кешу як в оперативній памяті, так і на диск. Підтримує кешування запитів.

- OSCache (OpenSymphony Cache). Підтримка кешування другого рівня в межах одного процесу однієї віртуальної машини. Підтримує кешування запитів.

- SwarmCache. Підтримка кешування другого рівня в межах кластера. Не підтримує кешування запитів.

- JBossCache. Підтримка кешування другого рівня в межах кластера. Підтримує кешування запитів.

Про підтримку стратегій кешування різними провайдерами можна дізнатися із наступної таблиці:

Провайдер

Read-only

Read-write

Nonstrict-read-write

Transactional

EHCache

+

+

+

OSCache

+

+

+

SwarmCache

+

+

JBossCache

+

+

Підтримка кешування другого рівня в Hibernate

Кешування другого рівня в Hibernate потрібно увімкнути, для того, щоб його використовувати. Увімкнути кешування другого рівня можна встановши property в конфігурації сесії Hibernate:

<property name="cache.use_second_level_cache">true</property>

Якщо необхідно увімкнути також кешування для запитів, то слід також встановити значення для property в конфігурації сесії Hibernate:

<property name="hibernate.cache.use_query_cache">true</property>

Також необхідно вказати клас провайдера кешування, а для цього треба задати наступний property в конфігурації сесії Hibernate :

- для провайдера EHCache

<property name="cache.provider_class">org.hibernate.cache.EhCacheProvider</property>

- для провайдера OSCache

<property name="cache.provider_class">org.hibernate.cache.OSCacheProvider</property>

- для провайдера SwarmCache

<property name="cache.provider_class">org.hibernate.cache.SwarmCacheProvider</property>

Але для того, щоб кешування другого рівня було в дії, слід позначити хоча б один клас для кешування. Позначити певний клас для кешування можна двома способами:

  1. В описі маппінгу класу у файлі *.hbm.xml добавити елемент cache із відповідним значенням.

<class name=”org.prisoft.sample.hibernate.Country” table=”Country” mutable="false">

<cache usage="read-only"/>

</class>

Вище описаний клас Country, який мапиться на таблицю Country. Ця таблиця є словником країн. Словник зміні не підлягає, тобто значення не добавляється, не редагується та не видаляється. <cache> вказує на те, що дані підлягають кешуванню, стратегія кешування read-only.

  1. У файлі конфігурації сесії hibernate.cfg.xml. Для цього використовується елемент <class-cache>, який наводиться після списку ресурсів. Ось як може описуватися кешування для класу Country:

<hibernate-configuration>

<session-factory>

….

<mapping resource=”org/prisoft/sample/hibernate/Country.hbm.xml”/>

….

<class-cache class=”org.prisoft.sample.hibernate.Country”

usage=”read-only”/>

</session-factory>

</hibernate-configuration>

Я надаю перевагу конфігурації кешування другого рівня через файл hibernate.cfg.xml, оскільки так видно повний список класів, що підлягають кешуванню, а також при необхідності, можна легко відключити кешування.

Отже, ми розглянули, як включити кешування другого рівня, а також як вказати які саме ентіті підлягають кешуванню. Ми не розглянули, як задається кешування для запитів.

Також ми не розглянули таке поняття, як регіони. Регіони – це так би мовити іменовані екземпляри кешу, в якому знаходяться різні класи із різними політиками часу на існування. По замовчуванню, ім’я регіону складає повна назва класу (якщо це кеш класу) або повна назва класу плюс імя property класу (якщо це колекція). За допомогою property сесії Hibernate hibernate.cache.region_prefix. Тоді ім’я регіону буде складатися із префіксу та повного імені класу. Вказати ім’я регіону можна явно за допомогою атрибута region в елементі <cache> або <class-cache>.

EHCache (http://ehcache.sourceforge.net)

Останнє обновлення аплікації це є ehcache-1.4-beta, яка вийшла 1 січня 2008. А це означає, що продукт не тільки живий, але і розвивається. Розповсюджується EhCache за ліцензією The Apache Software License, Version 2.0. Спонсори там також далеко не з послідніх. Так що, проект живе і розвивається. Не може не радувати повна підтримка JSR107 (JCache API).

Серед вказаних характеристик слід відзначити:

- Простота (API є досить простим, початкова конфігурація також не є необхідною).

- Залежність тільки від commons-logging та commons-collections. Вони в свою чергу також поширюються за APL. Та й більшість проектів їх використовує. То ж, ця залежність не є проблематичною.

- Кешування в оперативній пам’яті та на диск. Підтримка великих розмірів.

- Підтримка великої кількості кешів.

- Підтримка розширень для кешу (а також розширення для загружчиків та обробників виключень).

- А також багато чого іншого, про що ви можете точніше дізнатися за адресом http://ehcache.sourceforge.net/features.html

Як уже зазначено, можна і не конфігурувати EHCache. Але я не думаю, що це хороша ідея. Нам вона явно не підходить. Ми повинні навчитися конфігурувати EhCache.

Отже, конфігурація EHCache знаходиться у файлі ehcache.xml, який у свою чергу знаходиться в класспасі аплікації.

Отже, структура конфігураційного файла:

<ehcache>

<diskStore …/>

<defaultCache …/>

<cache name=”...” …/>

</ehcache>

Конфігураційний файл може містити не тільки ці елементи, але й інші. Але вони розглядатися не будуть.

Роглянемо детальніше ці елементи.

<diskStore path=”java.io.tmpdir”/> - вказує шлях до каталогу, де файли кешу створюються.

Наступні значення є можливими:

- user.homeдомашній каталог користувача;

- user.dirтекучий робочий каталог користувача;

- java.io.tmpdir – каталог тимчасових файлів.

Можна також вказувати субдиректорії, наприклад <diskStore path=”java.io.tmpdir/cache/myapp”/>

Конфігурація кешу, який не описаний за допомогою власного <cache> елемента, відбувається через <defaultCache>.

<defaultCache

maxElementsInMemory=”10000”

eternal=”false”

timeToIdleSeconds=”120”

timeToLiveSeconds=”120”

overflowToDisk=”true”

diskSpoolBufferSizeMB=”30”

maxElementsOnDisk=”1000000”

diskPersistent=”false”

diskExpiryThreadInternalSeconds=”120”

memoryStoreEvictionPolicy=”LRU”/>

Можна також задавати конфігурацію для окремого кешу; а для цього використовується наступний елемент

<сache name=”CacheName”

maxElementsInMemory=”10000”

eternal=”false”

timeToIdleSeconds=”120”

timeToLiveSeconds=”120”

overflowToDisk=”true”

maxElementsOnDisk=”1000000”

diskPersistent=”false”

diskExpiryThreadInternalSeconds=”120”

memoryStoreEvictionPolicy=”LRU”/>

А тепер детальніше про кожен атрибут.

mameназва кешу; назва, як ви вже, мабуть, зрозуміли має бути унікальна.

maxElementsInMemory – максимальна кількість об’єктів, які будуть створені та зберігатися в кеші в оперативній пам’яті.

maxElementsOnDisk – максимальна кількість об’єктів, які будуть збережені на диск.

eternalвказує на те, чи є елементи вічними. Якщо вони є вічними, то будь-які настройки Idle та Live Seconds ігноруються, елементи зберігаються до кінця.

timeToIdleSeconds час життя невикористаного елемента кешу (за умови, якщо eternal рівний false). Якщо значення 0, то це значить, що елемент може залишатися в кеші в стані не використовуваного нескінченний період.

timeToLiveSeconds – час життя елемента кешу (за умови, якщо eternal рівний false). Якщо значення 0, то це значить, що елемент може залишатися в кеші в стані невикористаного нескінченний період.

overflowToDisk – вказує на те, чи зберігати елементи на диск, якщо досягнуто максимуму елементів в пам’яті.

diskPersistent - чи зберігаються закешовані дані між пере запуском віртуальної машини. Значення по-замовчуванню false.

diskExpiryThreadInternalSeconds – кількість секунд, через які відбувається запуск потоку очищення диску від елементів кешу, що завершили свій час життя.

memoryStoreEvectionPolicy. Розрізняються наступні політики LRULeast Recently Used, FIFO – First In First Out, LFU Last Frequently Used.

Приклад аплікації із використанням кешу EHCache

Отже, розглянуто трохи теорії, а тепер ближче до практики. Для цього було створено просту аплікацію, яка містить 4 доменних об’єкта: Country, Materials, Product, ProductMaterial.

Отже, ми маємо аплікацію, яка містить інформацію про матеріали (назва, код, ціна, країна виробник), про продукцію (назва, код, ціна, країна виробник, матеріали із яких виготовлена продукція) і власне словник країн виробників.

Кешування класів та стратегії кешування описані у файлі hibernate.cfg.xml.

Отже, hibernate.cfg.xml має вигляд:

<hibernate-configuration>

<session-factory>

<property name="hibernate.connection.url">jdbc:mysql://localhost/proregister</property>

<property name="hibernate.connection.driver_class">com.mysql.jdbc.Driver</property>

<property name="hibernate.connection.username">root</property>

<property name="hibernate.connection.password"/>

<property name="hibernate.show_sql">false</property>

<mapping resource="org/prisoft/sample/proregister/objects/Country.hbm.xml"/>

<mapping resource="org/prisoft/sample/proregister/objects/Material.hbm.xml"/>

<mapping resource="org/prisoft/sample/proregister/objects/Product.hbm.xml"/>

<mapping resource="org/prisoft/sample/proregister/objects/ProductMaterial.hbm.xml"/>

<class-cache class="org.prisoft.sample.proregister.objects.Country" usage="read-only"/>

<class-cache class="org.prisoft.sample.proregister.objects.Material" usage="read-write"/>

<class-cache class="org.prisoft.sample.proregister.objects.Product" usage="read-write"/>

<class-cache class="org.prisoft.sample.proregister.objects.ProductMaterial" usage="read-write"/>

</session-factory>

</hibernate-configuration>

Клас Country має стратегію кешування read-only, в той час як всім іншим задана стратегія read-write. Country є по факту незмінним словником країн; його не можна змінювати.

Кеши класів Material, Product та ProductMaterial повинні підтримувати зміну, оскільки елементи можуть як добавлятися, так і редагуватися та видалятися. Саме тому їм задана стратегія read-write.

Аплікація працюватиме із базою даних на одній машині, тому виконувати кешування на диск немає потреби. Окрім того, кількість даних є невеликою, тому їх можна спокійно зберігати в базу даних.

Для детальної конфігурації EHCache потрібно конфігурувати кожен кеш у файлі ehcache.xml.

Отже, конфігурація має наступний вигляд:

<ehcache>

<diskStore path="java.io.tmpdir"/>

<defaultCache

maxElementsInMemory="10000"

eternal="false"

timeToIdleSeconds="120"

timeToLiveSeconds="120"

overflowToDisk="false"

diskPersistent="false"

diskExpiryThreadIntervalSeconds="120"

memoryStoreEvictionPolicy="LRU"/>

[тут має бути конфігурація кешів, розглянута дальше]

</ehcache>

Отже, ми вказали на каталог для файлів кешування та конфігурацію кешу по-замовчуванню.

Нижче подана конфігурація кешу для класу Country. Задаємо максимальну кількість елементів в пам’яті, забороняється записувати дані на диск, дані можуть зберігатися тривалий час. Якщо кількість елементів в пам’яті перевищує 200, то тоді відбувається очистка кешу.

<cache name="org.prisoft.sample.proregister.objects.Country "

maxElementsInMemory="200"

eternal="true"

overflowToDisk="false"/>

Настройка кешу для об’єктів Product, Material та ProductMaterial має відмінності, оскільки ці об’єкти можуть змінюватися, тому для них задана конфігурація тривалості існування елементів кешу. Конфігурація подана нижче.

<cache name=”org.prisoft.sample.proregister.Product”

maxElemenentsInMemory=”2000”

eternal=”false”

overflowToDisk=”false”

timeToIdleSeconds=”120”

timeToLiveSeconds=”300”/>

<cache name=”org.prisoft.sample.proregister.Material”

maxElemenentsInMemory=”5000”

eternal=”false”

overflowToDisk=”false”

timeToIdleSeconds=”240”

timeToLiveSeconds=”300”/>

<cache name=”org.prisoft.sample.proregister.ProductMaterial”

maxElemenentsInMemory=”10000”

eternal=”false”

overflowToDisk=”false”

timeToIdleSeconds=”120”

timeToLiveSeconds=”120”/>

Як бачите, для різних кешів задана різна кількість елементів в памяті. Атрибут eternal має значення false, тому конфігурація періоду існування елементів у кеші не буде ігноруватися. Дані не зберігаються на диск для жодного із кешів, використовуваних в аплікації.

Отже, ми розглянули які існують провайдери кешу другого рівня для Hibernate, також ми розглянули, як настроїти Hibernate для використання кешу другого рівня для класів (проте не зачепили теми кешування запитів та колекцій). Розглянуто було також більш детально провайдер EHCache.

Додаткова література

1. Hibernate In Action, Manning Publications, 2005

2. EhCache User Guide at http://ehcache.sourceforge.net/EhcacheUserGuide.html