SlideShare a Scribd company logo
Кеширование данных вне Java Heap
и работа с разделяемой памятью в Java



Андрей Паньгин
ведущий разработчик
проекта Одноклассники
Содержание


1.   О кешировании в Java
2.   Работа с памятью вне Java Heap
3.   Использование разделяемой памяти в Java
4.   Пример алгоритма кеширования




                                               1
Что кешировать?
• Результаты вычислений
• Данные из медленного хранилища (БД)
                 Numbers everyone should know
      L1 cache reference                             0.5 ns
      Main memory reference                          100 ns
      Compress 1K bytes w/ cheap algorithm         3,000 ns
      Send 2K bytes over 1 Gbps network           20,000 ns
      Read 1 MB sequentially from memory         250,000 ns
      Round trip within same datacenter          500,000 ns
      Read 1 MB sequentially from network     10,000,000 ns
      Read 1 MB sequentially from disk        30,000,000 ns
      Send packet CA->Netherlands->CA        150,000,000 ns

                                                              2
Где кешировать?


• В оперативной памяти
  – Java Heap
  – Off-heap memory
• На диске или флеш-накопителе




                                 3
Решения для кеширования в Java

• Apache Java Caching System
 http://commons.apache.org/jcs/

• Terracota Ehcache
 http://ehcache.org/

• JBoss Cache
 http://www.jboss.org/jbosscache/




                                    4
Стандартизация

• JSR 107: Java Temporary Caching API
  – javax.cache
  – Войдет в Java EE 7


• Реализации
  – Terracota Ehcache
  – Oracle Coherence




                                        5
Содержание


1.   О кешировании в Java
2.   Работа с памятью вне Java Heap
3.   Использование разделяемой памяти в Java
4.   Пример алгоритма кеширования




                                               6
Off-heap

• Почему вне Java Heap?
  – Большие объемы
  – Не оказывает влияния на GC


• Как?
  –   Native код
  –   Direct ByteBuffer
  –   Memory-mapped files
  –   Unsafe


                                 7
Native

• JNI или JNA обертки
• malloc / free
• Платформозависимый код
JNIEXPORT jlong JNICALL
Java_org_test_allocateMemory(JNIEnv* env, jclass cls, jlong size) {
    return (jlong) (intptr_t) malloc((size_t) size);
}

JNIEXPORT void JNICALL
Java_org_test_freeMemory(JNIEnv* env, jclass cls, jlong addr) {
    free((void*) (intptr_t) addr);
}


                                                                  8
Direct ByteBuffer

• Выделение
  – ByteBuffer buf = ByteBuffer.allocateDirect(size);
• Освобождение
  – Автоматически: GC
  – Вручную: ((sun.nio.ch.DirectBuffer) buf).cleaner().clean();
• Размер буфера ≤ 2 GB
• Ограничение на общий объем Direct буферов
  – -XX:MaxDirectMemorySize=



                                                                  9
Memory-mapped file

• FileChannel.map()
• Использование и освобождение
  – Аналогично Direct ByteBuffer
• Размер буфера ≤ 2 GB
• Подходит для персистентных кешей
 RandomAccessFile f =
     new RandomAccessFile("/tmp/cache", "rw");
 ByteBuffer buf = f.getChannel().
     map(FileChannel.MapMode.READ_WRITE, 0, f.length());




                                                       10
Unsafe

• Получение экземпляра sun.misc.Unsafe
  – (Unsafe) getField(Unsafe.class, "theUnsafe").get(null);
• Выделение / освободжение
  – unsafe.allocateMemory(), unsafe.freeMemory()
• Использование
  – unsafe.putByte(), putInt(), putLong() …
  – unsafe.getByte(), getInt(), getLong() …
  – unsafe.copyMemory()
• Нет ограничений
• Зависит от JVM, но есть почти везде

                                                              11
Cache persistence

• Решает проблему «холодного» старта

• Кеш в памяти?
  – Нужны снимки (snapshots)


• Загрузка снимков может занимать время
  – Читать с диска лучше последовательно




                                           12
Snapshots

• Должны быть целостными

• Способы создания снимков
  –   «Stop-the-world» snapshot
  –   Разбивка на сегменты
  –   Memory-mapped files: MappedByteBuffer.force()
  –   Shared memory objects
  –   Copy-on-write



                                                      13
