Многопоточная реализация алгоритма кеширования CARTИсточник: habrahabr tridemax
Некоторое время назад передо мной встала задача кеширования запросов в большую базу данных на диске в высоконагруженном многопоточном приложении (С++). Само приложение предназначалось для развертывания в облаке и диск очевидно стал бы самым узким местом. База при этом представляла из себя огромный граф, по которому одновременно ползало множество потоков. Таким образом кеш ко всему еще и должен был быть многопоточным. Идею использования внешних приложений, таких как memcached, я отбросил сразу же - это внесло бы в каждый переход по ребру графа неизбежный дополнительный лаг. Встал вопрос об имплементации внутри приложения. У меня появился выбор - положится на недостаточно оттестированную имплементацию, где починка любого бага стала бы отличным приключением, либо писать свою. Также, беглое изучение доступных публикаций алгоритмов кеширования привело меня к мысли о том, что, возможно, LRU не самая эффективная схема. Для высоконагруженных приложений даже несколько процентов дополнительной эффективности само по себе хлеб. Перебрав все найденные мной, я остановился на алгоритме CART. Данная схема сочетает в себе несколько полезных достоинств:
Давайте взглянем насколько схема эффективна на практике. Так как у меня не было достаточно времени, чтобы имплементировать и отлаживать другие схемы, я буду сравнивать эффективность все с той же имплементацией LRU из библиотеки TBB. Сначала тестируем эффективность обеих схем на случайно сгенерированной последовательности чисел (0…10000) для трех размеров кеша (100, 500 и 1000): Less is better. CART незначительно опережает LRU по эффективности, но, честно говоря, совершенно случайный доступ не самый хороший тест. Более того - в реальном приложении с таким типом доступа эффективность любого кеша будет низкой. Поэтому второй тест я сделал больше похожим на практическое применение. Здесь числа берутся из 6-ти корзин, каждая из которых имеет все большее количество элементов, что уменьшает вероятность выбрать какое-то конкретное число. Таким образом числа от 0 до 150 суммарно имеют такую же вероятность быть выбранными, как и числа от 5000 до 10000. Эта тактика похожа на паттерн выборки, например, из базы данных пользователей, где часто заходящие пользователи чаще дергают базу. Bins draw, cache size 100 В этом случае CART уже показывает значительно лучшие результаты, нежели LRU. Чего, собственно, мы и хотели достичь. Всем интересующимся предлагаю скачать пример и запустить собственноручно. Найти эту имплементацию можно здесь. Она тестировалась в реальном приложении под нагрузкой, что впрочем не гарантирует 100% отсутствия возможных багов. Используйте, так сказать, на свой страх и риск. Для интеграции в свои проекты вам необходимы только файлы в папке Include. От зависимости на HashMurmur3 можно избавиться, заменив его на любую подходящую вам хеш-функцию. В зависимостях у этой имплементации есть только TBB, но большинство тех, кто пишет многопоточные приложения на С++, и так его используют. В качестве бонуса прилагается неплохая имплементация генератора случайных чисел. Также оригинальная имплементация использует EASTL вместо STL, но я избавился от этой зависимости перед тем, как выложить публичную версию. Исходники примеров собираются под VS2010, но порт под Linux не должен вызвать проблем. Так как у меня сейчас нет под рукой Linux"а, я был бы благодарен сообществу, если бы кто-то потратил на это немного своего времени и сделал этот порт. P.S. Способов применения хорошей схемы кеширования можно придумать массу. У меня, например, данная имплементация также используется в доступе к Memory Mapped Files, где файл маппится не целиком, что может привести к огромному расходу виртуальной памяти на больших файлах, а ограниченным количеством небольших кусков по 16Мб. Кеш при этом управляет тем, какие куски выталкивать из памяти. Также можно парой сотен строк написать себе свой memcached, если вдруг возникнет такое желание. |