company_banner

Использование разделяемой памяти в Java и off-heap кеширование

    На прошлой неделе состоялся успешный эксперимент по запуску нового решения для download-сервиса. Один достаточно скромный сервер (2 x Intel Xeon E5620, 64 GB RAM) под управлением Java-приложения собственной разработки принял на себя нагрузку восьми Tomcat'ов, обслуживая более 70 тысяч HTTP-запросов в секунду общей пропускной способностью 3000 Mb/s. Таким образом, весь трафик Одноклассников, связанный с пользовательскими смайликами, обрабатывался одним сервером.

    Вполне естественно, что высокие нагрузки требовали нестандартных решений. В цикле статей о разработке высоконагруженного сервера на Java я расскажу о проблемах, с которыми нам пришлось столкнуться, и о том, как мы их преодолели. Сегодня речь пойдет о кешировании изображений вне Java Heap и об использовании Shared Memory в Java.


    Кеширование


    Поскольку тянуть изображения на каждый запрос из хранилища — не вариант, а о хранении картинок на диске не может быть и речи (дисковая очередь станет узким местом сервера гораздо раньше), необходимо иметь быстрый кеш в памяти приложения.

    Требования к кешу предъявлялись следующие:
    • 64-bit keys, byte array values: идентификатор изображения — целое число типа long, а данные — картинка в формате PNG, GIF или JPG со средним размером 4 KB;
    • In-process, in-memory: для максимальной скорости доступа все данные — в памяти процесса;
    • RAM utilization: под кеш выделяется вся доступная оперативная память;
    • Off-heap: 50 GB данных разместить в Java Heap было бы проблематично;
    • LRU или FIFO: устаревшие ключи могут вытесняться более новыми;
    • Concurrency: одновременное использование кеша в сотне потоков;
    • Persistence: приложение может быть перезапущено с сохранением уже закешированных данных.

    Самый быстрый способ обращения к памяти вне Heap из Java – через класс sun.misc.Unsafe, т.к. его методы getLong/putLong являются JVM intrinsics, то есть, их вызовы заменяются JIT-компилятором буквально в одну машинную инструкцию. Персистентность кеша между запусками приложения достигается использованием memory-mapped файлов. Однако связывать кеш с реальным файлом на диске крайне не хотелось (производительность сильно пострадала бы за счет обращений к диску), поэтому в адресное пространство приложения отображается не реальный файл, а объект разделяемой памяти. В этом случае, конечно, кеш уже не будет энергонезависимым, но, главное, позволит перезапускать приложение без потери данных.

    Shared Memory


    В Linux объекты Shared Memory реализованы посредством специальной файловой системы, монтируемой к /dev/shm. Так, например, POSIX функция shm_open("name", ...) эквивалентна системному вызову open("/dev/shm/name", ...). Таким образом, в Java мы можем работать с разделяемой памятью Linux как с обычными файлами. Следующий фрагмент кода откроет объект разделяемой памяти с именем image-cache размером 1 GB. Если объекта с таким именем не существует, будет создан новый. Важно, что после завершения приложения объект останется в памяти и будет доступен при следующем запуске.

        RandomAccessFile f = new RandomAccessFile("/dev/shm/image-cache", "rw");
        f.setLength(1024*1024*1024L);
    

    Теперь созданный объект-файл надо отобразить в адресное пространство процесса и получить адрес этого участка памяти.

    Способ 1. Легальный, но неполноценный


    Воспользуемся Java NIO API:

        RandomAccessFile f = ...
        MappedByteBuffer buffer =
            f.getChannel().map(FileChannel.MapMode.READ_WRITE, 0, f.length());
    

    Самый главный недостаток этого метода заключается в том, что нельзя отображать файлы размером более 2 GB, что и описано в Javadoc к методу map: The size of the region to be mapped; must be non-negative and no greater than Integer.MAX_VALUE.

    Работать с полученным участком памяти можно либо стандартными методами ByteBuffer'а, либо напрямую через Unsafe, вытащив адрес памяти с помощью Reflection:

        public static long getByteBufferAddress(ByteBuffer buffer) throws Exception {
            Field f = Buffer.class.getDeclaredField("address");
            f.setAccessible(true);
            return f.getLong(buffer);
        }
    

    Публично доступного метода unmap у такого MappedByteBuffer'а нет, однако есть полу-легальный способ освободить память без вызова GC:

        ((sun.nio.ch.DirectBuffer) buffer).cleaner().clean(); 
    


    Способ 2. Полностью на Java, но с использованием «тайных знаний»


    В Oracle JDK есть класс sun.nio.ch.FileChannelImpl с приватными методами map0 и unmap0, которые лишены ограничения в 2 GB. map0 возвращает непосредственно адрес «замапленного» участка, что для нас даже удобнее, если мы используем Unsafe.

       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());
    
       Method unmap0 = FileChannelImpl.class.getDeclaredMethod(
           "unmap0", long.class, long.class);
       unmap0.setAccessible(true);
       unmap0.invoke(null, addr, length);
    

    Такой механизм будет работать как в Linux, так и под Windows. Единственный его недостаток — отсутствие возможности выбора конкретного адреса, куда будет «замаплен» файл. Необходимость в этом может возникнуть, если в кеше присутствуют абсолютные ссылки на адреса памяти внутри этого же кеша: такие ссылки станут невалидными, если отобразить файл по другому адресу. Выхода два: либо хранить относительные ссылки в виде смещения относительно начала файла, либо прибегнуть к вызову нативного кода через JNI или JNA. Системные вызовы mmap в Linux и MapViewOfFileEx в Windows позволяют задать предпочитаемый адрес, куда «замапить» файл.

    Алгоритм кеширования


    Ключевым для производительности кеша, да и download-сервера в целом, является алгоритм поиска в кеше, т.е. метод get. Метод put в нашем сценарии вызывается значительно реже, но тоже не должен быть слишком медленным. Хочу представить наше решение для быстрого потокобезопасного FIFO кеша в непрерывной области памяти фиксированного размера.

    Вся память разделяется на сегменты одинакового размера — корзины хеш-таблицы, по которым равномерно распределяются ключи. В самом простом виде

        Segment s = segments[key % segments.length];
    



    Сегментов может быть много — несколько тысяч. Каждому из них сопоставляется ReadWriteLock. Одновременно с сегментом может работать либо неограниченное количество читателей, либо только один писатель.

    Интересная деталь: использование стандартных ReentrantReadWriteLock'ов привело к потере 2 GB в Java Heap. Как оказалось, в JDK 6 существует ошибка, приводящая к чрезмерному потреблению памяти таблицами ThreadLocal в реализации ReentrantReadWriteLock. Хотя в JDK 7 ошибка уже исправлена, в нашем случае мы заменили прожорливый Lock на Semaphore. Кстати, вот вам и маленькое упражнение:
    как реализовать ReadWriteLock при помощи Semaphore?

    Итак, сегмент. Он состоит из области индекса и области данных. Индекс представляет собой упорядоченный массив из 256 ключей, сразу за которым идет такой же длины массив из 256 ссылок на значения. Ссылка задает смещение внутри сегмента на начало блока данных и длину этого блока в байтах.



    Блоки данных, то есть, собственно сами изображения, выравнены по восьмибайтовой границе для оптимального копирования. Сегмент также хранит количество ключей в нем и адрес следующего блока данных для метода put. Новые блоки записываются друг за другом по принципу циклического буфера. Как только место в сегменте кончается, происходит запись с начала сегмента поверх более ранних данных.

    Алгоритм метода get чрезвычайно быстр и прост:
    1. по хешу ключа вычисляется сегмент, в котором будет производиться поиск;
    2. в области индекса бинарным поиском ищется ключ;
    3. если ключ найден, из массива ссылок достается смещение, по которому располагаются данные.

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

    Метод put тоже несложен:
    1. по хешу ключа вычисляется сегмент;
    2. считывается адрес очередного блока данных и вычисляется адрес следующего блока путём прибавления размера записываемого объекта с учетом выравнивания;
    3. если сегмент заполнен, линейным поиском по массиву ссылок находятся и удаляются из индекса ключи, чьи данные будут перезаписаны очередным блоком;
    4. значение, представленное байтовым массивом, копируется в область данных;
    5. бинарным поиском находится место в индексе, куда вставляется новый ключ.


    Скорость работы


    Разумеется, помимо нашего, существует ряд других решений для кеширования данных за пределами Java Heap, как бесплатных, так и платных. Из наиболее известных – Terracota Ehcache (с in-memory off-heap хранилищем) и Apache Java Caching System. Именно с ними мы и сравнивали собственный алгоритм. Эксперименты проводились на Linux JDK 7u4 64-bit и состояли из трех сценариев:
    • put: запись 1 млн. значений размером от 0 до 8 KB каждое;
    • get: поиск по ключу 1 млн. значений;
    • 90% get + 10% put: комбинирование get/put в отношении, приближенном к практическому сценарию использования кеша.

    Результаты замеров приведены в таблице. Как видно, и Ehcache, и JCS в разы уступают по производительности описанному алгоритму.



    Впрочем, стоит отметить, что описанный алгоритм, будучи предназначенным для решения задачи кеширования изображений, не охватывает многих других сценариев. Например, операции remove и replace, хотя и могут быть легко реализованы, не будут освобождать память, занятую прежними значениями.

    Где посмотреть?


    Исходные тексты алгоритма кеширования с использованием Shared Memory на github:
    https://github.com/odnoklassniki/shared-memory-cache

    Где послушать?


    На встрече JUG.RU в Санкт-Петербурге, которая состоится 25 июля 2012 г., apangin поделится опытом разработки высоконагруженного сервера на Java, расскажет о характерных проблемах и нетрадиционных приемах.

    Что дальше?


    В следующих статьях я расскажу, как написать RPC-сервер, обрабатывающий десятки тысяч запросов в секунду, а также поведаю об альтернативном методе сериализации, в разы превосходящем стандартные механизмы Java по производительности и объему трафика. Оставайтесь с нами!
    Одноклассники
    106,00
    Делимся экспертизой
    Поделиться публикацией

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

      +1
      64ГБ памяти позволяют легко закешировать картинки средствами ОС при использовании sendfile. Почему такой вариант не подошел, он медленнее?
        +1
        Картинок гораздо больше 64 GB, и хранятся они не на локальном диске, а в распределенном сетевом хранилище.
          0
          > Таким образом, весь трафик Одноклассников, связанный с пользовательскими смайликами, обрабатывался одним сервером.

          У вас 64GB смайликов? оО
            +1
            Около 500. Пользователи ежедневно по 1 GB новых добавляют.
              +1
              А можно пример? Я тогда вообще не понимаю что такое «пользовательские смайлики».
              Для меня смайлики это
            0
            Всего смайликов c подарками ~1.5 TB (500 GB x 3 replication factor)
        0
        А наружу торчит HTTP REST API? Если да — вы могли бы на таком же железе настроить nginx + memcached и оценить его производительность?
          0
          Скорее всего мемкеш будет самым медленным решением. Медленее ФС кеша, который медленее in-process кеша.
            0
            Nginx была идея использовать, но потребовалось бы дописать клиентский модуль для похода в хранилище, ведь все свое ПО разрабатываем на Java. А memcached даже не рассматривался, т.к. out-of-process и будет заведомо медленнее.
              0
              Latency возрастёт, но, возможно, при этом машина сможет обрабатывать больше запросов в секунду. Вы сейчас во что нибудь упираетесь или просто больше нагрузки сейчас нету?
                +1
                У memcached нет причин обрабатывать больше запросов:
                — аллокация у него медленнее (SLAB vs. pointer increment);
                — поиск медленнее (hash + linear search vs. hash + binary search);
                — копирование данных медленнее (socket i/o vs. memcpy).

                Сейчас ни во что не упираемся — во время эксперимента весь трафик смайликов и подарочков прокачивался через один сервер. CPU при этом был загружен на 50%.
                0
                То есть вы пришли к выводу, что горлышко бутылки — это запросы к [memcached|...], а не NFS?
                  0
                  Причем здесь NFS?
                  Узкие места бывают разного рода и устраняются разными способами: проблема с latency похода в хранилище решается размером локального кеша, нагрузка на CPU — быстрым алгоритмом кеширования и эффективным HTTP-сервером.
              0
              в статье нет ошибки?
              3000 Mb/s это точно 3 тысячи мегабайт в секунду?
                +5
                  0
                  нет. Это 3000 мегабит. когда мегабайты обычно пишут MB/s ( B — большое)
                    0
                    оу, спасибо
                  0
                  А sun.misc.Unsafe#allocateMemory и sun.misc.Unsafe#freeMemory не подошли бы? Вроде как принимают на вход long и возвращают адрес.
                    +4
                    Подошли бы. Только один важный момент — при рестарте приложения необходимо не потерять закешированные данные 55GB.
                    0
                    Зачем на каждый сегмент по локу?
                    Сколько потоков одноврменно пишут и читают?
                      0
                      Concurrency: одновременное использование кеша в сотне потоков;
                      10% из потоков пишут.
                        0
                        а почему не сделать 256 локов, распределить их на все сегменты?
                          0
                          На локи памяти не жалко, даже если сегментов 50000. Все же меньше вероятность, что попадут случайно несколько особо популярных картинок под один лок.
                          0
                          а почему не сделать 128 или 256 или N локов, распределить их на все сегменты?
                            +4
                            какая цель и в чем экономия?
                            сейчас работает, много не ест и нет проблем.
                          0
                          В первой версии был единый лок на весь кеш — и он тут же стал узким местом — пришлось сегментировать. Всего порядка 100 потоков.
                          +1
                          А сколько в сумме все картинки занимают (я понял, что больше 64Г — но насколько)?
                            +1
                            Всего смайликов c подарками ~1.5 TB (500 GB x 3 replication factor)
                              0
                              В таком случае интересно, если их все положить на диск (ну и докладывать по мере поступления новых, конечно) и отдавать nginx-ом через sendfile как статику, насколько это будет медленнее?
                                0
                                Насколько медленнее — не замерял, но прежде всего с количеством запросов не справится. Сейчас смотрю на один из наших blob-storage серверов с 4мя дисками — он обрабатывает в пике около 2 тыс. запросов в секунду. При этом IO Utilization составляет 20%. Т.е. при 10 тыс. запросов сервер загнется на дисковом I/O. Наш же download обрабатывает 70 тыс. запросов, и при этом остается еще приличный запас.
                                  0
                                  Может, blob storage часто мимо дискового кэша промахивается? А в картинках-подарках, насколько я понял из статьи, вероятность кэш-попадания значительно выше. Если это так, то эксперимент-сравнение должно быть провести легко — достаточно поставит nginx на точно такую же машина, настроить его, а потом забомбить запросами к одному и тому же статическому файлу: если даст 70К rps, значит, все то же самое (чем забомбить с такой интенсивностью только вопрос).
                                0
                                всё что выше дискового кэша менее 60-150 картинок в секунду в идеальном случае помноженное на x 3 replication factor
                            0
                            Никогда супер нагруженными системами не занимался, всегда хватало стандартных средств. Но вещь интересная, плюс заметил у вас вакансию java developer — пошел готовить резюме. :)
                              0
                              Наверное глупый вопрос, но все таки, я правильно понимаю, что на реализациях JVM отличных от Oracle/Sun это не будет работать?
                                0
                                Да, метод sun.nio.ch.FileChannelImpl.map0 специфичен для Oracle JDK.
                                sun.misc.Unsafe тоже не является частью публичного API, но поддерживается и другими JVM.
                                0
                                Молодцы!
                                Вы рушите мою светлую веру в превосходство nginx над любым велосипедом! Язычники!!!
                                  0
                                  Хорошее решение — чувствуется серьезный подход к делу. А комментарии к топику завистливые — всякий норовит докопаться к чему-нибудь.
                                    0
                                    А с доступом к файлу в памяти какое решение в итоге использовали — первый способ (который легальный, но полноценный) или второй (с использованием «тайных знаний»)?
                                      0
                                      Второй, т.к. надо мапить огромный файл одним куском.
                                      0
                                      Попробуйте уйти от традиционных файловых операций и посмотреть в сторону ZFS, которая умеет кэшировать часто востребованные данные в памяти, используя собственные алгоритмы кэширования и управления памятью в зависимости от потребностей приложений. Ожидается, что тандем «ZFS + Java приложение» будет использовать оперативную память гораздо эффективнее, чем традиционные механизмы ручного управления разделяемой памятью.
                                        0
                                        Любая файловая система делает это (кеширование в зависимости от потребностей приложений). Не могли бы вы пояснить, почему вы ожидаете, что ZFS тут будет лучше?
                                          0
                                          Потому что ZFS очень агрессивно кэширует запрашиваемые данные, в отличие от традиционных файловых систем. При этом память под кэш выделяется динамически, при этом автоматически занимается почти вся доступная память. Запускаемые приложения, естественно, тоже требуют своё, и ZFS в этом случае оперативно освобождает кэш — память, когда ZFS кэш разогрет, всегда занята на 100% под кэш и приложения, конечно, если не выставлены ограничения (через sysctl-переменные управления).

                                          При использовании традиционных файловых систем на сервере всегда остаётся какой-либо незанятый кусок памяти. Его приходится утилизировать всякими способами — это даётся очень трудоёмко и сопряжено с малопроизводительным ручным трудом прикладных программистов. Зачем делать одно и то же по-новому на прикладном уровне, если автоматическое управление всей доступной памятью уже реализовано на уровне ZFS и операционной системы? Попробуйте и сравните оба подхода.
                                        0
                                        А вы можете к Apache Java Caching System патч прислать, чтобы эта система не хуже вашего подхода работала? Или патча не нужно, нужно просто правильно настроить?
                                          0
                                          У Apache JCS принципиально другой подход к кешированию — патчем там не обойтись.
                                          Исходники нашей реализации открыты — если под условия задачи подходит, можно брать и пользоваться.
                                          0
                                          2 вопроса:
                                          — как бывшего СХД-шника интересует, почему не сгодились внешние решения типа CAS?
                                          — хочу заниматься крутыми штуками, типа описанной, можно с вами? :)
                                            0
                                            ну не совсем ясно зачем тут content addressable storage. Все картинки у нас адресуются в хранилище однозначно по цифровому ИД. К тому же сомнительно что кто то из CAS сможет работать с такой скоростью — просто из-за более сложного и ненужного для данной задачи алгоритма.
                                            Или вы предлагаете вместо blob storage использовать какой то CAS solution?

                                            Ну а если хочется заниматься крутыми штуками — используйте e-mail job@odnoklassniki.ru по назначению ;-)
                                              0
                                              мне вспомнился CAS по той причине, что он решает те же самые задачи:
                                              — адаптирован к частным get/нечастым put
                                              — производительный get по ид
                                              — имеют большой собственный кеш и перечисленные требования по LRU, FIFO, Concurrency и Persistence решаются cas-коробкой

                                              впрочем, отвечая на собственный вопрос, могу предположить, что стоимость цас-решения могла быть существенно выше предложенного.

                                              и спасибо за адрес :)

                                            +1
                                            Я вот не эксперт в низкоуровневых делах, а выясняли ли вы — обеспечивает ли архитектура x86 синхронизацию доступа к памяти? Я имею в виду следущее: вот вы засинхронизировали доступ потоков записи и чтения через самодельный ReadWriteLock, а реально ли после записи поток чтения получит консистентные данные в случае многоядерного процессора? То есть Java Memory Model это гарантирует, но ведь тут вы обращаетесь к памяти уже напрямую.

                                            Реализовать ReadWriteLock через Semaphore видимо можно создав семафор со счетчиком равным числу потоков в пуле (N). При этом каждый поток чтения будет запрашивать по единичке, а поток записи — сразу N единиц. Так?
                                              0
                                              Все так. С той лишь оговоркой, что семафор должен быть справедливым (fair), дабы избежать зависания (starvation) потока-писателя.

                                              Примитивы синхронизации в Java, в том числе ReentrantReadWriteLock и Semaphore, при захвате и освобожении обеспечивают полный memory barrier, в частности, все записи памяти в одном потоке (неважно, чем они сделаны, записью в поле объекта или через Unsafe) будут видны в другом потоке.
                                              0
                                              А вот вы упомянули 8-мь экземпляров Tomcat. Получается у вас кэш все равно находится в другом процессе (и в другой JVM) и вы все равно должны будете воспользоваться каким-либо механизмом IPC?

                                              В таком случае единственное что отличает от MemCached например, то что вся умная логика по инвалидации и обработке спрятана в сам процесс кэша? Т.е. у меня сомнение — будет ли какой-либо выигрыш вашего подхода относительно MemCached например при измерении производительности в комплексе, а именно с учетом IPC вызовов?

                                              Или я не понял насчет 8-ми томкэтов чего-либо?
                                                0
                                                Не так. Раньше smile download сервис обслуживался 8-ю серверами с Tomcat. На время эксперимента весь трафик вместо этих серверов был направлен на один единственный сервер, про который идет речь в статье, и он выдержал с хорошим запасом. HTTP-сервер, blob storage клиент и кеш работают в одном Java-процессе.
                                                +1
                                                Итак много букв:
                                                1. Вы так активно бросились оптимизировать под процессор, что стало интересно: как ваши оптимизации сочетаются с тем что у вас всего 16 железных обработчиков потоков (без HT 8 штук)? Просто вы упомянули что у вас 100 потоков, и любопытно понять как они вмещаются в 16 исполнителей? Переключения вас не страшат или много потоков в ожидании висят?

                                                2. Можно предположить что есть четыре базовых стратегии для ReadWriteLock: справедливая (по времени прихода) без разделения на read/write, приоритет на read, приоритет на write, случайная. Судя по вашим заявлениям у вас обычная справедливая стратегия. Почему так? Вы проверяли другие варианты? Может быть приоритет на read повысит производительность?

                                                3. Судя по всему у вас у каждой картинки есть уникальный числовой key. Вы его генератором фигачите? Просто меня смутила немного функция определения сегмента. Она или слишком простая, или вы для упрощения не стали приводить полную функцию. К примеру можно придти к выводу что возможно у вас перекос по нагрузке на сегменты, а часть сегментов занимает память и не используется (не исключаю что на ваших объемах таких проблем нет, но вдруг). У вас накапливается статистика по оптимальности функции определения сегмента по ключу?

                                                4. Еще был вопрос почему вы не используете inmemory базы данных? Было бы довольно любопытно сравнение их с вашим решением. Например почему бы не взять какое нибудь nosql решение? Также из-за предвзятости разработчиков решения, я бы доверил настройку других решений для сравнения, другим людям (которые специалисты в них). Надеюсь вы поступили именно так, а то так эти цифры только для улучшения самолюбия ваших инженеров.

                                                В целом спасибо за обзор. Было любопытно читать.
                                                  +2
                                                  1. Потоки ждут в epoll_wait(). Некоторые висят на запросе в хранилище. С трафиком справилось бы и меньшее число потоков, но так мы уменьшаем максимальное время отклика. И 100 потоков — это не 10 тысяч, накладные расходы на переключение незаметны.

                                                  2. Справедливая, чтобы не могла возникнуть ситуация «голодания» записывающего потока. Сравнивали производительность со случайной стратегией — никакой разницы.

                                                  3. Генератор простой, и хеш простой. Про любой хеш-функции будут перекосы из-за того, что одни картинки более популярны, другие менее. Но это нестрашно: время полного обновления сегментов составляет несколько часов. Память в кеше используется в среднем на 99.4%.

                                                  4. Очень даже используем, но здесь не та ситуация. Напомню одно из требований к кешу — in-proccess. Сравнить с Memcached или Tarantool/Box можно, но смысл? См. habrahabr.ru/company/odnoklassniki/blog/148139/#comment_5000546
                                                    0
                                                    1. Кстати, а у вас есть данные сколько времени в среднем отрабатывает запрос на получение данных из хранилища? И среднее время обработки запроса при попадании в кэш? Просто мне довольно интересна тема оптимального выбора количества потоков. Может быть вы посоветуете почитать что-нибудь на эту тему?

                                                    2. Возможно для оптимизации можно реализовать свою стратегию ReadWriteLock с группировкой писателей в кэш (с некоторым таймаутом для писателей в кэш). Например при накоплении 5-10 писателей запрещать write и отрабатывать пакетную запись в кэш. При этом можно сэкономить на числе перестановок (переупорядочивания) в для ключей.

                                                    4. Согласен, вам не особо требуются дополнительные плюшки внешних хранилищ. К примеру, в очень крайнем случае вы кэш пересоберете заново. Распределенность кэширования скорее всего ухудшит производительность.
                                                      0
                                                      Запрос к хранилищу — около 1 мс. Обращение в локальный кеш — 2 мкс. Число потоков выбирали экспериментально: от 64 до 128 разницы почти не было, при >200 начала снижаться максимальная пропускная способность, при <32 росло максимальное время отклика.
                                                  0
                                                  Интересная статья.
                                                  Вопрос: какая стратегия все-таки используется для замещения картинок при наполнении сегмента — FIFO или LRU? И как механизм реализован? Вот эта фраза не очень понятна:
                                                  если сегмент заполнен, линейным поиском по массиву ссылок находятся и удаляются из индекса ключи, чьи данные будут перезаписаны очередным блоком;

                                                  наверное подразумевается все-таки FIFO с указателем на текущую позицию для вставки, который при достижении конца сегмента сбрасывается на начало?
                                                  И если это FIFO, то не логичнее ли реализовать LRU, который позволил бы держать в кэше наиболее востребованные картинки?
                                                    +1
                                                    Используется FIFO по принципу циклического буфера. Полное обновление сегмента происходит в течение 8-12 часов, т.е. время жизни объекта в кеше составляет не менее 8 часов. При таких показателях FIFO полностью устраивает, в то время как честная реализация LRU значительно усложнила бы логику распределения памяти.

                                                    Вот эта фраза не очень понятна:
                                                    если сегмент заполнен, линейным поиском по массиву ссылок находятся и удаляются из индекса ключи, чьи данные будут перезаписаны очередным блоком
                                                    Старые данные (картинки) вытеснять из кеша не надо, они и так перезаписываются поверх. Но нужно удалять их id из индекса. Для этого пробегаем по порядку весь индекс, удаляя те ключи, чьи значения попадают в перезаписываемую область.
                                                    0
                                                    Может я чего-то не понимаю, но у меня вызывает сомнение обоснованность главного требования о том, что кеш должен быть in-process.

                                                    Вы пишете, что нужно обрабатывать 70 тыс. запросов в секунду, 3000 Мбит/с, но это ведь совсем не много. Только что поставил простой эксперимент на своем десктопе — померял пропускную способность http-сервера nxweb на файле размером 10Кбайт. Получилось 340 тыс. запросов в секунду и почти 3.5 Гбайт/с. Запас более чем десятикратный. Причем это далеко не два Xeon'а, а всего лишь i7-2600 (3.4 ГГц).

                                                    Другое дело, что Java в принципе медленнее C/C++. Банальный «hello, world» через Tomcat на моем же десктопе дает лишь 30-50 тыс. запросов в секунду. А если она еще к memcache начнет обращаться, то будет совсем нехорошо.
                                                      0
                                                      померял пропускную способность http-сервера nxweb на файле размером 10Кбайт

                                                      Что этот пример показывает? Где здесь out-of-process?
                                                        0
                                                        Ну, командная строка (точнее утилита измерения числа запросов в секунду) — она по отношению к http-серверу, как бы, не in-process, но работает достаточно быстро.

                                                        Показывает, что вызов чего-то быстрого out-of-process не должен стать узким местом при указанной частоте запросов и пропускной способности. Сокет на том же хосте работает достаточно быстро.
                                                          0
                                                          Утилита в данном случае — генератор пользовательских запросов. Чтобы тест был релевантным, нужна еще одна прослойка. Согласитесь, если точки A и B обмениваются данными со скоростью 340 тыс. rps, а B и C тоже со скоростью 340 rps, то вовсе не факт, что A и C через B тоже будут обмениваться с такой же скоростью. Про memcached уже писал.
                                                            0
                                                            Согласен, конечно. Смысл в другом. Мой тест показывает, что издержки на out-of-process могут составлять (на моем компьютере) лишь порядка 3 мкс. Понятно, что это может оказаться не совсем так на практике, надо проверять, но я веду к другому. Не стоит принимать за аксиому то, что кеш обязан быть in-process, чтобы справиться с указанной нагрузкой.
                                                              0
                                                              А в целом, спасибо за интересную статью. Давно пишу на Java, но мне бы и в голову не пришло решать на ней такие задачи. Вы расширили мой кругозор, за что премного благодарен.
                                                        0
                                                        А еще ведь можно запустить какой-нибудь Varnish в режиме malloc (чтобы кешировал в ОЗУ), а позади него поставить tomcat, который будет в случае непопадания в кеш подтягивать недостающий контент из хранилища. Не улучшит ли это результат? Причем значительно, так как большую часть запросов будет отрабать C-код, а не Java.
                                                          0
                                                          Мы, значит, делаем все возможное, чтобы по-максимуму использовать Java-решения, а вы нам тут C настоятельно советуете?.. Шучу, ничего не имею против. За Varnish спасибо. Впрочем, любое стандартное решение в чистом виде нам вряд ли подойдет, так как в любом случае нужна некоторая портало-специфичная логика, например, фильтрация по HTTP-заголовкам, проверка security-токенов в запросах и т.п. Malloc в чистом виде опять же не подойдет — т.к. при рестарте все теряется.

                                                          На самом деле, медленность Java по сравнению с C сильно преувеличивается. И, в частности, этой статьей я хотел показать, что на Java можно писать высокопроизводительные приложения, не уступающие программам на C.
                                                            0
                                                            Так как я достаточно давно пишу не только на Java, но и на C, могу сказать по личному опыту, что разница в скорости обычно измеряется коэффициентом от 2 до 10. Наверное что-то можно отыграть с помощью более эффективных алгоритмов, но если переложить те же алгоритмы на C, все равно получится в несколько раз быстрее. Хотя, опять же, это обобщение, а занчит, со всемы вытекающими оговорками.

                                                            И меня обычно тоже не устраивают стандартные решения. Сам я Varnish так ни разу в деле и не попробовал. Но по идее он должен помочь.

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

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