Fork trick

• Метод создания снимков в Tarantool

• fork() создает копию процесса
  –   Практически мгновенно
  –   Страницы памяти помечаются copy-on-write
  –   Родительский процесс продолжает обслуживание
  –   Дочерний процесс делает снимок


• Применимо в POSIX-совместимых ОС

                                                     14
Содержание


1.   О кешировании в Java
2.   Работа с памятью вне Java Heap
3.   Использование разделяемой памяти в Java
4.   Пример алгоритма кеширования




                                               15
Shared Memory

• Механизм IPC
  – POSIX: shm_open + mmap
  – Windows: CreateFileMapping + MapViewOfFile
  – Скорость доступа к оперативной памяти


• Linux
  – /dev/shm
  – shm_open("name", ...) ↔ open("/dev/shm/name", ...)
  – Можно работать как с обычными файлами


                                                         16
Shared Memory в Java/Linux

• Создание / открытие объекта Shared Memory
  RandomAccessFile f =
      new RandomAccessFile("/dev/shm/cache", "rw");
  f.setLength(1024 * 1024 * 1024L);


• read() / write() работает, но медленно
  – в 50 раз медленнее прямого доступа к памяти

• Предпочтительней отобразить разделяемую
  память в адресное пространство процесса

                                                      17
Mapping: легальный способ

• Java NIO API
  – FileChannel.map()


• MappedByteBuffer
• Ограничение ≤ 2 GB




                            18
Mapping: хитрый способ

• Private Oracle API
  – sun.nio.ch.FileChannelImpl
  – Методы map0, unmap0


• Адрес в виде long
• Нет ограничения в 2 GB
• Работает как в Linux, так и в Windows




                                          19
Mapping: пример

// Mapping
Method map0 = FileChannelImpl.class.getDeclaredMethod(
    "map0", int.class, long.class, long.class);
map0.setAccessible(true);
long addr = (Long) map0.invoke(f.getChannel(), 1, 0L, f.length());



// Unmapping
Method unmap0 = FileChannelImpl.class.getDeclaredMethod(
    "unmap0", long.class, long.class);
unmap0.setAccessible(true);
unmap0.invoke(null, addr, length);




                                                                20
Проблема абсолютных адресов

• Только относительная адресация
  – Хранение смещений вместо адресов


• mmap() с фиксированным базовым адресом
  – Возможно в ОС, но не поддерживается в Java


• Relocation
  – Сдвиг всех абсолютных адресов на старте



                                                 21
Malloc

• Распределение памяти в непрерывной области
• Doug Lea's Malloc, tcmalloc...


     16   24        32   48   …   256   384   …    1G




    sz         sz        sz        sz   sz        sz




                                                        22
ByteBuffer vs. Unsafe memory access

• Unsafe.getX, Unsafe.putX – JVM intrinsics
• ByteBuffer несет дополнительные проверки
  – Range check
  – Alignment check
  – Byte order check
• JNIEnv::GetDirectBufferAddress
  public static long getByteBufferAddress(ByteBuffer buffer) {
      Field f = Buffer.class.getDeclaredField("address");
      f.setAccessible(true);
      return f.getLong(buffer);
  }


                                                                 23
Содержание


1.   О кешировании в Java
2.   Работа с памятью вне Java Heap
3.   Использование разделяемой памяти в Java
4.   Пример алгоритма кеширования




                                               24
Постановка задачи

• Задача
  – Кеширование изображений для Download сервера
• Характеристики
  – 2 x Intel Xeon E5620
  – 64 GB RAM
• Нагрузка
  – 70 тыс. запросов в секунду
  – 3 Gbps исходящий трафик



                                               25
Требования

• Требования к системе кеширования
  –   Ключ – 64-bit long, значение – байтовый массив
  –   In-process, in-memory
  –   Эффективное использование RAM (~64 GB)
  –   FIFO или LRU
  –   100+ одновременных потоков
  –   Персистентность
  –   Cache HIT > 90%




                                                       26
Способ реализации

• Непрерывная область памяти с собственным
  аллокатором
• FIFO
• sun.misc.Unsafe
• Shared Memory (/dev/shm)
• Сегментирование ключей по хеш-коду
• Блокировка сегмента через ReadWriteLock



                                         27
Сегменты

