Comments 68
64ГБ памяти позволяют легко закешировать картинки средствами ОС при использовании sendfile. Почему такой вариант не подошел, он медленнее?
А наружу торчит HTTP REST API? Если да — вы могли бы на таком же железе настроить nginx + memcached и оценить его производительность?
UFO just landed and posted this here
Nginx была идея использовать, но потребовалось бы дописать клиентский модуль для похода в хранилище, ведь все свое ПО разрабатываем на Java. А memcached даже не рассматривался, т.к. out-of-process и будет заведомо медленнее.
Latency возрастёт, но, возможно, при этом машина сможет обрабатывать больше запросов в секунду. Вы сейчас во что нибудь упираетесь или просто больше нагрузки сейчас нету?
У memcached нет причин обрабатывать больше запросов:
— аллокация у него медленнее (SLAB vs. pointer increment);
— поиск медленнее (hash + linear search vs. hash + binary search);
— копирование данных медленнее (socket i/o vs. memcpy).
Сейчас ни во что не упираемся — во время эксперимента весь трафик смайликов и подарочков прокачивался через один сервер. CPU при этом был загружен на 50%.
— аллокация у него медленнее (SLAB vs. pointer increment);
— поиск медленнее (hash + linear search vs. hash + binary search);
— копирование данных медленнее (socket i/o vs. memcpy).
Сейчас ни во что не упираемся — во время эксперимента весь трафик смайликов и подарочков прокачивался через один сервер. CPU при этом был загружен на 50%.
То есть вы пришли к выводу, что горлышко бутылки — это запросы к [memcached|...], а не NFS?
в статье нет ошибки?
3000 Mb/s это точно 3 тысячи мегабайт в секунду?
3000 Mb/s это точно 3 тысячи мегабайт в секунду?
А sun.misc.Unsafe#allocateMemory и sun.misc.Unsafe#freeMemory не подошли бы? Вроде как принимают на вход long и возвращают адрес.
Зачем на каждый сегмент по локу?
Сколько потоков одноврменно пишут и читают?
Сколько потоков одноврменно пишут и читают?
Concurrency: одновременное использование кеша в сотне потоков;
10% из потоков пишут.
10% из потоков пишут.
а почему не сделать 256 локов, распределить их на все сегменты?
а почему не сделать 128 или 256 или N локов, распределить их на все сегменты?
В первой версии был единый лок на весь кеш — и он тут же стал узким местом — пришлось сегментировать. Всего порядка 100 потоков.
А сколько в сумме все картинки занимают (я понял, что больше 64Г — но насколько)?
Всего смайликов c подарками ~1.5 TB (500 GB x 3 replication factor)
В таком случае интересно, если их все положить на диск (ну и докладывать по мере поступления новых, конечно) и отдавать nginx-ом через sendfile как статику, насколько это будет медленнее?
Насколько медленнее — не замерял, но прежде всего с количеством запросов не справится. Сейчас смотрю на один из наших blob-storage серверов с 4мя дисками — он обрабатывает в пике около 2 тыс. запросов в секунду. При этом IO Utilization составляет 20%. Т.е. при 10 тыс. запросов сервер загнется на дисковом I/O. Наш же download обрабатывает 70 тыс. запросов, и при этом остается еще приличный запас.
Может, blob storage часто мимо дискового кэша промахивается? А в картинках-подарках, насколько я понял из статьи, вероятность кэш-попадания значительно выше. Если это так, то эксперимент-сравнение должно быть провести легко — достаточно поставит nginx на точно такую же машина, настроить его, а потом забомбить запросами к одному и тому же статическому файлу: если даст 70К rps, значит, все то же самое (чем забомбить с такой интенсивностью только вопрос).
А вот, кстати, и чем «забомбить» выложили:
roem.ru/2012/07/23/addednews51657/
roem.ru/2012/07/23/addednews51657/
всё что выше дискового кэша менее 60-150 картинок в секунду в идеальном случае помноженное на x 3 replication factor
Никогда супер нагруженными системами не занимался, всегда хватало стандартных средств. Но вещь интересная, плюс заметил у вас вакансию java developer — пошел готовить резюме. :)
Наверное глупый вопрос, но все таки, я правильно понимаю, что на реализациях JVM отличных от Oracle/Sun это не будет работать?
Молодцы!
Вы рушите мою светлую веру в превосходство nginx над любым велосипедом! Язычники!!!
Вы рушите мою светлую веру в превосходство nginx над любым велосипедом! Язычники!!!
UFO just landed and posted this here
А с доступом к файлу в памяти какое решение в итоге использовали — первый способ (который легальный, но полноценный) или второй (с использованием «тайных знаний»)?
Попробуйте уйти от традиционных файловых операций и посмотреть в сторону ZFS, которая умеет кэшировать часто востребованные данные в памяти, используя собственные алгоритмы кэширования и управления памятью в зависимости от потребностей приложений. Ожидается, что тандем «ZFS + Java приложение» будет использовать оперативную память гораздо эффективнее, чем традиционные механизмы ручного управления разделяемой памятью.
Любая файловая система делает это (кеширование в зависимости от потребностей приложений). Не могли бы вы пояснить, почему вы ожидаете, что ZFS тут будет лучше?
Потому что ZFS очень агрессивно кэширует запрашиваемые данные, в отличие от традиционных файловых систем. При этом память под кэш выделяется динамически, при этом автоматически занимается почти вся доступная память. Запускаемые приложения, естественно, тоже требуют своё, и ZFS в этом случае оперативно освобождает кэш — память, когда ZFS кэш разогрет, всегда занята на 100% под кэш и приложения, конечно, если не выставлены ограничения (через sysctl-переменные управления).
При использовании традиционных файловых систем на сервере всегда остаётся какой-либо незанятый кусок памяти. Его приходится утилизировать всякими способами — это даётся очень трудоёмко и сопряжено с малопроизводительным ручным трудом прикладных программистов. Зачем делать одно и то же по-новому на прикладном уровне, если автоматическое управление всей доступной памятью уже реализовано на уровне ZFS и операционной системы? Попробуйте и сравните оба подхода.
При использовании традиционных файловых систем на сервере всегда остаётся какой-либо незанятый кусок памяти. Его приходится утилизировать всякими способами — это даётся очень трудоёмко и сопряжено с малопроизводительным ручным трудом прикладных программистов. Зачем делать одно и то же по-новому на прикладном уровне, если автоматическое управление всей доступной памятью уже реализовано на уровне ZFS и операционной системы? Попробуйте и сравните оба подхода.
А вы можете к Apache Java Caching System патч прислать, чтобы эта система не хуже вашего подхода работала? Или патча не нужно, нужно просто правильно настроить?
ну не совсем ясно зачем тут content addressable storage. Все картинки у нас адресуются в хранилище однозначно по цифровому ИД. К тому же сомнительно что кто то из CAS сможет работать с такой скоростью — просто из-за более сложного и ненужного для данной задачи алгоритма.
Или вы предлагаете вместо blob storage использовать какой то CAS solution?
Ну а если хочется заниматься крутыми штуками — используйте e-mail job@odnoklassniki.ru по назначению ;-)
Или вы предлагаете вместо blob storage использовать какой то CAS solution?
Ну а если хочется заниматься крутыми штуками — используйте e-mail job@odnoklassniki.ru по назначению ;-)
мне вспомнился CAS по той причине, что он решает те же самые задачи:
— адаптирован к частным get/нечастым put
— производительный get по ид
— имеют большой собственный кеш и перечисленные требования по LRU, FIFO, Concurrency и Persistence решаются cas-коробкой
впрочем, отвечая на собственный вопрос, могу предположить, что стоимость цас-решения могла быть существенно выше предложенного.
и спасибо за адрес :)
— адаптирован к частным get/нечастым put
— производительный get по ид
— имеют большой собственный кеш и перечисленные требования по LRU, FIFO, Concurrency и Persistence решаются cas-коробкой
впрочем, отвечая на собственный вопрос, могу предположить, что стоимость цас-решения могла быть существенно выше предложенного.
и спасибо за адрес :)
Я вот не эксперт в низкоуровневых делах, а выясняли ли вы — обеспечивает ли архитектура x86 синхронизацию доступа к памяти? Я имею в виду следущее: вот вы засинхронизировали доступ потоков записи и чтения через самодельный ReadWriteLock, а реально ли после записи поток чтения получит консистентные данные в случае многоядерного процессора? То есть Java Memory Model это гарантирует, но ведь тут вы обращаетесь к памяти уже напрямую.
Реализовать ReadWriteLock через Semaphore видимо можно создав семафор со счетчиком равным числу потоков в пуле (N). При этом каждый поток чтения будет запрашивать по единичке, а поток записи — сразу N единиц. Так?
Реализовать ReadWriteLock через Semaphore видимо можно создав семафор со счетчиком равным числу потоков в пуле (N). При этом каждый поток чтения будет запрашивать по единичке, а поток записи — сразу N единиц. Так?
Все так. С той лишь оговоркой, что семафор должен быть справедливым (fair), дабы избежать зависания (starvation) потока-писателя.
Примитивы синхронизации в Java, в том числе ReentrantReadWriteLock и Semaphore, при захвате и освобожении обеспечивают полный memory barrier, в частности, все записи памяти в одном потоке (неважно, чем они сделаны, записью в поле объекта или через Unsafe) будут видны в другом потоке.
Примитивы синхронизации в Java, в том числе ReentrantReadWriteLock и Semaphore, при захвате и освобожении обеспечивают полный memory barrier, в частности, все записи памяти в одном потоке (неважно, чем они сделаны, записью в поле объекта или через Unsafe) будут видны в другом потоке.
А вот вы упомянули 8-мь экземпляров Tomcat. Получается у вас кэш все равно находится в другом процессе (и в другой JVM) и вы все равно должны будете воспользоваться каким-либо механизмом IPC?
В таком случае единственное что отличает от MemCached например, то что вся умная логика по инвалидации и обработке спрятана в сам процесс кэша? Т.е. у меня сомнение — будет ли какой-либо выигрыш вашего подхода относительно MemCached например при измерении производительности в комплексе, а именно с учетом IPC вызовов?
Или я не понял насчет 8-ми томкэтов чего-либо?
В таком случае единственное что отличает от MemCached например, то что вся умная логика по инвалидации и обработке спрятана в сам процесс кэша? Т.е. у меня сомнение — будет ли какой-либо выигрыш вашего подхода относительно MemCached например при измерении производительности в комплексе, а именно с учетом IPC вызовов?
Или я не понял насчет 8-ми томкэтов чего-либо?
Не так. Раньше smile download сервис обслуживался 8-ю серверами с Tomcat. На время эксперимента весь трафик вместо этих серверов был направлен на один единственный сервер, про который идет речь в статье, и он выдержал с хорошим запасом. HTTP-сервер, blob storage клиент и кеш работают в одном Java-процессе.
Итак много букв:
1. Вы так активно бросились оптимизировать под процессор, что стало интересно: как ваши оптимизации сочетаются с тем что у вас всего 16 железных обработчиков потоков (без HT 8 штук)? Просто вы упомянули что у вас 100 потоков, и любопытно понять как они вмещаются в 16 исполнителей? Переключения вас не страшат или много потоков в ожидании висят?
2. Можно предположить что есть четыре базовых стратегии для ReadWriteLock: справедливая (по времени прихода) без разделения на read/write, приоритет на read, приоритет на write, случайная. Судя по вашим заявлениям у вас обычная справедливая стратегия. Почему так? Вы проверяли другие варианты? Может быть приоритет на read повысит производительность?
3. Судя по всему у вас у каждой картинки есть уникальный числовой key. Вы его генератором фигачите? Просто меня смутила немного функция определения сегмента. Она или слишком простая, или вы для упрощения не стали приводить полную функцию. К примеру можно придти к выводу что возможно у вас перекос по нагрузке на сегменты, а часть сегментов занимает память и не используется (не исключаю что на ваших объемах таких проблем нет, но вдруг). У вас накапливается статистика по оптимальности функции определения сегмента по ключу?
4. Еще был вопрос почему вы не используете inmemory базы данных? Было бы довольно любопытно сравнение их с вашим решением. Например почему бы не взять какое нибудь nosql решение? Также из-за предвзятости разработчиков решения, я бы доверил настройку других решений для сравнения, другим людям (которые специалисты в них). Надеюсь вы поступили именно так, а то так эти цифры только для улучшения самолюбия ваших инженеров.
В целом спасибо за обзор. Было любопытно читать.
1. Вы так активно бросились оптимизировать под процессор, что стало интересно: как ваши оптимизации сочетаются с тем что у вас всего 16 железных обработчиков потоков (без HT 8 штук)? Просто вы упомянули что у вас 100 потоков, и любопытно понять как они вмещаются в 16 исполнителей? Переключения вас не страшат или много потоков в ожидании висят?
2. Можно предположить что есть четыре базовых стратегии для ReadWriteLock: справедливая (по времени прихода) без разделения на read/write, приоритет на read, приоритет на write, случайная. Судя по вашим заявлениям у вас обычная справедливая стратегия. Почему так? Вы проверяли другие варианты? Может быть приоритет на read повысит производительность?
3. Судя по всему у вас у каждой картинки есть уникальный числовой key. Вы его генератором фигачите? Просто меня смутила немного функция определения сегмента. Она или слишком простая, или вы для упрощения не стали приводить полную функцию. К примеру можно придти к выводу что возможно у вас перекос по нагрузке на сегменты, а часть сегментов занимает память и не используется (не исключаю что на ваших объемах таких проблем нет, но вдруг). У вас накапливается статистика по оптимальности функции определения сегмента по ключу?
4. Еще был вопрос почему вы не используете inmemory базы данных? Было бы довольно любопытно сравнение их с вашим решением. Например почему бы не взять какое нибудь nosql решение? Также из-за предвзятости разработчиков решения, я бы доверил настройку других решений для сравнения, другим людям (которые специалисты в них). Надеюсь вы поступили именно так, а то так эти цифры только для улучшения самолюбия ваших инженеров.
В целом спасибо за обзор. Было любопытно читать.
1. Потоки ждут в epoll_wait(). Некоторые висят на запросе в хранилище. С трафиком справилось бы и меньшее число потоков, но так мы уменьшаем максимальное время отклика. И 100 потоков — это не 10 тысяч, накладные расходы на переключение незаметны.
2. Справедливая, чтобы не могла возникнуть ситуация «голодания» записывающего потока. Сравнивали производительность со случайной стратегией — никакой разницы.
3. Генератор простой, и хеш простой. Про любой хеш-функции будут перекосы из-за того, что одни картинки более популярны, другие менее. Но это нестрашно: время полного обновления сегментов составляет несколько часов. Память в кеше используется в среднем на 99.4%.
4. Очень даже используем, но здесь не та ситуация. Напомню одно из требований к кешу — in-proccess. Сравнить с Memcached или Tarantool/Box можно, но смысл? См. habrahabr.ru/company/odnoklassniki/blog/148139/#comment_5000546
2. Справедливая, чтобы не могла возникнуть ситуация «голодания» записывающего потока. Сравнивали производительность со случайной стратегией — никакой разницы.
3. Генератор простой, и хеш простой. Про любой хеш-функции будут перекосы из-за того, что одни картинки более популярны, другие менее. Но это нестрашно: время полного обновления сегментов составляет несколько часов. Память в кеше используется в среднем на 99.4%.
4. Очень даже используем, но здесь не та ситуация. Напомню одно из требований к кешу — in-proccess. Сравнить с Memcached или Tarantool/Box можно, но смысл? См. habrahabr.ru/company/odnoklassniki/blog/148139/#comment_5000546
1. Кстати, а у вас есть данные сколько времени в среднем отрабатывает запрос на получение данных из хранилища? И среднее время обработки запроса при попадании в кэш? Просто мне довольно интересна тема оптимального выбора количества потоков. Может быть вы посоветуете почитать что-нибудь на эту тему?
2. Возможно для оптимизации можно реализовать свою стратегию ReadWriteLock с группировкой писателей в кэш (с некоторым таймаутом для писателей в кэш). Например при накоплении 5-10 писателей запрещать write и отрабатывать пакетную запись в кэш. При этом можно сэкономить на числе перестановок (переупорядочивания) в для ключей.
4. Согласен, вам не особо требуются дополнительные плюшки внешних хранилищ. К примеру, в очень крайнем случае вы кэш пересоберете заново. Распределенность кэширования скорее всего ухудшит производительность.
2. Возможно для оптимизации можно реализовать свою стратегию ReadWriteLock с группировкой писателей в кэш (с некоторым таймаутом для писателей в кэш). Например при накоплении 5-10 писателей запрещать write и отрабатывать пакетную запись в кэш. При этом можно сэкономить на числе перестановок (переупорядочивания) в для ключей.
4. Согласен, вам не особо требуются дополнительные плюшки внешних хранилищ. К примеру, в очень крайнем случае вы кэш пересоберете заново. Распределенность кэширования скорее всего ухудшит производительность.
Интересная статья.
Вопрос: какая стратегия все-таки используется для замещения картинок при наполнении сегмента — FIFO или LRU? И как механизм реализован? Вот эта фраза не очень понятна:
если сегмент заполнен, линейным поиском по массиву ссылок находятся и удаляются из индекса ключи, чьи данные будут перезаписаны очередным блоком;
наверное подразумевается все-таки FIFO с указателем на текущую позицию для вставки, который при достижении конца сегмента сбрасывается на начало?
И если это FIFO, то не логичнее ли реализовать LRU, который позволил бы держать в кэше наиболее востребованные картинки?
Вопрос: какая стратегия все-таки используется для замещения картинок при наполнении сегмента — FIFO или LRU? И как механизм реализован? Вот эта фраза не очень понятна:
если сегмент заполнен, линейным поиском по массиву ссылок находятся и удаляются из индекса ключи, чьи данные будут перезаписаны очередным блоком;
наверное подразумевается все-таки FIFO с указателем на текущую позицию для вставки, который при достижении конца сегмента сбрасывается на начало?
И если это FIFO, то не логичнее ли реализовать LRU, который позволил бы держать в кэше наиболее востребованные картинки?
Используется FIFO по принципу циклического буфера. Полное обновление сегмента происходит в течение 8-12 часов, т.е. время жизни объекта в кеше составляет не менее 8 часов. При таких показателях FIFO полностью устраивает, в то время как честная реализация LRU значительно усложнила бы логику распределения памяти.
Вот эта фраза не очень понятна:Старые данные (картинки) вытеснять из кеша не надо, они и так перезаписываются поверх. Но нужно удалять их id из индекса. Для этого пробегаем по порядку весь индекс, удаляя те ключи, чьи значения попадают в перезаписываемую область.
если сегмент заполнен, линейным поиском по массиву ссылок находятся и удаляются из индекса ключи, чьи данные будут перезаписаны очередным блоком
Может я чего-то не понимаю, но у меня вызывает сомнение обоснованность главного требования о том, что кеш должен быть in-process.
Вы пишете, что нужно обрабатывать 70 тыс. запросов в секунду, 3000 Мбит/с, но это ведь совсем не много. Только что поставил простой эксперимент на своем десктопе — померял пропускную способность http-сервера nxweb на файле размером 10Кбайт. Получилось 340 тыс. запросов в секунду и почти 3.5 Гбайт/с. Запас более чем десятикратный. Причем это далеко не два Xeon'а, а всего лишь i7-2600 (3.4 ГГц).
Другое дело, что Java в принципе медленнее C/C++. Банальный «hello, world» через Tomcat на моем же десктопе дает лишь 30-50 тыс. запросов в секунду. А если она еще к memcache начнет обращаться, то будет совсем нехорошо.
Вы пишете, что нужно обрабатывать 70 тыс. запросов в секунду, 3000 Мбит/с, но это ведь совсем не много. Только что поставил простой эксперимент на своем десктопе — померял пропускную способность http-сервера nxweb на файле размером 10Кбайт. Получилось 340 тыс. запросов в секунду и почти 3.5 Гбайт/с. Запас более чем десятикратный. Причем это далеко не два Xeon'а, а всего лишь i7-2600 (3.4 ГГц).
Другое дело, что Java в принципе медленнее C/C++. Банальный «hello, world» через Tomcat на моем же десктопе дает лишь 30-50 тыс. запросов в секунду. А если она еще к memcache начнет обращаться, то будет совсем нехорошо.
померял пропускную способность http-сервера nxweb на файле размером 10Кбайт
Что этот пример показывает? Где здесь out-of-process?
Ну, командная строка (точнее утилита измерения числа запросов в секунду) — она по отношению к http-серверу, как бы, не in-process, но работает достаточно быстро.
Показывает, что вызов чего-то быстрого out-of-process не должен стать узким местом при указанной частоте запросов и пропускной способности. Сокет на том же хосте работает достаточно быстро.
Показывает, что вызов чего-то быстрого out-of-process не должен стать узким местом при указанной частоте запросов и пропускной способности. Сокет на том же хосте работает достаточно быстро.
Утилита в данном случае — генератор пользовательских запросов. Чтобы тест был релевантным, нужна еще одна прослойка. Согласитесь, если точки A и B обмениваются данными со скоростью 340 тыс. rps, а B и C тоже со скоростью 340 rps, то вовсе не факт, что A и C через B тоже будут обмениваться с такой же скоростью. Про memcached уже писал.
Согласен, конечно. Смысл в другом. Мой тест показывает, что издержки на out-of-process могут составлять (на моем компьютере) лишь порядка 3 мкс. Понятно, что это может оказаться не совсем так на практике, надо проверять, но я веду к другому. Не стоит принимать за аксиому то, что кеш обязан быть in-process, чтобы справиться с указанной нагрузкой.
А в целом, спасибо за интересную статью. Давно пишу на Java, но мне бы и в голову не пришло решать на ней такие задачи. Вы расширили мой кругозор, за что премного благодарен.
А еще ведь можно запустить какой-нибудь Varnish в режиме malloc (чтобы кешировал в ОЗУ), а позади него поставить tomcat, который будет в случае непопадания в кеш подтягивать недостающий контент из хранилища. Не улучшит ли это результат? Причем значительно, так как большую часть запросов будет отрабать C-код, а не Java.
Мы, значит, делаем все возможное, чтобы по-максимуму использовать Java-решения, а вы нам тут C настоятельно советуете?.. Шучу, ничего не имею против. За Varnish спасибо. Впрочем, любое стандартное решение в чистом виде нам вряд ли подойдет, так как в любом случае нужна некоторая портало-специфичная логика, например, фильтрация по HTTP-заголовкам, проверка security-токенов в запросах и т.п. Malloc в чистом виде опять же не подойдет — т.к. при рестарте все теряется.
На самом деле, медленность Java по сравнению с C сильно преувеличивается. И, в частности, этой статьей я хотел показать, что на Java можно писать высокопроизводительные приложения, не уступающие программам на C.
На самом деле, медленность Java по сравнению с C сильно преувеличивается. И, в частности, этой статьей я хотел показать, что на Java можно писать высокопроизводительные приложения, не уступающие программам на C.
Так как я достаточно давно пишу не только на Java, но и на C, могу сказать по личному опыту, что разница в скорости обычно измеряется коэффициентом от 2 до 10. Наверное что-то можно отыграть с помощью более эффективных алгоритмов, но если переложить те же алгоритмы на C, все равно получится в несколько раз быстрее. Хотя, опять же, это обобщение, а занчит, со всемы вытекающими оговорками.
И меня обычно тоже не устраивают стандартные решения. Сам я Varnish так ни разу в деле и не попробовал. Но по идее он должен помочь.
И меня обычно тоже не устраивают стандартные решения. Сам я Varnish так ни разу в деле и не попробовал. Но по идее он должен помочь.
Sign up to leave a comment.
Использование разделяемой памяти в Java и off-heap кеширование