Как стать автором
Обновить

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

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

У вас 64GB смайликов? оО
Около 500. Пользователи ежедневно по 1 GB новых добавляют.
А можно пример? Я тогда вообще не понимаю что такое «пользовательские смайлики».
Для меня смайлики это
Всего смайликов c подарками ~1.5 TB (500 GB x 3 replication factor)
А наружу торчит HTTP REST API? Если да — вы могли бы на таком же железе настроить nginx + memcached и оценить его производительность?
Скорее всего мемкеш будет самым медленным решением. Медленее ФС кеша, который медленее in-process кеша.
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%.
То есть вы пришли к выводу, что горлышко бутылки — это запросы к [memcached|...], а не NFS?
Причем здесь NFS?
Узкие места бывают разного рода и устраняются разными способами: проблема с latency похода в хранилище решается размером локального кеша, нагрузка на CPU — быстрым алгоритмом кеширования и эффективным HTTP-сервером.
в статье нет ошибки?
3000 Mb/s это точно 3 тысячи мегабайт в секунду?
нет. Это 3000 мегабит. когда мегабайты обычно пишут MB/s ( B — большое)
оу, спасибо
А sun.misc.Unsafe#allocateMemory и sun.misc.Unsafe#freeMemory не подошли бы? Вроде как принимают на вход long и возвращают адрес.
Подошли бы. Только один важный момент — при рестарте приложения необходимо не потерять закешированные данные 55GB.
Зачем на каждый сегмент по локу?
Сколько потоков одноврменно пишут и читают?
Concurrency: одновременное использование кеша в сотне потоков;
10% из потоков пишут.
а почему не сделать 256 локов, распределить их на все сегменты?
На локи памяти не жалко, даже если сегментов 50000. Все же меньше вероятность, что попадут случайно несколько особо популярных картинок под один лок.
а почему не сделать 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, значит, все то же самое (чем забомбить с такой интенсивностью только вопрос).
всё что выше дискового кэша менее 60-150 картинок в секунду в идеальном случае помноженное на x 3 replication factor
Никогда супер нагруженными системами не занимался, всегда хватало стандартных средств. Но вещь интересная, плюс заметил у вас вакансию java developer — пошел готовить резюме. :)
Наверное глупый вопрос, но все таки, я правильно понимаю, что на реализациях JVM отличных от Oracle/Sun это не будет работать?
Да, метод sun.nio.ch.FileChannelImpl.map0 специфичен для Oracle JDK.
sun.misc.Unsafe тоже не является частью публичного API, но поддерживается и другими JVM.
Молодцы!
Вы рушите мою светлую веру в превосходство nginx над любым велосипедом! Язычники!!!
Хорошее решение — чувствуется серьезный подход к делу. А комментарии к топику завистливые — всякий норовит докопаться к чему-нибудь.
А с доступом к файлу в памяти какое решение в итоге использовали — первый способ (который легальный, но полноценный) или второй (с использованием «тайных знаний»)?
Второй, т.к. надо мапить огромный файл одним куском.
Попробуйте уйти от традиционных файловых операций и посмотреть в сторону ZFS, которая умеет кэшировать часто востребованные данные в памяти, используя собственные алгоритмы кэширования и управления памятью в зависимости от потребностей приложений. Ожидается, что тандем «ZFS + Java приложение» будет использовать оперативную память гораздо эффективнее, чем традиционные механизмы ручного управления разделяемой памятью.
Любая файловая система делает это (кеширование в зависимости от потребностей приложений). Не могли бы вы пояснить, почему вы ожидаете, что ZFS тут будет лучше?
Потому что ZFS очень агрессивно кэширует запрашиваемые данные, в отличие от традиционных файловых систем. При этом память под кэш выделяется динамически, при этом автоматически занимается почти вся доступная память. Запускаемые приложения, естественно, тоже требуют своё, и ZFS в этом случае оперативно освобождает кэш — память, когда ZFS кэш разогрет, всегда занята на 100% под кэш и приложения, конечно, если не выставлены ограничения (через sysctl-переменные управления).

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

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

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

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

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

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

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

В таком случае единственное что отличает от 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. Потоки ждут в epoll_wait(). Некоторые висят на запросе в хранилище. С трафиком справилось бы и меньшее число потоков, но так мы уменьшаем максимальное время отклика. И 100 потоков — это не 10 тысяч, накладные расходы на переключение незаметны.

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. Согласен, вам не особо требуются дополнительные плюшки внешних хранилищ. К примеру, в очень крайнем случае вы кэш пересоберете заново. Распределенность кэширования скорее всего ухудшит производительность.
Запрос к хранилищу — около 1 мс. Обращение в локальный кеш — 2 мкс. Число потоков выбирали экспериментально: от 64 до 128 разницы почти не было, при >200 начала снижаться максимальная пропускная способность, при <32 росло максимальное время отклика.
Интересная статья.
Вопрос: какая стратегия все-таки используется для замещения картинок при наполнении сегмента — 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 начнет обращаться, то будет совсем нехорошо.
померял пропускную способность http-сервера nxweb на файле размером 10Кбайт

Что этот пример показывает? Где здесь out-of-process?
Ну, командная строка (точнее утилита измерения числа запросов в секунду) — она по отношению к http-серверу, как бы, не in-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, могу сказать по личному опыту, что разница в скорости обычно измеряется коэффициентом от 2 до 10. Наверное что-то можно отыграть с помощью более эффективных алгоритмов, но если переложить те же алгоритмы на C, все равно получится в несколько раз быстрее. Хотя, опять же, это обобщение, а занчит, со всемы вытекающими оговорками.

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