• Корзины хеш-таблицы
  – Segment s = segments[key % segments.length];
  – Одинаковый размер (~1 MB)
  – До 50 тыс. сегментов


• Синхронизация через ReadWriteLock
  – Проблема в JDK6: Bug 6625723
  – Альтернатива: Semaphore



                                                   28
Структура сегментов




                      29
Индекс




• Ключи отсортированы
  – бинарный поиск
• Ключи сосредоточены в одной области
  – размещение индекса в кеше процессора



                                           30
Алгоритм GET

1. hash(key) → сегмент
2. Бинарный поиск key в индексе сегмента
3. key найден → offset(value) + length(value)




                                                31
Алгоритм PUT

1. hash(key) → сегмент
2. Сегмент → адрес следующего блока данных
3. Адрес следующего блока += length(value)
4. Линейный поиск по массиву ссылок
   для удаления ключей, чьи данные будут
   перезаписаны
5. Копирование value в область данных
6. Вставка key в индекс


                                         32
Сравнение производительности

• Условия
  – Linux JDK 7u4 64-bit
  – 1 млн. операций
  – 0 – 8 KB values




                               33
Спасибо!

• Примеры
  – https://github.com/odnoklassniki/one-nio
• Статья
  – http://habrahabr.ru/company/odnoklassniki/blog/148139/


• Контакты
  – andrey.pangin@odnoklassniki.ru
• Работа в Одноклассниках
  – http://v.ok.ru



                                                             34

More Related Content

PPTX
Developing highload servers with Java
PDF
Java tricks for high-load server programming
PDF
Выжимаем из сервера максимум (Андрей Паньгин)
PDF
Незаурядная Java как инструмент разработки высоконагруженного сервера
PDF
Распределенные системы в Одноклассниках
PPTX
MySQL 5.7 - NoSQL - JSON, Protocol X, Document Store / Петр Зайцев (Percona)
PDF
Мониторинг ожиданий в PostgreSQL / Курбангалиев Ильдус (Postgres Professional)
PPTX
Оптимизация работы с данными в мобильных приложениях / Святослав Иванов, Артё...
Developing highload servers with Java
Java tricks for high-load server programming
Выжимаем из сервера максимум (Андрей Паньгин)
Незаурядная Java как инструмент разработки высоконагруженного сервера
Распределенные системы в Одноклассниках
MySQL 5.7 - NoSQL - JSON, Protocol X, Document Store / Петр Зайцев (Percona)
Мониторинг ожиданий в PostgreSQL / Курбангалиев Ильдус (Postgres Professional)
Оптимизация работы с данными в мобильных приложениях / Святослав Иванов, Артё...

What's hot (20)

PDF
Практика совместного использования Lua и C в opensource спам-фильтре Rspamd /...
PPTX
Multithreading in java past and actual
PPTX
Mysql vs postgresql
PDF
Современная операционная система: что надо знать разработчику / Александр Кри...
PPTX
Жизнь проекта на production советы по эксплуатации / Николай Сивко (okmeter.io)
PDF
"Почему язык Lua — это интересно?", Ник Заварицкий, (Mail.ru Group)
PDF
nginx.CHANGES.2015 / Игорь Сысоев, Валентин Бартенев (Nginx)
PPTX
Java threads - part 2
PPTX
Java 8. Thread pools
PDF
Реализация восстановления после аварий / Сергей Бурладян (Avito)
PDF
Очереди и блокировки. Теория и практика / Александр Календарев (ad1.ru)
PDF
PPT
Проект «Одноклассники» Mail.Ru Group, Андрей Паньгин
PPTX
Евгений Потапов (Сумма Айти)
PDF
Производительность запросов в PostgreSQL - шаг за шагом / Илья Космодемьянски...
PDF
Денис Иванов
PDF
noBackend, или Как выжить в эпоху толстеющих клиентов / Самохвалов Николай
PDF
Владимир Алаев "Разработка на Node.js: инструменты, библиотеки, сервисы"
PDF
Анатомия веб-сервиса (РИТ-2014)
PPTX
Java threads - part 3
Практика совместного использования Lua и C в opensource спам-фильтре Rspamd /...
Multithreading in java past and actual
Mysql vs postgresql
Современная операционная система: что надо знать разработчику / Александр Кри...
Жизнь проекта на production советы по эксплуатации / Николай Сивко (okmeter.io)
"Почему язык Lua — это интересно?", Ник Заварицкий, (Mail.ru Group)
nginx.CHANGES.2015 / Игорь Сысоев, Валентин Бартенев (Nginx)
Java threads - part 2
Java 8. Thread pools
Реализация восстановления после аварий / Сергей Бурладян (Avito)
Очереди и блокировки. Теория и практика / Александр Календарев (ad1.ru)
Проект «Одноклассники» Mail.Ru Group, Андрей Паньгин
Евгений Потапов (Сумма Айти)
Производительность запросов в PostgreSQL - шаг за шагом / Илья Космодемьянски...
Денис Иванов
noBackend, или Как выжить в эпоху толстеющих клиентов / Самохвалов Николай
Владимир Алаев "Разработка на Node.js: инструменты, библиотеки, сервисы"
Анатомия веб-сервиса (РИТ-2014)
Java threads - part 3
Ad

Similar to Caching data outside Java Heap and using Shared Memory in Java (20)

PDF
андрей паньгин
PPTX
Как мы храним и анализируем большой социальный граф, Максим Бартенев (Норси-т...
PPTX
Hosting for forbes.ru_
PDF
Вячеслав Бирюков - Как Linux работает с памятью
PDF
Егор Львовский — «Кеширование на клиенте и сервере»
PDF
ekbpy'2012 - Данила Штань - Распределенное хранилище
PDF
CodeFest 2013. Иванов В. — Уменьшение расхода оперативной памяти в Java-прило...
PDF
TMPA-2013 Sartakov: Genode
PDF
Как Linux работает с памятью — Вячеслав Бирюков
PPT
An internal look at HotSpot JVM
PPTX
Безопасность без антивирусов 4
PDF
Оптимизация программ для современных процессоров и Linux, Александр Крижановс...
PDF
Ускоряем и разгружаем веб-сервер, прозрачно кэшируя на SSD, Станислав Николов...
PDF
Java Platform Performance BoF
PDF
Multithreading in JS. Myth or reality?
PDF
Лекция 4: Оптимизация доступа к памяти (Memory access optimization, caches)
PDF
20100307 virtualization igotti_lecture04
PPTX
Спасение 6 миллионов файлов в условиях полного Хецнера
PPT
Cache in web (Secon 2008)
PPT
Smirnov Memcached Highload 2008
андрей паньгин
Как мы храним и анализируем большой социальный граф, Максим Бартенев (Норси-т...
Hosting for forbes.ru_
Вячеслав Бирюков - Как Linux работает с памятью
Егор Львовский — «Кеширование на клиенте и сервере»
ekbpy'2012 - Данила Штань - Распределенное хранилище
CodeFest 2013. Иванов В. — Уменьшение расхода оперативной памяти в Java-прило...
TMPA-2013 Sartakov: Genode
Как Linux работает с памятью — Вячеслав Бирюков
An internal look at HotSpot JVM
Безопасность без антивирусов 4
Оптимизация программ для современных процессоров и Linux, Александр Крижановс...
Ускоряем и разгружаем веб-сервер, прозрачно кэшируя на SSD, Станислав Николов...
Java Platform Performance BoF
Multithreading in JS. Myth or reality?
Лекция 4: Оптимизация доступа к памяти (Memory access optimization, caches)
20100307 virtualization igotti_lecture04
Спасение 6 миллионов файлов в условиях полного Хецнера
Cache in web (Secon 2008)
Smirnov Memcached Highload 2008
Ad

Caching data outside Java Heap and using Shared Memory in Java

  • 1. Кеширование данных вне Java Heap и работа с разделяемой памятью в Java Андрей Паньгин ведущий разработчик проекта Одноклассники
  • 2. Содержание 1. О кешировании в Java 2. Работа с памятью вне Java Heap 3. Использование разделяемой памяти в Java 4. Пример алгоритма кеширования 1
  • 3. Что кешировать? • Результаты вычислений • Данные из медленного хранилища (БД) Numbers everyone should know L1 cache reference 0.5 ns Main memory reference 100 ns Compress 1K bytes w/ cheap algorithm 3,000 ns Send 2K bytes over 1 Gbps network 20,000 ns Read 1 MB sequentially from memory 250,000 ns Round trip within same datacenter 500,000 ns Read 1 MB sequentially from network 10,000,000 ns Read 1 MB sequentially from disk 30,000,000 ns Send packet CA->Netherlands->CA 150,000,000 ns 2
  • 4. Где кешировать? • В оперативной памяти – Java Heap – Off-heap memory • На диске или флеш-накопителе 3
  • 5. Решения для кеширования в Java • Apache Java Caching System http://commons.apache.org/jcs/ • Terracota Ehcache http://ehcache.org/ • JBoss Cache http://www.jboss.org/jbosscache/ 4
  • 6. Стандартизация • JSR 107: Java Temporary Caching API – javax.cache – Войдет в Java EE 7 • Реализации – Terracota Ehcache – Oracle Coherence 5
  • 7. Содержание 1. О кешировании в Java 2. Работа с памятью вне Java Heap 3. Использование разделяемой памяти в Java 4. Пример алгоритма кеширования 6
  • 8. Off-heap • Почему вне Java Heap? – Большие объемы – Не оказывает влияния на GC • Как? – Native код – Direct ByteBuffer – Memory-mapped files – Unsafe 7
  • 9. Native • JNI или JNA обертки • malloc / free • Платформозависимый код JNIEXPORT jlong JNICALL Java_org_test_allocateMemory(JNIEnv* env, jclass cls, jlong size) { return (jlong) (intptr_t) malloc((size_t) size); } JNIEXPORT void JNICALL Java_org_test_freeMemory(JNIEnv* env, jclass cls, jlong addr) { free((void*) (intptr_t) addr); } 8
  • 10. Direct ByteBuffer • Выделение – ByteBuffer buf = ByteBuffer.allocateDirect(size); • Освобождение – Автоматически: GC – Вручную: ((sun.nio.ch.DirectBuffer) buf).cleaner().clean(); • Размер буфера ≤ 2 GB • Ограничение на общий объем Direct буферов – -XX:MaxDirectMemorySize= 9
  • 11. Memory-mapped file • FileChannel.map() • Использование и освобождение – Аналогично Direct ByteBuffer • Размер буфера ≤ 2 GB • Подходит для персистентных кешей RandomAccessFile f = new RandomAccessFile("/tmp/cache", "rw"); ByteBuffer buf = f.getChannel(). map(FileChannel.MapMode.READ_WRITE, 0, f.length()); 10
  • 12. Unsafe • Получение экземпляра sun.misc.Unsafe – (Unsafe) getField(Unsafe.class, "theUnsafe").get(null); • Выделение / освободжение – unsafe.allocateMemory(), unsafe.freeMemory() • Использование – unsafe.putByte(), putInt(), putLong() … – unsafe.getByte(), getInt(), getLong() … – unsafe.copyMemory() • Нет ограничений • Зависит от JVM, но есть почти везде 11
  • 13. Cache persistence • Решает проблему «холодного» старта • Кеш в памяти? – Нужны снимки (snapshots) • Загрузка снимков может занимать время – Читать с диска лучше последовательно 12
  • 14. Snapshots • Должны быть целостными • Способы создания снимков – «Stop-the-world» snapshot – Разбивка на сегменты – Memory-mapped files: MappedByteBuffer.force() – Shared memory objects – Copy-on-write 13
  • 15. Fork trick • Метод создания снимков в Tarantool • fork() создает копию процесса – Практически мгновенно – Страницы памяти помечаются copy-on-write – Родительский процесс продолжает обслуживание – Дочерний процесс делает снимок • Применимо в POSIX-совместимых ОС 14
  • 16. Содержание 1. О кешировании в Java 2. Работа с памятью вне Java Heap 3. Использование разделяемой памяти в Java 4. Пример алгоритма кеширования 15
  • 17. Shared Memory • Механизм IPC – POSIX: shm_open + mmap – Windows: CreateFileMapping + MapViewOfFile – Скорость доступа к оперативной памяти • Linux – /dev/shm – shm_open("name", ...) ↔ open("/dev/shm/name", ...) – Можно работать как с обычными файлами 16
  • 18. Shared Memory в Java/Linux • Создание / открытие объекта Shared Memory RandomAccessFile f = new RandomAccessFile("/dev/shm/cache", "rw"); f.setLength(1024 * 1024 * 1024L); • read() / write() работает, но медленно – в 50 раз медленнее прямого доступа к памяти • Предпочтительней отобразить разделяемую память в адресное пространство процесса 17
  • 19. Mapping: легальный способ • Java NIO API – FileChannel.map() • MappedByteBuffer • Ограничение ≤ 2 GB 18
  • 20. Mapping: хитрый способ • Private Oracle API – sun.nio.ch.FileChannelImpl – Методы map0, unmap0 • Адрес в виде long • Нет ограничения в 2 GB • Работает как в Linux, так и в Windows 19
  • 21. Mapping: пример // Mapping Method map0 = FileChannelImpl.class.getDeclaredMethod( "map0", int.class, long.class, long.class); map0.setAccessible(true); long addr = (Long) map0.invoke(f.getChannel(), 1, 0L, f.length()); // Unmapping Method unmap0 = FileChannelImpl.class.getDeclaredMethod( "unmap0", long.class, long.class); unmap0.setAccessible(true); unmap0.invoke(null, addr, length); 20
  • 22. Проблема абсолютных адресов • Только относительная адресация – Хранение смещений вместо адресов • mmap() с фиксированным базовым адресом – Возможно в ОС, но не поддерживается в Java • Relocation – Сдвиг всех абсолютных адресов на старте 21
  • 23. Malloc • Распределение памяти в непрерывной области • Doug Lea's Malloc, tcmalloc... 16 24 32 48 … 256 384 … 1G sz sz sz sz sz sz 22
  • 24. ByteBuffer vs. Unsafe memory access • Unsafe.getX, Unsafe.putX – JVM intrinsics • ByteBuffer несет дополнительные проверки – Range check – Alignment check – Byte order check • JNIEnv::GetDirectBufferAddress public static long getByteBufferAddress(ByteBuffer buffer) { Field f = Buffer.class.getDeclaredField("address"); f.setAccessible(true); return f.getLong(buffer); } 23
  • 25. Содержание 1. О кешировании в Java 2. Работа с памятью вне Java Heap 3. Использование разделяемой памяти в Java 4. Пример алгоритма кеширования 24
  • 26. Постановка задачи • Задача – Кеширование изображений для Download сервера • Характеристики – 2 x Intel Xeon E5620 – 64 GB RAM • Нагрузка – 70 тыс. запросов в секунду – 3 Gbps исходящий трафик 25
  • 27. Требования • Требования к системе кеширования – Ключ – 64-bit long, значение – байтовый массив – In-process, in-memory – Эффективное использование RAM (~64 GB) – FIFO или LRU – 100+ одновременных потоков – Персистентность – Cache HIT > 90% 26
  • 28. Способ реализации • Непрерывная область памяти с собственным аллокатором • FIFO • sun.misc.Unsafe • Shared Memory (/dev/shm) • Сегментирование ключей по хеш-коду • Блокировка сегмента через ReadWriteLock 27
  • 29. Сегменты • Корзины хеш-таблицы – Segment s = segments[key % segments.length]; – Одинаковый размер (~1 MB) – До 50 тыс. сегментов • Синхронизация через ReadWriteLock – Проблема в JDK6: Bug 6625723 – Альтернатива: Semaphore 28
  • 31. Индекс • Ключи отсортированы – бинарный поиск • Ключи сосредоточены в одной области – размещение индекса в кеше процессора 30
  • 32. Алгоритм GET 1. hash(key) → сегмент 2. Бинарный поиск key в индексе сегмента 3. key найден → offset(value) + length(value) 31
  • 33. Алгоритм PUT 1. hash(key) → сегмент 2. Сегмент → адрес следующего блока данных 3. Адрес следующего блока += length(value) 4. Линейный поиск по массиву ссылок для удаления ключей, чьи данные будут перезаписаны 5. Копирование value в область данных 6. Вставка key в индекс 32
  • 34. Сравнение производительности • Условия – Linux JDK 7u4 64-bit – 1 млн. операций – 0 – 8 KB values 33
  • 35. Спасибо! • Примеры – https://github.com/odnoklassniki/one-nio • Статья – http://habrahabr.ru/company/odnoklassniki/blog/148139/ • Контакты – andrey.pangin@odnoklassniki.ru • Работа в Одноклассниках – http://v.ok.ru 34